MCP Server

Surface exposes an MCP-compatible JSON-RPC endpoint so AI assistants can scan files and payloads for malware without writing HTTP client code.

There are three ways to connect:

Local scanning (scanner binary)#

For local scanning, point your MCP client straight at the scanner binary — no Node/npm layer required:

json

{
  "mcpServers": {
    "surface": {
      "command": "surface-scanner",
      "args": ["--mcp"],
      "env": { "SURFACE_API_KEY": "<token-from-the-dashboard>" }
    }
  }
}

SURFACE_API_KEY is the same API token the SDKs read from SURFACE_KEY; the scanner binary just reads it under this name.

This runs the binary as a native stdio MCP server exposing three tools, all scanned locally. The scanned content never leaves your machine, and by default nothing else about a scan does either. Started with --report-results, the server reports a record of each scan (name, hash, size, verdict, and the rules and finding categories that fired), never the content or excerpts of it, and the scan tools' descriptions say so, so the agent knows.

Tool Description
scan_payload Scan raw text content: messages, JSON, code snippets, tool calls (10 MB cap, same as --stdin)
scan_file Scan a file by absolute path
scan_bundle Scan several payloads (e.g. main.py, boot.py, config.json) in one call and get an aggregate deploy_safe go/no-go, for gating a device deployment before you flash

The API key also unlocks the account tools and keeps threat feeds updating automatically. Add --report-results to the args if you additionally want each local scan recorded in your dashboard history. Reported scans are not metered, so they do not count against your monthly quota:

json

{
  "mcpServers": {
    "surface": {
      "command": "surface-scanner",
      "args": ["--mcp"],
      "env": { "SURFACE_API_KEY": "${SURFACE_API_KEY}" }
    }
  }
}

With the key set, tools/list additionally includes these tools, which call the hosted Surface API (only these calls go over the network — scanning stays local either way):

Tool Description
get_scan Retrieve results for a deferred scan by scan ID
get_scan_history Browse past scans (paginated)
get_scan_detail Full result for a historical scan
get_usage Check scan quota (used vs monthly limit)
get_account Account details
get_plans List available billing plans and scan limits
list_profiles List available scan profiles and their configurations
create_profile Create a new scan profile
update_profile Update an existing scan profile
delete_profile Delete a scan profile
list_api_keys List Surface API keys (metadata only — secrets are never returned)

If the API is unreachable, account tool calls return a clean error and the scanning tools keep working — local scans never depend on the network.

On a first-ever launch the binary downloads threat feeds in the background; the MCP handshake completes immediately and the first scan tool call waits until feeds are ready (progress is logged to stderr). Account tools don't wait — they work as soon as the server is up.

The scanner's daemon mode also serves MCP over streamable HTTP at http://localhost:8080/mcp, for url-based clients that should share one long-running local scanner.

If you also want API key create/delete tools, the Surface skills, or the .mcpb bundle, use the npx github:tendrl-inc-labs/surface-mcp server instead. Doc search (search_docs) is a hosted-endpoint tool only; the npx server publishes the docs as MCP resources under surface://docs/... rather than as a search tool.

Screening agent tool calls#

Connecting this server does not screen your agent's actions on its own. It adds scan_payload / scan_bundle as tools the model can call; nothing here intercepts your other tools. To actually guard, your host code wraps tool execution: when the model proposes a tool call, call scan_payload on it first and run it only if the verdict is Allow. Don't ask the model to scan itself — a hijacked agent won't. See How you wire it. On Pydantic AI, add surface_hooks(AsyncToolGuard(...)) to the agent's capabilities rather than giving the model scan_payload as a tool — see Agent Action Guardrails.

scan_payload and scan_bundle take a context argument so the scanner can judge a tool call the agent is about to make (see Action screening). The host application — not the model — supplies context from trusted state, then decides what to do with the verdict; the model calls the tool with the proposed action as the payload. Context fields are optional: pass only what you have, and those values are validated. Omit context and what is certainly malicious still Blocks, while irreversible actions are held for Review. context.strictness (relaxed, balanced or strict, default balanced) sets how readily a judgment call becomes a verdict. The hosted endpoint, the npx server and surface-scanner --mcp all accept context on scan_payload.

json

{
  "name": "scan_payload",
  "arguments": {
    "payload": "{\"tool\":\"http_request\",\"args\":{\"method\":\"POST\",\"url\":\"https://webhook.partner.io/sync\",\"body\":{}}}",
    "context": {
      "principal_domains": ["acme.io"],
      "allowed_egress": ["api.stripe.com", "hooks.slack.com"],
      "user_request": "pay this month's open invoices"
    }
  }
}

The result's safetyScore.recommendedAction is Allow, Review, or Block; gate execution of the tool call on it. context is a fixed set of fields (reference) — unknown keys are ignored. For scan_bundle, each item carries its own context. Both the MCP tools and ToolGuard accept omitting context.

Why the host passes context, not the model. context must come from your trusted application state (your real domains, declared egress hosts, and the user's actual request), never from the payload being scanned — otherwise a prompt injection could describe its own destination as "known" and clear itself. In practice the MCP host wraps the tool call: it intercepts the model's proposed action, calls scan_payload with context it controls, and only forwards the call to the real tool on Allow.

Hosted endpoint#

code

POST https://app.tendrl.com/surface/mcp

Requires an API key: Authorization: Bearer <API_KEY>. The endpoint is POST-only JSON-RPC.

Note

The hosted endpoint is being validated against url-based MCP clients. If a url client fails to connect, use the local npx github:tendrl-inc-labs/surface-mcp server instead.

Hosted tools#

Tool Description
scan_payload Scan raw text content: messages, JSON, code snippets, tool calls. Detects prompt injection, SQL/XSS injection, credential leaks, malicious code
get_scan Retrieve results for a deferred scan by scan ID
get_scan_history Browse past scans (paginated)
get_scan_detail Full result for a historical scan
list_profiles List available scan profiles and their configurations
create_profile Create a new scan profile
update_profile Update an existing scan profile
delete_profile Delete a scan profile
get_account Account details
get_usage Check scan quota (used vs monthly limit)
list_api_keys List Surface API keys
get_plans List available billing plans and scan limits
search_docs Search Surface documentation and return relevant snippets

The hosted endpoint does not scan files. To scan files via MCP, use the scanner binary's --mcp mode or the local npx github:tendrl-inc-labs/surface-mcp server — both expose a scan_file tool.

Client setup (hosted endpoint)#

These configs point a url-based client at the hosted endpoint.

Cursor#

In .cursor/mcp.json:

json

{
  "mcpServers": {
    "surface": {
      "url": "https://app.tendrl.com/surface/mcp",
      "headers": {
        "Authorization": "Bearer <your-token>"
      }
    }
  }
}

VS Code#

In .vscode/mcp.json:

json

{
  "servers": {
    "surface": {
      "type": "http",
      "url": "https://app.tendrl.com/surface/mcp",
      "headers": { "Authorization": "Bearer <your-token>" }
    }
  }
}

Claude Desktop#

In claude_desktop_config.json:

json

{
  "mcpServers": {
    "surface": {
      "url": "https://app.tendrl.com/surface/mcp",
      "headers": {
        "Authorization": "Bearer <your-token>"
      }
    }
  }
}

Local server (npx)#

Surface's local stdio MCP server installs straight from its GitHub repository — there is no npm package to publish or trust. If local scanning plus the account tools covers you, the scanner binary's --mcp mode does that with no Node layer at all — reach for this package when you also want API key create/delete, the Surface skills, or a .mcpb bundle, or when a url-based client can't reach the hosted endpoint. It exposes a different toolset from the hosted endpoint — broader in most respects, but without search_docs:

It also serves the Surface skills as MCP resources (see AI Skills). The hosted endpoint serves documentation as resources but not skills.

Install it by repository, not by name. An unrelated third-party package
called surface-mcp exists on npm; installing that name runs someone else's
code. The github:tendrl-inc-labs/surface-mcp specifier below is
unambiguous, and npm builds it from source on install.

Add it to your MCP client config:

json

{
  "mcpServers": {
    "surface": {
      "command": "npx",
      "args": ["-y", "github:tendrl-inc-labs/surface-mcp"],
      "env": {
        "SURFACE_KEY": "${SURFACE_KEY}"
      }
    }
  }
}

By default, scan_file, scan_payload, and scan_bundle upload to the Surface API. To scan locally (content never leaves your machine), set SURFACE_SCANNER_PATH to a local scanner binary. All three then run every scan through the local binary instead:

json

{
  "mcpServers": {
    "surface": {
      "command": "npx",
      "args": ["-y", "github:tendrl-inc-labs/surface-mcp"],
      "env": {
        "SURFACE_KEY": "${SURFACE_KEY}",
        "SURFACE_SCANNER_PATH": "/path/to/scanner"
      }
    }
  }
}

See Scanner Binary for details on local vs API mode.