X402.MCP.Server (X402 v0.6.0)

Copy Markdown View Source

Server half of the x402 MCP transport: gates a tool handler behind payment.

call/3 inspects an MCP tool-call request for a payment in _meta["x402/payment"] and drives the full verify → execute → settle flow against an X402.Facilitator:

  1. No payment → the payment-required tool result (isError: true with the PaymentRequired object in structuredContent and content[0].text).
  2. Invalid payment (wrong version, accepted not matching the advertised requirements, extension echo mismatch, failed verification) → the same payment-required result with the rejection reason.
  3. Valid payment → the wrapped handler runs; on success the payment is settled and the receipt is attached to result _meta["x402/payment-response"]. When settlement fails after execution, only the payment error is returned — never the tool's content.

The module is MCP-library agnostic: requests and results are plain maps in the shapes MCP libraries already use, so the wrapper drops into any tool dispatch function. See the MCP guide for integration snippets.

Example

config =
  X402.MCP.Server.init(
    tool: "premium_search",
    accepts: [
      %{
        price: "10000",
        network: "eip155:84532",
        asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        pay_to: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
        extra: %{"name" => "USDC", "version" => "2"}
      }
    ],
    facilitator: MyApp.Facilitator
  )

X402.MCP.Server.call(request, config, fn _request ->
  %{"content" => [%{"type" => "text", "text" => "results..."}]}
end)

Replay protection

Pass payment_identifier_cache: (an X402.Extensions.PaymentIdentifier.ETSCache server, the same option X402.Plug.PaymentGate takes) to atomically claim each payment proof before settlement, rejecting concurrent or repeated submissions of the same signed payment. The claim is released when the handler fails or settlement fails, so the client may retry with the same payment.

Summary

Types

An MCP tool-call handler: request params in, tool result map out.

Configuration map produced by init/1.

Functions

Gates an MCP tool-call request behind x402 payment verification.

Validates and compiles paid-tool options.

Builds the payment-required tool result advertised by this configuration.

Types

handler()

@type handler() :: (map() -> map())

An MCP tool-call handler: request params in, tool result map out.

options()

@type options() :: %{
  tool: String.t(),
  facilitator: X402.Facilitator.server(),
  hooks: module(),
  payment_identifier_cache:
    X402.Extensions.PaymentIdentifier.ETSCache.server() | nil,
  accepts: [map()],
  resource: map(),
  extensions: map()
}

Configuration map produced by init/1.

Functions

call(request, config, handler)

(since 0.6.0)
@spec call(map(), options(), handler()) :: map()

Gates an MCP tool-call request behind x402 payment verification.

request is the tool-call params map (typically with "name", "arguments", and "_meta" keys). handler receives the request and must return a tool result map (with a "content" list and optional "isError"); it only runs after the payment has been verified.

Always returns a tool result map:

  • the payment-required result when payment is missing, invalid, rejected by the facilitator, or already settled (replay)
  • the handler's result with the settlement receipt attached to _meta["x402/payment-response"] on success
  • the handler's error result unchanged (no settlement) when the handler sets "isError" => true
  • the settlement-failure result (payment-required format, without the tool's content) when settlement fails after execution
  • an opaque internal error result when the facilitator transport fails

If the handler raises, the replay claim is released and the exception is re-raised for the MCP library to surface.

init(opts)

(since 0.6.0)
@spec init(keyword()) :: options()

Validates and compiles paid-tool options.

Raises NimbleOptions.ValidationError for invalid options and ArgumentError when :accepts is empty, an accept's extra.paymentFlow is not "authorization", or the advertised data cannot be encoded as JSON.

Options

  • :tool (String.t/0) - Required. Tool name; used for the default mcp://tool/{tool} resource URL.

  • :accepts (list of map/0) - Required. Payment options advertised in PaymentRequired.accepts (at least one).

  • :facilitator (term/0) - Facilitator server pid/name used for verification and settlement. The default value is X402.Facilitator.

  • :hooks - Lifecycle hook module implementing X402.Hooks. The default value is X402.Hooks.Default.

  • :payment_identifier_cache - Optional idempotency cache: an ETSCache server pid/name (the default adapter), or a {module, cache} adapter tuple implementing X402.Extensions.PaymentIdentifier.Cache. When set, the wrapper performs an atomic claim (via put_new) on a hash of the signed scheme payload before settling, preventing concurrent requests from double-settling the same payment. The default value is nil.

  • :resource_url - Custom ResourceInfo.url (defaults to mcp://tool/{tool}). The default value is nil.

  • :description - ResourceInfo.description (defaults to Tool: {tool}). The default value is nil.

  • :mime_type (String.t/0) - ResourceInfo.mimeType. The default value is "application/json".

  • :service_name - ResourceInfo.serviceName (printable ASCII, max 32 characters recommended). The default value is nil.

  • :tags (list of String.t/0) - ResourceInfo.tags (max 5 recommended). The default value is [].

  • :icon_url - ResourceInfo.iconUrl (absolute http(s) URL). The default value is nil.

  • :extensions - Protocol extensions advertised in PaymentRequired.extensions. The default value is %{}.

Accept option fields (inside :accepts)

  • :scheme - Payment scheme (exact or upto). The default value is "exact".

  • :price - Required. Payment amount in atomic token units (PaymentRequirements amount).

  • :network (String.t/0) - Required. Blockchain network in CAIP-2 format (for example eip155:84532).

  • :asset (String.t/0) - Required. Token contract address or asset identifier.

  • :pay_to (String.t/0) - Required. Recipient wallet address (payTo in the PaymentRequirements schema).

  • :max_timeout_seconds (pos_integer/0) - Maximum time allowed for payment completion. The default value is 60.

  • :extra - Scheme-specific extra fields (string or atom keys). The default value is %{}.

payment_required_result(config, error_message \\ "Payment required to access this tool")

(since 0.6.0)
@spec payment_required_result(options(), String.t()) :: map()

Builds the payment-required tool result advertised by this configuration.

Useful for advertising the price of a paid tool outside call/3 (for example in a tools/list response or documentation).