Skip to main content

mcp-tool-pin-check: MCP Tool Definition Pinning Checker

Pin an MCP server's tool definitions, then diff a later listing for silent changes. One read-only Python script. No third-party packages.

Use it on servers you own or are explicitly allowed to assess.

Why it matters

OWASP's description of MCP tool poisoning calls out a trust gap: tool descriptions are reviewed once, when an agent first connects, and later server behavior is not checked the same way. A listing can still change after that review. This checker stores a normalized copy and a SHA-256 hash of each tool so you can diff a later tools/list export before you keep treating the server as unchanged.

Quickstart

Requires Python 3.9 or newer. The standard library is enough.

pip install mcp-tool-pin-check

This program does not connect to a server. From an MCP client you already use against a server you are authorized to assess, save the tools/list response to a file. The file can be any of these:

  • a JSON-RPC response with result.tools
  • an object with a tools array
  • a bare list of tools

Pin that file, then check a later export against the lockfile:

python3 mcp_tool_pin_check.py pin \
  --listing tools-list.json \
  --lock mcp-tools.lock.json \
  --source my-notes-server

python3 mcp_tool_pin_check.py check \
  --listing tools-list.json \
  --lock mcp-tools.lock.json

After install, the same subcommands are pin and check:

mcp-tool-pin-check pin \
  --listing tools-list.json \
  --lock mcp-tools.lock.json \
  --source my-notes-server

mcp-tool-pin-check check \
  --listing tools-list.json \
  --lock mcp-tools.lock.json

--source is a label stored in the lockfile. If you omit it, the listing path is stored. --pinned-at sets the timestamp; if you omit it, the checker uses the current UTC time. --json prints the same result as JSON.

The examples are a small notes server. examples/tools-list.changed.json adds create_note, rewords get_note, and changes list_notes by adding an optional limit property and dropping query from required. examples/tools-list.output.json adds an outputSchema to each of those tools, and examples/tools-list.output-enum-widened.json adds deleted to the get_note output status enum. examples/tools-list.identity.json is the same tools plus an MCP initialize result. examples/tools-list.identity-version.json, examples/tools-list.identity-protocol.json, and examples/tools-list.identity-name.json change only serverInfo.version, protocolVersion, or serverInfo.name. The sample below sets --pinned-at so the timestamp is reproducible:

python3 mcp_tool_pin_check.py pin \
  --listing examples/tools-list.json \
  --lock /tmp/notes.lock.json \
  --source local-notes \
  --pinned-at 2026-10-08T00:00:00Z

python3 mcp_tool_pin_check.py check \
  --listing examples/tools-list.changed.json \
  --lock /tmp/notes.lock.json

mcp-tool-pin-check pin \
  --listing examples/tools-list.json \
  --lock /tmp/notes.lock.json \
  --source local-notes \
  --pinned-at 2026-10-08T00:00:00Z

mcp-tool-pin-check check \
  --listing examples/tools-list.changed.json \
  --lock /tmp/notes.lock.json

pin prints:

Pinned 2 tools to /tmp/notes.lock.json
source: local-notes
pinned_at: 2026-10-08T00:00:00Z
  get_note 1550f3326897f328ef901e418c632b3aad519ab9126415e51d63a82cb930e6b6
  list_notes 10fb955592a3ea05a96f15c689e5a8f52450c83a9d72b57bd98a42a20d35b758

check prints:

MCP tool pin check
lock source: local-notes
pinned_at: 2026-10-08T00:00:00Z
result: drift

server identity:
  note: identity was not pinned

added (1):
  + create_note
removed (0):
  (none)
changed (2):
  ~ get_note
    description:
    --- description (pinned)
    +++ description (current)
    @@ -1 +1 @@
    -Return one note by its identifier.
    +Fetch a single saved note using its identifier.

  ~ list_notes
    schema:
    [widened] property added: properties.limit (optional)
    [widened] required removed: query
    schema widened: yes
unchanged (0): (none)

That check exits 1.

Exit codes

Code Meaning
0 The normalized tool listing matches the lockfile, and the pinned server name matches when identity was pinned. A server version change, a protocolVersion change, or identity added or removed does not by itself change this code.
1 Drift: a tool was added, removed, or its definition changed, including an output-schema change, or the pinned server name changed.
2 The listing or lockfile could not be read (missing file, invalid JSON, duplicate names, a lockfile hash that does not match its stored definition, or server identity that is present but not usable).

GitHub Actions

Commit the lockfile. Produce a fresh tools/list file in an earlier step you control, then fail the job when the listing has drifted:

- name: Check pinned MCP tool definitions
  run: python3 mcp_tool_pin_check.py check --listing tools-list.json --lock mcp-tools.lock.json

This repository's own workflow runs the unit tests only. It does not contact an MCP server.

What it does

Each tool is reduced to name, description, inputSchema, outputSchema, and annotations when annotations are present. Tools are sorted by name. pin writes a JSON lockfile (lockfile_version 1) with pinned_at, a source label, and one SHA-256 per normalized tool, plus the normalized definition. The hash is SHA-256 of the canonical JSON, with keys sorted, so outputSchema is covered by the same hash as inputSchema. A tool with no output schema stores outputSchema: null. check reports added tools, removed tools, a short unified diff when a description changed, input-schema changes, and output-schema changes on separate lines.

A schema line marked [widened] means the input schema accepts more than the pin:

  • a new property
  • a required name removed
  • enum removed or given new values
  • maxLength removed or increased
  • pattern removed
  • additionalProperties moving from false or a subschema to true or absent

Other input-schema edits are still drift. They are printed without [widened]. A tighter maxLength, for example, is a change and not a widening.

Output-schema findings are printed under output schema:, not in the input schema: block and not as schema widened. A new enum value anywhere in outputSchema is a line of its own:

    output widened: properties.status.enum (added "deleted"); a new output enum value can break downstream exhaustive switches

That note is there because a caller with an exhaustive switch on the pinned enum can break when the server adds a value. Other output edits are output changed lines. Those include added or removed properties, type changes, and removed enum values. Output drift exits 1.

A v0.1 lockfile has no outputSchema field. check still exits 0 when the current listing also has no output schema, so upgrading the checker does not fail that pin. Re-pinning the same listing writes a new hash, because the stored definition now includes outputSchema: null. If the listing has an outputSchema the old lock never pinned, check prints output changed: outputSchema added (not present in the pin) and exits 1. That counts as drift: an output contract the pin never recorded is a definition change. Re-pin after you have reviewed the new schema.

Server identity

pin can record which server produced the listing. Put the MCP initialize result in the same JSON file as the tools. The lock stores serverInfo.name, serverInfo.version, and protocolVersion when that field is present. Identity is not part of the tool hash. A listing with no identity writes the same tools-only lock as v0.2.0.

Either of these shapes works:

  • serverInfo and optional protocolVersion on the same object as tools, or inside result next to tools
  • an initialize object next to the tools. That object may be the initialize result itself, or a JSON-RPC response whose result is the initialize result

examples/tools-list.identity.json uses the initialize envelope. A later check against examples/tools-list.identity-version.json exits 0 and prints server version changed.

check prints a server identity section:

  • server version changed is informational. A version bump on the same server name does not change the exit code.
  • protocol version changed is informational, including when protocolVersion was added or dropped. It does not change the exit code.
  • server name changed is drift and exits 1, even when every tool still matches. A different serverInfo.name behind the same pin is a substitution signal: the listing may be from a different server than the one you reviewed.
  • identity added and identity removed are reported on their own lines. They do not by themselves change the exit code. A tools-only export after an identity pin prints identity removed and still exits 0 when the tools and, if present, the server name are otherwise unchanged.

A v0.1 or v0.2.0 lock has no server identity. check still exits 0 when the tools match, and it prints note: identity was not pinned. If that later listing also includes an initialize result, the note is kept and identity added is printed. That added line is informational. Re-pin once you want the name and version stored.

python3 mcp_tool_pin_check.py pin \
  --listing examples/tools-list.identity.json \
  --lock /tmp/notes.lock.json \
  --source local-notes \
  --pinned-at 2026-10-08T00:00:00Z

python3 mcp_tool_pin_check.py check \
  --listing examples/tools-list.identity-version.json \
  --lock /tmp/notes.lock.json

What it does not do

  • It does not open a network connection, call initialize or tools/list, start a server, or modify an MCP server. You save those responses yourself.
  • It does not execute text from descriptions or schemas. It only reads the files you name and writes the lockfile path you pass to pin.
  • It does not decide that a description is safe or unsafe. It shows whether the stored definition changed.
  • It does not pin tool fields other than name, description, input schema, output schema, and annotations. Server identity is only serverInfo.name, serverInfo.version, and protocolVersion.
  • A clean check is not a full review of the server. It means the normalized tool definitions match and, when identity was pinned, the server name matches. A version or protocolVersion difference can still be printed as information.

Authorized use

This repository is MIT licensed. It is for defensive checks only.

  • Pin and check tool listings from MCP servers you own or are authorized to assess.
  • Save those listings from clients you are allowed to use.
  • Do not point it at servers you are not allowed to review.

Tests

From this directory:

python3 -m unittest tests.test_pin_check -v
python3 -m unittest discover -s tests -v

Changelog

v0.2.2

  • Package the checker for installation with pip. pin and check behavior is unchanged from v0.2.1.

v0.2.1

  • pin records server identity from an MCP initialize result included in the listing JSON: serverInfo.name, serverInfo.version, and protocolVersion when it is present. Identity is stored on the lock and is not part of the tool hash. Listings without identity still produce a tools-only lock.
  • check reports identity in its own section. server version changed and protocol version changed are informational and do not change the exit code. server name changed is drift and exits 1, because a different server name behind the same pin is a substitution signal. identity added and identity removed are reported and do not by themselves change the exit code.
  • A v0.1 or v0.2.0 lock with no identity still checks cleanly (exit 0) when the tools match, and check prints note: identity was not pinned.

v0.2.0

  • pin stores and hashes each tool's outputSchema with the same canonical-JSON SHA-256 as the rest of the definition. A missing output schema is recorded as null.
  • check reports output-schema changes on their own lines. A new enum value anywhere in outputSchema is flagged output widened, with a note that it can break downstream exhaustive switches. Other output edits are reported as output changed.
  • v0.1 lockfiles, which have no outputSchema data, still check cleanly (exit 0) against a listing whose tools have no output schema. An outputSchema the old lock never pinned is an output-added finding and counts as drift (exit 1).

v0.1.0

  • Initial public release. Pin name, description, inputSchema, and annotations, and diff a later listing for added tools, removed tools, description edits, and input-schema changes.

For more from Digital Fortress

For more from Digital Fortress:

A free sibling starter, with copy-paste audit prompts and one read-only repository scan, is the AI Security Starter Checklist.

Sources

License

MIT © 2026 Digital Fortress · github.com/digitalfortressaudit. Authorized defensive use only, as described above.

Metadata

Release files for mcp-tool-pin-check 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mcp-tool-pin-check 0.2.2
File Size Uploaded Provenance
mcp_tool_pin_check-0.2.2.tar.gz 27.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mcp-tool-pin-check 0.2.2
File Interpreter ABI Platform
mcp_tool_pin_check-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 44.2 kB

Release files / mcp_tool_pin_check-0.2.2.tar.gz

Download URL mcp_tool_pin_check-0.2.2.tar.gz
Size 27.7 kB
Tags Source
SHA-256 checksum
How to use checksums
5d66cccc8a9d7e8c378847047c5251f673df2d278c05df7a62f35b16c703742f
BLAKE2b-256 checksum
How to use checksums
5f52fcc3c54b36a3f95e98a9aa4eeebecf992a6afe0dfb396c99c9ab0728eebf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / mcp_tool_pin_check-0.2.2-py3-none-any.whl

Download URL mcp_tool_pin_check-0.2.2-py3-none-any.whl
Size 16.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
64351f2765c1d34d765bb9bbd53f388592aadf6d6cd77548dad2837468486108
BLAKE2b-256 checksum
How to use checksums
01b077fe18659c49f9ece2af675c9ddcd32a2e13a38bd46bda375b8a228ebb67
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for mcp-tool-pin-check 0.2.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for mcp_tool_pin_check-0.2.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page