From db067b7faddcf44dc98dbee49ec33b37f70b3e28 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Wed, 29 Jul 2026 01:23:06 -0500 Subject: [PATCH] docs: map stateless MCP attribution paths --- .changeset/warm-pens-smoke.md | 5 + CLAUDE.md | 1 + README.md | 14 ++ docs/stateless-mcp-attribution-guide.md | 165 ++++++++++++++++++ docs/stateless-mcp-v2-follow-through.md | 12 +- .../mcp-wrap/examples/filesystem-smoke.ts | 14 +- 6 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 .changeset/warm-pens-smoke.md create mode 100644 docs/stateless-mcp-attribution-guide.md diff --git a/.changeset/warm-pens-smoke.md b/.changeset/warm-pens-smoke.md new file mode 100644 index 00000000..e316aef8 --- /dev/null +++ b/.changeset/warm-pens-smoke.md @@ -0,0 +1,5 @@ +--- +"@atrib/mcp-wrap": patch +--- + +Run the filesystem smoke through the stable MCP 2026-07-28 client. diff --git a/CLAUDE.md b/CLAUDE.md index 41915a56..9f195455 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,7 @@ atrib/ docs/evidence-profiles/ # ยง5.5.7 (D137) profile documents for the thirteen atrib-maintained evidence-envelope profiles, including Nostr and Buzz event evidence per D179. docs/extensions/dev.atrib-attribution/ # dev.atrib/attribution MCP extension specification (P049): v0.1 extension document (identifier, capability settings, prefixed _meta carriage, attestation receipts, negotiation, degradation, versioning per SEP-2133). docs/website-redesign-relay.md # Relay brief for the website-overhaul session: approved P042-P050 facts that change public language, vocabulary table, embargoes/timeline, and the governance rules copy changes are bound by. + docs/stateless-mcp-attribution-guide.md # Developer guide for native servers, wrappers, SDK clients, request receipts, retry behavior, and independent verification under stateless MCP. docs/ots-pending-receipt-worker.md # Host-owned OTS pending-receipt worker contract and a sample launchd schedule. It never installs a LaunchAgent. docs/attest-recall-rename-impact.md # Blast-radius catalog for the attest/recall verb rename (executed as D164): npm packages, MCP tool names, persisted producer labels, signed-bytes analysis, operator machine state, and doc/skill surfaces. docs/traces-integration-research.md # Research doc (2026-07-14, not an ADR): agent-trace landscape across harnesses, the four objects called "trace", overlap and difference with the D121/D122 runtime-log layer, dogfood trace-surface inventory, and decision candidates (session-transcript RuntimeLogSource adapter recommended; transcript recall corpus held for its own ADR). diff --git a/README.md b/README.md index 5b763bdb..99001816 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,20 @@ second `tool_call` record. The private integration package pins this rule in [`packages/integration/src/host-runtime-proof.ts`](packages/integration/src/host-runtime-proof.ts) so future OpenClaw, Hermes, and other host proofs share the same vocabulary. +## Stateless MCP attribution + +Add verifiable records to a tool call without adopting a new agent runtime or +maintaining an atrib transport session. A native server can advertise +`dev.atrib/attribution`; an existing server can sit behind `@atrib/mcp-wrap`; +and the TypeScript and Python SDKs carry the complete request envelope +automatically. The tool result can include a signed receipt that a verifier +checks without trusting atrib's hosted service. + +The [stateless MCP attribution guide](docs/stateless-mcp-attribution-guide.md) +shows the request, result, receipt, integration choices, runnable proofs, retry +rules, and the boundary between proof of a signed claim and proof that the +claim is true. + ## How it works - Each record is signed by the actor's Ed25519 key and JCS-canonicalized diff --git a/docs/stateless-mcp-attribution-guide.md b/docs/stateless-mcp-attribution-guide.md new file mode 100644 index 00000000..d94187a7 --- /dev/null +++ b/docs/stateless-mcp-attribution-guide.md @@ -0,0 +1,165 @@ +# Add verifiable records to stateless MCP calls + +An atrib MCP server is a request-shaped proof service, not a +conversation-shaped process. A complete request carries its own protocol +version, client capabilities, attribution context, trace context, and write +retry identity. The server can restart between calls without requiring the +client to repair a transport session. + +This model lets an existing tool call gain a signed result receipt without +moving the agent loop into a new runtime: + +1. The client sends an ordinary MCP tool request with + `dev.atrib/attribution` in request metadata. +2. The server runs the tool and signs the selected action facts. +3. The result carries the tool content plus a machine-readable receipt. +4. Any verifier can check the attached record locally with the creator's + Ed25519 public key. + +The signed record is durable application state. Stateless transport means the +server does not depend on a prior connection, initialize exchange, or session +header. It does not mean the action has no history. + +## Run the proofs + +From this repository: + +```bash +# build atribd and its workspace dependency closure +pnpm --filter '@atrib/daemon...' build + +# atribd through the stable MCP 2026-07-28 client +pnpm --filter @atrib/daemon test -- -t \ + "negotiates and lists tools with the stable v2 client" + +# atrib's TypeScript client through an independent Python server +pnpm --filter @atrib/sdk test -- independent-mcp-server.test.ts + +# the standard-library Python client request and receipt path +python -m pip install -e 'python[dev]' +python -m pytest python/tests/test_mcp_client.py + +# any upstream MCP server through the wrapper boundary +pnpm --filter @atrib/mcp-wrap smoke:filesystem +``` + +The independent server imports no atrib package and no MCP package. The test +verifies discovery, request negotiation, the propagation token, record hash, +creator key, context id, chain root, and Ed25519 signature. The wrapper smoke +starts a real filesystem MCP server and checks that its normal result still +arrives with a signed mirror record. + +The language-neutral request, receipt, degradation, and negative fixtures live +under +[`spec/conformance/mcp-extension/`](../spec/conformance/mcp-extension/). +The extension contract and a standalone server are in +[`docs/extensions/dev.atrib-attribution/`](extensions/dev.atrib-attribution/). + +## Native server path + +Use the native path when you own the MCP server. Advertise +`dev.atrib/attribution` in `server/discover`, read its request metadata on each +call, and emit a receipt only when that request negotiated the extension. +[`@atrib/mcp`](../packages/mcp/README.md) supplies the signing middleware and +receipt helpers. + +The request must be complete at the boundary: + +```json +{ + "jsonrpc": "2.0", + "id": 7, + "method": "tools/call", + "params": { + "name": "charge_card", + "arguments": { "invoice_id": "inv-7" } + }, + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "billing-agent", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": { + "extensions": { + "dev.atrib/attribution": { + "version": "0.1", + "accept": ["token", "record"] + } + } + }, + "dev.atrib/attribution": { + "context_id": "0123456789abcdef0123456789abcdef" + }, + "dev.atrib/idempotencyKey": "charge-inv-7-attempt-1" + } +} +``` + +The exact wire schema is defined by the +[`dev.atrib/attribution` v0.1 document](extensions/dev.atrib-attribution/v0.1.md). +Use a stable action-bound idempotency key when retrying a write after a timeout +or disconnect. Reusing a key with changed arguments is an error. + +## Existing server path + +Use [`@atrib/mcp-wrap`](../packages/mcp-wrap/README.md) when you do not own the +upstream server. Point the host at `atrib-wrap` instead of the upstream binary. +The agent loop and tool schema stay unchanged. The wrapper exposes a native +MCP 2026-07-28 boundary, signs request and outcome commitments, and can talk to +an older upstream MCP implementation behind that boundary. + +The outer boundary can therefore be native v2 while the wrapped dependency +remains a compatibility component. Operational reports must keep those two +facts separate. + +## Client SDK path + +Use [`@atrib/sdk`](../packages/sdk/README.md) or the +[`atrib` Python SDK](../python/README.md) for daemon-backed `attest` and +`recall`. Both clients send the full stateless metadata envelope by default, +negotiate attribution receipts, expose the observed transport facts, and use +the same write idempotency model. + +Application code that owns a non-MCP execution boundary can use the TypeScript +SDK's `action()` helper. It signs a request, executes the existing function, +then signs a linked success or failure outcome. It does not replace or own the +agent loop. + +## Integration map + +| Existing system | Integration point | Runnable proof | +| --- | --- | --- | +| Tool gateway | Native `dev.atrib/attribution` server capability or an `@atrib/mcp-wrap` outer boundary | `pnpm --filter @atrib/mcp-wrap smoke:filesystem` | +| Agent framework | `@atrib/agent` callback middleware, `@atrib/openinference` span intake, or MCP middleware | The framework examples cataloged in the [root README](../README.md#examples-and-proofs) | +| Cloud agent | The same request metadata and receipt contract over a hosted Streamable HTTP endpoint | [`packages/integration/examples/cloudflare-agents/`](../packages/integration/examples/cloudflare-agents/) | +| Commerce system | Signed tool request and outcome plus payment or authorization evidence selected by the host | [`docs/payments-profile.md`](payments-profile.md) and the x402/AP2 examples in the [root README](../README.md#examples-and-proofs) | +| Audit system | Store or forward the receipt, then verify the record and any disclosed evidence independently | `pnpm --filter @atrib/sdk test -- independent-mcp-server.test.ts` | + +## What the receipt proves + +A valid receipt proves that the holder of the named creator key signed the +attached record bytes and that the record commits to the disclosed action +facts. A log inclusion proof can also prove that the commitment was accepted at +a particular log position. + +The receipt alone does not prove that a tool result is true, that a side effect +occurred, that the signer was authorized, or that a real-world counterparty +agreed. Those claims need the relevant host, authorization, tool-side, +counterparty, or transparency-log evidence. atrib keeps those evidence checks +separate so a valid signature cannot be mistaken for independent truth. + +## Operations + +- Retry reads as complete new requests. +- Retry writes with the same complete arguments and idempotency key. +- Treat an indeterminate write as unresolved until the same key returns a + completed result. +- Cache `tools/list` only for its advertised `ttlMs` and `cacheScope`. +- Diagnose protocol version, request metadata, routing headers, result metadata, + and daemon health. Do not repair a removed transport session. +- Keep signing keys, mirrors, idempotency state, and authorization policy + isolated by operator profile. + +The detailed daemon evidence packet and restart procedure are in the +[`atribd` runbook](../services/atribd/README.md#request-diagnosis). diff --git a/docs/stateless-mcp-v2-follow-through.md b/docs/stateless-mcp-v2-follow-through.md index 371a9e9c..18737993 100644 --- a/docs/stateless-mcp-v2-follow-through.md +++ b/docs/stateless-mcp-v2-follow-through.md @@ -393,12 +393,12 @@ The follow-through program is complete when: | 2B: independent interoperability | complete | Python's standard-library client interoperates with atribd. A separate stdlib-only Python server imports no atrib or MCP package and is consumed by the TypeScript SDK, which verifies modern negotiation plus token, record hash, creator key, context, chain root, and Ed25519 signature against the language-neutral corpus. | | 2C: human-facing propagation | complete | The daemon runbook diagnoses bounded requests, separates read and write retries, covers restart recovery, and states the wrapper boundary. The spec now distinguishes application-owned agent continuity from removed MCP transport sessions; the payments profile no longer frames duplicate prevention as one transaction per transport session. | | 3A: daemon consolidation study | complete | [D187](../DECISIONS.md#d187-keep-one-atribd-daemon-per-operator-profile) keeps one daemon per profile. Live LaunchAgents have distinct ports, agent identities, mirrors, autochain sources, and coordinator routes; atribd has no authenticated complete-profile selector or fairness scheduler. | -| 3B: product propagation | deferred | Begins after released SDK and interop proof | +| 3B: product propagation | complete | The root README and stateless MCP attribution guide map the native server, wrapper, TypeScript, Python, gateway, framework, cloud-agent, commerce, and audit paths to runnable proofs. The guide shows request, result, receipt, retry rules, durable-state framing, independent verification, and the signed-claim truth boundary. | ## Immediate next slice -Release Tranches 0B, 1A, 1B, and 1C. Install the released daemon and SDKs on -the three operator profiles. Confirm modern traffic with no post-modern legacy -regression, then run the duplicate-write and cancellation proof matrix against -the installed daemon. Begin the restart, concurrency, and performance tranche -from that released baseline. +Merge the completed follow-through tranches, publish the daemon, MCP, wrapper, +and SDK packages plus the Python distribution, then install the released +daemon on the three operator profiles. Confirm modern traffic with no +post-modern legacy regression, then run the duplicate-write, cancellation, +restart, and receipt proofs against the installed daemon. diff --git a/packages/mcp-wrap/examples/filesystem-smoke.ts b/packages/mcp-wrap/examples/filesystem-smoke.ts index 49009326..97bbc3b5 100644 --- a/packages/mcp-wrap/examples/filesystem-smoke.ts +++ b/packages/mcp-wrap/examples/filesystem-smoke.ts @@ -13,8 +13,8 @@ import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { canonicalRecord, hexEncode, sha256, type AtribRecord } from '@atrib/mcp' -import { Client } from '@modelcontextprotocol/sdk/client/index.js' -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { Client } from '@modelcontextprotocol/client' +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio' const EVENT_TYPE_TOOL_CALL = 'https://atrib.dev/v1/types/tool_call' @@ -148,7 +148,10 @@ async function main(): Promise { const recordFile = join(tempDir, 'records.jsonl') const logFile = join(tempDir, 'wrapper.log') const localLog = await startLocalLog() - const client = new Client({ name: 'mcp-wrap-smoke-host', version: '0.1.0' }) + const client = new Client( + { name: 'mcp-wrap-smoke-host', version: '0.1.0' }, + { versionNegotiation: { mode: { pin: '2026-07-28' } } }, + ) try { mkdirSync(fixtureDir, { recursive: true }) @@ -190,6 +193,11 @@ async function main(): Promise { try { await client.connect(transport) + if (client.getNegotiatedProtocolVersion() !== '2026-07-28') { + throw new Error( + `expected MCP 2026-07-28, got ${client.getNegotiatedProtocolVersion() ?? 'none'}`, + ) + } const tools = await client.listTools() const toolNames = new Set(tools.tools.map((tool) => tool.name)) if (!toolNames.has('read_file')) {