sand0vvv

mayan-mcp

Community sand0vvv
Updated

MCP server for Mayan Finance cross-chain swaps. Read-only and unsigned transaction building: never holds keys, never signs, never broadcasts.

mayan-mcp

An MCP server for Mayan Finance cross-chain swaps.

Ask an AI assistant what a swap will cost, then get the exact transaction to sign — between Solanaand seven EVM chains, through Mayan's Swift and MCTP routes.

This server holds no private keys. It cannot sign and it cannot broadcast. build_swap returnsan unsigned transaction; the caller signs it in their own wallet. There is no tool here that canmove funds.

Every build is cross-checked against its quote before it is returned

Before any transaction is handed over, its own encoded arguments are read back and compared againstthe quote you were shown — the token, the amount, and the contract it forwards to. A payload thatspends more than it quoted is refused rather than returned.

Those are real numbers, not a mockup: scripts/capture-demo.ts drives the built server over stdioagainst the live API and writes what came back to docs/captured-output.json,which is what the image above renders. Run it yourself and compare — a tool whose claim is "checkwhat you are signing" should be checkable itself.

Custody model

Reads quotes, tokens, swap status yes
Builds transactions yes, unsigned
Holds private keys or seed phrases no
Signs transactions no
Broadcasts to a chain no

The guarantee is structural rather than procedural: there is no key material in the process, sothere is no code path from a compromised prompt to a moved balance. The signing step belongs to thewallet, which is also where the user sees the recipient address one last time.

Two consequences worth stating plainly:

  • Tool annotations are not the guarantee. This server sets readOnlyHint, but the MCPspecification requires clients to treat annotations from an untrusted server as untrusted. Theclaim that matters is "no keys in the process", which you can check by reading src/.
  • A prompt injection cannot move funds through this server, but it can still propose a bad swap.Every response echoes the recipient address, the minimum received and the slippage next to thepayload, so the terms are visible before anyone signs.

See SECURITY.md for the threat model, the invariant checks, and a reachabilityanalysis of the current npm audit output.

Install

Requires Node.js 20 or newer.

npx @sand0vvv/mayan-mcp --help

Claude Desktop / Claude Code

{
  "mcpServers": {
    "mayan": {
      "command": "npx",
      "args": ["-y", "@sand0vvv/mayan-mcp"],
      "env": {
        "SOLANA_RPC_URL": "https://your-solana-rpc-endpoint"
      }
    }
  }
}

Cursor / other MCP clients

Same command, over stdio: npx -y @sand0vvv/mayan-mcp.

Streamable HTTP

npx @sand0vvv/mayan-mcp --http --port 8787

Stateless — a fresh server per request, no session state. It binds to 127.0.0.1 and validates theHost and Origin headers, so a page in your browser cannot reach it by pointing its own hostname atloopback. There is no authentication: exposing it on another interface (--host 0.0.0.0) is adeliberate act that belongs behind a proxy providing auth and rate limiting.

Configuration

Everything comes from the environment. Nothing is required to get a quote.

Variable Required Purpose
SOLANA_RPC_URL for Solana builds Building a Solana-source swap needs a live RPC connection. Without it the server falls back to the public mainnet endpoint, which throttles hard.
MAYAN_RPC_URL_<CHAIN> for allowance reads Per-chain EVM endpoint, e.g. MAYAN_RPC_URL_BASE. Lets the server read your existing allowance instead of assuming an approve is needed. Read-only: the only call made is eth_call.
MAYAN_API_KEY no Lifts Mayan's per-IP rate limit. Request one from [email protected].
MAYAN_HTTP_TIMEOUT_MS no Default 30000.
MAYAN_MAX_RETRIES no Default 3. Retries are full-jitter backoff, reads only.
MAYAN_TOKEN_CACHE_TTL_MS no Default 600000.
MAYAN_REFERRER_EVM / MAYAN_REFERRER_SOLANA no Attaches a fee-split referrer to quotes. Unset by default, and left unset in this project — see SECURITY.md.
MAYAN_MCP_DEBUG no 1 for verbose logging on stderr.

There is no variable for a private key, and there never will be.

Tools

Tool What it does
get_quote Every available route for a swap: expected output, guaranteed minimum, slippage, fees, ETA.
build_swap The unsigned transaction, plus the quote terms and any ERC20 approval needed first.
list_tokens Tokens Mayan can route on a chain, with addresses and decimals.
track_order Status of a swap by its source-chain transaction hash.
check_allowance How much the Mayan forwarder is currently allowed to spend, and whether that covers a swap.
list_chains Supported chains and their EVM chain ids.
estimate_gas Gas estimate for a built EVM transaction.

Supported chains: solana, ethereum, base, arbitrum, optimism, polygon, bsc,avalanche.

Worked example

"What do I get for 0.5 SOL into USDC on Base?"

get_quote → two routes:

0.5 SOL on solana -> USDC on base

  SWIFT V2 · Swift
    expected  37.137748 USDC
    minimum   36.933584 USDC  (slippage 55 bps)
    eta       3s
    price     GOOD (ratio 0.9996)
    quote id  0xa4c71a85927b104fe039e05d56efe944

  FAST_MCTP · Best Price
    expected  37.155133 USDC
    minimum   36.954199 USDC  (slippage 50 bps)
    eta       35s
    price     GOOD (ratio 0.9996)

"Build it."

build_swap → the transaction, unsigned:

Unsigned swap built: 10 USDC on base -> SOL on solana via SWIFT

  You receive at least 0.130172143 SOL (expected 0.134198086, slippage 300 bps, ETA 3s)
  Recipient: 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM

  Transaction to sign:
    to       0x337685fdaB40D39bd02028545a4FfA7D287cC3E2
    chainId  8453
    value    0x0
    data     932 bytes (forwardERC20)

  Checked before returning it:
    ok target is the Mayan forwarder from the SDK constant
    ok chain id matches the quote
    ok the call spends USDC, the token quoted
    ok the call spends 10000000 base units, the amount quoted
    ok forwards to 0x40fFE85A28DC9993541449464d7529a922142960, the SWIFT contract the quote declared

  Approval required BEFORE this transaction:
    approve 10000000 (base units) of USDC
    spender 0x337685fdaB40D39bd02028545a4FfA7D287cC3E2   (Mayan forwarder)

  Signers returned: 0. Unsigned. This server holds no private keys and cannot sign or broadcast.

Those checks are not decoration. The transaction is assembled from data Mayan's backend served, so theSDK's own encoded arguments are read back and compared against the quote you were shown: the token,the amount, and the protocol contract. A payload that spends more than it quoted, or routes through asubstituted contract, is refused rather than returned. See SECURITY.md.

structuredContent carries the same thing machine-readably: evm_transaction, approval, routeand a verification block recording which invariants were checked.

Solana output

Two things are different when the source chain is Solana, both stated here rather than left to bediscovered:

  • The cross-check above does not apply. It reads an EVM call's encoded arguments; a Solana buildis a list of instructions with no such envelope. Those fields are therefore absent from theverification block rather than reported as failures. What still holds is the route allow-list andthe empty signer set — the custody claim itself. Review the instructions in your wallet.
  • MCTP from Solana cannot be built here. That route needs an ephemeral keypair to carry itsCCTP message, and this server does not emit key material of any kind. SWIFT from Solana buildswithout one and is what an unpinned build_swap selects.

A Solana-source swap returns instructions in the SDK's own InstructionInfo shape, so the outputfeeds straight back into deserializeInstructionInfo() without a translation step:

{
  "programId": "ComputeBudget111111111111111111111111111111",
  "accounts": [],
  "data": "AsBcFQA="
}

Address lookup tables come back as base58 addresses, ready forconnection.getMultipleAccountsInfo().

What the caller still has to do

  1. Send the approval first on EVM sources, or the swap reverts. build_swap tells you thetoken, the amount and the spender. With MAYAN_RPC_URL_<CHAIN> set it reads your currentallowance and says nothing when you have already approved enough. (USDT on Ethereum additionallyrequires zeroing an existing allowance before raising it — the response says so when it applies.)
  2. Sign and submit. Not this server's job, by design.
  3. Check the recipient address in your wallet before signing.

Development

npm install
npm run build
npm test          # unit tests: invariants, serialisation round-trip, schemas, layering
npm run typecheck

npx tsx scripts/smoke.ts   # live: quotes and builds against Mayan's API
npx tsx scripts/e2e.ts     # live: spawns the built server and speaks MCP over stdio

The layout keeps one boundary strictly:

src/client/      the only code that talks to @mayanfinance/swap-sdk
src/tools/       MCP tool definitions — Zod in, structuredContent out
src/transport/   stdio and Streamable HTTP

test/layering.test.ts fails the build if anything above src/client imports the SDK or a chainlibrary directly, and it verifies its own detector fires before trusting the result.

License

MIT. See LICENSE.

Not affiliated with Mayan Finance. This is client software for their public API; it makes nowarranty about routing, pricing or settlement, and it is not financial advice.

MCP Server · Populars

MCP Server · New