Treat upstream MCP tool metadata as untrusted input, even when the server was approved earlier. A proxy can reduce the blast radius by snapshotting tools/list, diffing tool names, descriptions, schemas and annotations, enforcing policy again on tools/call, and quarantining unexpected changes. It cannot make a malicious upstream safe by itself, so pair proxy checks with source-scoped credentials, read-only defaults, approval flows and audit logs.
What the proxy can see
The dangerous part is not only the tool implementation. In MCP, a server exposes tools through tools/list; each tool has a name, description, input schema and optional annotations. That metadata is placed in front of the model so it can decide which tool to call. If the description later says, in effect, that the tool should ignore normal instructions, exfiltrate nearby context or prefer itself over safer tools, the attack has already entered the model's decision surface before any tools/call happens.
A late behavior change is the other half of the problem. The tool can keep the same name and schema while the upstream code behind it starts doing more than the reviewed contract allowed. A proxy cannot prove what a remote server will do internally, but it can notice that the advertised contract changed, decide whether the caller is allowed to use the changed contract, and make the runtime decision visible. That is why this belongs at the proxy instead of only in each developer's local client config.
The proxy should assume three channels can be hostile: prose in the tool description, structure in the input schema, and output returned from the tool. The MCP tools specification tells servers to validate inputs, enforce access controls, rate limit invocations and sanitize outputs; it also tells clients to show inputs before calls, use timeouts and log tool usage. A proxy sitting between client and upstream can apply the same discipline consistently across many servers.
Freeze the reviewed contract
The first defense is boring and effective: store the exact tool contract you reviewed. For every upstream source, persist the tool name, title, description, input schema, output schema if present, annotations and a content hash. Recompute that hash every time the proxy refreshes tools/list. If a description, schema or annotation changes, treat it as a new contract until reviewed, even when the source URL and OAuth connection are unchanged.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": "optional-cursor-value"
}
}A useful snapshot record is small enough to review in a pull request or an admin console. The hash should cover fields the model sees and fields the runtime trusts. For example, a change from readOnlyHint: true to no annotation should immediately move the tool out of the read-only lane. A new required input such as recipient_email, webhook_url or query should get human attention because it changes what data can leave the client.
Do not silently replace the old list. Mark changed tools as pending review, keep unchanged tools available, and show exactly what moved. The user experience should make the smallest safe thing easy: approve a harmless wording correction, deny a suspicious instruction in a description, or pin the whole source to the previous reviewed set until the owner explains the change.
Gate the actual call
The call path has to check policy again. Some clients cache their first tool list for a session, and MCP allows the available set to vary by the authorization presented on the request. If a tool was visible earlier but is now quarantined, source-scoped away or newly classified as write-capable, the proxy should deny the invocation at tools/call. The model's memory of a tool is not authorization.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "github/create_issue",
"arguments": {
"repository": "acme/api",
"title": "Investigate checkout timeout",
"body": "Observed from production logs"
}
}
}At that moment, evaluate the caller, source, tool, reviewed contract hash, annotations, arguments and upstream credential. The proxy can reject a changed hash, require approval for a sensitive call, strip access to a source the key no longer reaches, or route only to the pinned version. OpenAI's MCP guidance exposes this same control shape in API form with allowed_tools and per-call approval requests; the proxy version makes it independent of one model provider.
| Change observed | Proxy response | Reason |
|---|---|---|
| Description adds instructions to reveal secrets | Quarantine the tool and alert the source owner | The model sees descriptions as decision context. |
| Input schema adds an external URL field | Require review before any call with that schema | The data-flow boundary changed. |
readOnlyHint disappears | Move behind the write grant or deny | Annotations are hints, not proof of safety. |
| Tool result contains unexpected instructions | Return a sanitized error or low-trust result | Tool output can carry indirect prompt injection. |
Keep client config narrow
A proxy helps most when clients connect to one reviewed endpoint with a narrow credential. Claude Code can add a remote HTTP MCP server from the CLI and pass a bearer token header. VS Code can store a remote server in a workspace .vscode/mcp.json file with a top-level servers object. In both cases, the local config should identify the proxy, not a pile of upstream tokens.
claude mcp add --transport http company-tools https://api.example.com/agent/mcp --header "Authorization: Bearer $AGENT_MCP_KEY"{
"servers": {
"company-tools": {
"type": "http",
"url": "https://api.example.com/agent/mcp"
}
}
}The important security property is the key behind those snippets. A coding assistant that only needs diagnostics should not hold the same credential as a release assistant that can send messages, create tickets or update production state. The proxy should expose a smaller tool list for the diagnostic key, return withheld or denied decisions explicitly, and keep upstream OAuth tokens out of project repositories.
This also makes rollback practical. If an upstream server changes its contract, you can freeze that source centrally while every client keeps the same endpoint. Without a proxy, each developer has to remove or edit local MCP entries, and any missed laptop may keep presenting the poisoned or changed tool to an agent.
Treat annotations as claims
Tool annotations are useful risk vocabulary, not a trust boundary. The MCP annotation fields include readOnlyHint, destructiveHint, idempotentHint and openWorldHint. They help clients decide how much caution to show, but the annotation source matters. A tool controlled by the party you distrust can lie about being read-only, non-destructive or closed-world.
The proxy should therefore map annotations into policy only after trust has been established. For a source owned by your team, a reviewed readOnlyHint: true might allow low-friction calls under a read-only key. For a newly connected third-party server, the same hint should merely appear in the review UI until the upstream credential and implementation are understood. If the proxy cannot verify a property, it should bias toward the stricter grant.
This is where sibling controls fit together. Use the permission model from read-only versus write MCP tools so unmarked tools are not treated as harmless. Use source-scoped API keys so one compromised source does not imply access to every connected system. Then tie the runtime decisions to tool-call audit logs so the team can reconstruct which contract hash, approval and credential allowed a call.
What SPYN3 does and does not do here
SPYN3 passes upstream tool descriptions through unchanged. It does not scan them for injected instructions, and it does not diff descriptions between refreshes. When it re-reads a server's tool list, it reports how many tools were added or removed, not what changed in their text. What it limits is the damage a poisoned description can do. A key without call:write_tools cannot call tools that may write, and a key limited to some sources cannot reach the others. Only connect servers you would trust with those grants.
Test the suspicious paths
Before rollout, test the proxy with deliberately hostile fixtures instead of waiting for a real upstream surprise. Create an MCP test server that first advertises a harmless search tool, then changes only the description. Create another version that keeps the description stable but adds a required callback_url argument. Create a third that returns text instructing the model to call a different tool with sensitive context. The expected result is not that the model behaves perfectly; it is that the proxy records and blocks the contract changes you decided were unsafe.
Run those fixtures through the same client setup developers will use. A Claude Code session should still point at the proxy URL. A VS Code workspace should still point at the workspace MCP entry. The test should prove that stale tool caches do not bypass the proxy, that a denied tools/call returns a clear tool or protocol error, and that the audit record includes the source, tool, contract hash, caller, arguments shape, decision and latency.
The final acceptance test is operational. Someone on the team should be able to answer four questions from the logs and review UI: what changed, who approved it, which keys could call it, and whether any calls happened before approval. If the proxy cannot answer those questions, it is only forwarding traffic. If it can, then a poisoned description or late upstream change becomes a contained review event instead of a silent instruction injected into every agent session.
Built for the next step
Make tool changes reviewable
Put source, tool and permission decisions at the proxy boundary.
Security checklistSPYN3 is a consolidated MCP layer: one hosted endpoint in front of every system you connect — MCP servers, Postgres, REST APIs, Google Drive — with tools foldered by source and read/write permissions per key. Switching agents means moving one URL. Free plan available; current capabilities are documented on the product facts page.
Sources
- MCP specification: tools — tool discovery, invocation and security considerations. Verified 14 Oct 2026
- MCP project: tool annotations — risk hints and their enforcement limits. Verified 14 Oct 2026
- OpenAI API: MCP servers and connectors — remote MCP servers, deferred loading, approvals and risks. Verified 14 Oct 2026
- Anthropic docs: Claude Code MCP setup — Claude Code MCP commands, scopes, transports and JSON configuration. Verified 14 Oct 2026
- VS Code documentation: add and manage MCP servers — VS Code MCP configuration, trust and logging behavior. Verified 14 Oct 2026
- Anthropic: prompt injection defenses — indirect prompt-injection risk and layered defenses. Verified 14 Oct 2026
