refactor
This commit is contained in:
@@ -21,5 +21,9 @@ SOCHAIN_API_KEY=your-sochain-api-key
|
|||||||
|
|
||||||
# Tool Registration Controls (true/false)
|
# Tool Registration Controls (true/false)
|
||||||
ENABLE_SOLANA_TOOLS=true # Enable/disable Solana-specific tools
|
ENABLE_SOLANA_TOOLS=true # Enable/disable Solana-specific tools
|
||||||
ENABLE_ETHEREUM_TOOLS=true # Enable/disable Ethereum and EVM chain tools
|
ENABLE_EVM_TOOLS=true # Enable/disable EVM chain tools (Ethereum, Base, Arbitrum, etc.)
|
||||||
ENABLE_XCHAIN_TOOLS=true # Enable/disable cross-chain tools (THORChain, etc.)
|
ENABLE_BITCOIN_TOOLS=true
|
||||||
|
ENABLE_LITECOIN_TOOLS=true
|
||||||
|
ENABLE_DOGECOIN_TOOLS=true
|
||||||
|
ENABLE_BITCOINCASH_TOOLS=true
|
||||||
|
ENABLE_THORCHAIN_TOOLS=true
|
||||||
45
README.md
45
README.md
@@ -79,9 +79,15 @@ cp .env.example .env
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
# Tool Registration Controls
|
# 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_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)
|
# Private Keys (required for transactions)
|
||||||
ETH_PRIVATE_KEY=your-ethereum-private-key
|
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:
|
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_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:
|
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
|
## Usage Examples
|
||||||
|
|
||||||
Ask Claude:
|
Ask Claude (or your MCP client of choice):
|
||||||
|
|
||||||
### Solana Operations (when ENABLE_SOLANA_TOOLS=true)
|
### Solana Operations (when ENABLE_SOLANA_TOOLS=true)
|
||||||
- "What's my Solana address?" - Shows your address derived from private key in .env
|
- "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?"
|
- "What's the current gas price on Arbitrum?"
|
||||||
- "Send 100 USDC to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Polygon"
|
- "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?"
|
- "What's the RUNE balance of thor13zpdckczd0jvyhwxmrwnpap8gmy9m5kk2gzum3?"
|
||||||
- "Show me the pool information for BTC.BTC"
|
- "Show me the pool information for BTC.BTC"
|
||||||
- "Get a swap quote for 0.1 BTC.BTC to ETH.ETH"
|
- "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
|
## Security Notes
|
||||||
|
|
||||||
|
|||||||
145
src/chains/UTXO/bitcoin.ts
Normal file
145
src/chains/UTXO/bitcoin.ts
Normal 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}` }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
144
src/chains/UTXO/bitcoincash.ts
Normal file
144
src/chains/UTXO/bitcoincash.ts
Normal 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
144
src/chains/UTXO/dogecoin.ts
Normal 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
4
src/chains/UTXO/index.ts
Normal 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
144
src/chains/UTXO/litecoin.ts
Normal 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}` }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ const ERC20_ABI = [
|
|||||||
"function approve(address spender, uint256 amount) returns (bool)"
|
"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
|
// Get native token balance for any EVM network
|
||||||
server.tool(
|
server.tool(
|
||||||
"getEvmBalance",
|
"getEvmBalance",
|
||||||
20
src/chains/index.ts
Normal file
20
src/chains/index.ts
Normal 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
173
src/chains/thorchain.ts
Normal 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}` }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
43
src/index.ts
43
src/index.ts
@@ -23,8 +23,14 @@ if (result.error) {
|
|||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { registerSolanaTools } from "./chains/solana.js";
|
import { registerSolanaTools } from "./chains/solana.js";
|
||||||
import { registerEthereumTools } from "./chains/ethereum.js";
|
import { registerEvmTools } from "./chains/evm/evm.js";
|
||||||
import { registerAllTools } from "./chains/xchain.js";
|
import {
|
||||||
|
registerBitcoinTools,
|
||||||
|
registerLitecoinTools,
|
||||||
|
registerDogecoinTools,
|
||||||
|
registerBitcoinCashTools,
|
||||||
|
} from "./chains/UTXO/index.js";
|
||||||
|
import { registerThorchainTools } from "./chains/thorchain.js";
|
||||||
|
|
||||||
// Create server instance
|
// Create server instance
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
@@ -44,14 +50,35 @@ if (isEnabled('ENABLE_SOLANA_TOOLS')) {
|
|||||||
registerSolanaTools(server);
|
registerSolanaTools(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnabled('ENABLE_ETHEREUM_TOOLS')) {
|
if (isEnabled('ENABLE_EVM_TOOLS')) {
|
||||||
console.error('Registering Ethereum tools...');
|
console.error('Registering EVM chain tools...');
|
||||||
registerEthereumTools(server);
|
registerEvmTools(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnabled('ENABLE_XCHAIN_TOOLS')) {
|
// UTXO Chain Tools
|
||||||
console.error('Registering cross-chain tools...');
|
if (isEnabled('ENABLE_BITCOIN_TOOLS')) {
|
||||||
registerAllTools(server);
|
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() {
|
async function main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user