Base, Arbitrum, Optimism, BSC, Polygon, Avalanche query support

This commit is contained in:
Alex Cheng
2025-02-07 14:02:45 -06:00
parent 5a0fdab8f7
commit 8cbf6324fa
4 changed files with 658 additions and 507 deletions

249
src/chains/ethereum.ts Normal file
View File

@@ -0,0 +1,249 @@
import { JsonRpcProvider, formatEther, formatUnits, Contract } from 'ethers';
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Network configurations
interface NetworkConfig {
name: string;
rpc: string;
chainId: number;
currencySymbol: string;
explorer: string;
}
const NETWORKS: { [key: string]: NetworkConfig } = {
ethereum: {
name: "Ethereum",
rpc: "https://eth-mainnet.g.alchemy.com/v2/demo",
chainId: 1,
currencySymbol: "ETH",
explorer: "https://etherscan.io"
},
base: {
name: "Base",
rpc: "https://mainnet.base.org",
chainId: 8453,
currencySymbol: "ETH",
explorer: "https://basescan.org"
},
arbitrum: {
name: "Arbitrum",
rpc: "https://arb1.arbitrum.io/rpc",
chainId: 42161,
currencySymbol: "ETH",
explorer: "https://arbiscan.io"
},
optimism: {
name: "Optimism",
rpc: "https://mainnet.optimism.io",
chainId: 10,
currencySymbol: "ETH",
explorer: "https://optimistic.etherscan.io"
},
bsc: {
name: "BNB Smart Chain",
rpc: "https://bsc-dataseed.binance.org",
chainId: 56,
currencySymbol: "BNB",
explorer: "https://bscscan.com"
},
polygon: {
name: "Polygon",
rpc: "https://polygon-rpc.com",
chainId: 137,
currencySymbol: "MATIC",
explorer: "https://polygonscan.com"
},
avalanche: {
name: "Avalanche",
rpc: "https://api.avax.network/ext/bc/C/rpc",
chainId: 43114,
currencySymbol: "AVAX",
explorer: "https://snowtrace.io"
},
tron: {
name: "Tron",
rpc: "https://api.trongrid.io",
chainId: 728126428, // TRON's chainId
currencySymbol: "TRX",
explorer: "https://tronscan.org"
}
};
// Initialize providers for each network
const providers: { [key: string]: JsonRpcProvider } = {};
for (const [network, config] of Object.entries(NETWORKS)) {
if (network !== 'tron') { // Tron needs special handling
providers[network] = new JsonRpcProvider(config.rpc);
}
}
// ERC-20 minimal ABI
const ERC20_ABI = [
"function balanceOf(address owner) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)"
];
export function registerEthereumTools(server: McpServer) {
// Get native token balance for any EVM network
server.tool(
"getEvmBalance",
"Get native token balance for an EVM address on any supported network",
{
address: z.string().describe("EVM account address"),
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"),
},
async ({ address, network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`,
},
],
};
}
const provider = providers[network];
const balance = await provider.getBalance(address);
const formattedBalance = formatEther(balance);
const networkConfig = NETWORKS[network];
return {
content: [
{
type: "text",
text: `Balance on ${networkConfig.name}:\n${formattedBalance} ${networkConfig.currencySymbol}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve balance: ${error.message}`,
},
],
};
}
}
);
// Get ERC-20 token balance for any EVM network
server.tool(
"getEvmTokenBalance",
"Get ERC-20 token balance for an address on any supported EVM network",
{
address: z.string().describe("EVM account address"),
tokenAddress: z.string().describe("ERC-20 token contract address"),
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"),
},
async ({ address, tokenAddress, network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`,
},
],
};
}
const provider = providers[network];
const contract = new Contract(tokenAddress, ERC20_ABI, provider);
const [balance, decimals, symbol] = await Promise.all([
contract.balanceOf(address),
contract.decimals(),
contract.symbol()
]);
const formattedBalance = formatUnits(balance, decimals);
const networkConfig = NETWORKS[network];
return {
content: [
{
type: "text",
text: `Token Balance on ${networkConfig.name}:\n${formattedBalance} ${symbol} (${tokenAddress})\nExplorer: ${networkConfig.explorer}/token/${tokenAddress}`
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token balance: ${error.message}`,
},
],
};
}
}
);
// Get gas price for any EVM network
server.tool(
"getGasPrice",
"Get current gas price for any supported EVM network",
{
network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"),
},
async ({ network }) => {
try {
if (!NETWORKS[network]) {
return {
content: [
{
type: "text",
text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`,
},
],
};
}
const provider = providers[network];
const feeData = await provider.getFeeData();
const gasPrice = formatUnits(feeData.gasPrice || 0, 'gwei');
const maxFeePerGas = feeData.maxFeePerGas ? formatUnits(feeData.maxFeePerGas, 'gwei') : null;
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : null;
const networkConfig = NETWORKS[network];
let response = `Gas Prices on ${networkConfig.name}:\nGas Price: ${gasPrice} Gwei`;
if (maxFeePerGas) {
response += `\nMax Fee: ${maxFeePerGas} Gwei`;
}
if (maxPriorityFeePerGas) {
response += `\nMax Priority Fee: ${maxPriorityFeePerGas} Gwei`;
}
return {
content: [
{
type: "text",
text: response,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve gas price: ${error.message}`,
},
],
};
}
}
);
}

396
src/chains/solana.ts Normal file
View File

@@ -0,0 +1,396 @@
import {
Connection,
PublicKey,
Keypair,
Transaction,
SystemProgram,
sendAndConfirmTransaction,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
import {
TOKEN_PROGRAM_ID,
AccountLayout,
} from '@solana/spl-token';
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import bs58 from 'bs58';
const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
// Initialize Solana connection
const solanaConnection = new Connection(SOLANA_RPC, 'confirmed');
// Helper function to get token accounts for a wallet
async function getTokenAccounts(walletAddress: string) {
try {
const owner = new PublicKey(walletAddress);
const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner(
owner,
{
programId: TOKEN_PROGRAM_ID
}
);
return tokenAccounts.value.map(account => {
const parsedAccountInfo = account.account.data.parsed.info;
return {
mint: new PublicKey(parsedAccountInfo.mint),
amount: parsedAccountInfo.tokenAmount.uiAmount,
decimals: parsedAccountInfo.tokenAmount.decimals,
tokenAccount: account.pubkey.toString()
};
});
} catch (error) {
console.error('Error fetching token accounts:', error);
return [];
}
}
export function registerSolanaTools(server: McpServer) {
server.tool(
"getSlot",
"Get the current slot",
{},
async () => {
try {
const slot = await solanaConnection.getSlot();
return {
content: [
{
type: "text",
text: `Current slot: ${slot}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve current slot: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getBalance",
"Get balance for a Solana address",
{
address: z.string().describe("Solana account address"),
},
async ({ address }) => {
try {
const publicKey = new PublicKey(address);
const balance = await solanaConnection.getBalance(publicKey);
const solBalance = balance / LAMPORTS_PER_SOL;
return {
content: [
{
type: "text",
text: `Balance for ${address}:\n${solBalance} SOL`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve balance for address: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getKeypairInfo",
"Get information about a keypair from its secret key",
{
secretKey: z.string().describe("Base58 encoded secret key or array of bytes"),
},
async ({ secretKey }) => {
try {
let keypair: Keypair;
try {
const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim())));
keypair = Keypair.fromSecretKey(decoded);
} catch {
keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey)));
}
const publicKey = keypair.publicKey;
const balance = await solanaConnection.getBalance(publicKey);
const accountInfo = await solanaConnection.getAccountInfo(publicKey);
return {
content: [
{
type: "text",
text: `Keypair Information:
Public Key: ${publicKey.toBase58()}
Balance: ${balance / LAMPORTS_PER_SOL} SOL
Account Program Owner: ${accountInfo?.owner?.toBase58() || 'N/A'}
Account Size: ${accountInfo?.data.length || 0} bytes
Is Executable: ${accountInfo?.executable || false}
Rent Epoch: ${accountInfo?.rentEpoch || 0}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve keypair information: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getAccountInfo",
"Get detailed account information for a Solana address",
{
address: z.string().describe("Solana account address"),
encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().describe("Data encoding format"),
},
async ({ address, encoding = 'base64' }) => {
try {
const publicKey = new PublicKey(address);
const accountInfo = await solanaConnection.getAccountInfo(
publicKey,
'confirmed'
);
if (!accountInfo) {
return {
content: [
{
type: "text",
text: `No account found for address: ${address}`,
},
],
};
}
let formattedData: string;
if (encoding === 'base58') {
formattedData = bs58.encode(accountInfo.data);
} else if (encoding === 'base64') {
formattedData = Buffer.from(accountInfo.data).toString('base64');
} else {
formattedData = Buffer.from(accountInfo.data).toString('base64');
}
return {
content: [
{
type: "text",
text: `Account Information for ${address}:
Lamports: ${accountInfo.lamports} (${accountInfo.lamports / LAMPORTS_PER_SOL} SOL)
Owner: ${accountInfo.owner.toBase58()}
Executable: ${accountInfo.executable}
Rent Epoch: ${accountInfo.rentEpoch}
Data Length: ${accountInfo.data.length} bytes
Data (${encoding}): ${formattedData}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve account information: ${error.message}`,
},
],
};
}
}
);
server.tool(
"transfer",
"Transfer SOL from your keypair to another address",
{
secretKey: z.string().describe("Your keypair's secret key (as comma-separated numbers or JSON array)"),
toAddress: z.string().describe("Destination wallet address"),
amount: z.number().positive().describe("Amount of SOL to send"),
},
async ({ secretKey, toAddress, amount }) => {
try {
let fromKeypair: Keypair;
try {
const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim())));
fromKeypair = Keypair.fromSecretKey(decoded);
} catch {
fromKeypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey)));
}
const lamports = amount * LAMPORTS_PER_SOL;
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey: new PublicKey(toAddress),
lamports,
})
);
const signature = await sendAndConfirmTransaction(
solanaConnection,
transaction,
[fromKeypair]
);
return {
content: [
{
type: "text",
text: `Transfer successful!
From: ${fromKeypair.publicKey.toBase58()}
To: ${toAddress}
Amount: ${amount} SOL
Transaction signature: ${signature}
Explorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to transfer SOL: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getSplTokenBalances",
"Get SPL token balances for a Solana address",
{
address: z.string().describe("Solana account address"),
},
async ({ address }) => {
try {
const tokenAccounts = await getTokenAccounts(address);
if (tokenAccounts.length === 0) {
return {
content: [
{
type: "text",
text: `No token accounts found for address: ${address}`,
},
],
};
}
const balancesList = tokenAccounts
.filter(account => account.amount > 0)
.map(account => `Mint: ${account.mint.toString()}\nBalance: ${account.amount}\nDecimals: ${account.decimals}\nToken Account: ${account.tokenAccount}`)
.join('\n\n');
return {
content: [
{
type: "text",
text: `Token Balances for ${address}:\n\n${balancesList}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token balances: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getSplTokenInfo",
"Get detailed information about a specific SPL token account",
{
tokenMint: z.string().describe("Token mint address"),
ownerAddress: z.string().describe("Token account owner address"),
},
async ({ tokenMint, ownerAddress }) => {
try {
const mint = new PublicKey(tokenMint);
const owner = new PublicKey(ownerAddress);
const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner(
owner,
{
mint: mint,
}
);
if (tokenAccounts.value.length === 0) {
return {
content: [
{
type: "text",
text: `No token account found for mint ${tokenMint} owned by ${ownerAddress}`,
},
],
};
}
const accountInfo = tokenAccounts.value[0].account;
const parsedInfo = accountInfo.data.parsed.info;
return {
content: [
{
type: "text",
text: `Token Account Information:
Token Account: ${tokenAccounts.value[0].pubkey.toString()}
Mint: ${parsedInfo.mint}
Owner: ${parsedInfo.owner}
Balance: ${parsedInfo.tokenAmount.uiAmount}
Decimals: ${parsedInfo.tokenAmount.decimals}
State: ${parsedInfo.state}
Is Native: ${parsedInfo.isNative}
Delegation: ${parsedInfo.delegate || 'None'}
Close Authority: ${parsedInfo.closeAuthority || 'None'}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token information: ${error.message}`,
},
],
};
}
}
);
}

View File

@@ -1,31 +1,7 @@
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 { import { registerSolanaTools } from "./chains/solana.js";
Connection, import { registerEthereumTools } from "./chains/ethereum.js";
PublicKey,
Keypair,
Transaction,
SystemProgram,
sendAndConfirmTransaction,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
import {
TOKEN_PROGRAM_ID,
AccountLayout,
} from '@solana/spl-token';
import { z } from "zod";
import bs58 from 'bs58';
import { JsonRpcProvider, formatEther, formatUnits, Contract, Interface } from 'ethers';
const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
const ETH_RPC = "https://eth-mainnet.g.alchemy.com/v2/demo";
// ERC-20 minimal ABI for balance checking
const ERC20_ABI = [
"function balanceOf(address owner) view returns (uint256)",
"function decimals() view returns (uint8)",
"function symbol() view returns (string)"
];
// Create server instance // Create server instance
const server = new McpServer({ const server = new McpServer({
@@ -33,450 +9,9 @@ const server = new McpServer({
version: "1.0.0", version: "1.0.0",
}); });
// Initialize blockchain connections // Register chain-specific tools
const solanaConnection = new Connection(SOLANA_RPC, 'confirmed'); registerSolanaTools(server);
const ethProvider = new JsonRpcProvider(ETH_RPC); registerEthereumTools(server);
// Register Solana tools
server.tool(
"getSlot",
"Get the current slot",
{},
async () => {
try {
const slot = await solanaConnection.getSlot();
return {
content: [
{
type: "text",
text: `Current slot: ${slot}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve current slot: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getBalance",
"Get balance for a Solana address",
{
address: z.string().describe("Solana account address"),
},
async ({ address }) => {
try {
const publicKey = new PublicKey(address);
const balance = await solanaConnection.getBalance(publicKey);
const solBalance = balance / LAMPORTS_PER_SOL;
return {
content: [
{
type: "text",
text: `Balance for ${address}:\n${solBalance} SOL`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve balance for address: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getKeypairInfo",
"Get information about a keypair from its secret key",
{
secretKey: z.string().describe("Base58 encoded secret key or array of bytes"),
},
async ({ secretKey }) => {
try {
// Handle both base58 encoded strings and byte arrays
let keypair: Keypair;
try {
// First try parsing as comma-separated string
const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim())));
keypair = Keypair.fromSecretKey(decoded);
} catch {
// If that fails, try as a byte array string
keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey)));
}
// Get account info and balance
const publicKey = keypair.publicKey;
const balance = await solanaConnection.getBalance(publicKey);
const accountInfo = await solanaConnection.getAccountInfo(publicKey);
return {
content: [
{
type: "text",
text: `Keypair Information:
Public Key: ${publicKey.toBase58()}
Balance: ${balance / LAMPORTS_PER_SOL} SOL
Account Program Owner: ${accountInfo?.owner?.toBase58() || 'N/A'}
Account Size: ${accountInfo?.data.length || 0} bytes
Is Executable: ${accountInfo?.executable || false}
Rent Epoch: ${accountInfo?.rentEpoch || 0}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve keypair information: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getAccountInfo",
"Get detailed account information for a Solana address",
{
address: z.string().describe("Solana account address"),
encoding: z.enum(['base58', 'base64', 'jsonParsed']).optional().describe("Data encoding format"),
},
async ({ address, encoding = 'base64' }) => {
try {
const publicKey = new PublicKey(address);
const accountInfo = await solanaConnection.getAccountInfo(
publicKey,
'confirmed'
);
if (!accountInfo) {
return {
content: [
{
type: "text",
text: `No account found for address: ${address}`,
},
],
};
}
// Format the data based on encoding
let formattedData: string;
if (encoding === 'base58') {
formattedData = bs58.encode(accountInfo.data);
} else if (encoding === 'base64') {
formattedData = Buffer.from(accountInfo.data).toString('base64');
} else {
// For jsonParsed, we'll still return base64 but note that it's not parsed
formattedData = Buffer.from(accountInfo.data).toString('base64');
}
return {
content: [
{
type: "text",
text: `Account Information for ${address}:
Lamports: ${accountInfo.lamports} (${accountInfo.lamports / LAMPORTS_PER_SOL} SOL)
Owner: ${accountInfo.owner.toBase58()}
Executable: ${accountInfo.executable}
Rent Epoch: ${accountInfo.rentEpoch}
Data Length: ${accountInfo.data.length} bytes
Data (${encoding}): ${formattedData}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve account information: ${error.message}`,
},
],
};
}
}
);
server.tool(
"transfer",
"Transfer SOL from your keypair to another address",
{
secretKey: z.string().describe("Your keypair's secret key (as comma-separated numbers or JSON array)"),
toAddress: z.string().describe("Destination wallet address"),
amount: z.number().positive().describe("Amount of SOL to send"),
},
async ({ secretKey, toAddress, amount }) => {
try {
// Parse the secret key and create keypair
let fromKeypair: Keypair;
try {
// First try parsing as comma-separated string
const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim())));
fromKeypair = Keypair.fromSecretKey(decoded);
} catch {
// If that fails, try as a JSON array string
fromKeypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey)));
}
// Convert SOL amount to lamports
const lamports = amount * LAMPORTS_PER_SOL;
// Create transfer instruction
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey: new PublicKey(toAddress),
lamports,
})
);
// Send and confirm transaction
const signature = await sendAndConfirmTransaction(
solanaConnection,
transaction,
[fromKeypair]
);
return {
content: [
{
type: "text",
text: `Transfer successful!
From: ${fromKeypair.publicKey.toBase58()}
To: ${toAddress}
Amount: ${amount} SOL
Transaction signature: ${signature}
Explorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to transfer SOL: ${error.message}`,
},
],
};
}
}
);
// Register Ethereum tools
server.tool(
"getEthBalance",
"Get balance for an Ethereum address",
{
address: z.string().describe("Ethereum account address"),
},
async ({ address }) => {
try {
const balance = await ethProvider.getBalance(address);
const ethBalance = formatEther(balance); // Convert from wei to ETH
return {
content: [
{
type: "text",
text: `Balance for ${address}:\n${ethBalance} ETH`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve ETH balance: ${error.message}`,
},
],
};
}
}
);
// Register token balance checking tool
server.tool(
"getTokenBalance",
"Get ERC-20 token balance for an Ethereum address",
{
address: z.string().describe("Ethereum account address"),
tokenAddress: z.string().describe("ERC-20 token contract address")
},
async ({ address, tokenAddress }) => {
try {
// Create contract interface
const contract = new Contract(tokenAddress, ERC20_ABI, ethProvider);
// Get token details and balance
const [balance, decimals, symbol] = await Promise.all([
contract.balanceOf(address),
contract.decimals(),
contract.symbol()
]);
// Format the balance using the correct number of decimals
const formattedBalance = formatUnits(balance, decimals);
return {
content: [
{
type: "text",
text: `Token Balance for ${address}:\n${formattedBalance} ${symbol} (${tokenAddress})`
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token balance: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getSplTokenBalances",
"Get SPL token balances for a Solana address",
{
address: z.string().describe("Solana account address"),
},
async ({ address }) => {
try {
const tokenAccounts = await getTokenAccounts(address);
if (tokenAccounts.length === 0) {
return {
content: [
{
type: "text",
text: `No token accounts found for address: ${address}`,
},
],
};
}
// Create formatted output of all token balances
const balancesList = tokenAccounts
.filter(account => account.amount > 0) // Only show non-zero balances
.map(account => `Mint: ${account.mint.toString()}\nBalance: ${account.amount}\nDecimals: ${account.decimals}\nToken Account: ${account.tokenAccount}`)
.join('\n\n');
return {
content: [
{
type: "text",
text: `Token Balances for ${address}:\n\n${balancesList}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token balances: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getSplTokenInfo",
"Get detailed information about a specific SPL token account",
{
tokenMint: z.string().describe("Token mint address"),
ownerAddress: z.string().describe("Token account owner address"),
},
async ({ tokenMint, ownerAddress }) => {
try {
const mint = new PublicKey(tokenMint);
const owner = new PublicKey(ownerAddress);
// Find the token account
const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner(
owner,
{
mint: mint,
}
);
if (tokenAccounts.value.length === 0) {
return {
content: [
{
type: "text",
text: `No token account found for mint ${tokenMint} owned by ${ownerAddress}`,
},
],
};
}
const accountInfo = tokenAccounts.value[0].account;
const parsedInfo = accountInfo.data.parsed.info;
return {
content: [
{
type: "text",
text: `Token Account Information:
Token Account: ${tokenAccounts.value[0].pubkey.toString()}
Mint: ${parsedInfo.mint}
Owner: ${parsedInfo.owner}
Balance: ${parsedInfo.tokenAmount.uiAmount}
Decimals: ${parsedInfo.tokenAmount.decimals}
State: ${parsedInfo.state}
Is Native: ${parsedInfo.isNative}
Delegation: ${parsedInfo.delegate || 'None'}
Close Authority: ${parsedInfo.closeAuthority || 'None'}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve token information: ${error.message}`,
},
],
};
}
}
);
async function main() { async function main() {
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
@@ -489,30 +24,3 @@ main().catch((err: unknown) => {
console.error("Fatal error in main():", error.message); console.error("Fatal error in main():", error.message);
process.exit(1); process.exit(1);
}); });
// Helper function to get token accounts for a wallet
async function getTokenAccounts(walletAddress: string) {
try {
const owner = new PublicKey(walletAddress);
const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner(
owner,
{
programId: TOKEN_PROGRAM_ID
}
);
return tokenAccounts.value.map(account => {
const parsedAccountInfo = account.account.data.parsed.info;
return {
mint: new PublicKey(parsedAccountInfo.mint),
amount: parsedAccountInfo.tokenAmount.uiAmount,
decimals: parsedAccountInfo.tokenAmount.decimals,
tokenAccount: account.pubkey.toString()
};
});
} catch (error) {
console.error('Error fetching token accounts:', error);
return [];
}
}

View File

@@ -1,17 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "es2020",
"module": "ES2020", "module": "es2020",
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./build", "allowSyntheticDefaultImports": true,
"rootDir": "./src",
"strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "outDir": "build",
"resolveJsonModule": true, "rootDir": "src"
"allowJs": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules"] "exclude": ["node_modules", "build"]
} }