Repository navigation
MCP Proxy
witness-log-mcp-proxy sits between an MCP host and a stdio MCP server
and records every tool call in a witness-log journal. The server is
unchanged, the host's configuration changes by one line, and the model is
not asked to do anything. It ships in the witness-log Python package
(no extra: stdlib only).
pip install witness-log
witness-log-mcp-proxy --name git --journal ./git.wlog -- mcp-server-gitPut that where the host had mcp-server-git:
{"mcpServers": {"git": {
"command": "witness-log-mcp-proxy",
"args": ["--name", "git", "--journal", "/var/lib/witness-log/git.wlog", "--", "mcp-server-git"]}}}Anything the host sends arrives at the server unchanged and anything the server answers arrives at the host unchanged, with three exceptions described below (two injected tools, and errors the proxy itself issues on timeout or crash).
For each tools/call request the proxy appends an intent leaf before
forwarding it and a result leaf when the answer arrives. The leaves
carry hashes, not content: the host keeps its own transcript, and the
hashes bind that transcript to a signed, witnessable tree. Profile string:
"mcp.tools/call v1".
intent payload = canonical_json({
"profile": "mcp.tools/call v1",
"server": <--name>,
"tool": <params.name>,
"args_sha256": hex(SHA-256(canonical_json(params.arguments or {})))
})
result payload = canonical_json({
"profile": "mcp.tools/call v1",
"server": <--name>,
"tool": <params.name>,
"intent_index": <index of the intent leaf>,
"result_sha256": hex(SHA-256(canonical_json(X))),
"is_error": <bool>
})
where canonical_json is RFC 8785 (JCS; witness_log.canonical_json),
and X is:
| case | X |
is_error |
|---|---|---|
| success response |
response.result (the whole CallToolResult object) |
result.isError == true |
| JSON-RPC error response | {"error": response.error} |
true |
| proxy timeout | {"error": {"code": -32000, "message": "witness-log-mcp-proxy: timed out after <N>s"}} |
true |
| server exited mid-call | {"error": {"code": -32000, "message": "witness-log-mcp-proxy: server exited with code <c> during <tool>"}} |
true |
Each payload is then committed as SHA256(bucket_pad(payload) || salt || index) with a fresh salt (SPEC §8), so log.forget(i) can erase one and
log.disclose(i) can reveal one. To check a recorded call against a
transcript: rebuild the payload above from the transcript's request and
response, then witness_log.verify_disclosure(leaf, payload, salt).
Everything other than tools/call (initialize, tools/list,
resources/*, prompts/*, notifications, server-initiated requests) is
passed through without a leaf.
The proxy appends two tools to every tools/list response and answers
them itself; the server never sees them.
| tool | returns |
|---|---|
audit__status |
{server, journal, leaf_count, root, identity, calls, restarts, witnessed, last_verdict} |
audit__export |
writes <journal>.dossier.json and returns {path, sha256, bytes, verdict}; the verdict is unanchored unless --directory was given (a verifier needs a directory it trusts) |
A model can call them like any other tool; so can an operator with
witness-log verify <journal>.dossier.json --identity <identity> --directory trusted.json.
-
--timeout N(default 60 s): a call unanswered afterNseconds gets a JSON-RPC error at the host and anis_errorresult leaf; a late answer from the server is dropped. - If the server exits, every in-flight call fails the same way, the
server is restarted (
--max-restarts, default 3) and the host'sinitializerequest andnotifications/initializedare replayed to it. The new server'sinitializeresponse is not forwarded. Past the limit the proxy exits with the server's code and the host sees the pipe close. -
--directory trusted.json --submit-every Nsubmits the journal to those witnesses everyNcompleted calls and once more when the host closes stdin;audit__status.last_verdictreports the outcome.witness_log.openrules apply:WITNESS_LOG_IDENTITY_SEED_ENVseals the journal, one process per journal. - Newline-delimited JSON-RPC over stdio only. HTTP transports are not proxied; put the proxy in front of a stdio server, or use the MCP server tools instead.
The MCP server gives the model audit tools: it
decides what to record and can record intent in its own words, handoffs
and authorizations. The proxy records what happened at the protocol
boundary, whether or not the model cooperates, and cannot see anything
the server does outside tools/call. They compose: a host can run the
proxy around each server and the witness-log server alongside.
Start
- Home
- Why witness-log
- Concepts
-
Comparison
- Tessera · Microsoft AGT · Signet · immudb · Agent Receipts · SCITT
Use it
- Quickstart: Python
- Quickstart: TypeScript
- Quickstart: Rust
- Quickstart: CLI
- CLI reference
- MCP integration
- Framework guides
- Quickstart: Go
- Browser verifier
- Other languages
Tutorials
Understand it
- Architecture
- Design decisions
- How a leaf becomes a proof
- Witness protocol walkthrough
- Redaction explained
- Keys and identity
Trust it
- Verification and verdicts
- Getting witnessed
- Operator onboarding
- Witness status
- Running a witness
- Compliance
- Threat model
- Spec and interop
- SCITT mapping
Run it
Project