Skip to content

Commit d485f4d

Browse files
committed
feat: add verifiable MCP audit interceptor proof
1 parent fbcb37a commit d485f4d

9 files changed

Lines changed: 649 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ atrib/
111111
paid-x402-action-gate/ # Local Cloudflare x402 paid-agent proof with Action Gate policy and hash-only lifecycle facts.
112112
x402-path-b-reference/ # Local x402 v2 Path B reference: 402 challenge, paid retry context propagation, PAYMENT-RESPONSE detection, agent-side transaction emission, and counterparty attestation.
113113
oauth-evidence-infra/ # Cloudflare Worker + Durable Object reference for DPoP replay cache and OAuth introspection evidence.
114+
mcp-interceptor-audit/ # Experimental MCP interceptor proof: exact request/response pairing, hash-only receipt, private sidecar.
114115
vercel-ai-sdk/ # createMCPClient + AI Gateway example
115116
langchain-js/ # MultiServerMCPClient + loadMcpTools example
116117
langgraph-store/ # LangGraph JS BaseStore memory proof with hash-only atrib records.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ your stack.
255255
| Vercel AI SDK + AI Gateway | [`packages/integration/examples/vercel-ai-sdk/`](packages/integration/examples/vercel-ai-sdk/) |
256256
| Claude Agent SDK (Case A + Case B) | [`packages/integration/examples/claude-agent-sdk/`](packages/integration/examples/claude-agent-sdk/) |
257257
| Cloudflare Agents | [`packages/integration/examples/cloudflare-agents/`](packages/integration/examples/cloudflare-agents/) including [`live-worker-proof`](packages/integration/examples/cloudflare-agents/live-worker-proof/), [`live-client-proof`](packages/integration/examples/cloudflare-agents/live-client-proof/), the interactive [`approval-trace`](packages/integration/examples/cloudflare-agents/approval-trace/) HITL example, the [`paid-x402-action-gate`](packages/integration/examples/cloudflare-agents/paid-x402-action-gate/) proof, the [`x402-path-b-reference`](packages/integration/examples/cloudflare-agents/x402-path-b-reference/) proof, and the [D111](DECISIONS.md#d111-host-owned-oauth-evidence-infrastructure) [`oauth-evidence-infra`](packages/integration/examples/cloudflare-agents/oauth-evidence-infra/) Worker reference |
258+
| MCP verifiable audit interceptor | [`packages/integration/examples/mcp-interceptor-audit/`](packages/integration/examples/mcp-interceptor-audit/) runs the Interceptors WG draft methods through a real MCP client and server. It emits a signed hash-only receipt for exactly paired request and response phases and emits no receipt when the operation identity is missing. |
258259
| Cloudflare x402 paid agent proof | [`packages/integration/examples/cloudflare-agents/paid-x402-action-gate/`](packages/integration/examples/cloudflare-agents/paid-x402-action-gate/) and [`proof-packets/cloudflare-x402-paid-agent/`](proof-packets/cloudflare-x402-paid-agent/) show a paid MCP request gated by `@atrib/action-gate`, with hash-only x402 lifecycle facts bound to the decision and outcome. |
259260
| LangChain JS | [`packages/integration/examples/langchain-js/`](packages/integration/examples/langchain-js/) |
260261
| LangGraph Store memory | [`packages/integration/examples/langgraph-store/`](packages/integration/examples/langgraph-store/) |

packages/integration/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Every signature, every chain hash, and every transaction event in that output is
7878
| **End-to-end demo** | [`examples/end-to-end/`](examples/end-to-end/) | All moving parts in a single process: dev log + merchant + agent + payment + visualizer. Run with `pnpm demo`. |
7979
| **Claude Agent SDK** | [`examples/claude-agent-sdk/`](examples/claude-agent-sdk/) | Both Case A (in-process tools. wrap the SDK's `McpServer` with `atrib()`) and Case B (third-party MCP servers; proxy via `createAtribProxy`). |
8080
| **Cloudflare Agents** | [`examples/cloudflare-agents/`](examples/cloudflare-agents/) | Both surfaces: server-side `McpAgent` (Surface 1) and client-side `Agent` calling upstream MCP servers (Surface 2), with live Worker proofs, an interactive HITL approval-trace example, a paid x402 Action Gate proof, and a [D111](../../DECISIONS.md#d111-host-owned-oauth-evidence-infrastructure) OAuth evidence infrastructure Worker reference. |
81+
| **MCP verifiable audit interceptor** | [`examples/mcp-interceptor-audit/`](examples/mcp-interceptor-audit/) | A real `@modelcontextprotocol/sdk` client and server use the experimental `interceptors/list` and `interceptor/invoke` methods. Matching request and response phases produce one signed hash-only receipt; missing or reused operation identities stay unpaired. |
8182
| **Vercel AI SDK + AI Gateway** | [`examples/vercel-ai-sdk/`](examples/vercel-ai-sdk/) | Vercel AI SDK with MCP tools, routed through the AI Gateway (recommended pattern for model fallback + observability). |
8283
| **LangChain JS** | [`examples/langchain-js/`](examples/langchain-js/) | `MultiServerMCPClient` patched in-place by `attributeLangchainMcp` so every server it manages emits attributed records. including forked clients used for per-call header workflows. |
8384
| **LangGraph Store memory** | [`examples/langgraph-store/`](examples/langgraph-store/) | A real `@langchain/langgraph` `entrypoint` receives an attributed `InMemoryStore`. LangGraph routes workflow memory calls through `BaseStore.batch`, public records stay hash-only, and local sidecars keep the underlying put/get/search payloads. |
@@ -368,6 +369,7 @@ Run with `pnpm --filter @atrib/integration test`. Focused cross-package tests in
368369
- **`test/mcp-oauth-evidence.test.ts`** (1 test), producer-to-verifier MCP/OAuth evidence coverage. It proves `@atrib/mcp` captures local-only authorization evidence from validated MCP auth metadata, omits raw bearer tokens, preserves DPoP proof material, and feeds `verifyRecord()` generic `evidence[]` without changing the signed record validity bit.
369370
- **`test/mcp-oauth-archive-e2e.test.ts`** (2 tests), archive-backed MCP/OAuth evidence coverage. It proves producer-captured evidence and host-owned introspection evidence can be archived as explorer-ready public evidence projections without storing raw bearer tokens.
370371
- **`test/mcp-oauth-live-archive.test.ts`** (1 test), local coverage for the opt-in public fixture helper. It proves the same helper can submit to a log, archive selected OAuth evidence, fetch the evidence projection, and return a stable explorer action URL.
372+
- **`test/mcp-interceptor-audit.test.ts`** (4 tests), experimental Interceptors WG wire coverage. It runs a real MCP client and server, signs a receipt only after exact request and response pairing, keeps tool payloads in a private sidecar, leaves missing or conflicting operation identities unpaired, and rejects undeclared interceptor names or lifecycle events.
371373
- **`test/langgraph-store-attribution.test.ts`** (6 tests), LangGraph Store memory coverage. It proves direct `InMemoryStore` put/get/search/delete/list calls preserve store values, emit signed hash-only records, keep raw memory text in local sidecars, and chain records in one context.
372374
- **`test/langgraph-python-checkpointer.test.ts`** (opt-in), LangGraph Python checkpointer coverage. When `ATRIB_RUN_LANGGRAPH_PYTHON_CHECKPOINTER_SMOKE=1` is set, it imports `langgraph==1.2.4` through `uv`, runs a real Python `StateGraph` with `InMemorySaver`, then verifies nine hash-only atrib records for `get_tuple`, `put`, and `put_writes` checkpointer events.
373375
- **`test/llamaindex-memory-attribution.test.ts`** (6 tests), LlamaIndex.TS memory coverage. It proves real `createMemory()` add/get/getLLM/snapshot calls preserve memory values, emit signed hash-only records, keep raw memory text in local sidecars, and chain records in one context.

packages/integration/demo-record-surfaces.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,13 @@
427427
"record_class": "public-proof",
428428
"endpoint_posture": "public-write",
429429
"commands": ["mcp-oauth-live-archive"]
430+
},
431+
{
432+
"id": "mcp-interceptor-audit",
433+
"path": "examples/mcp-interceptor-audit",
434+
"record_class": "offline-local",
435+
"endpoint_posture": "local-only",
436+
"commands": ["mcp-interceptor-audit"]
430437
}
431438
]
432439
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# MCP verifiable audit interceptor
2+
3+
This example implements the current Interceptors WG draft through a real
4+
`@modelcontextprotocol/sdk` client and server. It advertises the
5+
`io.modelcontextprotocol/interceptors` capability, responds to
6+
`interceptors/list`, and handles request and response phases through
7+
`interceptor/invoke`.
8+
9+
The interceptor keeps the request body private until the matching response
10+
arrives. It then signs one atrib `tool_call` record with request and result
11+
commitments and returns a `dev.atrib/attribution` receipt in
12+
`ValidationResult.info`. The full payloads stay in a local sidecar.
13+
14+
```bash
15+
pnpm --filter @atrib/integration mcp-interceptor-audit
16+
```
17+
18+
The smoke also sends a response without `spanId`. The draft exposes optional
19+
`traceId` and `spanId` fields, but it does not require a stable operation
20+
identity across phases. The interceptor reports that response as unpaired and
21+
does not emit a receipt. This avoids claiming a request-to-outcome relationship
22+
that the invoker did not identify.
23+
24+
This example is implementation feedback for the experimental interceptor
25+
surface. It is not a claim that SEP-2624 has been accepted. The choice to carry
26+
the receipt under `ValidationResult.info["dev.atrib/attribution"]` is also a
27+
candidate integration point, not an MCP standard.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
import { runMcpInterceptorAuditProof } from '../../src/mcp-interceptor-audit.js'
4+
5+
const result = await runMcpInterceptorAuditProof()
6+
console.log(JSON.stringify(result, null, 2))
7+
8+
if (
9+
!result.sdk.capability_declared ||
10+
result.paired.request.info.pairing.status !== 'pending_response' ||
11+
result.paired.response.info.pairing.status !== 'paired' ||
12+
!result.paired.receipt_valid ||
13+
!result.paired.record_valid ||
14+
result.missing_identity.response.info.pairing.status !== 'unpaired' ||
15+
result.missing_identity.receipt_emitted ||
16+
!result.privacy.private_sidecar_contains_phrase ||
17+
result.privacy.public_record_contains_phrase
18+
) {
19+
process.exitCode = 1
20+
}

packages/integration/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"graphiti-core-ollama-smoke": "tsx examples/graphiti/graphiti-core-ollama-smoke.ts",
7474
"mcp-oauth-evidence": "tsx scripts/mcp-oauth-evidence.ts",
7575
"mcp-oauth-live-archive": "tsx scripts/mcp-oauth-live-archive.ts",
76+
"mcp-interceptor-audit": "tsx examples/mcp-interceptor-audit/mcp-interceptor-audit-smoke.ts",
7677
"x401-proof-sdk-compat": "tsx scripts/check-proof-x401-sdk-compat.ts",
7778
"open-x401-credential-e2e": "tsx scripts/open-x401-credential-e2e.ts",
7879
"open-x401-credential-packet": "tsx scripts/open-x401-credential-packet.ts",

0 commit comments

Comments
 (0)