supports solana tx sends + account reads and ethereum account reads
This commit is contained in:
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.tmp/
|
||||||
|
.temp/
|
||||||
57
README.md
57
README.md
@@ -1 +1,56 @@
|
|||||||
# web3-mcp
|
# Web3 MCP
|
||||||
|
|
||||||
|
A Model-Context-Protocol server for interacting with multiple blockchains including Solana and Ethereum. This server provides simple RPC endpoints for common blockchain operations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Solana Operations:
|
||||||
|
- Get current slot number
|
||||||
|
- Check SOL account balances
|
||||||
|
- Get detailed account information
|
||||||
|
- Display keypair information
|
||||||
|
- Transfer SOL between accounts
|
||||||
|
|
||||||
|
Ethereum Operations:
|
||||||
|
- Check ETH account balances
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
clone and install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/web3-mcp.git
|
||||||
|
cd web3-mcp
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
build the tool
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
add the tool to your claude_desktop_config.json
|
||||||
|
```
|
||||||
|
"web3-rpc": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"/PATH/TO/web3-mcp/build/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Ask Claude:
|
||||||
|
- whats the latest slot on solana?
|
||||||
|
- whats the balance of 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3?
|
||||||
|
- Here's my test key [REPLACE WITH SECRET KEY]. let's transfer 0.001 SOL to [REPLACE WITH PUBLIC ADDRESS]
|
||||||
|
|
||||||
|
## Security Note
|
||||||
|
|
||||||
|
Only use this with a test wallet with a small amount of funds.
|
||||||
|
|
||||||
|
## RPC Endpoint
|
||||||
|
|
||||||
|
The server connects to Solana's mainnet at `https://api.mainnet-beta.solana.com`. To use a different network (like devnet or testnet), modify the `SOLANA_RPC` constant in `src/index.ts`.
|
||||||
|
|||||||
59
package.json
Normal file
59
package.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "web3-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"web3-mcp": "./build/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf build",
|
||||||
|
"build": "npm run clean && tsc && chmod +x build/index.js",
|
||||||
|
"start": "node build/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"solana",
|
||||||
|
"mcp",
|
||||||
|
"rpc",
|
||||||
|
"blockchain"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "Solana RPC Model-Context-Protocol Server",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/akc2267/web3-mcp.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/akc2267/web3-mcp#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/akc2267/web3-mcp/issues"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Alex Cheng",
|
||||||
|
"email": "alexanderkcheng@gmail.com",
|
||||||
|
"url": "https://github.com/akc2267"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.4.1",
|
||||||
|
"@solana/web3.js": "^1.87.6",
|
||||||
|
"bs58": "^5.0.0",
|
||||||
|
"ethers": "^6.11.1",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.1",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
321
src/index.ts
Normal file
321
src/index.ts
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
Connection,
|
||||||
|
PublicKey,
|
||||||
|
Keypair,
|
||||||
|
Transaction,
|
||||||
|
SystemProgram,
|
||||||
|
sendAndConfirmTransaction,
|
||||||
|
LAMPORTS_PER_SOL
|
||||||
|
} from '@solana/web3.js';
|
||||||
|
import { z } from "zod";
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
import { JsonRpcProvider, formatEther } from 'ethers';
|
||||||
|
|
||||||
|
const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
|
||||||
|
const ETH_RPC = "https://eth-mainnet.g.alchemy.com/v2/demo";
|
||||||
|
|
||||||
|
// Create server instance
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "web3-rpc",
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize blockchain connections
|
||||||
|
const solanaConnection = new Connection(SOLANA_RPC, 'confirmed');
|
||||||
|
const ethProvider = new JsonRpcProvider(ETH_RPC);
|
||||||
|
|
||||||
|
// 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}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error("Web3 MCP running on stdio");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err: unknown) => {
|
||||||
|
const error = err as Error;
|
||||||
|
console.error("Fatal error in main():", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
12
src/types.ts
Normal file
12
src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface MCPRequest {
|
||||||
|
method: string;
|
||||||
|
params?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPResponse {
|
||||||
|
success: boolean;
|
||||||
|
data?: any;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MCPHandler = (params: any) => Promise<MCPResponse>;
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./build",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user