1. 项目概述:一个钱包的MCP服务器意味着什么?
最近在折腾AI智能体开发,特别是围绕Claude Desktop这类工具构建个人工作流时,遇到了一个高频痛点:如何让AI安全、可控地访问我的链上资产信息,或者执行一些简单的链上操作?比如,我想让AI帮我汇总一下几个钱包的余额,或者在不暴露私钥的前提下,授权一笔小额交易。直接让AI接触私钥是天方夜谭,而手动复制粘贴地址和金额又太繁琐。这时候,
MCP(Model Context Protocol)
的概念就进入了视野。
genoshide/wallet-mcp
这个项目,从标题拆解来看,核心是“钱包”和“MCP服务器”。简单说,它就是一个专门为加密货币钱包功能设计的MCP服务器实现。MCP是Anthropic推出的一种协议,旨在让AI模型(如Claude)能够安全、结构化地使用外部工具、数据和计算能力。你可以把它理解为AI模型的“外挂”或“插件”系统,但更强调标准化和安全性。那么,一个“钱包MCP服务器”,就是把这个“外挂”的能力,聚焦在了区块链钱包操作上。
这解决了什么问题?它本质上是在AI与区块链世界之间,架起了一座
标准化、权限可控的桥梁
。对于开发者或高级用户,这意味着你可以让Claude这样的AI助手,通过一系列定义好的、安全的“工具”(Tools),来查询你的钱包余额、读取交易历史、甚至发起交易(需要你确认),而无需让AI直接接触你的私钥或助记词。这极大地扩展了AI在DeFi、资产管理、链上数据分析等场景的自动化能力,同时将风险隔离在可控范围内。
这个项目适合谁?首先是
AI智能体开发者
,尤其是那些构建金融、加密相关应用的开发者,他们可以以此为基础,快速集成钱包功能。其次是
加密货币的深度用户和研究者
,他们希望用AI来辅助管理多链资产、监控地址、分析交易流。最后,它也是
学习MCP协议和智能体开发
的一个绝佳实践案例,因为钱包功能涉及权限、安全、签名等核心概念,非常有代表性。
2. 核心设计思路:如何安全地让AI“碰”钱包?
让AI操作钱包,听起来就让人神经紧绷。所以,这个项目的首要设计原则,也是所有类似项目的生命线,就是
安全
。它的核心思路不是让AI成为钱包的主人,而是让AI成为一个
受严格指令控制的“操作员”
,而用户始终是拥有最终审批权的“指挥官”。
2.1 权限分离与最小化原则
最核心的设计是
私钥绝不离开安全环境
。在这个架构中,MCP服务器本身并不存储私钥。私钥的管理通常由以下几类方式处理:
本地加密存储
:私钥经过用户密码加密后,仅存储在用户本地设备上。MCP服务器在运行时,通过安全的方式(如内存中解密)临时使用,操作完成后立即从内存中清除。
硬件钱包集成
:通过连接Ledger、Trezor等硬件钱包,私钥始终保存在硬件设备的安全芯片中,签名操作在硬件内完成,MCP服务器只能发起签名请求,无法触及私钥明文。
远程签名服务
:对于更复杂的场景,可以连接如Keplr、MetaMask的扩展程序后台,或者自建的远程签名服务(如
signingd
)。MCP服务器通过与这些服务通信来发起交易,由用户在这些服务的界面中进行最终确认。
wallet-mcp
项目需要实现的就是与上述一种或多种安全后端的对接。它的角色是一个
协议转换器和工具暴露器
。它接收来自AI(通过MCP客户端)的结构化请求,比如“查询地址0x…的ETH余额”,然后将其转换为对相应区块链节点(如Infura、Alchemy)或安全签名后端的调用,最后将结果格式化后返回给AI。
2.2 MCP工具(Tools)的设计
MCP协议的核心是“工具”。一个工具由名称、描述、输入参数模式(JSON Schema)和实际的执行函数构成。对于钱包MCP,工具的设计需要兼顾功能性和安全性:
-
只读工具(安全等级高)
:
-
get_balance
: 查询指定地址在指定链上的原生代币余额。
-
get_token_balances
: 查询地址的ERC-20等标准代币余额。
-
get_transaction_history
: 获取地址的历史交易列表。
-
get_gas_price
: 获取当前网络的实时Gas价格。
- 这些工具通常只需要区块链RPC节点,不涉及私钥,可以放心暴露。
-
-
需确认的写工具(安全等级中,需用户交互)
:
-
transfer_native
: 转账原生代币(如ETH)。
-
transfer_token
: 转账标准代币。
-
approve_token
: 授权代币给某个合约。
-
这些工具是“危险操作”。MCP服务器的设计
绝不能
直接执行它们。正确的流程是:AI发起请求 -> MCP服务器生成一个未签名的交易对象 -> 通过某种方式(如返回一个需要用户确认的链接、触发一个桌面通知、更新一个待审批列表)呈现给用户 -> 用户在安全的环境(如硬件钱包、MetaMask弹窗)中审查并签名 -> 签名后的交易被广播到链上。
-
-
工具输入参数的严谨定义
:为了防止AI胡乱构造参数,每个工具的输入模式必须定义得非常严格。例如,
transfer_native
工具的参数模式会要求
to
地址必须符合EIP-55校验和格式,
amount
必须是字符串类型(避免JS数字精度问题),并且可以附加一个
maxFeePerGas
和
maxPriorityFeePerGas
的可选参数,而不是一个模糊的
gasPrice
。
注意
:一个关键的设计决策是,MCP服务器
不应该
提供“估算交易费用并自动发送”这种全自动工具。所有涉及资产转移的操作,必须有一个明确的、阻断式的用户确认环节。这是不可妥协的安全底线。
2.3 多链支持与抽象
现在的区块链生态是多链的。一个好的钱包MCP服务器不能只支持以太坊。其架构应该是模块化的,核心是一个
钱包管理器
和
链适配器
接口。
-
钱包管理器
:负责加载和管理不同的钱包(账户),每个钱包对应一个私钥或硬件钱包连接。
-
链适配器
:每个支持的区块链(如Ethereum, Polygon, Arbitrum, Solana, Cosmos Hub)都有一个对应的适配器。适配器实现了该链特有的RPC调用、交易构造、签名验证逻辑。
- 当AI请求“在Polygon上转账MATIC”时,MCP服务器会通过钱包管理器找到对应的账户,然后调用Polygon链适配器来构造交易,最后再通过钱包管理器请求签名。
这种设计使得添加对新链的支持变得清晰,只需要实现新的链适配器即可,核心的业务逻辑和MCP协议层不需要改动。
3. 核心细节解析与实操要点
理解了设计思路,我们深入到实现层面,看看几个最关键的技术细节和实操中容易踩坑的地方。
3.1 私钥的安全加载与生命周期管理
这是整个项目的安全基石。以本地加密存储为例,一个常见的做法是使用
keytar
(Node.js)或
keyring
(Python)等库,利用操作系统提供的安全凭证存储(如macOS的Keychain、Linux的Secret Service、Windows的Credential Vault)来保存加密后的私钥。
实操步骤示例(Node.js思路)
:
首次运行/初始化
:
# 假设项目使用Node.js,初始化一个钱包
node cli.js init-wallet –name "my-main-eth"
此时,CLI会提示你输入私钥或助记词(在终端中隐藏输入),然后要求你设置一个强密码。接下来:
// 伪代码逻辑
const privateKey = await promptForPrivateKey(); // 安全获取私钥
const userPassword = await promptForPassword(); // 获取用户加密密码
const salt = crypto.randomBytes(16);
const key = crypto.scryptSync(userPassword, salt, 32); // 用密码派生加密密钥
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(privateKey, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// 将加密后的数据、salt、iv、authTag通过keytar存储到系统密钥链,标识为`wallet-mcp:my-main-eth`
await keytar.setPassword('wallet-mcp', 'my-main-eth', JSON.stringify({encrypted, salt, iv, authTag}));
// 内存中的私钥明文立即被覆盖清除
MCP服务器运行时加载
:
当MCP服务器启动,需要用到这个钱包时,它会提示输入密码(或从环境变量等安全位置获取)。
const storedData = JSON.parse(await keytar.getPassword('wallet-mcp', 'my-main-eth'));
const userPassword = await getPasswordFromSecureSource(); // 获取密码
const key = crypto.scryptSync(userPassword, storedData.salt, 32);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, storedData.iv);
decipher.setAuthTag(storedData.authTag);
let decrypted = decipher.update(storedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
// 此时decrypted是私钥明文,将其加载到ethers.js或viem的Wallet对象中
const wallet = new ethers.Wallet(decrypted);
// 重要:私钥明文只存在于这个wallet对象内部,且该对象应在内存中生命周期最短
实操心得
:
绝不记录日志
:任何可能包含私钥、助记词、解密密码的变量,在调试时绝不能通过
console.log
输出,即使是
****
掩码也可能在日志聚合系统中暴露长度信息。
内存清零
:在JavaScript中,字符串是不可变的,单纯地
privateKey = null
可能不会立即从内存中清除。对于极度敏感的场景,考虑使用
Buffer
或
Uint8Array
来存储私钥,并在使用后调用
.fill(0)
来覆盖内存。或者使用专门的安全库。
密码输入超时
:如果密码是从交互式终端输入的,考虑设置一个超时,如果MCP服务器在后台空闲太久,应自动锁定,需要重新输入密码。
环境变量的陷阱
:很多人喜欢用
WALLET_PRIVATE_KEY
环境变量。这非常危险,因为进程的环境变量可能通过
/proc/[pid]/environ
或系统监控工具泄露。如果必须用,确保该环境变量仅在启动时读取一次,并立即从
process.env
中删除(
delete process.env.WALLET_PRIVATE_KEY
),但这并非绝对安全。
3.2 交易构造与用户确认流程
这是“写操作”安全的关键。流程必须是异步的、可交互的。
标准流程设计
:
AI请求
:AI通过MCP发送
transfer_native
工具调用请求,包含
to
,
amount
,
chainId
参数。
服务器构造未签名交易
:MCP服务器根据当前网络Gas价格(可通过
eth_gasPrice
或EIP-1559的
eth_feeHistory
估算),构造一个未签名的交易对象(
UnsignedTransaction
)。
生成待确认请求
:服务器
不直接签名
。而是将这个未签名交易转换成一个
待确认请求
,该请求包含一个唯一的ID、交易详情(美化后的,给用户看),并存储在一个临时的内存存储(如Map)或数据库里。
const pendingTxId = uuidv4();
const pendingTxStore.set(pendingTxId, {
unsignedTx: rawUnsignedTxObject,
createdAt: Date.now(),
status: 'pending'
});
返回用户确认指令
:MCP服务器通过MCP协议返回一个结果给AI,这个结果不是交易哈希,而是一条清晰的文本消息,例如:
“我已为您构造了一笔向
0x742d35Cc6634C0532925a3b844Bc9e…
转账
0.1 ETH
的交易。预估Gas费用约为
0.0012 ETH
。请确认是否发送。\\n\\n
请复制以下命令并在你的终端中执行以进行确认:
\\n
confirm-tx ${pendingTxId}
”
或者,如果集成了桌面通知,可以触发一个系统通知,点击后打开一个本地的小型Web服务器页面来展示交易详情和确认按钮。
用户确认
:用户在终端执行确认命令,或在前端页面点击确认。
签名并广播
:确认命令的处理程序从
pendingTxStore
中取出对应的未签名交易,
此时才调用安全的钱包对象进行签名
,然后将签名后的交易广播到网络。
返回最终结果
:将交易哈希返回给用户和AI。
这个流程确保了私钥签名动作与AI的请求之间,永远隔着一个
明确的、由用户触发的动作
。
3.3 多链适配器的统一接口
为了实现优雅的多链支持,需要定义一个抽象的
ChainAdapter
接口。
// 伪TypeScript接口定义
interface ChainAdapter {
chainId: number | string;
chainName: string;
// 只读操作
getBalance(address: string): Promise<BigNumberish>;
getTokenBalances(address: string, tokenAddresses?: string[]): Promise<TokenBalance[]>;
getTransactionHistory(address: string, limit?: number): Promise<Transaction[]>;
estimateGas(transaction: Partial<TransactionRequest>): Promise<BigNumberish>;
// 写操作(返回未签名交易)
buildTransferNativeTx(from: string, to: string, amount: BigNumberish): Promise<UnsignedTx>;
buildTransferTokenTx(from: string, to: string, tokenAddress: string, amount: BigNumberish): Promise<UnsignedTx>;
// 签名与广播(由钱包管理器调用)
signTransaction(unsignedTx: UnsignedTx, signer: Signer): Promise<SignedTx>;
sendSignedTransaction(signedTx: SignedTx): Promise<string>; // 返回txHash
}
然后,为每条链实现这个接口:
-
EthereumAdapter
: 基于
ethers.js
或
viem
实现。
-
PolygonAdapter
: 继承自
EthereumAdapter
,只需覆盖
chainId
和RPC端点。
-
SolanaAdapter
: 基于
@solana/web3.js
实现,接口相同但内部实现完全不同。
-
CosmosAdapter
: 基于
cosmjs
实现。
在MCP服务器启动时,根据配置加载所有需要的适配器。钱包管理器根据请求中的
chainId
或链名称,路由到正确的适配器。
注意事项
:
-
代币精度
:不同链、不同代币的精度(decimals)不同。
buildTransferTokenTx
时,必须将人类可读的金额(如“1.5 USDC”)转换为基于精度的最小单位(如
1.5 * 10^6 = 1500000
)。这是一个常见的错误来源。
-
Gas货币
:每条链的Gas费支付货币可能不同(ETH、MATIC、AVAX等)。在估算和显示Gas费时,需要适配器提供Gas货币的符号和精度信息。
-
RPC节点稳定性
:务必为每条链配置备用RPC节点URL,并在主节点失败时自动切换。可以考虑使用像
ethers.js
的
FallbackProvider
。
4. 实操过程与核心环节实现
让我们以一个具体的场景来串联整个实操过程:
为Claude Desktop配置
wallet-mcp
,并实现查询余额和发起转账(需确认)的功能。
4.1 环境准备与项目初始化
假设我们使用Node.js环境,基于一个现有的
wallet-mcp
项目模板进行开发。
# 1. 克隆项目或使用模板
git clone <https://github.com/genoshide/wallet-mcp.git> # 假设这是项目地址
cd wallet-mcp
# 2. 安装依赖
npm install
# 3. 安装并配置Claude Desktop(如果尚未安装)
# 从Anthropic官网下载安装Claude Desktop。
# 4. 配置Claude Desktop的MCP设置
# Claude Desktop的配置通常位于:
# macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
# Windows: %APPDATA%/Claude/claude_desktop_config.json
# Linux: ~/.config/Claude/claude_desktop_config.json
编辑这个配置文件,添加我们的MCP服务器:
{
"mcpServers": {
"wallet-mcp": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/YOUR/wallet-mcp/dist/index.js", // 指向编译后的入口文件
"–config",
"/ABSOLUTE/PATH/TO/YOUR/wallet-mcp/config.json" // 配置文件路径
],
"env": {
// 可以在这里传递加密密码,但不推荐明文。更安全的方式是让服务器启动时交互式询问。
"NODE_ENV": "production"
}
}
}
}
4.2 配置文件与钱包初始化
创建
config.json
,这是服务器的核心配置:
{
"wallets": [
{
"name": "my-ethereum-wallet",
"type": "encrypted-keystore", // 类型:加密存储
"keySource": "keychain", // 存储位置:系统密钥链
"keyIdentifier": "wallet-mcp:my-ethereum-wallet",
"defaultChainId": 1 // 默认以太坊主网
}
],
"chains": {
"1": { // 以太坊主网
"adapter": "ethereum",
"rpcUrls": [
"https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY",
"https://rpc.ankr.com/eth"
],
"explorerUrl": "https://etherscan.io"
},
"137": { // Polygon主网
"adapter": "ethereum", // 使用相同的EVM适配器
"rpcUrls": ["https://polygon-mainnet.g.alchemy.com/v2/YOUR_API_KEY"],
"explorerUrl": "https://polygonscan.com"
}
},
"server": {
"host": "127.0.0.1",
"port": 3000,
"pendingTxTimeout": 300 // 待处理交易过期时间(秒)
}
}
运行初始化命令来加密存储你的私钥:
node ./cli.js init-wallet –name my-ethereum-wallet –chain 1
# 随后按提示输入私钥和加密密码。
4.3 核心工具的实现示例
我们看看两个核心工具在代码中如何实现。这里使用
@modelcontextprotocol/sdk
来创建MCP服务器。
// src/tools/balanceTool.js
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function setupBalanceTool(server, walletManager, chainRegistry) {
server.tool(
"get_balance",
"获取指定地址在指定区块链上的原生代币余额。",
{
address: z.string().describe("要查询余额的区块链地址。"),
chainId: z.number().optional().describe("区块链的Chain ID。如不提供,使用默认链。")
},
async ({ address, chainId }) => {
try {
// 1. 获取链适配器
const chain = chainRegistry.getChain(chainId);
const adapter = chain.adapter;
// 2. 调用适配器方法
const balanceWei = await adapter.getBalance(address);
// 3. 格式化输出(例如,从wei转换为ether)
const balanceFormatted = ethers.formatEther(balanceWei);
return {
content: [{
type: "text",
text: `地址 ${address} 在 ${chain.chainName} 上的余额为: ${balanceFormatted} ${chain.nativeCurrencySymbol}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `查询余额失败: ${error.message}`
}],
isError: true
};
}
}
);
}
// src/tools/transferTool.js
import { z } from "zod";
import { v4 as uuidv4 } from 'uuid';
export function setupTransferTool(server, walletManager, chainRegistry, pendingTxStore) {
server.tool(
"transfer_native",
"发起一笔原生代币转账。此操作需要用户最终确认。",
{
to: z.string().describe("收款人地址。"),
amount: z.string().describe("转账金额,以字符串形式表示(例如 '0.1')。"),
chainId: z.number().optional().describe("区块链的Chain ID。")
},
async ({ to, amount, chainId }, extra) => {
// extra 可能包含会话等信息
const wallet = walletManager.getDefaultWallet();
const fromAddress = await wallet.getAddress();
const chain = chainRegistry.getChain(chainId || wallet.defaultChainId);
const adapter = chain.adapter;
// 1. 构造未签名交易
const unsignedTx = await adapter.buildTransferNativeTx(fromAddress, to, amount);
// 2. 估算Gas(可选,但推荐,用于给用户展示)
const estimatedGas = await adapter.estimateGas(unsignedTx);
const gasPrice = await adapter.getGasPrice(); // 可能是EIP-1559的fee数据
const estimatedFee = estimatedGas * gasPrice.maxFeePerGas; // 简化计算
// 3. 生成待确认请求ID并存储
const pendingTxId = uuidv4();
pendingTxStore.set(pendingTxId, {
unsignedTx,
chainId: chain.chainId,
createdAt: Date.now(),
from: fromAddress,
to,
amount,
estimatedFee: ethers.formatEther(estimatedFee),
status: 'pending'
});
// 4. 返回需要用户确认的消息
const confirmationMessage = `
已为您构造一笔转账交易:
– **发款人**: ${fromAddress}
– **收款人**: ${to}
– **金额**: ${amount} ${chain.nativeCurrencySymbol}
– **网络**: ${chain.chainName}
– **预估手续费**: ${ethers.formatEther(estimatedFee)} ${chain.nativeCurrencySymbol}
**此交易尚未发送。为了安全,需要您手动确认。**
请执行以下命令来批准并发送此交易:
\\`\\`\\`bash
npm run confirm-tx — ${pendingTxId}
\\`\\`\\`
此命令将在 ${Math.floor(pendingTxStore.timeout / 60)} 分钟内有效。
`;
return {
content: [{
type: "text",
text: confirmationMessage
}]
};
}
);
}
4.4 用户确认命令的实现
我们需要一个独立的CLI命令或一个内置的HTTP端点来处理用户确认。
// cli/confirmTx.js
import { walletManager, chainRegistry, pendingTxStore } from '../src/core/index.js';
import { program } from 'commander';
program
.argument('<txId>', '待确认交易的ID')
.action(async (txId) => {
const pending = pendingTxStore.get(txId);
if (!pending) {
console.error('错误:交易ID无效或已过期。');
process.exit(1);
}
if (pending.status !== 'pending') {
console.error(`错误:交易状态为"${pending.status}",无法确认。`);
process.exit(1);
}
// 显示交易详情,最后一次让用户确认
console.log(`\\n=== 交易详情 ===`);
console.log(`网络: ${chainRegistry.getChain(pending.chainId).chainName}`);
console.log(`从: ${pending.from}`);
console.log(`到: ${pending.to}`);
console.log(`金额: ${pending.amount}`);
console.log(`预估手续费: ${pending.estimatedFee}`);
console.log(`\\n是否确认发送此交易?(y/N)`);
// 等待用户输入
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise(resolve => readline.question('', resolve));
readline.close();
if (answer.toLowerCase() !== 'y') {
console.log('交易已取消。');
pendingTxStore.delete(txId);
process.exit(0);
}
// 用户确认,开始签名和发送
console.log('正在签名并发送交易…');
try {
const wallet = walletManager.getWalletForAddress(pending.from);
const chain = chainRegistry.getChain(pending.chainId);
const signedTx = await chain.adapter.signTransaction(pending.unsignedTx, wallet.signer);
const txHash = await chain.adapter.sendSignedTransaction(signedTx);
pendingTxStore.update(txId, { status: 'broadcasted', txHash });
console.log(`✅ 交易已成功广播!\\n交易哈希: ${txHash}`);
console.log(`您可以在区块浏览器查看: ${chain.explorerUrl}/tx/${txHash}`);
} catch (error) {
console.error(`❌ 交易发送失败: ${error.message}`);
pendingTxStore.update(txId, { status: 'failed', error: error.message });
process.exit(1);
}
});
program.parse();
4.5 与Claude的交互实战
配置并启动一切后,重启Claude Desktop。在Claude的聊天界面中,你就可以直接使用这些工具了。
场景一:查询余额
你:
“我的主钱包地址0x…在以太坊上还有多少ETH?”
Claude(识别到
get_balance
工具,并自动调用):
“正在为您查询… 地址 0x… 在 Ethereum Mainnet 上的余额为: 1.542 ETH”
场景二:发起转账
你:
“向地址0x…转0.01个ETH。”
Claude(识别到
transfer_native
工具,调用后返回):
“已为您构造一笔转账交易:
-
发款人
: 0xYourAddress…
-
收款人
: 0xRecipientAddress…
-
金额
: 0.01 ETH
-
网络
: Ethereum Mainnet
-
预估手续费
: 0.00042 ETH
此交易尚未发送。为了安全,需要您手动确认。
请执行以下命令来批准并发送此交易:
npm run confirm-tx — abc123-xyz-456
此命令将在5分钟内有效。”
然后你切换到终端,运行给出的命令,在终端中再次确认后,交易才被签名并广播。整个过程中,私钥从未离开过安全存储,AI只是起到了一个“提议”和“信息传递”的作用。
5. 常见问题与排查技巧实录
在实际开发和运行中,你会遇到各种各样的问题。下面是我在实现和测试过程中遇到的一些典型问题及解决方法。
5.1 MCP服务器连接失败
问题现象
:Claude Desktop启动后,右下角提示“MCP服务器连接错误”,或者在Claude中无法使用钱包工具。
排查步骤
:
检查配置文件路径
:这是最常见的问题。确保
claude_desktop_config.json
中的
command
和
args
路径是
绝对路径
,并且指向正确的文件。Node.js脚本需要有可执行权限。
手动启动服务器
:在终端中,使用配置文件中相同的命令和参数手动启动MCP服务器。例如:
node /path/to/wallet-mcp/dist/index.js –config /path/to/config.json
观察服务器是否能正常启动,是否有错误输出(如缺少模块、配置文件语法错误、RPC连接失败等)。
检查端口冲突
:MCP服务器可能默认监听某个端口(如3000),检查该端口是否被其他程序占用。
查看Claude日志
:Claude Desktop通常有应用日志。在macOS上可以在
~/Library/Logs/Claude/
找到,查看其中是否有关于MCP服务器启动失败的详细错误信息。
环境变量问题
:确保服务器运行所需的环境变量(如
NODE_ENV
,或某些API密钥)在Claude的配置中通过
env
字段正确传递,或者已在系统层面设置。
5.2 工具调用无响应或超时
问题现象
:在Claude中调用
get_balance
后,长时间没有反应,最后可能超时。
排查步骤
:
检查RPC节点
:钱包操作严重依赖区块链RPC节点。首先检查你的RPC URL是否有效、是否有速率限制或需要API密钥。尝试在浏览器或
curl
中直接访问RPC端点。
curl -X POST -H "Content-Type: application/json" –data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' <https://your-rpc-url>
服务器端日志
:在手动启动的服务器终端中,查看当Claude调用工具时,是否有请求进来,以及卡在了哪一步。添加详细的调试日志。
网络问题
:如果服务器和RPC节点都在远程,网络延迟可能导致超时。考虑使用更稳定的RPC提供商,或者为请求增加超时设置。
工具函数错误
:工具函数内部可能有未处理的异常,导致没有返回有效的MCP响应。确保所有工具函数都有完善的
try-catch
,并返回格式正确的
ToolResult
对象(包含
content
或
isError
)。
5.3 交易构造失败(Gas估算错误、nonce问题)
问题现象
:调用
transfer_native
时,服务器日志报错“failed to estimate gas”或“invalid nonce”。
原因与解决
:
-
Gas估算失败
:通常是因为交易参数有问题,比如
to
地址格式错误,或者
amount
超过了发送者余额。确保在构造交易前进行基本的参数校验(地址格式、余额充足性检查)。
-
Nonce值错误
:Nonce是防止重放攻击的计数器。如果你手动构造交易,需要从链上实时获取下一个可用的nonce。使用
provider.getTransactionCount(address, 'pending')
来获取最新的nonce,而不是缓存或自己维护。
-
EIP-1559参数
:对于支持EIP-1559的链(如以太坊),Gas费由
maxFeePerGas
和
maxPriorityFeePerGas
构成。需要从RPC节点获取当前的Fee市场数据来填充这些值,而不是简单的
gasPrice
。使用
provider.getFeeData()
来获取推荐值。
5.4 余额查询显示为0或错误
问题现象
:查询一个明明有余额的地址,却返回0。
排查步骤
:
链ID错误
:确认你查询的链ID是否正确。地址在以太坊主网(chainId=1)上有余额,在测试网(如Goerli chainId=5)上可能就是0。
RPC节点不同步
:有些免费的公共RPC节点可能同步状态落后。尝试切换到另一个备用RPC节点。
代币类型
:
get_balance
通常只查询原生代币(ETH、MATIC等)。如果要查ERC-20代币,需要使用
get_token_balances
工具,并传入代币合约地址。
地址格式
:确保传入的地址是正确的大小写校验和格式(EIP-55),特别是对于以太坊地址。一些库或RPC节点对非校验和地址的处理可能不一致。
5.5 安全相关的最佳实践与警示
定期审计依赖
:项目依赖了
ethers.js
、
@solana/web3.js
等许多第三方库。定期运行
npm audit
或使用
dependabot
来更新有安全漏洞的依赖。
限制工具权限
:在MCP服务器配置中,可以考虑实现一个权限模型。例如,某些只读工具对所有会话开放,而转账工具只对受信任的、经过认证的会话(比如来自你本地IP的Claude实例)开放。MCP协议本身支持会话级别的认证和元数据。
备份与恢复
:加密后的密钥存储(如系统密钥链)也需要备份方案。指导用户如何安全地备份他们的加密密钥文件或恢复短语(如果使用助记词派生)。
清晰的警告
:在工具的
description
和返回给用户的消息中,反复强调安全须知。例如,在转账工具的说明中写明:“此操作将移动真实资产。请务必仔细核对收款地址和金额。开发者不对因误操作导致的资产损失负责。”
模拟测试网先行
:在集成任何新链或新功能时,
永远先在测试网上进行完整测试
。配置测试网的RPC和适配器,使用测试币进行完整的“查询->构造->确认->发送”流程验证。
开发这样一个钱包MCP服务器,就像给AI装上了一双可以安全查看和操作链上世界的手。它带来的自动化潜力是巨大的,但与之匹配的安全责任也同样重大。每一行处理私钥和交易的代码,都需要经过深思熟虑。从我的经验来看,最大的挑战往往不是功能实现,而是在便捷性和绝对安全之间找到那个完美的平衡点。这个项目不是一个可以“设置完就忘”的工具,它需要开发者持续关注安全动态、更新依赖、并教育用户养成良好的安全习惯。但一旦它稳定运行起来,那种让AI成为你链上资产得力助手的感觉,绝对是传统手动操作无法比拟的。
网硕互联帮助中心

评论前必须登录!
注册