paraswap-mcp
by rustok-org
README.md
# paraswap-mcp
An MCP server that builds token swaps and hands them over **unsigned**.
It quotes a swap on [Paraswap](https://www.paraswap.io/) v5 and returns the raw
transactions (`to` / `data` / `value`) for someone else to execute — in
practice, a [Rustok](https://github.com/rustok-org) self-custody wallet, where a
human approves every transaction in a separate console before anything is
signed.
**This server holds no keys, signs nothing and sends nothing on-chain.** It has
no filesystem state and no secrets. The worst it can do is return a transaction
you decline to sign.
Chains: Ethereum (1), Arbitrum One (42161), Base (8453).
## Why it exists
An agent that can swap tokens usually needs a private key sitting in `.env`.
The alternative on offer is usually an agent that cannot touch funds at all.
The middle rung — the agent acts on its own, but a human releases every
transaction — needs the building of a transaction to be separate from the
signing of it. This server is the building half. The signing half belongs to a
wallet the agent does not control.
## Install
> The published image lands with the first release; until then, run from source
> (bottom of this section) — it needs nothing but Python 3.12+.
Pull the image (no Python install needed):
```bash
docker pull ghcr.io/rustok-org/paraswap-mcp:1.0.0
```
Register it as a stdio MCP server — for example, in an MCP client config:
```json
{
"mcpServers": {
"paraswap": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/rustok-org/paraswap-mcp:1.0.0"]
}
}
}
```
Podman works the same way; substitute `podman` for `docker`.
Running from source needs nothing but Python 3.12+ — the server imports only the
standard library:
```bash
python3 paraswap_mcp.py
```
## Configuration
Both variables are optional. Defaults are the three chains above.
| Variable | Meaning |
| --- | --- |
| `PARASWAP_ALLOWED_CHAINS` | Comma-separated chain ids the server will serve, e.g. `1,42161`. **An empty or malformed value refuses to start** — a server with zero chains is never intentional. To get the defaults, leave the variable unset. |
| `PARASWAP_RPC_URLS_<id>` | JSON-RPC endpoint used for the on-chain allowance check on that chain, e.g. `PARASWAP_RPC_URLS_1`. Public nodes are used when unset. Allowing a chain with no default and no override refuses to start. |
## The tool
`build_swap_transaction` — one call, one swap plan.
Input: `chain_id`, `src_token`, `src_decimals`, `dest_token`, `dest_decimals`,
`amount` (human units, e.g. `"5"`), `user_address`. Optional: `slippage_bps`
(default 250 = 2.5%), `src_symbol` / `dest_symbol`.
Output: a fresh quote, the allowance check, and an **ordered** list of raw
transactions. When the wallet's current allowance to the Paraswap
`TokenTransferProxy` is short, an **exact-amount** ERC-20 approve is prepended —
never an unlimited one. If a *partial* allowance is already in place, a reset to
zero comes first: USDT and the tokens that copied it revert an approve made over
a non-zero allowance. Execute the transactions in the order returned.
```jsonc
{
"chain": { "id": 42161, "name": "Arbitrum One" },
"quote": {
"src_amount": "5",
"src_token": { "address": "0xaf88…5831", "chain_id": 42161 },
"dest_amount_estimate": "0.002663",
"dest_token": { "address": "0x82aF…Bab1", "chain_id": 42161 },
"slippage_bps": 250,
"note": "quote is fresh now and expires in minutes; execute promptly"
},
"allowance_check": { "spender": "0x216b…fcae", "approve_included": true },
"transactions": [ { "purpose": "approve exactly …", "to": "…", "data": "0x…", "value": "0", "chain_id": 42161 } ]
}
```
## Things this server deliberately does not promise
Read these before putting it in front of real money.
- **Quotes go stale in minutes.** Build immediately before executing. If a human
approval is going to take a while, build again afterwards rather than
executing an old plan.
- **`src_symbol` / `dest_symbol` are your own labels, echoed back unverified.**
They are never read from the contract, and every echoed symbol is returned
alongside `"symbol_unverified": true`. The **address** is the identity of a
token; a screen shown to a human must not present these symbols as confirmed
fact.
- **The spender comes from the live quote, never from a hardcoded address.** The
Paraswap `TokenTransferProxy` differs per chain, and approving the wrong
contract wastes gas at best.
- **A wallet reporting "executed" means the transaction was broadcast, not that
it succeeded on-chain.** Verify the receipt independently (an explorer, or
`eth_getTransactionReceipt` — `status` must be `0x1`) until the wallet you use
checks receipts itself. A reverted swap still costs gas.
- **Prices, routes and liquidity come from Paraswap.** This server does not
audit them, and does not compare against other venues.
## Development
```bash
uv run --with ruff ruff check paraswap_mcp.py test_paraswap_mcp.py
uv run --with ruff ruff format --check paraswap_mcp.py test_paraswap_mcp.py
uv run --with mypy mypy --strict paraswap_mcp.py
uv run --with pytest pytest -q
```
Tests never touch the network: the HTTP boundary is the only thing stubbed, and
the swap-planning logic is exercised for real.
## License
MIT-0 — see [LICENSE](LICENSE). Use it without attribution.
TDQS
A4.5/5.0
Scored across 1 tool
Disambiguation5/5
Only one tool exists, so there is no possibility of confusion between tools. The tool's purpose is clearly and specifically stated.
Naming Consistency5/5
The single tool uses a clear verb_noun pattern ('build_swap_transaction'). There are no other tools to introduce inconsistency.
Tool Count2/5
One tool is too few for a server that might be expected to provide a broader range of swap-related operations (e.g., quoting, token info). The scope feels thin.
Completeness4/5
The tool covers the core swap workflow by returning ordered transactions including approval and swap. However, it lacks separate quote retrieval or token discovery capabilities, which are minor gaps for a swap-focused server.
Maintenance
ActivitySlowing
ResponsivenessNo issues