Skip to main content
Glama
README.md
# Zerion MCP Server

A production-ready [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that provides AI assistants with access to the Zerion API for cryptocurrency portfolio management, DeFi positions, NFTs, and market data.

## Features

- ๐Ÿ”Œ **Auto-generated Tools**: Automatically exposes Zerion API endpoints as MCP tools via OpenAPI specification
- โš™๏ธ **Flexible Configuration**: YAML config files with environment variable overrides
- ๐Ÿ“ **Structured Logging**: JSON and text formats with sensitive data redaction
- ๐Ÿ›ก๏ธ **Robust Error Handling**: Custom exceptions with detailed context and troubleshooting hints
- ๐Ÿ” **Automatic Retry Logic**: Exponential backoff for rate limits (429) and wallet indexing (202)
- ๐Ÿ“„ **Pagination Support**: Manual and automatic pagination for large result sets (5000+ items)
- ๐Ÿšฆ **Rate Limit Management**: Transparent retry handling with configurable backoff strategies
- ๐Ÿ” **Advanced Filtering**: Chain filtering, DeFi positions, spam filtering, transaction types, and more
- ๐Ÿงช **Testnet Support**: Test applications on testnets (Sepolia, Monad, etc.) via X-Env header
- ๐Ÿ’ฑ **Multi-Currency**: Portfolio values in USD, ETH, EUR, or BTC denomination
- โœ… **Comprehensive Tests**: Unit and integration tests with pytest
- ๐Ÿš€ **Async HTTP**: Non-blocking API calls with httpx

## Operational Capabilities

The Zerion API provides three powerful operational capabilities that make it stand out from traditional blockchain data providers:

### 1. Multi-Chain Aggregation

**100+ blockchains in one API call** - Zerion automatically aggregates data across all supported blockchain networks without requiring multiple API requests.

**How it works:**
- No special parameters needed - multi-chain aggregation is automatic
- Single call to `getWalletPortfolio` returns balances across Ethereum, Base, Polygon, Arbitrum, Optimism, Solana, and 100+ other chains
- Eliminates the need for per-chain API providers (Alchemy, Infura, Moralis)

**Supported chains include:**
- **EVM chains**: Ethereum, Polygon, Arbitrum, Optimism, Base, BNB Chain, Avalanche, Fantom, Gnosis, zkSync, StarkNet
- **L2s**: Optimism, Arbitrum, Base, zkSync Era, Polygon zkEVM, Linea, Scroll
- **Non-EVM**: Solana
- **100+ total chains** (see `listChains` for complete list)

**Examples:**

```
Get wallet portfolio across all chains:
Use getWalletPortfolio with:
- address: "0x42b9dF65B219B3dD36FF330A4dD8f327A6Ada990"
- currency: "usd"
```

Result: Portfolio data aggregated from all 100+ supported chains in a single response.

```
Get positions across all chains:
Use listWalletPositions with:
- address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
- filter[positions]: "only_complex"
- currency: "usd"
```

Result: DeFi positions from Ethereum, Base, Arbitrum, and all other chains in one call.

**Benefits vs. per-chain APIs:**
- **Zerion**: 1 API call for cross-chain portfolio
- **Per-chain APIs**: 10+ separate API calls needed (one per chain)
- **Quota savings**: 90% reduction in API requests
- **Simplified logic**: No manual chain aggregation required

**Use cases:**
- Cross-chain portfolio dashboards
- DeFi aggregators (track positions across all chains)
- Multi-chain wallet analytics
- Cross-chain transaction history

### 2. DeFi Protocol Coverage

**8,000+ DeFi protocols tracked** - Zerion provides comprehensive protocol metadata for lending, staking, LP, and yield farming positions.

**Available protocol metadata:**
- `relationships.dapp.data.id` - Protocol identifier (e.g., "uniswap-v3", "aave-v3")
- `attributes.position_type` - Position category (staked, deposit, loan, reward, etc.)
- DApp data following JSON:API relationship format

**Protocol categories covered:**
- **DEX**: Uniswap, Curve, Balancer, SushiSwap, PancakeSwap
- **Lending**: Aave, Compound, MakerDAO, Euler
- **Staking**: Lido, Rocket Pool, StakeWise
- **Yield aggregators**: Yearn, Beefy, Convex

**Examples:**

```
Get all DeFi positions with protocol data:
Use listWalletPositions with:
- address: "0x42b9dF65B219B3dD36FF330A4dD8f327A6Ada990"
- filter[positions]: "only_complex"
- filter[trash]: "only_non_trash"
- currency: "usd"
```

Result: Returns positions with `relationships.dapp` containing protocol metadata (Uniswap, Aave, Curve, etc.).

```
Filter positions by specific protocol:
Use listWalletPositions with:
- address: "0x..."
- filter[dapp_ids]: "aave-v3,compound-v2"
- filter[positions]: "only_complex"
```

Result: Only Aave V3 and Compound V2 positions.

**Use cases:**
- DeFi protocol analytics dashboards
- Position aggregation by protocol
- Protocol yield tracking
- Risk management (track exposure by protocol)

### 3. NFT Metadata Completeness

**Comprehensive NFT metadata** - Zerion provides rich metadata for ERC-721 and ERC-1155 tokens, enabling full-featured NFT displays.

**Available metadata fields:**
- `metadata.name` - NFT name
- `metadata.description` - NFT description
- `metadata.content.preview` - Thumbnail/preview image URL
- `metadata.content.detail` - High-resolution image URL
- `market_data.prices.floor` - Collection floor price (where available)
- `metadata.attributes` - NFT traits and properties
- `relationships.nft_collection` - Collection relationship data

**Image URLs:**
- Preview URLs for thumbnails/gallery views
- Detail URLs for full-size displays
- URLs may be IPFS gateways

**Floor price availability:**
- Available for most established collections
- New/small collections may lack floor data
- Measured in requested currency (USD, ETH, EUR, BTC)

**Examples:**

```
Get NFT with full metadata:
Use getNFTById with:
- nft_id: "ethereum:0x74ee68a33f6c9f113e22b3b77418b75f85d07d22:10"
- currency: "usd"
```

Result: Complete NFT metadata including name, description, images, floor price, traits, and collection data.

```
Get wallet NFT positions:
Use listWalletNFTPositions with:
- address: "0x..."
- filter[trash]: "only_non_trash"
```

Result: NFT positions with metadata for rich NFT gallery displays.

**Use cases:**
- NFT gallery applications
- NFT marketplace displays
- Collection analytics (floor price tracking)
- Trait-based filtering and rarity analysis

---

## Available Functions

### Wallet & Portfolio
- **getWalletPortfolio**: Returns the portfolio overview of a web3 wallet.
- **listWalletPositions**: Returns a list of wallet positions (supports `filter[positions]=only_complex` for DeFi).
- **getWalletChart**: Returns a portfolio balance chart for a wallet.
- **getWalletPNL**: Returns the Profit and Loss (PnL) details of a web3 wallet.
- **listWalletTransactions**: Returns a list of transactions associated with the wallet (supports advanced filters).

### NFTs
- **getWalletNftPortfolio**: Returns the NFT portfolio overview of a web3 wallet.
- **listWalletNFTCollections**: Returns a list of the NFT collections held by a specific wallet.
- **listWalletNFTPositions**: Returns a list of the NFT positions held by a specific wallet.
- **getNFTById**: Returns a single NFT by its unique identifier.
- **listNFTs**: Returns a list of NFTs by using different parameters.

### Webhooks & Real-Time Notifications (NEW!)
- **createTxSubscription**: Create webhook subscription for real-time transaction notifications.
- **listTxSubscriptions**: List all active transaction subscriptions.
- **getTxSubscription**: Get subscription details by ID.
- **updateTxSubscription**: Update subscription addresses, callback URL, or chain filters.
- **deleteTxSubscription**: Delete a transaction subscription.

### Market Data
- **getFungibleById**: Returns a fungible asset by its unique identifier.
- **getFungibleChart**: Returns the chart for a fungible asset for a selected period.
- **listFungibles**: Returns a paginated list of fungible assets supported by Zerion.
- **listGasPrices**: Provides real-time information on the current gas prices across all supported blockchain networks.
- **listChains**: Returns a list of all chains supported by Zerion.
- **getChainById**: Returns a chain by its unique chain identifier.

### Swaps & Bridges
- **swapFungibles**: Provides a list of fungibles available for bridge exchange.
- **swapOffers**: Provides a comprehensive overview of relevant trades and bridge exchanges.

## Requirements

### For Docker (Recommended)
- Docker 20.10 or higher
- Docker Compose V2
- Zerion API key ([Get one here](https://developers.zerion.io/))

### For Native Python Installation
- Python 3.11 or higher
- Zerion API key ([Get one here](https://developers.zerion.io/))

## Installation

### Using Docker (Recommended)

```bash
# Clone the repository
git clone https://github.com/SAK1337/myzerionmcp.git
cd myzerionmcp

# Create .env file with your API key
cp .env.example .env
# Edit .env and set ZERION_API_KEY

# Start with Docker Compose
docker-compose up -d

# View logs
docker-compose logs -f
```

See [DOCKER.md](DOCKER.md) for detailed Docker deployment guide.

### From Source

```bash
# Clone the repository
git clone https://github.com/SAK1337/myzerionmcp.git
cd myzerionmcp

# Install the package
pip install -e .

# For development (includes testing dependencies)
pip install -e ".[dev]"
```

### From PyPI (when published)

```bash
pip install zerion-mcp-server
```

## Quick Start

### 1. Set up your API key

```bash
export ZERION_API_KEY="Bearer your-api-key-here"
```

### 2. Run the server

```bash
zerion-mcp-server
```

### 3. Connect with an MCP client

The server will start and listen for MCP protocol connections. You can connect it to AI assistants like Claude Desktop.

#### Claude Desktop Configuration

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "zerion": {
      "command": "zerion-mcp-server",
      "env": {
        "ZERION_API_KEY": "Bearer your-api-key-here"
      }
    }
  }
}
```

## Configuration

### Configuration File

Create a `config.yaml` file in your working directory:

```yaml
# Server configuration
name: "Zerion API"
base_url: "https://api.zerion.io"
oas_url: "https://raw.githubusercontent.com/smart-mcp-proxy/zerion-mcp-server/main/zerion_mcp_server/openapi_zerion.yaml"

# API authentication
api_key: "${ZERION_API_KEY}"  # Environment variable substitution

# Logging configuration
logging:
  level: "INFO"      # DEBUG, INFO, WARN, ERROR
  format: "text"     # text or json
```

See [`config.example.yaml`](config.example.yaml) for a complete example.

### Environment Variables

Environment variables override config file values:

| Variable | Description | Default |
|----------|-------------|---------|
| `ZERION_API_KEY` | Zerion API key (required) | - |
| `ZERION_BASE_URL` | Zerion API base URL | `https://api.zerion.io` |
| `ZERION_OAS_URL` | OpenAPI spec URL | GitHub raw URL |
| `CONFIG_PATH` | Path to config.yaml | `./config.yaml` |
| `LOG_LEVEL` | Logging level | `INFO` |
| `LOG_FORMAT` | Logging format (text/json) | `text` |

## Usage Examples

Once connected to an MCP client (like Claude), you can query Zerion data:

### Portfolio Balance

```
Get the portfolio balance for wallet 0x1234...
```

The server exposes tools like `getWalletChart`, `getWalletPositions`, etc.

### DeFi Positions

```
Show me the DeFi positions for address 0xabcd...
```

### NFT Collections

```
List NFTs owned by 0x5678...
```

All Zerion API endpoints are automatically available as MCP tools. See the [Zerion API documentation](https://developers.zerion.io/reference) for available operations.

## Webhooks - Real-Time Transaction Notifications

The Zerion MCP Server now supports **transaction subscription webhooks** for real-time wallet activity monitoring. This is critical for production applications because webhooks eliminate inefficient polling, conserve API quotas, and enable sub-second notification latency.

### Why Use Webhooks?

- **Rate Limit Conservation**: Polling wastes API quota (~5K requests/day on free tier). Webhooks push updates only when transactions occur.
- **Real-Time**: Sub-second notifications when monitored wallets transact
- **Scalability**: Monitor hundreds of wallets without quota waste
- **Cost Efficiency**: On paid tiers ($149/mo Builder), webhooks eliminate redundant polling requests

### Architecture Overview

```
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  AI Client   โ”‚ (Claude Desktop)
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚ stdio (MCP protocol)
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MCP Server  โ”‚ - Manages subscriptions via API calls
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚ HTTPS
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Zerion API  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚ Webhooks (HTTP POST)
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Your HTTP    โ”‚ - Receives webhook payloads (separate service)
โ”‚ Receiver     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
```

**Important**: The MCP server manages webhook subscriptions but does NOT receive webhook payloads. You must deploy a separate HTTP service to receive webhook POST requests from Zerion.

### Available Tools

- **`createTxSubscription`** - Create new webhook subscription
- **`listTxSubscriptions`** - List all active subscriptions
- **`getTxSubscription`** - Get subscription details by ID
- **`updateTxSubscription`** - Update addresses, callback URL, or chain filters
- **`deleteTxSubscription`** - Delete subscription

### Example: Create Subscription for Ethereum Address

```
Use the createTxSubscription tool with:
- addresses: ["0x42b9dF65B219B3dD36FF330A4dD8f327A6Ada990"]
- callback_url: "https://webhook.site/your-unique-url"
- chain_ids: ["ethereum", "base"]
```

This creates a subscription that sends webhook notifications to your callback URL whenever the specified address has transactions on Ethereum or Base.

### Example: Create Subscription for Solana Address

```
Use the createTxSubscription tool with:
- addresses: ["8BH9pjtgyZDC4iAQH5ZiYDZ1MDWC98xki2V8NzqqKW3K"]
- callback_url: "https://your-server.com/webhooks/zerion"
- chain_ids: ["solana"]
```

### Webhook Payload Structure

When a monitored address has a new transaction, Zerion sends a POST request to your callback URL with this structure:

```json
{
  "data": {
    "type": "callback",
    "id": "bf300927-3f57-4d00-a01a-f7b75bd9b8de",
    "attributes": {
      "address": "0x42b9dF65B219B3dD36FF330A4dD8f327A6Ada990",
      "callback_url": "https://webhook.site/test",
      "timestamp": "2025-10-16T10:56:50Z"
    },
    "relationships": {
      "subscription": {
        "id": "61f13641-443e-4068-932b-c28edeaefd85",
        "type": "tx-subscriptions"
      }
    }
  },
  "included": [
    {
      "type": "transactions",
      "id": "13de850a-bfa4-54c7-a7bb-fd6371d98894",
      "attributes": {
        "operation_type": "trade",
        "hash": "0x...",
        "mined_at": "2025-10-16T10:56:48Z",
        "status": "confirmed",
        "fee": { ... },
        "transfers": [ ... ]
      },
      "relationships": {
        "chain": { "id": "ethereum", "type": "chains" },
        "dapp": { "id": "uniswap", "type": "dapps" }
      }
    }
  ]
}
```

The `included` array contains full transaction details using the same schema as the `listWalletTransactions` endpoint.

### Setting Up a Webhook Receiver

#### Option 1: Testing with webhook.site (Recommended for Development)

1. Visit https://webhook.site
2. Copy your unique URL (e.g., `https://webhook.site/abc-123-def`)
3. Use that URL as `callback_url` when creating subscriptions
4. View incoming webhooks in real-time on the webhook.site page

#### Option 2: Python Flask Receiver (Production)

```python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/zerion', methods=['POST'])
def handle_zerion_webhook():
    payload = request.json

    # Extract transaction data
    callback_data = payload['data']
    transactions = payload.get('included', [])

    for tx in transactions:
        if tx['type'] == 'transactions':
            print(f"New transaction: {tx['attributes']['hash']}")
            print(f"Operation: {tx['attributes']['operation_type']}")
            print(f"Chain: {tx['relationships']['chain']['id']}")

    return jsonify({"status": "received"}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)
```

Deploy this to a public server (Heroku, Vercel, AWS Lambda, etc.) and use the public HTTPS URL as your callback.

#### Option 3: Node.js Express Receiver

```javascript
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/zerion', (req, res) => {
  const { data, included } = req.body;

  const transactions = included.filter(item => item.type === 'transactions');

  transactions.forEach(tx => {
    console.log(`New ${tx.attributes.operation_type}: ${tx.attributes.hash}`);
  });

  res.status(200).json({ status: 'received' });
});

app.listen(8080, () => console.log('Webhook receiver running on port 8080'));
```

### Webhook Best Practices

1. **Respond Quickly**: Zerion expects < 5 second response time. Do heavy processing asynchronously.
2. **Idempotency**: Webhooks may be delivered multiple times. Check for duplicate transaction IDs.
3. **Retry Policy**: Zerion retries delivery up to 3 times on failure.
4. **HTTPS Required**: Callback URLs must use HTTPS (not HTTP).
5. **No Guaranteed Order**: Don't assume webhook delivery order matches blockchain order.

### Security Considerations

- **Signature Verification**: Future enhancement - webhook payload signing (check Zerion docs)
- **IP Whitelisting**: Consider restricting webhook receiver to Zerion's IP ranges
- **HTTPS Only**: Never use HTTP callback URLs in production

### Troubleshooting Webhooks

#### Webhooks Not Arriving

1. **Check callback URL is publicly accessible**: Test with `curl -X POST https://your-url.com`
2. **Verify subscription is active**: Use `listTxSubscriptions` to confirm subscription exists
3. **Check Zerion dashboard**: Verify API key has webhook permissions
4. **Test with webhook.site first**: Confirm Zerion can reach your infrastructure

#### Rate Limits on Subscription Management

- Developer keys: Limited subscriptions (1-5)
- Contact api@zerion.io for enterprise webhook quotas

---

## Advanced Filtering - Query Optimization

The Zerion API supports powerful filter parameters that are already available in the MCP server but often underutilized. Using filters **reduces API quota usage**, **improves response times**, and enables **precise data queries**.

### Filter Applicability Matrix

| Filter | Endpoints | Use Case |
|--------|-----------|----------|
| **`filter[positions]`** | `getWalletPortfolio`, `listWalletPositions` | Isolate DeFi positions from simple balances |
| **`filter[chain_ids]`** | `listWalletPositions`, `listWalletTransactions`, `getWalletChart`, `listWalletNFTPositions` | Query specific blockchains only |
| **`filter[trash]`** | `listWalletPositions`, `listWalletTransactions`, `listWalletNFTPositions` | Hide spam/dust |
| **`filter[operation_types]`** | `listWalletTransactions` | Filter by transaction type (trade, transfer, execute) |
| **`filter[position_types]`** | `listWalletPositions` | Filter by DeFi category (staked, deposit, loan, reward) |
| **`filter[fungible_ids]`** | `listWalletPositions`, `listWalletTransactions` | Track specific tokens only |

### DeFi Position Filtering (`only_complex`)

**Problem**: By default, `listWalletPositions` returns simple token balances. DeFi protocols (staking, LPs, lending) are excluded.

**Solution**: Use `filter[positions]=only_complex` to get **only DeFi positions**.

#### Example: Get DeFi Positions Only

```
Use listWalletPositions with:
- address: "0x42b9dF65B219B3dD36FF330A4dD8f327A6Ada990"
- filter[positions]: "only_complex"
- filter[trash]: "only_non_trash"
- currency: "usd"
```

**Result**: Returns staking positions (e.g., staked ETH), liquidity pool tokens (Uniswap, Curve), lending positions (Aave, Compound), and yield farming positions. Excludes simple ERC-20 balances.

**Use Cases**:
- DeFi analytics dashboards
- Institutional risk management (track complex positions separately)
- Yield farming trackers
- Portfolio categorization for tax reporting

#### Filter Options

- **`only_simple`** (default): Only wallet-type positions (basic token balances)
- **`only_complex`**: Only DeFi protocol positions (staking, LP, lending)
- **`no_filter`**: Both simple and complex positions

### Chain Filtering (`filter[chain_ids]`)

**Problem**: Fetching positions across all 50+ chains wastes quota and returns irrelevant data.

**Solution**: Use `filter[chain_ids]` to query specific blockchains.

#### Example: Ethereum + Base Only

```
Use listWalletTransactions with:
- address: "0x..."
- filter[chain_ids]: "ethereum,base"
- filter[trash]: "only_non_trash"
```

**Result**: Only transactions on Ethereum and Base. Ignores activity on Polygon, Arbitrum, Optimism, etc.

#### Example: Single Chain (Optimism)

```
Use listWalletPositions with:
- address: "0x..."
- filter[chain_ids]: "optimism"
```

**Use Cases**:
- L2-focused analytics (Base, Optimism, Arbitrum)
- Cross-chain comparison (Ethereum vs. L2s)
- Mainnet-only queries

**Valid Chain IDs**: `ethereum`, `base`, `optimism`, `arbitrum`, `polygon`, `solana`, `binance-smart-chain`, `avalanche`, `fantom`, `gnosis`, etc. Use `listChains` to see all supported chains.

### Spam Filtering (`filter[trash]`)

**Problem**: Wallet transaction history polluted with spam tokens, dust, and airdrop scams.

**Solution**: Use `filter[trash]=only_non_trash` to hide spam.

#### Example: Clean Transaction History

```
Use listWalletTransactions with:
- address: "0x..."
- filter[trash]: "only_non_trash"
- page[size]: 100
```

**Result**: Clean transaction history without spam tokens or dust transfers.

#### Filter Options

- **`only_non_trash`**: Hide spam/dust (recommended for user-facing apps)
- **`only_trash`**: Show only spam/dust (for security monitoring)
- **`no_filter`** (default): Show all transactions

**Use Cases**:
- User-facing wallets (clean UI)
- Portfolio analytics (exclude noise)
- Tax reporting (ignore spam airdrops)

### Transaction Type Filtering (`filter[operation_types]`)

**Problem**: Need to analyze specific transaction types (e.g., only swaps, only transfers).

**Solution**: Use `filter[operation_types]` to isolate transaction categories.

#### Example: Trades Only

```
Use listWalletTransactions with:
- address: "0x..."
- filter[operation_types]: "trade"
- filter[chain_ids]: "ethereum,base"
```

**Result**: Only DEX swaps and trades. Excludes transfers, approvals, contract executions.

#### Available Operation Types

- **`trade`**: Token swaps (Uniswap, 1inch, etc.)
- **`transfer`**: Simple token transfers
- **`execute`**: Smart contract interactions
- **`approve`**: Token approvals
- **`deposit`**: Protocol deposits
- **`withdraw`**: Protocol withdrawals

**Use Cases**:
- Trading analytics (volume, frequency)
- Transfer tracking (payroll, payments)
- Smart contract interaction audits

### Position Type Filtering (`filter[position_types]`)

**Problem**: DeFi positions span many categories (staking, lending, rewards). Need to isolate specific types.

**Solution**: Use `filter[position_types]` for granular DeFi categorization.

#### Example: Staking + Rewards Only

```
Use listWalletPositions with:
- address: "0x..."
- filter[positions]: "only_complex"
- filter[position_types]: "staked,reward"
```

**Result**: Only staked assets and claimable rewards. Excludes LP positions, loans, deposits.

#### Available Position Types

- **`wallet`**: Simple balance
- **`deposit`**: Lending protocol deposits (Aave, Compound)
- **`loan`**: Borrowed assets
- **`staked`**: Staked tokens (ETH staking, governance staking)
- **`reward`**: Claimable rewards
- **`locked`**: Vested/locked tokens
- **`margin`**: Margin trading positions
- **`airdrop`**: Airdrop eligibility

**Use Cases**:
- Staking dashboards
- Yield optimization
- Loan/collateralization tracking
- Rewards harvesting

### Query Optimization Best Practices

#### 1. Use Filters to Reduce Quota Usage

**Bad** (fetches all data, filters client-side):
```
- Get all positions for address
- Filter out spam in your app
- Filter to Ethereum-only in your app
```
**Cost**: ~1000 positions returned, large response

**Good** (filter at API level):
```
Use listWalletPositions with:
- filter[chain_ids]: "ethereum"
- filter[trash]: "only_non_trash"
```
**Cost**: ~50 positions returned, 20x smaller response

#### 2. Combine Filters for Precision

**Example: DeFi-Only, Ethereum, No Spam**
```
Use listWalletPositions with:
- filter[positions]: "only_complex"
- filter[chain_ids]: "ethereum"
- filter[trash]: "only_non_trash"
- currency: "usd"
- sort: "value"
```

**Result**: High-value Ethereum DeFi positions only. Perfect for risk dashboards.

#### 3. Page Size Control

```
Use listWalletTransactions with:
- page[size]: 50
- filter[trash]: "only_non_trash"
```

Smaller pages = faster responses, less quota per request.

### Common Filter Combinations

| Use Case | Filters |
|----------|---------|
| **Clean transaction feed** | `filter[trash]=only_non_trash`, `filter[operation_types]=trade,transfer` |
| **DeFi risk dashboard** | `filter[positions]=only_complex`, `filter[chain_ids]=ethereum`, `sort=value` |
| **L2 trading analytics** | `filter[chain_ids]=base,optimism,arbitrum`, `filter[operation_types]=trade` |
| **Staking tracker** | `filter[positions]=only_complex`, `filter[position_types]=staked,reward` |
| **NFT portfolio (no spam)** | `filter[trash]=only_non_trash` on `listWalletNFTPositions` |

---

## Pagination - Fetching Large Result Sets

The Zerion API uses cursor-based pagination for endpoints that return lists (transactions, positions, NFTs). The MCP server provides both manual and automatic pagination support.

### Why Pagination Matters

Active wallets can have **thousands of transactions**. Without pagination:
- API quota exhausted quickly (Developer tier: ~5K requests/day)
- Incomplete data (default page size: 100 items)
- Slow responses (large payloads)

### Manual Pagination

All list endpoints support `page[size]` and `page[after]` parameters:

```
Use listWalletTransactions with:
- address: "0x..."
- page[size]: 100
- filter[trash]: "only_non_trash"
```

The response includes a `links.next` URL for fetching the next page:

```json
{
  "data": [...],
  "links": {
    "next": "https://api.zerion.io/v1/wallets/.../transactions?page[after]=cursor123"
  }
}
```

**To fetch the next page**, extract the cursor from `links.next` and use it as `page[after]`:

```
Use listWalletTransactions with:
- address: "0x..."
- page[size]: 100
- page[after]: "cursor123"
```

Continue until `links.next` is absent (last page reached).

### Automatic Pagination (Python SDK)

For programmatic access, use the auto-pagination helper to fetch all pages automatically:

```python
from zerion_mcp_server.pagination import fetch_all_pages
from zerion_mcp_server import RetryAsyncClient

# Create client
client = RetryAsyncClient(
    base_url="https://api.zerion.io",
    headers={"Authorization": "Bearer your-key"}
)

# Define your API call
async def get_transactions(address, **kwargs):
    response = await client.get(
        f"/v1/wallets/{address}/transactions",
        params=kwargs
    )
    return response.json()

# Fetch all transactions (up to max_pages)
all_transactions = await fetch_all_pages(
    api_call=lambda **kw: get_transactions("0x123...", **kw),
    max_pages=50,  # Safety limit
    page_size=100
)

print(f"Total transactions: {len(all_transactions)}")
```

### Pagination Configuration

Control pagination behavior in `config.yaml`:

```yaml
pagination:
  # Default page size (max: 100)
  default_page_size: 100

  # Safety limit for auto-pagination
  max_auto_pages: 50
```

**Example calculations**:
- 50 pages ร— 100 items = 5,000 results (covers 99% of wallets)
- 50 API requests consumed (watch your quota!)

### Pagination Best Practices

#### 1. Use Filters to Reduce Results

**Bad** (fetches everything):
```
listWalletTransactions:
  - address: "0x..."
  - page[size]: 100
```
**Result**: 2,000 transactions (20 pages)

**Good** (filter first):
```
listWalletTransactions:
  - address: "0x..."
  - filter[chain_ids]: "ethereum"
  - filter[trash]: "only_non_trash"
  - page[size]: 100
```
**Result**: 300 transactions (3 pages) - 85% quota savings!

#### 2. Choose Appropriate Page Sizes

- **Small pages (25-50)**: Faster individual requests, more total requests
- **Large pages (100)**: Fewer requests, larger payloads (recommended)
- **Default**: 100 items per page

#### 3. Monitor Auto-Pagination Limits

The auto-pagination helper logs warnings at key thresholds:

```
INFO: Fetched 10 pages - quota impact may be significant
WARNING: Fetched 25 pages - high quota usage
WARNING: Reached max page limit (50) - results may be incomplete
```

If you see "results may be incomplete", increase `max_auto_pages` or use manual pagination.

### Quota Impact Example

**Scenario**: Fetch all transactions for an active wallet (5,000 transactions)

| Approach | Requests | Quota Impact (Developer Tier) |
|----------|----------|-------------------------------|
| Manual (1 page) | 1 | ~0.02% of daily quota |
| Auto-paginate (50 pages) | 50 | ~1% of daily quota |
| Without filters (200 pages) | 200 | ~4% of daily quota |

**Takeaway**: Use filters + reasonable `max_pages` limits to avoid quota exhaustion.

---

## Rate Limiting - Automatic Retry with Backoff

The Zerion API enforces rate limits based on your subscription tier:

| Tier | Rate Limit | Daily Requests | Price |
|------|------------|----------------|-------|
| **Developer** (free) | 2 RPS | ~5,000/day | $0 |
| **Builder** | 50 RPS | ~500,000/day | $149/mo |
| **Pro** | 150 RPS | ~1.5M/day | $599/mo |
| **Enterprise** | Custom | Custom | Contact sales |

When you exceed your quota, the API returns `429 Too Many Requests` with a `Retry-After` header.

### Automatic Retry Behavior

The MCP server **automatically retries** rate-limited requests using exponential backoff with jitter:

```
Request โ†’ 429 (rate limit)
 โ†“ Wait 1 second
Retry 1 โ†’ 429
 โ†“ Wait 2 seconds (exponential backoff)
Retry 2 โ†’ 429
 โ†“ Wait 4 seconds
Retry 3 โ†’ 200 OK (success!)
```

**You don't need to handle retries manually** - the server handles them transparently.

### Retry Configuration

Customize retry behavior in `config.yaml`:

```yaml
retry_policy:
  # Maximum retry attempts before raising error
  max_attempts: 5

  # Base delay for exponential backoff (seconds)
  base_delay: 1

  # Maximum delay between retries (seconds)
  max_delay: 60

  # Exponential backoff multiplier
  exponential_base: 2
```

**Delay sequence** (with exponential_base=2):
- Retry 1: 1 second
- Retry 2: 2 seconds
- Retry 3: 4 seconds
- Retry 4: 8 seconds
- Retry 5: 16 seconds

Total maximum wait: ~31 seconds (if all retries needed).

### Rate Limit Error Messages

If retries are exhausted, you'll see an actionable error:

```
RateLimitError: Rate limit exceeded after 5 retry attempts.
Retry after 60 seconds. Consider upgrading tier or reducing
request frequency. See: https://zerion.io/pricing
```

**Next steps**:
1. Wait for the `retry_after` duration (from error message)
2. Reduce request frequency in your app
3. Upgrade to a higher tier if needed

### Rate Limiting Best Practices

#### 1. Use Webhooks Instead of Polling

**Bad** (polling):
```python
# Check for new transactions every 10 seconds
while True:
    transactions = get_transactions(address)
    time.sleep(10)
```
**Cost**: 8,640 requests/day (exceeds Developer tier quota!)

**Good** (webhooks):
```python
# Webhook delivers new transactions as they happen
# 0 polling requests, only create subscription once
create_webhook_subscription(addresses=[address])
```
**Cost**: 1 request total ๐ŸŽฏ

#### 2. Implement Client-Side Caching

```python
from functools import lru_cache
import time

@lru_cache(maxsize=100)
def get_portfolio(address, ttl_hash):
    return fetch_wallet_portfolio(address)

# Cache for 5 minutes
def get_ttl_hash(seconds=300):
    return round(time.time() / seconds)

# Usage
portfolio = get_portfolio(address, get_ttl_hash())
```

#### 3. Use Filters to Reduce Data Volume

Smaller responses = faster processing = fewer timeout retries:

```
Use listWalletTransactions with:
- filter[chain_ids]: "ethereum"
- filter[trash]: "only_non_trash"
- page[size]: 50
```

### Monitoring Rate Limit Status

The server logs rate limit events:

```
WARNING: Rate limit exceeded (url=/v1/wallets/.../transactions, retry_after=30s)
INFO: Retrying request after rate limit (attempt=2)
INFO: Request succeeded after 2 retry attempts
```

Enable **DEBUG** logging to see detailed retry information:

```yaml
logging:
  level: "DEBUG"
```

### Disabling Automatic Retry

To disable automatic retry (e.g., for testing):

```yaml
retry_policy:
  max_attempts: 0  # Disable retries
```

Now `429` responses will immediately raise `RateLimitError`.

---

## Error Handling - 202 Accepted (Wallet Indexing)

When you query a **newly created wallet** (first request for that address), Zerion may return `202 Accepted` instead of `200 OK`. This means:

> "Wallet is being indexed. Data will be ready in 2-10 seconds."

### Automatic 202 Retry

The MCP server **automatically retries** `202` responses with a fixed delay:

```
Request โ†’ 202 Accepted (indexing...)
 โ†“ Wait 3 seconds
Retry 1 โ†’ 202 Accepted (still indexing...)
 โ†“ Wait 3 seconds
Retry 2 โ†’ 200 OK (indexing complete!)
```

**You don't see `202` responses** - the server handles them transparently.

### When Does 202 Occur?

- **First request to a new wallet address**: Zerion hasn't indexed it yet
- **Recently created on-chain wallet**: Blockchain confirmed, Zerion indexing in progress
- **Rare edge case**: Heavy load on Zerion's indexing service

**Typical indexing time**: 2-10 seconds (usually 3-5 seconds).

### 202 Configuration

Customize retry behavior in `config.yaml`:

```yaml
wallet_indexing:
  # Delay between retries (seconds)
  retry_delay: 3

  # Maximum retry attempts
  max_retries: 3

  # Automatically retry (recommended: true)
  auto_retry: true
```

**Total wait time**: `retry_delay ร— max_retries` (e.g., 3s ร— 3 = 9 seconds).

### 202 Error Messages

If indexing times out after max retries:

```
WalletIndexingError: Wallet is still being indexed by Zerion.
Tried 3 times over 9 seconds. Please retry in 30-60 seconds.
```

**Next steps**:
1. Wait 30-60 seconds for Zerion to complete indexing
2. Retry your request
3. If issue persists, contact api@zerion.io

### Disabling 202 Auto-Retry

To disable automatic retry (e.g., for instant feedback):

```yaml
wallet_indexing:
  auto_retry: false
```

Now `202` responses will immediately raise:

```
WalletIndexingError: Wallet indexing in progress. This is a
new wallet address for Zerion. Enable auto_retry in
configuration or wait and retry manually.
```

### 202 Logging

The server logs indexing events:

```
INFO: Wallet indexing in progress, will retry (retry_delay=3s, max_retries=3)
INFO: Retrying wallet indexing request (attempt=1/3)
INFO: Wallet indexing completed successfully (attempts=2, total_wait=6s)
```

Or if timeout:

```
WARNING: Wallet indexing timeout (attempts=3, total_wait=9s)
```

### Troubleshooting 202 Errors

#### "Indexing timeout after 3 retries"

**Cause**: Wallet indexing taking longer than expected (>9 seconds).

**Solution**:
1. Increase `max_retries` or `retry_delay`:
   ```yaml
   wallet_indexing:
     retry_delay: 5
     max_retries: 5
   ```
   New total wait: 5s ร— 5 = 25 seconds

2. Wait 1-2 minutes and retry manually

#### "This is a new wallet address"

**Cause**: Normal behavior for first request to a wallet.

**Solution**: No action needed if `auto_retry: true` (default). The server will retry automatically.

---

## Testnet Support

Certain Zerion API endpoints support testnet data through the `X-Env` header parameter. This allows developers to test applications on testnets before deploying to mainnet.

### Supported Testnet Endpoints

The following endpoints accept the `X-Env` header:

- **`listWalletPositions`** - Get wallet positions on testnets
- **`getWalletPortfolio`** - Get portfolio data for testnet wallets
- **`listWalletTransactions`** - Fetch testnet transaction history
- **`listWalletNFTPositions`** - Get NFT positions on testnets
- **`listWalletNFTCollections`** - List NFT collections on testnets
- **`getWalletNftPortfolio`** - Get NFT portfolio for testnets
- **`listFungibles`** - List fungible assets on testnets
- **`getFungibleById`** - Get specific fungible on testnets
- **`listChains`** - List chains (including testnets)

### Using Testnet Data

To query testnet data, include `X-Env: testnet` when calling supported endpoints:

```
Use listWalletPositions with:
- address: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
- X-Env: "testnet"
- currency: "usd"
```

This will return positions for the specified address on testnet chains (e.g., Ethereum Sepolia, Monad Testnet).

### Testnet API Access

**Important**: Testnet access may require special API credentials or specific tier access. Check your API key capabilities:

- Contact api@zerion.io for testnet API access
- Verify your tier includes testnet support
- Some testnets may have limited data availability

### Testnet Development Workflow

**Recommended workflow for testnet-first development:**

1. **Create Test Wallet on Testnet** (e.g., Sepolia)
2. **Fund with Testnet Tokens** (use faucets)
3. **Test MCP Tools** with `X-Env: testnet`
4. **Verify Data Accuracy** on testnet
5. **Deploy to Mainnet** after validation

**Example - Testing Portfolio Fetch**:

```
# Test on Sepolia first
Use getWalletPortfolio with:
- address: "0x..."
- X-Env: "testnet"
- currency: "usd"

# Then move to mainnet
Use getWalletPortfolio with:
- address: "0x..."
- currency: "usd"
```

### Supported Testnet Chains

Use `listChains` with `X-Env: testnet` to see all available testnet chains. Common testnets include:

- **Ethereum Sepolia** (`sepolia`)
- **Monad Testnet** (`monad-testnet`)
- **Base Sepolia** (`base-sepolia`)
- **Optimism Sepolia** (`optimism-sepolia`)
- **Arbitrum Sepolia** (`arbitrum-sepolia`)

**Note**: Testnet chain availability may vary. Use the `listChains` endpoint to get the current list.

### Testnet Limitations

- **Data Freshness**: Testnet indexing may be slower than mainnet
- **Historical Data**: Some testnets have limited historical data
- **Protocol Coverage**: Not all DeFi protocols deployed on testnets
- **Price Data**: Testnet tokens typically have no market price

---

## Multi-Currency Support

The Zerion API supports multiple currencies for price denomination. You can request portfolio values, position prices, and charts in USD, ETH, EUR, or BTC.

### Supported Currencies

| Currency | Code | Use Case |
|----------|------|----------|
| **US Dollar** | `usd` | Default, most common for general users |
| **Ethereum** | `eth` | DeFi analytics, ETH-native communities |
| **Euro** | `eur` | European users, EU compliance requirements |
| **Bitcoin** | `btc` | Bitcoin-maximalist perspectives |

### Currency-Compatible Endpoints

The `currency` parameter is supported on these endpoints:

- **`getWalletPortfolio`** - Portfolio total value in specified currency
- **`listWalletPositions`** - Position values in specified currency
- **`getWalletChart`** - Historical balance chart in specified currency
- **`getWalletPNL`** - Profit/Loss calculations in specified currency
- **`getFungibleChart`** - Asset price charts in specified currency

### Using Different Currencies

**USD (Default)**:
```
Use getWalletPortfolio with:
- address: "0x..."
- currency: "usd"
```

**ETH Denomination**:
```
Use getWalletPortfolio with:
- address: "0x..."
- currency: "eth"
```

This returns portfolio value in ETH. For example, if a wallet is worth $10,000 and ETH is $2,000, the value would be shown as 5 ETH.

**EUR Denomination**:
```
Use listWalletPositions with:
- address: "0x..."
- currency: "eur"
- filter[chain_ids]: "ethereum"
```

All position values will be shown in EUR instead of USD.

**BTC Denomination**:
```
Use getWalletChart with:
- address: "0x..."
- currency: "btc"
- period: "month"
```

Historical portfolio values will be shown in BTC equivalent.

### Currency Parameter Behavior

**Default Behavior**: If `currency` parameter is omitted, USD is used by default.

**Price Fields Affected**: The currency parameter affects:
- `attributes.value` - Total value
- `attributes.price` - Individual asset prices
- `attributes.changes` - Value changes over time
- `attributes.floor_price` - NFT floor prices (where applicable)

**Exchange Rates**: Zerion uses real-time exchange rates for currency conversion. Rates are updated continuously.

### Multi-Currency Examples

**Example 1: DeFi Portfolio in ETH**
```
Use listWalletPositions with:
- address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
- filter[positions]: "only_complex"
- currency: "eth"
```

Result: All DeFi position values shown in ETH denomination.

**Example 2: European User Portfolio**
```
Use getWalletPortfolio with:
- address: "0x..."
- currency: "eur"
```

Result: Portfolio value and all asset prices in EUR.

**Example 3: Historical Performance in BTC**
```
Use getWalletChart with:
- address: "0x..."
- currency: "btc"
- period: "year"
```

Result: Year-long portfolio performance chart denominated in BTC.

### Currency Best Practices

1. **Use USD for General Applications** - Most users expect USD pricing
2. **Use ETH for DeFi Dashboards** - DeFi users often think in ETH terms
3. **Use EUR for EU Compliance** - Required for some European regulatory reporting
4. **Use BTC for Bitcoin Communities** - Aligns with Bitcoin-centric worldview

---

## Development

### Setup Development Environment

```bash
# Clone and install with dev dependencies
git clone https://github.com/SAK1337/myzerionmcp.git
cd myzerionmcp
pip install -e ".[dev]"

# Set up API key
export ZERION_API_KEY="Bearer your-test-key"
```

### Running Tests

```bash
# Run all tests
pytest

# Run with coverage
pytest --cov=zerion_mcp_server --cov-report=html

# Run specific test file
pytest tests/test_config.py

# Run with verbose output
pytest -v
```

### Code Quality

```bash
# Type checking (if mypy is installed)
mypy zerion_mcp_server

# Linting (if ruff is installed)
ruff check zerion_mcp_server
```

## Troubleshooting

### Common Issues

#### "Configuration error: Missing required configuration: api_key"

**Solution**: Set the `ZERION_API_KEY` environment variable:

```bash
export ZERION_API_KEY="Bearer your-api-key-here"
```

#### "Timeout loading OpenAPI specification"

**Solution**: Check your internet connection. The server needs to download the OpenAPI spec from GitHub.

#### "Unauthorized: Invalid or missing API key"

**Solution**: Verify your API key is correct and includes the "Bearer " prefix:

```bash
export ZERION_API_KEY="Bearer your-actual-key"
```

#### "Rate limit exceeded"

**Solution**: Wait for the rate limit window to reset. Check the error message for `retry_after_sec` value.

### Debug Mode

Enable debug logging for detailed information:

```bash
export LOG_LEVEL="DEBUG"
zerion-mcp-server
```

Or in `config.yaml`:

```yaml
logging:
  level: "DEBUG"
  format: "json"  # Structured logs for analysis
```

### Log Interpretation

- **INFO**: Normal operation (startup, requests)
- **WARN**: Potential issues (slow operations)
- **ERROR**: Failures (API errors, network issues)
- **DEBUG**: Detailed traces (request/response bodies)

## Architecture

```
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   MCP Client    โ”‚ (e.g., Claude Desktop)
โ”‚  (AI Assistant) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚ MCP Protocol
         โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  FastMCP Server โ”‚
โ”‚                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ Config    โ”‚  โ”‚ (YAML + Env)
โ”‚  โ”‚ Manager   โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  Logger   โ”‚  โ”‚ (Structured)
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  Error    โ”‚  โ”‚ (Custom Exceptions)
โ”‚  โ”‚  Handler  โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ HTTP      โ”‚  โ”‚ (httpx AsyncClient)
โ”‚  โ”‚ Client    โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ HTTPS
         โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Zerion API    โ”‚
โ”‚                 โ”‚
โ”‚  - Portfolios   โ”‚
โ”‚  - DeFi         โ”‚
โ”‚  - NFTs         โ”‚
โ”‚  - Transactions โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
```

## Tech Stack

- **Python 3.11+**: Core language
- **FastMCP**: MCP server framework with OpenAPI integration
- **httpx**: Async HTTP client
- **PyYAML**: Configuration parsing
- **pytest**: Testing framework

## Contributing

Contributions are welcome! Please follow these guidelines:

1. **Fork the repository**
2. **Create a feature branch**: `git checkout -b feature/your-feature`
3. **Write tests** for your changes
4. **Ensure tests pass**: `pytest`
5. **Submit a pull request**

### Development Workflow

- Follow PEP 8 style guidelines
- Add type hints to function signatures
- Write docstrings for modules and functions
- Update tests for any code changes
- Keep commits focused and atomic

## License

MIT License - see [LICENSE](LICENSE) file for details

## Support

- **Issues**: [GitHub Issues](https://github.com/SAK1337/myzerionmcp/issues)
- **Zerion API Docs**: [developers.zerion.io](https://developers.zerion.io/)
- **MCP Specification**: [modelcontextprotocol.io](https://modelcontextprotocol.io/)

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for version history and changes