Local chain

Paid MCP tool

A paid resource reachable only as an MCP tool, not invocable over HTTP.

A paid resource exposed only as an MCP tool. With protocols.http.enabled: false, HTTP invocation returns 404 RESOURCE_NOT_FOUND for every resource.

examples/paid-mcp-tool/config.yamlyaml
version: 1

merchant:
  id: paid-mcp-tool-example
  name: Paid MCP Tool Example
  publicBaseUrl: ${GATEWAY_PUBLIC_BASE_URL:-http://localhost:8080}

server:
  port: ${GATEWAY_PORT:-8080}
  host: 0.0.0.0

storage:
  receipts:
    driver: sqlite
    path: ${RECEIPT_STORE_PATH:-./data/receipts.sqlite}

protocols:
  # Off: `POST /api/resources/:id/invoke` answers 404 for every resource, so
  # this one is reachable only as an MCP tool call
  http:
    enabled: false
  mcp:
    enabled: true
    mountPath: /mcp

resources:
  market_report_tool:
    name: Get market report
    description: Latest premium market analysis, callable only as an MCP tool. 0.01 USDC per call.
    input:
      type: object
      properties: {}
      additionalProperties: false
    backend:
      type: http
      method: GET
      url: ${MERCHANT_API_BASE_URL:-http://localhost:3000}/api/report
      timeoutMs: 10000
    pricing:
      type: fixed
      amount: "0.01"
      currency: USDC
    expose: [mcp]
    payments: [x402]

payments:
  x402:
    enabled: true
    network: ${X402_NETWORK:-eip155:31337}
    rpcUrl: ${X402_RPC_URL:-http://127.0.0.1:8545}
    # Placeholder: replace with the deployed MockUSDC address from
    # `.deploy/local.json` after `npm run chain:deploy` (see README.md)
    asset: ${X402_ASSET:-0x000000000000000000000000000000000000dEaD}
    assetName: ${X402_ASSET_NAME:-MockUSDC}
    assetVersion: ${X402_ASSET_VERSION:-2}
    assetDecimals: ${X402_ASSET_DECIMALS:-6}
    # Merchant-controlled settlement destination. NEVER a gateway-owned wallet.
    # LOCAL DEVELOPMENT ONLY - DO NOT FUND (Anvil's well-known account #1).
    payTo: ${MERCHANT_WALLET:-0x70997970C51812dc3A010C7d01b50e0d17dc79C8}
    maxTimeoutSeconds: 120
    facilitator:
      mode: local
      # LOCAL DEVELOPMENT ONLY - DO NOT FUND (Anvil's well-known account #0)
      signerPrivateKey: ${X402_FACILITATOR_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}

What it demonstrates

  • No resource is invocable over HTTP. GET /api/resources still lists the tool, with exposedVia: ["mcp"].
  • expose: [http] here would fail validation with CONFIG_INVALID: Resource "market_report_tool" is exposed via "http" but protocols.http.enabled is false.
  • The id doubles as the tool name, so it is limited to A-Z a-z 0-9 . _ -, 1–128 characters.
  • The MCP endpoint itself stays open to any client that speaks MCP: this setting controls exposure, not authentication.

Run it

From a clone of the repository, after npm install. The chain, the merchant and the gateway each stay in the foreground, so give each one its own terminal.

  1. The local chain
    bash
    npm run chain:start
  2. Mock USDC, in a second terminal
    bash
    npm run chain:deploy

    Deploys MockUSDC, funds the buyer and writes .deploy/local.json.

  3. The “existing backend” this example fronts
    bash
    npm run dev:merchant
  4. The gateway, with this example's config
    bash
    AGENT_COMMERCE_CONFIG=examples/paid-mcp-tool/config.yaml \
    X402_ASSET=$(node -p "require('./.deploy/local.json').asset") \
      npm run dev:gateway
  5. Verify
    bash
    npm run agent-commerce -- validate --config examples/paid-mcp-tool/config.yaml
    npm run agent-commerce -- doctor --config examples/paid-mcp-tool/config.yaml
  6. Call it
    bash
    curl -s http://localhost:8080/mcp \
      -H 'content-type: application/json' \
      -H 'accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    # -> "market_report_tool" appears; invoking it over HTTP returns 404
The config validates standalone with no environment set: every ${VAR:-default} falls back to the local demo stack's defaults. The default X402_ASSET is a placeholder, which is why the gateway step supplies the deployed MockUSDC address from .deploy/local.json.

Source: examples/paid-mcp-tool