Graph Uniswap MCP
# Graph Uniswap MCP
[](https://www.npmjs.com/package/graph-uniswap-mcp)
[](https://www.npmjs.com/package/graph-uniswap-mcp)
[](LICENSE)
[](https://nodejs.org)
**Simulate a Uniswap swap before you make it — with no RPC, no node, and no key beyond a free Graph API key.**
One MCP interface over **Uniswap V2, V3 and V4** across **Ethereum, Arbitrum, Base, Polygon, Optimism and BSC**, powered by [The Graph](https://thegraph.com).
```
quote_swap pool=0x8ad5…e6d8 tokenIn=WETH amountIn=100
{ "quotable": true, "amount_out": 223661.14, "token_out": "USDC",
"effective_price": 2236.61, "spot_price": 2250.80,
"price_impact_pct": 0.6291, "fee_pct": 0.3, "ticks_crossed": 2 }
```
## Why this exists
Most Uniswap tooling can tell an agent what a pool *is*. Very little can tell it what a trade
would actually *cost* — and that is the only question that precedes moving money.
Uniswap's own [routing-api](https://github.com/uniswap/routing-api) answers it, but it is
self-host-only: an AWS CDK deployment plus RPC provider keys. `@uniswap/v3-sdk` answers it too,
but it drags ~300MB of Solidity build artifacts (hardhat → `@nomicfoundation/edr-*` prebuilt
binaries) into `node_modules`, which is a poor trade for a server launched with `npx`.
Everything the math needs — `sqrtPrice`, `tick`, `liquidity`, `feeTier`, and the initialised
`ticks` — is already in the subgraph. So this server fetches that and runs the protocol's own
concentrated-liquidity math locally, in ~200 lines of BigInt with **zero runtime dependencies**
beyond the MCP SDK and zod.
**We do not ask you to trust that.** `test/fixtures/v3-quote-oracle.json` holds real mainnet pool
state plus expected outputs *generated by `@uniswap/v3-sdk`*, and `npm test` asserts every case
matches **to the wei** — including trades that cross 26 initialised ticks. Not "close". Identical.
Rounding direction is exactly where a reimplementation of this math goes wrong, so the test
tolerates none.
## What it refuses to do
The failure that matters for a pre-trade tool is not an error — it is a confident wrong number,
because the caller acts on it and never finds out. So `quote_swap` returns `quotable: false` with
a reason instead of a figure when it cannot be trusted:
| Situation | Why it refuses |
|---|---|
| **V4 pool with a hook** | Hooks can override the fee and the pricing curve, so simulating the vanilla curve would be wrong. On Base the top V4 pools by volume are hook-driven with zero in-range liquidity — this is the common case, not an edge case. |
| **Trade larger than the liquidity in view** | The tick window is ±20,000 ticks. A trade that consumes all of it gets a `lower_bound_amount_out`, explicitly not a quote. |
| **Zero in-range liquidity** | Nothing to swap against at the current tick. |
| **No `ticks` entity on the deployment** | The liquidity curve is unknown. Every mapped market now exposes ticks except V4 on Base. |
## TVL is reported as `null`, deliberately
Uniswap V3/V4 native subgraphs accumulate `totalValueLockedUSD` from per-event deltas, and it
drifts. Measured 2026-08-19 on the canonical USDC/WETH 0.3% pool:
| Source | USDC | WETH |
|---|---|---|
| On-chain balance | 6,110,083 | 5,337 |
| Native V3 subgraph | **138,939,548** | **61,693** |
That is 22.7x high on a blue-chip pool, so "spam-token pools inflate TVL" — the usual explanation,
and the one earlier versions of this README gave — does not account for it. Prior versions shipped
that number with a caveat string attached. Agents read fields, not prose, so the caveat did nothing.
`tvl_usd` is now `null` on V3/V4 paths. V2 `reserve0`/`reserve1` are real token balances and are
still returned. **For actual depth, use `quote_swap`** — simulating the trade is a truthful answer
to "how deep is this pool", and a fabricated scalar is not.
## Tools
| Tool | What it does |
|---|---|
| **`quote_swap`** | **Simulate an exact-input swap: amount out, effective price, price impact, ticks crossed** |
| `list_markets` | Every version×chain and its backing subgraph |
| `discover_markets` | Re-resolve live subgraphs; with `apply:true`, replace a market **only if it is broken** |
| `get_token_price` | USD price of a token (symbol or address) |
| `top_pools` | Top pools on a chain, ranked by volume |
| `find_pool` | Pool(s) for a token pair, either order |
| `pool_info` | Full stats for one pool/pair |
| `recent_swaps` | Newest swaps, optionally scoped to a pool |
| `raw_query` | Arbitrary GraphQL against the resolved subgraph |
All tools except `list_markets` take `chain` plus an optional `version` (`v2`/`v3`/`v4`). Chain
aliases (`eth`, `arb`, `matic`, `bnb`, …) are accepted.
### Coverage, precisely
| Chain | V2 | V3 | V4 | `quote_swap` |
|---|---|---|---|---|
| Ethereum | ✅ | ✅ **default** | ✅ pin `v4` | V3 + V4 |
| Arbitrum | — | ✅ **default** | ✅ pin `v4` | V3 |
| Base | ✅ | ✅ **default** | ⚠️ pin `v4` | V3 |
| Polygon | — | ✅ **default** | ✅ pin `v4` | V3 + V4 |
| Optimism | — | ✅ **default** | ✅ pin `v4` | V3 |
| BSC | — | ✅ **default** | ✅ pin `v4` | V3 |
Omit `version` and you get **V3** wherever it exists. Earlier versions ranked by query traffic,
which picked the *most queried* market rather than the most useful one: `chain:"base"` landed on
V4, whose top pools by volume are zero-liquidity hook pools reporting billions in fabricated
volume. Pinning a version always overrides this.
`⚠️` Base V4 is servable when pinned, but is not chosen for you: its top pools by volume are
hook pools with zero in-range liquidity. Uniswap V4 on **Ethereum** is the opposite — real
hookless pairs (USDC/USDT, WBTC/cbBTC, ETH/USDC) with a `ticks` entity, so `quote_swap` works
there too.
Pool field names are **detected, not assumed**: V3 uses `feeTier`/`sqrtPrice`/`feesUSD` while
uniswap-v4-ethereum uses `fee`/`sqrtPriceX96`/`totalFeesUSD` and exposes `tickSpacing`. A new
deployment with either convention works without a code change.
Ranking is always by `volumeUSD`, never TVL — a single spam-token pool can claim trillions in fake
liquidity, so a TVL sort returns junk at the top. This holds even on subgraphs whose per-pool TVL
is accurate.
### discover_markets only heals what is broken
`apply: true` used to install the top GRT-signal name match unconditionally, which could displace
Uniswap Labs' canonical deployment with a stranger and break the market for the rest of the
session — invisibly, since `list_markets` still showed the old id. It now probes the incumbent
first, keeps it if healthy, and probes any replacement before installing it. Candidates are always
listed in `live_signal_candidates` whether or not anything is applied.
## Setup
**1. Get a free Graph API key** at <https://thegraph.com/studio> (Billing → API Keys — first
100k queries/month are free).
**2. Add the server to your MCP client.** No install needed — `npx` fetches the published build.
**Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json`
(Windows: `%APPDATA%\Claude\claude_desktop_config.json`):
```json
{
"mcpServers": {
"uniswap": {
"command": "npx",
"args": ["-y", "graph-uniswap-mcp"],
"env": { "GRAPH_API_KEY": "your_key_here" }
}
}
}
```
**Claude Code:**
```bash
claude mcp add uniswap -e GRAPH_API_KEY=your_key_here -- npx -y graph-uniswap-mcp
```
## Try it
> "What would selling 100 WETH into the USDC/WETH 0.3% pool on Ethereum actually get me?"
> "Compare price impact for a 50 ETH sell across the 0.05% and 0.3% pools."
> "Find the USDC/WETH pools on Base and show me recent swaps."
## Accuracy envelope
Be precise about what the number means:
- The simulation runs against the subgraph's **most recently indexed block**, not the pending
state. Real execution depends on the state at inclusion — MEV, pending swaps and indexer lag all
move it.
- It is a **single-pool** simulation. It does not split across pools or hops; it is not a router.
- Exact-input only.
- V2 uses constant-product math (exact). V3/V4 use tick-walking math, wei-exact versus
`@uniswap/v3-sdk` on the pinned oracle.
## Development
```bash
npm ci
npm run build
npm test # offline gate: MCP handshake + tool registry + wei-exact swap math
npm run test:live # deeper checks against live subgraphs (needs GRAPH_API_KEY)
```
To regenerate the oracle fixture (the only thing that needs the Uniswap SDK installed):
```bash
npm i --no-save @uniswap/v3-sdk @uniswap/sdk-core
node scripts/gen-oracle.mjs
```
## License
MIT
TDQS
Scored across 8 tools
Each tool targets a distinct purpose: market listing, pool details, swaps, custom queries, market discovery, token price, top pools, and pool finding. No two tools have overlapping functionality, ensuring clear selection.
All tool names use snake_case, but the verb + noun pattern is not uniform. For example, 'list_markets' and 'get_token_price' are verb-oriented, while 'pool_info' and 'top_pools' are noun-oriented. This minor inconsistency slightly reduces clarity.
With 8 tools, the server is well-scoped for querying Uniswap data. Each tool adds value without redundancy, covering market discovery, pool analytics, swaps, and token pricing. The count feels natural for the domain.
The tool set covers core Uniswap operations: listing markets, pool details, swaps, token price, top pools, and pool finding. Missing are token metadata (e.g., symbol, decimals) and historical data beyond recent swaps. The 'raw_query' escape hatch partially compensates.