Web3 MCP Server

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