Web3 MCP Server

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}` }], } } } ) }