Skip to main content

provmcp

A provenance and replay-verification proxy for scientific MCP tool use.

Status: working, 62 tests passing, pip install provmcp. Verified end to end against MCPmed's geo-mcp on 2026-09-20 (record → verify → replay). Not production-ready: no load testing, one ledger writer per process, no auth.

What it does

Sits between an MCP client and any downstream MCP servers — GEO, Open Targets, ClinicalTrials.gov, your own. Forwards everything. On the way through, records each tool call into an append-only, hash-chained, Ed25519-signed ledger:

  • the call and its arguments (with named fields redacted if configured)
  • a SHA-256 of the result over canonical JSON
  • the resolved data-source release, where the source exposes one
  • the calling client's name/version, when it identifies itself
  • a contemporaneous UTC timestamp and duration

Exports a portable W3C PROV bundle you can attach to an ELN entry, a methods section, or supplementary material, and verify without provmcp installed.

The feature that matters: replay verification

$ provmcp replay ./provenance.jsonl --config servers.yaml      # illustrative output
12 recorded calls replayed
  3 of 12 now return different data:
    seq 4    geo__search_geo_series    [GEO gds Build260301-1102.1 -> GEO gds Build260919-1942.1]
    seq 7    opentargets__target_info  [OT 26.06 -> OT 26.09]
    seq 9    geo__search_geo_datasets  [GEO gds Build260301-1102.1 -> GEO gds Build260919-1942.1]
  1 flaky:
    seq 11   trials__search            two fresh runs disagree with each other

Signed, hash-chained MCP audit proxies already exist — mcp-audit-gateway, auditmcp, ArkForge's commercial certifying proxy, Oculix's in-server journal. They prove a call happened. Record/replay tools exist too — Agent VCR, mcp-cassette — but they replay cassettes as mock servers and diff recordings against each other. None re-executes against the live source, none knows which release the source was at, and none tells flaky apart from changed. That is the part provmcp adds, and it is the only part that is new.

Replay distinguishes genuine upstream change from a flaky tool by re-running a disagreeing call a second time. Without that, nondeterminism reads as changed data, which is the false alarm that would make the whole tool untrustworthy. --single-run turns this off, and a test shows exactly what you lose.

Release resolution

A changed hash is an alarm. A changed hash with [OT 26.06 -> OT 26.09] next to it is an explanation. Resolvers are per-source, best-effort, and return None rather than guess. Verified live on 2026-09-20:

source Where the release comes from Example (2026-09-20)
geo NCBI E-utilities einfo dbbuild, db=gds GEO gds Build260919-1942.1
eutils same, any NCBI db (pubmed, clinvar, sra, …) PubMed Build-2026.09.20.06.18
opentargets GraphQL meta.dataVersion OT 26.06
clinicaltrials API v2 /version dataTimestamp CT.gov 2026-09-18T09:00:04
uniprot X-UniProt-Release response header UniProt 2026_03
ensembl REST /info/data Ensembl 116
chembl /chembl/api/data/status ChEMBL_37
string /api/json/version STRING 12.0
reactome ContentService /data/database/version Reactome 97
hgnc /info lastModified HGNC 2026-09-18T12:19:06.043Z
ols OLS4 ontology config.version (mondo, go, hp, …) mondo 2026-09-01
kegg /info/<db> last-update line KEGG pathway 2026/09/17
http-json any JSON endpoint + dotted path whatever you point it at
http-header any endpoint + response header
field a key inside the tool result DepMap 24Q2
static a string in config a pinned local dataset

Every preset above has a live test (tests/test_sources.py) that runs when the network is reachable. Checked and rejected: DepMap (no API — use field), Europe PMC (reports API version, not data release), RCSB PDB, gnomAD.

Resolver plugins

A resolver for a source provmcp doesn't know is a separate package, not a pull request. Declare an entry point in the provmcp.resolvers group:

[project.entry-points."provmcp.resolvers"]
depmap = "provmcp_resolver_depmap:resolve"

where resolve is async (source_options: dict, result: object) -> str | None. pip install it, and source: depmap works in servers.yaml. provmcp resolvers lists everything installed and where it came from; provmcp resolve <name> --opt k=v runs one now. A plugin cannot shadow a built-in name (so a stray install can't change what geo means in someone's ledger), a plugin that fails to import is skipped with a warning, and a plugin that raises yields an unresolved release, never a failed call.

examples/provmcp-resolver-example is a complete, installable plugin in one file; the test suite builds a venv, installs it, and checks it appears.

Resolution is a side channel: it says what the source was serving around the time of the call, not what the downstream server actually used. A server that bundles or caches data can lag the live release. That is a property of the server, and exactly what replay surfaces.

What this is not

Not GxP compliance. Not a validated system. It is provenance capture that supports an audit trail.

The limitation is structural and worth stating plainly. A practitioner systematization of regulated agent deployments argues that auditability is an interface design constraint that must be established before tools are built, not a logging concern addressable at the infrastructure layer — tools built without audit requirements in scope carry no case identifiers or session context in their call signatures, and retrofitting that meant changing every tool's interface contract and revalidating each one.

That critique is correct. provmcp records what is observable at the proxy boundary and nothing the downstream tools never carried. It records which client software made a call, not which person; it cannot vouch for what a downstream tool reported — only that what it reported was recorded faithfully and has not been altered since.

Ledger guarantees

Property Mechanism Test
No entry altered after writing SHA-256 over canonical JSON; chain verified on the recomputed hash so an edit invalidates every later link test_tampering_with_content_is_detected, test_verify_tool_detects_tampering
No entry removed sequence numbers + chain continuity test_removing_an_entry_is_detected
Chain not silently rebuilt Ed25519 signature per entry; a foreign key fails test_bundle_with_foreign_pubkey_fails_signatures
Nothing sensitive on disk hash-only retention by default; full retention is per-server opt-in; named arguments redacted before hashing test_result_is_not_stored_by_default, test_redacted_arguments_never_reach_disk
No unrecorded calls a ledger write failure fails the call with an error the client sees test_ledger_write_failure_fails_the_call
Bundle verifies standalone manifest hashes + chain + bundled public key test_bundle_round_trips, test_bundle_detects_tampering

Signing keys never enter the repo. A missing key fails loudly; --unsigned is an explicit opt-out that still hashes the chain.

There is no update or delete path on the ledger, and there must never be one.

Known weaknesses, stated rather than hidden:

  • The ledger is signed by the thing that wrote it. A signature proves the chain wasn't rebuilt by someone without the key; it proves nothing against the proxy operator. ArkForge answers this with RFC 3161 timestamps and a Sigstore Rekor entry per call; provmcp does not, yet. Treat the signature as tamper-evidence, not non-repudiation.
  • Result hashes are SDK-dependent. The hash covers the Python SDK's JSON rendering of the result. mcp-audit-gateway's differential tests found 26 serialization divergences across 10 SDKs. Replay through provmcp is consistent with itself; comparing a hash against one computed by another implementation is not a supported claim.
  • Fail-closed by choice. A ledger write failure fails the call. auditmcp made the opposite choice (fail-open) for an ops audit log. For provenance, an unrecorded call is worse than a failed one; for uptime, it isn't.

Usage

pip install provmcp                    # or: git clone … && uv pip install -e ".[dev]"
provmcp keygen --out ~/.provmcp/
export PROVMCP_SIGNING_KEY=~/.provmcp/signing.pem

cp servers.example.yaml servers.yaml   # edit
provmcp serve --config servers.yaml                                  # stdio
provmcp serve --config servers.yaml --transport streamable-http --port 8080

provmcp verify ./provenance.jsonl --pubkey ~/.provmcp/pub.pem
provmcp replay ./provenance.jsonl --config servers.yaml [--json]
provmcp export ./provenance.jsonl --out ./bundle/
provmcp verify-bundle ./bundle/        # from any machine, no key needed

Point your MCP client at provmcp instead of the downstream servers. Tools appear as <server_id>__<tool> with their original schemas. The proxy adds three of its own: provmcp__verify, provmcp__status (live releases per server), and provmcp__export. Those are not recorded; they are questions about the record.

A Claude Desktop / Claude Code entry:

{
  "mcpServers": {
    "provmcp": {
      "command": "provmcp",
      "args": ["serve", "--config", "/abs/path/servers.yaml"],
      "env": {"PROVMCP_SIGNING_KEY": "/abs/path/.provmcp/signing.pem"}
    }
  }
}

The bundle

bundle/
  provenance.jsonl   the ledger, verbatim
  provenance.json    W3C PROV-JSON — loads with the `prov` library; convert to
                     PROV-N, PROV-O (RDF), or PROV-XML from there
  manifest.json      chain head, entry count, file hashes, provmcp version
  PUBKEY.pem         Ed25519 public key, so a third party can verify signatures

PROV mapping: each call is an Activity associated with the downstream server, the client, and provmcp (SoftwareAgents); it used its arguments and the source release (Entitys) and generated the result, an Entity identified by its content hash so identical results share a node. wasInformedBy links each call to the previous one, so ledger order survives in the graph.

Protocol notes

Built on mcp 2.2, which implements MCP revision 2026-07-28. That revision made the protocol stateless: no initialize handshake, no Mcp-Session-Id, no standalone GET stream (the proxy answers GET with 405). With nothing to pin, the proxy holds the audit chain and nothing else.

The legacy handshake is still accepted on both sides — the proxy serves handshake-era clients, and connects to handshake-era servers (which, today, is most of them: geo-mcp 0.1.2 speaks 2025-11-25 and crashes outright on the 2.x SDK). Tests cover both eras.

Deployment

stdio is the default and the right choice for one person's work: the proxy runs as a child of the client, the ledger is a local file, the key is a local file.

Streamable HTTP is stateless per request, so any replica can serve any call. The one shared thing is the ledger. One process per ledger file is the current constraint: appends are serialized in-process and the chain head is cached in memory, so two replicas writing the same file would fork the chain. Run one instance, or one instance per ledger. Multi-writer (a database-backed ledger with an atomic head) is out of scope for now.

There is no auth. Do not expose the HTTP transport beyond localhost or a private network; the SDK's DNS-rebinding protection is on for localhost binds.

License

MIT.

Release files for provmcp 0.1.0

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

Source distribution (sdist)

Source distribution for provmcp 0.1.0
File Size Uploaded
provmcp-0.1.0.tar.gz 91.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for provmcp 0.1.0
File Interpreter ABI Platform
provmcp-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 123.9 kB

Release files / provmcp-0.1.0.tar.gz

Download URL provmcp-0.1.0.tar.gz
Size 91.0 kB
Tags Source
SHA-256 checksum
How to use checksums
e104eeacdbae9bf7b77f2817c23dc3bbac5ee43b194bba07d6fba238d9ea326c
BLAKE2b-256 checksum
How to use checksums
aceb897da3e08fdd27bcbca4d7e0b6d8f51f56ef0c8b7deceb12ed423ef58032
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 Sep 20, 2026.

Transparency log

Release files / provmcp-0.1.0-py3-none-any.whl

Download URL provmcp-0.1.0-py3-none-any.whl
Size 32.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
baae944c7207fa336d7949093007e11f6df5b5c5282b49cb50cf6aae6bcabc5d
BLAKE2b-256 checksum
How to use checksums
0d435ee2151652d57d84298a22ad710ae5df65aaf455e1d0ff244e2ca59a12d2
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 Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

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