This commit is contained in:
Alex Cheng
2025-02-19 16:20:54 -06:00
parent 4e25635d60
commit c2f3c4d4e0
12 changed files with 852 additions and 462 deletions

View File

@@ -21,5 +21,9 @@ SOCHAIN_API_KEY=your-sochain-api-key
# Tool Registration Controls (true/false)
ENABLE_SOLANA_TOOLS=true # Enable/disable Solana-specific tools
ENABLE_ETHEREUM_TOOLS=true # Enable/disable Ethereum and EVM chain tools
ENABLE_XCHAIN_TOOLS=true # Enable/disable cross-chain tools (THORChain, etc.)
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

View File

@@ -79,9 +79,15 @@ cp .env.example .env
```env
# Tool Registration Controls
ENABLE_SOLANA_TOOLS=true # Enable/disable Solana-specific tools
ENABLE_SOLANA_TOOLS=true # Enable/disable Solana tools
ENABLE_ETHEREUM_TOOLS=true # Enable/disable Ethereum and EVM chain tools
ENABLE_XCHAIN_TOOLS=true # Enable/disable cross-chain tools (THORChain, etc.)
# 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
# Private Keys (required for transactions)
ETH_PRIVATE_KEY=your-ethereum-private-key
@@ -139,9 +145,13 @@ npm run build
The Web3 MCP server allows you to control which blockchain tools are registered through environment variables:
- `ENABLE_SOLANA_TOOLS`: Enable/disable Solana-specific tools
- `ENABLE_SOLANA_TOOLS`: Enable/disable Solana tools
- `ENABLE_ETHEREUM_TOOLS`: Enable/disable Ethereum and EVM chain tools
- `ENABLE_XCHAIN_TOOLS`: Enable/disable cross-chain tools (THORChain, etc.)
- `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
Set these variables to `true` or `false` in your `.env` file to control which tools are available to the server. This allows you to:
@@ -152,7 +162,7 @@ Set these variables to `true` or `false` in your `.env` file to control which to
## Usage Examples
Ask Claude:
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
@@ -168,13 +178,30 @@ Ask Claude:
- "What's the current gas price on Arbitrum?"
- "Send 100 USDC to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Polygon"
### Cross-chain Operations (when ENABLE_XCHAIN_TOOLS=true)
### 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"
- "What's the current state of the RUNE.RUNE pool?"
- "What's the BTC balance of 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?"
- "Show me the transaction history for bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
## Security Notes

145
src/chains/UTXO/bitcoin.ts Normal file
View File

@@ -0,0 +1,145 @@
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

@@ -0,0 +1,144 @@
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}` }],
}
}
}
)
}

144
src/chains/UTXO/dogecoin.ts Normal file
View File

@@ -0,0 +1,144 @@
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}` }],
}
}
}
)
}

4
src/chains/UTXO/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export { registerBitcoinTools } from './bitcoin.js';
export { registerLitecoinTools } from './litecoin.js';
export { registerDogecoinTools } from './dogecoin.js';
export { registerBitcoinCashTools } from './bitcoincash.js';

144
src/chains/UTXO/litecoin.ts Normal file
View File

@@ -0,0 +1,144 @@
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

@@ -85,7 +85,7 @@ const ERC20_ABI = [
"function approve(address spender, uint256 amount) returns (bool)"
];
export function registerEthereumTools(server: McpServer) {
export function registerEvmTools(server: McpServer) {
// Get native token balance for any EVM network
server.tool(
"getEvmBalance",

20
src/chains/index.ts Normal file
View File

@@ -0,0 +1,20 @@
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 { registerThorchainTools } from "./thorchain"
// Export all UTXO tools registration function
export function registerUtxoTools(server: McpServer) {
registerBitcoinTools(server)
registerLitecoinTools(server)
registerDogecoinTools(server)
registerBitcoinCashTools(server)
}
// Export all tools registration function
export function registerAllTools(server: McpServer) {
registerUtxoTools(server)
registerThorchainTools(server)
}

173
src/chains/thorchain.ts Normal file
View File

@@ -0,0 +1,173 @@
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,442 +0,0 @@
import { Client as BitcoinClient, defaultBTCParams, BlockcypherDataProviders as BTCBlockcypherDataProviders } from '@xchainjs/xchain-bitcoin'
import { Client as LitecoinClient, defaultLtcParams } from '@xchainjs/xchain-litecoin'
import { Client as DogeClient, defaultDogeParams } from '@xchainjs/xchain-doge'
import { Client as BitcoinCashClient, defaultBchParams } from '@xchainjs/xchain-bitcoincash'
import { Network } from '@xchainjs/xchain-client'
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 { Asset, Chain, CryptoAmount, assetFromString, assetToString, baseAmount } 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'
config()
// Initialize Nine Realms headers
import axios from 'axios'
import { register9Rheader } from '@xchainjs/xchain-util'
import fetch from 'cross-fetch'
register9Rheader(axios)
// Initialize clients with mainnet by default
const bitcoinClient = new BitcoinClient({
...defaultBTCParams,
network: Network.Mainnet,
dataProviders: [BTCBlockcypherDataProviders],
})
const litecoinClient = new LitecoinClient({
...defaultLtcParams,
network: Network.Mainnet,
})
const dogeClient = new DogeClient({
...defaultDogeParams,
network: Network.Mainnet,
})
// 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)
const bitcoinCashClient = new BitcoinCashClient({
...defaultBchParams,
network: Network.Mainnet,
})
// Helper function to format base amounts for all UTXO chains
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}` }],
}
}
}
)
}
export function registerUtxoTools(server: McpServer) {
// Bitcoin Tools
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}` }],
}
}
}
)
// Litecoin Tools
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}` }],
}
}
}
)
// Dogecoin Tools
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}` }],
}
}
}
)
// Bitcoin Cash Tools
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}` }],
}
}
}
)
// Common Tools for all UTXO chains
// Transaction History
const getTransactionHistory = async (client: any, address: string, chain: string, limit = 10, offset = 0) => {
try {
const txs = await client.getTransactions({ address, limit, offset })
const txList = txs.txs.map((tx: any) => {
const fromAmount = tx.from[0]?.amount ? `${formatBaseAmount(tx.from[0].amount)} ${chain}` : 'Unknown'
const toAmount = tx.to[0]?.amount ? `${formatBaseAmount(tx.to[0].amount)} ${chain}` : '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 { success: true, content: txList }
} catch (err) {
const error = err as Error
return { success: false, error: error.message }
}
}
// Register history tools for each chain
const chains = [
{ name: 'Bitcoin', symbol: 'BTC', client: bitcoinClient },
{ name: 'Litecoin', symbol: 'LTC', client: litecoinClient },
{ name: 'Dogecoin', symbol: 'DOGE', client: dogeClient },
{ name: 'Bitcoin Cash', symbol: 'BCH', client: bitcoinCashClient },
]
chains.forEach(({ name, symbol, client }) => {
server.tool(
`get${symbol}TransactionHistory`,
`Get transaction history for a ${name} address`,
{
address: z.string().describe(`${name} 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 }) => {
const result = await getTransactionHistory(client, address, symbol, limit, offset)
return {
content: [
{
type: "text",
text: result.success
? `${name} Transaction History for ${address}:\n${result.content}`
: `Failed to retrieve ${name} transaction history: ${result.error}`,
},
],
}
}
)
// Add address validation
server.tool(
`validate${symbol}Address`,
`Validate a ${name} address format`,
{
address: z.string().describe(`${name} address to validate`),
},
async ({ address }) => {
try {
const isValid = client.validateAddress(address)
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} is a valid ${name} address`
: `The address ${address} is NOT a valid ${name} address`,
},
],
}
} catch (err) {
const error = err as Error
return {
content: [
{
type: "text",
text: `Error validating ${name} address: ${error.message}`,
},
],
}
}
}
)
// Add network info
server.tool(
`get${symbol}NetworkInfo`,
`Get current ${name} network information`,
{},
async () => {
try {
const fees = await client.getFeeRates()
return {
content: [
{
type: "text",
text: `${name} Network Information:
Current Network: ${client.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 ${name} network information: ${error.message}`,
},
],
}
}
}
)
})
}
// Export both tool registration functions
export function registerAllTools(server: McpServer) {
registerUtxoTools(server)
registerThorchainTools(server)
}

View File

@@ -23,8 +23,14 @@ if (result.error) {
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerSolanaTools } from "./chains/solana.js";
import { registerEthereumTools } from "./chains/ethereum.js";
import { registerAllTools } from "./chains/xchain.js";
import { registerEvmTools } from "./chains/evm/evm.js";
import {
registerBitcoinTools,
registerLitecoinTools,
registerDogecoinTools,
registerBitcoinCashTools,
} from "./chains/UTXO/index.js";
import { registerThorchainTools } from "./chains/thorchain.js";
// Create server instance
const server = new McpServer({
@@ -44,14 +50,35 @@ if (isEnabled('ENABLE_SOLANA_TOOLS')) {
registerSolanaTools(server);
}
if (isEnabled('ENABLE_ETHEREUM_TOOLS')) {
console.error('Registering Ethereum tools...');
registerEthereumTools(server);
if (isEnabled('ENABLE_EVM_TOOLS')) {
console.error('Registering EVM chain tools...');
registerEvmTools(server);
}
if (isEnabled('ENABLE_XCHAIN_TOOLS')) {
console.error('Registering cross-chain tools...');
registerAllTools(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_THORCHAIN_TOOLS')) {
console.error('Registering THORChain tools...');
registerThorchainTools(server);
}
async function main() {