Skip to content

feat(mcp): MPPMCP, the JSON-RPC / MCP payment rail#78

Merged
amitach merged 5 commits into
mainfrom
feat/mppmcp
May 31, 2026
Merged

feat(mcp): MPPMCP, the JSON-RPC / MCP payment rail#78
amitach merged 5 commits into
mainfrom
feat/mppmcp

Conversation

@amitach
Copy link
Copy Markdown
Owner

@amitach amitach commented May 31, 2026

Binds the MPP 402 flow (challenge → credential → receipt) to JSON-RPC / Model Context Protocol, on the official MCP Swift SDK. Spec: draft-payment-transport-mcp-00. Rail-agnostic: the server gate reuses MPPServerMiddleware's mint/verify pipeline and the client wrapper reuses the 402 method-selection step; the payment method (e.g. the Tempo zero-amount proof) is injected.

What's here

  • MCPValueBridgeJSONValueMCP.Value, failing closed on a non-integer number / binary data so the protocol's integer-only invariant holds.
  • MCPPaymentCodecChallenge/Credential/Receipt ⇄ the native-JSON the MCP transport carries (the challenge request is decoded to/from EncodedJSON so the challenge-id binding recomputes; JCS canonicalization parity vs the peer is byte-verified), plus the -32042 error.data frame and the receipt's challengeId. Receipt/ProblemDetails reuse their own Codable via a JSON data hop.
  • MCPPaymentServer.gated — wraps a tools/call handler; reuses MPPServerMiddleware.evaluate via a Credential.headerValue adapter (empty body, no digest over MCP); -32042 when no credential, -32043 when one is rejected; attaches the receipt to result._meta.
  • MCPPaymentClient — pays a -32042 transparently (select → build → retry once), reading the receipt from result._meta via the RequestContext callTool overload.
  • Lifted PaymentClient.select(from:) → shared selectPaymentMethod(for:from:) in MPPClient (no duplication; HTTP client behavior unchanged, its 29 tests still green).

Tests (26) + peer parity

Value bridge, codec (including parsing a real captured mppx frame), server gate, client wrapper, and an in-process MCP.ClientMCP.Server e2e through the gate with the Tempo proof method. G7.5 peer-parity: ported the union of wevm/mppx's mcp-sdk client + server test scenarios (no-credential → -32042, valid → proceed + receipt, replay → -32043 + problem, receipt-attach preserves existing _meta/content, pass-through when unpaid, non-payment error rethrown). One reconciled divergence (G3.5): mppx skips expired challenges client-side; our shared selection defers expiry to the server, consistent with our existing HTTP client.

Dependency + the Swift 6.2 floor

Depends on a fork of modelcontextprotocol/swift-sdk pinned to an immutable commit — it adds MCPError.paymentRequired because the stock SDK can't carry error.data and hardwires -32042 to URL elicitation. Upstream PR: modelcontextprotocol/swift-sdk#229; the pin swaps to a tagged release once it merges.

The MCP SDK's transitive graph (swift-log 1.13 → tools 6.2, swift-nio 2.100 + eventsource → 6.1) raises the package floor to Swift 6.2: swift-tools-version, the README/ARCHITECTURE badges, CI Linux containers swift:6.0swift:6.2, and the macOS jobs xcode-select the runner's Xcode 26. Verified: full graph + all 26 tests build/pass on swift:6.2 Linux (6.2.4) and macOS.

Deferred (follow-up, see devlog)

The live mppx-process cross-SDK conformance harness (forward + reverse) + its CI job. Cross-SDK fidelity is covered hermetically meanwhile: a real captured frame is parsed, JCS parity is byte-proven, and the in-process e2e exercises the full client↔server flow.


Open in Devin Review

amitach added 2 commits May 30, 2026 23:52
Binds the MPP 402 flow (challenge -> credential -> receipt) to JSON-RPC /
Model Context Protocol, on the official MCP Swift SDK. Rail-agnostic: the
server gate reuses MPPServerMiddleware's mint/verify pipeline and the client
wrapper reuses the 402 method-selection step; the payment method (e.g. the
Tempo zero-amount proof) is injected.

- MCPValueBridge: JSONValue <-> MCP.Value, failing closed on a non-integer
  number / binary data so the protocol's integer-only invariant holds.
- MCPPaymentCodec: Challenge/Credential/Receipt <-> the native-JSON the MCP
  transport carries (request decoded to/from EncodedJSON so the challenge-id
  binding recomputes; JCS parity vs the peer is byte-verified), plus the
  -32042 error.data frame and the receipt's challengeId. Receipt/ProblemDetails
  reuse their own Codable via a JSON data hop.
- MCPPaymentServer.gated: wraps a tools/call handler; reuses
  MPPServerMiddleware.evaluate via a Credential.headerValue adapter (empty body,
  no digest over MCP); -32042 when no credential, -32043 when one is rejected;
  attaches the receipt to result._meta.
- MCPPaymentClient: pays a -32042 transparently (select -> build -> retry once),
  reading the receipt from result._meta via the RequestContext callTool overload.
- Lift PaymentClient.select(from:) -> shared selectPaymentMethod(for:from:) in
  MPPClient (no duplication; HTTP client behavior unchanged, its 29 tests green).

Tests (26): value bridge, codec (incl. parsing a real captured mppx frame),
server gate, client wrapper, and an in-process MCP.Client <-> MCP.Server e2e
with the Tempo proof method. Peer-parity (G7.5): ported the union of mppx's
mcp-sdk client + server test scenarios.

Depends on a fork of modelcontextprotocol/swift-sdk pinned to an immutable
commit (adds MCPError.paymentRequired; the stock SDK can't carry error.data and
hardwires -32042 to URL elicitation). Upstream PR: swift-sdk#229; the pin swaps
to a tagged release once it merges. The MCP SDK's transitive graph (swift-log
1.13) raises the package floor to Swift 6.2: tools-version, README/ARCHITECTURE
badges, CI containers swift:6.0 -> swift:6.2, and macOS jobs select Xcode 26.

Deferred to a follow-up (devlog): the live mppx-process cross-SDK conformance
harness + a CI job. Cross-SDK fidelity is covered hermetically meanwhile (real
captured frame, byte-proven JCS parity, in-process e2e).
The OSV gate fails closed on revision/branch pins (they have no SemVer for OSV
to range-match). Our fork dependency github.com/amitach/swift-sdk is a
revision pin, so the gate failed. The fork is our own 36-line reviewed patch
(MCPError.paymentRequired, upstreamed as modelcontextprotocol/swift-sdk#229),
not a third-party dependency OSV tracks. Add a documented allowlist for
self-authored revision pins: an accepted entry logs a CI warning, any other
revision pin still fails closed, and the fork's TRANSITIVE deps (swift-log,
swift-nio, ...) remain versioned pins that are scanned. Remove the entry and
pin a tagged upstream release once #229 merges.
devin-ai-integration[bot]

This comment was marked as resolved.

Address Devin review on #78:
- 🚩 MCPPaymentCodec.challenge(from:) parsed opaque via stringValue.map, which
  would SILENTLY DROP a non-string opaque, changing the challenge-id binding
  input and surfacing as an opaque verification failure. Fail closed instead
  (invalidField("opaque")). Tests: string opaque round-trips; a native-object
  opaque is rejected.
- 📝 The macOS xcode-select step now guards that an Xcode 26 install exists and
  emits a clear ::error:: if not, instead of failing opaquely on an empty glob.

The other 3 Devin notes are intentional/documented (the headerValue adapter
round-trip, the server-authoritative expiry divergence, the deferred live
mppx conformance) and are resolved with rationale on the threads.
devin-ai-integration[bot]

This comment was marked as resolved.

…wift MCP server

Answers 'is this proven against the real peer?' The reference mppx mcp-sdk
CLIENT pays our Swift MCP server over a real stdio transport, end to end:
mppx reads our -32042 challenge, builds the Tempo zero-amount proof credential,
sends it in params._meta, our server (MPPMCPConformanceServer = MCP.Server +
the MPPMCP gate + TempoProofVerifier) verifies it and mints a receipt into
result._meta, and mppx reads the receipt back. Offline + deterministic
(ecrecover, no RPC).

- MPPMCPConformanceServer: dev-only MCP server over stdio (no product), gates a
  premium tool behind a zero-amount Tempo proof via MPPMCP.
- Scripts/conformance/mcp-client.mjs: the mppx mcp-sdk client (spawns the Swift
  server via StdioClientTransport, pays, asserts the receipt + content).
- run-mcp.sh + a step in the Conformance (local) CI job.
- Adds @modelcontextprotocol/sdk (>=1.25.0) to the dev-only harness
  (npm ci --ignore-scripts; not shipped, not in the SwiftPM graph).

Reverse direction (our Swift client -> mppx mcp-sdk server) remains a follow-up;
the forward run + the hermetic suite already prove the wire frame and the flow.
devin-ai-integration[bot]

This comment was marked as resolved.

…tial)

From the security crawl + Devin review on #78:
- 🚩 A present-but-malformed credential in params._meta threw CodecError out of
  the gate handler, which the SDK wraps as -32603 internalError. Map a credential
  parse failure to -32602 invalidParams (a client error, matching the reference
  peer's MalformedCredentialError); an ABSENT credential still yields -32042.
- DoS: the gate passes body: Data() so MPPServerMiddleware's maxBodyBytes never
  bounds the MCP credential, and JSONValue decode/canonicalize recurse without a
  depth guard. Add a depth bound (100; payment payloads are ~depth 5) to the
  Value->JSONValue bridge so a hostile, deeply-nested credential/challenge fails
  closed (BridgeError.tooDeep) instead of risking a stack overflow.

Tests: malformed credential -> -32602; deeply-nested value -> tooDeep; within-limit
still converts. Container conversion extracted to keep cyclomatic complexity <= 10.
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread Sources/MPPMCP/MCPValueBridge.swift
Comment thread Package.swift
@amitach amitach merged commit c573b65 into main May 31, 2026
10 checks passed
@amitach amitach deleted the feat/mppmcp branch May 31, 2026 04:45
amitach added a commit that referenced this pull request May 31, 2026
…cp-sdk server (#79)

Closes the remaining MPPMCP conformance gap (the symmetric direction to the
forward run shipped in #78). OUR Swift MCP client spawns the reference mppx
mcp-sdk SERVER over a real stdio transport and pays its payment-gated tool:
our client reads mppx's -32042 challenge, builds the Tempo zero-amount proof
credential, sends it in params._meta, mppx verifies it and mints a receipt,
and our client reads the receipt back. Offline + deterministic (ecrecover).

- mcp-server.mjs: the mppx mcp-sdk server (McpServer + Transport.mcpSdk() +
  StdioServerTransport), gating a premium tool behind a zero-amount proof.
- MPPMCPConformanceClient: dev-only MCP client (no product) that spawns the
  Node server (Process + pipes), wires MCP.StdioTransport to its FDs, and pays
  via MCPPaymentClient + TempoProofMethod.
- run-mcp-reverse.sh + a step in the Conformance (local) CI job.

Both cross-SDK MCP directions are now proven live against the real peer in CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant