ripple support

This commit is contained in:
Alex Cheng
2025-03-11 13:45:37 -05:00
parent 5cca76eb6a
commit 7135d5af5a
8 changed files with 579 additions and 12 deletions

View File

@@ -9,12 +9,17 @@ POLYGON_RPC_URL=https://polygon-rpc.com
AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
BERACHAIN_RPC_URL=https://rpc.berachain.com
SONIC_RPC_URL=https://rpc.soniclabs.com/
XRP_RPC_URL=https://xrplcluster.com/
# Private keys (DO NOT SHARE)
ETH_PRIVATE_KEY=your-eth-private-key
SOLANA_PRIVATE_KEY=your-solana-private-key
THORCHAIN_PRIVATE_KEY=your-thorchain-private-key
THORCHAIN_MNEMONIC=your-thorchain-mnemonic
# XRP Ledger credentials - can use either private key or mnemonic
XRP_PRIVATE_KEY=your-xrp-private-key # Private key in hex format
XRP_MNEMONIC=your-xrp-mnemonic # Alternative: 12-word recovery phrase
XRP_ADDRESS=your-xrp-address # Optional: Used to verify key derivation
# UTXO Chain API Keys (optional)
BLOCKCYPHER_API_KEY=your-blockcypher-api-key
@@ -28,6 +33,7 @@ ENABLE_LITECOIN_TOOLS=true
ENABLE_DOGECOIN_TOOLS=true
ENABLE_BITCOINCASH_TOOLS=true
ENABLE_THORCHAIN_TOOLS=true
ENABLE_RIPPLE_TOOLS=true
# CoinGecko API Key
COINGECKO_API_KEY=CG-Z9ajuvwG1cheNJSByUgmvTgg #replace with your-coingecko-api-key if you hit rate limits

View File

@@ -1,6 +1,6 @@
# Web3 MCP
A Model-Context-Protocol server for interacting with multiple blockchains including Solana, Ethereum, THORChain, and UTXO chains. This server provides simple RPC endpoints for common blockchain operations, allowing secure interactions with various blockchains through environment variables.
A Model-Context-Protocol server for interacting with multiple blockchains including Solana, Ethereum, THORChain, XRP Ledger, and UTXO chains. This server provides simple RPC endpoints for common blockchain operations, allowing secure interactions with various blockchains through environment variables.
<a href="https://glama.ai/mcp/servers/an8x6gmzdn"><img width="380" height="200" src="https://glama.ai/mcp/servers/an8x6gmzdn/badge" alt="Web3 Server MCP server" /></a>
@@ -49,6 +49,15 @@ UTXO Chain Operations:
- Validate addresses
- Get network info and fees
XRP Ledger Operations:
- Check XRP account balances
- View transaction history
- Validate XRP addresses
- Send XRP to another address
- Get current XRP Ledger information
- Check token balances
- Create trustlines for tokens
Supported EVM Networks:
- Ethereum
- Base
@@ -92,6 +101,14 @@ ENABLE_THORCHAIN_TOOLS=true # Enable/disable THORChain tools
# Private Keys (required for transactions)
ETH_PRIVATE_KEY=your-ethereum-private-key
SOLANA_PRIVATE_KEY=your-base58-encoded-solana-private-key
# XRP Ledger credentials (required for XRP transactions)
# Either private key or mnemonic is required
XRP_PRIVATE_KEY=your-xrp-private-key-in-hex
# OR
XRP_MNEMONIC=your-xrp-mnemonic-recovery-phrase
# Optional - used to verify the derived address
XRP_ADDRESS=your-xrp-account-address
```
### Optional Configuration
@@ -112,6 +129,9 @@ POLYGON_RPC_URL=https://polygon-rpc.com
AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
BERACHAIN_RPC_URL=https://rpc.berachain.com
# XRP Ledger
XRP_RPC_URL=https://xrplcluster.com/ # Optional - will use public endpoint if not specified
# THORChain Configuration
THORCHAIN_NODE_URL=https://thornode.ninerealms.com # Optional - will use public endpoint if not specified
THORCHAIN_PRIVATE_KEY=your-thorchain-private-key
@@ -152,6 +172,7 @@ The Web3 MCP server allows you to control which blockchain tools are registered
- `ENABLE_DOGECOIN_TOOLS`: Enable/disable Dogecoin tools
- `ENABLE_BITCOINCASH_TOOLS`: Enable/disable Bitcoin Cash tools
- `ENABLE_THORCHAIN_TOOLS`: Enable/disable THORChain tools
- `ENABLE_RIPPLE_TOOLS`: Enable/disable XRP Ledger 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:
@@ -203,6 +224,15 @@ Ask Claude (or your MCP client of choice):
- "Show me the pool information for BTC.BTC"
- "Get a swap quote for 0.1 BTC.BTC to ETH.ETH"
### XRP Ledger Operations (when ENABLE_RIPPLE_TOOLS=true)
- "What's the XRP balance of rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe?"
- "Show me the transaction history for rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
- "Is rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe a valid XRP address?"
- "Send 10 XRP to rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
- "What's the current XRP Ledger information?"
- "Show me token balances for rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
- "Create a trustline for USD with issuer rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe with a limit of 1000"
## Security Notes
1. **Environment Variables**: All private keys are stored in the .env file and never exposed in the conversation history
@@ -222,6 +252,13 @@ For EVM operations, you can specify the network by name (ethereum, base, arbitru
### THORChain Configuration
The tool uses Nine Realms public endpoints by default, but you can configure a custom THORChain node URL in the .env file for better reliability and rate limits.
### XRP Ledger Configuration
The tool can use either a private key or mnemonic phrase for XRP transactions. Configure these in your .env file:
- `XRP_PRIVATE_KEY`: Your XRP private key in hex format
- `XRP_MNEMONIC`: Alternative to private key - your 12-word recovery phrase
- `XRP_ADDRESS`: Optional - Your XRP account address (used to verify the derived address)
- `XRP_RPC_URL`: Optional - Custom XRP Ledger node URL (defaults to public endpoint)
### UTXO Chain Data Providers
The tool uses several data providers for UTXO chains:
- BlockCypher

View File

@@ -18,11 +18,13 @@
"solana",
"mcp",
"rpc",
"blockchain"
"blockchain",
"xrp",
"ripple"
],
"author": "",
"license": "ISC",
"description": "Solana RPC Model-Context-Protocol Server",
"description": "Blockchain RPC Model-Context-Protocol Server for Solana, EVM chains, UTXO chains, Ripple (XRP) and more",
"repository": {
"type": "git",
"url": "https://github.com/akc2267/web3-mcp.git"
@@ -46,31 +48,33 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@cosmos-client/core": "^0.46.1",
"@jup-ag/api": "^6.0.12",
"@modelcontextprotocol/sdk": "^1.4.1",
"@psf/bitcoincashjs-lib": "^4.0.3",
"@solana/spl-token": "^0.4.1",
"@solana/web3.js": "^1.87.6",
"@xchainjs/xchain-bitcoin": "^1.2.2",
"@xchainjs/xchain-bitcoincash": "^1.0.10",
"@xchainjs/xchain-client": "^1.0.10",
"@xchainjs/xchain-crypto": "^0.3.7",
"@xchainjs/xchain-doge": "^1.0.10",
"@xchainjs/xchain-litecoin": "^1.0.10",
"@xchainjs/xchain-midgard-query": "^1.0.11",
"@xchainjs/xchain-thorchain": "^2.0.13",
"@xchainjs/xchain-thorchain-amm": "^2.0.23",
"@xchainjs/xchain-thorchain-query": "^1.0.13",
"@xchainjs/xchain-midgard-query": "^1.0.11",
"@cosmos-client/core": "^0.46.1",
"@xchainjs/xchain-util": "^1.0.8",
"@psf/bitcoincashjs-lib": "^4.0.3",
"bchaddrjs": "^0.5.2",
"bitcoinjs-lib": "^6.1.5",
"wif": "^4.0.0",
"@jup-ag/api": "^6.0.12",
"@modelcontextprotocol/sdk": "^1.4.1",
"@solana/spl-token": "^0.4.1",
"@solana/web3.js": "^1.87.6",
"bs58": "^5.0.0",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"ethers": "^6.11.1",
"fetch-retry": "^5.0.6",
"ripple-keypairs": "^1.3.1",
"wif": "^4.0.0",
"xrpl": "^2.14.0",
"zod": "^3.24.1"
},
"devDependencies": {

View File

@@ -4,6 +4,7 @@ import { registerLitecoinTools } from "./UTXO/litecoin"
import { registerDogecoinTools } from "./UTXO/dogecoin"
import { registerBitcoinCashTools } from "./UTXO/bitcoincash"
import { registerThorchainTools } from "./thorchain/thorchain"
import { registerRippleTools } from "./ripple/ripple"
// Export all UTXO tools registration function
export function registerUtxoTools(server: McpServer) {
@@ -17,4 +18,5 @@ export function registerUtxoTools(server: McpServer) {
export function registerAllTools(server: McpServer) {
registerUtxoTools(server)
registerThorchainTools(server)
registerRippleTools(server)
}

View File

@@ -0,0 +1 @@
export { registerRippleTools } from './ripple.js';

505
src/chains/ripple/ripple.ts Normal file
View File

@@ -0,0 +1,505 @@
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();
});
}

6
src/env.d.ts vendored
View File

@@ -13,5 +13,11 @@ declare namespace NodeJS {
BSC_RPC_URL: string;
POLYGON_RPC_URL: string;
AVALANCHE_RPC_URL: string;
// Ripple (XRP)
XRP_RPC_URL: string;
XRP_ADDRESS: string;
XRP_PRIVATE_KEY: string;
XRP_PUBLIC_KEY: string;
}
}

View File

@@ -31,6 +31,7 @@ import {
registerBitcoinCashTools,
} from "./chains/UTXO/index.js";
import { registerThorchainTools } from "./chains/thorchain/thorchain.js";
import { registerRippleTools } from "./chains/ripple/ripple.js";
import { registerGeneralTools } from "./general/index.js";
// Create server instance
@@ -82,6 +83,11 @@ if (isEnabled('ENABLE_THORCHAIN_TOOLS')) {
registerThorchainTools(server);
}
if (isEnabled('ENABLE_RIPPLE_TOOLS')) {
console.error('Registering Ripple (XRP) tools...');
registerRippleTools(server);
}
// Register general tools
console.error('Registering general tools...');
registerGeneralTools(server);