TON support

This commit is contained in:
Alex Cheng
2025-03-18 13:54:58 -05:00
parent 9b86ce4a74
commit 1b9e5ce23c
7 changed files with 514 additions and 3 deletions

View File

@@ -10,6 +10,8 @@ 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/
TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC
TON_API_KEY=your-toncenter-api-key
# Private keys (DO NOT SHARE)
ETH_PRIVATE_KEY=your-eth-private-key
@@ -21,6 +23,10 @@ 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
# TON Configuration
TON_MNEMONIC=word1 word2 word3 ... word24 # 24-word recovery phrase
TON_ADDRESS=your-ton-address # Your TON wallet address
# Cardano Configuration
BLOCKFROST_API_KEY=your-blockfrost-api-key # Register at https://blockfrost.io/
CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod'
@@ -41,6 +47,7 @@ ENABLE_BITCOINCASH_TOOLS=true
ENABLE_THORCHAIN_TOOLS=true
ENABLE_RIPPLE_TOOLS=true
ENABLE_CARDANO_TOOLS=true # Enable/disable Cardano tools
ENABLE_TON_TOOLS=true # Enable/disable TON tools
# 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, XRP Ledger, Cardano, 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, TON (The Open Network), Cardano, 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>
@@ -67,6 +67,13 @@ XRP Ledger Operations:
- Check token balances
- Create trustlines for tokens
TON (The Open Network) Operations:
- Get TON account balances
- View transaction history
- Validate TON addresses
- Send TON to another address with optional memo/comment
- Get current TON network information
Supported EVM Networks:
- Ethereum
- Base
@@ -107,6 +114,8 @@ 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
ENABLE_RIPPLE_TOOLS=true # Enable/disable XRP Ledger tools
ENABLE_TON_TOOLS=true # Enable/disable TON tools
# Private Keys (required for transactions)
ETH_PRIVATE_KEY=your-ethereum-private-key
@@ -120,6 +129,12 @@ XRP_MNEMONIC=your-xrp-mnemonic-recovery-phrase
# Optional - used to verify the derived address
XRP_ADDRESS=your-xrp-account-address
# TON Configuration
TON_MNEMONIC=word1 word2 word3 ... word24 # 24-word recovery phrase for TON wallet
TON_ADDRESS=your-ton-wallet-address # Your TON wallet address
TON_API_KEY=your-toncenter-api-key # Get from @tonapibot on Telegram (optional but recommended)
TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC # Optional - default is TON Center API
# Cardano Configuration
BLOCKFROST_API_KEY=your-blockfrost-api-key # Get a real API key from https://blockfrost.io/
CARDANO_NETWORK=mainnet # or 'testnet', 'preview', 'preprod'
@@ -148,6 +163,10 @@ BERACHAIN_RPC_URL=https://rpc.berachain.com
# XRP Ledger
XRP_RPC_URL=https://xrplcluster.com/ # Optional - will use public endpoint if not specified
# TON Network
TON_RPC_URL=https://toncenter.com/api/v2/jsonRPC # Optional - default is TON Center API
TON_API_KEY=your-toncenter-api-key # Get from @tonapibot on Telegram (optional but recommended)
# THORChain Configuration
THORCHAIN_NODE_URL=https://thornode.ninerealms.com # Optional - will use public endpoint if not specified
THORCHAIN_PRIVATE_KEY=your-thorchain-private-key
@@ -190,6 +209,7 @@ The Web3 MCP server allows you to control which blockchain tools are registered
- `ENABLE_THORCHAIN_TOOLS`: Enable/disable THORChain tools
- `ENABLE_RIPPLE_TOOLS`: Enable/disable XRP Ledger tools
- `ENABLE_CARDANO_TOOLS`: Enable/disable Cardano tools
- `ENABLE_TON_TOOLS`: Enable/disable TON 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:
@@ -256,6 +276,14 @@ Ask Claude (or your MCP client of choice):
- "Show me token balances for rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe"
- "Create a trustline for USD with issuer rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe with a limit of 1000"
### TON Operations (when ENABLE_TON_TOOLS=true)
- "What's the TON balance of EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ?"
- "Show me the transaction history for EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ"
- "Is UQD0BRQt-QdIEbsjuRsMqzDlBkUAEfQixShDECoKEOXRc4eR a valid TON address?"
- "Send 0.1 TON to EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFQF8tvUZ"
- "Send 0.01 TON to UQD0BRQt-QdIEbsjuRsMqzDlBkUAEfQixShDECoKEOXRc4eR with comment 'test payment'"
- "What's the current TON Network information?"
## Security Notes
1. **Environment Variables**: All private keys are stored in the .env file and never exposed in the conversation history
@@ -291,6 +319,15 @@ The tool can use either a private key or mnemonic phrase for XRP transactions. C
- `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)
### TON Configuration
The tool uses TON Center's API by default and requires a mnemonic phrase for TON transactions. Configure these in your .env file:
- `TON_MNEMONIC`: Required - Your 24-word recovery phrase for TON wallet
- `TON_ADDRESS`: Required - Your TON wallet address
- `TON_API_KEY`: Recommended - API key from @tonapibot on Telegram (for higher rate limits)
- `TON_RPC_URL`: Optional - Custom TON RPC URL (defaults to TON Center API)
The implementation includes automatic retry logic with exponential backoff for rate limit handling.
### UTXO Chain Data Providers
The tool uses several data providers for UTXO chains:
- BlockCypher

View File

@@ -21,11 +21,12 @@
"blockchain",
"xrp",
"ripple",
"cardano"
"cardano",
"ton"
],
"author": "",
"license": "ISC",
"description": "Blockchain RPC Model-Context-Protocol Server for Solana, EVM chains, UTXO chains, Ripple (XRP), Cardano and more",
"description": "Blockchain RPC Model-Context-Protocol Server for Solana, EVM chains, UTXO chains, Ripple (XRP), Cardano, TON and more",
"repository": {
"type": "git",
"url": "https://github.com/akc2267/web3-mcp.git"
@@ -78,6 +79,9 @@
"ripple-keypairs": "^1.3.1",
"wif": "^4.0.0",
"xrpl": "^2.14.0",
"@ton/ton": "13.5.1",
"@ton/crypto": "3.2.0",
"@ton/core": "0.52.2",
"zod": "^3.24.1"
},
"devDependencies": {

View File

@@ -6,6 +6,7 @@ import { registerBitcoinCashTools } from "./UTXO/bitcoincash"
import { registerCardanoTools } from "./UTXO/cardano"
import { registerThorchainTools } from "./thorchain/thorchain"
import { registerRippleTools } from "./ripple/ripple"
import { registerTonTools } from "./ton/ton"
// Export all UTXO tools registration function
export function registerUtxoTools(server: McpServer) {
@@ -21,4 +22,5 @@ export function registerAllTools(server: McpServer) {
registerUtxoTools(server)
registerThorchainTools(server)
registerRippleTools(server)
registerTonTools(server)
}

1
src/chains/ton/index.ts Normal file
View File

@@ -0,0 +1 @@
export { registerTonTools } from './ton';

454
src/chains/ton/ton.ts Normal file
View File

@@ -0,0 +1,454 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { TonClient, Address, toNano, fromNano, beginCell, WalletContractV4, internal, SendMode } from '@ton/ton';
import { mnemonicToWalletKey, KeyPair } from '@ton/crypto';
import { Contract, Builder } from '@ton/core';
// Initialize TON client
let tonClient: TonClient | null = null;
// TON Network URL - Default to mainnet
const TON_MAINNET_ENDPOINT = process.env.TON_RPC_URL || 'https://toncenter.com/api/v2/jsonRPC';
const TON_TESTNET_ENDPOINT = 'https://testnet.toncenter.com/api/v2/jsonRPC';
const TON_API_KEY = process.env.TON_API_KEY || '';
const TON_EXPLORER = 'https://tonscan.org';
// Helper function to get or initialize the client
function getClient(testnet: boolean = false): TonClient {
const endpoint = testnet ? TON_TESTNET_ENDPOINT : TON_MAINNET_ENDPOINT;
if (!tonClient) {
tonClient = new TonClient({
endpoint,
apiKey: TON_API_KEY
});
}
return tonClient;
}
// Helper function to create a wallet from mnemonic
async function createWalletFromMnemonic(mnemonic: string): Promise<KeyPair> {
const mnemonicArray = mnemonic.split(' ');
const keyPair = await mnemonicToWalletKey(mnemonicArray);
return keyPair;
}
// Helper function to determine wallet version and create appropriate contract
async function createWalletContract(client: TonClient, address: Address, keyPair: KeyPair): Promise<{ contract: WalletContractV4, walletVersion: string }> {
// We'll use WalletV4 (most common and recent standard wallet)
// For production use, you might want to detect the wallet version from on-chain data
const walletContract = WalletContractV4.create({
publicKey: keyPair.publicKey,
workchain: 0
});
// Check if the derived address matches the expected address
const derivedAddress = walletContract.address;
if (!derivedAddress.equals(address)) {
console.warn('Warning: Derived wallet address does not match provided address.');
console.warn(`Derived: ${derivedAddress.toString()}`);
console.warn(`Provided: ${address.toString()}`);
// We'll use the provided address anyway, but this is a warning sign
}
return {
contract: walletContract,
walletVersion: 'v4r2'
};
}
// Helper function to implement exponential backoff for API rate limits
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> {
let retries = 0;
while (true) {
try {
return await fn();
} catch (error: any) {
retries++;
// If we've exceeded our max retries or it's not a rate limit error, rethrow
if (retries > maxRetries || !error.message.includes('429')) {
throw error;
}
// Calculate exponential backoff time (1s, 2s, 4s, 8s, 16s)
const delay = Math.pow(2, retries - 1) * 1000;
console.log(`Rate limit hit, retrying in ${delay}ms (attempt ${retries} of ${maxRetries})...`);
// Wait for the calculated time before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Register all TON tools
export function registerTonTools(server: McpServer) {
// Get TON Balance
server.tool(
"getTonBalance",
"Get balance for a TON address",
{
address: z.string().describe("TON address to check"),
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ address, testnet = false }) => {
try {
const client = getClient(testnet);
// Validate the address
let validAddress;
try {
validAddress = Address.parse(address);
} catch (error) {
return {
content: [{ type: "text", text: `Invalid TON address format: ${address}` }],
};
}
// Get account information including balance with retry logic
const accountInfo = await withRetry(() => client.getBalance(validAddress));
const balance = fromNano(accountInfo);
return {
content: [
{
type: "text",
text: `TON Balance for ${validAddress.toString()}:\n${balance} TON`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve TON balance: ${error.message}` }],
};
}
}
);
// Get TON transaction history
server.tool(
"getTonTransactionHistory",
"Get transaction history for a TON address",
{
address: z.string().describe("TON address to check"),
limit: z.number().optional().describe("Maximum number of transactions to return (default: 10)"),
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ address, limit = 10, testnet = false }) => {
try {
const client = getClient(testnet);
// Validate the address
let validAddress;
try {
validAddress = Address.parse(address);
} catch (error) {
return {
content: [{ type: "text", text: `Invalid TON address format: ${address}` }],
};
}
// Get transactions with retry logic
const transactions = await withRetry(() =>
client.getTransactions(validAddress, { limit: limit })
);
if (transactions.length === 0) {
return {
content: [{ type: "text", text: `No transactions found for ${validAddress.toString()}` }],
};
}
const txList = transactions.map((tx: any) => {
const timestamp = new Date(tx.time * 1000).toLocaleString();
const inMsg = tx.inMessage;
const outMsgs = tx.outMessages;
let txType = "Unknown";
let fromAddress = "N/A";
let toAddress = "N/A";
let amount = "0";
let comment = "";
// Determine if it's incoming or outgoing
if (inMsg && inMsg.source) {
txType = "Incoming";
fromAddress = inMsg.source.toString();
toAddress = validAddress.toString();
amount = inMsg.value ? fromNano(inMsg.value) : "0";
// Try to extract comment if present
if (inMsg.body) {
try {
const msgBody = inMsg.body;
// Attempt to decode comment if it's an op=0 message
if (msgBody && msgBody.beginParse) {
const slice = msgBody.beginParse();
if (slice.loadUint(32) === 0) { // op=0 indicates text comment
comment = slice.loadStringTail();
}
}
} catch (e) {
// Silent fail if we can't parse the comment
}
}
} else if (outMsgs && outMsgs.length > 0) {
txType = "Outgoing";
fromAddress = validAddress.toString();
// Sum total sent amount across all outgoing messages
let totalAmount = 0n;
outMsgs.forEach((msg: any) => {
if (msg.destination) {
toAddress = msg.destination.toString();
}
if (msg.value) {
totalAmount += msg.value;
}
// Try to extract comment from the first message if present
if (!comment && msg.body) {
try {
const msgBody = msg.body;
// Attempt to decode comment if it's an op=0 message
if (msgBody && msgBody.beginParse) {
const slice = msgBody.beginParse();
if (slice.loadUint(32) === 0) { // op=0 indicates text comment
comment = slice.loadStringTail();
}
}
} catch (e) {
// Silent fail if we can't parse the comment
}
}
});
amount = fromNano(totalAmount);
}
return `
Transaction: ${tx.hash}
Type: ${txType}
Date: ${timestamp}
From: ${fromAddress}
To: ${toAddress}
Amount: ${amount} TON${comment ? `\nComment: ${comment}` : ""}`;
}).join('\n---\n');
return {
content: [
{
type: "text",
text: `TON Transaction History for ${validAddress.toString()}:\n${txList}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve TON transaction history: ${error.message}` }],
};
}
}
);
// Validate TON address
server.tool(
"validateTonAddress",
"Validate a TON address format",
{
address: z.string().describe("TON address to validate"),
},
async ({ address }) => {
try {
let isValid = false;
let normalized = "";
try {
const parsedAddress = Address.parse(address);
isValid = true;
normalized = parsedAddress.toString();
} catch (e) {
isValid = false;
}
return {
content: [
{
type: "text",
text: isValid
? `The address ${address} has a valid TON address format.\nNormalized format: ${normalized}`
: `The address ${address} does NOT have a valid TON address format.`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Error validating TON address: ${error.message}` }],
};
}
}
);
// Get TON Network Info
server.tool(
"getTonNetworkInfo",
"Get current TON network information",
{
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ testnet = false }) => {
try {
const client = getClient(testnet);
// Get masterchain info with retry logic
const masterchainInfo = await withRetry(() => client.getMasterchainInfo());
return {
content: [
{
type: "text",
text: `TON Network Information (${testnet ? 'Testnet' : 'Mainnet'}):
Current Workchain: ${masterchainInfo.workchain}
Current Shard: ${masterchainInfo.shard}
Initial Seqno: ${masterchainInfo.initSeqno}
Latest Seqno: ${masterchainInfo.latestSeqno}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [{ type: "text", text: `Failed to retrieve TON Network information: ${error.message}` }],
};
}
}
);
// Send TON transaction using mnemonic
server.tool(
"sendTonTransaction",
"Send TON from your wallet to another address using mnemonic from .env",
{
toAddress: z.string().describe("TON address to send to"),
amount: z.union([z.string(), z.number()]).describe("Amount of TON to send"),
comment: z.string().optional().describe("Optional comment to include with the transaction"),
testnet: z.boolean().optional().describe("Use testnet instead of mainnet"),
},
async ({ toAddress, amount, comment, testnet = false }) => {
try {
// Check if mnemonic is available
if (!process.env.TON_MNEMONIC) {
throw new Error("TON_MNEMONIC is required in the .env file");
}
// Check if address is available
if (!process.env.TON_ADDRESS) {
throw new Error("TON_ADDRESS is required in the .env file");
}
// Get client
const client = getClient(testnet);
// Parse sender and destination address
const walletAddress = Address.parse(process.env.TON_ADDRESS);
let destinationAddress: Address;
try {
destinationAddress = Address.parse(toAddress);
} catch (error) {
throw new Error(`Invalid destination address: ${toAddress}`);
}
// Create wallet from mnemonic
const keyPair = await createWalletFromMnemonic(process.env.TON_MNEMONIC);
// Convert amount to nanoTON
const amountInNano = toNano(typeof amount === 'number' ? amount.toString() : amount);
// Create wallet contract and determine wallet version
const { contract: walletContract, walletVersion } =
await createWalletContract(client, walletAddress, keyPair);
// Connect to the wallet and ensure we're working with the right contract
const wallet = client.open(walletContract);
// Check wallet balance with retry logic
const balance = await withRetry(() => wallet.getBalance());
const balanceInTon = fromNano(balance);
console.log(`Wallet balance: ${balanceInTon} TON`);
// Ensure wallet has enough funds for the transaction
const sendAmount = BigInt(amountInNano);
const gasFee = toNano('0.05'); // rough estimate for fees
if (balance < sendAmount + gasFee) {
throw new Error(`Insufficient funds: ${balanceInTon} TON available, need at least ${fromNano(sendAmount + gasFee)} TON (including fees)`);
}
// Create transfer payload with comment if provided
const body = comment
? beginCell().storeUint(0, 32).storeStringTail(comment).endCell()
: undefined;
// Create internal message for the transfer
const message = internal({
to: destinationAddress,
value: amountInNano,
body: body,
});
// Send the transaction with retry logic for rate limits
console.log(`Sending ${fromNano(amountInNano)} TON to ${destinationAddress.toString()}`);
// Get seqno with retry
const seqno = await withRetry(() => wallet.getSeqno());
// Get current time to create transaction ID
const timestamp = Math.floor(Date.now() / 1000);
const txId = `${timestamp}-${walletAddress.toString().slice(0, 8)}-${destinationAddress.toString().slice(0, 8)}`;
// Send transfer with retry
await withRetry(() =>
wallet.sendTransfer({
seqno: seqno,
secretKey: keyPair.secretKey,
messages: [message],
sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS,
})
);
// For explorers, we'll create links based on the addresses since we can't get the hash directly
const explorerLink = testnet
? `https://testnet.tonscan.org/address/${destinationAddress.toString()}`
: `${TON_EXPLORER}/address/${destinationAddress.toString()}`;
return {
content: [
{
type: "text",
text: `TON Transaction Sent Successfully!
From: ${walletAddress.toString()}
To: ${destinationAddress.toString()}
Amount: ${typeof amount === 'number' ? amount.toString() : amount} TON
${comment ? `Comment: ${comment}\n` : ''}
Wallet Version: ${walletVersion}
Transaction ID: ${txId}
Explorer Link: ${explorerLink}
Note: The transaction has been sent to the network. Check the explorer link to see when it appears (usually within seconds).`,
},
],
};
} catch (err) {
const error = err as Error;
console.error('Error in sendTonTransaction:', error);
return {
content: [{ type: "text", text: `Failed to send TON transaction: ${error.message}` }],
};
}
}
);
}

View File

@@ -33,6 +33,7 @@ import {
} from "./chains/UTXO/index.js";
import { registerThorchainTools } from "./chains/thorchain/thorchain.js";
import { registerRippleTools } from "./chains/ripple/ripple.js";
import { registerTonTools } from "./chains/ton/ton.js";
import { registerGeneralTools } from "./general/index.js";
// Create server instance
@@ -94,6 +95,11 @@ if (isEnabled('ENABLE_RIPPLE_TOOLS')) {
registerRippleTools(server);
}
if (isEnabled('ENABLE_TON_TOOLS')) {
console.error('Registering TON tools...');
registerTonTools(server);
}
// Register general tools
console.error('Registering general tools...');
registerGeneralTools(server);