provmcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@provmcpreplay ./provenance.jsonl and show what changed"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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'sgeo-mcpon 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.
Related MCP server: tatastu-proof
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 otherSigned, 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:
| Where the release comes from | Example (2026-09-20) |
| NCBI E-utilities |
|
| same, any NCBI db ( |
|
| GraphQL |
|
| API v2 |
|
|
|
|
| REST |
|
|
|
|
|
|
|
| ContentService |
|
|
|
|
| OLS4 ontology |
|
|
|
|
| any JSON endpoint + dotted path | whatever you point it at |
| any endpoint + response header | |
| a key inside the tool result |
|
| 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 |
|
No entry removed | sequence numbers + chain continuity |
|
Chain not silently rebuilt | Ed25519 signature per entry; a foreign key fails |
|
Nothing sensitive on disk | hash-only retention by default; full retention is per-server opt-in; named arguments redacted before hashing |
|
No unrecorded calls | a ledger write failure fails the call with an error the client sees |
|
Bundle verifies standalone | manifest hashes + chain + bundled public key |
|
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 neededPoint 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 signaturesPROV 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.
This server cannot be deployed
Maintenance
Related MCP Connectors
Tamper-evident proof creation and verification for AI agents via MCP, A2A, and REST.
Issue & verify signed (ed25519), hash-chained, timestamped provenance receipts for agent actions.
Governed MCP: agent audit, provenance, deterministic checks, and receipt-backed FragGate execution.
Hash-chained HMAC-signed audit log MCP for A2A (agent-to-agent) calls. Every tool-call, agent-ha...
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceEnables users to define and run MCP tools using declarative YAML configs with built-in trust enforcement, credential brokering, and tamper-evident audit logging.14MIT
- AlicenseNot gradedqualityBmaintenanceEnables creating and verifying permanent, publicly verifiable content provenance stamps via MCP tools.8 npmMIT
- AlicenseAqualityBmaintenanceProvides tamper-evident, hash-chained logging for MCP agents, enabling append, verification, and artefact-binding of action records.5259 PyPIMIT
- AlicenseBqualityBmaintenancePrivacy-first, tamper-evident receipts for MCP tool calls. It hashes and signs arguments/results so you can verify what ran, when, without storing raw payloads.2MIT