From c2f3c4d4e0d50069f230067713f1e2d210e5cfde Mon Sep 17 00:00:00 2001 From: Alex Cheng Date: Wed, 19 Feb 2025 16:20:54 -0600 Subject: [PATCH] refactor --- .env.example | 8 +- README.md | 45 ++- src/chains/UTXO/bitcoin.ts | 145 ++++++++ src/chains/UTXO/bitcoincash.ts | 144 ++++++++ src/chains/UTXO/dogecoin.ts | 144 ++++++++ src/chains/UTXO/index.ts | 4 + src/chains/UTXO/litecoin.ts | 144 ++++++++ src/chains/{ethereum.ts => evm/evm.ts} | 2 +- src/chains/index.ts | 20 ++ src/chains/thorchain.ts | 173 ++++++++++ src/chains/xchain.ts | 442 ------------------------- src/index.ts | 43 ++- 12 files changed, 852 insertions(+), 462 deletions(-) create mode 100644 src/chains/UTXO/bitcoin.ts create mode 100644 src/chains/UTXO/bitcoincash.ts create mode 100644 src/chains/UTXO/dogecoin.ts create mode 100644 src/chains/UTXO/index.ts create mode 100644 src/chains/UTXO/litecoin.ts rename src/chains/{ethereum.ts => evm/evm.ts} (99%) create mode 100644 src/chains/index.ts create mode 100644 src/chains/thorchain.ts delete mode 100644 src/chains/xchain.ts diff --git a/.env.example b/.env.example index 8820aaa..c518490 100644 --- a/.env.example +++ b/.env.example @@ -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.) \ No newline at end of file +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 \ No newline at end of file diff --git a/README.md b/README.md index 65cf049..d465962 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/chains/UTXO/bitcoin.ts b/src/chains/UTXO/bitcoin.ts new file mode 100644 index 0000000..b04d19f --- /dev/null +++ b/src/chains/UTXO/bitcoin.ts @@ -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}` }], + } + } + } + ) +} \ No newline at end of file diff --git a/src/chains/UTXO/bitcoincash.ts b/src/chains/UTXO/bitcoincash.ts new file mode 100644 index 0000000..6c727c3 --- /dev/null +++ b/src/chains/UTXO/bitcoincash.ts @@ -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}` }], + } + } + } + ) +} \ No newline at end of file diff --git a/src/chains/UTXO/dogecoin.ts b/src/chains/UTXO/dogecoin.ts new file mode 100644 index 0000000..40d51be --- /dev/null +++ b/src/chains/UTXO/dogecoin.ts @@ -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}` }], + } + } + } + ) +} \ No newline at end of file diff --git a/src/chains/UTXO/index.ts b/src/chains/UTXO/index.ts new file mode 100644 index 0000000..172bcaa --- /dev/null +++ b/src/chains/UTXO/index.ts @@ -0,0 +1,4 @@ +export { registerBitcoinTools } from './bitcoin.js'; +export { registerLitecoinTools } from './litecoin.js'; +export { registerDogecoinTools } from './dogecoin.js'; +export { registerBitcoinCashTools } from './bitcoincash.js'; \ No newline at end of file diff --git a/src/chains/UTXO/litecoin.ts b/src/chains/UTXO/litecoin.ts new file mode 100644 index 0000000..0d38175 --- /dev/null +++ b/src/chains/UTXO/litecoin.ts @@ -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}` }], + } + } + } + ) +} \ No newline at end of file diff --git a/src/chains/ethereum.ts b/src/chains/evm/evm.ts similarity index 99% rename from src/chains/ethereum.ts rename to src/chains/evm/evm.ts index 9d8ef14..cdc63ff 100644 --- a/src/chains/ethereum.ts +++ b/src/chains/evm/evm.ts @@ -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", diff --git a/src/chains/index.ts b/src/chains/index.ts new file mode 100644 index 0000000..d7c6291 --- /dev/null +++ b/src/chains/index.ts @@ -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) +} \ No newline at end of file diff --git a/src/chains/thorchain.ts b/src/chains/thorchain.ts new file mode 100644 index 0000000..83b18e4 --- /dev/null +++ b/src/chains/thorchain.ts @@ -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}` }], + } + } + } + ) +} \ No newline at end of file diff --git a/src/chains/xchain.ts b/src/chains/xchain.ts deleted file mode 100644 index 4150f91..0000000 --- a/src/chains/xchain.ts +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a2610f9..a4edb90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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() {