MCP for AI agents

Your own MCP server for AI agents

SwapZilla already publishes an open-source MCP server, and for most agents that is the answer. Your own is what you build when the swaps have to be attributed to your key and bounded by your rules rather than the agent's judgement.

  • Ready-madeswapzilla-mcp · MIT · Node 18+
  • Tools it exposes6, from assets to order status
  • Your own addsyour PartnerID, limits, approvals
  • Repeat-call guardIdempotency-Key per intent

Built on/v1/assets/v1/partner/quotes/v1/validate-address/v1/partner/orders/v1/partner/orders/{id}

There are two ways to give an AI agent SwapZilla, and the fast one is already published. swapzilla-mcp is an open-source (MIT, Node 18+) Model Context Protocol server that runs in one line and needs no key — that is /mcp/. Build your own instead when the swaps have to carry your partner key, your allow-list and your spending limits.

  • Ready-made: six tools, no account, no key, non-custodial by design.
  • Your own: orders tagged with your PartnerID and your client_order_id.
  • Either way, creating an order moves no money — it returns an address.
  • The key, if there is one, lives in the server process and never in the model's context.

The published server, in one line

npx -y https://swapzilla.io/mcp/swapzilla-mcp-1.0.0.tgz

Register it with any MCP client — Claude Desktop, Claude Code, Cursor, Windsurf — and the agent gets six tools:

ToolWhat the agent does with it
swapzilla_list_assetsResolve the exact asset code and network before anything else.
swapzilla_list_providersSee which exchanges are aggregated and enabled.
swapzilla_get_quoteCompare live offers across every provider; each carries an ID.
swapzilla_validate_addressCheck the recipient address for that network first.
swapzilla_create_exchangeOpen the swap and return a DepositAddress.
swapzilla_get_orderPoll status through to the payout transaction hash.

Two environment variables configure it: SWAPZILLA_API_BASE (defaults to https://api.swapzilla.io) and an optional SWAPZILLA_API_KEY, which it sends as X-API-Key. Public rate data needs neither. Install instructions, the client config block and the package itself are on /mcp/.

When to host your own instead

The published server is deliberately plain: it is the aggregator, exposed as tools, for whoever is running the agent. A partner integration usually wants things it does not have — and every one of them is a reason to wrap the partner API yourself.

You needWhy your own server
The swaps to be yoursCalls made with a partner key are tagged with your PartnerID, and GET /v1/partner/orders lists them — attribution and reconciliation you cannot get from an agent's own machine.
An allow-list of destinationsAn agent that can name any to_address can pay anyone. The check belongs in a tool you control, not in a prompt.
Spending capsPer intent and per day, enforced in code before the order is created.
A human in the loopYour create_swap can block on an approval your product already has.
Your own identifiersclient_order_id ties each swap to the intent, ticket or user that caused it.
Safety against retriesAn Idempotency-Key per intent turns a retried tool call into the same order, not a second swap.

Your own server, in about fifty lines

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const BASE = "https://api.swapzilla.io";
const KEY = process.env.SWAPZILLA_KEY!;              // stays in this process
const server = new McpServer({ name: "acme-swaps", version: "1.0.0" });

server.tool(
  "quote_swap",
  {
    from_asset: z.string(), from_network: z.string(),
    to_asset: z.string(), to_network: z.string(),
    amount_from: z.string().optional(), amount_to: z.string().optional(),
    rate_type: z.enum(["floating", "fixed"]).default("floating"),
  },
  async (args) => {
    const r = await fetch(`${BASE}/v1/partner/quotes?${new URLSearchParams(args)}`, {
      headers: { "X-API-Key": KEY },
    });
    const { offers } = await r.json();
    // Hand back only what a model should reason about, not the whole payload.
    return { content: [{ type: "text", text: JSON.stringify(offers.slice(0, 5).map((o) => ({
      id: o.ID, provider: o.ProviderName, receive: o.ToAmount,
      minutes: o.EstimatedMins, kyc: o.KycRating, worse_by_percent: o.DeviationPercent,
    }))) }] };
  },
);

await server.connect(new StdioServerTransport());

The writing tool is the same shape with the guardrails wrapped around it:

server.tool(
  "create_swap",
  { offer_id: z.string(), to_address: z.string(), intent_id: z.string() },
  async ({ offer_id, to_address, intent_id }) => {
    assertAllowed(to_address);                       // your allow-list
    await requireApproval(intent_id);                // your human, or your policy

    const r = await fetch(`${BASE}/v1/partner/orders`, {
      method: "POST",
      headers: {
        "X-API-Key": KEY, "Content-Type": "application/json",
        "Idempotency-Key": intent_id,                // one intent, one order
      },
      body: JSON.stringify({
        offer_id, to_address,
        refund_address: process.env.TREASURY_ADDRESS,
        client_order_id: intent_id,
      }),
    });
    const order = await r.json();
    return { content: [{ type: "text", text:
      `Send exactly ${order.AmountFrom} ${order.FromAsset} on ${order.FromNetwork} ` +
      `to ${order.DepositAddress}. Order ${order.ID}.` }] };
  },
);

The read-only tools — assets, quotes, address validation, status — are safe to let an agent call freely. Only the one that creates an order needs a gate in front of it.

Guardrails worth having

  • Allow-list the destination. Restrict to_address to addresses your system already knows, and reject the rest inside the tool.
  • Cap the amounts. Per intent and per day, checked in the server. A model's judgement is not a spending limit.
  • One intent, one idempotency key. Agents retry; the key is what keeps a retry from becoming a second swap.
  • Quote and confirm inside five minutes. Offers expire, so a plan approved twenty minutes later must be re-quoted — and the tool should say so rather than silently repricing.
  • Never put the key in the model's context. It lives in the server process, is never returned by a tool and never rendered into a prompt.
  • Log ProviderOrderID. It is the reference the provider's support recognises when a swap needs chasing.

The strongest guardrail is structural. The API is non-custodial: creating an order produces a deposit address and nothing else. Unless the same agent can also send the deposit, the worst it can do is open an order nobody funds — which expires by itself.

What neither server can do

Not availableWhat that means
Push notificationsNo webhooks — status is a polled tool call, and a waiting agent should poll every 5–10 seconds, not faster.
CancellationAn order cannot be cancelled through the API. An unfunded one expires on its own.
Moving fundsBy design. Every swap is funded by a wallet outside the agent's reach.
A sandboxNot in production; for deterministic tests we can enable the mock provider on your key.
Wallet risk scoringNo AML or graph analysis anywhere in the API, so an agent cannot ask whether an address is risky.

Questions

Does SwapZilla have an official MCP server?

Yes — swapzilla-mcp, open-source under MIT, running on Node 18+ and installed in one line with npx -y https://swapzilla.io/mcp/swapzilla-mcp-1.0.0.tgz. It exposes six tools and needs no account or API key for public rate data. Everything about installing it is on /mcp/; this page is about hosting your own when you need your key and your limits in the loop.

When is the published server not enough?

When the swaps have to be attributed to your partner id, when destinations must come from an allow-list, when spending needs a cap or a human approval, or when each swap has to carry your own client_order_id for reconciliation. All of that lives in the server process, so it means running your own.

Can an AI agent spend my money through this API?

Not on its own. The swap is non-custodial: creating an order returns a deposit address and moves nothing. Funds move only when a wallet sends that deposit, which is a separate action outside the API. Keep the deposit-sending capability away from the agent and the worst case is an unfunded order that expires.

How do I stop an agent from retrying itself into two swaps?

Give every intent an id and pass it as the Idempotency-Key when creating the order. A repeated call with the same key returns the original order rather than opening a second one.

How long is a quote valid for an agent to act on?

Five minutes. After that, creating an order with the offer id fails with 400 and the tool has to re-quote. Surface that expiry to the model so a slowly approved plan gets re-priced rather than failing mid-execution.

Should the agent see the raw API responses?

Better not. Offers carry more fields than a model needs; returning a trimmed shape — provider, amount received, minutes, KYC rating, how much worse than the best — keeps reasoning cheap and stops incidental values from turning into hallucinated guarantees.

Getting a key

Register in the partner cabinet and your partner id and the api_key that goes in X-API-Key are issued at once. The key starts working once it is activated: write to @swapzilla_support_bot with a sentence about what you are building. Every endpoint, field and status is in the partner API reference.