TachyonicTachyonic

We Audited Both MCP SDKs — Here Are the Three Vulnerability Classes We Found

Feb 24, 2026 by Emmanuel Ndangurura

MCP (Model Context Protocol) is the protocol every AI company is adopting. Anthropic built it. OpenAI endorsed it. The reference repository has 77,000+ stars. Every major agent framework — LangChain, CrewAI, AutoGen — is building MCP integrations.

No public source-code-level security audit of the SDKs has been published.

We did. We audited both official MCP SDKs (TypeScript and Python), the 7 reference servers, and 5 high-profile community servers. We found three classes of boundary-crossing vulnerabilities that affect every multi-server MCP deployment.

All three are confirmed with live proof-of-concept exploits and validated against production LLMs.


What We Tested

Official SDKs:

  • modelcontextprotocol/typescript-sdk (v0.8.x) — the TypeScript implementation used by most MCP clients
  • modelcontextprotocol/python-sdk (v1.26.0) — the Python implementation used by most MCP servers

Reference servers (modelcontextprotocol/servers):

  • Filesystem (15 tools), Git (12 tools), Fetch, Memory (9 tools), Time, Everything, Sequential Thinking

Community servers:

  • GitHub MCP Server (25k+ stars, 77 tools)
  • Playwright MCP (26k+ stars, 12+ browser automation tools)
  • Supabase MCP (2k+ stars, SQL execution + edge functions)
  • Slack MCP Server (1k+ stars, workspace access)
  • Kubernetes MCP Server (1k+ stars, 23 kubectl/helm tools)

Real-model validation:

  • gpt-5.2 and gpt-5-nano with MCP tool definitions
  • 7 scan runs, 500+ attacks, triaged to genuine findings only

How We Tested

Our methodology combined three approaches:

1. Source code audit. We read the SDK source code — auth middleware, token verification, tool registration, session management. We traced data flows from token issuance through tool dispatch and identified where boundary assumptions break.

2. Live proof-of-concept exploits. For each vulnerability class, we built multi-server test infrastructure using the SDK's actual auth components (BearerAuthBackend, RequireAuthMiddleware, TokenVerifier) and demonstrated the attack end-to-end.

3. Real-model attack simulation. We ran our 122-attack scanner against MCP servers backed by production LLMs, generating tool calls that would be silently routed to attacker-controlled servers under the conditions we discovered.


The MCP Trust Model

Before the findings, you need to understand how MCP is supposed to work.

MCP separates concerns into three layers:

  1. MCP Client (your AI agent) — connects to one or more MCP servers, aggregates their tools into a single registry, and presents them to the LLM.
  2. MCP Servers — expose tools (functions the LLM can call), resources (data the LLM can read), and prompts (templates). Each server is a separate process with its own capabilities.
  3. OAuth Authorization Server — issues bearer tokens that MCP servers validate before allowing tool access.

The trust assumption: each server is an isolated security domain. A token for Server A should only work on Server A. Tools from Server A should be distinguishable from tools from Server B. If Server A's permissions are revoked, the client should stop accessing Server A immediately.

Every one of these assumptions is violated in the current SDK implementations.

MCP Trust Boundary Architecture — showing where H1, H2, and H3 break the protocol's security model

Finding 1: Tool Capability Shadowing (H1)

Severity: HIGH Affects: Both SDKs, all multi-server deployments Root cause: Tool names are flat strings with no namespace or origin tracking

The Problem

When an MCP client connects to multiple servers, it aggregates all tools into a single registry. Tool names are stored as plain strings — read_data, write_file, execute_sql — with no information about which server registered them.

If two servers register a tool with the same name, one silently wins.

In the Python SDK, tool_manager.py lines 67-70 check if a tool name already exists and silently return the old tool — the new registration is ignored without error. First registered wins. The warn_on_duplicate_tools flag only logs a warning. It does not prevent shadowing.

In the TypeScript SDK, _registeredTools is a flat { [name: string]: RegisteredTool } dictionary with no server-level prefix. Duplicate registration overwrites the existing entry — last registered wins.

The Attack

An attacker registers a malicious MCP server that exposes a tool with the same name as a legitimate server's tool. When the client connects to both:

  1. Both servers register read_data
  2. The SDK silently resolves the collision — Python SDK keeps the first registration, TypeScript SDK overwrites with the last
  3. Every read_data call from the LLM routes to the attacker's server
  4. The attacker receives all arguments (keys, paths, queries) and can return manipulated data

What We Proved

We deployed two MCP servers — server_a (legitimate, returns safe data) and server_b (shadow, exfiltrates arguments) — both exposing read_data. The Python SDK silently routed all calls to the shadow server.

We then ran our scanner against production LLMs with the read_data tool definition. The models made these tool calls unprompted when given attack payloads:

ModelFindingDetail
gpt-5-nanoPath traversalread_data("../../../etc/passwd"), read_data("/etc/passwd"), read_data("../")
gpt-5-nanoCredential exfiltrationread_data("credential"), read_data("api_key"), read_data("password"), read_data("ENV_API_KEY")
gpt-5-nanoMCP tool poisoningModel accepted MCP_TOOL_REGISTERED canary
gpt-5-nanoGoal hijackingModel output OBJECTIVE_MODIFIED
gpt-5-nanoSQL injection via toolModel output SQL_INJECTION_VIA_TOOL
gpt-5.2Path traversalread_data("/proc/self/environ"), read_data("/var/run/secrets/kubernetes.io/serviceaccount/token")

Every one of these calls, in a multi-server deployment with tool shadowing, would route to the attacker's server. The model has no way to distinguish which server handles the call.

gpt-5-nano: 10 genuine tool abuse findings out of 10 flagged. 0% false positive rate. Smaller models are significantly more exploitable for tool abuse because they are more easily manipulated into making tool calls with attacker-controlled arguments.

Remediation

  1. Namespace tool names. Prefix tools with their server identity: server-a/read_data vs server-b/read_data. The Python SDK's ClientSessionGroup supports an optional component_name_hook for this — but it's optional, off by default.
  2. Reject duplicate registrations. The SDK should raise an error, not log a warning, when two servers register the same tool name.
  3. Track tool origin. Include server identity in the tool execution context so the LLM and the application can verify which server will handle a call.

Finding 2: Token Audience Confusion (H2)

Severity: CRITICAL Affects: Both SDKs, all deployments with shared OAuth issuers Root cause: TokenVerifier.verify_token() has no audience parameter

The Problem

OAuth tokens include an aud (audience) claim that specifies which server the token is intended for. A token minted for Server A (audience=server-a) should be rejected by Server B (audience=server-b).

Neither MCP SDK validates the audience claim.

In the Python SDK, the TokenVerifier protocol defines:

class TokenVerifier(Protocol):
    async def verify_token(self, token: str) -> AccessToken | None:
        """Verify a bearer token and return access info if valid."""

One parameter: the token string. No expected audience. No server URL. No way to reject a token meant for a different server.

The AccessToken model has a resource field (RFC 8707), but it's optional and never checked by the middleware:

class AccessToken(BaseModel):
    token: str
    client_id: str
    scopes: list[str]
    expires_at: int | None = None
    resource: str | None = None  # Present but unchecked

The BearerAuthBackend extracts the token, calls verify_token(), checks expiration and scopes — but never compares AccessToken.resource to the server's own URL.

In the TypeScript SDK, auth.ts line 41 sets the audience when creating JWT claims. But nowhere in the SDK is the received token's audience validated against the MCP server being accessed.

The Attack

In multi-server MCP deployments sharing an OAuth issuer (common in enterprise environments):

  1. Client authenticates with Server A (read-only code search) and receives a token with audience=server-a
  2. Attacker (or compromised client) presents that token to Server B (database admin)
  3. Server B's auth middleware calls verify_token(), gets back a valid AccessToken
  4. Server B accepts the request — the read-only token now grants admin access

What We Proved

We built a 3-server test environment using the SDK's real auth components:

  • Auth Server (port 9000): Issues tokens with RFC 8707 audience claims, provides RFC 7662 introspection
  • Server A (port 8001): Read-only tools, BearerAuthBackend + RequireAuthMiddleware
  • Server B (port 8002): Admin tools (admin_delete, admin_execute_sql), same auth stack

We issued a token with audience=http://localhost:8001 (Server A) and used it to call Server B:

TestExpectedActual
Token for A → Server A read_data200200
Token for A → Server B admin_delete401200
Token for A → Server B admin_execute_sql401200
No token → Server B401401
Invalid token → Server B401401

Server B's response included token_audience: http://localhost:8001. It knew the token was for a different server. It accepted the request anyway.

The auth middleware correctly rejects missing tokens (401) and invalid tokens (401). It has no mechanism to reject audience mismatches.

This is the strongest finding. It's not an implementation bug in one server — it's a gap in the protocol's SDK interface. Every MCP server built on these SDKs inherits this vulnerability.

Remediation

  1. Add audience parameter to TokenVerifier. The interface should be verify_token(token: str, expected_audience: str) so implementations can reject mismatched tokens.
  2. Default validate_resource to True. The SDK's IntrospectionTokenVerifier has a validate_resource flag — but it defaults to False. This should be True by default, with an explicit opt-out for deployments that don't need audience binding.
  3. Check audience in BearerAuthBackend. After verify_token() returns, the middleware should compare AccessToken.resource to the server's configured URL and reject mismatches.

Finding 3: Stale Authorization (H3)

Severity: HIGH Affects: Both SDKs, all deployments with token caching Root cause: No server-push invalidation mechanism in MCP protocol

The Problem

Token verification is expensive. Introspecting every request adds latency and load to the auth server. So servers cache verification results — this is standard practice and explicitly supported by the SDK.

The problem: when a token is revoked or a scope is downgraded, there is no mechanism to notify MCP servers. The cached verification result persists until it expires, creating a stale authorization window.

In the TypeScript SDK:

  • invalidateCredentials() is optional in the provider interface
  • Tokens cached in currentTokens (line 482) with no automatic refresh on permission changes
  • No OAuthTokenRevocationRequest is called when sessions end
  • Tokens persist indefinitely in provider storage until manually cleared

In the Python SDK:

  • Sessions persist until manually closed — no automatic re-negotiation
  • Protected resource metadata cached with no TTL
  • No server-initiated session revocation mechanism
  • No hook for "permission changed" events

The Attack

  1. User authenticates and receives a token with [read, write, admin] scopes
  2. MCP server verifies the token via introspection and caches the result (5-minute TTL)
  3. Admin revokes the user's write and admin scopes (fired employee, policy change, etc.)
  4. Auth server introspection now returns only [read] for this token
  5. MCP server serves the next request from cache — still sees [read, write, admin]
  6. User continues executing admin operations for the duration of the cache TTL

What We Proved

We built an MCP server with a caching TokenVerifier (configurable TTL) and measured the stale authorization window across two scenarios.

Scenario 1 — Token Revocation:

Time After RevokeStatusNote
0s200STALE — cached result
+1s200STALE
+2s200STALE
+3s200STALE
+4s200STALE
+5s (cache TTL)401Cache expired, re-introspected, revocation detected

The revoked token continued working for the full cache TTL.

Scenario 2 — Scope Downgrade:

PhaseCached ScopesServer-Side Scopes
Before downgrade[read, write, admin][read, write, admin]
After downgrade (cached)[read, write, admin][read]
After cache expiry[read][read]

The client retained write and admin scopes in the cached verification result even after they were removed server-side.

Scaling the stale window:

  • Cache TTL 5 seconds (our test): 5-second stale window
  • Cache TTL 5 minutes (common): 5-minute stale window
  • Cache TTL 1 hour (aggressive caching): 1-hour stale window
  • JWT-only validation (no introspection): no revocation possible until token itself expires (hours to days) — a different and worse problem, since there is no cache to expire

Remediation

  1. Define a session invalidation message in the MCP spec. Servers should be able to push "your permissions changed" events to clients.
  2. Support token revocation push. Webhook or SSE channel for the auth server to notify MCP servers of revocations.
  3. Provide cache TTL guidance. The SDK should recommend short TTLs for sensitive operations and support per-operation cache bypass.
  4. Force re-introspection for critical operations. Destructive operations (delete, execute, deploy) should bypass the cache and verify the token in real-time.

The Combined Attack Chain

These three vulnerabilities compose into a full attack chain against multi-server MCP deployments:

1. Reconnaissance. Attacker enumerates tool names across connected MCP servers. No namespace isolation prevents discovery.

2. Tool Shadowing (H1). Attacker registers a shadow server with an identical tool name. The SDK silently routes tool calls to the attacker's implementation. Path traversal and credential exfiltration happen through normal tool calls.

3. Privilege Escalation (H2). Attacker obtains or steals a token from a low-privilege server. Presents it to a high-privilege server. Accepted without audience check. Read-only token executes admin operations.

4. Persistence (H3). Even after detection and revocation, cached tokens grant access for the duration of the cache TTL. The attacker maintains access for minutes to hours after the defender responds.


What We Did Not Find

Credit where it's due — several things in the MCP SDKs are well-implemented:

  • Filesystem server path validation handles null bytes, symlink resolution, and directory allowlisting correctly.
  • Git server injection prevention rejects ref injection and uses argument separation properly.
  • OAuth 2.1 implementation in the Python SDK is comprehensive — PKCE, S256 challenge, well-structured token models.
  • Tool input/output validation exists and works. The SDKs validate tool arguments against JSON schemas before execution.

The vulnerabilities we found are in the boundaries between servers, not in individual server implementations.


Model Size Matters

One finding from our scanner runs deserves its own section.

We ran the same attacks against gpt-5.2 (large, capable model) and gpt-5-nano (small, cheap model). The difference in tool abuse exploitability was dramatic:

ModelTool Abuse FindingsGenuine Rate
gpt-5-nano10 out of 10 flagged100%
gpt-5.29 out of ~20 flagged~45%

Smaller models are significantly more exploitable for tool abuse. They are more easily manipulated into making tool calls with attacker-controlled arguments.

This matters because many production MCP deployments use smaller models for cost efficiency. The model most likely to be deployed in a cost-sensitive MCP environment is also the model most vulnerable to the attacks that MCP's architecture fails to prevent.


Existing MCP Security Research

We are not the first to examine MCP security. But existing research focuses on different areas:

SourceFocusWhat We Add
Invariant LabsTool poisoning attacksWe cover auth boundary failures (H2, H3) they don't
Elastic Security LabsAttack vectors and defensesWe provide SDK-level source code analysis, not pattern descriptions
SemgrepSecurity engineer's guideGuidance, not vulnerability research
HiddenLayerParameter abuse for prompt extractionNarrow focus on one vector
DatadogMonitoring MCP serversDetection, not offensive testing

Our differentiation: SDK-level source code audit of both implementations, live proof-of-concept exploits for all three vulnerability classes, and real-model validation with production LLMs.

To our knowledge, this is the first published research on MCP token audience confusion (H2) and the first quantified measurement of the stale authorization window (H3).


What Should Change

For the MCP specification:

  • Define tool namespacing requirements for multi-server environments
  • Require audience validation in the token verification interface
  • Add a session invalidation message to the protocol
  • Publish security hardening guidance for multi-server deployments

For the SDK maintainers:

  • Reject duplicate tool registrations by default (not just warn)
  • Add expected_audience parameter to TokenVerifier.verify_token()
  • Default validate_resource to True in IntrospectionTokenVerifier
  • Add BearerAuthBackend audience checking after token verification
  • Provide cache TTL guidance and per-operation cache bypass

For teams deploying MCP today:

  • Enable validate_resource=True if using the SDK's IntrospectionTokenVerifier
  • Use distinct OAuth issuers per server if possible (eliminates H2)
  • Set short cache TTLs for servers with destructive operations
  • Monitor for duplicate tool name registrations
  • Prefer larger models for MCP tool-calling tasks (smaller models are more exploitable)

Methodology Notes

Scanner: Our 122-attack taxonomy (open source), run via our scanner against MCP servers backed by production LLMs.

False positive handling: Raw scanner output had a 33-77% false positive rate (models refuse but quote evidence strings). All findings in this report were manually triaged to genuine results only. Genuine = model made an actual tool call with attacker arguments, or output only the canary with no refusal.

PoC infrastructure: All proof-of-concept code uses the MCP Python SDK's actual auth components (BearerAuthBackend, RequireAuthMiddleware, IntrospectionTokenVerifier) — not mocked implementations.

Responsible disclosure: The findings in this report describe vulnerability classes in the SDK architecture, not exploits against specific deployments. They affect the SDK interface design and default configurations. We have shared our findings with the MCP maintainers.

Total cost of all scanner runs: $2.83.


Download the Full Report

  • Download PDF Report — the complete research report with all findings, architecture diagrams, and remediation guidance (13 pages)

Sample Scanner Reports

These are the actual scanner reports from this research — unedited output showing every attack, model response, and severity classification.

This is what a Tachyonic assessment produces. Every finding includes the exact attack payload, the model's full response, tool calls made, and severity classification.


Tachyonic provides AI red team assessments. If your product uses MCP, tool-calling agents, or any LLM with external integrations, we test the boundaries that matter. Book a 15-minute scoping call.

Secure Your AI Agents

We find vulnerabilities in AI applications in 48 hours. Resistance score, reproduction steps, remediation playbook included.

Book a Free Scoping Call