mcp-fingerprint
Deterministic fingerprinting for MCP tool contracts.
If you expose tools through an MCP server, those tools have a contract: names,
schemas, descriptions, and related metadata. mcp-fingerprint turns that
contract into a stable SHA-256 fingerprint, can save a receipt-bound baseline,
and can later tell you whether the contract changed — and if so, what
changed as a structural diff.
Same input → same fingerprint. Differences are inspectable field-level changes, not opaque scores or AI judgments.
Why use it
Use MCP Fingerprint when you want to:
- detect tool-contract drift between environments or releases
- pin a known-good MCP tool surface as a local baseline
- review exact structural changes (added/removed tools, schema edits, etc.)
- keep the check deterministic and automatable in CI or scripts
Install
pip install 'mcp-fingerprint==0.2.1'
Requires Python >=3.12,<3.13.
This pulls salt-grain 0.1.0 for the
small deterministic primitives (canonicalize, digest, structured diff, receipt
bind/verify) and the official mcp Python SDK
(>=1.28,<2) for stdio acquisition. You do not need to install those separately.
CLI (0.2.0)
Fingerprint a live stdio MCP server, save a project-local baseline, or check for tool-contract drift:
mcp-fingerprint fingerprint -- <command> [args...]
mcp-fingerprint save -- <command> [args...]
mcp-fingerprint check -- <command> [args...]
The -- separator before the server command is required. Transport is stdio
only in 0.2.0 (no HTTP/SSE). There is no --strict-tools flag.
Useful flags:
| Flag | Meaning |
|---|---|
--baseline PATH |
Explicit baseline file (overrides default path) |
--baseline-dir DIR |
Default dir for baselines (default .mcp-fingerprint) |
--json |
Emit the machine-readable result envelope |
--timeout SECONDS |
Stdio acquisition timeout |
Default baseline path:
.mcp-fingerprint/<sanitized-server-name>.json
Option A live projection
When acquiring tools from a live MCP server, 0.2.x projects each Tool into the
closed mcp.fingerprint.snapshot.v0.1 identity by omitting icons,
_meta, and execution (and any other non-identity keys). The same projector
is the supported public API:
from mcp_fingerprint import project_live_snapshot
The library path for caller-supplied snapshots still rejects icons /
_meta / unknown tool keys as UNSUPPORTED_SNAPSHOT.
Already have serverInfo + tools/list
Integrations that already initialized an MCP server and obtained serverInfo
plus tools/list should project once, then call the library API — do not
reimplement Option A rules:
from pathlib import Path
from mcp_fingerprint import (
check,
fingerprint,
project_live_snapshot,
save,
)
# Already acquired from your MCP client:
server_info = {"name": "demo", "version": "1.0.0"}
tools_list = [
{
"name": "search",
"description": "Search documents",
"inputSchema": {"type": "object", "properties": {"q": {"type": "string"}}},
"icons": [{"src": "https://example.invalid/icon.png"}], # omitted by Option A
"_meta": {"vendor": "x"}, # omitted by Option A
"execution": {"taskSupport": "optional"}, # omitted by Option A
}
]
snapshot = project_live_snapshot(server=server_info, tools=tools_list)
# snapshot is closed mcp.fingerprint.snapshot.v0.1 (icons/_meta/execution gone)
fp = fingerprint(
{
"schema_version": "mcp.fingerprint.request.v0.1",
"snapshot": snapshot,
}
)
assert fp["ok"] is True
baseline_path = Path("baseline.json")
if baseline_path.exists():
baseline_path.unlink()
saved = save(
{
"schema_version": "mcp.fingerprint.save.request.v0.1",
"snapshot": snapshot,
"baseline_path": str(baseline_path),
}
)
assert saved["ok"] is True
result = check(
{
"schema_version": "mcp.fingerprint.check.request.v0.1",
"snapshot": snapshot,
"baseline_path": str(baseline_path),
}
)
assert result["status"] == "UNCHANGED"
Exit codes
| Code | Meaning |
|---|---|
0 |
Success (fingerprint / save) or check UNCHANGED |
1 |
Operational / core failure |
2 |
Check observed CHANGED |
Example:
mcp-fingerprint check --json -- python -m my_mcp_server
echo $? # 0, 1, or 2
Expected snapshot shape
Pass an MCP tool-contract snapshot shaped like:
snapshot = {
"schema_version": "mcp.fingerprint.snapshot.v0.1",
"server": {"name": "demo", "version": "1.0.0"},
"tools": [
{
"name": "search",
"description": "Search documents", # optional
"inputSchema": {
"type": "object",
"properties": {"q": {"type": "string"}},
},
# optional when present on the tool object:
# "title": "...",
# "outputSchema": {...},
# "annotations": {...},
}
],
}
Identity includes schema_version, server.name, server.version, each
tool's name and inputSchema, plus optional description / title /
outputSchema / annotations only when those keys are present. Missing
optional keys are not synthesized. Tool order in the list does not matter
(tools are sorted by name). Unsupported keys such as icons and _meta are
rejected.
Quickstart: fingerprint → save → check
from pathlib import Path
from mcp_fingerprint import fingerprint, save, check
snapshot = {
"schema_version": "mcp.fingerprint.snapshot.v0.1",
"server": {"name": "demo", "version": "1.0.0"},
"tools": [
{
"name": "search",
"description": "Search documents",
"inputSchema": {
"type": "object",
"properties": {"q": {"type": "string"}},
},
}
],
}
# 1) Fingerprint
fp = fingerprint(
{
"schema_version": "mcp.fingerprint.request.v0.1",
"snapshot": snapshot,
}
)
assert fp["ok"] is True
print(fp["fingerprint"]) # 64 lowercase hex chars; stable for this snapshot
# 2) Save a receipt-bound baseline (fails if the path already exists)
baseline_path = Path("baseline.json")
if baseline_path.exists():
baseline_path.unlink()
saved = save(
{
"schema_version": "mcp.fingerprint.save.request.v0.1",
"snapshot": snapshot,
"baseline_path": str(baseline_path),
}
)
assert saved["ok"] is True
# 3) Check unchanged
unchanged = check(
{
"schema_version": "mcp.fingerprint.check.request.v0.1",
"snapshot": snapshot,
"baseline_path": str(baseline_path),
}
)
assert unchanged["ok"] is True
assert unchanged["status"] == "UNCHANGED"
assert unchanged["changes"] == []
print(unchanged["status"])
# 4) Change a tool contract and get CHANGED + structural diff
changed_snapshot = {
"schema_version": "mcp.fingerprint.snapshot.v0.1",
"server": {"name": "demo", "version": "1.0.0"},
"tools": [
{
"name": "search",
"description": "Search documents (updated)",
"inputSchema": {
"type": "object",
"properties": {"q": {"type": "string"}},
},
}
],
}
changed = check(
{
"schema_version": "mcp.fingerprint.check.request.v0.1",
"snapshot": changed_snapshot,
"baseline_path": str(baseline_path),
}
)
assert changed["ok"] is True
assert changed["status"] == "CHANGED"
assert changed["changes"] # inspectable structured diff entries
print(changed["status"], len(changed["changes"]))
print(changed["changes"][0])
Expected behavior for that example:
fingerprintprints the same 64-character hex string every run for the same snapshot- first
checkprintsUNCHANGED - second
checkprintsCHANGEDwith a non-emptychangeslist describing the description edit
How it works
normalized MCP tool contract
->
canonical bytes
->
deterministic SHA-256 fingerprint
->
optional receipt-bound baseline
->
structural diff
- Normalize — sort tools by name; keep only supported identity fields; reject unsupported envelope keys.
- Canonicalize — encode the normalized snapshot as exact deterministic bytes.
- SHA-256 — digest those bytes to a lowercase hex fingerprint.
- Baseline (optional) —
savewrites fingerprint + snapshot + receipt to a local JSON file;checkverifies the receipt before comparing. - Structural diff — when fingerprints differ,
checkreturns field-level changes (or a typed limit failure if the diff budget is exceeded).
CHANGED vs UNCHANGED
| Status | Meaning |
|---|---|
UNCHANGED |
Current fingerprint matches the verified baseline; changes is []. |
CHANGED |
Fingerprint differs; changes is a structural diff of identity fields. |
check fails with a typed error (not CHANGED / UNCHANGED) when the
baseline is missing, invalid, fails receipt verification, or when limits are
exceeded.
Determinism guarantees
- Identical normalized snapshots → identical fingerprints.
- Tool list order does not affect identity.
- Missing optional tool keys are not filled in; empty string ≠ absent key.
- Caller-supplied mappings are not mutated.
- No AI / model judgment is involved.
Failure behavior / limits
Operations return structured envelopes with ok: true|false. Common failure
codes:
| Code | Meaning |
|---|---|
DUPLICATE_TOOL_NAME |
Two tools share a name |
UNSUPPORTED_SNAPSHOT |
Rejected fields (e.g. icons, _meta) |
BASELINE_ALREADY_EXISTS |
save path already exists |
BASELINE_INVALID |
Baseline file is not a valid baseline |
BASELINE_VERIFICATION_FAILED |
Receipt verification failed (e.g. tamper) |
LIMIT_EXCEEDED |
Canonical payload or diff change budget exceeded |
Oversized or non-conforming inputs fail closed with typed codes.
What this is not
MCP Fingerprint does not:
- use AI judgment or semantic “similarity”
- detect malware
- authenticate publishers
- sign code or establish trust anchors
- intercept or filter runtime MCP traffic
It fingerprints and diffs the tool-contract snapshot you supply.
Python support
- Python
>=3.12,<3.13
License
Apache License 2.0. See LICENSE.
Release files for mcp-fingerprint 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcp_fingerprint-0.2.1.tar.gz | 32.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_fingerprint-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 60.8 kB
Release files / mcp_fingerprint-0.2.1.tar.gz
| Download URL | mcp_fingerprint-0.2.1.tar.gz |
|---|---|
| Size | 32.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cc859a4a8e438c6a891587899205b49ed35dfcdae7ca95bd3b55d9da5851d32e
|
|
BLAKE2b-256 checksum How to use checksums |
0c1c4019b391d550fa3bc0d8ea7ab995df1e09abd6c55c4d9eeab91df7c5ef72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 11, 2026.
Transparency logRelease files / mcp_fingerprint-0.2.1-py3-none-any.whl
| Download URL | mcp_fingerprint-0.2.1-py3-none-any.whl |
|---|---|
| Size | 28.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f6e13158b94997b00b1692d1cf3b9b34865e4199099bff72998494a4a3543d92
|
|
BLAKE2b-256 checksum How to use checksums |
bd935362b18a10fc902548591454b949d1d2af5c257c6f1487f02842ac32aaac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 11, 2026.
Transparency log