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 (
surface-scanner --mcp): the scanner binary itself is a stdio MCP server. Zero dependencies — no Node, no npm. Content never leaves your machine. It needs your API key, and exposes the scanning tools (scan_payload,scan_file,scan_bundle) plus the account tools (history, usage, profiles, plans). See Local scanning below. - Hosted MCP endpoint: a remote URL you point a
url-based MCP client at. Best for inline payload scanning, profiles, account, and usage. No install. - Local MCP server (
npx github:tendrl-inc-labs/surface-mcp): a richer stdio server that runs on your machine. Adds API key management, the Surface skills, and a.mcpbbundle, and can proxy scans to the hosted API. See Local server below.
Local scanning (scanner binary)#
For local scanning, point your MCP client straight at the scanner binary — no Node/npm layer required:
{
"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:
{
"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.
{
"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.contextmust 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, callsscan_payloadwith context it controls, and only forwards the call to the real tool onAllow.
Hosted endpoint#
POST https://app.tendrl.com/surface/mcp
Requires an API key: Authorization: Bearer <API_KEY>. The endpoint is POST-only JSON-RPC.
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:
{
"mcpServers": {
"surface": {
"url": "https://app.tendrl.com/surface/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}
VS Code#
In .vscode/mcp.json:
{
"servers": {
"surface": {
"type": "http",
"url": "https://app.tendrl.com/surface/mcp",
"headers": { "Authorization": "Bearer <your-token>" }
}
}
}
Claude Desktop#
In claude_desktop_config.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:
- The hosted scanning, account and profile tools (except
search_docs), plus: scan_file: scan a file by absolute pathscan_bundle: scan several payloads (e.g.main.py,boot.py,config.json) in one call and get an aggregatedeploy_safego/no-go, for gating a device deployment before you flashcreate_api_key/delete_api_key: create and revoke Surface API keys.create_api_keytakes alabeland an optionalprofile_idandrole_name(defaultscanner), and returns the new key's token once. Both tools needSURFACE_KEYto be an admin-role key.
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:
{
"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:
{
"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.
Tendrl