One MCP call. One local receipt.
Doorstep passes an ordinary stdio MCP server through Bulla byte-for-byte and records completed tool calls locally. The server does not need to change.
Install Bulla 0.49.2
$ python -m pip install "bulla==0.49.2"Doorstep is the bulla capture command, not a separate service or protocol. It creates no account, daemon, network dependency, or default signing key.
Package version used on this page
The command shown here comes from the package published on PyPI, not from unreleased repository code.
Configure the wrapper once
Keep your existing MCP server command after --. Point --session-root at a private absolute path. Each server lifecycle receives a distinct session directory, so the same client configuration can remain in place across restarts.
{
"mcpServers": {
"your-server": {
"command": "bulla",
"args": [
"capture",
"mcp",
"--session-root",
"/absolute/path/to/bulla-captures",
"--",
"python",
"-m",
"your_mcp_server"
]
}
}
}The upstream client still performs the MCP handshake. Bulla forwards stdin, stdout, and server stderr without injecting tools, renaming methods, or rewriting JSON-RPC IDs. A complete client-originated tools/call and its matching response produce one ActionReceipt.
Privacy default: commitments only.
Request and response payloads are not retained unless you explicitly add --retain-payloads. That option stores the exact frames locally and can preserve prompts, credentials, tool arguments, and results. Review the session-root location and its access controls before enabling it.
Check the capture locally
$ bulla capture check /absolute/path/to/bulla-captures --show-receipts
$ bulla receipt verify /absolute/path/to/receipt.jsoncapture check inventories every session, verifies receipts, and recomputes frame commitments when retained sidecars exist. receipt verify checks one ActionReceipt. An unsigned receipt reaches the digest rung; a receipt created with --key FILE can authenticate the local observer's statement.
What the receipt carries
These are the central fields from one commitments-only unsigned receipt. The full record also contains its canonical hashes, receiver-local event identifier, completion time, and producer metadata.
{
"schema_version": "0.4",
"kind": "action_receipt",
"action": {
"type": "mcp.tools.call.observed",
"subject": {
"transport": "mcp-stdio-jsonrpc",
"tool_name": "demo.echo",
"request_frame_sha256": "sha256:8da4f7118b25956f40db428961733f1f9e3bbc08968e704fea60c959aa605096",
"response_frame_sha256": "sha256:091265a2d0fefb624c26991ed8758035003c21cfbafb13ef2b31a1ec0a00dbbe",
"response_kind": "result"
}
},
"evidence_refs": [
{
"name": "mcp.request.frame",
"hash": "sha256:8da4f7118b25956f40db428961733f1f9e3bbc08968e704fea60c959aa605096",
"grounding": "self_asserted"
},
{
"name": "mcp.response.frame",
"hash": "sha256:091265a2d0fefb624c26991ed8758035003c21cfbafb13ef2b31a1ec0a00dbbe",
"grounding": "self_asserted"
}
],
"retention": {
"record": "operational",
"disclosure": "party"
},
"signature": null
}The frame hashes commit the exact newline-terminated bytes that crossed the wrapper. With commitments-only capture, those bytes are not present for an independent recheck.
What this establishes
| Claim | Status |
|---|---|
| Canonical structure and internal consistency | Checkable |
| Request/response commitments carried | Yes |
| Underlying bytes match commitments | Only with retained sidecars |
| Call correlation | Recorded by local observer |
| Observer statement authenticated | Only when signed |
| MCP server authenticated | No |
| Tool execution proven | No |
| Result correctness established | No |
A signature authenticates only the local observer's statement. It does not authenticate the MCP server, prove that a tool executed, establish result correctness, or turn a local correlation into an independent observation.
Continue with the ActionReceipt specification or the published CLI reference.