How to Verify MCP-Server Provenance for Claude / GPT Agent Dependencies

Advanced 25 minutes ML Platform Engineers / AppSec / DevOps AI-ML Supply Chain

An MCP server is a tool you give an LLM. Treat it like one. This guide shows how Chainsaw flags npm and pip packages that ship MCP server descriptors, scores their provenance, and gates which servers can reach your agent runtimes.

Why MCP Servers Are a Special Case

A Model Context Protocol server is, from the LLM’s perspective, a tool. The agent calls it, hands it user data, and acts on what it returns. Anything that runs as an MCP server inside an agent runtime gets to:

  • See every prompt the agent receives (the agent forwards relevant context as tool input).
  • Return arbitrary text into the agent’s context window (a clean prompt-injection vector).
  • Trigger downstream tool calls by suggesting them in its response.

The blast radius is: anything the agent has access to. So a compromised MCP server is identical to compromising the agent itself — and yet many teams install MCP servers from npm or pip with the same npm install / pip install muscle memory they use for ordinary libraries, with none of the extra scrutiny.

Chainsaw’s job here is to recognize “this package is an MCP server” as a distinct artifact subtype and apply higher provenance and capability bars before letting it reach an agent runtime.

Prerequisites

Step 1: How Chainsaw Identifies an MCP Server

A package is tagged aiml.artifact_subtype == "mcp-server" when any of the following are true:

HeuristicWhere it looks
mcp-server in the package keywordsnpm package.json#keywords[], PyPI pyproject.toml#[project].keywords
Declared MCP entrypointpackage.json#mcp block, pyproject.toml#[tool.mcp] block
Imports @modelcontextprotocol/sdknpm dependency tree
Imports mcp (the official Python SDK)pip dependency tree
Ships a .mcp.json manifest at the repository rootSource archive contents

The detection is deliberately broad — it is better to over-tag and have a slightly stricter policy fire than to miss a server because it didn’t include the keyword.

Step 2: View MCP-Specific Signals

When a package is tagged mcp-server, the BOM detail shows an additional MCP panel:

FieldMeaning
mcp.declared_capabilitiesCapabilities the server declares (read_files, write_files, network, shell, etc.) — taken from the .mcp.json if present, otherwise from the SDK introspection at install time
mcp.transportstdio, streamable_http, sse
mcp.server_versionReported by the server descriptor
mcp.publisher_matchDoes the npm / PyPI publisher match the GitHub org of the source repo?
mcp.attestationSigstore / SLSA attestation chain (same primitive as ordinary packages — see tutorial 07)

Step 3: Require Verified Provenance for MCP Servers

The first policy to ship:

SettingValue
NameMCP servers must have verified provenance
ActionBlock
Conditionsaiml.artifact_subtype == "mcp-server" AND provenance_status != "verified"
Surfaceproxy, pr
ScopeThe service tokens that feed agent runtimes

This is strictly stricter than the ordinary provenance bar in tutorial 07. For ordinary libraries, missing provenance is acceptable in some ecosystems. For MCP servers, missing provenance means anyone could have published this, which is unacceptable when the install gives the publisher RCE inside an agent runtime.

Step 4: Gate on Capability Declarations

Read the mcp.declared_capabilities carefully and gate on what your agents are allowed to invoke. Two example policies:

No shell access from MCP servers

SettingValue
ActionBlock
Conditionsaiml.artifact_subtype == "mcp-server" AND mcp.declared_capabilities CONTAINS "shell"

If an MCP server declares a shell capability, it can run arbitrary commands on the host. There are legitimate uses (a “run my tests” tool inside a sandboxed CI agent), but they should be exception-bound, not default-on.

No filesystem write outside an allowlist

SettingValue
ActionQuarantine + ActionNotifyOwner
Conditionsaiml.artifact_subtype == "mcp-server" AND mcp.declared_capabilities CONTAINS "write_files" AND package_name NOT IN (allowed_mcp_writers)

The allowed_mcp_writers set is a list you maintain — a vetted handful of MCP servers that your AppSec team has reviewed and approved for write access. Everything else gets quarantined and routed to an owner.

Step 5: Publisher / Repo-Owner Match

mcp.publisher_match is the highest-signal detector for typo-squat MCP servers. If a server claims to be @anthropic/file-tool on npm but the linked GitHub repo is randomuser/file-tool, the publisher does not match and the package is almost certainly a squatter.

SettingValue
NameBlock MCP servers with publisher mismatch
ActionBlock
Conditionsaiml.artifact_subtype == "mcp-server" AND mcp.publisher_match == false

This single condition catches the most common MCP supply-chain attack pattern: an attacker who registers a similar-sounding npm name with a fake repo link.

Step 6: Wire MCP Server Selection into Agent Runtimes

Chainsaw decides whether the package can be installed. The agent runtime decides whether the installed package can be loaded. Both layers matter.

For Claude Code agents in particular, expose the resulting allowlist as a fetched configuration:

curl -H "Authorization: Bearer $CHAINSAW_TOKEN" \
     https://chain305.com/chainproxy/api/aiml/mcp/allowlist \
     | jq -r '.servers[].npm_package' \
     > ~/.claude/allowed_mcp_servers.txt

The agent runtime reads this file at startup and refuses to load any MCP server not on the list, even if npm install already succeeded — defense in depth.

Verification

  1. Pull @modelcontextprotocol/server-filesystem (the reference filesystem MCP server) through Chainsaw. Detail should show aiml.artifact_subtype: mcp-server, provenance_status: verified, mcp.declared_capabilities: ["read_files", "write_files"].
  2. Construct a test package that imports @modelcontextprotocol/sdk but has no provenance attestation. The proxy should block it under the policy in Step 3.
  3. Confirm a publisher-mismatch case (a private fork published under a different scope) blocks under the Step 5 policy.