Add MCPError.paymentRequired for the MPP Payment scheme over JSON-RPC#229
Open
amitach wants to merge 1 commit into
Open
Add MCPError.paymentRequired for the MPP Payment scheme over JSON-RPC#229amitach wants to merge 1 commit into
amitach wants to merge 1 commit into
Conversation
JSON-RPC servers implementing the Machine Payments Protocol "Payment" authentication scheme (paymentauth.org) signal payment required with error code -32042 (and -32043 for verification failed), carrying the offered challenges in `error.data.challenges`. MCPError could not represent that: `serverError` has no `data`, and -32042 decoded unconditionally to `urlElicitationRequired`, discarding the challenges. Add an additive `paymentRequired(code:message:data:)` case. The decoder disambiguates by payload: a -32042/-32043 error whose `data` carries a `challenges` array decodes to `.paymentRequired`; a -32042 with `elicitations` still decodes to `.urlElicitationRequired`, and a bare -32043 stays `.serverError`. No existing case signature changes and no existing decode behavior changes.
amitach
added a commit
to amitach/mpp-swift
that referenced
this pull request
May 31, 2026
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.
amitach
added a commit
to amitach/mpp-swift
that referenced
this pull request
May 31, 2026
* feat(mcp): MPPMCP, the JSON-RPC / MCP payment rail 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). * ci(audit): allowlist self-authored revision pins in the OSV gate 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. * fix(mcp): fail closed on a non-string opaque; guard the Xcode 26 select 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. * test(mcp): live cross-SDK conformance: mppx mcp-sdk client pays our Swift 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. * fix(mcp): harden the untrusted-input boundary (DoS + malformed-credential) 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
JSON-RPC servers that implement the Machine Payments Protocol ("Payment" HTTP auth scheme, bound to JSON-RPC/MCP) signal payment required with error code -32042 (and -32043 for verification failed), carrying the offered challenges in
error.data.challenges.MCPErrorcurrently can't represent that:serverError(code:message:)has nodata, so the challenge set can't be carried.-32042unconditionally to.urlElicitationRequired, which readsdata["elicitations"]and discards anychallenges.So a client built on this SDK can neither emit nor read a payment-required error.
Change
Add an additive case:
datais[String: Value]to match the decoder's existingdecodeIfPresent([String: Value]), and so the SDK carries the JSON-RPCdataobject verbatim without taking on any payment-protocol types.The decoder disambiguates by payload, so existing behavior is preserved:
-32042withdata.challenges.paymentRequired-32042withdata.elicitations(or neither).urlElicitationRequired(unchanged)-32043withdata.challenges.paymentRequired-32043withoutdata.challenges.serverError(unchanged)No existing case signature changes; no existing decode path changes.
Tests
New
Tests/MCPTests/ErrorTests.swift(swift-testing): round-trip ofpaymentRequired, the-32042payment-vs-elicitation disambiguation in both directions,-32043with/without challenges, and the carried-code accessor. Full suite green (557 tests).Notes
-32042is shared between this SDK's URL-elicitation feature and the MPP payment scheme; disambiguating on the presence ofchallengesvselicitationslets both coexist on the wire without a breaking change.