zapper-mcp
An MCP server that exposes the Zapper 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. 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.
Requirements
- Node.js 20+
- pnpm
- A Zapper API key
Install
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:
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):
ZAPPER_API_KEY=your_key pnpm client
Output: lists tools/resources/prompts, then calls each tool against vitalik.eth.
Direct server start:
ZAPPER_API_KEY=your_key pnpm start
Claude Desktop wiring
Add to ~/Library/Application Support/Claude/claude_desktop_config.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:
- Start the server:
node /path/to/build/server.js - Configure the Mastra MCP client with stdio transport, server name
zapper-mcp - The agent consumes Zapper data exclusively through MCP —
lib/zapper.tsin 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_KEYand 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-networksis a static list. Live updates would require the server to advertise subscribe capability and emitnotifications/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.