Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-pens-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atrib/mcp-wrap": patch
---

Run the filesystem smoke through the stable MCP 2026-07-28 client.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
165 changes: 165 additions & 0 deletions docs/stateless-mcp-attribution-guide.md
Original file line number Diff line number Diff line change
@@ -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).
12 changes: 6 additions & 6 deletions docs/stateless-mcp-v2-follow-through.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 11 additions & 3 deletions packages/mcp-wrap/examples/filesystem-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -148,7 +148,10 @@ async function main(): Promise<void> {
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 })
Expand Down Expand Up @@ -190,6 +193,11 @@ async function main(): Promise<void> {

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')) {
Expand Down