Metadata-Version: 2.4
Name: mcp-fingerprint
Version: 0.1.1
Summary: Deterministic MCP tool-contract fingerprinting, baseline save, and structured change check
Author: SaltyDiff
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/SaltyDiff/mcp-fingerprint
Project-URL: Repository, https://github.com/SaltyDiff/mcp-fingerprint
Project-URL: Issues, https://github.com/SaltyDiff/mcp-fingerprint/issues
Keywords: saltydiff,mcp,fingerprint,deterministic,baseline,structured-diff
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: <3.13,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: salt-grain==0.1.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Dynamic: license-file

# 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

```bash
pip install mcp-fingerprint
```

Requires Python `>=3.12,<3.13`.

This pulls [`salt-grain`](https://pypi.org/project/salt-grain/) `0.1.0` for the
small deterministic primitives (canonicalize, digest, structured diff, receipt
bind/verify). You do not need to install those separately.

## Expected snapshot shape

Pass an MCP tool-contract snapshot shaped like:

```python
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

```python
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:

- `fingerprint` prints the same 64-character hex string every run for the same snapshot
- first `check` prints `UNCHANGED`
- second `check` prints `CHANGED` with a non-empty `changes` list describing the description edit

## How it works

```
normalized MCP tool contract
    ->
canonical bytes
    ->
deterministic SHA-256 fingerprint
    ->
optional receipt-bound baseline
    ->
structural diff
```

1. **Normalize** — sort tools by name; keep only supported identity fields;
   reject unsupported envelope keys.
2. **Canonicalize** — encode the normalized snapshot as exact deterministic bytes.
3. **SHA-256** — digest those bytes to a lowercase hex fingerprint.
4. **Baseline (optional)** — `save` writes fingerprint + snapshot + receipt to a
   local JSON file; `check` verifies the receipt before comparing.
5. **Structural diff** — when fingerprints differ, `check` returns 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](LICENSE).
