Skip to content

Tool Integrity for MCP: Digest Pinning in MCP Hangar v1.2

v1.1 gave you observability. v1.2 gives you certainty that the tools you observed are the tools you're running.

This release introduces digest pinning for Model Context Protocol (MCP) tools, implementing a preemptive version of SEP-1766. This feature prevents tool drift and unauthorized mutation by validating the SHA-256 fingerprint of every tool definition before it reaches the agent. Here is what shipped and how to use it.

The Problem: Tool Mutation and Drift

In a standard MCP flow, the server provides tool definitions (name, description, and input schema) to the client. If a server update changes a schema or a malicious actor modifies a tool definition in transit, the client might invoke a tool with unexpected parameters or for an unintended purpose. There has been no mechanism to ensure that the tool being executed matches the version approved by security teams.

The Solution: Digest Pinning (SEP-1766)

SEP-1766 proposes that MCP servers publish a SHA-256 digest for every tool, enabling clients to pin specific tool versions and detect changes. The spec envisions the digest computed over the tool's code archive (ZIP/TAR) and published by the server as part of tools/list.

MCP Hangar takes a different and complementary approach. Instead of relying on the server to publish a code-level digest, the agent computes the digest client-side from the tool's schema as returned by tools/list. This is a deliberate design choice:

  • No server cooperation required. The agent can pin and verify tool schemas from any MCP server, including servers that do not implement SEP-1766. There is nothing to adopt on the server side.
  • Schema drift is the proxy-visible threat. The agent sits between the LLM and the MCP server. It sees tools/list responses but never sees the server's source code. Schema-level pinning detects the class of mutations that are observable from the proxy layer: renamed parameters, changed descriptions, modified input schemas.
  • Code-level and schema-level pinning are complementary. A tool's schema can stay stable while the code changes (bug fix, optimization), and a tool's code can stay stable while the schema changes (new parameter, updated description). SEP-1766 code digests catch the first case. MCP Hangar schema digests catch the second. When SEP-1766 is ratified and servers start publishing code digests, MCP Hangar will validate both.

The specification is not yet ratified. If the final SEP changes the digest algorithm or field structure, our implementation adapts -- the core concept is abstracted behind the ToolDigest value object, so a field rename is a mechanical refactor.

How It Works

At the core of the implementation is the ToolDigest value object and a canonicalization process.

Canonical Digest Computation

To ensure consistent hashing across different JSON serializers, MCP Hangar canonicalizes the tool definition before hashing. Only the name, description, inputSchema, and outputSchema are included in the digest.

python
def compute_tool_digest(tool: dict[str, Any]) -> ToolDigest:
    name = tool.get("name")
    canonical_payload: dict[str, Any] = {"name": name}
    description = tool.get("description")
    if description is not None:
        canonical_payload["description"] = description
    input_schema = tool.get("inputSchema")
    if input_schema is not None:
        canonical_payload["inputSchema"] = input_schema
    output_schema = tool.get("outputSchema")
    if output_schema is not None:
        canonical_payload["outputSchema"] = output_schema
    serialized = json.dumps(canonical_payload, sort_keys=True, separators=(",", ":"))
    sha256_hex = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
    return ToolDigest(tool_name=name, sha256=sha256_hex)

The ToolDigest stores the result:

python
@dataclass(frozen=True)
class ToolDigest:
    tool_name: str
    sha256: str
    algorithm: str = "sha256"

Validation Logic

The DigestValidator compares the observed digest against an expected digest from a configured DigestPolicy.

python
class DigestValidator:
    def __init__(self, policy: DigestPolicy) -> None:
        self._policy = policy

    def validate_tool(self, tool: dict[str, Any], mcp_server_id: str, correlation_id: str) -> DigestValidationResult:
        observed = compute_tool_digest(tool)
        expected = self._policy.get_expected_digest(observed.tool_name)
        if expected is None:
            return self._handle_unknown_tool(...)
        if expected.sha256 == observed.sha256:
            return DigestValidationResult(tool_name=observed.tool_name, valid=True, blocked=False, event=None)
        return self._handle_mismatch(...)

Enforcement Modes

Policies are defined using DigestPolicy, allowing you to control how Hangar reacts to mismatches or unknown tools.

python
@dataclass(frozen=True)
class DigestPolicy:
    enforcement: DigestEnforcement
    unknown: DigestUnknownPolicy
    allowlist: frozenset[ToolDigest]

Mismatch Enforcement (DigestEnforcement)

ModeAction
auditLog the mismatch but allow the tool call to proceed.
warnLog the mismatch, emit a warning event, and allow the call.
blockPrevent the tool call and return a validation error to the client.

Unknown Tool Policy (DigestUnknownPolicy)

ModeAction
allow_degradedAllow tools that are not in the allowlist without validation.
warnAllow the call but emit a warning that the tool is untracked.
blockDeny any tool not explicitly listed in the policy.

What's Next

Digest pinning is the first step toward a full chain of custody for MCP tools. Upcoming work includes automated policy generation from observed behavior in audit mode and integration with the SEP-1763 interceptor framework for runtime enforcement.

MCP Hangar · Released under MIT License.