import { connection, wallet } from "./wallet.js";
import z from "zod";
import { createSolanaDocsMcpClient } from "./client.js";
import { solanaConfig } from "./config.js";
import { Commitment, type SolanaTool } from "./types.js";
import {
Keypair,
PublicKey,
Transaction,
SystemProgram,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import bs58 from "bs58";
import * as bip39 from "bip39";
import nacl from "tweetnacl";
export const solanaTools: SolanaTool[] = [
{
name: "search_documentation",
title: "Search Solana Documentation",
description:
"Search across the Solana documentation to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about Solana, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages.",
inputSchema: {
query: z.string().describe("The search query string"),
},
callback: async ({ query }: { query: string }) => {
try {
const solanaDocsMcpClient = await createSolanaDocsMcpClient();
const response = await solanaDocsMcpClient.client.callTool({
name: "searchDocumentation",
arguments: { query },
});
return {
content: [
{
type: "text",
text: JSON.stringify(response, null, 2),
},
],
};
} catch (err) {
const isAbort = (err as Error)?.name === "AbortError";
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: isAbort
? "Request timed out"
: "Failed to search documentation",
reason: String((err as Error)?.message ?? err),
},
null,
2,
),
},
],
};
}
},
},
// Solana RPC Methods
{
name: "get_account_info",
title: "Get Solana Account Info",
description:
"Get information about a Solana account by its public key. Returns account balance, owner, data, and executable status.",
inputSchema: {
address: z
.string()
.regex(
solanaConfig.general.addressRegex,
"Invalid Solana address: expected base58-encoded public key",
)
.describe("Solana account public key (base58-encoded)"),
commitment: z
.enum([
Commitment.Processed,
Commitment.Confirmed,
Commitment.Finalized,
])
.optional()
.describe("Commitment level (processed | confirmed | finalized)")
.default(Commitment.Confirmed),
},
callback: async ({
address,
commitment = Commitment.Confirmed,
}: {
address: string;
commitment?: Commitment;
}) => {
try {
const publicKey = new PublicKey(address);
const accountInfo = await connection.getAccountInfo(
publicKey,
commitment,
);
if (!accountInfo) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
message: "Account not found or has no data",
address,
commitment,
},
null,
2,
),
},
],
};
}
const result = {
address,
lamports: accountInfo.lamports,
sol: accountInfo.lamports / LAMPORTS_PER_SOL,
owner: accountInfo.owner.toBase58(),
executable: accountInfo.executable,
rentEpoch: accountInfo.rentEpoch,
dataLength: accountInfo.data.length,
commitment,
};
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to fetch account info",
reason: String((err as Error)?.message ?? err),
address,
},
null,
2,
),
},
],
};
}
},
},
{
name: "get_balance",
title: "Get SOL Balance",
description:
"Get the SOL balance of a Solana account in both lamports and SOL.",
inputSchema: {
address: z
.string()
.regex(
solanaConfig.general.addressRegex,
"Invalid Solana address: expected base58-encoded public key",
)
.describe("Solana account public key (base58-encoded)"),
commitment: z
.enum([
Commitment.Processed,
Commitment.Confirmed,
Commitment.Finalized,
])
.optional()
.describe("Commitment level (processed | confirmed | finalized)")
.default(Commitment.Confirmed),
},
callback: async ({
address,
commitment = Commitment.Confirmed,
}: {
address: string;
commitment?: Commitment;
}) => {
try {
const publicKey = new PublicKey(address);
const balance = await connection.getBalance(publicKey, commitment);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
address,
lamports: balance,
sol: balance / LAMPORTS_PER_SOL,
commitment,
},
null,
2,
),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to fetch balance",
reason: String((err as Error)?.message ?? err),
address,
},
null,
2,
),
},
],
};
}
},
},
{
name: "get_transaction",
title: "Get Solana Transaction",
description: "Retrieve details of a Solana transaction by its signature.",
inputSchema: {
signature: z
.string()
.regex(
solanaConfig.general.txidRegex,
"Invalid transaction signature: expected base58-encoded signature",
)
.describe("Transaction signature (base58-encoded)"),
commitment: z
.enum([
Commitment.Processed,
Commitment.Confirmed,
Commitment.Finalized,
])
.optional()
.describe("Commitment level")
.default(Commitment.Confirmed),
},
callback: async ({
signature,
commitment = Commitment.Confirmed,
}: {
signature: string;
commitment?: Commitment;
}) => {
try {
const transaction = await connection.getTransaction(signature, {
commitment: commitment as "confirmed" | "finalized",
maxSupportedTransactionVersion: 0,
});
if (!transaction) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
message: "Transaction not found",
signature,
commitment,
},
null,
2,
),
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(transaction, null, 2),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to fetch transaction",
reason: String((err as Error)?.message ?? err),
signature,
},
null,
2,
),
},
],
};
}
},
},
{
name: "get_block",
title: "Get Solana Block",
description: "Retrieve information about a Solana block by slot number.",
inputSchema: {
slot: z.number().int().nonnegative().describe("Slot number"),
commitment: z
.enum([
Commitment.Processed,
Commitment.Confirmed,
Commitment.Finalized,
])
.optional()
.describe("Commitment level")
.default(Commitment.Finalized),
},
callback: async ({
slot,
commitment = Commitment.Finalized,
}: {
slot: number;
commitment?: Commitment;
}) => {
try {
const block = await connection.getBlock(slot, {
commitment: commitment as "confirmed" | "finalized",
maxSupportedTransactionVersion: 0,
});
if (!block) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
message: "Block not found",
slot,
commitment,
},
null,
2,
),
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(block, null, 2),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to fetch block",
reason: String((err as Error)?.message ?? err),
slot,
},
null,
2,
),
},
],
};
}
},
},
{
name: "get_slot",
title: "Get Current Slot",
description: "Get the current slot that the node is processing.",
inputSchema: {
commitment: z
.enum([
Commitment.Processed,
Commitment.Confirmed,
Commitment.Finalized,
])
.optional()
.describe("Commitment level")
.default(Commitment.Finalized),
},
callback: async ({
commitment = Commitment.Finalized,
}: {
commitment?: Commitment;
}) => {
try {
const slot = await connection.getSlot(commitment);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
slot,
commitment,
},
null,
2,
),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to fetch current slot",
reason: String((err as Error)?.message ?? err),
},
null,
2,
),
},
],
};
}
},
},
// Wallet Management
{
name: "create_wallet",
title: "Create a Solana Wallet",
description:
"Generate a new Solana wallet with mnemonic phrase (12/24 words). Returns mnemonic, private key (base58), and public key.",
inputSchema: {
wordCount: z
.union([z.literal(12), z.literal(24)])
.optional()
.describe("Number of words in mnemonic (12 or 24)")
.default(12),
},
callback: async ({ wordCount = 12 }: { wordCount?: 12 | 24 }) => {
try {
const strength = wordCount === 12 ? 128 : 256;
const mnemonic = bip39.generateMnemonic(strength);
// Generate seed from mnemonic
const seed = await bip39.mnemonicToSeed(mnemonic);
// Create keypair from first 32 bytes of seed
const keypair = Keypair.fromSeed(seed.slice(0, 32));
const result = {
mnemonic,
publicKey: keypair.publicKey.toBase58(),
secretKey: bs58.encode(keypair.secretKey),
};
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to create wallet",
reason: String((err as Error)?.message ?? err),
},
null,
2,
),
},
],
};
}
},
},
{
name: "get_wallet_address",
title: "Get Current Wallet Address",
description: "Get the public key of the currently configured wallet.",
inputSchema: {},
callback: async () => {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
publicKey: wallet.publicKey.toBase58(),
},
null,
2,
),
},
],
};
},
},
{
name: "sign_message",
title: "Sign Message",
description: "Sign an arbitrary message with the wallet's private key.",
inputSchema: {
message: z.string().describe("Message to sign"),
},
callback: async ({ message }: { message: string }) => {
try {
const messageBytes = new TextEncoder().encode(message);
const signature = nacl.sign.detached(messageBytes, wallet.secretKey);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
message,
signature: bs58.encode(signature),
publicKey: wallet.publicKey.toBase58(),
},
null,
2,
),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to sign message",
reason: String((err as Error)?.message ?? err),
},
null,
2,
),
},
],
};
}
},
},
{
name: "request_airdrop",
title: "Request SOL Airdrop (Devnet/Testnet only)",
description:
"Request an airdrop of SOL tokens on Devnet or Testnet. Not available on Mainnet.",
inputSchema: {
address: z
.string()
.regex(solanaConfig.general.addressRegex, "Invalid Solana address")
.describe("Recipient address (base58-encoded public key)"),
amount: z
.number()
.positive()
.max(5)
.describe("Amount of SOL to request (max 5 SOL per request)"),
},
callback: async ({
address,
amount,
}: {
address: string;
amount: number;
}) => {
try {
const publicKey = new PublicKey(address);
const lamports = amount * LAMPORTS_PER_SOL;
const signature = await connection.requestAirdrop(publicKey, lamports);
// Wait for confirmation
await connection.confirmTransaction(signature);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
signature,
address,
amount,
lamports,
},
null,
2,
),
},
],
};
} catch (err) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to request airdrop",
reason: String((err as Error)?.message ?? err),
address,
amount,
},
null,
2,
),
},
],
};
}
},
},
];