在日常接入 AI 编程助手到各种本地服务时,最消耗耐心的环节之一就是反复配置 MCP 服务器:换一台电脑、换一个客户端、换一个项目目录,就得重新配一遍地址、参数和环境变量。尤其是本地起了多个 MCP 服务之后,配置文件散落在不同项目里,有时候甚至想不起来上一次用的到底是哪一种启动方式。
这篇文章整理了一套“个人 MCP wallet”的实操方案。它不是某个平台自带的官方功能,而是把 MCP 服务器的连接信息集中保存成一份本地钱包,用命令行统一添加、展示、导出和备份。适合已经接触过 MCP、但经常被配置散落问题困扰的开发者;零基础读者也可以先看完概念部分,再照着步骤搭建。
1. 背景与核心概念
1.1 MCP 是什么
MCP 的全称是 Model Context Protocol,中文通常叫“模型上下文协议”。它的核心作用是让 AI 模型能够调用外部工具和数据源,而不是只能基于训练数据回答问题。你可以把 MCP 理解成 AI 客户端与工具服务之间的一座标准化桥梁:AI 客户端通过协议发现工具、发起调用、接收结果;工具服务则按协议暴露能力。
典型的 MCP 架构包含三类角色:
- MCP Client:AI 客户端,例如 Claude Desktop、Cursor、自研 AI 应用。
- MCP Server:工具提供方,例如文件系统服务、数据库查询服务、HTTP API 服务。
- MCP Host:承载客户端和交互界面的应用进程。
MCP Server 的数据传输方式通常有两种:stdio 和 HTTP/SSE。stdio 方式下,客户端本地启动一个子进程,双方通过标准输入输出通信;HTTP 方式下,客户端通过网络请求连接远程服务。这也就意味着,配置 MCP 时至少要告诉客户端“命令是什么、参数是什么、环境变量是什么”。这些信息一旦分散管理,就会出现重复配置、配置丢失、参数不一致等连锁问题。
1.2 为什么“每次重连 MCP”会让人头疼
初次使用 MCP 时,开发者往往是在客户端界面里手动添加一个 server,例如:
{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] } } }这段配置看起来简单,但实际使用中会频繁遇到以下场景:
- 换电脑后,客户端配置目录为空,需要重新回忆所有 MCP 启动命令。
- 切换 IDE 或桌面客户端时,不同产品对 MCP 配置文件的位置和格式要求不一致。
- 同一个 MCP 服务在 A 项目用一套 tokens,在 B 项目用另一套 tokens,配置散布在多个工程里。
- 多人协作时,团队成员的 MCP 配置“各写各的”,很难对齐。
重复配置不只是耗时,还容易引发安全问题。比如把服务 token 直接写进某个项目的 JSON 文件,随着代码仓库发出去了,才发现凭据已经泄露。这也提醒我们,MCP 配置里最值得治理的其实是“连接信息”和“密钥信息”两类内容。
1.3 个人 MCP wallet 解决什么问题
“MCP wallet”这个概念,借鉴了现实世界钱包的使用体验:平时把卡、证件、票据放在一个地方,需要支付时取出来用,而不是每次办卡都重新填表。个人 MCP wallet 做的是类似的事情:
- 统一存放 MCP 服务器的连接配置。
- 提供命令行入口来管理这些配置。
- 按客户端类型导出标准格式。
- 密钥信息从配置文件中剥离,通过环境变量注入。
这样,当你在 Claude Desktop 里配好了一套 MCP 服务,重装系统或者切换到另一台开发机时,不需要一个个重新填写,只需要把钱包目录同步过来,再执行一次导出命令,就能生成目标客户端需要的配置。这是一个“配置一次、多处复用”的思路。
2. 环境准备与版本说明
2.1 运行环境建议
本文示例使用纯 Node.js 实现,不依赖前端框架,也没有引入重量级运行库。建议环境如下:
- 操作系统:macOS / Linux / Windows 均可。
- Node.js:v18 及以上。示例会用到
fs.mkdirSync的recursive参数、fs/promises以及解构赋值,较老的 Node 版本可能不支持部分写法。 - 包管理器:npm 或 yarn,仅在初始化项目时使用。
- 终端:支持常见 shell 即可,Windows 下推荐 PowerShell 或 Git Bash。
如果本机 Node 版本较低,可以先通过node -v查看当前版本,必要时升级或使用 nvm 切换。本文不会把版本写死,重点演示整体思路;不同环境下的路径和命令需要结合实际情况微调。
2.2 项目结构规划
为了让代码更清晰,我们把项目拆成入口、存储、命令、模板四个模块。目录结构如下:
mcp-wallet/ ├── package.json ├── bin/ │ └── mcp-wallet.js ├── lib/ │ ├── storage.js │ ├── registry.js │ └── render.js ├── examples/ │ └── wallet.sample.json └── README.mdbin/mcp-wallet.js:命令行入口,负责解析参数并调用具体命令。lib/storage.js:负责读取和写入钱包文件。lib/registry.js:负责添加、列出、删除 MCP 服务。lib/render.js:负责按客户端模板渲染导出内容。examples/wallet.sample.json:示例钱包数据。
在真实项目中,还可以把测试文件、日志模块、备份策略都加进来,但本文先保持最小可运行结构。
2.3 技术选型说明
选择纯 Node.js 的原因有三个:
- MCP 生态中的很多 Server 本身就是 Node.js 写的,用 Node 写管理工具没有额外运行时负担。
- 个人工具不需要复杂的 UI,CLI 足够完成“添加、查看、导出”的操作。
- 钱包文件使用 JSON 存储,格式直观,方便手工核对和版本管理。
需要说明的是,JSON 格式虽然容易读懂,但不支持注释。如果想要更人性化的配置体验,可以把钱包文件改成 YAML 格式,再引入 YAML 解析库。本文为了减少依赖,依旧以 JSON 为存储格式。
3. 核心设计拆解:MCP wallet 的数据模型与命令设计
3.1 钱包数据模型
先看钱包文件长什么样。示例数据如下:
{ "version": 1, "updatedAt": "2025-01-01T12:00:00Z", "servers": { "filesystem": { "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"], "env": {}, "enabled": true, "tags": ["local", "file"] }, "weather": { "transport": "stdio", "command": "node", "args": ["/path/to/weather-server/index.js"], "env": { "API_KEY": "env:WEATHER_API_KEY" }, "enabled": true, "tags": ["http", "api"] } } }字段含义如下:
version:钱包文件版本号,便于以后做迁移。updatedAt:最近更新时间,便于备份对比。servers:MCP 服务集合,以服务名为键。transport:传输方式,当前大多为stdio。command和args:启动 MCP Server 的实际命令。env:环境变量配置。注意这里的值写成env:WEATHER_API_KEY,意思是“启动时从当前 shell 环境变量中读取WEATHER_API_KEY”,而不是把真实密钥直接写进钱包文件。enabled:是否启用该服务。导出时,禁用的服务不会进入客户端配置。tags:标签,方便按用途筛选。
这个数据模型最重要的设计点是“密钥占位符”。钱包文件可以被同步到代码仓库或云盘,但真实密钥只存在于运行环境中。导出配置时,CLI 会把占位符解析成真实值。
3.2 CLI 命令设计
个人 MCP wallet 需要提供五个基础命令:
| 命令 | 作用 | 示例 |
|---|---|---|
list | 列出钱包中所有 MCP 服务 | mcp-wallet list |
add | 添加或更新一个 MCP 服务 | mcp-wallet add filesystem --command npx --args "-y @modelcontextprotocol/server-filesystem /workspace" |
remove | 删除指定服务 | mcp-wallet remove filesystem |
export | 导出为指定客户端的 MCP 配置 | mcp-wallet export claude |
backup | 备份钱包文件 | mcp-wallet backup |
命令相对简单,但已经覆盖了日常管理的主要场景。后续如果需要,还可以增加rename、enable、disable、doctor等命令。doctor命令尤其有价值,它可以在导出前检查命令是否存在、环境变量是否配置完整。
3.3 导出渲染逻辑
导出是钱包的核心能力。不同客户端对 MCP 配置的格式要求大同小异,但文件位置不同。例如 Claude Desktop 的配置通常位于claude_desktop_config.json,Cursor 的项目级配置通常位于项目根目录的.cursor/mcp.json。为了让钱包不绑定到具体客户端,我们在渲染时只生成标准 JSON 片段,最终落盘位置由用户自己决定。
渲染逻辑可以概括为:
- 读取钱包文件。
- 遍历
servers,过滤掉enabled: false的服务。 - 处理
env字段,把env:KEY_NAME替换为process.env.KEY_NAME。 - 组装成
{ mcpServers: { ... } }结构。 - 按客户端模板输出。
其中第三步最容易出错。如果某个环境变量未设置,导出应该主动报错,而不是生成一份运行时会失败的配置。所以在实现时,我们要在渲染函数里加入校验。
4. 完整实战案例:从零构建个人 MCP wallet
4.1 初始化项目与依赖
首先创建一个空目录,并初始化package.json。
mkdir mcp-wallet && cd mcp-wallet npm init -y然后修改package.json,加入bin字段,方便以后全局安装和命令行调用。
{ "name": "mcp-wallet", "version": "0.1.0", "description": "个人 MCP 配置管理工具", "main": "bin/mcp-wallet.js", "bin": { "mcp-wallet": "./bin/mcp-wallet.js" }, "engines": { "node": ">=18" }, "license": "MIT" }这里先不引入额外依赖,保持最小实现。如果你希望命令行解析更完善,可以在后续引入commander或yargs,但本文通过手写参数解析来降低学习门槛。
4.2 实现钱包存储层
创建lib/storage.js,封装钱包文件的读写。
// 文件路径:lib/storage.js const fs = require("fs"); const path = require("path"); const os = require("os"); const WALLET_DIR = path.join(os.homedir(), ".mcp-wallet"); const WALLET_FILE = path.join(WALLET_DIR, "wallet.json"); const BACKUP_DIR = path.join(WALLET_DIR, "backups"); function ensureWalletDir() { fs.mkdirSync(WALLET_DIR, { recursive: true }); fs.mkdirSync(BACKUP_DIR, { recursive: true }); } function readWallet() { ensureWalletDir(); if (!fs.existsSync(WALLET_FILE)) { return { version: 1, updatedAt: null, servers: {} }; } try { const raw = fs.readFileSync(WALLET_FILE, "utf-8"); return JSON.parse(raw); } catch (err) { throw new Error(`钱包文件解析失败: ${err.message}`); } } function writeWallet(data) { ensureWalletDir(); const snapshot = { ...data, updatedAt: new Date().toISOString() }; fs.writeFileSync(WALLET_FILE, JSON.stringify(snapshot, null, 2), "utf-8"); return snapshot; } function backupWallet() { ensureWalletDir(); if (!fs.existsSync(WALLET_FILE)) { return null; } const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const dest = path.join(BACKUP_DIR, `wallet-${stamp}.json`); fs.copyFileSync(WALLET_FILE, dest); return dest; } module.exports = { WALLET_DIR, WALLET_FILE, BACKUP_DIR, readWallet, writeWallet, backupWallet, };这里的重点在于ensureWalletDir和backupWallet。前者保证钱包目录存在,避免首次运行时因目录缺失而失败;后者在做改动前生成一份带时间戳的备份。对于任何修改配置的操作,备份都不是可有可无的步骤。
4.3 实现服务登记命令
创建lib/registry.js,包含list、add、remove三个命令的核心逻辑。
// 文件路径:lib/registry.js const { readWallet, writeWallet, backupWallet } = require("./storage"); function listServers() { const wallet = readWallet(); const servers = wallet.servers || {}; const names = Object.keys(servers); if (names.length === 0) { console.log("钱包为空。可以使用 mcp-wallet add <name> 添加一个 MCP 服务。"); return; } console.log("当前 MCP 钱包内容:\n"); for (const name of names) { const server = servers[name]; const status = server.enabled === false ? "禁用" : "启用"; console.log(`- ${name} [${status}]`); console.log(` 命令: ${server.command} ${(server.args || []).join(" ")}`); console.log(` 传输方式: ${server.transport || "stdio"}\n`); } } function parseKeyValuePairs(args) { const result = {}; for (const item of args || []) { const index = item.indexOf("="); if (index > 0) { result[item.slice(0, index)] = item.slice(index + 1); } } return result; } function addServer(name, options = {}) { if (!name) { throw new Error("必须指定服务名称,例如 mcp-wallet add filesystem"); } if (!options.command) { throw new Error("必须指定 command 参数,例如 --command npx"); } const wallet = readWallet(); const args = typeof options.args === "string" ? options.args.split(/\s+/).filter(Boolean) : options.args || []; const env = typeof options.env === "string" ? parseKeyValuePairs(options.env.split(",")) : {}; wallet.servers = wallet.servers || {}; wallet.servers[name] = { transport: options.transport || "stdio", command: options.command, args, env, enabled: options.enabled !== "false", tags: options.tags ? options.tags.split(",") : [], }; const backupPath = backupWallet(); const updated = writeWallet(wallet); console.log(`已写入服务 ${name}。`); if (backupPath) { console.log(`备份文件: ${backupPath}`); } } function removeServer(name) { if (!name) { throw new Error("必须指定要删除的服务名称"); } const wallet = readWallet(); if (!wallet.servers || !wallet.servers[name]) { throw new Error(`服务 ${name} 不存在`); } delete wallet.servers[name]; backupWallet(); writeWallet(wallet); console.log(`已删除服务 ${name}。`); } module.exports = { listServers, addServer, removeServer, };这段代码有几个容易理解错的地方,我单独说明一下。
parseKeyValuePairs用于解析形如FOO=bar的字符串列表。在实际命令行中,--env参数可以接收多个以逗号分隔的键值对,例如--env "A=1,B=2"。这里为了演示简单,没有处理复杂的引号转义;真实产品中建议使用更规范的参数解析库。
backupWallet放在写入之前,是为了确保写坏文件时仍能回滚。尤其当你准备删除某个服务,却发现删除脚本有 bug,此时有备份就能恢复数据。
4.4 实现导出渲染逻辑
创建lib/render.js,负责把钱包数据渲染成 MCP 客户端配置。
// 文件路径:lib/render.js const { readWallet } = require("./storage"); function resolveEnv(server) { const env = {}; const rawEnv = server.env || {}; for (const key of Object.keys(rawEnv)) { const rawValue = rawEnv[key]; if (typeof rawValue === "string" && rawValue.startsWith("env:")) { const envKey = rawValue.slice(4); if (!process.env[envKey]) { throw new Error(`环境变量 ${envKey} 未设置,无法导出服务 ${server.name} 的配置。`); } env[key] = process.env[envKey]; } else { env[key] = rawValue; } } return env; } function renderClient(clientName) { const wallet = readWallet(); const servers = wallet.servers || {}; const result = { mcpServers: {} }; for (const name of Object.keys(servers)) { const server = servers[name]; if (server.enabled === false) { continue; } const target = { command: server.command, args: server.args || [], }; const env = resolveEnv(server); if (Object.keys(env).length > 0) { target.env = env; } result.mcpServers[name] = target; } if (clientName === "cursor") { return JSON.stringify(result, null, 2); } return JSON.stringify(result, null, 2); } module.exports = { renderClient };当前claude、cursor、generic三种客户端的 MCP 配置结构差异并不大,核心部分都是mcpServers。所以render.js先保留统一的渲染逻辑,后续如果某个客户端格式出现差异,再在渲染函数里按clientName分支处理。
这里最关键的是resolveEnv函数。它保证了导出的 JSON 中不会包含钱包文件里的env:占位符,而是真正的运行环境变量值。比如钱包文件里写的是API_KEY=env:OPENWEATHER_KEY,导出后就变成API_KEY=xxxxx。
4.5 编写命令行入口
创建bin/mcp-wallet.js,让所有命令能通过终端使用。
#!/usr/bin/env node // 文件路径:bin/mcp-wallet.js const { listServers, addServer, removeServer } = require("../lib/registry"); const { renderClient } = require("../lib/render"); const { backupWallet, WALLET_DIR } = require("../lib/storage"); function parseOptions(args) { const options = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith("--")) { const key = arg.slice(2); const value = args[i + 1]; if (value !== undefined && !value.startsWith("--")) { options[key] = value; i++; } else { options[key] = true; } } } return options; } async function main() { const args = process.argv.slice(2); const command = args[0]; if (!command || command === "--help" || command === "-h") { console.log(` 用法: mcp-wallet list mcp-wallet add <name> --command <cmd> [--args "<arg1> <arg2>"] [--env "A=1,B=2"] mcp-wallet remove <name> mcp-wallet export <claude|cursor|generic> mcp-wallet backup 钱包目录: ${WALLET_DIR} `); return; } switch (command) { case "list": listServers(); break; case "add": { const name = args[1]; const options = parseOptions(args.slice(2)); addServer(name, options); break; } case "remove": { removeServer(args[1]); break; } case "export": { const client = args[1] || "generic"; const output = renderClient(client); console.log(output); break; } case "backup": { const path = backupWallet(); if (path) { console.log(`备份完成: ${path}`); } else { console.log("钱包文件不存在,无需备份。"); } break; } default: console.log(`未知命令: ${command}`); } } main().catch((err) => { console.error(`运行失败: ${err.message}`); process.exit(1); });先给bin/mcp-wallet.js执行权限:
chmod +x bin/mcp-wallet.js如果想直接使用mcp-wallet命令,可以在项目目录下执行:
npm link这样系统会把当前命令链接到全局node_modules/.bin目录下。npm link在个人开发机上很常用,但在团队协作中要注意环境一致性,建议在 README 里写明安装方式。
4.6 运行与验证
添加一个文件系统服务并查看列表。
node bin/mcp-wallet.js add filesystem --command npx --args "-y @modelcontextprotocol/server-filesystem /workspace" node bin/mcp-wallet.js list预期输出类似:
当前 MCP 钱包内容: - filesystem [启用] 命令: npx -y @modelcontextprotocol/server-filesystem /workspace 传输方式: stdio再添加一个带环境变量的服务,然后导出配置。
node bin/mcp-wallet.js add weather --command node --args "/path/to/weather-server/index.js" --env "API_KEY=env:WEATHER_API_KEY" WEATHER_API_KEY=123456 node bin/mcp-wallet.js export claude预期输出:
{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] }, "weather": { "command": "node", "args": ["/path/to/weather-server/index.js"], "env": { "API_KEY": "123456" } } } }如果忘记设置WEATHER_API_KEY就执行导出,命令行会主动报错,提示环境变量未设置。这个设计看起来有些“严格”,但能避免生成一份在启动时才会失败的残配置。
5. 常见问题与排查思路
5.1 常见问题对照表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
执行命令时报command not found | 钱包里配置的 command 不在当前 PATH 中 | 先手动在终端执行该命令确定绝对路径,并在钱包中改成绝对路径 |
| 导出成功,但客户端连接失败 | MCP Server 启动报错,或参数格式不对 | 先在终端直接执行command args,观察进程是否能正常启动 |
| 环境变量没有生效 | 环境变量名拼写错误,或值未正确传递 | 检查钱包中env字段是否为env:KEY_NAME,并确认当前 shell 中存在该变量 |
| 备份文件越来越多 | ||
| 每次写入都生成一个备份 | 定期清理~/.mcp-wallet/backups,或保留最近 N 份备份 | |
| 配置文件同步后路径失效 | 不同电脑上项目路径不同 | 尽量避免在 args 中写绝对路径,使用相对路径或统一的工作目录 |
| 服务被禁用了但还出现在导出里 | 导出逻辑未过滤enabled字段 | 检查钱包 JSON 中的enabled是否为false |
5.2 排查步骤
如果客户端始终无法连接到 MCP Server,不要急着改钱包文件,按照下面的顺序排查:
- 先确认钱包中的 command 能否手动启动。例如运行
npx -y @modelcontextprotocol/server-filesystem /workspace,看是否报缺依赖或缺权限。 - 确认客户端配置文件的格式正确。多余逗号、嵌套层级错误都会导致 JSON 解析失败。
- 确认环境变量已注入到当前进程。不同的客户端对
env字段的支持程度不同,有些客户端可能在配置层不读取系统环境变量,需要把env显式写在 JSON 里。 - 确认端口或标准输入输出没有被占用。stdio 模式通常不涉及端口,但 HTTP 模式会依赖端口,端口冲突时服务会异常退出。
- 最后再检查钱包备份,看配置是什么时候开始不生效的。
5.3 如何避免再次踩坑
使用个人 MCP wallet 后,团队可以约定“钱包文件入库,密钥不进库”的策略。钱包入库意味着团队所有成员共享同一份服务定义,新人加入时不用靠口口相传;密钥不进库意味着即使仓库泄露,攻击者也拿不到真实 token。这样既保留了协作效率,也把密钥泄露风险控制在可接受范围。
6. 最佳实践与工程建议
6.1 密钥管理
钱包文件本身可以提交到 Git,但包含真实密钥的文件绝对不能提交。推荐的做法是:
- 在钱包 JSON 中只保存
env:ENV_NAME占位符。 - 在
.env文件中保存真实密钥,并且把.env加入.gitignore。 - 导出配置前,先 source 或加载
.env文件。 - 在 CI 或团队内部工具中,使用密钥管理服务注入环境变量。
这条边界一定要守住。很多人觉得“本地工具无所谓”,但本地工具的配置一旦被同步到网盘或仓库,风险就完全不一样了。
6.2 多机同步
个人 MCP wallet 适合配合云盘或 Git 使用。具体方案可以是:
- 钱包目录固定在
~/.mcp-wallet/。 - 将该目录下的
wallet.json纳入 Git 仓库,或者通过软链接指向云盘同步目录。 - 每台开发机只配置不同真实环境变量。
- 在新机器上拉取仓库后,执行
mcp-wallet export claude,得到配置后落到目标客户端。
如果使用 Git 同步,建议不要在仓库中包含backups/目录。备份文件往往带有时间戳,数量多了会造成仓库膨胀。可以把backups/添加到.gitignore,仅在本地保留。
6.3 安全边界
MCP 服务拥有执行能力和数据访问能力,所以在统一管理时尤其要注意安全边界:
- 只添加可信来源的 MCP Server。
- 定位为“管理工具”的 MCP wallet,不应该提供远程控制能力。
- 导出配置前,再次检查
env中是否出现了未脱敏的密钥。 - 非必要不要把 MCP 服务绑定到公网端口;本地开发优先使用 stdio。
- 执行
remove或覆盖写入前,先备份,确认操作有回滚路径。
特别是团队内部共享钱包文件时,要给不同角色的成员设置不同权限。例如普通开发者只需要list和export,管理员才有add和remove权限。如果工具支持多用户,建议引入简单的访问控制,而不是让所有命令无差别开放。
6.4 演进方向
当前这个最小实现还有不少可扩展空间。实际使用一段时间后,可以考虑加入:
doctor命令:自动检查所有服务的启动命令是否可用。- 配置模板:为常见的 MCP Server 提供预设参数。
- 客户端差异化渲染:针对 Claude Desktop、Cursor 等不同配置结构做更精细的输出。
- 生命周期管理:直接由钱包启动或停止本地 MCP Server 进程,便于开发调试。
- 导入导出:支持从已有客户端配置文件导入,降低迁移成本。
如果项目发展到一定规模,还可以用 TypeScript 重写,增强类型安全;引入单元测试,保证存储逻辑和渲染逻辑在长期维护中保持稳定。
7. 总结与下一步
个人 MCP wallet 解决的核心问题是“配置散落、重复连接、密钥难管”。本文从概念出发,解释了 MCP 的基本角色和日常配置痛点;然后设计了一个最小可运行的 CLI 工具,包含钱包存储、服务登记、配置渲染和自动备份;最后给出了一些常见问题和最佳实践。
你可以先照着示例把工具跑起来,添加一两个常用 MCP Server,再尝试把它接入 Claude Desktop 或 Cursor。运行稳定后,再把密钥占位符、Git 同步、备份策略这些工程细节补上。这样就不用在换了客户端或电脑后,一遍遍重复填表式的 MCP 配置了。