Skip to main content

Crate r402_mcp

Crate r402_mcp 

Source
Expand description

§r402-mcp

MCP transport for the x402 payment protocol, part of the r402 workspace.

§Design

Binds the official Rust MCP SDK rmcp (modelcontextprotocol/rust-sdk), the same role as:

LanguageOfficial MCP SDK
Gogithub.com/modelcontextprotocol/go-sdk/mcp
TypeScript@modelcontextprotocol/sdk
Rust (this crate)rmcp

Protocol behaviour is pinned to:

  • specs/transports-v2/mcp.md
  • Go go/mcp/* (primary control-flow pin)
  • TypeScript @x402/mcp (secondary)

V1 payment paths are omitted (r402 is V2-only).

§Constants (must match foundation)

NameValue
MCP_PAYMENT_REQUIRED_CODE402 (i32)
MCP_PAYMENT_META_KEY"x402/payment"
MCP_PAYMENT_RESPONSE_META_KEY"x402/payment-response"

Payment-required tool results set both structuredContent and content[0].text (JSON of the same PaymentRequired object).

§Dependency

[dependencies]
r402-core = "0.14"
r402-mcp = { version = "0.15", features = ["server"] }   # or "client" / "full"
# optional umbrella:
# r402 = { version = "0.15", features = ["mcp", "server"] }
FeatureSurface
serverPaymentWrapper + encode helpers
clientX402McpClient + McpToolCaller / PaymentSigner
fullserver + client + telemetry

§Server usage

Requires a Facilitator (local chain crate or HTTP facilitator). Wire the wrapper around your tool handler; this crate does not own the MCP transport loop — plug invoke / wrap into whatever hosts rmcp (stdio, HTTP, etc.).

use std::sync::Arc;

use r402_core::{ResourceServer, wire::{PaymentRequirements, ResourceInfo}};
use r402_mcp::{PaymentWrapper, PaymentWrapperConfig};
use rmcp::model::{CallToolRequestParams, CallToolResult, ContentBlock};

// 1. Facilitator that can verify + settle (your implementation / HTTP client)
let facilitator = Arc::new(my_facilitator);
let resource_server = ResourceServer::new(facilitator);

// 2. Advertise one or more payment options for this tool
let accepts = vec![PaymentRequirements::new(
    "exact".into(),
    "eip155:84532".parse()?,
    "10000".into(),          // amount in asset smallest unit
    "0xPayTo...".into(),
    "0xUSDC...".into(),
    60,
)];
let config = PaymentWrapperConfig::try_new(
    accepts,
    Some(
        ResourceInfo::new("mcp://tool/get_weather")
            .with_description("Weather lookup")
            .with_mime_type("application/json"),
    ),
)?;
let wrapper = PaymentWrapper::try_new(resource_server, config)?;

// 3a. One-shot: wrap a single tools/call
async fn handle_tool(wrapper: &PaymentWrapper, params: CallToolRequestParams) -> CallToolResult {
    wrapper
        .invoke(params, |_params| async move {
            CallToolResult::success(vec![ContentBlock::text(r#"{"temp":18}"#)])
        })
        .await
}

// 3b. Cloneable handler for routers (Go Wrap equivalent)
let paid = wrapper.wrap(|_params| async move {
    CallToolResult::success(vec![ContentBlock::text("ok")])
});
// router.register("get_weather", paid);

Control flow (Go PaymentWrapper.Wrap):

  1. Extract _meta["x402/payment"]
  2. Match accepts → verify (tool-level 402 on failure)
  3. Hooks → business handler
  4. On tool success → settle → _meta["x402/payment-response"]
  5. Settlement failure uses the same dual-format payment-required body (R5)

§Client usage

You implement two thin adapters:

  • McpToolCaller — one method: call MCP tools/call (typically an rmcp client session).
  • PaymentSigner — turn a PaymentRequired challenge into a signed PaymentPayload (scheme clients / your wallet glue).
use r402_core::wire::PaymentRequired;
use r402_mcp::{
    McpPaymentPayload, McpToolCaller, PaymentSigner, X402McpClient, X402McpClientOptions,
};
use rmcp::model::{CallToolRequestParams, CallToolResult};
use serde_json::Map;

struct MyMcpSession { /* rmcp Client / session */ }

impl McpToolCaller for MyMcpSession {
    async fn call_tool(
        &self,
        params: CallToolRequestParams,
    ) -> Result<CallToolResult, String> {
        // forward to rmcp session.call_tool(...)
        todo!()
    }
}

struct MySigner { /* scheme clients */ }

impl PaymentSigner for MySigner {
    async fn sign_payment(
        &self,
        required: PaymentRequired,
    ) -> Result<McpPaymentPayload, String> {
        // Select accepts[i], sign scheme payload → McpPaymentPayload::new(accepted, body)
        todo!()
    }
}

let client = X402McpClient::new(MyMcpSession { /* ... */ }, MySigner { /* ... */ })
    .with_options(X402McpClientOptions { auto_payment: true });

let out = client.call_tool("get_weather", Some(Map::from_iter([
    ("city".into(), serde_json::json!("SF")),
]))).await?;

assert!(out.payment_made);
// out.result — tool content; out.payment_response — settle meta if present

Free function: call_paid_tool(caller, signer, name, args).

Hooks (ClientHooks): on_payment_required (abort / supply payload), on_payment_requested (approve/deny), on_before_payment, on_after_payment.

§Encode helpers (shared)

HelperRole
payment_required_tool_resultdual-format 402 challenge
settlement_failed_tool_resultR5 dual-format settle failure
extract_payment_requiredprefer structuredContent, else text
attach_payment_to_params / extract_payment_from_params_meta["x402/payment"]
attach_settle_response / extract_settle_response_meta["x402/payment-response"]
create_tool_resource_urlmcp://tool/{name} or custom

§Known intentional gaps vs foundation

ItemNotes
V1Not supported
SEP-1036 / JSON-RPC -32042TS-only path; not in Go pin
Paid retry still 402Returns McpClientError::StillRequired (stricter than Go)
Live e2e example binaryNot shipped; use unit tests under src/{server,client,encode}.rs

§License

Dual-licensed under MIT and Apache-2.0. MCP transport for x402 on the official rmcp SDK.

§Official pins

ComponentSource
Behaviourspecs/transports-v2/mcp.md
Control flowGo go/mcp/{server,client,utils}.go
MCP typesrmcp (modelcontextprotocol/rust-sdk)

§Feature flags

FeatureSurface
serverPaymentWrapper
clientX402McpClient, call_paid_tool
fullboth + telemetry

Re-exports§

pub use client::AfterPaymentContext;client
pub use client::ClientHooks;client
pub use client::McpToolCaller;client
pub use client::PaidToolCallResult;client
pub use client::PaymentRequiredContext;client
pub use client::PaymentRequiredHookResult;client
pub use client::PaymentSigner;client
pub use client::X402McpClient;client
pub use client::X402McpClientOptions;client
pub use client::call_paid_tool;client
pub use constants::MCP_PAYMENT_META_KEY;
pub use constants::MCP_PAYMENT_REQUIRED_CODE;
pub use constants::MCP_PAYMENT_RESPONSE_META_KEY;
pub use constants::MCP_TOOL_URL_PREFIX;
pub use encode::McpPaymentPayload;client or server
pub use encode::attach_payment_to_params;client or server
pub use encode::attach_settle_response;client or server
pub use encode::create_tool_resource_url;client or server
pub use encode::extract_payment_from_params;client or server
pub use encode::extract_payment_required;client or server
pub use encode::extract_settle_response;client or server
pub use encode::is_payment_required_result;client or server
pub use encode::payment_required_tool_result;client or server
pub use encode::settlement_failed_tool_result;client or server
pub use error::McpClientError;client or server
pub use error::PaymentRequiredError;client or server
pub use server::AfterExecutionContext;server
pub use server::PaymentWrapper;server
pub use server::PaymentWrapperConfig;server
pub use server::PaymentWrapperConfigError;server
pub use server::PaymentWrapperHooks;server
pub use server::ServerHookContext;server
pub use server::SettlementContext;server

Modules§

clientclient
Client-side MCP auto-pay — Go X402MCPClient / CallPaidTool.
constants
Protocol constants for MCP × x402.
encodeclient or server
Transport encoding for MCP × x402.
errorclient or server
Errors for MCP × x402 (Go PaymentRequiredError + client errors).
serverserver
Server-side MCP payment wrapper.