zapper-mcp
by mehdi-loup
README.md
# zapper-mcp
An MCP server that exposes the [Zapper](https://zapper.xyz) DeFi portfolio API as a thoughtfully designed tool surface for LLM clients. Connect it to Claude Desktop or any MCP-compatible host and ask natural-language questions about any wallet — "what is this wallet worth?", "does it have any Aave positions?", "show me the top holdings on Base."
Built on Day 9 of a 21-day AI engineering sprint. Day 10 wires this server into a Mastra agent.
---
## Tool surface
The design rationale for each primitive is in [`DESIGN.md`](./DESIGN.md). The short version:
| Primitive | Name | Why this placement |
|-----------|------|--------------------|
| Tool | `get_portfolio` | Model-invoked, dynamic per address, returns full token + DeFi breakdown |
| Tool | `get_token_balances` | Focused tool for spot-token questions; avoids making the model parse a full portfolio when it only needs token holdings |
| Tool | `get_app_positions` | Focused tool for DeFi questions; separate from `get_portfolio` so the model can express precise intent and receive a focused schema |
| Resource | `zapper://supported-networks` | Static network list — host injects it as ambient context at prompt-assembly time so the model knows valid network names without burning a tool-call turn |
| Prompt | `analyze-wallet` | User-invoked workflow that pre-seeds a multi-turn portfolio analysis conversation with analyst persona, tool inventory, and wallet address |
**Why not one big `get_everything` tool?** Collapsing the tools would force the model to receive and parse a large mixed-schema response for every question, even focused ones. A tool boundary is a declaration of scope — the right tool returns exactly what the reasoning step needs.
**Why is the API key in server config, not a tool argument?** Credentials belong in the host layer (env vars injected at process spawn), not in the MCP protocol. If `api_key` were a tool parameter, it would flow through the LLM's reasoning and appear in conversation history. For a multi-tenant deploy the right mechanism is transport-layer auth (Bearer token over Streamable HTTP) or per-user OAuth — both out of scope here. See [Known limitations](#known-limitations).
---
## Requirements
- Node.js 20+
- pnpm
- A [Zapper API key](https://build.zapper.xyz)
---
## Install
```bash
git clone https://github.com/mehdi-loup/zapper-mcp
cd zapper-mcp
pnpm install
pnpm build
```
---
## Configuration
Copy `.env.example` to `.env` and add your key:
```bash
cp .env.example .env
# edit .env and set ZAPPER_API_KEY=your_key_here
```
The server fails fast at boot if `ZAPPER_API_KEY` is missing — you'll see the error immediately, not on the first tool call.
---
## Run
**Standalone smoke test** (confirms everything works without Claude Desktop):
```bash
ZAPPER_API_KEY=your_key pnpm client
```
Output: lists tools/resources/prompts, then calls each tool against `vitalik.eth`.
**Direct server start:**
```bash
ZAPPER_API_KEY=your_key pnpm start
```
---
## Claude Desktop wiring
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"zapper-mcp": {
"command": "node",
"args": ["/absolute/path/to/zapper-mcp/build/server.js"],
"env": {
"ZAPPER_API_KEY": "your_key_here"
}
}
}
}
```
Restart Claude Desktop. The three tools, the `zapper://supported-networks` resource, and the `analyze-wallet` prompt will be available.
**Logs** (if the server fails to load):
```
~/Library/Logs/Claude/mcp-server-zapper-mcp.log
```
---
## Mastra integration (Day 10)
To wire this server into a Mastra agent via Mastra's MCP client:
1. Start the server: `node /path/to/build/server.js`
2. Configure the Mastra MCP client with stdio transport, server name `zapper-mcp`
3. The agent consumes Zapper data exclusively through MCP — `lib/zapper.ts` in the agent repo becomes unused
Not all tools need to be exposed to the Mastra agent; that's a Day 10 design call.
---
## Tool reference
### `get_portfolio(address, networks?)`
Full portfolio breakdown: total USD, all token holdings, all DeFi positions.
```
address — wallet address or ENS name
networks — optional array: ["ethereum", "base", "arbitrum", ...]
```
### `get_token_balances(address, networks?)`
Spot token balances only (no DeFi positions).
### `get_app_positions(address, networks?, app_slug?)`
DeFi app positions only (Aave, Uniswap, Sablier, etc.).
```
app_slug — optional filter: "aave-v3", "uniswap-v3", ...
```
### Resource: `zapper://supported-networks`
JSON array of `{ name, chainId }` for all indexed networks. Read by host at context-assembly time.
### Prompt: `analyze-wallet`
Pre-seeds a portfolio analysis conversation. Takes an `address` argument.
---
## Error handling
Every tool returns `isError: true` with a model-actionable message on:
- HTTP 401 / invalid API key
- HTTP 429 / rate limited
- HTTP 5xx / Zapper server error
- Network timeout (15s)
- Malformed response
An empty wallet (`totalUSD: 0, tokens: []`) returns `isError: false` — empty is not an error.
---
## Known limitations
- **Single-key trust model**: the server holds one `ZAPPER_API_KEY` and serves one owner. A multi-tenant deploy needs per-user OAuth or transport-layer auth (Streamable HTTP with Bearer tokens).
- **No caching**: every tool call hits the Zapper API. A production server would add a short TTL cache (positions change slowly) and respect rate limits proactively.
- **No `resources/subscribe`**: `zapper://supported-networks` is a static list. Live updates would require the server to advertise subscribe capability and emit `notifications/resources/updated`.
- **stdio transport only**: Streamable HTTP transport deferred to a future iteration.
- **Pagination ceiling**: tools return up to 50 tokens and 20 app positions per request.
---
## What's next
**Day 10**: wire this server into the Mastra wallet agent at `../day1-wallet-agent/` via Mastra's MCP client. The agent will consume Zapper data exclusively through MCP, validating that the tool surface actually decouples the capability from the agent framework.
TDQS
A4.2/5.0
Scored across 3 tools
Disambiguation5/5
Each tool targets a distinct aspect of wallet data: token balances, DeFi positions, or full portfolio. Descriptions clearly differentiate them, leaving no ambiguity for an agent.
Naming Consistency5/5
All tools follow a consistent 'get_<descriptive_noun>' pattern (get_app_positions, get_portfolio, get_token_balances), making naming predictable and readable.
Tool Count5/5
Three tools is well-scoped for a wallet data server, covering the core needs without excess or deficiency.
Completeness4/5
The set covers token balances, DeFi positions, and a combined portfolio, which forms a complete picture for most wallet queries. Missing advanced features like transaction history are acceptable for the scope.
Maintenance
ActivityInactive
ResponsivenessNo issues