diff --git a/.env.example b/.env.example index 1de85e2..8efa43d 100644 --- a/.env.example +++ b/.env.example @@ -4,17 +4,23 @@ INDEXER_URL=https://analytics.indigoprotocol.io/api/v1 BLOCKFROST_API_KEY= # ── x402 payment gating (optional — omit or leave blank to disable) ────────── -# EVM wallet address to receive USDC on Base (required to enable EVM payments) -X402_EVM_ADDRESS= +# +# Payment uses the split execution flow: signing happens locally, verification +# and settlement route through the openmm.io proxy (or your own PAYMENT_SERVER). +# Private keys never leave this process. +# +# To enable: set X402_PRIVATE_KEY to your EVM wallet private key. +# The wallet pays for tool calls; funds must be available in USDC on Base. -# Cardano address (optional) — NOTE: @qbtlabs/x402 v0.5.0 does not yet emit a -# Cardano entry in 402 accepts[] because USDC_CONTRACTS has no cardano: entry. -# Setting this enables Cardano payment *verification* but not advertisement. -# Leave blank until a future x402 release adds native Cardano token support. -X402_CARDANO_ADDRESS= +# EVM private key of the payer wallet (0x-prefixed). Enables auto-payment. +X402_PRIVATE_KEY= -# Set to "true" to use testnets (Base Sepolia = chainId 84532, Cardano preprod) +# Proxy / settlement worker URL. Override for self-hosted deployments. +PAYMENT_SERVER=https://mcp.openmm.io + +# Set to "true" to use Base Sepolia testnet X402_TESTNET=false -# Override default facilitator URL (defaults to https://x402.org/facilitator) +# Legacy: direct facilitator URL fallback (used only when PAYMENT_SERVER is unset +# and you want to bypass mcp.openmm.io). Defaults to https://mcp.openmm.io. X402_FACILITATOR_URL= diff --git a/README.md b/README.md index 4590350..136d39c 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ npx @indigoprotocol/indigo-mcp setup ``` This will: + 1. Ask which client you're using (Claude Desktop, Claude Code, Cursor, Windsurf) 2. Prompt for your Blockfrost API key 3. Automatically update your config file @@ -124,6 +125,7 @@ MCP_TRANSPORT=http PORT=3000 npx @indigoprotocol/indigo-mcp ``` This starts an HTTP server with: + - `POST /mcp` — MCP endpoint (Streamable HTTP with SSE) - `GET /health` — Health check @@ -144,6 +146,7 @@ notepad "$env:APPDATA\Claude\claude_desktop_config.json" ``` **Standard config:** + ```json { "mcpServers": { @@ -301,154 +304,158 @@ For any client that supports MCP over stdio, point it to the `npx @indigoprotoco ### Asset Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_assets` | Get all Indigo iAssets with prices and interest data | None | -| `get_asset` | Get details for a specific iAsset | `asset`: iUSD, iBTC, iETH, or iSOL | -| `get_asset_price` | Get the current price for a specific iAsset | `asset`: iUSD, iBTC, iETH, or iSOL | -| `get_ada_price` | Get the current ADA price in USD | None | -| `get_indy_price` | Get the current INDY token price in ADA and USD | None | +| Tool | Description | Parameters | +| ----------------- | ---------------------------------------------------- | ---------------------------------- | +| `get_assets` | Get all Indigo iAssets with prices and interest data | None | +| `get_asset` | Get details for a specific iAsset | `asset`: iUSD, iBTC, iETH, or iSOL | +| `get_asset_price` | Get the current price for a specific iAsset | `asset`: iUSD, iBTC, iETH, or iSOL | +| `get_ada_price` | Get the current ADA price in USD | None | +| `get_indy_price` | Get the current INDY token price in ADA and USD | None | ### CDP / Loan Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_all_cdps` | Get all CDPs/loans, optionally filtered by iAsset | `asset?`: iAsset filter; `limit?`: 1-500 (default 50); `offset?`: pagination offset | -| `get_cdps_by_owner` | Get CDPs for a specific owner | `owner`: payment key hash (56-char hex) or bech32 address | -| `get_cdps_by_address` | Get CDPs for a specific Cardano address | `address`: bech32 address (addr1... or addr_test1...) | -| `analyze_cdp_health` | Analyze collateral ratios and liquidation risk | `owner`: payment key hash or bech32 address | +| Tool | Description | Parameters | +| --------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------- | +| `get_all_cdps` | Get all CDPs/loans, optionally filtered by iAsset | `asset?`: iAsset filter; `limit?`: 1-500 (default 50); `offset?`: pagination offset | +| `get_cdps_by_owner` | Get CDPs for a specific owner | `owner`: payment key hash (56-char hex) or bech32 address | +| `get_cdps_by_address` | Get CDPs for a specific Cardano address | `address`: bech32 address (addr1... or addr_test1...) | +| `analyze_cdp_health` | Analyze collateral ratios and liquidation risk | `owner`: payment key hash or bech32 address | ### CDP Write Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `open_cdp` | Open a new CDP position (returns unsigned CBOR tx) | `address`: bech32 address; `asset`: iUSD, iBTC, iETH, or iSOL; `collateralAmount`: lovelace; `mintAmount`: iAsset smallest unit | -| `deposit_cdp` | Deposit additional collateral into a CDP | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index; `amount`: lovelace | -| `withdraw_cdp` | Withdraw collateral from a CDP | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index; `amount`: lovelace | -| `close_cdp` | Close a CDP and reclaim collateral | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index | +| Tool | Description | Parameters | +| -------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `open_cdp` | Open a new CDP position (returns unsigned CBOR tx) | `address`: bech32 address; `asset`: iUSD, iBTC, iETH, or iSOL; `collateralAmount`: lovelace; `mintAmount`: iAsset smallest unit | +| `deposit_cdp` | Deposit additional collateral into a CDP | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index; `amount`: lovelace | +| `withdraw_cdp` | Withdraw collateral from a CDP | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index; `amount`: lovelace | +| `close_cdp` | Close a CDP and reclaim collateral | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index | ### CDP Mint/Burn Tools -| Tool | Description | Parameters | -|------|-------------|------------| +| Tool | Description | Parameters | +| ---------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mint_cdp` | Mint additional iAssets from an existing CDP (increases debt) | `address`: bech32 address; `asset`: iUSD, iBTC, iETH, or iSOL; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: CDP UTxO output index; `amount`: iAsset amount in smallest unit | -| `burn_cdp` | Burn iAssets to reduce CDP debt | `address`: bech32 address; `asset`: iUSD, iBTC, iETH, or iSOL; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: CDP UTxO output index; `amount`: iAsset amount in smallest unit | +| `burn_cdp` | Burn iAssets to reduce CDP debt | `address`: bech32 address; `asset`: iUSD, iBTC, iETH, or iSOL; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: CDP UTxO output index; `amount`: iAsset amount in smallest unit | ### CDP Liquidation & Redemption Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `liquidate_cdp` | Liquidate an undercollateralized CDP through the stability pool | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index | -| `redeem_cdp` | Redeem iAssets from a CDP | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index; `amount`: iAsset amount in smallest unit | -| `freeze_cdp` | Freeze a CDP to prevent further operations | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index | -| `merge_cdps` | Merge multiple CDPs into one | `address`: bech32 address; `cdpOutRefs`: array of `{txHash, outputIndex}` (min 2) | +| Tool | Description | Parameters | +| --------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `liquidate_cdp` | Liquidate an undercollateralized CDP through the stability pool | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index | +| `redeem_cdp` | Redeem iAssets from a CDP | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index; `amount`: iAsset amount in smallest unit | +| `freeze_cdp` | Freeze a CDP to prevent further operations | `address`: bech32 address; `asset`: iAsset; `cdpTxHash`: CDP UTxO tx hash; `cdpOutputIndex`: output index | +| `merge_cdps` | Merge multiple CDPs into one | `address`: bech32 address; `cdpOutRefs`: array of `{txHash, outputIndex}` (min 2) | ### Leverage CDP Tools -| Tool | Description | Parameters | -|------|-------------|------------| +| Tool | Description | Parameters | +| -------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `leverage_cdp` | Open a leveraged CDP by redeeming against ROB positions | `address`: bech32 address; `asset`: iAsset; `leverage`: multiplier (e.g. 2.0); `baseCollateral`: lovelace amount | ### Stability Pool Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_stability_pools` | Get the latest stability pool state for each iAsset | None | -| `get_stability_pool_accounts` | Get all open stability pool accounts, optionally filtered by iAsset | `asset?`: iUSD, iBTC, iETH, or iSOL | -| `get_sp_account_by_owner` | Get stability pool accounts for specific owners | `owners`: array of payment key hashes or bech32 addresses | +| Tool | Description | Parameters | +| ----------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------- | +| `get_stability_pools` | Get the latest stability pool state for each iAsset | None | +| `get_stability_pool_accounts` | Get all open stability pool accounts, optionally filtered by iAsset | `asset?`: iUSD, iBTC, iETH, or iSOL | +| `get_sp_account_by_owner` | Get stability pool accounts for specific owners | `owners`: array of payment key hashes or bech32 addresses | ### Staking Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_staking_info` | Get the current INDY staking manager state | None | -| `get_staking_positions` | Get all open INDY staking positions | None | -| `get_staking_positions_by_owner` | Get INDY staking positions for specific owners | `owners`: array of payment key hashes or bech32 addresses | -| `get_staking_position_by_address` | Get INDY staking positions for a single address | `address`: Cardano bech32 address | +| Tool | Description | Parameters | +| --------------------------------- | ----------------------------------------------- | --------------------------------------------------------- | +| `get_staking_info` | Get the current INDY staking manager state | None | +| `get_staking_positions` | Get all open INDY staking positions | None | +| `get_staking_positions_by_owner` | Get INDY staking positions for specific owners | `owners`: array of payment key hashes or bech32 addresses | +| `get_staking_position_by_address` | Get INDY staking positions for a single address | `address`: Cardano bech32 address | ### Stability Pool Request Tools -| Tool | Description | Parameters | -|------|-------------|------------| +| Tool | Description | Parameters | +| -------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `process_sp_request` | Process a pending stability pool request (protocol maintenance) | `address`: bech32 address; `asset`: iAsset; `accountTxHash`: account UTxO tx hash; `accountOutputIndex`: output index | -| `annul_sp_request` | Cancel a pending stability pool request | `address`: bech32 address; `accountTxHash`: account UTxO tx hash; `accountOutputIndex`: output index | +| `annul_sp_request` | Cancel a pending stability pool request | `address`: bech32 address; `accountTxHash`: account UTxO tx hash; `accountOutputIndex`: output index | ### Staking Write Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `open_staking_position` | Stake INDY tokens by creating a new staking position | `address`: bech32 address; `amount`: INDY amount in smallest unit | +| Tool | Description | Parameters | +| ------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `open_staking_position` | Stake INDY tokens by creating a new staking position | `address`: bech32 address; `amount`: INDY amount in smallest unit | | `adjust_staking_position` | Adjust an existing staking position (add or remove INDY) | `address`: bech32 address; `amount`: positive=stake more, negative=unstake; `positionTxHash`: UTxO tx hash; `positionOutputIndex`: UTxO output index | -| `close_staking_position` | Close a staking position and unstake all INDY | `address`: bech32 address; `positionTxHash`: UTxO tx hash; `positionOutputIndex`: UTxO output index | +| `close_staking_position` | Close a staking position and unstake all INDY | `address`: bech32 address; `positionTxHash`: UTxO tx hash; `positionOutputIndex`: UTxO output index | ### Staking Reward Tools -| Tool | Description | Parameters | -|------|-------------|------------| +| Tool | Description | Parameters | +| ---------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `distribute_staking_rewards` | Distribute collected ADA rewards from collector UTxOs to stakers | `address`: bech32 address; `collectorTxHashes`: array of `{txHash, outputIndex}` | ### Analytics & APR Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_tvl` | Get historical TVL data from DefiLlama | None | -| `get_apr_rewards` | Get all APR reward records | None | -| `get_apr_by_key` | Get APR for a specific key | `key`: APR key (e.g. sp_iUSD_indy, stake_ada) | -| `get_dex_yields` | Get DEX farm yields for iAsset pairs | None | -| `get_protocol_stats` | Get aggregated protocol statistics | None | +| Tool | Description | Parameters | +| -------------------- | -------------------------------------- | --------------------------------------------- | +| `get_tvl` | Get historical TVL data from DefiLlama | None | +| `get_apr_rewards` | Get all APR reward records | None | +| `get_apr_by_key` | Get APR for a specific key | `key`: APR key (e.g. sp_iUSD_indy, stake_ada) | +| `get_dex_yields` | Get DEX farm yields for iAsset pairs | None | +| `get_protocol_stats` | Get aggregated protocol statistics | None | ### Governance Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_protocol_params` | Get latest governance protocol parameters | None | -| `get_temperature_checks` | Get temperature check polls | None | -| `get_polls` | Get all governance polls | None | +| Tool | Description | Parameters | +| ------------------------ | ----------------------------------------- | ---------- | +| `get_protocol_params` | Get latest governance protocol parameters | None | +| `get_temperature_checks` | Get temperature check polls | None | +| `get_polls` | Get all governance polls | None | ### Redemption & Order Book Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_order_book` | Get open limited redemption positions | `asset?`: iAsset filter; `owners?`: array of payment key hashes | -| `get_redemption_orders` | Get redemption orders with optional filters | `timestamp?`: Unix ms; `in_range?`: filter by price range | -| `get_redemption_queue` | Get aggregated redemption queue for an iAsset | `asset`: iUSD, iBTC, iETH, or iSOL | +| Tool | Description | Parameters | +| ----------------------- | --------------------------------------------- | --------------------------------------------------------------- | +| `get_order_book` | Get open limited redemption positions | `asset?`: iAsset filter; `owners?`: array of payment key hashes | +| `get_redemption_orders` | Get redemption orders with optional filters | `timestamp?`: Unix ms; `in_range?`: filter by price range | +| `get_redemption_queue` | Get aggregated redemption queue for an iAsset | `asset`: iUSD, iBTC, iETH, or iSOL | ### ROB Write Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `open_rob` | Open a new ROB position with ADA and a max price limit | `address`: bech32 address; `asset`: iAsset; `lovelacesAmount`: lovelace to deposit; `maxPrice`: on-chain integer string | -| `cancel_rob` | Cancel an existing ROB position | `address`: bech32 address; `robTxHash`: ROB UTxO tx hash; `robOutputIndex`: output index | -| `adjust_rob` | Adjust ADA in a ROB (positive to add, negative to remove) | `address`: bech32 address; `robTxHash`: ROB UTxO tx hash; `robOutputIndex`: output index; `lovelacesAdjustAmount`: adjustment; `newMaxPrice?`: optional new max price | -| `claim_rob` | Claim received iAssets from an ROB position | `address`: bech32 address; `robTxHash`: ROB UTxO tx hash; `robOutputIndex`: output index | -| `redeem_rob` | Redeem iAssets against one or more ROB positions | `address`: bech32 address; `redemptionRobs`: array of `{txHash, outputIndex, iAssetAmount}`; `priceOracleTxHash`; `priceOracleOutputIndex`; `iassetTxHash`; `iassetOutputIndex` | +| Tool | Description | Parameters | +| ------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `open_rob` | Open a new ROB position with ADA and a max price limit | `address`: bech32 address; `asset`: iAsset; `lovelacesAmount`: lovelace to deposit; `maxPrice`: on-chain integer string | +| `cancel_rob` | Cancel an existing ROB position | `address`: bech32 address; `robTxHash`: ROB UTxO tx hash; `robOutputIndex`: output index | +| `adjust_rob` | Adjust ADA in a ROB (positive to add, negative to remove) | `address`: bech32 address; `robTxHash`: ROB UTxO tx hash; `robOutputIndex`: output index; `lovelacesAdjustAmount`: adjustment; `newMaxPrice?`: optional new max price | +| `claim_rob` | Claim received iAssets from an ROB position | `address`: bech32 address; `robTxHash`: ROB UTxO tx hash; `robOutputIndex`: output index | +| `redeem_rob` | Redeem iAssets against one or more ROB positions | `address`: bech32 address; `redemptionRobs`: array of `{txHash, outputIndex, iAssetAmount}`; `priceOracleTxHash`; `priceOracleOutputIndex`; `iassetTxHash`; `iassetOutputIndex` | ### DEX Proxy Tools -| Tool | Description | Parameters | -|------|-------------|------------| -| `get_steelswap_tokens` | Get all tokens available on Steelswap DEX | None | -| `get_steelswap_estimate` | Get a swap estimate from Steelswap | `tokenIn`: input token; `tokenOut`: output token; `amountIn`: amount | -| `get_iris_liquidity_pools` | Get liquidity pools from Iris | `tokenA?`: first token; `tokenB?`: second token; `dex?`: DEX filter | -| `get_blockfrost_balances` | Get token balances for a Cardano address | `address`: Cardano bech32 address | +| Tool | Description | Parameters | +| -------------------------- | ----------------------------------------- | -------------------------------------------------------------------- | +| `get_steelswap_tokens` | Get all tokens available on Steelswap DEX | None | +| `get_steelswap_estimate` | Get a swap estimate from Steelswap | `tokenIn`: input token; `tokenOut`: output token; `amountIn`: amount | +| `get_iris_liquidity_pools` | Get liquidity pools from Iris | `tokenA?`: first token; `tokenB?`: second token; `dex?`: DEX filter | +| `get_blockfrost_balances` | Get token balances for a Cardano address | `address`: Cardano bech32 address | ### Collector & IPFS Tools -| Tool | Description | Parameters | -|------|-------------|------------| +| Tool | Description | Parameters | +| --------------------- | ---------------------------------------- | ------------------------------ | | `get_collector_utxos` | Get collector UTXOs for fee distribution | `length?`: max UTXOs to return | -| `store_on_ipfs` | Store text content on IPFS | `text`: content to store | -| `retrieve_from_ipfs` | Retrieve content from IPFS by CID | `cid`: IPFS content identifier | +| `store_on_ipfs` | Store text content on IPFS | `text`: content to store | +| `retrieve_from_ipfs` | Retrieve content from IPFS by CID | `cid`: IPFS content identifier | ## Environment Variables -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `INDEXER_URL` | No | `https://analytics.indigoprotocol.io/api/v1` | Indigo analytics API base URL | -| `BLOCKFROST_API_KEY` | For write ops | — | Blockfrost project ID for transaction building | -| `CARDANO_NETWORK` | No | `mainnet` | Cardano network: `mainnet`, `preprod`, or `preview` | -| `MCP_TRANSPORT` | No | `stdio` | Transport mode: `stdio` or `http` | -| `PORT` | No | `3000` | HTTP server port (only used when `MCP_TRANSPORT=http`) | +| Variable | Required | Default | Description | +| ---------------------- | ------------- | -------------------------------------------- | ----------------------------------------------------------- | +| `INDEXER_URL` | No | `https://analytics.indigoprotocol.io/api/v1` | Indigo analytics API base URL | +| `BLOCKFROST_API_KEY` | For write ops | — | Blockfrost project ID for transaction building | +| `CARDANO_NETWORK` | No | `mainnet` | Cardano network: `mainnet`, `preprod`, or `preview` | +| `MCP_TRANSPORT` | No | `stdio` | Transport mode: `stdio` or `http` | +| `PORT` | No | `3000` | HTTP server port (only used when `MCP_TRANSPORT=http`) | +| `X402_PRIVATE_KEY` | No | — | EVM private key (`0x…`) of the payer wallet — enables auto-payment via split flow | +| `PAYMENT_SERVER` | No | `https://mcp.openmm.io` | Settlement worker / proxy URL | +| `X402_TESTNET` | No | `false` | Use Base Sepolia testnet | +| `X402_FACILITATOR_URL` | No | — | Fallback facilitator (used only when `PAYMENT_SERVER` unset) | ## Example Queries @@ -507,6 +514,8 @@ npm run test:watch # run tests in watch mode ``` src/ ├── index.ts # Server entry point (stdio transport) +├── payment.ts # x402 configuration: chain addresses + tool price tiers +├── payment-client.ts # withAutoPayment: client-side auto-pay on 402 responses ├── types/ │ └── tx-types.ts # UnsignedTxResult, TxSummary types ├── tools/ @@ -560,36 +569,66 @@ Indigo MCP optionally gates tools behind per-call micropayments using the [x402 ### How it works +Payment uses the **split execution** model — the same architecture as openMM-MCP: + +1. A tool is called (no payment header needed from the caller) +2. The gate intercepts and contacts the **settlement worker** (`mcp.openmm.io` by default) +3. Worker responds `402` with EIP-3009 requirements (amount, recipient, chain) +4. Gate **signs locally** using `X402_PRIVATE_KEY` — the key never leaves this process +5. Gate retries with the signed payment → worker verifies on-chain, issues a JWT +6. Gate verifies JWT locally, executes the original tool handler +7. Settlement tx hash is injected into the tool response + +This keeps process isolation clean: indigo-mcp never holds the recipient wallet — only the payer key. Verification and settlement are handled by the openmm.io proxy. + - Read tools (`get_tvl`, `get_asset_price`, …) cost **$0.001 USDC** per call - Analysis tools (`analyze_cdp_health`) cost **$0.005 USDC** per call - Write tools (`open_cdp`, `mint_cdp`, …) cost **$0.01 USDC** per call -- Tools called without a valid payment return a `402 Payment Required` JSON response with an `accepts[]` array listing supported chains and amounts ### Environment variables -| Variable | Required | Default | Description | -|---|---|---|---| -| `X402_EVM_ADDRESS` | to enable | — | EVM wallet to receive USDC on Base | -| `X402_CARDANO_ADDRESS` | optional | — | Cardano address to receive USDM on Cardano | -| `X402_TESTNET` | optional | `false` | Use Base Sepolia / Cardano preprod | -| `X402_FACILITATOR_URL` | optional | `https://x402.org/facilitator` | Override facilitator | +| Variable | Required | Default | Description | +| ---------------------- | --------- | ----------------------- | ------------------------------------------------------------------------------ | +| `X402_PRIVATE_KEY` | to enable | — | EVM private key (`0x…`) of the payer wallet — enables split payment | +| `PAYMENT_SERVER` | optional | `https://mcp.openmm.io` | Settlement worker / proxy URL | +| `X402_TESTNET` | optional | `false` | Use Base Sepolia testnet | +| `X402_FACILITATOR_URL` | optional | — | Fallback facilitator URL (used only when `PAYMENT_SERVER` is not set) | + +### Split execution flow + +When `X402_PRIVATE_KEY` is set, every paid tool call is handled transparently: + +1. Gate contacts `PAYMENT_SERVER` (`https://mcp.openmm.io` by default) +2. Signs EIP-3009 locally — the private key never leaves this process +3. Proxy verifies on-chain, issues a short-lived JWT +4. Gate verifies JWT, executes tool, injects settlement tx hash into response + +If `X402_PRIVATE_KEY` is not set the gate is disabled and all tools execute without payment. + +```bash +# Minimal: just set the payer key (proxy defaults to mcp.openmm.io) +X402_PRIVATE_KEY=0xYourPayerPrivateKey npx @indigoprotocol/indigo-mcp + +# Self-hosted proxy +X402_PRIVATE_KEY=0xYourPayerPrivateKey \ +PAYMENT_SERVER=https://your-own-proxy \ +npx @indigoprotocol/indigo-mcp +``` ### Local development ```bash # 1. Copy example env cp .env.example .env -# Edit .env and fill in X402_EVM_ADDRESS (and optionally X402_CARDANO_ADDRESS) +# Edit .env and set X402_PRIVATE_KEY to a funded Base Sepolia wallet # 2. Start the HTTP server MCP_TRANSPORT=http PORT=3000 npm run dev # 3. Run the payment e2e tests -X402_EVM_ADDRESS=0x... X402_TESTNET=true npm test -- x402-payment +X402_TESTNET=true npm test -- x402-payment ``` -The e2e tests work without a real wallet address — the "real env" test case is the only one that requires `X402_EVM_ADDRESS` to be set. - ### MCP client config with x402 Add the `env` block to whichever MCP config file your client uses: @@ -605,7 +644,7 @@ Add the `env` block to whichever MCP config file your client uses: "env": { "INDEXER_URL": "https://analytics.indigoprotocol.io/api/v1", "BLOCKFROST_API_KEY": "your-blockfrost-project-id", - "X402_EVM_ADDRESS": "0xYourEVMWalletAddress", + "X402_PRIVATE_KEY": "0xYourPayerPrivateKey", "X402_TESTNET": "true" } } @@ -624,7 +663,7 @@ Add the `env` block to whichever MCP config file your client uses: "env": { "INDEXER_URL": "https://analytics.indigoprotocol.io/api/v1", "BLOCKFROST_API_KEY": "your-blockfrost-project-id", - "X402_EVM_ADDRESS": "0xYourEVMWalletAddress", + "X402_PRIVATE_KEY": "0xYourPayerPrivateKey", "X402_TESTNET": "true" } } @@ -634,7 +673,7 @@ Add the `env` block to whichever MCP config file your client uses: **Cursor / Windsurf** — same `env` block applies to `~/.cursor/mcp.json` or `~/.codeium/windsurf/mcp_config.json`. -> Set `X402_TESTNET` to `false` (or omit it) for Base mainnet / Cardano mainnet. +> Set `X402_TESTNET` to `false` (or omit it) for Base mainnet. ## License diff --git a/src/payment-client.ts b/src/payment-client.ts deleted file mode 100644 index c55daeb..0000000 --- a/src/payment-client.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * x402 client-side auto-payment wrapper - * - * When X402_PRIVATE_KEY is set, intercepts 402 responses from withX402-gated - * tool handlers, signs a payment, and retries — so callers (e.g. Claude Code) - * receive the actual tool result rather than a payment-required error. - * - * If X402_PRIVATE_KEY is not set, this is a no-op passthrough. - * - * PAYMENT_SERVER overrides the facilitator URL at startup (before configure() - * is called in payment.ts); at runtime it has no effect here because the - * signing is purely local — the server-side withX402 middleware calls the - * facilitator for verification. - */ - -import { appendFileSync } from 'node:fs'; -import { signPayment, buildPaymentPayload, parsePaymentRequired } from '@qbtlabs/x402'; - -type ToolResult = { content: Array<{ type: string; text: string }> }; -type AnyHandler = (params: Record) => Promise; - -const LOG_FILE = process.env.X402_LOG_FILE ?? '/tmp/indigo-mcp-x402.log'; - -function log(msg: string): void { - const line = `${new Date().toISOString()} ${msg}\n`; - process.stderr.write(line); - try { - appendFileSync(LOG_FILE, line); - } catch { - /* ignore */ - } -} - -/** - * Wraps a tool handler so that 402 responses are automatically paid using - * the configured X402_PRIVATE_KEY. - */ -export function withAutoPayment(handler: AnyHandler): AnyHandler { - const privateKey = process.env.X402_PRIVATE_KEY as `0x${string}` | undefined; - if (!privateKey) return handler; - - return async (params: Record) => { - const result = await handler(params); - - const text = result?.content?.[0]?.text; - if (!text) return result; - - let parsed: Record; - try { - parsed = JSON.parse(text); - } catch { - return result; - } - - if (parsed['code'] !== 402) return result; - - const requirements = parsePaymentRequired(parsed as Parameters[0]); - if (!requirements) return result; - - const signed = await signPayment({ - privateKey, - to: requirements.payTo as `0x${string}`, - amount: requirements.price, - chainId: requirements.chainId, - }); - - const paymentSignature = buildPaymentPayload(signed); - - log( - `[x402] paying $${requirements.price} USDC → ${requirements.payTo} (chain ${requirements.chainId}) from ${signed.from}` - ); - - const finalResult = await handler({ ...params, paymentSignature }); - - const finalText = finalResult?.content?.[0]?.text ?? ''; - let finalParsed: Record = {}; - try { - finalParsed = JSON.parse(finalText); - } catch { - /* not JSON */ - } - - if (finalParsed['code'] === 402) { - log(`[x402] payment verification failed: ${finalParsed['reason'] ?? finalText}`); - } else { - log(`[x402] payment accepted — tool executed successfully`); - } - - return finalResult; - }; -} diff --git a/src/payment.ts b/src/payment.ts index 4b7cc07..81cd911 100644 --- a/src/payment.ts +++ b/src/payment.ts @@ -1,115 +1,19 @@ -/** - * Payment module — x402 per-tool payment gating - * - * Configures chain addresses and maps every Indigo MCP tool to a pricing tier. - * Import this module before registering tools so configure() and setToolPrices() - * are in effect when withX402 wrappers evaluate their first call. - */ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { wrapWithSplitPayment } from '@qbtlabs/x402/split'; -import { configure, setToolPrices } from '@qbtlabs/x402'; +export const FREE_TOOLS: string[] = []; -configure({ - evm: { address: process.env.X402_EVM_ADDRESS! }, - cardano: process.env.X402_CARDANO_ADDRESS - ? { address: process.env.X402_CARDANO_ADDRESS } - : undefined, - // PAYMENT_SERVER is an alias for the facilitator URL (mirrors openmm-mcp convention) - facilitatorUrl: - process.env.PAYMENT_SERVER ?? - process.env.X402_FACILITATOR_URL ?? - 'https://x402.org/facilitator', - testnet: process.env.X402_TESTNET === 'true', -}); +export function applyPaymentGate(server: McpServer): void { + const privateKey = process.env.X402_PRIVATE_KEY as `0x${string}` | undefined; + if (!privateKey) return; -setToolPrices({ - // ── Read-only tools: protocol state, prices, positions ────────────────── - // Analytics - get_tvl: 'read', - get_apr_rewards: 'read', - get_apr_by_key: 'read', - get_dex_yields: 'read', - get_protocol_stats: 'read', + const workerUrl = + process.env.PAYMENT_SERVER ?? process.env.X402_FACILITATOR_URL ?? 'https://mcp.openmm.io'; - // Asset / price feeds - get_assets: 'read', - get_asset: 'read', - get_asset_price: 'read', - get_ada_price: 'read', - get_indy_price: 'read', - - // CDP read - get_all_cdps: 'read', - get_cdps_by_owner: 'read', - get_cdps_by_address: 'read', - analyze_cdp_health: 'analysis', - - // Collector / IPFS read - get_collector_utxos: 'read', - retrieve_from_ipfs: 'read', - - // DEX - get_steelswap_tokens: 'read', - get_steelswap_estimate: 'read', - get_iris_liquidity_pools: 'read', - get_blockfrost_balances: 'read', - - // Governance - get_protocol_params: 'read', - get_temperature_checks: 'read', - get_polls: 'read', - - // Redemption - get_order_book: 'read', - get_redemption_orders: 'read', - get_redemption_queue: 'read', - - // Stability pool read - get_stability_pools: 'read', - get_stability_pool_accounts: 'read', - get_sp_account_by_owner: 'read', - - // Staking read - get_staking_info: 'read', - get_staking_positions: 'read', - get_staking_positions_by_owner: 'read', - get_staking_position_by_address: 'read', - - // ── Write tools: transaction builders / on-chain mutations ────────────── - // CDP write - open_cdp: 'write', - deposit_cdp: 'write', - withdraw_cdp: 'write', - close_cdp: 'write', - mint_cdp: 'write', - burn_cdp: 'write', - leverage_cdp: 'write', - - // CDP liquidation - liquidate_cdp: 'write', - redeem_cdp: 'write', - freeze_cdp: 'write', - merge_cdps: 'write', - - // ROB write - open_rob: 'write', - cancel_rob: 'write', - adjust_rob: 'write', - claim_rob: 'write', - redeem_rob: 'write', - - // Stability pool write - process_sp_request: 'write', - annul_sp_request: 'write', - create_sp_account: 'write', - adjust_sp_account: 'write', - close_sp_account: 'write', - - // Staking write - open_staking_position: 'write', - adjust_staking_position: 'write', - close_staking_position: 'write', - distribute_staking_rewards: 'write', - - // IPFS write - store_on_ipfs: 'write', -}); + wrapWithSplitPayment(server as any, { + privateKey, + workerUrl, + testnet: process.env.X402_TESTNET === 'true', + freeTools: FREE_TOOLS, + }); +} diff --git a/src/server.ts b/src/server.ts index 1ed3917..d70a9c2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,11 +3,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { createServer as createHttpServer } from 'node:http'; import { randomUUID } from 'node:crypto'; -import { withX402 } from '@qbtlabs/x402'; import { registerTools } from './tools/index.js'; import { registerResources } from './resources/index.js'; -import { withAutoPayment } from './payment-client.js'; -import './payment.js'; +import { applyPaymentGate } from './payment.js'; const SERVER_NAME = 'indigo-mcp'; const SERVER_VERSION = '0.2.0'; @@ -18,20 +16,7 @@ export function createServer(): McpServer { version: SERVER_VERSION, }); - // Intercept server.tool to auto-apply withX402 around every handler. - // This avoids modifying each of the 19 tool files individually. - // withX402 is a no-op when X402_EVM_ADDRESS (or any chain address) is not set. - const originalTool = server.tool.bind(server); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (server as any).tool = function (name: string, ...rest: unknown[]): unknown { - const lastIdx = rest.length - 1; - if (typeof rest[lastIdx] === 'function') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - rest[lastIdx] = withAutoPayment(withX402(name, rest[lastIdx] as any)); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (originalTool as any).apply(server, [name, ...rest]); - }; + applyPaymentGate(server); registerTools(server); registerResources(server);