Skip to main content
Glama
README.md
# cardano-defi-mcp — Cardano DeFi taxonomy + MCP server

Open-source proof of concept for the **Cardano PRIME** grant. Two things in one small package:

1. **A taxonomy index of Cardano DeFi venues** — 11 hand-researched venue records (DEXes, lending,
   CDPs, stablecoins, perps) with categories, assets, capabilities and honest caveats, validated by a
   zod schema.
2. **An MCP server** that gives an AI agent rails into Cardano DeFi: discover venues, get cross-chain
   swap quotes into ADA, read balances and positions, and receive **unsigned** transactions the agent
   signs with its own keys.

The **BazaarSwap routing backend is a separate, closed-source service**. This repo is only an HTTP
client of it (`BAZAAR_API_URL`); nothing in here does routing, and nothing in here is a wallet.

## Quickstart

```bash
npm i
cp .env.example .env    # fill in BLOCKFROST_PROJECT_ID; the rest have working defaults
npm test                # vitest, fully mocked — no network
npx tsx src/server.ts   # MCP server on stdio
```

### Environment

| Variable | Required for | Default |
| --- | --- | --- |
| `BLOCKFROST_PROJECT_ID` | `get_balance`, `open_cdp`, `close_cdp` | — (clear error if unset) |
| `BAZAAR_API_URL` | `get_quote`, `build_swap_tx` | `http://localhost:3001` |
| `LIQWID_GRAPHQL_URL` | `get_market_data`, Liqwid `get_position` | `https://v2.api.liqwid.finance/graphql` |
| `CARDANO_NETWORK` | Blockfrost / Indigo host selection | `mainnet` (`mainnet` \| `preprod`) |
| `INDIGO_API_URL` | Indigo `get_position` | `https://analytics.indigoprotocol.io` |
| `INDIGO_SYSTEM_PARAMS_URL` | `open_cdp`, `close_cdp` | — (or `INDIGO_SYSTEM_PARAMS_FILE`) |

Every read tool except `get_balance` and the Indigo tools works with no API keys at all.

### Register with Claude Code

Copy `.mcp.json.example` to `.mcp.json` in your project root, fill in the env placeholders, and
restart Claude Code. Then `/mcp` should list `cardano-defi-mcp` with nine tools.

## Tools

| Tool | What it does | Funds |
| --- | --- | --- |
| `list_venues` | List/filter indexed venues by category, asset, capability, or free-text `query` | read-only |
| `get_venue` | Full taxonomy record for one venue id | read-only |
| `get_quote` | Race the BazaarSwap backend for cross-chain routes; returns `best` + `all` by net output | read-only |
| `build_swap_tx` | Turn a `quoteId` into transaction data | **unsigned** |
| `get_balance` | ADA + native-asset balance of a Cardano address (Blockfrost) | read-only |
| `get_position` | Indigo CDPs or Liqwid loans for an address (`protocol: indigo \| liqwid`) | read-only |
| `get_market_data` | Liqwid supply APY / borrow APR / utilization per market | read-only |
| `open_cdp` | Lock ADA collateral, mint an iAsset on Indigo | **unsigned** |
| `close_cdp` | Burn iAsset debt, withdraw ADA collateral on Indigo | **unsigned** |

All tools return `{ content: [{ type: 'text', text: <pretty JSON> }] }`; failures return
`isError: true` with a one-line message and no stack trace.

## Security model

- **No keys, ever.** The server has no signing code path. For Indigo, the wallet is selected by
  address only (`lucid.selectWallet.fromAddress`), which can balance and build a transaction but is
  structurally incapable of signing one.
- **No funds, ever.** Nothing is custodied, pooled or forwarded.
- **No broadcasting.** The server never submits a transaction.
- **Unsigned CBOR out.** `open_cdp` / `close_cdp` return full transaction CBOR hex with an empty
  witness set — exactly the input a CIP-30 `signTx` expects — plus a human-readable `description` of
  what the transaction does, so the agent (or the human behind it) can check before signing.
- Secrets come from the environment and are never logged. `stdout` carries only JSON-RPC; all
  diagnostics go to `stderr`.

## Limitations (read this before trusting it)

- **Liqwid is read-only.** Liqwid v2's supply/borrow/repay actions go through the protocol's
  off-chain batcher with no documented public transaction-building API or SDK. Building those
  transactions would mean reverse-engineering an undocumented batcher contract, which is not
  something a PoC should ship. Rates and loan positions are read live from the public GraphQL API.
- **Indigo's Pyth-oracle path leans on Indigo's analytics API.** `open_cdp` resolves the collateral
  price oracle from the collateral asset's datum. `OracleNft` and `Delisted` are handled directly.
  Indigo's newer `DeferredValidation` path needs a *signed* Pyth Lazer price message, which needs a
  Pyth Lazer access token this server has no business holding — so it proxies the signed message
  and the Pyth state UTxO from Indigo's public, unauthenticated analytics API
  (`/api/v3/assets/{iasset}/ada/price`, `/api/v3/pyth-state/utxo`), the same route Indigo's own
  `indigo-mcp` takes. Those messages expire 280 s after their timestamp, so the transaction must be
  signed and submitted promptly after it is built.
- **Indigo needs `INDIGO_SYSTEM_PARAMS_URL`.** The Indigo SDK ships no default SystemParams and
  Indigo publishes no documented stable URL for the file, so the tools refuse to guess.
- **Indigo position reads use the analytics API, not the SDK.** The SDK has no "find CDPs by owner"
  helper — only datum parsing, which would mean scanning every UTxO at the CDP validator. The
  analytics endpoint needs no Blockfrost key and returns the CDP out-ref that `close_cdp` needs.
- **Taxonomy asset lists are directional, not exhaustive.** Most venues publish no authoritative
  pool or market listing, so asset lists are the best-confirmed subset, not a complete index. Each
  venue's `notes` field states exactly what was and was not verified — read it rather than treating
  `assets` as ground truth.
- **RealFi is not on mainnet yet** (testnet at time of research; mainnet stated for late 2026), and
  its token tickers are unresolved between the live site (`USDrf`/`sUSDrf`) and press coverage
  (`USDr`/`sUSDr`). It is indexed for completeness, not because it is usable today.
- **Quotes are live and perishable.** `get_quote` can take up to ~25 s (it waits out a provider
  race) and quotes expire; build shortly after quoting.

## Layout

```
taxonomy/venues/*.json      one file per venue, validated by VenueSchema
src/taxonomy/               zod schema + loadVenues / listVenues / getVenue / searchVenues
src/adapters/               blockfrost, liqwid, indigo, swap (BazaarSwap API client)
src/tools/                  MCP tool registrations (thin: parse -> adapter -> JSON text)
src/server.ts               McpServer + StdioServerTransport
src/__tests__/              vitest; all network mocked
```

See `DESIGN.md` for the module contracts and the top of `src/adapters/indigo.ts` for where the
implementation deviates from the original design and why.

## Taxonomy explorer

`scripts/build-explorer.mjs` renders the venue records into a static page plus a machine-readable
`taxonomy.json` bundle. The output is generated, not committed — `docs/` is gitignored.

```bash
npm run build:explorer && open docs/index.html   # local preview over file://
```

The published version is built and deployed to GitHub Pages by `.github/workflows/pages.yml` on
every push to `main` that touches `taxonomy/` or `scripts/`. This requires the repository's
**Settings → Pages → Source** to be set to **GitHub Actions**.

## Add your protocol

The taxonomy is a community registry — a venue is one JSON file, and adding one takes a PR.

1. Fork the repo and create a branch named `taxonomy/<something>` (e.g. `taxonomy/add-myvenue`) —
   the PR gate rejects other branch names.
2. Add `taxonomy/venues/<id>.json`. The filename (minus `.json`) must equal the record's `id`, and
   the record must match [`src/taxonomy/schema.ts`](src/taxonomy/schema.ts). Copy an existing file
   such as `taxonomy/venues/minswap.json` as a starting point.
3. Run `npm run validate:taxonomy` locally — it needs no dependencies and names every problem.
4. Open a PR. CI validates the file and prints a summary of what changed. A maintainer applies the
   `taxonomy-addition` label after review; the PR cannot merge without it.

Keep `notes` honest: say what you verified and what you did not. Asset lists are expected to be
directional rather than exhaustive. On merge, the explorer and `taxonomy.json` republish
automatically.

## Try the reference agent

`npm run demo` runs `examples/reference-agent.ts`: a minimal MCP client that spawns this server over
stdio, lists its tools, and calls `list_venues`, `get_market_data` and `get_position` against live
public APIs. No API keys needed; network access is.

## Execute a real swap (advanced)

`examples/execute-swap.ts` is the other half of the story: the **agent-side signer**. The MCP server
quotes and builds but never signs — something outside it has to close the loop, and this example
shows what that something looks like. It moves real funds.

> **Use a burner wallet.** Fund it with exactly the amount you intend to trade and nothing more.
> This is example code for a proof of concept, not a production signer.

```bash
npm run execute -- gen      # new burner: appends PRIVATE_KEY to .env.local, prints only the address
npm run execute -- quote    # dry run: warms the API token cache, races quotes (no key needed)
npm run execute -- run      # quote → build → approve → summary, then STOPS
npm run execute -- run --yes  # the same, but signs and broadcasts
```

Configure the pair with flags or env (`.env.local` wins over `.env`):
`FROM_CHAIN` (1 or 42161), `FROM_TOKEN` (`native` or an ERC-20 address), `AMOUNT` (wei),
`DEST_ADDRESS` (`addr1…`), plus optional `TO_CHAIN` / `TO_TOKEN` / `SLIPPAGE` / `RPC_URL`.

- **`--yes` is the only way to broadcast.** Without it `run` prints the exact transaction — sender,
  target, value, calldata size, estimated ADA out, destination address — and exits with
  `DRY RUN — add --yes to broadcast`.
- **ERC-20 sources get an exact-amount approval**, only when the current allowance is short, and the
  script waits for that receipt before it signs the swap.
- After broadcasting it registers the hash with the API (`POST /status/register`, sending both the
  `quoteId` and the signed `tracking` token from the execute response) and polls
  `GET /status/{txHash}` every 15 s for up to 10 minutes, printing each transition until
  `complete` / `failed` / `untracked`. A failed swap exits non-zero.
- **The private key never touches the MCP server.** It is read by the example only, and the server
  is spawned with `PRIVATE_KEY` stripped from its environment. The server still has no signing code
  path — `viem` is a devDependency used by this example alone, never by `src/`.

## License

Apache-2.0

TDQS

A4.3/5.0

Scored across 9 tools

Disambiguation4/5

Tools are mostly distinct, but get_quote and build_swap_tx are tightly coupled (quote must precede build), and get_balance/get_position both read address state but for different purposes. Minor potential confusion between get_position and get_balance for an agent, but descriptions clarify domains.

Naming Consistency4/5

All verbs are in present tense (get, list, build, open, close) and nouns are clear (venue, quote, swap_tx, balance, position, market_data, cdp). The pattern is consistent, with minor deviation like 'list_venues' vs 'get_*' but still predictable. Overall coherent.

Tool Count5/5

9 tools is well-scoped for a Cardano DeFi server covering venue discovery, quoting/swap, balance/position reads, market data, and CDP lifecycle. Each tool has a clear purpose and no redundancy.

Completeness4/5

The surface covers venue discovery, read-only balances/positions, market data, swap quoting/building, and CDP open/close. Missing operations like updating or adjusting a CDP (adding collateral, minting more iAsset) and Liqwid loan lifecycle (borrow, repay) are notable gaps, but the core DeFi workflows are present.

Maintenance

ActivityMaintained
ResponsivenessNo issues