Added support for Vector L2 and removed everything else

This commit is contained in:
david
2026-03-17 10:15:55 +01:00
parent 1b9e5ce23c
commit 5b52a3b273
32 changed files with 1378 additions and 5119 deletions

View File

@@ -1,53 +1,16 @@
# Network RPC URLs
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc
BASE_RPC_URL=https://mainnet.base.org
OPTIMISM_RPC_URL=https://mainnet.optimism.io
BSC_RPC_URL=https://bsc-dataseed.binance.org
POLYGON_RPC_URL=https://polygon-rpc.com
AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
BERACHAIN_RPC_URL=https://rpc.berachain.com
SONIC_RPC_URL=https://rpc.soniclabs.com/
XRP_RPC_URL=https://xrplcluster.com/
TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC
TON_API_KEY=your-toncenter-api-key
# Vector MCP Server Configuration
# ================================
# Private keys (DO NOT SHARE)
ETH_PRIVATE_KEY=your-eth-private-key
SOLANA_PRIVATE_KEY=your-solana-private-key
THORCHAIN_PRIVATE_KEY=your-thorchain-private-key
THORCHAIN_MNEMONIC=your-thorchain-mnemonic
# XRP Ledger credentials - can use either private key or mnemonic
XRP_PRIVATE_KEY=your-xrp-private-key # Private key in hex format
XRP_MNEMONIC=your-xrp-mnemonic # Alternative: 12-word recovery phrase
XRP_ADDRESS=your-xrp-address # Optional: Used to verify key derivation
# Vector Network Endpoints
VECTOR_OGMIOS_URL=https://ogmios.vector.testnet.apexfusion.org
VECTOR_KOIOS_URL=https://koios.vector.testnet.apexfusion.org/
VECTOR_SUBMIT_URL=https://submit.vector.testnet.apexfusion.org/api/submit/tx
VECTOR_EXPLORER_URL=https://vector.testnet.apexscan.org
# TON Configuration
TON_MNEMONIC=word1 word2 word3 ... word24 # 24-word recovery phrase
TON_ADDRESS=your-ton-address # Your TON wallet address
# Wallet Configuration
VECTOR_MNEMONIC=word1 word2 word3 ... word15 # Must be exactly 15 or 24 words
VECTOR_ACCOUNT_INDEX=0 # Wallet account index (default: 0)
# Cardano Configuration
BLOCKFROST_API_KEY=your-blockfrost-api-key # Register at https://blockfrost.io/
CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod'
CARDANO_MNEMONIC=word1 word2 word3 ... word15 # Must be exactly 15 or 24 words
CARDANO_ACCOUNT_INDEX=0 # Wallet account index (default: 0)
# UTXO Chain API Keys (optional)
BLOCKCYPHER_API_KEY=your-blockcypher-api-key
SOCHAIN_API_KEY=your-sochain-api-key
# Tool Registration Controls (true/false)
ENABLE_SOLANA_TOOLS=true # Enable/disable Solana-specific tools
ENABLE_EVM_TOOLS=true # Enable/disable EVM chain tools (Ethereum, Base, Arbitrum, etc.)
ENABLE_BITCOIN_TOOLS=true
ENABLE_LITECOIN_TOOLS=true
ENABLE_DOGECOIN_TOOLS=true
ENABLE_BITCOINCASH_TOOLS=true
ENABLE_THORCHAIN_TOOLS=true
ENABLE_RIPPLE_TOOLS=true
ENABLE_CARDANO_TOOLS=true # Enable/disable Cardano tools
ENABLE_TON_TOOLS=true # Enable/disable TON tools
# CoinGecko API Key
COINGECKO_API_KEY=CG-Z9ajuvwG1cheNJSByUgmvTgg #replace with your-coingecko-api-key if you hit rate limits
# Safety Limits (in lovelace, 1 ADA = 1,000,000 lovelace)
VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA per transaction
VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA daily limit

390
README.md
View File

@@ -1,354 +1,112 @@
# Web3 MCP
# Vector MCP Server
A Model-Context-Protocol server for interacting with multiple blockchains including Solana, Ethereum, THORChain, XRP Ledger, TON (The Open Network), Cardano, and UTXO chains. This server provides simple RPC endpoints for common blockchain operations, allowing secure interactions with various blockchains through environment variables.
MCP (Model Context Protocol) server for the **Vector blockchain** — Apex Fusion's L2. Enables AI agents (Claude, GPT, Gemini, or any MCP client) to interact with Vector natively: query balances, send ADA, transfer tokens, and manage spending safely.
<a href="https://glama.ai/mcp/servers/an8x6gmzdn"><img width="380" height="200" src="https://glama.ai/mcp/servers/an8x6gmzdn/badge" alt="Web3 Server MCP server" /></a>
Built on [Ogmios](https://ogmios.dev/) + [Koios](https://www.koios.rest/) — no Blockfrost dependency.
## Features
Solana Operations:
- Check SOL account balances
- Get detailed account information
- Transfer SOL between accounts
- View SPL token balances
- Get your wallet address from private key
- Swap tokens using Jupiter (Best price routing across all Solana DEXs)
- **Wallet management** — derive addresses from mnemonic, query balances and UTxOs
- **ADA transfers** — build, sign, and submit transactions via submit-api
- **Native token transfers** — send any Vector native asset
- **Safety controls** — per-transaction and daily spend limits with audit logging
- **Ogmios-native** — direct chain access, no third-party indexer lock-in
Ethereum & EVM Chain Operations:
- Check native token balances across multiple networks
- Check ERC-20 token balances
- Send native tokens (using private key from .env)
- Send ERC-20 tokens (using private key from .env)
- Approve ERC-20 token spending (using private key from .env)
## MCP Tools
Cardano Operations:
- Get network information and statistics
- Check address balances and transaction history
- View UTxOs for an address
- Explore stake pools and delegation information
- Get details about native assets
- View detailed transaction information
- Get statistics about the current epoch
| Tool | Description |
|------|-------------|
| `vector_get_balance` | Get ADA and token balances for any Vector address |
| `vector_get_address` | Get the agent's wallet address and balance |
| `vector_get_utxos` | List UTxOs for an address or the agent's wallet |
| `vector_send_ada` | Send ADA (respects spend limits) |
| `vector_send_tokens` | Send native tokens |
| `vector_get_spend_limits` | Check spend limits and remaining budget |
THORChain Operations:
- Check RUNE balances
- Get detailed pool information
- Get swap quotes between any supported assets
- Cross-chain swaps via THORChain protocol
## Quick Start
UTXO Chain Operations:
- Bitcoin (BTC)
- Check address balances
- View transaction history
- Validate addresses
- Get network info and fees
- Litecoin (LTC)
- Check address balances
- View transaction history
- Validate addresses
- Get network info and fees
- Dogecoin (DOGE)
- Check address balances
- View transaction history
- Validate addresses
- Get network info and fees
- Bitcoin Cash (BCH)
- Check address balances
- View transaction history
- Validate addresses
- Get network info and fees
### 1. Install dependencies
XRP Ledger Operations:
- Check XRP account balances
- View transaction history
- Validate XRP addresses
- Send XRP to another address
- Get current XRP Ledger information
- Check token balances
- Create trustlines for tokens
TON (The Open Network) Operations:
- Get TON account balances
- View transaction history
- Validate TON addresses
- Send TON to another address with optional memo/comment
- Get current TON network information
Supported EVM Networks:
- Ethereum
- Base
- Arbitrum
- Optimism
- BSC (Binance Smart Chain)
- Polygon
- Avalanche
- Berachain
## Setup
1. Clone and install dependencies:
```bash
git clone https://github.com/strangelove-ventures/web3-mcp.git
cd web3-mcp
npm install
```
2. Create a .env file in the root directory:
### 2. Configure environment
```bash
cp .env.example .env
# Edit .env with your mnemonic and endpoint URLs
```
3. Configure your environment variables in .env:
### 3. Build
### Required Configuration
```env
# Tool Registration Controls
ENABLE_SOLANA_TOOLS=true # Enable/disable Solana tools
ENABLE_ETHEREUM_TOOLS=true # Enable/disable Ethereum and EVM chain tools
ENABLE_CARDANO_TOOLS=true # Enable/disable Cardano tools
# UTXO Chain Tools
ENABLE_BITCOIN_TOOLS=true # Enable/disable Bitcoin tools
ENABLE_LITECOIN_TOOLS=true # Enable/disable Litecoin tools
ENABLE_DOGECOIN_TOOLS=true # Enable/disable Dogecoin tools
ENABLE_BITCOINCASH_TOOLS=true # Enable/disable Bitcoin Cash tools
ENABLE_THORCHAIN_TOOLS=true # Enable/disable THORChain tools
ENABLE_RIPPLE_TOOLS=true # Enable/disable XRP Ledger tools
ENABLE_TON_TOOLS=true # Enable/disable TON tools
# Private Keys (required for transactions)
ETH_PRIVATE_KEY=your-ethereum-private-key
SOLANA_PRIVATE_KEY=your-base58-encoded-solana-private-key
# XRP Ledger credentials (required for XRP transactions)
# Either private key or mnemonic is required
XRP_PRIVATE_KEY=your-xrp-private-key-in-hex
# OR
XRP_MNEMONIC=your-xrp-mnemonic-recovery-phrase
# Optional - used to verify the derived address
XRP_ADDRESS=your-xrp-account-address
# TON Configuration
TON_MNEMONIC=word1 word2 word3 ... word24 # 24-word recovery phrase for TON wallet
TON_ADDRESS=your-ton-wallet-address # Your TON wallet address
TON_API_KEY=your-toncenter-api-key # Get from @tonapibot on Telegram (optional but recommended)
TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC # Optional - default is TON Center API
# Cardano Configuration
BLOCKFROST_API_KEY=your-blockfrost-api-key # Get a real API key from https://blockfrost.io/
CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod'
CARDANO_MNEMONIC=your-cardano-mnemonic-phrase # Required for transaction signing
CARDANO_ACCOUNT_INDEX=0 # Optional - defaults to 0
```
### Optional Configuration
```env
# Network RPC URLs (optional - will use public endpoints if not specified)
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
# Ethereum & Layer 2s
ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc
BASE_RPC_URL=https://mainnet.base.org
OPTIMISM_RPC_URL=https://mainnet.optimism.io
# Other EVM Chains
BSC_RPC_URL=https://bsc-dataseed.binance.org
POLYGON_RPC_URL=https://polygon-rpc.com
AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
BERACHAIN_RPC_URL=https://rpc.berachain.com
# XRP Ledger
XRP_RPC_URL=https://xrplcluster.com/ # Optional - will use public endpoint if not specified
# TON Network
TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC # Optional - default is TON Center API
TON_API_KEY=your-toncenter-api-key # Get from @tonapibot on Telegram (optional but recommended)
# THORChain Configuration
THORCHAIN_NODE_URL=https://thornode.ninerealms.com # Optional - will use public endpoint if not specified
THORCHAIN_PRIVATE_KEY=your-thorchain-private-key
THORCHAIN_MNEMONIC=your-thorchain-mnemonic
# UTXO Chain API Keys (optional)
BLOCKCYPHER_API_KEY=your-blockcypher-api-key
SOCHAIN_API_KEY=your-sochain-api-key
```
4. Build the tool:
```bash
npm run build
```
5. Add the tool to your claude_desktop_config.json:
### 4. Add to Claude Desktop
Add to your Claude Desktop MCP config (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"web3-rpc": {
"vector": {
"command": "node",
"args": [
"/PATH/TO/web3-mcp/build/index.js"
]
"args": ["/path/to/vector-mcp-server/build/index.js"],
"env": {
"VECTOR_MNEMONIC": "your mnemonic words here",
"VECTOR_OGMIOS_URL": "https://ogmios.vector.testnet.apexfusion.org",
"VECTOR_SUBMIT_URL": "https://submit.vector.testnet.apexfusion.org/api/submit/tx",
"VECTOR_KOIOS_URL": "https://koios.vector.testnet.apexfusion.org/",
"VECTOR_EXPLORER_URL": "https://vector.testnet.apexscan.org"
}
}
}
}
```
## Tool Registration
## Configuration
The Web3 MCP server allows you to control which blockchain tools are registered through environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `VECTOR_OGMIOS_URL` | Ogmios HTTP JSON-RPC endpoint | `https://ogmios.vector.testnet.apexfusion.org` |
| `VECTOR_KOIOS_URL` | Koios REST API endpoint | `https://koios.vector.testnet.apexfusion.org/` |
| `VECTOR_SUBMIT_URL` | Transaction submit API | `https://submit.vector.testnet.apexfusion.org/api/submit/tx` |
| `VECTOR_EXPLORER_URL` | Block explorer base URL | `https://vector.testnet.apexscan.org` |
| `VECTOR_MNEMONIC` | 15 or 24-word BIP39 mnemonic | *(required)* |
| `VECTOR_ACCOUNT_INDEX` | HD wallet account index | `0` |
| `VECTOR_SPEND_LIMIT_PER_TX` | Max lovelace per transaction | `100000000` (100 ADA) |
| `VECTOR_SPEND_LIMIT_DAILY` | Max lovelace per day | `500000000` (500 ADA) |
- `ENABLE_SOLANA_TOOLS`: Enable/disable Solana tools
- `ENABLE_ETHEREUM_TOOLS`: Enable/disable Ethereum and EVM chain tools
- `ENABLE_BITCOIN_TOOLS`: Enable/disable Bitcoin tools
- `ENABLE_LITECOIN_TOOLS`: Enable/disable Litecoin tools
- `ENABLE_DOGECOIN_TOOLS`: Enable/disable Dogecoin tools
- `ENABLE_BITCOINCASH_TOOLS`: Enable/disable Bitcoin Cash tools
- `ENABLE_THORCHAIN_TOOLS`: Enable/disable THORChain tools
- `ENABLE_RIPPLE_TOOLS`: Enable/disable XRP Ledger tools
- `ENABLE_CARDANO_TOOLS`: Enable/disable Cardano tools
- `ENABLE_TON_TOOLS`: Enable/disable TON tools
## Architecture
Set these variables to `true` or `false` in your `.env` file to control which tools are available to the server. This allows you to:
```
┌──────────────────────┐ ┌──────────────────────────┐
│ Claude / GPT / etc. │◄────►│ vector-mcp-server │
│ (any MCP client) │ MCP │ │
└──────────────────────┘ │ ┌────────────────────┐ │
│ │ Safety Layer │ │
│ │ - Per-tx limits │ │
│ │ - Daily limits │ │
│ │ - Audit log │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Lucid + Ogmios │ │
│ │ Provider │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Ogmios / Koios / │ │
│ │ Submit API │ │
│ └────────────────────┘ │
└──────────────────────────┘
```
- Reduce startup time by only loading required tools
- Minimize security surface area by disabling unused chains
- Customize the server for specific use cases
- Control resource usage by limiting active connections
## About Vector
## Usage Examples
Vector is Apex Fusion's UTXO-based L2 blockchain, running with Cardano's mainnet parameters. It provides near-instant finality and 4x Cardano throughput, making it ideal for AI agent interactions.
Ask Claude (or your MCP client of choice):
### Solana Operations (when ENABLE_SOLANA_TOOLS=true)
- "What's my Solana address?" - Shows your address derived from private key in .env
- "What's the balance of 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3?"
- "Transfer 0.001 SOL to Cg6cVS4tjkxHthm3K9BHhmvqF7kSz8GnXqqYXnHBzGXd"
- "Show me my SPL token balances"
- "Swap 0.1 SOL to USDC" (Uses Jupiter for best price routing)
### EVM Operations (when ENABLE_ETHEREUM_TOOLS=true)
- "What's the ETH balance of 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933?"
- "Check the USDC balance for 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Ethereum"
- "Send 0.1 ETH to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933"
- "What's the current gas price on Arbitrum?"
- "Send 100 USDC to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Polygon"
### Cardano Operations (when ENABLE_CARDANO_TOOLS=true)
- Get the balance of any Cardano
- View your wallet information
- Send ADA to another address
- Send native tokens to another address
### Bitcoin Operations (when ENABLE_BITCOIN_TOOLS=true)
- "What's the BTC balance of 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?"
- "Show me the transaction history for bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
- "Validate this Bitcoin address: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
### Litecoin Operations (when ENABLE_LITECOIN_TOOLS=true)
- "What's the LTC balance of LM2WMpR1Rp6j3Sa59cMXMs1SPzj9eXpGc1?"
- "Show me the transaction history for this Litecoin address"
- "What's the current Litecoin network fee?"
### Dogecoin Operations (when ENABLE_DOGECOIN_TOOLS=true)
- "Check this DOGE address balance: D8vFz4p1L37jdg47HXKtSHA5uYLYxbGgPD"
- "Show me recent Dogecoin transactions"
- "What are the current DOGE network fees?"
### Bitcoin Cash Operations (when ENABLE_BITCOINCASH_TOOLS=true)
- "What's the BCH balance of this address?"
- "Show me the BCH transaction history"
- "Validate this Bitcoin Cash address"
### THORChain Operations (when ENABLE_THORCHAIN_TOOLS=true)
- "What's the RUNE balance of thor13zpdckczd0jvyhwxmrwnpap8gmy9m5kk2gzum3?"
- "Show me the pool information for BTC.BTC"
- "Get a swap quote for 0.1 BTC.BTC to ETH.ETH"
### XRP Ledger Operations (when ENABLE_RIPPLE_TOOLS=true)
- "What's the XRP balance of rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe?"
- "Show me the transaction history for rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
- "Is rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe a valid XRP address?"
- "Send 10 XRP to rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
- "What's the current XRP Ledger information?"
- "Show me token balances for rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
- "Create a trustline for USD with issuer rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe with a limit of 1000"
### TON Operations (when ENABLE_TON_TOOLS=true)
- "What's the TON balance of EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ?"
- "Show me the transaction history for EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ"
- "Is UQD0BRQt-QdIEbsjuRsMqzDlBkUAEfQixShDECoKEOXRc4eR a valid TON address?"
- "Send 0.1 TON to EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ"
- "Send 0.01 TON to UQD0BRQt-QdIEbsjuRsMqzDlBkUAEfQixShDECoKEOXRc4eR with comment 'test payment'"
- "What's the current TON Network information?"
## Security Notes
1. **Environment Variables**: All private keys are stored in the .env file and never exposed in the conversation history
2. **Private Keys**: Only use this with test wallets containing small amounts of funds
3. **RPC Endpoints**: Custom RPC endpoints can be configured in the .env file for better reliability and rate limits
4. **.env Security**: The .env file is automatically ignored by git to prevent accidental exposure of private keys
5. **Tool Registration**: Use the tool registration controls to minimize security surface area by only enabling required chains
## Advanced Configuration
### Custom RPC Endpoints
You can configure custom RPC endpoints in your .env file for better reliability and higher rate limits. If not specified, the tool will fall back to public RPC endpoints.
### Network Selection
For EVM operations, you can specify the network by name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain). The tool will automatically use the appropriate RPC endpoint and network configuration.
### Cardano Configuration
The tool uses the Blockfrost API and Lucid library to interact with the Cardano blockchain.
- `BLOCKFROST_API_KEY`: Required - Your Blockfrost API key (register at https://blockfrost.io/)
- `CARDANO_NETWORK`: Optional - The Cardano network to use (mainnet, testnet, preview, preprod). Defaults to 'mainnet'
- `CARDANO_MNEMONIC`: Required for transactions - Your Cardano wallet's mnemonic phrase (15 or 24 words)
- `CARDANO_ACCOUNT_INDEX`: Optional - The account index to use (defaults to 0)
The wallet derived from your mnemonic will be used to sign and send transactions.
### THORChain Configuration
The tool uses Nine Realms public endpoints by default, but you can configure a custom THORChain node URL in the .env file for better reliability and rate limits.
### XRP Ledger Configuration
The tool can use either a private key or mnemonic phrase for XRP transactions. Configure these in your .env file:
- `XRP_PRIVATE_KEY`: Your XRP private key in hex format
- `XRP_MNEMONIC`: Alternative to private key - your 12-word recovery phrase
- `XRP_ADDRESS`: Optional - Your XRP account address (used to verify the derived address)
- `XRP_RPC_URL`: Optional - Custom XRP Ledger node URL (defaults to public endpoint)
### TON Configuration
The tool uses TON Center's API by default and requires a mnemonic phrase for TON transactions. Configure these in your .env file:
- `TON_MNEMONIC`: Required - Your 24-word recovery phrase for TON wallet
- `TON_ADDRESS`: Required - Your TON wallet address
- `TON_API_KEY`: Recommended - API key from @tonapibot on Telegram (for higher rate limits)
- `TON_RPC_URL`: Optional - Custom TON RPC URL (defaults to TON Center API)
The implementation includes automatic retry logic with exponential backoff for rate limit handling.
### UTXO Chain Data Providers
The tool uses several data providers for UTXO chains:
- BlockCypher
- SoChain
- Haskoin (for Bitcoin Cash)
You can configure API keys for these providers in the .env file for better rate limits.
## Development
To modify or extend the tool:
1. Source code is in the `src` directory
2. Chain-specific code in `src/chains`
3. Run `npm run build` after making changes
4. Use TypeScript for all new code
## Contributing
Contributions are welcome! Please submit pull requests with any improvements or bug fixes.
## License
ISC License
- **Explorer:** https://vector.testnet.apexscan.org
- **Apex Fusion:** https://apexfusion.org

View File

@@ -1,213 +0,0 @@
# Cardano Support for Web3-MCP
This document covers the Cardano blockchain integration for web3-mcp.
## Overview
The Cardano integration in web3-mcp allows you to interact with the Cardano blockchain through the Blockfrost API, providing access to network data, address information, transaction details, stake pools, and more.
## Prerequisites
To use the Cardano integration, you need a Blockfrost API key. You can get one by signing up at [blockfrost.io](https://blockfrost.io/).
## Configuration
Update your `.env` file with the following settings:
```
# Cardano Configuration
BLOCKFROST_API_KEY=your-blockfrost-api-key-here
CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod'
ENABLE_CARDANO_TOOLS=true
```
## Available Tools
### Network Information
#### 1. Get Cardano Network Information
```javascript
const response = await server.call("getCardanoNetworkInfo", {});
```
Returns current information about the Cardano network including:
- Latest block details (height, hash, slot, epoch)
- Network parameters
- Timing information
#### 2. Get Cardano Network Statistics
```javascript
const response = await server.call("getCardanoNetworkStats", {});
```
Returns statistics about the Cardano network including:
- Number of blocks
- Number of transactions
- Number of native tokens
- Number of addresses with ADA
- Number of delegated stakes
#### 3. Get Current Epoch Information
```javascript
const response = await server.call("getCardanoEpochInfo", {});
```
Returns information about the current epoch including:
- Epoch number
- Start and end time
- Block and transaction counts
- Protocol parameters
### Address Operations
#### 1. Get Address Information
```javascript
const response = await server.call("getCardanoAddressInfo", {
address: "addr1qyx...",
});
```
Returns information about a Cardano address including:
- Balance in ADA
- UTxO count
- Address type (script or key-based)
- Stake address (if delegated)
#### 2. Get Address Transactions
```javascript
const response = await server.call("getCardanoAddressTransactions", {
address: "addr1qyx...",
count: 10, // Optional, default is 10
page: 1 // Optional, default is 1
});
```
Returns a list of transactions for the specified address, including:
- Transaction hash
- Block and slot
- Output amounts
- Fees
- Date and time
#### 3. Get Address UTxOs
```javascript
const response = await server.call("getCardanoAddressUtxos", {
address: "addr1qyx...",
});
```
Returns a list of unspent transaction outputs (UTxOs) for the specified address.
### Staking Operations
#### 1. Get Stake Address Information
```javascript
const response = await server.call("getCardanoStakeAddressInfo", {
stakeAddress: "stake1ux...",
});
```
Returns information about a Cardano stake address including:
- Total balance delegated
- Rewards balance
- Withdrawal history
- Delegation information
- Recent rewards
#### 2. Get Pool Information
```javascript
const response = await server.call("getCardanoPoolInfo", {
poolId: "pool1...",
});
```
Returns detailed information about a specific stake pool:
- Pool metadata (name, ticker, description)
- Performance metrics
- Stake and delegator information
- Reward details
#### 3. List Top Stake Pools
```javascript
const response = await server.call("listCardanoTopPools", {
count: 10, // Optional, default is 10
});
```
Returns a list of the top stake pools by live stake, including key metrics for each pool.
### Asset Operations
#### 1. Get Asset Information
```javascript
const response = await server.call("getCardanoAssetInfo", {
assetId: "asset1...",
});
```
Returns detailed information about a Cardano native asset:
- Asset policy and name
- Total supply
- Mint/burn history
- Metadata
- Top addresses holding the asset
### Transaction Operations
#### 1. Get Transaction Information
```javascript
const response = await server.call("getCardanoTransactionInfo", {
txHash: "5c7...",
});
```
Returns detailed information about a Cardano transaction:
- Basic transaction details (block, slot, fees)
- Inputs and outputs
- Metadata
- Validity interval
## Error Handling
All tools return a standardized error format if they fail:
```javascript
{
content: [
{
type: "text",
text: "Failed to [operation]: [error message]"
}
]
}
```
## Advanced Usage
For more advanced Cardano operations that go beyond querying blockchain data, such as:
1. Creating and submitting transactions
2. Minting native tokens
3. Working with smart contracts
You may need to extend the functionality by implementing additional tools that use:
- [Cardano Serialization Library](https://github.com/Emurgo/cardano-serialization-lib)
- [Mesh.js](https://meshjs.dev/) for client-side operations
## References
- [Blockfrost Documentation](https://docs.blockfrost.io/)
- [Cardano Developer Portal](https://developers.cardano.org/)
- [Mesh.js Documentation](https://meshjs.dev/)

104
docs/VECTOR.md Normal file
View File

@@ -0,0 +1,104 @@
# Vector MCP Server — Tool Reference
## Overview
The Vector MCP Server exposes blockchain tools that AI agents can use to interact with the Vector network. All tools use the `vector_` prefix and communicate with Vector via Ogmios (chain queries) and the submit API (transaction submission).
## Prerequisites
1. A funded Vector wallet (15 or 24-word mnemonic)
2. Access to Vector network endpoints (Ogmios, submit API, Koios)
3. Node.js >= 14.0.0
## Tools
### vector_get_balance
Get ADA and token balances for any Vector address.
**Parameters:**
- `address` (string, required) — Vector address to check (addr1...)
**Example prompt:** "What's the balance of addr1qx..."
### vector_get_address
Get the agent's own wallet address, balance, and token holdings.
**Parameters:** None
**Example prompt:** "What's my wallet address and balance?"
### vector_get_utxos
List unspent transaction outputs for an address or the agent's wallet.
**Parameters:**
- `address` (string, optional) — Address to query. Uses agent's wallet if omitted.
**Example prompt:** "Show me the UTxOs in my wallet"
### vector_send_ada
Send ADA from the agent's wallet to a recipient. Enforces spend limits.
**Parameters:**
- `recipientAddress` (string, required) — Recipient address (addr1...)
- `amount` (number, required) — Amount of ADA to send (minimum 1)
- `metadata` (string, optional) — Transaction metadata in JSON format
**Example prompt:** "Send 5 ADA to addr1qy..."
### vector_send_tokens
Send Vector native tokens from the agent's wallet.
**Parameters:**
- `recipientAddress` (string, required) — Recipient address (addr1...)
- `policyId` (string, required) — Token policy ID
- `assetName` (string, required) — Asset name (can be empty)
- `amount` (string, required) — Amount of tokens to send
- `adaAmount` (number, optional) — ADA to include with tokens
**Example prompt:** "Send 100 of token with policy abc123... to addr1qy..."
### vector_get_spend_limits
Check current spend limits, daily usage, and recent transaction audit log.
**Parameters:** None
**Example prompt:** "What are my current spend limits?"
## Safety Controls
The server enforces configurable spend limits:
- **Per-transaction limit** — Maximum ADA per single transaction (default: 100 ADA)
- **Daily limit** — Maximum ADA per 24-hour period (default: 500 ADA)
- **Audit log** — All transactions are logged with timestamp, amount, and recipient
Limits are configured via environment variables:
```
VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA in lovelace
VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA in lovelace
```
## Network Endpoints
| Service | URL | Protocol |
|---------|-----|----------|
| Ogmios | ogmios.vector.testnet.apexfusion.org | HTTP JSON-RPC + WebSocket |
| Submit API | submit.vector.testnet.apexfusion.org/api/submit/tx | HTTP POST (CBOR) |
| Koios | koios.vector.testnet.apexfusion.org | REST API |
| Explorer | vector.testnet.apexscan.org | Web UI |
## Error Handling
All tools return helpful error messages with troubleshooting tips. Common issues:
- **Invalid mnemonic** — Check VECTOR_MNEMONIC has exactly 15 or 24 words
- **Insufficient balance** — Ensure wallet has enough ADA for the transaction + fees
- **Spend limit exceeded** — Check limits with `vector_get_spend_limits`
- **Connection failed** — Verify Ogmios endpoint is reachable
- **Invalid address** — Vector addresses start with `addr1`

View File

@@ -1,47 +1,39 @@
{
"name": "web3-mcp",
"name": "vector-mcp-server",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"bin": {
"web3-mcp": "./build/index.js"
"vector-mcp": "./build/index.js"
},
"scripts": {
"clean": "rm -rf build",
"build": "npm run clean && tsc && chmod +x build/index.js",
"build": "npm run clean && NODE_OPTIONS='--max-old-space-size=8192' tsc && chmod +x build/index.js",
"start": "node build/index.js"
},
"files": [
"build"
],
"keywords": [
"solana",
"vector",
"mcp",
"rpc",
"blockchain",
"xrp",
"ripple",
"cardano",
"ton"
"apex-fusion",
"ai-agent",
"utxo"
],
"author": "",
"author": "Apex Fusion",
"license": "ISC",
"description": "Blockchain RPC Model-Context-Protocol Server for Solana, EVM chains, UTXO chains, Ripple (XRP), Cardano, TON and more",
"description": "MCP server for Vector blockchain — AI agent interface for Apex Fusion's L2",
"repository": {
"type": "git",
"url": "https://github.com/akc2267/web3-mcp.git"
"url": "https://github.com/Apex-Fusion/vector-ai-agents.git"
},
"homepage": "https://github.com/akc2267/web3-mcp#readme",
"homepage": "https://github.com/Apex-Fusion/vector-ai-agents#readme",
"bugs": {
"url": "https://github.com/akc2267/web3-mcp/issues"
"url": "https://github.com/Apex-Fusion/vector-ai-agents/issues"
},
"contributors": [
{
"name": "Alex Cheng",
"email": "alexanderkcheng@gmail.com",
"url": "https://github.com/akc2267"
}
],
"engines": {
"node": ">=14.0.0"
},
@@ -50,42 +42,14 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@cosmos-client/core": "^0.46.1",
"@jup-ag/api": "^6.0.12",
"@modelcontextprotocol/sdk": "^1.4.1",
"@psf/bitcoincashjs-lib": "^4.0.3",
"@solana/spl-token": "^0.4.1",
"@solana/web3.js": "^1.87.6",
"@xchainjs/xchain-bitcoin": "^1.2.2",
"@xchainjs/xchain-bitcoincash": "^1.0.10",
"@xchainjs/xchain-client": "^1.0.10",
"@xchainjs/xchain-crypto": "^0.3.7",
"@xchainjs/xchain-doge": "^1.0.10",
"@xchainjs/xchain-litecoin": "^1.0.10",
"@xchainjs/xchain-midgard-query": "^1.0.11",
"@xchainjs/xchain-thorchain": "^2.0.13",
"@xchainjs/xchain-thorchain-amm": "^2.0.23",
"@xchainjs/xchain-thorchain-query": "^1.0.13",
"@xchainjs/xchain-util": "^1.0.8",
"bchaddrjs": "^0.5.2",
"bip39": "^3.1.0",
"bitcoinjs-lib": "^6.1.5",
"bs58": "^5.0.0",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"ethers": "^6.11.1",
"fetch-retry": "^5.0.6",
"lucid-cardano": "^0.10.11",
"ripple-keypairs": "^1.3.1",
"wif": "^4.0.0",
"xrpl": "^2.14.0",
"@ton/ton": "13.5.1",
"@ton/crypto": "3.2.0",
"@ton/core": "0.52.2",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^22.13.1",
"typescript": "^5.7.3"
}
}
}

8
shell.nix Normal file
View File

@@ -0,0 +1,8 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodePackages.npm
];
}

View File

@@ -1,145 +0,0 @@
import { Client as BitcoinClient, defaultBTCParams, BlockcypherDataProviders as BTCBlockcypherDataProviders } from '@xchainjs/xchain-bitcoin'
import { Network } from '@xchainjs/xchain-client'
import { z } from "zod"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
// Initialize client with mainnet by default
const bitcoinClient = new BitcoinClient({
...defaultBTCParams,
network: Network.Mainnet,
dataProviders: [BTCBlockcypherDataProviders],
})
// Helper function to format base amounts
function formatBaseAmount(baseAmount: any): string {
return baseAmount.amount().toString()
}
export function registerBitcoinTools(server: McpServer) {
// Bitcoin Balance
server.tool(
"getBitcoinBalance",
"Get balance for a Bitcoin address",
{
address: z.string().describe("Bitcoin address to check"),
},
async ({ address }) => {
try {
const balances = await bitcoinClient.getBalance(address)
return {
content: [
{
type: "text",
text: `Bitcoin Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} BTC`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve BTC balance: ${error.message}` }],
}
}
}
)
// Transaction History
server.tool(
"getBTCTransactionHistory",
"Get transaction history for a Bitcoin address",
{
address: z.string().describe("Bitcoin address to check"),
limit: z.number().optional().describe("Maximum number of transactions to return"),
offset: z.number().optional().describe("Number of transactions to skip"),
},
async ({ address, limit = 10, offset = 0 }) => {
try {
const txs = await bitcoinClient.getTransactions({ address, limit, offset })
const txList = txs.txs.map((tx: any) => {
const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} BTC` : 'Unknown'
const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} BTC` : 'Unknown'
return `
Transaction: ${tx.hash}
Type: ${tx.type}
From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount})
To: ${tx.to[0]?.to || 'Unknown'} (${toAmount})
Asset: ${tx.asset.ticker}
Date: ${new Date(tx.date).toLocaleString()}`
}).join('\n---\n')
return {
content: [
{
type: "text",
text: `Bitcoin Transaction History for ${address}:\n${txList}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve BTC transaction history: ${error.message}` }],
}
}
}
)
// Address Validation
server.tool(
"validateBTCAddress",
"Validate a Bitcoin address format",
{
address: z.string().describe("Bitcoin address to validate"),
},
async ({ address }) => {
try {
const isValid = bitcoinClient.validateAddress(address)
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} is a valid Bitcoin address`
: `The address ${address} is NOT a valid Bitcoin address`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Error validating Bitcoin address: ${error.message}` }],
}
}
}
)
// Network Info
server.tool(
"getBTCNetworkInfo",
"Get current Bitcoin network information",
{},
async () => {
try {
const fees = await bitcoinClient.getFeeRates()
return {
content: [
{
type: "text",
text: `Bitcoin Network Information:
Current Network: ${bitcoinClient.getNetwork()}
Fee Rates (sats/byte):
Fast: ${fees.fast}
Average: ${fees.average}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve Bitcoin network information: ${error.message}` }],
}
}
}
)
}

View File

@@ -1,144 +0,0 @@
import { Client as BitcoinCashClient, defaultBchParams } from '@xchainjs/xchain-bitcoincash'
import { Network } from '@xchainjs/xchain-client'
import { z } from "zod"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
// Initialize client with mainnet by default
const bitcoinCashClient = new BitcoinCashClient({
...defaultBchParams,
network: Network.Mainnet,
})
// Helper function to format base amounts
function formatBaseAmount(baseAmount: any): string {
return baseAmount.amount().toString()
}
export function registerBitcoinCashTools(server: McpServer) {
// Bitcoin Cash Balance
server.tool(
"getBitcoinCashBalance",
"Get balance for a Bitcoin Cash address",
{
address: z.string().describe("Bitcoin Cash address to check"),
},
async ({ address }) => {
try {
const balances = await bitcoinCashClient.getBalance(address)
return {
content: [
{
type: "text",
text: `Bitcoin Cash Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} BCH`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve BCH balance: ${error.message}` }],
}
}
}
)
// Transaction History
server.tool(
"getBCHTransactionHistory",
"Get transaction history for a Bitcoin Cash address",
{
address: z.string().describe("Bitcoin Cash address to check"),
limit: z.number().optional().describe("Maximum number of transactions to return"),
offset: z.number().optional().describe("Number of transactions to skip"),
},
async ({ address, limit = 10, offset = 0 }) => {
try {
const txs = await bitcoinCashClient.getTransactions({ address, limit, offset })
const txList = txs.txs.map((tx: any) => {
const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} BCH` : 'Unknown'
const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} BCH` : 'Unknown'
return `
Transaction: ${tx.hash}
Type: ${tx.type}
From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount})
To: ${tx.to[0]?.to || 'Unknown'} (${toAmount})
Asset: ${tx.asset.ticker}
Date: ${new Date(tx.date).toLocaleString()}`
}).join('\n---\n')
return {
content: [
{
type: "text",
text: `Bitcoin Cash Transaction History for ${address}:\n${txList}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve BCH transaction history: ${error.message}` }],
}
}
}
)
// Address Validation
server.tool(
"validateBCHAddress",
"Validate a Bitcoin Cash address format",
{
address: z.string().describe("Bitcoin Cash address to validate"),
},
async ({ address }) => {
try {
const isValid = bitcoinCashClient.validateAddress(address)
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} is a valid Bitcoin Cash address`
: `The address ${address} is NOT a valid Bitcoin Cash address`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Error validating Bitcoin Cash address: ${error.message}` }],
}
}
}
)
// Network Info
server.tool(
"getBCHNetworkInfo",
"Get current Bitcoin Cash network information",
{},
async () => {
try {
const fees = await bitcoinCashClient.getFeeRates()
return {
content: [
{
type: "text",
text: `Bitcoin Cash Network Information:
Current Network: ${bitcoinCashClient.getNetwork()}
Fee Rates (sats/byte):
Fast: ${fees.fast}
Average: ${fees.average}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve Bitcoin Cash network information: ${error.message}` }],
}
}
}
)
}

View File

@@ -1,657 +0,0 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import fetch from 'cross-fetch';
import { Lucid, Blockfrost, fromText } from 'lucid-cardano';
import * as dotenv from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
// Type definitions
export interface CardanoToken {
unit: string;
name: string;
quantity: string;
}
export interface CardanoWalletInfo {
address: string;
utxoCount: number;
ada: string;
tokens: CardanoToken[];
}
export interface CardanoAdaTransactionResult {
txHash: string;
senderAddress: string;
recipientAddress: string;
amount: number;
links: {
explorer: string;
};
}
export interface CardanoTokenTransactionResult {
txHash: string;
senderAddress: string;
recipientAddress: string;
token: {
policyId: string;
name: string;
amount: string;
};
ada: string;
links: {
explorer: string;
};
}
// Direct .env loading for debugging
const __filename = fileURLToPath(import.meta.url);
const projectRoot = resolve(__filename, '../../../..');
const envPath = resolve(projectRoot, '.env');
// Load env file directly
if (existsSync(envPath)) {
const result = dotenv.config({ path: envPath });
if (result.error) {
console.error('Error loading .env file:', result.error);
}
}
// Configuration from environment variables
const BLOCKFROST_API_KEY = process.env.BLOCKFROST_API_KEY || '';
const CARDANO_NETWORK = process.env.CARDANO_NETWORK || 'mainnet';
const BLOCKFROST_BASE_URL = `https://cardano-${CARDANO_NETWORK}.blockfrost.io/api/v0`;
const CARDANO_MNEMONIC = process.env.CARDANO_MNEMONIC || '';
const CARDANO_ACCOUNT_INDEX = parseInt(process.env.CARDANO_ACCOUNT_INDEX || '0');
// Helper function to make Blockfrost API requests
async function blockfrostRequest<T>(endpoint: string): Promise<T> {
const url = `${BLOCKFROST_BASE_URL}${endpoint}`;
try {
const headers = {
'project_id': BLOCKFROST_API_KEY
};
const response = await fetch(url, {
method: 'GET',
headers: headers
});
if (!response.ok) {
let errorText = await response.text();
throw new Error(`API error ${response.status}: ${errorText}`);
}
return await response.json() as T;
} catch (error) {
throw error;
}
}
// Helper function to format ADA amounts
function lovelaceToAda(lovelace: string | number): string {
return (parseInt(String(lovelace)) / 1000000).toFixed(6);
}
// Helper function to format asset name
function formatAssetName(name: string): string {
try {
// If the name is in hex, try to convert it to ASCII
if (/^[0-9a-fA-F]+$/.test(name)) {
return Buffer.from(name, 'hex').toString('utf8');
}
return name;
} catch (e) {
return name; // Return original if conversion fails
}
}
// Initialize Lucid instance
async function initLucid() {
// Map network name to Lucid network name
let network: 'Mainnet' | 'Preprod' | 'Preview';
if (CARDANO_NETWORK === 'mainnet') {
network = 'Mainnet';
} else if (['testnet', 'preprod'].includes(CARDANO_NETWORK)) {
network = 'Preprod';
} else if (CARDANO_NETWORK === 'preview') {
network = 'Preview';
} else {
network = 'Mainnet';
}
try {
// Check for required configurations
if (!BLOCKFROST_API_KEY) {
throw new Error('BLOCKFROST_API_KEY is required in .env file');
}
if (!CARDANO_MNEMONIC) {
throw new Error('CARDANO_MNEMONIC is required in .env file');
}
// Create Lucid instance
const provider = new Blockfrost(
`https://cardano-${CARDANO_NETWORK}.blockfrost.io/api/v0`,
BLOCKFROST_API_KEY
);
const lucid = await Lucid.new(provider, network);
// Load wallet from mnemonic
try {
// Trim mnemonic and check for valid words
const trimmedMnemonic = CARDANO_MNEMONIC.trim();
const words = trimmedMnemonic.split(/\s+/);
// Check if word count is valid (should be 15 or 24 for Cardano)
if (words.length !== 15 && words.length !== 24) {
throw new Error(`Invalid mnemonic: Expected 15 or 24 words, got ${words.length}`);
}
// Select wallet from seed
lucid.selectWalletFromSeed(trimmedMnemonic, { accountIndex: CARDANO_ACCOUNT_INDEX });
// Verify wallet was loaded correctly by checking address
const address = await lucid.wallet.address();
if (!address) {
throw new Error('Failed to derive address from mnemonic');
}
return lucid;
} catch (error) {
throw new Error(`Failed to load wallet: ${error instanceof Error ? error.message : String(error)}`);
}
} catch (error) {
throw new Error(`Failed to initialize Lucid: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Get wallet info
export async function getWalletInfo(): Promise<CardanoWalletInfo> {
try {
// Initialize Lucid with wallet
const lucid = await initLucid();
// Get wallet address
const address = await lucid.wallet.address();
// Get UTXOs
const utxos = await lucid.wallet.getUtxos();
// Calculate balance
let adaBalance = '0';
let tokenBalances: CardanoToken[] = [];
if (utxos.length > 0) {
// Combine all UTXOs to get total balance
const value = utxos.reduce(
(acc, utxo) => acc.add(utxo.assets),
// @ts-ignore: Lucid types are not fully compatible
lucid.newValue()
).assets;
// Extract ADA balance
adaBalance = value.lovelace ? lovelaceToAda(value.lovelace) : '0';
// Extract token balances
for (const [unit, quantity] of Object.entries(value)) {
if (unit === 'lovelace') continue;
try {
// Try to get a readable name for the token
// @ts-ignore: Lucid types are not fully compatible
const { policyId, assetName } = lucid.utils.fromUnit(unit);
const displayName = assetName ?
// @ts-ignore: Lucid types are not fully compatible
(lucid.utils.toText(assetName) || formatAssetName(assetName)) :
`${policyId.substring(0, 8)}...`;
tokenBalances.push({
unit,
name: displayName,
quantity: (quantity as bigint).toString()
});
} catch (e) {
tokenBalances.push({
unit,
name: unit,
quantity: (quantity as bigint).toString()
});
}
}
}
return {
address,
utxoCount: utxos.length,
ada: adaBalance,
tokens: tokenBalances
};
} catch (error) {
throw error;
}
}
// Send ADA transaction
export async function sendAda(
recipientAddress: string,
amountAda: number,
metadata: any = null
): Promise<CardanoAdaTransactionResult> {
try {
// Validate input
if (!recipientAddress) {
throw new Error('Recipient address is required');
}
if (typeof amountAda !== 'number' || amountAda <= 0) {
throw new Error('Amount must be a positive number');
}
// Initialize Lucid with wallet
const lucid = await initLucid();
// Get sender address for return value
const senderAddress = await lucid.wallet.address();
// Validate recipient address
try {
// @ts-ignore: Lucid types are not fully compatible
lucid.utils.getAddressDetails(recipientAddress);
} catch (error) {
throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`);
}
// Convert ADA to lovelace
const lovelaceAmount = BigInt(Math.floor(amountAda * 1000000));
// Build transaction
// @ts-ignore: Lucid types are not fully compatible
let tx = lucid.newTx()
.payToAddress(recipientAddress, { lovelace: lovelaceAmount });
// Add metadata if provided
if (metadata) {
const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata;
// @ts-ignore: Lucid types are not fully compatible
tx = tx.attachMetadata(674, parsedMetadata);
}
// Complete the transaction (adds inputs and change output)
// @ts-ignore: Lucid types are not fully compatible
tx = await tx.complete();
// Sign the transaction
// @ts-ignore: Lucid types are not fully compatible
const signedTx = await tx.sign().complete();
// Submit the transaction
const txHash = await signedTx.submit();
return {
txHash,
senderAddress,
recipientAddress,
amount: amountAda,
links: {
explorer: `https://cardanoscan.io/transaction/${txHash}`
}
};
} catch (error) {
throw error;
}
}
// Send tokens transaction
export async function sendTokens(
recipientAddress: string,
policyId: string,
assetName: string,
amount: string,
adaAmount: number | null = null
): Promise<CardanoTokenTransactionResult> {
try {
// Validate input
if (!recipientAddress) {
throw new Error('Recipient address is required');
}
if (!policyId) {
throw new Error('Policy ID is required');
}
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
throw new Error('Amount must be a positive number');
}
// Initialize Lucid with wallet
const lucid = await initLucid();
// Get sender address for return value
const senderAddress = await lucid.wallet.address();
// Validate recipient address
try {
// @ts-ignore: Lucid types are not fully compatible
lucid.utils.getAddressDetails(recipientAddress);
} catch (error) {
throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`);
}
// Convert asset name to hex if it's in readable format
let assetNameHex = assetName;
if (assetName && !/^[0-9a-fA-F]+$/.test(assetName)) {
assetNameHex = fromText(assetName);
}
// Create the unit identifier
const unit = `${policyId}${assetNameHex}`;
// Create the assets object
const assets = { [unit]: BigInt(amount) };
// Calculate minimum required ADA
// @ts-ignore: Lucid types are not fully compatible
const minLovelace = await lucid.utils.minAdaRequired({
outputs: [{ address: recipientAddress, assets }]
});
// Use provided ADA amount or minimum required
const outputLovelace = adaAmount
? BigInt(Math.floor(adaAmount * 1000000))
: minLovelace;
// Check if we're sending at least the minimum required
if (outputLovelace < minLovelace) {
throw new Error(`Minimum ${lovelaceToAda(minLovelace)} ADA required to send these tokens`);
}
// Build transaction
// @ts-ignore: Lucid types are not fully compatible
let tx = lucid.newTx()
.payToAddress(recipientAddress, {
lovelace: outputLovelace,
...assets
});
// Complete the transaction (adds inputs and change output)
// @ts-ignore: Lucid types are not fully compatible
tx = await tx.complete();
// Sign the transaction
// @ts-ignore: Lucid types are not fully compatible
const signedTx = await tx.sign().complete();
// Submit the transaction
const txHash = await signedTx.submit();
// Format asset name for display
const displayAssetName = assetName ? formatAssetName(assetNameHex) : '';
return {
txHash,
senderAddress,
recipientAddress,
token: {
policyId,
name: displayAssetName,
amount
},
ada: lovelaceToAda(outputLovelace),
links: {
explorer: `https://cardanoscan.io/transaction/${txHash}`
}
};
} catch (error) {
throw error;
}
}
// Interface for Cardano amount
interface CardanoAmount {
unit: string;
quantity: string;
}
// Interface for address info
interface AddressInfo {
address: string;
amount: string;
stake_address: string | null;
type: string;
script: boolean;
}
export function registerCardanoTools(server: McpServer) {
// Get Address Balance
server.tool(
"getCardanoAddressBalance",
"Get balance and token holdings for a Cardano address",
{
address: z.string().describe("Cardano address to check"),
},
async ({ address }) => {
try {
// Get address info
const addressInfo = await blockfrostRequest<AddressInfo>(`/addresses/${address}`);
// Get address UTXOs to get token info
interface CardanoUtxo {
tx_hash: string;
tx_index: number;
output_index: number;
amount: CardanoAmount[];
block: string;
}
const utxos = await blockfrostRequest<CardanoUtxo[]>(`/addresses/${address}/utxos`);
// Extract all tokens from UTXOs
const tokenMap = new Map<string, bigint>();
utxos.forEach(utxo => {
utxo.amount.forEach(asset => {
const currentAmount = tokenMap.get(asset.unit) || BigInt(0);
tokenMap.set(asset.unit, currentAmount + BigInt(asset.quantity));
});
});
// Get ADA balance
const adaBalance = tokenMap.get('lovelace') || BigInt(addressInfo.amount);
// Remove lovelace from token map for separate display
tokenMap.delete('lovelace');
// Convert token map to array for sorting and formatting
const tokens = Array.from(tokenMap.entries()).map(([unit, quantity]) => {
// Split into policy ID and asset name
const policyId = unit.slice(0, 56);
const assetName = unit.slice(56);
const formattedName = formatAssetName(assetName);
return {
unit,
policyId,
assetName: formattedName,
quantity: quantity.toString()
};
});
// Sort tokens by quantity (descending)
tokens.sort((a, b) => (BigInt(b.quantity) - BigInt(a.quantity)) > 0n ? 1 : -1);
// Format token list for display
const tokenList = tokens.length > 0
? tokens.map(token =>
`${token.quantity} ${token.assetName || token.unit} (Policy: ${token.policyId.substring(0, 8)}...)`
).join('\n')
: 'No tokens found';
// Build response
return {
content: [
{
type: "text",
text: `Cardano Address Balance for ${address}:
ADA Balance: ${lovelaceToAda(adaBalance.toString())} ADA
Stake Address: ${addressInfo.stake_address || 'Not staked'}
Address Type: ${addressInfo.script ? 'Script' : 'Key-based'}
${tokens.length > 0 ? `Token Holdings (${tokens.length}):\n${tokenList}` : 'No token holdings found'}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{
type: "text",
text: `Failed to retrieve Cardano address balance: ${error.message}`
}],
}
}
}
)
// Get Cardano Wallet Info
server.tool(
"getCardanoWalletInfo",
"Get the current wallet information, including balance and tokens",
{},
async () => {
try {
// Get wallet info
const walletInfo = await getWalletInfo();
// Format token list
const tokenList = walletInfo.tokens.length > 0
? walletInfo.tokens.map((token: CardanoToken) => `${token.quantity} ${token.name}`).join('\n')
: 'No tokens found';
return {
content: [
{
type: "text",
text: `# Cardano Wallet Information
Address: ${walletInfo.address}
ADA Balance: ${walletInfo.ada} ADA
UTXO Count: ${walletInfo.utxoCount}
${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length}):\n${tokenList}` : 'No token holdings found'}`
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get wallet information: ${error.message}\n\n**Troubleshooting Tips:**\n1. Make sure you have a valid 15 or 24-word Cardano mnemonic in your .env file\n2. Verify your Blockfrost API key is correct and has sufficient access rights\n3. Check the console logs for detailed error information`
},
],
};
}
}
)
// Send ADA Transaction
server.tool(
"sendCardanoAda",
"Send ADA from your wallet to a recipient address",
{
recipientAddress: z.string().describe("Recipient Cardano address"),
amount: z.number().min(1).describe("Amount of ADA to send"),
metadata: z.optional(z.string()).describe("Optional transaction metadata in JSON format")
},
async ({ recipientAddress, amount, metadata }) => {
try {
// Call sendAda
const result = await sendAda(recipientAddress, amount, metadata);
return {
content: [
{
type: "text",
text: `# ADA Transaction Successful
Transaction Hash: ${result.txHash}
From: ${result.senderAddress}
To: ${result.recipientAddress}
Amount: ${result.amount} ADA
[View on Explorer](${result.links.explorer})`
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to send ADA: ${error.message}\n\n**Troubleshooting Tips:**\n1. Make sure you have a valid 15 or 24-word Cardano mnemonic in your .env file\n2. Verify your Blockfrost API key is correct and has sufficient access rights\n3. Check that your wallet has sufficient balance\n4. Verify the recipient address is correct\n5. Check the console logs for detailed error information`
},
],
};
}
}
)
// Send Tokens Transaction
server.tool(
"sendCardanoTokens",
"Send Cardano native tokens from your wallet to a recipient address",
{
recipientAddress: z.string().describe("Recipient Cardano address"),
policyId: z.string().describe("Token policy ID"),
assetName: z.string().describe("Asset name (can be empty for policy-only tokens)"),
amount: z.string().describe("Amount of tokens to send"),
adaAmount: z.optional(z.number()).describe("Optional amount of ADA to send with tokens (will use minimum required if not specified)")
},
async ({ recipientAddress, policyId, assetName, amount, adaAmount }) => {
try {
// Call sendTokens
const result = await sendTokens(recipientAddress, policyId, assetName, amount, adaAmount);
return {
content: [
{
type: "text",
text: `# Token Transaction Successful
Transaction Hash: ${result.txHash}
From: ${result.senderAddress}
To: ${result.recipientAddress}
Token Details:
- Policy ID: ${result.token.policyId}
- Asset Name: ${result.token.name || '(none)'}
- Amount: ${result.token.amount}
Included ADA: ${result.ada} ADA
[View on Explorer](${result.links.explorer})`
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to send tokens: ${error.message}\n\n**Troubleshooting Tips:**\n1. Make sure you have a valid 15 or 24-word Cardano mnemonic in your .env file\n2. Verify your Blockfrost API key is correct and has sufficient access rights\n3. Check that your wallet has sufficient ADA balance for the transaction\n4. Verify the policy ID and asset name are correct\n5. Verify the recipient address is correct\n6. Check the console logs for detailed error information`
},
],
};
}
}
)
}

View File

@@ -1,144 +0,0 @@
import { Client as DogeClient, defaultDogeParams } from '@xchainjs/xchain-doge'
import { Network } from '@xchainjs/xchain-client'
import { z } from "zod"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
// Initialize client with mainnet by default
const dogeClient = new DogeClient({
...defaultDogeParams,
network: Network.Mainnet,
})
// Helper function to format base amounts
function formatBaseAmount(baseAmount: any): string {
return baseAmount.amount().toString()
}
export function registerDogecoinTools(server: McpServer) {
// Dogecoin Balance
server.tool(
"getDogeBalance",
"Get balance for a Dogecoin address",
{
address: z.string().describe("Dogecoin address to check"),
},
async ({ address }) => {
try {
const balances = await dogeClient.getBalance(address)
return {
content: [
{
type: "text",
text: `Dogecoin Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} DOGE`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve DOGE balance: ${error.message}` }],
}
}
}
)
// Transaction History
server.tool(
"getDOGETransactionHistory",
"Get transaction history for a Dogecoin address",
{
address: z.string().describe("Dogecoin address to check"),
limit: z.number().optional().describe("Maximum number of transactions to return"),
offset: z.number().optional().describe("Number of transactions to skip"),
},
async ({ address, limit = 10, offset = 0 }) => {
try {
const txs = await dogeClient.getTransactions({ address, limit, offset })
const txList = txs.txs.map((tx: any) => {
const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} DOGE` : 'Unknown'
const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} DOGE` : 'Unknown'
return `
Transaction: ${tx.hash}
Type: ${tx.type}
From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount})
To: ${tx.to[0]?.to || 'Unknown'} (${toAmount})
Asset: ${tx.asset.ticker}
Date: ${new Date(tx.date).toLocaleString()}`
}).join('\n---\n')
return {
content: [
{
type: "text",
text: `Dogecoin Transaction History for ${address}:\n${txList}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve DOGE transaction history: ${error.message}` }],
}
}
}
)
// Address Validation
server.tool(
"validateDOGEAddress",
"Validate a Dogecoin address format",
{
address: z.string().describe("Dogecoin address to validate"),
},
async ({ address }) => {
try {
const isValid = dogeClient.validateAddress(address)
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} is a valid Dogecoin address`
: `The address ${address} is NOT a valid Dogecoin address`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Error validating Dogecoin address: ${error.message}` }],
}
}
}
)
// Network Info
server.tool(
"getDOGENetworkInfo",
"Get current Dogecoin network information",
{},
async () => {
try {
const fees = await dogeClient.getFeeRates()
return {
content: [
{
type: "text",
text: `Dogecoin Network Information:
Current Network: ${dogeClient.getNetwork()}
Fee Rates (sats/byte):
Fast: ${fees.fast}
Average: ${fees.average}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve Dogecoin network information: ${error.message}` }],
}
}
}
)
}

View File

@@ -1,8 +0,0 @@
export { registerBitcoinTools } from './bitcoin.js';
export { registerLitecoinTools } from './litecoin.js';
export { registerDogecoinTools } from './dogecoin.js';
export { registerBitcoinCashTools } from './bitcoincash.js';
export { registerCardanoTools, getWalletInfo, sendAda, sendTokens } from './cardano.js';
// Export Cardano types
export type { CardanoWalletInfo, CardanoToken, CardanoAdaTransactionResult, CardanoTokenTransactionResult } from './cardano.js';

View File

@@ -1,144 +0,0 @@
import { Client as LitecoinClient, defaultLtcParams } from '@xchainjs/xchain-litecoin'
import { Network } from '@xchainjs/xchain-client'
import { z } from "zod"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
// Initialize client with mainnet by default
const litecoinClient = new LitecoinClient({
...defaultLtcParams,
network: Network.Mainnet,
})
// Helper function to format base amounts
function formatBaseAmount(baseAmount: any): string {
return baseAmount.amount().toString()
}
export function registerLitecoinTools(server: McpServer) {
// Litecoin Balance
server.tool(
"getLitecoinBalance",
"Get balance for a Litecoin address",
{
address: z.string().describe("Litecoin address to check"),
},
async ({ address }) => {
try {
const balances = await litecoinClient.getBalance(address)
return {
content: [
{
type: "text",
text: `Litecoin Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} LTC`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve LTC balance: ${error.message}` }],
}
}
}
)
// Transaction History
server.tool(
"getLTCTransactionHistory",
"Get transaction history for a Litecoin address",
{
address: z.string().describe("Litecoin address to check"),
limit: z.number().optional().describe("Maximum number of transactions to return"),
offset: z.number().optional().describe("Number of transactions to skip"),
},
async ({ address, limit = 10, offset = 0 }) => {
try {
const txs = await litecoinClient.getTransactions({ address, limit, offset })
const txList = txs.txs.map((tx: any) => {
const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} LTC` : 'Unknown'
const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} LTC` : 'Unknown'
return `
Transaction: ${tx.hash}
Type: ${tx.type}
From: ${tx.from[0]?.from || 'Unknown'} (${fromAmount})
To: ${tx.to[0]?.to || 'Unknown'} (${toAmount})
Asset: ${tx.asset.ticker}
Date: ${new Date(tx.date).toLocaleString()}`
}).join('\n---\n')
return {
content: [
{
type: "text",
text: `Litecoin Transaction History for ${address}:\n${txList}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve LTC transaction history: ${error.message}` }],
}
}
}
)
// Address Validation
server.tool(
"validateLTCAddress",
"Validate a Litecoin address format",
{
address: z.string().describe("Litecoin address to validate"),
},
async ({ address }) => {
try {
const isValid = litecoinClient.validateAddress(address)
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} is a valid Litecoin address`
: `The address ${address} is NOT a valid Litecoin address`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Error validating Litecoin address: ${error.message}` }],
}
}
}
)
// Network Info
server.tool(
"getLTCNetworkInfo",
"Get current Litecoin network information",
{},
async () => {
try {
const fees = await litecoinClient.getFeeRates()
return {
content: [
{
type: "text",
text: `Litecoin Network Information:
Current Network: ${litecoinClient.getNetwork()}
Fee Rates (sats/byte):
Fast: ${fees.fast}
Average: ${fees.average}`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve Litecoin network information: ${error.message}` }],
}
}
}
)
}

View File

@@ -1,481 +0,0 @@
import { JsonRpcProvider, formatEther, formatUnits, Contract, Wallet, parseUnits, MaxUint256 } from 'ethers';
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Network configurations
interface NetworkConfig {
name: string;
rpc: string;
chainId: number;
currencySymbol: string;
explorer: string;
}
const NETWORKS: { [key: string]: NetworkConfig } = {
ethereum: {
name: "Ethereum",
rpc: process.env.ETH_RPC_URL || "https://eth-mainnet.g.alchemy.com/v2/demo",
chainId: 1,
currencySymbol: "ETH",
explorer: "https://etherscan.io"
},
base: {
name: "Base",
rpc: process.env.BASE_RPC_URL || "https://mainnet.base.org",
chainId: 8453,
currencySymbol: "ETH",
explorer: "https://basescan.org"
},
arbitrum: {
name: "Arbitrum",
rpc: process.env.ARBITRUM_RPC_URL || "https://arb1.arbitrum.io/rpc",
chainId: 42161,
currencySymbol: "ETH",
explorer: "https://arbiscan.io"
},
optimism: {
name: "Optimism",
rpc: process.env.OPTIMISM_RPC_URL || "https://mainnet.optimism.io",
chainId: 10,
currencySymbol: "ETH",
explorer: "https://optimistic.etherscan.io"
},
bsc: {
name: "BNB Smart Chain",
rpc: process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org",
chainId: 56,
currencySymbol: "BNB",
explorer: "https://bscscan.com"
},
polygon: {
name: "Polygon",
rpc: process.env.POLYGON_RPC_URL || "https://polygon-rpc.com",
chainId: 137,
currencySymbol: "MATIC",
explorer: "https://polygonscan.com"
},
avalanche: {
name: "Avalanche",
rpc: process.env.AVALANCHE_RPC_URL || "https://api.avax.network/ext/bc/C/rpc",
chainId: 43114,
currencySymbol: "AVAX",
explorer: "https://snowtrace.io"
},
berachain: {
name: "Berachain",
rpc: process.env.BERACHAIN_RPC_URL || "https://rpc.berachain.com",
chainId: 80094,
currencySymbol: "BERA",
explorer: "https://berascan.com"
},
sonic: {
name: "Sonic",
rpc: process.env.SONIC_RPC_URL || "https://rpc.soniclabs.com/",
chainId: 2024,
currencySymbol: "SONIC",
explorer: "https://explorer.sonic.ooo"
}
};
// Initialize providers for each network
const providers: { [key: string]: JsonRpcProvider } = {};
for (const [network, config] of Object.entries(NETWORKS)) {
providers[network] = new JsonRpcProvider(config.rpc);
}
// ERC-20 minimal ABI
const ERC20_ABI = [
"function balanceOf(address owner) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function transfer(address to, uint256 amount) returns (bool)",
"function approve(address spender, uint256 amount) returns (bool)"
];
export function registerEvmTools(server: McpServer) {
// Get native token balance for any EVM network
server.tool(
"getEvmBalance",
"Get native token balance for an EVM address on any supported network",
{
address: z.string().describe("EVM account address"),
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"),
},
async ({ address, network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`,
},
],
};
}
const provider = providers[network];
const balance = await provider.getBalance(address);
const formattedBalance = formatEther(balance);
const networkConfig = NETWORKS[network];
return {
content: [
{
type: "text",
text: `Balance on ${networkConfig.name}:\n${formattedBalance} ${networkConfig.currencySymbol}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve balance: ${error.message}`,
},
],
};
}
}
);
// Get ERC-20 token balance for any EVM network
server.tool(
"getEvmTokenBalance",
"Get ERC-20 token balance for an address on any supported EVM network",
{
address: z.string().describe("EVM account address"),
tokenAddress: z.string().describe("ERC-20 token contract address"),
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"),
},
async ({ address, tokenAddress, network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`,
},
],
};
}
const provider = providers[network];
const contract = new Contract(tokenAddress, ERC20_ABI, provider);
const [balance, decimals, symbol] = await Promise.all([
contract.balanceOf(address),
contract.decimals(),
contract.symbol()
]);
const formattedBalance = formatUnits(balance, decimals);
const networkConfig = NETWORKS[network];
return {
content: [
{
type: "text",
text: `Token Balance on ${networkConfig.name}:\n${formattedBalance} ${symbol} (${tokenAddress})\nExplorer: ${networkConfig.explorer}/token/${tokenAddress}`
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token balance: ${error.message}`,
},
],
};
}
}
);
// Get gas price for any EVM network
server.tool(
"getGasPrice",
"Get current gas price for any supported EVM network",
{
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"),
},
async ({ network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`,
},
],
};
}
const provider = providers[network];
const feeData = await provider.getFeeData();
const gasPrice = formatUnits(feeData.gasPrice || 0, 'gwei');
const maxFeePerGas = feeData.maxFeePerGas ? formatUnits(feeData.maxFeePerGas, 'gwei') : null;
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : null;
const networkConfig = NETWORKS[network];
let response = `Gas Prices on ${networkConfig.name}:\nGas Price: ${gasPrice} Gwei`;
if (maxFeePerGas) {
response += `\nMax Fee: ${maxFeePerGas} Gwei`;
}
if (maxPriorityFeePerGas) {
response += `\nMax Priority Fee: ${maxPriorityFeePerGas} Gwei`;
}
return {
content: [
{
type: "text",
text: response,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve gas price: ${error.message}`,
},
],
};
}
}
);
// Send native tokens on any EVM network
server.tool(
"sendEvmTransaction",
"Send native tokens on any supported EVM network (using private key from .env)",
{
toAddress: z.string().describe("Recipient's address"),
amount: z.string().describe("Amount to send (in native tokens)"),
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"),
},
async ({ toAddress, amount, network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`
}
]
};
}
const provider = providers[network];
const networkConfig = NETWORKS[network];
// Get private key from environment variables
if (!process.env.ETH_PRIVATE_KEY) {
throw new Error('ETH_PRIVATE_KEY not found in environment variables');
}
// Create wallet from private key
const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider);
const fromAddress = wallet.address;
// Get current gas price and nonce
const [gasPrice, nonce] = await Promise.all([
provider.getFeeData(),
provider.getTransactionCount(fromAddress)
]);
// Prepare transaction
const tx = {
to: toAddress,
value: parseUnits(amount),
nonce: nonce,
maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
maxFeePerGas: gasPrice.maxFeePerGas,
gasLimit: 21000, // Standard ETH transfer
chainId: networkConfig.chainId
};
// Sign and send transaction
const txResponse = await wallet.sendTransaction(tx);
return {
content: [
{
type: "text",
text: `Transaction sent on ${networkConfig.name}!\nFrom: ${fromAddress}\nTo: ${toAddress}\nAmount: ${amount} ${networkConfig.currencySymbol}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}`
}
]
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to send transaction: ${error.message}`
}
]
};
}
}
);
// Send ERC-20 tokens on any EVM network
server.tool(
"sendEvmToken",
"Send ERC-20 tokens on any supported EVM network (using private key from .env)",
{
toAddress: z.string().describe("Recipient's address"),
tokenAddress: z.string().describe("Token contract address"),
amount: z.string().describe("Amount to send (in token units)"),
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain)"),
},
async ({ toAddress, tokenAddress, amount, network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`
}
]
};
}
const provider = providers[network];
const networkConfig = NETWORKS[network];
// Get private key from environment variables
if (!process.env.ETH_PRIVATE_KEY) {
throw new Error('ETH_PRIVATE_KEY not found in environment variables');
}
// Create wallet from private key
const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider);
const fromAddress = wallet.address;
// Create contract instance
const contract = new Contract(tokenAddress, ERC20_ABI, wallet);
// Get token details
const [decimals, symbol] = await Promise.all([
contract.decimals(),
contract.symbol()
]);
// Convert amount to token units
const amountInTokenUnits = parseUnits(amount, decimals);
// Send transaction
const txResponse = await contract.transfer(toAddress, amountInTokenUnits, {
gasLimit: 100000 // Estimated gas limit for token transfers
});
return {
content: [
{
type: "text",
text: `Token transfer sent on ${networkConfig.name}!\nFrom: ${fromAddress}\nTo: ${toAddress}\nAmount: ${amount} ${symbol}\nToken Address: ${tokenAddress}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}`
}
]
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to send token transfer: ${error.message}`
}
]
};
}
}
);
// Approve ERC-20 token spending
server.tool(
"approveEvmToken",
"Approve ERC-20 token spending on any supported EVM network (using private key from .env)",
{
spenderAddress: z.string().describe("Address to approve for spending"),
tokenAddress: z.string().describe("Token contract address"),
amount: z.string().optional().describe("Amount to approve (in token units, defaults to unlimited)"),
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain)"),
},
async ({ spenderAddress, tokenAddress, amount, network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`
}
]
};
}
const provider = providers[network];
const networkConfig = NETWORKS[network];
// Get private key from environment variables
if (!process.env.ETH_PRIVATE_KEY) {
throw new Error('ETH_PRIVATE_KEY not found in environment variables');
}
// Create wallet from private key
const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider);
// Create contract instance
const contract = new Contract(tokenAddress, ERC20_ABI, wallet);
// Get token details
const [decimals, symbol] = await Promise.all([
contract.decimals(),
contract.symbol()
]);
// Calculate approval amount
const approvalAmount = amount ? parseUnits(amount, decimals) : MaxUint256;
// Send approval transaction
const txResponse = await contract.approve(spenderAddress, approvalAmount, {
gasLimit: 60000 // Estimated gas limit for approvals
});
const formattedAmount = amount || "unlimited";
return {
content: [
{
type: "text",
text: `Token approval sent on ${networkConfig.name}!\nToken: ${symbol} (${tokenAddress})\nSpender: ${spenderAddress}\nAmount: ${formattedAmount}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}`
}
]
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to approve token spending: ${error.message}`
}
]
};
}
}
);
}

View File

@@ -1,26 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { registerBitcoinTools } from "./UTXO/bitcoin"
import { registerLitecoinTools } from "./UTXO/litecoin"
import { registerDogecoinTools } from "./UTXO/dogecoin"
import { registerBitcoinCashTools } from "./UTXO/bitcoincash"
import { registerCardanoTools } from "./UTXO/cardano"
import { registerThorchainTools } from "./thorchain/thorchain"
import { registerRippleTools } from "./ripple/ripple"
import { registerTonTools } from "./ton/ton"
// Export all UTXO tools registration function
export function registerUtxoTools(server: McpServer) {
registerBitcoinTools(server)
registerLitecoinTools(server)
registerDogecoinTools(server)
registerBitcoinCashTools(server)
registerCardanoTools(server)
}
// Export all tools registration function
export function registerAllTools(server: McpServer) {
registerUtxoTools(server)
registerThorchainTools(server)
registerRippleTools(server)
registerTonTools(server)
}

View File

@@ -1 +0,0 @@
export { registerRippleTools } from './ripple.js';

View File

@@ -1,505 +0,0 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client, Wallet, Payment, TrustSet } from 'xrpl';
// Initialize XRP Ledger client
let xrpClient: Client | null = null;
// XRP Network URL
const XRP_NETWORK_URL = process.env.XRP_RPC_URL || 'wss://s1.ripple.com';
const XRP_EXPLORER = 'https://livenet.xrpl.org';
// Helper function to get or initialize the client
async function getClient(): Promise<Client> {
if (!xrpClient || !xrpClient.isConnected()) {
xrpClient = new Client(XRP_NETWORK_URL);
await xrpClient.connect();
}
return xrpClient;
}
// Helper function to format drops to XRP
function dropsToXrp(drops: string): string {
return (parseInt(drops) / 1000000).toFixed(6);
}
// Helper function to format XRP to drops
function xrpToDrops(xrp: string | number): string {
// Ensure xrp is a string
const xrpStr = typeof xrp === 'number' ? xrp.toString() : xrp;
// Use a string to preserve precision and avoid floating point issues
const drops = Math.floor(parseFloat(xrpStr) * 1000000).toString();
return drops;
}
// Helper function to clean up resources
async function cleanUp() {
if (xrpClient && xrpClient.isConnected()) {
await xrpClient.disconnect();
}
}
// Helper function to create a wallet from available credentials
async function createWallet() {
// Try with private key first if available
if (process.env.XRP_PRIVATE_KEY) {
try {
console.log("Creating wallet from private key...");
return Wallet.fromSeed(process.env.XRP_PRIVATE_KEY);
} catch (err) {
console.log("Failed to create wallet from private key:", err);
}
}
// Try with mnemonic if private key failed or isn't available
if (process.env.XRP_MNEMONIC) {
try {
console.log("Creating wallet from mnemonic...");
return Wallet.fromMnemonic(process.env.XRP_MNEMONIC);
} catch (err) {
console.log("Failed to create wallet from mnemonic:", err);
}
}
// If we have the address and we get here, throw an error
if (process.env.XRP_ADDRESS) {
throw new Error('Could not create wallet from available credentials');
}
throw new Error('No wallet credentials provided. Please add XRP_PRIVATE_KEY or XRP_MNEMONIC to your .env file');
}
// Register all XRP tools
export function registerRippleTools(server: McpServer) {
// Get XRP Balance
server.tool(
"getXrpBalance",
"Get balance for an XRP address",
{
address: z.string().describe("XRP address to check"),
},
async ({ address }) => {
try {
const client = await getClient();
const response = await client.request({
command: 'account_info',
account: address,
ledger_index: 'validated'
});
const balance = dropsToXrp(response.result.account_data.Balance);
return {
content: [
{
type: "text",
text: `XRP Balance for ${address}:\n${balance} XRP`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve XRP balance: ${error.message}` }],
};
}
}
);
// Get account transactions
server.tool(
"getXrpTransactionHistory",
"Get transaction history for an XRP address",
{
address: z.string().describe("XRP address to check"),
limit: z.number().optional().describe("Maximum number of transactions to return (default: 10)"),
},
async ({ address, limit = 10 }) => {
try {
const client = await getClient();
const response = await client.request({
command: 'account_tx',
account: address,
limit: limit
});
if (!response.result.transactions || response.result.transactions.length === 0) {
return {
content: [{ type: "text", text: `No transactions found for ${address}` }],
};
}
const txList = response.result.transactions.map((tx: any) => {
const transaction = tx.tx;
let txInfo = `
Transaction: ${transaction.hash}
Type: ${transaction.TransactionType}
Date: ${new Date(transaction.date ? (transaction.date + 946684800) * 1000 : 0).toLocaleString()}`;
if (transaction.TransactionType === 'Payment') {
txInfo += `
From: ${transaction.Account}
To: ${transaction.Destination}
Amount: ${transaction.Amount.currency ?
`${transaction.Amount.value} ${transaction.Amount.currency}` :
`${dropsToXrp(transaction.Amount)} XRP`}`;
}
return txInfo;
}).join('\n---\n');
return {
content: [
{
type: "text",
text: `XRP Transaction History for ${address}:\n${txList}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve XRP transaction history: ${error.message}` }],
};
}
}
);
// Validate XRP address
server.tool(
"validateXrpAddress",
"Validate an XRP address format",
{
address: z.string().describe("XRP address to validate"),
},
async ({ address }) => {
try {
// XRP addresses start with 'r' and are 25-35 characters in length
const isValid = /^r[a-zA-Z0-9]{24,34}$/.test(address);
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} has a valid XRP address format`
: `The address ${address} does NOT have a valid XRP address format`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Error validating XRP address: ${error.message}` }],
};
}
}
);
// Get XRP Ledger Info
server.tool(
"getXrpLedgerInfo",
"Get current XRP Ledger information",
{},
async () => {
try {
const client = await getClient();
const serverInfo = await client.request({
command: 'server_info'
});
const ledgerInfo = await client.request({
command: 'ledger',
ledger_index: 'validated'
});
// Extract values safely with null checks
const serverState = serverInfo.result.info.server_state || 'Unknown';
const ledgerIndex = ledgerInfo.result.ledger.ledger_index || 'Unknown';
const ledgerHash = ledgerInfo.result.ledger.ledger_hash || 'Unknown';
const closeTime = ledgerInfo.result.ledger.close_time
? new Date((ledgerInfo.result.ledger.close_time + 946684800) * 1000).toLocaleString()
: 'Unknown';
// Safe access to validated_ledger properties
const baseFee = serverInfo.result.info.validated_ledger?.base_fee_xrp || 'Unknown';
const reserveBase = serverInfo.result.info.validated_ledger?.reserve_base_xrp || 'Unknown';
const reserveInc = serverInfo.result.info.validated_ledger?.reserve_inc_xrp || 'Unknown';
return {
content: [
{
type: "text",
text: `XRP Ledger Information:
Server Status: ${serverState}
Current Ledger: ${ledgerIndex}
Ledger Hash: ${ledgerHash}
Close Time: ${closeTime}
Base Fee: ${baseFee} XRP
Reserve Base: ${reserveBase} XRP
Reserve Inc: ${reserveInc} XRP`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve XRP Ledger information: ${error.message}` }],
};
} finally {
// No need to clean up here as we want to keep the connection for other operations
}
}
);
// Send XRP transaction using private key or mnemonic
server.tool(
"sendXrpTransaction",
"Send XRP from your wallet to another address using private key from .env",
{
toAddress: z.string().describe("XRP address to send to"),
amount: z.union([z.string(), z.number()]).describe("Amount of XRP to send (string or number)"),
memo: z.string().optional().describe("Optional memo to include with the transaction"),
},
async ({ toAddress, amount, memo }: { toAddress: string, amount: string | number, memo?: string }) => {
try {
// Ensure amount is a string for display
const amountStr = typeof amount === 'number' ? amount.toString() : amount;
// Get client
const client = await getClient();
// Create wallet
let wallet;
try {
wallet = await createWallet();
} catch (walletError) {
console.error('Wallet creation error:', walletError);
throw new Error(`Failed to create wallet: ${(walletError as Error).message}`);
}
// Log debug info
console.log(`Wallet created. Address: ${wallet.address}, Public Key: ${wallet.publicKey}`);
// Verify wallet address matches expected address if provided
if (process.env.XRP_ADDRESS && wallet.address !== process.env.XRP_ADDRESS) {
console.log(`Warning: Derived wallet address ${wallet.address} does not match provided XRP_ADDRESS ${process.env.XRP_ADDRESS}`);
}
try {
// Get account info first to confirm the account exists
console.log(`Getting account info for ${wallet.address}...`);
const accountInfo = await client.request({
command: 'account_info',
account: wallet.address,
ledger_index: 'validated'
});
console.log(`Account exists with sequence: ${accountInfo.result.account_data.Sequence}`);
// Create a payment transaction
const payment: Payment = {
TransactionType: 'Payment',
Account: wallet.address,
Destination: toAddress,
// Convert amount to drops and ensure it's a string
Amount: xrpToDrops(amount)
};
// Add memo if provided
if (memo) {
payment.Memos = [{
Memo: {
MemoData: Buffer.from(memo, 'utf8').toString('hex').toUpperCase()
}
}];
}
// Prepare transaction with autofill to get all needed fields
console.log('Preparing transaction...');
const prepared = await client.autofill(payment);
console.log('Prepared transaction:', JSON.stringify(prepared));
// Use the wallet's sign method to create the signed tx_blob
console.log('Signing transaction...');
const signed = wallet.sign(prepared);
console.log('Signed transaction. tx_blob length:', signed.tx_blob.length);
console.log('Signed transaction hash:', signed.hash);
// First submit the transaction to get immediate feedback
console.log('Submitting transaction...');
const submitResult = await client.submit(signed.tx_blob);
console.log('Submit response:', JSON.stringify(submitResult));
// If the submit was successful, wait for validation
if (submitResult.result.engine_result === 'tesSUCCESS' || submitResult.result.engine_result.startsWith('tes')) {
// Wait for transaction to be validated
console.log(`Transaction submitted successfully with hash: ${signed.hash}. Waiting for validation...`);
// Use submitAndWait which will wait for validation
try {
const finalResult = await client.submitAndWait(signed.tx_blob);
console.log('Final transaction result:', JSON.stringify(finalResult));
return {
content: [
{
type: "text",
text: `XRP Transaction Sent!
From: ${wallet.address}
To: ${toAddress}
Amount: ${amountStr} XRP
Transaction Hash: ${signed.hash}
Explorer Link: ${XRP_EXPLORER}/transactions/${signed.hash}`,
},
],
};
} catch (waitError) {
// Even if we can't check the result, if submission was successful we return success
console.log('Could not verify transaction, but submission was successful:', waitError);
return {
content: [
{
type: "text",
text: `XRP Transaction Submitted!
From: ${wallet.address}
To: ${toAddress}
Amount: ${amountStr} XRP
Transaction Hash: ${signed.hash}
Explorer Link: ${XRP_EXPLORER}/transactions/${signed.hash}
Note: Transaction was submitted successfully but validation status is pending.`,
},
],
};
}
} else {
// If submission wasn't successful, throw an error with the engine result
throw new Error(`Transaction submission failed: ${submitResult.result.engine_result} - ${submitResult.result.engine_result_message}`);
}
} catch (txError) {
console.error('Transaction error details:', txError);
throw new Error(`Transaction error: ${(txError as Error).message}`);
}
} catch (err) {
const error = err as Error;
console.error('Error in sendXrpTransaction:', error);
return {
content: [{ type: "text", text: `Failed to send XRP transaction: ${error.message}` }],
};
}
}
);
// Check token balances on XRP Ledger
server.tool(
"getXrpTokenBalances",
"Get token balances for an XRP address",
{
address: z.string().describe("XRP address to check"),
},
async ({ address }) => {
try {
const client = await getClient();
const response = await client.request({
command: 'account_lines',
account: address
});
if (!response.result.lines || response.result.lines.length === 0) {
return {
content: [
{
type: "text",
text: `No token balances found for ${address}`,
},
],
};
}
const tokenBalances = response.result.lines.map((line: any) => {
return `${line.balance} ${line.currency} (Issuer: ${line.account})`;
}).join('\n');
return {
content: [
{
type: "text",
text: `Token Balances for ${address}:\n${tokenBalances}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve XRP token balances: ${error.message}` }],
};
}
}
);
// Create trustline for a token on XRP Ledger
server.tool(
"createXrpTrustline",
"Create a trustline for a token on the XRP Ledger using private key from .env",
{
currency: z.string().describe("Currency code (3-letter ISO code or hex string)"),
issuer: z.string().describe("Issuer's XRP address"),
limit: z.string().describe("Maximum amount of the token to trust"),
},
async ({ currency, issuer, limit }) => {
try {
// Get client
const client = await getClient();
// Create wallet
const wallet = await createWallet();
// Create a trustline transaction
const trustSetTx: TrustSet = {
TransactionType: 'TrustSet',
Account: wallet.address,
LimitAmount: {
currency,
issuer,
value: limit
}
};
try {
// Prepare and sign the transaction
const prepared = await client.autofill(trustSetTx);
const signed = wallet.sign(prepared);
// Submit the transaction
const result = await client.submitAndWait(signed.tx_blob);
return {
content: [
{
type: "text",
text: `XRP Trustline Created!
Account: ${wallet.address}
Currency: ${currency}
Issuer: ${issuer}
Limit: ${limit}
Transaction Hash: ${result.result.hash}
Explorer Link: ${XRP_EXPLORER}/transactions/${result.result.hash}`,
},
],
};
} catch (signError) {
throw new Error(`Failed to sign/submit transaction: ${(signError as Error).message}`);
}
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to create XRP trustline: ${error.message}` }],
};
}
}
);
process.on('beforeExit', async () => {
await cleanUp();
});
}

View File

@@ -1,277 +0,0 @@
import { Connection, PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
import { Keypair } from '@solana/web3.js';
import fetch from 'cross-fetch';
import bs58 from 'bs58';
const JUPITER_API_BASE = 'https://quote-api.jup.ag/v6';
// Common token addresses
const COMMON_TOKENS = {
SOL: 'So11111111111111111111111111111111111111112',
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
BONK: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
ORCA: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE',
} as const;
interface TokenMetadata {
symbol: string;
decimals: number;
address: string;
name: string;
logoURI?: string;
}
// Types for Jupiter API responses
interface JupiterQuoteResponse {
inputMint: string;
inAmount: string;
outputMint: string;
outAmount: string;
otherAmountThreshold: string;
swapMode: string;
slippageBps: number;
platformFee: {
amount?: string;
feeBps?: number;
} | null;
priceImpactPct: string;
routePlan: Array<{
swapInfo: {
ammKey: string;
label?: string;
inputMint: string;
outputMint: string;
inAmount: string;
outAmount: string;
feeAmount: string;
feeMint: string;
};
percent: number;
}>;
contextSlot?: number;
timeTaken?: number;
}
interface JupiterSwapResponse {
swapTransaction: string;
lastValidBlockHeight: number;
prioritizationFeeLamports: number;
computeUnitLimit: number;
}
// Cache for token metadata
const tokenMetadataCache = new Map<string, TokenMetadata>();
// Initialize token list
async function initializeTokenList() {
if (tokenMetadataCache.size > 0) return;
try {
const response = await fetch('https://token.jup.ag/strict');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
for (const token of data) {
tokenMetadataCache.set(token.address, {
symbol: token.symbol,
decimals: token.decimals,
address: token.address,
name: token.name,
logoURI: token.logoURI
});
}
// Ensure we have the common tokens
ensureCommonTokens();
} catch (error) {
console.error('Failed to fetch token list:', error);
// Fallback to common tokens
ensureCommonTokens();
}
}
// Ensure we have common tokens in cache
function ensureCommonTokens() {
// SOL
if (!tokenMetadataCache.has(COMMON_TOKENS.SOL)) {
tokenMetadataCache.set(COMMON_TOKENS.SOL, {
symbol: 'SOL',
decimals: 9,
address: COMMON_TOKENS.SOL,
name: 'Solana'
});
}
// USDC
if (!tokenMetadataCache.has(COMMON_TOKENS.USDC)) {
tokenMetadataCache.set(COMMON_TOKENS.USDC, {
symbol: 'USDC',
decimals: 6,
address: COMMON_TOKENS.USDC,
name: 'USD Coin'
});
}
// Add other common tokens as needed
}
// Get token metadata
async function getTokenMetadata(mint: string): Promise<TokenMetadata> {
await initializeTokenList();
const tokenInfo = tokenMetadataCache.get(mint);
if (!tokenInfo) {
throw new Error(`Token metadata not found for ${mint}`);
}
return tokenInfo;
}
// Format amount based on decimals
function formatTokenAmount(amount: string, decimals: number): string {
const value = parseInt(amount) / Math.pow(10, decimals);
return value.toFixed(decimals).replace(/\.?0+$/, '');
}
// Get quote from Jupiter
export async function getJupiterQuote(
inputMint: string,
outputMint: string,
amount: string,
slippageBps: number = 50
): Promise<JupiterQuoteResponse> {
// Initialize token list before getting quote
await initializeTokenList();
// Clean input parameters
inputMint = inputMint.toString().trim();
outputMint = outputMint.toString().trim();
amount = amount.toString().trim();
// Validate addresses
try {
new PublicKey(inputMint);
new PublicKey(outputMint);
} catch (error: any) {
throw new Error(`Invalid token address: ${error?.message || 'Invalid format'}`);
}
const url = new URL('/quote', JUPITER_API_BASE);
url.searchParams.set('inputMint', inputMint);
url.searchParams.set('outputMint', outputMint);
url.searchParams.set('amount', amount);
url.searchParams.set('slippageBps', slippageBps.toString());
url.searchParams.set('onlyDirectRoutes', 'false');
url.searchParams.set('restrictIntermediateTokens', 'true');
console.log('Fetching quote from:', url.toString());
const response = await fetch(url.toString());
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to get Jupiter quote: ${error}`);
}
return response.json();
}
// Build swap transaction
export async function buildJupiterSwapTransaction(
quoteResponse: JupiterQuoteResponse,
userPublicKey: string
): Promise<string> {
const url = new URL('/swap', JUPITER_API_BASE);
const body = {
quoteResponse,
userPublicKey,
dynamicComputeUnitLimit: true,
dynamicSlippage: true,
prioritizationFeeLamports: {
priorityLevelWithMaxLamports: {
maxLamports: 1000000,
priorityLevel: "veryHigh"
}
}
};
const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to build swap transaction: ${error}`);
}
const { swapTransaction } = await response.json();
return swapTransaction;
}
// Execute swap transaction
export async function executeJupiterSwap(
connection: Connection,
swapTransaction: string,
privateKey: Uint8Array
): Promise<string> {
try {
const keypair = Keypair.fromSecretKey(privateKey);
// Convert base64 transaction to Uint8Array
const transactionBinary = Buffer.from(swapTransaction, 'base64');
// Deserialize and sign the transaction
const transaction = VersionedTransaction.deserialize(transactionBinary);
transaction.sign([keypair]);
// Send the transaction with optimized parameters
const signature = await connection.sendRawTransaction(
transaction.serialize(),
{
skipPreflight: true,
maxRetries: 2,
preflightCommitment: 'confirmed'
}
);
// Wait for confirmation
await connection.confirmTransaction(signature, 'confirmed');
return signature;
} catch (error) {
console.error('Error executing swap:', error);
throw error;
}
}
// Format quote details for display
export async function formatQuoteDetails(quote: JupiterQuoteResponse): Promise<string> {
const [inputToken, outputToken] = await Promise.all([
getTokenMetadata(quote.inputMint),
getTokenMetadata(quote.outputMint)
]);
const inputAmount = formatTokenAmount(quote.inAmount, inputToken.decimals);
const outputAmount = formatTokenAmount(quote.outAmount, outputToken.decimals);
const routeSteps = quote.routePlan
.map(r => r.swapInfo.label || 'Unknown AMM')
.join(' → ');
// Calculate rate
const inAmountNum = parseFloat(quote.inAmount) / Math.pow(10, inputToken.decimals);
const outAmountNum = parseFloat(quote.outAmount) / Math.pow(10, outputToken.decimals);
const rate = outAmountNum / inAmountNum;
return `Swap Quote Details:
Input: ${inputAmount} ${inputToken.symbol}
Output: ${outputAmount} ${outputToken.symbol}
Rate: 1 ${inputToken.symbol} = ${rate.toFixed(6)} ${outputToken.symbol}
Price Impact: ${quote.priceImpactPct}%
Slippage Tolerance: ${quote.slippageBps / 100}%
Route: ${routeSteps}`;
}

View File

@@ -1,424 +0,0 @@
import { config } from 'dotenv';
config();
import {
Connection,
PublicKey,
Keypair,
Transaction,
SystemProgram,
sendAndConfirmTransaction,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
import {
TOKEN_PROGRAM_ID,
AccountLayout,
} from '@solana/spl-token';
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import bs58 from 'bs58';
import { getJupiterQuote, buildJupiterSwapTransaction, executeJupiterSwap, formatQuoteDetails } from './jupiter.js';
const SOLANA_RPC = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com";
// Initialize Solana connection
const solanaConnection = new Connection(SOLANA_RPC, 'confirmed');
// Helper function to get token accounts for a wallet
async function getTokenAccounts(walletAddress: string) {
try {
const owner = new PublicKey(walletAddress);
const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner(
owner,
{
programId: TOKEN_PROGRAM_ID
}
);
return tokenAccounts.value.map(account => {
const parsedAccountInfo = account.account.data.parsed.info;
return {
mint: new PublicKey(parsedAccountInfo.mint),
amount: parsedAccountInfo.tokenAmount.uiAmount,
decimals: parsedAccountInfo.tokenAmount.decimals,
tokenAccount: account.pubkey.toString()
};
});
} catch (error) {
console.error('Error fetching token accounts:', error);
return [];
}
}
interface SwapParams {
inputMint: string;
outputMint: string;
amount: string;
slippageBps?: number;
}
export function registerSolanaTools(server: McpServer) {
// Debug: Check environment variables
console.error('Debug - ENV vars available:', Object.keys(process.env));
console.error('Debug - SOLANA_PRIVATE_KEY exists:', !!process.env.SOLANA_PRIVATE_KEY);
if (!process.env.SOLANA_PRIVATE_KEY) {
console.error('Warning: SOLANA_PRIVATE_KEY not found in environment');
}
server.tool(
"getMyAddress",
"Get your Solana public address from private key in .env",
{},
async () => {
try {
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('SOLANA_PRIVATE_KEY not found in environment variables');
}
const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY);
const keypair = Keypair.fromSecretKey(privateKeyBytes);
const publicKey = keypair.publicKey.toString();
// Get SOL balance
const balance = await solanaConnection.getBalance(keypair.publicKey);
const solBalance = balance / LAMPORTS_PER_SOL;
// Get token balances
const tokenAccounts = await getTokenAccounts(publicKey);
const tokenBalances = tokenAccounts
.filter(account => account.amount > 0)
.map(account => `${account.amount} (Mint: ${account.mint.toString()})`)
.join('\n');
return {
content: [
{
type: "text",
text: `Your Solana Address: ${publicKey}\nSOL Balance: ${solBalance} SOL\n\nToken Balances:\n${tokenBalances}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get address: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getBalance",
"Get balance for a Solana address",
{
address: z.string().describe("Solana account address"),
},
async ({ address }) => {
try {
const publicKey = new PublicKey(address);
const balance = await solanaConnection.getBalance(publicKey);
const solBalance = balance / LAMPORTS_PER_SOL;
return {
content: [
{
type: "text",
text: `Balance for ${address}:\n${solBalance} SOL`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve balance for address: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getAccountInfo",
"Get detailed account information for a Solana address",
{
address: z.string().describe("Solana account address"),
encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().describe("Data encoding format"),
},
async ({ address, encoding = 'base64' }) => {
try {
const publicKey = new PublicKey(address);
const accountInfo = await solanaConnection.getAccountInfo(
publicKey,
'confirmed'
);
if (!accountInfo) {
return {
content: [
{
type: "text",
text: `No account found for address: ${address}`,
},
],
};
}
let formattedData: string;
if (encoding === 'base58') {
formattedData = bs58.encode(accountInfo.data);
} else if (encoding === 'base64') {
formattedData = Buffer.from(accountInfo.data).toString('base64');
} else {
formattedData = Buffer.from(accountInfo.data).toString('base64');
}
return {
content: [
{
type: "text",
text: `Account Information for ${address}:
Lamports: ${accountInfo.lamports} (${accountInfo.lamports / LAMPORTS_PER_SOL} SOL)
Owner: ${accountInfo.owner.toBase58()}
Executable: ${accountInfo.executable}
Rent Epoch: ${accountInfo.rentEpoch}
Data Length: ${accountInfo.data.length} bytes
Data (${encoding}): ${formattedData}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve account information: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getSplTokenBalances",
"Get SPL token balances for a Solana address",
{
address: z.string().describe("Solana account address"),
},
async ({ address }) => {
try {
const tokenAccounts = await getTokenAccounts(address);
if (tokenAccounts.length === 0) {
return {
content: [
{
type: "text",
text: `No token accounts found for address: ${address}`,
},
],
};
}
const balancesList = tokenAccounts
.filter(account => account.amount > 0)
.map(account => `Mint: ${account.mint.toString()}\nBalance: ${account.amount}\nDecimals: ${account.decimals}\nToken Account: ${account.tokenAccount}`)
.join('\n\n');
return {
content: [
{
type: "text",
text: `Token Balances for ${address}:\n\n${balancesList}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token balances: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getSwapQuote",
"Get best swap quote from Jupiter DEX aggregator",
{
inputMint: z.string().describe("Input token mint address"),
outputMint: z.string().describe("Output token mint address"),
amount: z.string().describe("Amount of input tokens (in smallest denomination)"),
slippageBps: z.number().optional().describe("Slippage tolerance in basis points (optional, default 50 = 0.5%)"),
},
async ({ inputMint, outputMint, amount, slippageBps }: SwapParams) => {
try {
// Validate input parameters
inputMint = inputMint.trim();
outputMint = outputMint.trim();
amount = amount.toString().trim();
const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps);
const formattedDetails = await formatQuoteDetails(quote);
return {
content: [
{
type: "text",
text: formattedDetails,
},
],
quote, // Store quote for subsequent swap
};
} catch (err) {
console.error('Error getting swap quote:', err);
return {
content: [
{
type: "text",
text: `Failed to get swap quote: ${err instanceof Error ? err.message : 'Unknown error'}`,
},
],
};
}
}
);
server.tool(
"executeSwap",
"Execute a token swap using Jupiter DEX aggregator (using private key from .env)",
{
inputMint: z.string().describe("Input token mint address"),
outputMint: z.string().describe("Output token mint address"),
amount: z.string().describe("Amount of input tokens (in smallest denomination)"),
slippageBps: z.number().optional().describe("Slippage tolerance in basis points (optional, default 50 = 0.5%)"),
},
async ({ inputMint, outputMint, amount, slippageBps }: SwapParams) => {
try {
// Validate input parameters
inputMint = inputMint.trim();
outputMint = outputMint.trim();
amount = amount.toString().trim();
// Check for private key
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('SOLANA_PRIVATE_KEY not found in environment variables');
}
// Decode private key
const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY);
const keypair = Keypair.fromSecretKey(privateKeyBytes);
// Get quote
const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps);
const formattedQuote = await formatQuoteDetails(quote);
// Build transaction
const swapTransaction = await buildJupiterSwapTransaction(
quote,
keypair.publicKey.toString()
);
// Execute swap
const signature = await executeJupiterSwap(
solanaConnection,
swapTransaction,
privateKeyBytes
);
return {
content: [
{
type: "text",
text: `${formattedQuote}\n\nSwap executed successfully!\nTransaction signature: ${signature}\nExplorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
} catch (err) {
console.error('Error executing swap:', err);
return {
content: [
{
type: "text",
text: `Failed to execute swap: ${err instanceof Error ? err.message : 'Unknown error'}`,
},
],
};
}
}
);
server.tool(
"transfer",
"Transfer SOL from your keypair (using private key from .env) to another address",
{
toAddress: z.string().describe("Destination wallet address"),
amount: z.number().positive().describe("Amount of SOL to send"),
},
async ({ toAddress, amount }) => {
try {
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('SOLANA_PRIVATE_KEY not found in environment variables');
}
const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY);
const fromKeypair = Keypair.fromSecretKey(privateKeyBytes);
const lamports = amount * LAMPORTS_PER_SOL;
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey: new PublicKey(toAddress),
lamports,
})
);
const signature = await sendAndConfirmTransaction(
solanaConnection,
transaction,
[fromKeypair]
);
return {
content: [
{
type: "text",
text: `Transfer successful!
From: ${fromKeypair.publicKey.toBase58()}
To: ${toAddress}
Amount: ${amount} SOL
Transaction signature: ${signature}
Explorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to transfer SOL: ${error.message}`,
},
],
};
}
}
);
}

View File

@@ -1,173 +0,0 @@
import { Client as ThorchainClient } from '@xchainjs/xchain-thorchain'
import { ThorchainAMM } from '@xchainjs/xchain-thorchain-amm'
import { ThorchainQuery } from '@xchainjs/xchain-thorchain-query'
import { Thornode } from '@xchainjs/xchain-thorchain-query'
import { Network } from '@xchainjs/xchain-client'
import { Asset, assetFromString, assetToString } from '@xchainjs/xchain-util'
import { BigNumber } from 'bignumber.js'
import { z } from "zod"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { config } from 'dotenv'
import axios from 'axios'
import { register9Rheader } from '@xchainjs/xchain-util'
import fetch from 'cross-fetch'
config()
// Initialize Nine Realms headers
register9Rheader(axios)
// Initialize Thorchain clients
const thorchainClient = new ThorchainClient({
network: Network.Mainnet,
phrase: process.env.THORCHAIN_MNEMONIC || ''
})
const thornode = new Thornode(Network.Mainnet)
const thorchainQuery = new ThorchainQuery()
const thorchainAmm = new ThorchainAMM(thorchainQuery)
// Helper function to format base amounts
function formatBaseAmount(baseAmount: any): string {
return baseAmount.amount().toString()
}
export function registerThorchainTools(server: McpServer) {
// Get THORChain balance
server.tool(
"getThorchainBalance",
"Get RUNE balance for an address",
{
address: z.string().describe("THORChain address to check"),
},
async ({ address }) => {
try {
const balances = await thorchainClient.getBalance(address)
return {
content: [{
type: "text",
text: `THORChain Balance for ${address}:\n${formatBaseAmount(balances[0].amount)} RUNE`,
}],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve RUNE balance: ${error.message}` }],
}
}
}
)
// Get pool info
server.tool(
"getThorchainPoolInfo",
"Get information about a THORChain liquidity pool",
{
asset: z.string().describe("Asset symbol (e.g., 'BTC.BTC', 'ETH.ETH')"),
},
async ({ asset }) => {
try {
const pool = await thornode.getPool(asset)
return {
content: [{
type: "text",
text: `Pool Information for ${asset}:\n` +
`Status: ${pool.status}\n` +
`Asset Depth: ${pool.balance_asset}\n` +
`RUNE Depth: ${pool.balance_rune}\n` +
`LP Units: ${pool.LP_units}\n` +
`Synth Units: ${pool.synth_units}`,
}],
}
} catch (err) {
const error = err as Error
return {
content: [{ type: "text", text: `Failed to retrieve pool information: ${error.message}` }],
}
}
}
)
// Get swap quote
server.tool(
"getThorchainSwapQuote",
"Get a quote for swapping assets on THORChain",
{
fromAsset: z.string().describe("Source asset (e.g., 'BTC.BTC')"),
toAsset: z.string().describe("Destination asset (e.g., 'ETH.ETH')"),
amount: z.string().describe("Amount to swap"),
},
async ({ fromAsset: fromAssetString, toAsset: toAssetString, amount: amountString }) => {
try {
// Parse assets
const fromAsset = assetFromString(fromAssetString);
const toAsset = assetFromString(toAssetString);
if (!fromAsset || !toAsset) {
return {
content: [{ type: "text", text: `Invalid asset format. Expected format: 'CHAIN.SYMBOL' (e.g., 'BTC.BTC', 'ETH.ETH')` }],
}
}
// Parse amount
let numAmount;
try {
numAmount = new BigNumber(amountString);
if (numAmount.isNaN() || numAmount.isLessThanOrEqualTo(0)) {
throw new Error('Invalid amount');
}
} catch (error) {
return {
content: [{ type: "text", text: `Invalid amount format. Please provide a valid positive number.` }],
}
}
// Convert amount to base units
const amountInBaseUnits = numAmount.multipliedBy(10 ** 8).toFixed(0);
// Format the quote request parameters
const quoteParams = {
amount: amountInBaseUnits,
from_asset: assetToString(fromAsset),
to_asset: assetToString(toAsset).replace('-B1A', ''), // Remove B1A suffix
destination: '', // Optional destination address
streaming_interval: '1',
streaming_quantity: '0'
};
// Get quote from THORNode directly
const response = await fetch(`https://thornode.ninerealms.com/thorchain/quote/swap?${new URLSearchParams(quoteParams)}`);
if (!response.ok) {
throw new Error(`THORNode API error: ${response.status} ${response.statusText}`);
}
const quote = await response.json();
// Helper function to format asset amounts with proper decimals
const formatAssetAmount = (amount: string | number, decimals: number = 8) => {
const num = Number(amount) / Math.pow(10, decimals);
return num.toLocaleString('en-US', { maximumFractionDigits: decimals });
};
return {
content: [{
type: "text",
text: `Swap Quote:\n` +
`Expected Output: ${formatAssetAmount(quote.expected_amount_out)} ${quoteParams.to_asset}\n` +
`Fees:\n` +
`- Affiliate Fee: ${formatAssetAmount(quote.fees.affiliate)} ${quote.fees.asset}\n` +
`- Outbound Fee: ${formatAssetAmount(quote.fees.outbound)} ${quote.fees.asset}\n` +
`- Liquidity Fee: ${formatAssetAmount(quote.fees.liquidity)} ${quote.fees.asset}\n` +
`- Total Fee: ${formatAssetAmount(quote.fees.total)} ${quote.fees.asset}\n` +
`Slippage: ${quote.fees.slippage_bps / 100}%\n` +
`Expires: ${new Date(quote.expiry * 1000).toLocaleString()}\n` +
`Total Swap Time: ~${quote.total_swap_seconds} seconds`,
}],
}
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to get swap quote: ${error.message}` }],
}
}
}
)
}

View File

@@ -1 +0,0 @@
export { registerTonTools } from './ton';

View File

@@ -1,454 +0,0 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { TonClient, Address, toNano, fromNano, beginCell, WalletContractV4, internal, SendMode } from '@ton/ton';
import { mnemonicToWalletKey, KeyPair } from '@ton/crypto';
import { Contract, Builder } from '@ton/core';
// Initialize TON client
let tonClient: TonClient | null = null;
// TON Network URL - Default to mainnet
const TON_MAINNET_ENDPOINT = process.env.TON_RPC_URL || 'https://toncenter.com/api/v2/jsonRPC';
const TON_TESTNET_ENDPOINT = 'https://testnet.toncenter.com/api/v2/jsonRPC';
const TON_API_KEY = process.env.TON_API_KEY || '';
const TON_EXPLORER = 'https://tonscan.org';
// Helper function to get or initialize the client
function getClient(testnet: boolean = false): TonClient {
const endpoint = testnet ? TON_TESTNET_ENDPOINT : TON_MAINNET_ENDPOINT;
if (!tonClient) {
tonClient = new TonClient({
endpoint,
apiKey: TON_API_KEY
});
}
return tonClient;
}
// Helper function to create a wallet from mnemonic
async function createWalletFromMnemonic(mnemonic: string): Promise<KeyPair> {
const mnemonicArray = mnemonic.split(' ');
const keyPair = await mnemonicToWalletKey(mnemonicArray);
return keyPair;
}
// Helper function to determine wallet version and create appropriate contract
async function createWalletContract(client: TonClient, address: Address, keyPair: KeyPair): Promise<{ contract: WalletContractV4, walletVersion: string }> {
// We'll use WalletV4 (most common and recent standard wallet)
// For production use, you might want to detect the wallet version from on-chain data
const walletContract = WalletContractV4.create({
publicKey: keyPair.publicKey,
workchain: 0
});
// Check if the derived address matches the expected address
const derivedAddress = walletContract.address;
if (!derivedAddress.equals(address)) {
console.warn('Warning: Derived wallet address does not match provided address.');
console.warn(`Derived: ${derivedAddress.toString()}`);
console.warn(`Provided: ${address.toString()}`);
// We'll use the provided address anyway, but this is a warning sign
}
return {
contract: walletContract,
walletVersion: 'v4r2'
};
}
// Helper function to implement exponential backoff for API rate limits
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> {
let retries = 0;
while (true) {
try {
return await fn();
} catch (error: any) {
retries++;
// If we've exceeded our max retries or it's not a rate limit error, rethrow
if (retries > maxRetries || !error.message.includes('429')) {
throw error;
}
// Calculate exponential backoff time (1s, 2s, 4s, 8s, 16s)
const delay = Math.pow(2, retries - 1) * 1000;
console.log(`Rate limit hit, retrying in ${delay}ms (attempt ${retries} of ${maxRetries})...`);
// Wait for the calculated time before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Register all TON tools
export function registerTonTools(server: McpServer) {
// Get TON Balance
server.tool(
"getTonBalance",
"Get balance for a TON address",
{
address: z.string().describe("TON address to check"),
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ address, testnet = false }) => {
try {
const client = getClient(testnet);
// Validate the address
let validAddress;
try {
validAddress = Address.parse(address);
} catch (error) {
return {
content: [{ type: "text", text: `Invalid TON address format: ${address}` }],
};
}
// Get account information including balance with retry logic
const accountInfo = await withRetry(() => client.getBalance(validAddress));
const balance = fromNano(accountInfo);
return {
content: [
{
type: "text",
text: `TON Balance for ${validAddress.toString()}:\n${balance} TON`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve TON balance: ${error.message}` }],
};
}
}
);
// Get TON transaction history
server.tool(
"getTonTransactionHistory",
"Get transaction history for a TON address",
{
address: z.string().describe("TON address to check"),
limit: z.number().optional().describe("Maximum number of transactions to return (default: 10)"),
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ address, limit = 10, testnet = false }) => {
try {
const client = getClient(testnet);
// Validate the address
let validAddress;
try {
validAddress = Address.parse(address);
} catch (error) {
return {
content: [{ type: "text", text: `Invalid TON address format: ${address}` }],
};
}
// Get transactions with retry logic
const transactions = await withRetry(() =>
client.getTransactions(validAddress, { limit: limit })
);
if (transactions.length === 0) {
return {
content: [{ type: "text", text: `No transactions found for ${validAddress.toString()}` }],
};
}
const txList = transactions.map((tx: any) => {
const timestamp = new Date(tx.time * 1000).toLocaleString();
const inMsg = tx.inMessage;
const outMsgs = tx.outMessages;
let txType = "Unknown";
let fromAddress = "N/A";
let toAddress = "N/A";
let amount = "0";
let comment = "";
// Determine if it's incoming or outgoing
if (inMsg && inMsg.source) {
txType = "Incoming";
fromAddress = inMsg.source.toString();
toAddress = validAddress.toString();
amount = inMsg.value ? fromNano(inMsg.value) : "0";
// Try to extract comment if present
if (inMsg.body) {
try {
const msgBody = inMsg.body;
// Attempt to decode comment if it's an op=0 message
if (msgBody && msgBody.beginParse) {
const slice = msgBody.beginParse();
if (slice.loadUint(32) === 0) { // op=0 indicates text comment
comment = slice.loadStringTail();
}
}
} catch (e) {
// Silent fail if we can't parse the comment
}
}
} else if (outMsgs && outMsgs.length > 0) {
txType = "Outgoing";
fromAddress = validAddress.toString();
// Sum total sent amount across all outgoing messages
let totalAmount = 0n;
outMsgs.forEach((msg: any) => {
if (msg.destination) {
toAddress = msg.destination.toString();
}
if (msg.value) {
totalAmount += msg.value;
}
// Try to extract comment from the first message if present
if (!comment && msg.body) {
try {
const msgBody = msg.body;
// Attempt to decode comment if it's an op=0 message
if (msgBody && msgBody.beginParse) {
const slice = msgBody.beginParse();
if (slice.loadUint(32) === 0) { // op=0 indicates text comment
comment = slice.loadStringTail();
}
}
} catch (e) {
// Silent fail if we can't parse the comment
}
}
});
amount = fromNano(totalAmount);
}
return `
Transaction: ${tx.hash}
Type: ${txType}
Date: ${timestamp}
From: ${fromAddress}
To: ${toAddress}
Amount: ${amount} TON${comment ? `\nComment: ${comment}` : ""}`;
}).join('\n---\n');
return {
content: [
{
type: "text",
text: `TON Transaction History for ${validAddress.toString()}:\n${txList}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve TON transaction history: ${error.message}` }],
};
}
}
);
// Validate TON address
server.tool(
"validateTonAddress",
"Validate a TON address format",
{
address: z.string().describe("TON address to validate"),
},
async ({ address }) => {
try {
let isValid = false;
let normalized = "";
try {
const parsedAddress = Address.parse(address);
isValid = true;
normalized = parsedAddress.toString();
} catch (e) {
isValid = false;
}
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} has a valid TON address format.\nNormalized format: ${normalized}`
: `The address ${address} does NOT have a valid TON address format.`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Error validating TON address: ${error.message}` }],
};
}
}
);
// Get TON Network Info
server.tool(
"getTonNetworkInfo",
"Get current TON network information",
{
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ testnet = false }) => {
try {
const client = getClient(testnet);
// Get masterchain info with retry logic
const masterchainInfo = await withRetry(() => client.getMasterchainInfo());
return {
content: [
{
type: "text",
text: `TON Network Information (${testnet ? 'Testnet' : 'Mainnet'}):
Current Workchain: ${masterchainInfo.workchain}
Current Shard: ${masterchainInfo.shard}
Initial Seqno: ${masterchainInfo.initSeqno}
Latest Seqno: ${masterchainInfo.latestSeqno}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve TON Network information: ${error.message}` }],
};
}
}
);
// Send TON transaction using mnemonic
server.tool(
"sendTonTransaction",
"Send TON from your wallet to another address using mnemonic from .env",
{
toAddress: z.string().describe("TON address to send to"),
amount: z.union([z.string(), z.number()]).describe("Amount of TON to send"),
comment: z.string().optional().describe("Optional comment to include with the transaction"),
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ toAddress, amount, comment, testnet = false }) => {
try {
// Check if mnemonic is available
if (!process.env.TON_MNEMONIC) {
throw new Error("TON_MNEMONIC is required in the .env file");
}
// Check if address is available
if (!process.env.TON_ADDRESS) {
throw new Error("TON_ADDRESS is required in the .env file");
}
// Get client
const client = getClient(testnet);
// Parse sender and destination address
const walletAddress = Address.parse(process.env.TON_ADDRESS);
let destinationAddress: Address;
try {
destinationAddress = Address.parse(toAddress);
} catch (error) {
throw new Error(`Invalid destination address: ${toAddress}`);
}
// Create wallet from mnemonic
const keyPair = await createWalletFromMnemonic(process.env.TON_MNEMONIC);
// Convert amount to nanoTON
const amountInNano = toNano(typeof amount === 'number' ? amount.toString() : amount);
// Create wallet contract and determine wallet version
const { contract: walletContract, walletVersion } =
await createWalletContract(client, walletAddress, keyPair);
// Connect to the wallet and ensure we're working with the right contract
const wallet = client.open(walletContract);
// Check wallet balance with retry logic
const balance = await withRetry(() => wallet.getBalance());
const balanceInTon = fromNano(balance);
console.log(`Wallet balance: ${balanceInTon} TON`);
// Ensure wallet has enough funds for the transaction
const sendAmount = BigInt(amountInNano);
const gasFee = toNano('0.05'); // rough estimate for fees
if (balance < sendAmount + gasFee) {
throw new Error(`Insufficient funds: ${balanceInTon} TON available, need at least ${fromNano(sendAmount + gasFee)} TON (including fees)`);
}
// Create transfer payload with comment if provided
const body = comment
? beginCell().storeUint(0, 32).storeStringTail(comment).endCell()
: undefined;
// Create internal message for the transfer
const message = internal({
to: destinationAddress,
value: amountInNano,
body: body,
});
// Send the transaction with retry logic for rate limits
console.log(`Sending ${fromNano(amountInNano)} TON to ${destinationAddress.toString()}`);
// Get seqno with retry
const seqno = await withRetry(() => wallet.getSeqno());
// Get current time to create transaction ID
const timestamp = Math.floor(Date.now() / 1000);
const txId = `${timestamp}-${walletAddress.toString().slice(0, 8)}-${destinationAddress.toString().slice(0, 8)}`;
// Send transfer with retry
await withRetry(() =>
wallet.sendTransfer({
seqno: seqno,
secretKey: keyPair.secretKey,
messages: [message],
sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS,
})
);
// For explorers, we'll create links based on the addresses since we can't get the hash directly
const explorerLink = testnet
? `https://testnet.tonscan.org/address/${destinationAddress.toString()}`
: `${TON_EXPLORER}/address/${destinationAddress.toString()}`;
return {
content: [
{
type: "text",
text: `TON Transaction Sent Successfully!
From: ${walletAddress.toString()}
To: ${destinationAddress.toString()}
Amount: ${typeof amount === 'number' ? amount.toString() : amount} TON
${comment ? `Comment: ${comment}\n` : ''}
Wallet Version: ${walletVersion}
Transaction ID: ${txId}
Explorer Link: ${explorerLink}
Note: The transaction has been sent to the network. Check the explorer link to see when it appears (usually within seconds).`,
},
],
};
} catch (err) {
const error = err as Error;
console.error('Error in sendTonTransaction:', error);
return {
content: [{ type: "text", text: `Failed to send TON transaction: ${error.message}` }],
};
}
}
);
}

30
src/env.d.ts vendored
View File

@@ -1,23 +1,17 @@
declare namespace NodeJS {
interface ProcessEnv {
// Solana
SOLANA_RPC_URL: string;
SOLANA_PRIVATE_KEY: string;
// Vector Network Endpoints
VECTOR_OGMIOS_URL: string;
VECTOR_KOIOS_URL: string;
VECTOR_SUBMIT_URL: string;
VECTOR_EXPLORER_URL: string;
// Ethereum and other EVM chains
ETH_RPC_URL: string;
ETH_PRIVATE_KEY: string;
BASE_RPC_URL: string;
ARBITRUM_RPC_URL: string;
OPTIMISM_RPC_URL: string;
BSC_RPC_URL: string;
POLYGON_RPC_URL: string;
AVALANCHE_RPC_URL: string;
// Wallet Configuration
VECTOR_MNEMONIC: string;
VECTOR_ACCOUNT_INDEX: string;
// Ripple (XRP)
XRP_RPC_URL: string;
XRP_ADDRESS: string;
XRP_PRIVATE_KEY: string;
XRP_PUBLIC_KEY: string;
// Safety Limits
VECTOR_SPEND_LIMIT_PER_TX: string;
VECTOR_SPEND_LIMIT_DAILY: string;
}
}
}

View File

@@ -1,309 +0,0 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const COINGECKO_API_BASE = 'https://api.coingecko.com/api/v3';
// Define interfaces for type safety
interface CoinGeckoSearchCoin {
id: string;
symbol: string;
name: string;
market_cap_rank?: number;
}
interface FormattedCoin {
id: string;
symbol: string;
name: string;
marketCapRank: number | string;
}
interface PlatformInfo {
name: string;
address: string;
decimal_place?: number;
}
export function registerCoinGeckoTools(server: McpServer) {
// Search tokens
server.tool(
"searchCoinGecko",
"Search for coins by ticker symbol OR name to get their CoinGecko ID. IMPORTANT: Only search with one term - either ticker (e.g., 'BTC', 'BERA') or name (e.g., 'Bitcoin', 'Berachain'), but not both. $ symbol will be automatically removed from tickers. Use this first to find the coin's CoinGecko ID before querying detailed information.",
{
query: z.string().describe("Search query (e.g., 'BTC' or 'Bitcoin', but not 'BTC Bitcoin')"),
},
async ({ query }) => {
try {
const apiKey = process.env.COINGECKO_API_KEY;
if (!apiKey) {
throw new Error('COINGECKO_API_KEY not found in environment variables');
}
// Remove $ symbol and trim whitespace
const cleanQuery = query.replace('$', '').trim();
const url = new URL(`${COINGECKO_API_BASE}/search`);
url.searchParams.append('query', cleanQuery);
const headers: Record<string, string> = {
'accept': 'application/json',
'x-cg-demo-api-key': apiKey
};
const response = await fetch(url.toString(), {
headers,
method: 'GET'
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`CoinGecko API error (${response.status}): ${errorText}`);
}
const data = await response.json();
const coins = (data.coins || []) as CoinGeckoSearchCoin[];
const formattedCoins: FormattedCoin[] = coins.map(coin => ({
id: coin.id,
symbol: coin.symbol.toUpperCase(),
name: coin.name,
marketCapRank: coin.market_cap_rank || 'N/A'
}));
let textResponse: string;
if (formattedCoins.length === 0) {
textResponse = `No coins found matching '${cleanQuery}'.\n\nTips:\n1. Search with just the ticker (e.g., 'BERA') or just the name (e.g., 'Berachain'), but not both\n2. Try searching with the ticker first, then try the name if no results\n3. Make sure the ticker or name is spelled correctly`;
} else {
textResponse = `Found ${formattedCoins.length} coins matching '${cleanQuery}':\n\n${
formattedCoins.map(coin =>
`${coin.name} (${coin.symbol})\n` +
`CoinGecko ID: ${coin.id}\n` +
`Market Cap Rank: ${coin.marketCapRank}\n`
).join('\n')}`;
}
return {
content: [
{
type: "text",
text: textResponse
}
],
data: formattedCoins
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to search CoinGecko: ${error.message}`
}
]
};
}
}
);
// Get token prices
server.tool(
"getCoinGeckoPrices",
"Get current prices of tokens using their CoinGecko IDs. Must provide valid CoinGecko IDs (use searchCoinGecko to find IDs first).",
{
ids: z.array(z.string()).describe("Array of CoinGecko token IDs"),
vsCurrencies: z.array(z.string()).describe("Array of currencies to get prices in (e.g., ['usd', 'eur'])"),
includeMarketCap: z.boolean().optional().describe("Include market cap data"),
include24hrVol: z.boolean().optional().describe("Include 24h volume data"),
include24hrChange: z.boolean().optional().describe("Include 24h price change data"),
includeLastUpdatedAt: z.boolean().optional().describe("Include last updated timestamp"),
},
async ({ ids, vsCurrencies, includeMarketCap, include24hrVol, include24hrChange, includeLastUpdatedAt }) => {
try {
const apiKey = process.env.COINGECKO_API_KEY;
if (!apiKey) {
throw new Error('COINGECKO_API_KEY not found in environment variables');
}
const url = new URL(`${COINGECKO_API_BASE}/simple/price`);
const params = {
ids: ids.join(','),
vs_currencies: vsCurrencies.join(','),
include_market_cap: includeMarketCap?.toString() ?? 'false',
include_24hr_vol: include24hrVol?.toString() ?? 'false',
include_24hr_change: include24hrChange?.toString() ?? 'false',
include_last_updated_at: includeLastUpdatedAt?.toString() ?? 'false',
};
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
const headers: Record<string, string> = {
'accept': 'application/json',
'x-cg-demo-api-key': apiKey
};
const response = await fetch(url.toString(), {
headers,
method: 'GET'
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`CoinGecko API error (${response.status}): ${errorText}`);
}
const data = await response.json();
// Format the response
let textResponse = '';
for (const [coinId, prices] of Object.entries(data)) {
textResponse += `${coinId}:\n`;
for (const [currency, value] of Object.entries(prices as Record<string, any>)) {
const formattedValue = currency.includes('last_updated') ? new Date(value * 1000).toISOString()
: currency.includes('market_cap') ? `$${value.toLocaleString()}`
: currency.includes('24h_vol') ? `$${value.toLocaleString()}`
: currency.includes('24h_change') ? `${value.toFixed(2)}%`
: `${value} ${currency.toUpperCase()}`;
const label = currency.includes('market_cap') ? 'Market Cap'
: currency.includes('24h_vol') ? '24h Volume'
: currency.includes('24h_change') ? '24h Change'
: currency.includes('last_updated') ? 'Last Updated'
: `Price (${currency.toUpperCase()})`;
textResponse += ` ${label}: ${formattedValue}\n`;
}
textResponse += '\n';
}
return {
content: [
{
type: "text",
text: textResponse || 'No price data found.'
}
],
data
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get CoinGecko prices: ${error.message}`
}
]
};
}
}
);
// Get detailed coin information including contract addresses
server.tool(
"getCoinInfoByCoingeckoId",
"Look up contract addresses and chain information for a token using its CoinGecko ID. Use searchCoinGecko first to find the correct CoinGecko ID before using this tool.",
{
id: z.string().describe("CoinGecko coin ID (get this from searchCoinGecko)"),
marketData: z.boolean().optional().describe("Include market data"),
localization: z.boolean().optional().describe("Include localized data"),
tickers: z.boolean().optional().describe("Include ticker data"),
communityData: z.boolean().optional().describe("Include community data"),
developerData: z.boolean().optional().describe("Include developer data"),
sparkline: z.boolean().optional().describe("Include sparkline data"),
},
async ({ id, localization = false, tickers = false, marketData = true, communityData = false, developerData = false, sparkline = false }) => {
try {
const apiKey = process.env.COINGECKO_API_KEY;
if (!apiKey) {
throw new Error('COINGECKO_API_KEY not found in environment variables');
}
const url = new URL(`${COINGECKO_API_BASE}/coins/${encodeURIComponent(id)}`);
url.searchParams.append('localization', localization.toString());
url.searchParams.append('tickers', tickers.toString());
url.searchParams.append('market_data', marketData.toString());
url.searchParams.append('community_data', communityData.toString());
url.searchParams.append('developer_data', developerData.toString());
url.searchParams.append('sparkline', sparkline.toString());
const headers: Record<string, string> = {
'accept': 'application/json',
'x-cg-demo-api-key': apiKey
};
const response = await fetch(url.toString(), {
headers,
method: 'GET'
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`CoinGecko API error (${response.status}): ${errorText}`);
}
const data = await response.json();
// Format platform and contract info first as it's the primary purpose
let contractInfo = '';
if (data.platforms && Object.keys(data.platforms).length > 0) {
contractInfo = '\nContract Addresses:\n';
for (const [platform, address] of Object.entries(data.platforms)) {
if (address && address !== '') {
contractInfo += ` ${platform}: ${address}\n`;
}
}
}
// Format general info
let generalInfo = `${data.name} (${data.symbol.toUpperCase()})`;
if (data.market_cap_rank) {
generalInfo += `\nMarket Cap Rank: #${data.market_cap_rank}`;
}
// Format market data if requested
let marketInfo = '';
if (marketData && data.market_data) {
const md = data.market_data;
marketInfo = '\nMarket Data:\n';
if (md.current_price?.usd) {
marketInfo += ` Current Price: $${md.current_price.usd.toLocaleString()}\n`;
}
if (md.market_cap?.usd) {
marketInfo += ` Market Cap: $${md.market_cap.usd.toLocaleString()}\n`;
}
if (md.total_volume?.usd) {
marketInfo += ` 24h Volume: $${md.total_volume.usd.toLocaleString()}\n`;
}
if (md.price_change_percentage_24h) {
marketInfo += ` 24h Change: ${md.price_change_percentage_24h.toFixed(2)}%\n`;
}
}
const textResponse = `${generalInfo}${contractInfo}${marketInfo}\n\nLast Updated: ${data.last_updated}`;
return {
content: [
{
type: "text",
text: textResponse
}
],
data // Include raw data for potential future use
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get coin info: ${error.message}`
}
]
};
}
}
);
}

View File

@@ -1,8 +0,0 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerCoinGeckoTools } from "./coingecko.js";
import { registerRubicTools } from "./rubic.js";
export function registerGeneralTools(server: McpServer) {
registerCoinGeckoTools(server);
registerRubicTools(server);
}

View File

@@ -1,478 +0,0 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const RUBIC_API_BASE = 'https://api-v2.rubic.exchange/api';
// Define interfaces for type safety matching the actual Rubic API response
interface TokenInfo {
address: string;
blockchain: string;
blockchainId: number;
decimals: number;
name: string;
symbol: string;
price?: number;
}
interface RubicQuoteResponse {
id: string;
estimate: {
destinationTokenAmount: string;
destinationTokenMinAmount: string;
destinationUsdAmount: number;
destinationUsdMinAmount: number;
destinationWeiAmount: string;
destinationWeiMinAmount: string;
durationInMinutes: number;
priceImpact: number;
slippage: number;
};
fees: {
gasTokenFees: {
nativeToken: TokenInfo;
protocol: {
fixedAmount: string;
fixedUsdAmount: number;
fixedWeiAmount: string;
};
provider: {
fixedAmount: string;
fixedUsdAmount: number;
fixedWeiAmount: string;
};
};
percentFees: {
percent: number;
token: TokenInfo | null;
};
};
providerType: string;
routing: Array<{
path: Array<TokenInfo & { amount: string }>;
provider: string;
type: string;
}>;
swapType: 'cross-chain' | 'on-chain';
tokens: {
from: TokenInfo & { amount: string };
to: TokenInfo;
};
transaction: {
approvalAddress: string;
};
warnings: Array<any>;
}
interface AvailableBlockchain {
name: string;
id: number;
testnet: boolean;
providers: {
crossChain: string[];
onChain: string[];
};
proxyAvailable: boolean;
type: 'EVM' | 'TRON' | 'SOLANA' | 'Other';
}
interface CrossChainStatusResponse {
srcTxHash: string;
dstTxHash?: string;
status: 'pending' | 'indexing' | 'revert' | 'failed' | 'claim' | 'success' | 'error';
message?: string;
error?: string;
bridgeName?: string;
}
export function registerRubicTools(server: McpServer) {
// Get available blockchains
server.tool(
"getRubicSupportedChains",
"Get a list of all blockchains supported by Rubic for cross-chain bridging.",
{
includeTestnets: z.boolean().optional().describe("Include testnet blockchains in the results."),
},
async ({ includeTestnets = false }) => {
try {
const url = new URL(`${RUBIC_API_BASE}/info/chains`);
url.searchParams.append('includeTestnets', includeTestnets.toString());
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Rubic API error (${response.status}): ${errorText}`);
}
const data = await response.json() as AvailableBlockchain[];
// Format the response for readable output
const formattedChains = data.map(chain => ({
name: chain.name,
id: chain.id,
testnet: chain.testnet,
type: chain.type,
crossChainProviders: chain.providers.crossChain,
onChainProviders: chain.providers.onChain,
proxyAvailable: chain.proxyAvailable
}));
// Create readable text response
const textResponse = `Available blockchains for cross-chain bridging:\n\n${
formattedChains.map(chain =>
`${chain.name} (ID: ${chain.id})${chain.testnet ? ' [TESTNET]' : ''}\n` +
`Type: ${chain.type}\n` +
`Cross-Chain Providers: ${chain.crossChainProviders.join(', ')}\n` +
`On-Chain Providers: ${chain.onChainProviders.join(', ')}\n` +
`Fee Collection Available: ${chain.proxyAvailable ? 'Yes' : 'No'}\n`
).join('\n')
}`;
return {
content: [
{
type: "text",
text: textResponse
}
],
data: formattedChains
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get supported blockchains: ${error.message}`
}
]
};
}
}
);
// Get bridge quote
server.tool(
"getRubicBridgeQuote",
"Get the best cross-chain bridge route for swapping tokens between different blockchains.",
{
srcTokenAddress: z.string().describe("Source token address. Use 0x0000000000000000000000000000000000000000 for native tokens like ETH, BNB, etc."),
srcTokenBlockchain: z.string().describe("Source blockchain name (e.g., ETH, BSC, POLYGON, etc.)"),
srcTokenAmount: z.string().describe("Amount of source token to bridge (as a string with decimals)"),
dstTokenAddress: z.string().describe("Destination token address. Use 0x0000000000000000000000000000000000000000 for native tokens."),
dstTokenBlockchain: z.string().describe("Destination blockchain name (e.g., ETH, BSC, POLYGON, etc.)"),
walletAddress: z.string().optional().describe("Wallet address to send tokens to on the destination blockchain"),
slippageTolerance: z.number().min(0.01).max(50).optional().describe("Slippage tolerance in percentage (min: 0.01, max: 50)"),
showFailedRoutes: z.boolean().optional().describe("Show failed routes in the response"),
includeTestnets: z.boolean().optional().describe("Include testnets in calculations"),
timeout: z.number().min(5).max(60).optional().describe("Calculation timeout in seconds (min: 5, max: 60)"),
},
async ({ srcTokenAddress, srcTokenBlockchain, srcTokenAmount, dstTokenAddress, dstTokenBlockchain, walletAddress, slippageTolerance = 1, showFailedRoutes = false, includeTestnets = false, timeout = 30 }) => {
try {
const url = new URL(`${RUBIC_API_BASE}/routes/quoteBest`);
// Ensure all values are properly formatted as strings where needed
const requestBody = {
srcTokenBlockchain: String(srcTokenBlockchain),
srcTokenAddress: String(srcTokenAddress),
srcTokenAmount: String(srcTokenAmount),
dstTokenBlockchain: String(dstTokenBlockchain),
dstTokenAddress: String(dstTokenAddress),
referrer: "web3-mcp", // Recommended to set your application name as referrer
timeout: Number(timeout),
includeTestnets: Boolean(includeTestnets),
showFailedRoutes: Boolean(showFailedRoutes),
slippageTolerance: Number(slippageTolerance) / 100, // Convert percentage to decimal
...(walletAddress ? { walletAddress: String(walletAddress) } : {})
};
const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Rubic API error (${response.status}): ${errorText}`);
}
const data = await response.json() as RubicQuoteResponse;
// Format response for readable output
let textResponse = `Bridge Quote Results:\n\n`;
textResponse += `From: ${data.tokens.from.amount} ${data.tokens.from.symbol} (${data.tokens.from.blockchain})\n`;
textResponse += `To: ${data.estimate.destinationTokenAmount} ${data.tokens.to.symbol} (${data.tokens.to.blockchain})\n\n`;
if (data.tokens.from.price && data.tokens.to.price) {
textResponse += `USD Value: $${(parseFloat(data.tokens.from.amount) * data.tokens.from.price).toFixed(2)} → $${data.estimate.destinationUsdAmount.toFixed(2)}\n\n`;
}
textResponse += `Provider: ${data.providerType.toUpperCase()}\n`;
textResponse += `Type: ${data.swapType}\n`;
textResponse += `Estimated Duration: ${data.estimate.durationInMinutes} minutes\n`;
if (data.fees.gasTokenFees) {
textResponse += `Gas Fee: ${data.fees.gasTokenFees.provider.fixedAmount} ${data.fees.gasTokenFees.nativeToken.symbol} (≈$${data.fees.gasTokenFees.provider.fixedUsdAmount.toFixed(2)})\n`;
}
textResponse += `\nFees:\n`;
if (data.fees.percentFees) {
textResponse += `Percent Fee: ${data.fees.percentFees.percent}%\n`;
}
if (data.estimate.priceImpact) {
textResponse += `\nPrice Impact: ${(data.estimate.priceImpact * 100).toFixed(2)}%\n`;
}
if (data.warnings && data.warnings.length > 0) {
textResponse += `\nWarnings: ${data.warnings.length}\n`;
}
// Add routing path details
if (data.routing && data.routing.length > 0) {
textResponse += `\nRouting Path:\n`;
data.routing.forEach((route, i) => {
textResponse += `Step ${i + 1}: ${route.provider} (${route.type})\n`;
if (route.path.length > 0) {
const fromToken = route.path[0];
const toToken = route.path[route.path.length - 1];
textResponse += ` ${fromToken.amount} ${fromToken.symbol} → ${toToken.amount} ${toToken.symbol}\n`;
}
});
}
return {
content: [
{
type: "text",
text: textResponse
}
],
data
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get bridge quote: ${error.message}`
}
]
};
}
}
);
// Get multiple bridge quotes
server.tool(
"getRubicBridgeQuotes",
"Get all available cross-chain bridge routes for swapping tokens between different blockchains.",
{
srcTokenAddress: z.string().describe("Source token address. Use 0x0000000000000000000000000000000000000000 for native tokens like ETH, BNB, etc."),
srcTokenBlockchain: z.string().describe("Source blockchain name (e.g., ETH, BSC, POLYGON, etc.)"),
srcTokenAmount: z.string().describe("Amount of source token to bridge (as a string with decimals)"),
dstTokenAddress: z.string().describe("Destination token address. Use 0x0000000000000000000000000000000000000000 for native tokens."),
dstTokenBlockchain: z.string().describe("Destination blockchain name (e.g., ETH, BSC, POLYGON, etc.)"),
walletAddress: z.string().optional().describe("Wallet address to send tokens to on the destination blockchain"),
slippageTolerance: z.number().min(0.01).max(50).optional().describe("Slippage tolerance in percentage (min: 0.01, max: 50)"),
showFailedRoutes: z.boolean().optional().describe("Show failed routes in the response"),
includeTestnets: z.boolean().optional().describe("Include testnets in calculations"),
timeout: z.number().min(5).max(60).optional().describe("Calculation timeout in seconds (min: 5, max: 60)"),
},
async ({ srcTokenAddress, srcTokenBlockchain, srcTokenAmount, dstTokenAddress, dstTokenBlockchain, walletAddress, slippageTolerance = 1, showFailedRoutes = false, includeTestnets = false, timeout = 30 }) => {
try {
const url = new URL(`${RUBIC_API_BASE}/routes/quoteAll`);
// Ensure all values are properly formatted as strings where needed
const requestBody = {
srcTokenBlockchain: String(srcTokenBlockchain),
srcTokenAddress: String(srcTokenAddress),
srcTokenAmount: String(srcTokenAmount),
dstTokenBlockchain: String(dstTokenBlockchain),
dstTokenAddress: String(dstTokenAddress),
referrer: "web3-mcp", // Recommended to set your application name as referrer
timeout: Number(timeout),
includeTestnets: Boolean(includeTestnets),
showFailedRoutes: Boolean(showFailedRoutes),
slippageTolerance: Number(slippageTolerance) / 100, // Convert percentage to decimal
...(walletAddress ? { walletAddress: String(walletAddress) } : {})
};
const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Rubic API error (${response.status}): ${errorText}`);
}
const data = await response.json() as RubicQuoteResponse[];
// Format response for readable output
let textResponse = `Available Bridge Routes:\n\n`;
if (data.length === 0) {
textResponse += `No available routes found.`;
} else {
data.forEach((route, index) => {
textResponse += `Route ${index + 1}: ${route.providerType.toUpperCase()}\n`;
textResponse += `From: ${route.tokens.from.amount} ${route.tokens.from.symbol} (${route.tokens.from.blockchain})\n`;
textResponse += `To: ${route.estimate.destinationTokenAmount} ${route.tokens.to.symbol} (${route.tokens.to.blockchain})\n`;
if (route.tokens.from.price && route.tokens.to.price) {
textResponse += `USD Value: $${(parseFloat(route.tokens.from.amount) * route.tokens.from.price).toFixed(2)} → $${route.estimate.destinationUsdAmount.toFixed(2)}\n`;
}
textResponse += `Estimated Time: ${route.estimate.durationInMinutes} minutes\n`;
if (route.fees.gasTokenFees) {
textResponse += `Gas Fee: ${route.fees.gasTokenFees.provider.fixedAmount} ${route.fees.gasTokenFees.nativeToken.symbol} (≈$${route.fees.gasTokenFees.provider.fixedUsdAmount.toFixed(2)})\n`;
}
if (route.estimate.priceImpact) {
textResponse += `Price Impact: ${(route.estimate.priceImpact * 100).toFixed(2)}%\n`;
}
textResponse += `\n`;
});
}
return {
content: [
{
type: "text",
text: textResponse
}
],
data
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get bridge quotes: ${error.message}`
}
]
};
}
}
);
// Check cross-chain transaction status
server.tool(
"getRubicBridgeStatus",
"Check the status of a cross-chain bridge transaction.",
{
srcTxHash: z.string().describe("Source transaction hash to check status"),
},
async ({ srcTxHash }) => {
try {
const url = new URL(`${RUBIC_API_BASE}/info/status`);
url.searchParams.append('srcTxHash', String(srcTxHash));
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Rubic API error (${response.status}): ${errorText}`);
}
const data = await response.json() as CrossChainStatusResponse;
// Format response for readable output
let textResponse = `Cross-Chain Transaction Status:\n\n`;
textResponse += `Source Transaction: ${data.srcTxHash}\n`;
if (data.dstTxHash) {
textResponse += `Destination Transaction: ${data.dstTxHash}\n`;
}
textResponse += `Status: ${data.status.toUpperCase()}\n`;
if (data.message) {
textResponse += `Message: ${data.message}\n`;
}
if (data.error) {
textResponse += `Error: ${data.error}\n`;
}
if (data.bridgeName) {
textResponse += `Bridge Provider: ${data.bridgeName}\n`;
}
// Provide a human-readable explanation of the status
textResponse += `\nStatus Explanation:\n`;
switch (data.status) {
case 'pending':
textResponse += `Your transaction is still in progress. This could take a few minutes to complete.`;
break;
case 'indexing':
textResponse += `The transaction has been detected but is still being indexed. Please check back soon.`;
break;
case 'revert':
textResponse += `The transaction on the destination chain failed and needs to be reverted. You should collect your funds.`;
break;
case 'failed':
textResponse += `The transaction has failed. Your funds may be reverted automatically.`;
break;
case 'claim':
textResponse += `The transaction was successful! You can now claim your tokens on the destination chain.`;
break;
case 'success':
textResponse += `The transaction was completed successfully! Your tokens have been sent to the destination address.`;
break;
case 'error':
textResponse += `An error occurred during the transaction. Please check the error message for details.`;
break;
default:
textResponse += `Unknown status. Please check the Rubic interface for more information.`;
}
return {
content: [
{
type: "text",
text: textResponse
}
],
data
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to get bridge transaction status: ${error.message}`
}
]
};
}
}
);
}

View File

@@ -1,5 +1,4 @@
import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { existsSync } from 'fs';
@@ -13,96 +12,29 @@ const envPath = resolve(__dirname, '../.env');
console.error('Looking for .env file at:', envPath);
console.error('.env file exists:', existsSync(envPath));
// Load environment variables
const result = config({ path: envPath });
if (result.error) {
console.error('Error loading .env file:', result.error);
process.exit(1);
// Load environment variables (non-fatal if missing — env vars can be passed directly)
if (existsSync(envPath)) {
const result = config({ path: envPath });
if (result.error) {
console.error('Warning: Error loading .env file:', result.error);
}
} else {
console.error('No .env file found — using environment variables directly');
}
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerSolanaTools } from "./chains/solana/solana.js";
import { registerEvmTools } from "./chains/evm/evm.js";
import {
registerBitcoinTools,
registerLitecoinTools,
registerDogecoinTools,
registerBitcoinCashTools,
registerCardanoTools,
} from "./chains/UTXO/index.js";
import { registerThorchainTools } from "./chains/thorchain/thorchain.js";
import { registerRippleTools } from "./chains/ripple/ripple.js";
import { registerTonTools } from "./chains/ton/ton.js";
import { registerGeneralTools } from "./general/index.js";
import { registerVectorTools } from "./vector/index.js";
// Create server instance
const server = new McpServer({
name: "web3-rpc",
name: "vector-mcp-server",
version: "1.0.0",
});
// Helper function to check if a feature is enabled
const isEnabled = (envVar: string): boolean => {
const value = process.env[envVar]?.toLowerCase();
return value === 'true' || value === '1' || value === 'yes';
};
// Register chain-specific tools based on environment variables
if (isEnabled('ENABLE_SOLANA_TOOLS')) {
console.error('Registering Solana tools...');
registerSolanaTools(server);
}
if (isEnabled('ENABLE_EVM_TOOLS')) {
console.error('Registering EVM chain tools...');
registerEvmTools(server);
}
// UTXO Chain Tools
if (isEnabled('ENABLE_BITCOIN_TOOLS')) {
console.error('Registering Bitcoin tools...');
registerBitcoinTools(server);
}
if (isEnabled('ENABLE_LITECOIN_TOOLS')) {
console.error('Registering Litecoin tools...');
registerLitecoinTools(server);
}
if (isEnabled('ENABLE_DOGECOIN_TOOLS')) {
console.error('Registering Dogecoin tools...');
registerDogecoinTools(server);
}
if (isEnabled('ENABLE_BITCOINCASH_TOOLS')) {
console.error('Registering Bitcoin Cash tools...');
registerBitcoinCashTools(server);
}
if (isEnabled('ENABLE_CARDANO_TOOLS')) {
console.error('Registering Cardano tools...');
registerCardanoTools(server);
}
if (isEnabled('ENABLE_THORCHAIN_TOOLS')) {
console.error('Registering THORChain tools...');
registerThorchainTools(server);
}
if (isEnabled('ENABLE_RIPPLE_TOOLS')) {
console.error('Registering Ripple (XRP) tools...');
registerRippleTools(server);
}
if (isEnabled('ENABLE_TON_TOOLS')) {
console.error('Registering TON tools...');
registerTonTools(server);
}
// Register general tools
console.error('Registering general tools...');
registerGeneralTools(server);
// Register Vector tools
console.error('Registering Vector tools...');
registerVectorTools(server);
async function main() {
const transport = new StdioServerTransport();
@@ -113,4 +45,4 @@ main().catch((err: unknown) => {
const error = err as Error;
console.error("Fatal error in main():", error.message);
process.exit(1);
});
});

View File

@@ -1,12 +0,0 @@
export interface MCPRequest {
method: string;
params?: Record<string, any>;
}
export interface MCPResponse {
success: boolean;
data?: any;
error?: string;
}
export type MCPHandler = (params: any) => Promise<MCPResponse>;

2
src/vector/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { registerVectorTools } from './vector.js';
export type { VectorWalletInfo, VectorToken, VectorAdaTransactionResult, VectorTokenTransactionResult } from './types.js';

View File

@@ -0,0 +1,391 @@
/**
* Custom Lucid Provider backed by Ogmios HTTP JSON-RPC + Vector submit-api.
*
* Implements the lucid-cardano Provider interface so we can use Lucid's
* transaction building/signing without depending on Blockfrost.
*/
import fetch from 'cross-fetch';
import type {
Provider,
ProtocolParameters,
UTxO,
OutRef,
Credential,
Delegation,
// @ts-ignore - type aliases used as string in lucid
} from 'lucid-cardano';
interface OgmiosProviderConfig {
ogmiosUrl: string;
submitUrl: string;
koiosUrl?: string;
}
export class OgmiosProvider implements Provider {
private ogmiosUrl: string;
private submitUrl: string;
private koiosUrl: string | undefined;
constructor(config: OgmiosProviderConfig) {
// Strip trailing slashes for consistent URL building
this.ogmiosUrl = config.ogmiosUrl.replace(/\/+$/, '');
this.submitUrl = config.submitUrl.replace(/\/+$/, '');
this.koiosUrl = config.koiosUrl?.replace(/\/+$/, '');
}
/**
* Send a JSON-RPC request to Ogmios
*/
private async rpc(method: string, params?: unknown): Promise<any> {
const response = await fetch(this.ogmiosUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method,
params: params || {},
id: null,
}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Ogmios RPC error (${response.status}): ${text}`);
}
const json = await response.json();
if (json.error) {
throw new Error(`Ogmios RPC error: ${JSON.stringify(json.error)}`);
}
return json.result;
}
async getProtocolParameters(): Promise<ProtocolParameters> {
const result = await this.rpc('queryLedgerState/protocolParameters');
// Map Ogmios v6 response to Lucid's ProtocolParameters format
return {
minFeeA: result.minFeeCoefficient,
minFeeB: result.minFeeConstant?.ada?.lovelace ?? result.minFeeConstant,
maxTxSize: result.maxTransactionSize?.bytes ?? result.maxTransactionSize,
maxValSize: result.maxValueSize?.bytes ?? result.maxValueSize,
keyDeposit: BigInt(result.stakeCredentialDeposit?.ada?.lovelace ?? result.stakeCredentialDeposit ?? 2000000),
poolDeposit: BigInt(result.stakePoolDeposit?.ada?.lovelace ?? result.stakePoolDeposit ?? 500000000),
priceMem: result.scriptExecutionPrices?.memory
? this.parseFraction(result.scriptExecutionPrices.memory)
: (result.prices?.memory ?? 0.0577),
priceStep: result.scriptExecutionPrices?.cpu
? this.parseFraction(result.scriptExecutionPrices.cpu)
: (result.prices?.steps ?? 0.0000721),
maxTxExMem: BigInt(result.maxExecutionUnitsPerTransaction?.memory ?? 14000000),
maxTxExSteps: BigInt(result.maxExecutionUnitsPerTransaction?.cpu ?? 10000000000),
coinsPerUtxoByte: BigInt(result.minUtxoDepositCoefficient ?? result.coinsPerUtxoByte ?? 4310),
collateralPercentage: result.collateralPercentage ?? 150,
maxCollateralInputs: result.maxCollateralInputs ?? 3,
costModels: this.parseCostModels(result.plutusCostModels ?? result.costModels ?? {}),
minfeeRefscriptCostPerByte: result.minFeeReferenceScripts?.base ?? 15,
} as ProtocolParameters;
}
private parseFraction(value: string | number): number {
if (typeof value === 'number') return value;
const parts = value.split('/');
if (parts.length === 2) {
return Number(parts[0]) / Number(parts[1]);
}
return parseFloat(value);
}
private parseCostModels(raw: any): any {
const costModels: any = {};
if (raw['plutus:v1']) {
costModels.PlutusV1 = raw['plutus:v1'];
}
if (raw['plutus:v2']) {
costModels.PlutusV2 = raw['plutus:v2'];
}
if (raw['plutus:v3']) {
costModels.PlutusV3 = raw['plutus:v3'];
}
// Fallback: if keys are already PlutusV1/V2 format
if (raw.PlutusV1) costModels.PlutusV1 = raw.PlutusV1;
if (raw.PlutusV2) costModels.PlutusV2 = raw.PlutusV2;
if (raw.PlutusV3) costModels.PlutusV3 = raw.PlutusV3;
return costModels;
}
async getUtxos(addressOrCredential: string | Credential): Promise<UTxO[]> {
let address: string;
if (typeof addressOrCredential === 'string') {
address = addressOrCredential;
} else {
throw new Error('Credential-based UTxO lookup not supported via Ogmios. Use an address string.');
}
const result = await this.rpc('queryLedgerState/utxo', { addresses: [address] });
return this.ogmiosUtxosToLucid(result);
}
async getUtxosWithUnit(addressOrCredential: string | Credential, unit: string): Promise<UTxO[]> {
const utxos = await this.getUtxos(addressOrCredential);
return utxos.filter((utxo) => utxo.assets[unit]);
}
async getUtxoByUnit(unit: string): Promise<UTxO> {
// This requires an indexer; try Koios if available
if (this.koiosUrl) {
const policyId = unit.slice(0, 56);
const assetName = unit.slice(56);
const response = await fetch(`${this.koiosUrl}/api/v1/asset_utxos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ _asset_list: [[policyId, assetName]] }),
});
if (response.ok) {
const data = await response.json();
if (data.length > 0) {
return this.koiosUtxoToLucid(data[0]);
}
}
}
throw new Error('getUtxoByUnit requires an indexer (Koios). Not available or asset not found.');
}
async getUtxosByOutRef(outRefs: OutRef[]): Promise<UTxO[]> {
const outputReferences = outRefs.map((ref) => ({
transaction: { id: ref.txHash },
index: ref.outputIndex,
}));
const result = await this.rpc('queryLedgerState/utxo', { outputReferences });
return this.ogmiosUtxosToLucid(result);
}
async getDelegation(rewardAddress: string): Promise<Delegation> {
// Try Koios first if available
if (this.koiosUrl) {
try {
const response = await fetch(`${this.koiosUrl}/api/v1/account_info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ _stake_addresses: [rewardAddress] }),
});
if (response.ok) {
const data = await response.json();
if (data.length > 0) {
return {
poolId: data[0].delegated_pool || null,
rewards: BigInt(data[0].rewards_available || 0),
};
}
}
} catch {
// Fall through to default
}
}
return { poolId: null, rewards: 0n };
}
async getDatum(datumHash: string): Promise<string> {
// Try Koios if available
if (this.koiosUrl) {
try {
const response = await fetch(`${this.koiosUrl}/api/v1/datum_info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ _datum_hashes: [datumHash] }),
});
if (response.ok) {
const data = await response.json();
if (data.length > 0 && data[0].bytes) {
return data[0].bytes;
}
}
} catch {
// Fall through
}
}
throw new Error(`Datum not found for hash: ${datumHash}. Datum lookups require Koios indexer.`);
}
async awaitTx(txHash: string, checkInterval: number = 3000): Promise<boolean> {
// Poll for tx confirmation
const maxAttempts = 60; // ~3 minutes at 3s intervals
for (let i = 0; i < maxAttempts; i++) {
await new Promise((resolve) => setTimeout(resolve, checkInterval));
// Try Koios tx_status if available
if (this.koiosUrl) {
try {
const response = await fetch(`${this.koiosUrl}/api/v1/tx_status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ _tx_hashes: [txHash] }),
});
if (response.ok) {
const data = await response.json();
if (data.length > 0 && data[0].num_confirmations > 0) {
return true;
}
}
} catch {
// Continue polling
}
} else {
// Without Koios, try querying the UTxO by outRef
try {
const utxos = await this.getUtxosByOutRef([{ txHash, outputIndex: 0 }]);
if (utxos.length > 0) {
return true;
}
} catch {
// Not yet confirmed, continue
}
}
}
return false;
}
async submitTx(tx: string): Promise<string> {
// tx is a hex-encoded CBOR transaction
// submit-api expects raw CBOR bytes with Content-Type: application/cbor
const bytes = Buffer.from(tx, 'hex');
const response = await fetch(this.submitUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/cbor' },
body: bytes,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Transaction submission failed (${response.status}): ${errorText}`);
}
const result = await response.text();
// submit-api returns the tx hash as a JSON string
try {
const parsed = JSON.parse(result);
return typeof parsed === 'string' ? parsed : parsed.txId || parsed.txHash || tx.slice(0, 64);
} catch {
// If response is plain text tx hash
return result.replace(/"/g, '').trim();
}
}
/**
* Convert Ogmios UTxO response format to Lucid UTxO format
*/
private ogmiosUtxosToLucid(ogmiosUtxos: any[]): UTxO[] {
if (!Array.isArray(ogmiosUtxos)) return [];
return ogmiosUtxos.map((utxo) => {
const txHash = utxo.transaction?.id ?? utxo.txId;
const outputIndex = utxo.index ?? utxo.outputIndex;
const output = utxo.output ?? utxo;
// Parse assets
const assets: Record<string, bigint> = {};
// Handle different Ogmios response formats for value
const value = output.value ?? output.amount;
if (typeof value === 'object' && value !== null) {
// Ogmios v6 format: { ada: { lovelace: N }, policyId: { assetName: N } }
if (value.ada?.lovelace !== undefined) {
assets['lovelace'] = BigInt(value.ada.lovelace);
} else if (value.coins !== undefined) {
// Older format
assets['lovelace'] = BigInt(value.coins);
} else if (typeof value === 'bigint' || typeof value === 'number') {
assets['lovelace'] = BigInt(value);
}
// Parse native assets
for (const [key, val] of Object.entries(value)) {
if (key === 'ada' || key === 'coins' || key === 'lovelace') continue;
if (typeof val === 'object' && val !== null) {
// key is policy ID, val is { assetName: quantity }
for (const [assetName, quantity] of Object.entries(val as Record<string, any>)) {
const unit = `${key}${assetName === '' ? '' : assetName}`;
assets[unit] = BigInt(quantity);
}
}
}
} else if (typeof value === 'number' || typeof value === 'string') {
assets['lovelace'] = BigInt(value);
}
// Handle address
const address = output.address ?? '';
// Handle datum
let datumHash: string | null = null;
let datum: string | null = null;
if (output.datumHash) {
datumHash = output.datumHash;
}
if (output.datum) {
if (typeof output.datum === 'string') {
datum = output.datum;
} else if (output.datum.hash) {
datumHash = output.datum.hash;
} else if (output.datum.value || output.datum.bytes) {
datum = output.datum.value || output.datum.bytes;
}
}
// Handle script reference
let scriptRef: any = null;
if (output.script) {
const script = output.script;
if (script['plutus:v2']) {
scriptRef = { type: 'PlutusV2', script: script['plutus:v2'] };
} else if (script['plutus:v1']) {
scriptRef = { type: 'PlutusV1', script: script['plutus:v1'] };
} else if (script['plutus:v3']) {
scriptRef = { type: 'PlutusV3', script: script['plutus:v3'] };
} else if (script.native) {
scriptRef = { type: 'Native', script: JSON.stringify(script.native) };
}
}
return {
txHash,
outputIndex,
assets,
address,
datumHash,
datum,
scriptRef,
} as UTxO;
});
}
/**
* Convert a Koios UTxO response to Lucid format
*/
private koiosUtxoToLucid(koiosUtxo: any): UTxO {
const assets: Record<string, bigint> = {};
assets['lovelace'] = BigInt(koiosUtxo.value || 0);
if (koiosUtxo.asset_list) {
for (const asset of koiosUtxo.asset_list) {
const unit = `${asset.policy_id}${asset.asset_name || ''}`;
assets[unit] = BigInt(asset.quantity || 0);
}
}
return {
txHash: koiosUtxo.tx_hash,
outputIndex: koiosUtxo.tx_index,
assets,
address: koiosUtxo.address,
datumHash: koiosUtxo.datum_hash || null,
datum: koiosUtxo.inline_datum?.bytes || null,
scriptRef: null,
} as UTxO;
}
}

85
src/vector/safety.ts Normal file
View File

@@ -0,0 +1,85 @@
import type { SpendLimits, SpendStatus, AuditEntry } from './types.js';
const VECTOR_SPEND_LIMIT_PER_TX = parseInt(process.env.VECTOR_SPEND_LIMIT_PER_TX || '100000000'); // 100 ADA
const VECTOR_SPEND_LIMIT_DAILY = parseInt(process.env.VECTOR_SPEND_LIMIT_DAILY || '500000000'); // 500 ADA
export class SafetyLayer {
private dailySpent: number = 0;
private dailyResetTime: number;
private auditLog: AuditEntry[] = [];
private limits: SpendLimits;
constructor() {
this.limits = {
perTransaction: VECTOR_SPEND_LIMIT_PER_TX,
daily: VECTOR_SPEND_LIMIT_DAILY,
};
// Reset daily limit at midnight UTC
this.dailyResetTime = this.getNextMidnightUTC();
}
private getNextMidnightUTC(): number {
const now = new Date();
const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
return tomorrow.getTime();
}
private checkAndResetDaily(): void {
if (Date.now() >= this.dailyResetTime) {
this.dailySpent = 0;
this.dailyResetTime = this.getNextMidnightUTC();
}
}
checkTransaction(amountLovelace: number): { allowed: boolean; reason?: string } {
this.checkAndResetDaily();
if (amountLovelace > this.limits.perTransaction) {
return {
allowed: false,
reason: `Transaction amount ${(amountLovelace / 1_000_000).toFixed(6)} ADA exceeds per-transaction limit of ${(this.limits.perTransaction / 1_000_000).toFixed(6)} ADA`,
};
}
if (this.dailySpent + amountLovelace > this.limits.daily) {
const remaining = this.limits.daily - this.dailySpent;
return {
allowed: false,
reason: `Transaction would exceed daily spend limit. Daily limit: ${(this.limits.daily / 1_000_000).toFixed(6)} ADA, already spent: ${(this.dailySpent / 1_000_000).toFixed(6)} ADA, remaining: ${(remaining / 1_000_000).toFixed(6)} ADA`,
};
}
return { allowed: true };
}
recordTransaction(txHash: string, amountLovelace: number, recipient: string): void {
this.checkAndResetDaily();
this.dailySpent += amountLovelace;
this.auditLog.push({
timestamp: new Date().toISOString(),
txHash,
amountLovelace,
recipient,
action: 'send',
});
}
getSpendStatus(): SpendStatus {
this.checkAndResetDaily();
return {
perTransactionLimit: this.limits.perTransaction,
dailyLimit: this.limits.daily,
dailySpent: this.dailySpent,
dailyRemaining: Math.max(0, this.limits.daily - this.dailySpent),
resetTime: new Date(this.dailyResetTime).toISOString(),
};
}
getAuditLog(): AuditEntry[] {
return [...this.auditLog];
}
}
// Singleton instance
export const safetyLayer = new SafetyLayer();

60
src/vector/types.ts Normal file
View File

@@ -0,0 +1,60 @@
// Vector-specific type definitions
export interface VectorToken {
unit: string;
name: string;
quantity: string;
}
export interface VectorWalletInfo {
address: string;
utxoCount: number;
ada: string;
tokens: VectorToken[];
}
export interface VectorAdaTransactionResult {
txHash: string;
senderAddress: string;
recipientAddress: string;
amount: number;
links: {
explorer: string;
};
}
export interface VectorTokenTransactionResult {
txHash: string;
senderAddress: string;
recipientAddress: string;
token: {
policyId: string;
name: string;
amount: string;
};
ada: string;
links: {
explorer: string;
};
}
export interface SpendLimits {
perTransaction: number; // lovelace
daily: number; // lovelace
}
export interface SpendStatus {
perTransactionLimit: number;
dailyLimit: number;
dailySpent: number;
dailyRemaining: number;
resetTime: string;
}
export interface AuditEntry {
timestamp: string;
txHash: string;
amountLovelace: number;
recipient: string;
action: string;
}

602
src/vector/vector.ts Normal file
View File

@@ -0,0 +1,602 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Lucid, fromText } from 'lucid-cardano';
import * as dotenv from 'dotenv';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
import { OgmiosProvider } from './ogmios-provider.js';
import { safetyLayer } from './safety.js';
import type {
VectorToken,
VectorWalletInfo,
VectorAdaTransactionResult,
VectorTokenTransactionResult,
} from './types.js';
// Direct .env loading
const __filename = fileURLToPath(import.meta.url);
const projectRoot = resolve(__filename, '../../../..');
const envPath = resolve(projectRoot, '.env');
if (existsSync(envPath)) {
const result = dotenv.config({ path: envPath });
if (result.error) {
console.error('Error loading .env file:', result.error);
}
}
// Configuration from environment variables
const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vector.testnet.apexfusion.org';
const VECTOR_SUBMIT_URL = process.env.VECTOR_SUBMIT_URL || 'https://submit.vector.testnet.apexfusion.org/api/submit/tx';
const VECTOR_KOIOS_URL = process.env.VECTOR_KOIOS_URL || 'https://koios.vector.testnet.apexfusion.org/';
const VECTOR_EXPLORER_URL = process.env.VECTOR_EXPLORER_URL || 'https://vector.testnet.apexscan.org';
const VECTOR_MNEMONIC = process.env.VECTOR_MNEMONIC || '';
const VECTOR_ACCOUNT_INDEX = parseInt(process.env.VECTOR_ACCOUNT_INDEX || '0');
// Helper function to format ADA amounts
function lovelaceToAda(lovelace: string | number | bigint): string {
return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6);
}
// Helper function to format asset name
function formatAssetName(name: string): string {
try {
if (/^[0-9a-fA-F]+$/.test(name) && name.length > 0) {
return Buffer.from(name, 'hex').toString('utf8');
}
return name;
} catch {
return name;
}
}
// Explorer link helper
function explorerTxLink(txHash: string): string {
return `${VECTOR_EXPLORER_URL}/transaction/${txHash}`;
}
// Initialize Lucid instance with Ogmios provider
async function initLucid() {
const provider = new OgmiosProvider({
ogmiosUrl: VECTOR_OGMIOS_URL,
submitUrl: VECTOR_SUBMIT_URL,
koiosUrl: VECTOR_KOIOS_URL,
});
// Vector uses --mainnet flag, so addresses are addr1... format
const lucid = await Lucid.new(provider, 'Mainnet');
if (!VECTOR_MNEMONIC) {
throw new Error('VECTOR_MNEMONIC is required in .env file');
}
const trimmedMnemonic = VECTOR_MNEMONIC.trim();
const words = trimmedMnemonic.split(/\s+/);
if (words.length !== 15 && words.length !== 24) {
throw new Error(`Invalid mnemonic: Expected 15 or 24 words, got ${words.length}`);
}
lucid.selectWalletFromSeed(trimmedMnemonic, { accountIndex: VECTOR_ACCOUNT_INDEX });
const address = await lucid.wallet.address();
if (!address) {
throw new Error('Failed to derive address from mnemonic');
}
return lucid;
}
// Get wallet info
export async function getWalletInfo(): Promise<VectorWalletInfo> {
const lucid = await initLucid();
const address = await lucid.wallet.address();
const utxos = await lucid.wallet.getUtxos();
let adaBalance = '0';
let tokenBalances: VectorToken[] = [];
if (utxos.length > 0) {
// Aggregate all UTxO assets
const aggregated: Record<string, bigint> = {};
for (const utxo of utxos) {
for (const [unit, qty] of Object.entries(utxo.assets)) {
aggregated[unit] = (aggregated[unit] || 0n) + BigInt(qty);
}
}
adaBalance = aggregated['lovelace'] ? lovelaceToAda(aggregated['lovelace']) : '0';
for (const [unit, quantity] of Object.entries(aggregated)) {
if (unit === 'lovelace') continue;
try {
const policyId = unit.slice(0, 56);
const assetNameHex = unit.slice(56);
const displayName = assetNameHex
? formatAssetName(assetNameHex)
: `${policyId.substring(0, 8)}...`;
tokenBalances.push({
unit,
name: displayName,
quantity: quantity.toString(),
});
} catch {
tokenBalances.push({
unit,
name: unit,
quantity: quantity.toString(),
});
}
}
}
return {
address,
utxoCount: utxos.length,
ada: adaBalance,
tokens: tokenBalances,
};
}
// Send ADA transaction
export async function sendAda(
recipientAddress: string,
amountAda: number,
metadata: any = null
): Promise<VectorAdaTransactionResult> {
if (!recipientAddress) {
throw new Error('Recipient address is required');
}
if (typeof amountAda !== 'number' || amountAda <= 0) {
throw new Error('Amount must be a positive number');
}
const lovelaceAmount = Math.floor(amountAda * 1_000_000);
// Safety check
const safetyCheck = safetyLayer.checkTransaction(lovelaceAmount);
if (!safetyCheck.allowed) {
throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`);
}
const lucid = await initLucid();
const senderAddress = await lucid.wallet.address();
// Validate recipient address
try {
// @ts-ignore
lucid.utils.getAddressDetails(recipientAddress);
} catch (error) {
throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`);
}
// Build transaction
// @ts-ignore
let tx = lucid.newTx()
.payToAddress(recipientAddress, { lovelace: BigInt(lovelaceAmount) });
if (metadata) {
const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata;
// @ts-ignore
tx = tx.attachMetadata(674, parsedMetadata);
}
// @ts-ignore
tx = await tx.complete();
// @ts-ignore
const signedTx = await tx.sign().complete();
const txHash = await signedTx.submit();
// Record in safety layer
safetyLayer.recordTransaction(txHash, lovelaceAmount, recipientAddress);
return {
txHash,
senderAddress,
recipientAddress,
amount: amountAda,
links: {
explorer: explorerTxLink(txHash),
},
};
}
// Send tokens transaction
export async function sendTokens(
recipientAddress: string,
policyId: string,
assetName: string,
amount: string,
adaAmount: number | null = null
): Promise<VectorTokenTransactionResult> {
if (!recipientAddress) throw new Error('Recipient address is required');
if (!policyId) throw new Error('Policy ID is required');
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
throw new Error('Amount must be a positive number');
}
// Safety check on ADA portion
const adaLovelace = adaAmount ? Math.floor(adaAmount * 1_000_000) : 2_000_000; // min ~2 ADA
const safetyCheck = safetyLayer.checkTransaction(adaLovelace);
if (!safetyCheck.allowed) {
throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`);
}
const lucid = await initLucid();
const senderAddress = await lucid.wallet.address();
try {
// @ts-ignore
lucid.utils.getAddressDetails(recipientAddress);
} catch (error) {
throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`);
}
let assetNameHex = assetName;
if (assetName && !/^[0-9a-fA-F]+$/.test(assetName)) {
assetNameHex = fromText(assetName);
}
const unit = `${policyId}${assetNameHex}`;
const assets = { [unit]: BigInt(amount) };
const outputLovelace = adaAmount
? BigInt(Math.floor(adaAmount * 1_000_000))
: BigInt(2_000_000); // Default min ADA
// @ts-ignore
let tx = lucid.newTx()
.payToAddress(recipientAddress, {
lovelace: outputLovelace,
...assets,
});
// @ts-ignore
tx = await tx.complete();
// @ts-ignore
const signedTx = await tx.sign().complete();
const txHash = await signedTx.submit();
safetyLayer.recordTransaction(txHash, Number(outputLovelace), recipientAddress);
const displayAssetName = assetName ? formatAssetName(assetNameHex) : '';
return {
txHash,
senderAddress,
recipientAddress,
token: {
policyId,
name: displayAssetName,
amount,
},
ada: lovelaceToAda(outputLovelace),
links: {
explorer: explorerTxLink(txHash),
},
};
}
// Register all Vector MCP tools
export function registerVectorTools(server: McpServer) {
// vector_get_balance — Get balance for any address
// @ts-ignore: MCP SDK deep type instantiation
server.tool(
"vector_get_balance",
"Get ADA and token balances for a Vector address",
{
address: z.string().describe("Vector address to check (addr1...)"),
},
async ({ address }) => {
try {
const provider = new OgmiosProvider({
ogmiosUrl: VECTOR_OGMIOS_URL,
submitUrl: VECTOR_SUBMIT_URL,
koiosUrl: VECTOR_KOIOS_URL,
});
const utxos = await provider.getUtxos(address);
// Aggregate all assets
const tokenMap = new Map<string, bigint>();
for (const utxo of utxos) {
for (const [unit, qty] of Object.entries(utxo.assets)) {
const current = tokenMap.get(unit) || 0n;
tokenMap.set(unit, current + BigInt(qty));
}
}
const adaBalance = tokenMap.get('lovelace') || 0n;
tokenMap.delete('lovelace');
const tokens = Array.from(tokenMap.entries()).map(([unit, quantity]) => {
const policyId = unit.slice(0, 56);
const assetNameHex = unit.slice(56);
return {
unit,
policyId,
assetName: formatAssetName(assetNameHex),
quantity: quantity.toString(),
};
});
tokens.sort((a, b) => (BigInt(b.quantity) - BigInt(a.quantity)) > 0n ? 1 : -1);
const tokenList = tokens.length > 0
? tokens.map(t => `${t.quantity} ${t.assetName || t.unit} (Policy: ${t.policyId.substring(0, 8)}...)`).join('\n')
: 'No tokens found';
return {
content: [{
type: "text",
text: `Vector Address Balance for ${address}:
ADA Balance: ${lovelaceToAda(adaBalance)} ADA
UTxO Count: ${utxos.length}
${tokens.length > 0 ? `Token Holdings (${tokens.length}):\n${tokenList}` : 'No token holdings found'}`,
}],
};
} catch (err) {
const error = err as Error;
return {
content: [{
type: "text",
text: `Failed to retrieve Vector address balance: ${error.message}`,
}],
};
}
}
);
// vector_get_address — Get the agent's wallet address and balance
// @ts-ignore: MCP SDK deep type instantiation
server.tool(
"vector_get_address",
"Get the agent's Vector wallet address, balance, and token holdings",
{},
async () => {
try {
const walletInfo = await getWalletInfo();
const tokenList = walletInfo.tokens.length > 0
? walletInfo.tokens.map((t: VectorToken) => `${t.quantity} ${t.name}`).join('\n')
: 'No tokens found';
return {
content: [{
type: "text",
text: `# Vector Wallet Information
Address: ${walletInfo.address}
ADA Balance: ${walletInfo.ada} ADA
UTXO Count: ${walletInfo.utxoCount}
${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length}):\n${tokenList}` : 'No token holdings found'}`,
}],
};
} catch (err) {
const error = err as Error;
return {
content: [{
type: "text",
text: `Failed to get wallet information: ${error.message}
**Troubleshooting Tips:**
1. Make sure you have a valid 15 or 24-word mnemonic in VECTOR_MNEMONIC
2. Verify the Ogmios endpoint is reachable: ${VECTOR_OGMIOS_URL}
3. Check the console logs for detailed error information`,
}],
};
}
}
);
// vector_get_utxos — List UTxOs for an address or the wallet
// @ts-ignore: MCP SDK deep type instantiation
server.tool(
"vector_get_utxos",
"List unspent transaction outputs (UTxOs) for a Vector address or the agent's wallet",
{
address: z.string().optional().describe("Vector address to query UTxOs for. If omitted, uses the agent's wallet."),
},
async ({ address }) => {
try {
let utxos;
let queryAddress: string;
if (address) {
const provider = new OgmiosProvider({
ogmiosUrl: VECTOR_OGMIOS_URL,
submitUrl: VECTOR_SUBMIT_URL,
koiosUrl: VECTOR_KOIOS_URL,
});
utxos = await provider.getUtxos(address);
queryAddress = address;
} else {
const lucid = await initLucid();
queryAddress = await lucid.wallet.address();
utxos = await lucid.wallet.getUtxos();
}
if (utxos.length === 0) {
return {
content: [{
type: "text",
text: `No UTxOs found for ${queryAddress}`,
}],
};
}
const utxoList = utxos.map((utxo, i) => {
const ada = utxo.assets['lovelace'] ? lovelaceToAda(utxo.assets['lovelace']) : '0';
const tokenCount = Object.keys(utxo.assets).filter(k => k !== 'lovelace').length;
return `${i + 1}. ${utxo.txHash}#${utxo.outputIndex} — ${ada} ADA${tokenCount > 0 ? ` + ${tokenCount} token(s)` : ''}`;
}).join('\n');
return {
content: [{
type: "text",
text: `# UTxOs for ${queryAddress}
Total: ${utxos.length} UTxO(s)
${utxoList}`,
}],
};
} catch (err) {
const error = err as Error;
return {
content: [{
type: "text",
text: `Failed to get UTxOs: ${error.message}`,
}],
};
}
}
);
// vector_send_ada — Send ADA with safety limits
// @ts-ignore: MCP SDK deep type instantiation
server.tool(
"vector_send_ada",
"Send ADA from the agent's wallet to a recipient address (respects spend limits)",
{
recipientAddress: z.string().describe("Recipient Vector address (addr1...)"),
amount: z.number().min(1).describe("Amount of ADA to send"),
metadata: z.string().optional().describe("Optional transaction metadata in JSON format"),
},
async ({ recipientAddress, amount, metadata }) => {
try {
const result = await sendAda(recipientAddress, amount, metadata);
return {
content: [{
type: "text",
text: `# ADA Transaction Successful
Transaction Hash: ${result.txHash}
From: ${result.senderAddress}
To: ${result.recipientAddress}
Amount: ${result.amount} ADA
[View on Explorer](${result.links.explorer})`,
}],
};
} catch (err) {
const error = err as Error;
return {
content: [{
type: "text",
text: `Failed to send ADA: ${error.message}
**Troubleshooting Tips:**
1. Check that your wallet has sufficient balance
2. Verify the recipient address is correct (addr1...)
3. Check spend limits with vector_get_spend_limits
4. Verify the Ogmios endpoint is reachable`,
}],
};
}
}
);
// vector_send_tokens — Send native tokens with safety limits
// @ts-ignore: MCP SDK deep type instantiation
server.tool(
"vector_send_tokens",
"Send Vector native tokens from the agent's wallet to a recipient address",
{
recipientAddress: z.string().describe("Recipient Vector address (addr1...)"),
policyId: z.string().describe("Token policy ID"),
assetName: z.string().describe("Asset name (can be empty for policy-only tokens)"),
amount: z.string().describe("Amount of tokens to send"),
adaAmount: z.number().optional().describe("Optional ADA to include (uses minimum required if not specified)"),
},
async ({ recipientAddress, policyId, assetName, amount, adaAmount }) => {
try {
const result = await sendTokens(recipientAddress, policyId, assetName, amount, adaAmount);
return {
content: [{
type: "text",
text: `# Token Transaction Successful
Transaction Hash: ${result.txHash}
From: ${result.senderAddress}
To: ${result.recipientAddress}
Token Details:
- Policy ID: ${result.token.policyId}
- Asset Name: ${result.token.name || '(none)'}
- Amount: ${result.token.amount}
Included ADA: ${result.ada} ADA
[View on Explorer](${result.links.explorer})`,
}],
};
} catch (err) {
const error = err as Error;
return {
content: [{
type: "text",
text: `Failed to send tokens: ${error.message}
**Troubleshooting Tips:**
1. Check that your wallet has sufficient ADA and token balance
2. Verify the policy ID and asset name are correct
3. Verify the recipient address is correct (addr1...)
4. Check spend limits with vector_get_spend_limits`,
}],
};
}
}
);
// vector_get_spend_limits — Check safety layer status
// @ts-ignore: MCP SDK deep type instantiation
server.tool(
"vector_get_spend_limits",
"Check current spend limits and remaining daily budget",
{},
async () => {
try {
const status = safetyLayer.getSpendStatus();
const log = safetyLayer.getAuditLog();
const recentTxs = log.slice(-5).reverse().map((entry) =>
`- ${entry.timestamp}: ${lovelaceToAda(entry.amountLovelace)} ADA → ${entry.recipient.substring(0, 20)}... (${entry.txHash.substring(0, 16)}...)`
).join('\n');
return {
content: [{
type: "text",
text: `# Vector Spend Limits
Per-Transaction Limit: ${lovelaceToAda(status.perTransactionLimit)} ADA
Daily Limit: ${lovelaceToAda(status.dailyLimit)} ADA
Daily Spent: ${lovelaceToAda(status.dailySpent)} ADA
Daily Remaining: ${lovelaceToAda(status.dailyRemaining)} ADA
Resets At: ${status.resetTime}
${log.length > 0 ? `## Recent Transactions (last ${Math.min(5, log.length)}):\n${recentTxs}` : 'No transactions recorded yet.'}`,
}],
};
} catch (err) {
const error = err as Error;
return {
content: [{
type: "text",
text: `Failed to get spend limits: ${error.message}`,
}],
};
}
}
);
}