Skip to content

Python: [Feature]: Supply-chain: MCP server allowlist & signature-verification primitive #5864

Description

Description

Context

The framework currently supports MCP server integration via MCPClient (Python) and ModelContextProtocol (NuGet). Library versions are pinned (mcp[ws]==1.27.0, ModelContextProtocol Version="1.1.0") — but the MCP servers themselves that a deployer connects to are not subject to any framework-level trust governance:

  • No allowlist primitive (any URL works)
  • No signature/pubkey verification
  • No version-pinning guidance for servers (vs. the MCP library)
  • No documented threat model in SECURITY.md or TRANSPARENCY_FAQ.md for MCP source trust

The closest existing item is #4927 (declarative MCP binding ergonomics), which is adjacent but distinct — that's about referencing servers, this is about trusting them.

Why this matters now (2026 supply-chain precedents)

The MCP ecosystem has had three publicly-discussed incidents in the last ~6 months:

  1. CrewAI CVE-2026-2275 / CVE-2026-2287 — prompt injection chained into RCE via silent sandbox degradation. The trust path between agent runtime and untrusted MCP-sourced tooling was the core enabling condition.
  2. Postmark MCP rug-pull — a previously-trusted MCP server published a malicious update; downstream deployments inherited it because no version pinning or signature check was in place.
  3. PhantomRaven "slopsquatting" — typosquatted MCP server names targeting the LLM-suggestion pathway (LLMs hallucinating package/server names that attackers then register).

In each case, deployer-side mitigation alone was insufficient because there was no framework primitive to enforce it.

Proposed primitive (sketch — open to other shapes)

A built-in allowlist + verification layer at the MCPClient boundary:

client = MCPClient(
    server_url="https://mcp.example.com",
    trust_policy=MCPTrustPolicy(
        allowed_origins=["https://mcp.example.com"],   # exact-match or glob
        require_signature=True,                          # PKI / sigstore
        pinned_version="1.4.2",                          # or hash
        on_violation="halt",                             # halt | warn | ignore
    ),
)

For .NET, an equivalent MCPTrustPolicy builder option on ModelContextProtocolClientFactory.

Acceptance criteria suggestion:

  • Default on_violation="halt" for new clients (fail-closed)
  • Documented threat model in a new ADR
  • Sample showing the verification flow against a public MCP registry
  • Test coverage for: unknown origin, version mismatch, signature mismatch, pubkey rotation

Regulatory context

This control maps to:

  • EU AI Act Art. 15 (third-party components)
  • ISO 27001:2022 A.5.19–22 (supplier relationships)
  • NIST AI RMF Map 3.4 (AI supply chain risk)
  • SOC 2 Type II CC9.2 (third-party risk)

Without a framework-level primitive, deployers building under any of these frameworks must implement the equivalent layer themselves — which is fragile and not easy to audit consistently.

Source

Found during an internal Aegis Release Gate v2.5 security assessment of MAF python-1.3.0 + dotnet-1.6.1. Happy to share more findings or PoC test cases if helpful.

Cross-link: #4927.

Code Sample

A built-in allowlist + verification layer at the `MCPClient` boundary:


client = MCPClient(
    server_url="https://mcp.example.com",
    trust_policy=MCPTrustPolicy(
        allowed_origins=["https://mcp.example.com"],   # exact-match or glob
        require_signature=True,                          # PKI / sigstore
        pinned_version="1.4.2",                          # or hash
        on_violation="halt",                             # halt | warn | ignore
    ),
)

Language/SDK

Both

Activity

  1. added
    .NETUsage: [Issues, PRs], Target: .Net
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on May 14, 2026
  2. changed the title [-][Feature]: Supply-chain: MCP server allowlist & signature-verification primitive[/-] [+].NET: [Feature]: Supply-chain: MCP server allowlist & signature-verification primitive[/+] on May 14, 2026
  3. changed the title [-].NET: [Feature]: Supply-chain: MCP server allowlist & signature-verification primitive[/-] [+]Python: [Feature]: Supply-chain: MCP server allowlist & signature-verification primitive[/+] on May 14, 2026
  4. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on May 21, 2026
  5. Santoshkumarpuppala commented on Sep 2, 2026

    @Santoshkumarpuppala

    The three controls sketched here — allowed_origins, require_signature, pinned_version — are all evaluated once, at MCPClient construction. Four things follow from that, checkable against this repo.

    1. Connect-time checks do not cover this codebase's discovery surface. python/packages/core/agent_framework/_mcp.py:1662-1665 handles notifications/tools/list_changed and notifications/prompts/list_changed by scheduling load_tools() / load_prompts(). A server that passes origin, signature and version at connect can push an entirely new set of tool and prompt definitions afterwards, and nothing in the sketch re-evaluates. Discovery here is not a connect-time event, so a connect-time primitive covers the first listing only.

    2. Two of the three controls are inert against the incident cited to justify them. In a Postmark-shaped rug pull the malicious update ships from the legitimate publisher: the origin allowlist passes because the URL is unchanged, and signature verification passes because the key is the real one. Only content pinning bears on that case — and only if the pin covers the tool definition, not a pinned_version="1.4.2" string the server itself reports. A version the server asserts about itself is not a control against a server that has decided to lie.

    3. Three defects we shipped in exactly this design, each mapping onto something already in this file.

    • Pin identity. Ours is trust-on-first-use keyed on (server, tool name). A rename produces a key with no prior, so it registers as new rather than changed and the integrity check never runs. That maps onto the existing reload path: _mcp.py:~1905 skips by local_name and treats anything not already present as new, so a renamed tool takes the new branch. A name-keyed pin would inherit that.
    • Field coverage. Ours pins an enumerated list of fields, which makes it an allowlist — anything the spec adds later ships unpinned with no signal that coverage shrank. There is a live instance here: #7824 tracks MCP 2026-07-28 including the Tasks Extension, and this file already reads tool.meta (672, 1877) and execution.taskSupport (351, 1840). A fixed field list drafted today would not cover them.
    • Scope. Ours pins tools only; prompts, resources and initialize.instructions are scanned but never hashed. That one is load-bearing here in a specific way: MAF turns prompts into FunctionTools in the same self._functions list, and _filtered_functions() already applies allowed_tools across the merged list — so the allowlist got the scope right. A pinning primitive needs the same scope the allowlist already has, or it covers half of what this client actually exposes to the model.

    4. A number on on_violation="halt" as the default. We shipped that posture and measured its cost: it withheld create_directory from the official MCP filesystem server, because the description contains the phrase "succeed silently" and a concealment rule matched the adverb bare. Upstream served 14 tools, the model saw 13, and the only signal was one line on stderr. Worth setting a false-positive budget alongside the acceptance criteria rather than discovering one.

    Related, for the audit criterion rather than the gate: we also shipped a defect where a policy refusal was persisted to the audit trail as an allow, because the record was built by a different code path than the one that enforced. Both paths were individually correct. If this grows an audit story, the enforcing path and the recording path should be the same object rather than two readings of the same event.

    Disclosure: I maintain an MCP policy enforcement point, which is where all of the above was measured — offered as what the design cost us, not as a recommendation to adopt anything.

  6. GitSerge-crypto commented on Sep 29, 2026

    @GitSerge-crypto

    The failure modes you list — a previously-trusted server shipping a malicious update, typosquatted names riding the LLM-suggestion path — are exactly the cases where signature verification plus content-hash pinning earn their keep. On the signing piece specifically: Ed25519-signed server/tool descriptors with a digest pin work well in practice — the signature proves who published it, the SHA-256 pin fixes the exact revision, and pubkey rotation is handled by verifying against a key registry instead of one pinned key.

    We built this shape in AOTrust (open provenance layer): each allow/deny decision at the client boundary gets sealed as a short hash-chained signed record (tool, origin, pinned digest, decision, timestamp), so deployments get the ISO 27001 / SOC 2 audit trail you map this control to without logging full payloads — a third party can verify the chain offline. Agree on fail-closed on_violation="halt" as the default; happy to compare notes on the trust_policy surface.

  7. Paraphern commented on Oct 10, 2026

    @Paraphern

    Field data on the "post-connection" gap Santosh Kumar Puppala (@Santoshkumarpuppala) identified. We run a weekly drift tracker over the most-installed MCP servers on npm (pin approved contracts, diff on update, grade by direction). The three controls sketched here - allowed_origins, require_signature, pinned_version - all operate at connect time. What our tracker shows is that the contract your agent obeys changes between connections, silently, without the version string moving:

    • One package erased "Requires confirmation" from all 48 destructive SSH tools in a patch release (2.0.2 -> 2.0.3). Same version bump class as a bug fix. pinned_version at the minor range would not have caught it.
    • The This repo is missing a LICENSE file #1 server on npm (1.7M installs/week) re-serialized all 28 tool schemas in five weeks via an SDK migration - every hash-level pin fires, but zero parameters actually changed. Without grading, this is a false-positive storm that trains users to ignore the gate.
    • A Lightning wallet gained five betting tools (spending from the agent's balance) in a minor update. allowed_origins was unchanged - same server, same URL, new capabilities.

    218 silent contract changes in our October audit across the npm top. The full distribution: 4 of 9 servers drifted (concentrated in fast movers), 5 were clean for comparable windows. Receipts: https://github.com/Paraphern/rugsnare/blob/main/audits/npm-top-mcp-drift-2026-10.md

    What this suggests for the framework primitives:

    1. pinned_version needs a content-hash component, not just a semver string. Version numbers don't capture contract changes within the same version (mid-session description rewrites are invisible to semver entirely). A sha-256 over each tool's {name, description, inputSchema} pinned at approval, compared on every reconnect or tools/list_changed notification, closes the gap.

    2. The diff needs grading. A naive hash-comparison flags SDK upgrades (key reordering, dialect changes, additionalProperties form) identically to a rewritten safety instruction. We grade three ways - BREAKING (parameters changed), LOOSENED (constraints dropped), NOTATION (semantically void) - and all three still exit non-zero (it's drift), but the label tells the deployer whether to page someone or click through. Without grading, the false-positive rate on legitimate SDK upgrades will train users to disable the control.

    3. tools/list_changed should trigger a diff, not just a re-fetch. If the framework already handles this notification, the re-listed contract can be compared against the pinned baseline at near-zero cost. We described this pattern for the cline CLI (CLI: MCP notifications/tools/list_changed is ignored — tools added mid-session never appear (extension fixed in #12619) cline/cline#14924, fix(core): refresh MCP tools on list_changed notifications in the session runtime cline/cline#14946) - same architecture applies here.

    Happy to contribute the hash-grading implementation or test material (we keep a public corpus of benign/weaponized contract pairs for exactly this kind of integration).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETUsage: [Issues, PRs], Target: .NetpythonUsage: [Issues, PRs], Target: Python

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions