1. 为什么我要把项目交给 Claude 来管
先说结论:Claude Desktop 本身不能直接读你硬盘上的项目文件,但通过 MCP(Model Context Protocol)挂一个文件管理工具,它就能在你的授权目录里读代码、列目录、写文件、找 TODO。我现在的日常是:打开 Claude Desktop,问一句「帮我看看 src 下有哪些没处理的 TODO」,它直接给我按文件分组的结果,不用我切编辑器、不用我复制粘贴。
这套东西适合谁?三类人最合适:一是手上同时用 Claude Desktop、Cursor、命令行脚本,Key 散在四五个地方,想统一收口的;二是想让 AI 真正碰到项目文件、而不是只在聊天框里空谈的;三是想给团队搭一套「AI 能安全访问指定目录」骨架的。核心检索词就三个:MCP、Claude Desktop、统一 Key 接入。
我踩过的坑是:一开始每个工具单独配 Key,Claude 一个、Cursor 一个、脚本里再写一个,改一次 Key 要翻五个配置文件,还容易漏。后来把通道统一到 TaoToken 一个 Key 上,MCP 服务器只认环境变量,配置文件骨架固定下来,换 Key 只改一处。这篇就把这套骨架和验证动作完整给你,照着做能跑通。
2. TaoToken 前置:一个 Key 打通 MCP 与 Claude
MCP 服务器本质是个本地进程,它要调用模型能力时,得有个 API 通道。如果每个 MCP 工具、每个客户端各配各的 Key,管理成本会爆炸。TaoToken 在这里的角色是「统一入口」:你拿一个 Key,MCP 服务器、Claude Desktop、Cursor、脚本都指向同一个 API 地址,通道统一、额度统一、换 Key 只改一处。
具体要准备的东西:
- 一个 TaoToken 账号,登录后进控制台创建 API Key。地址走 https://taotoken.net/api ,Key 管理在 console 里,接入文档在 doc 里,模型对话验证在模型对话页。
- 本地装好 Node.js 18+,因为 MCP 服务器用 TypeScript 写,跑起来要 node。
- Claude Desktop 装好,配置文件路径记住:macOS 是
~/Library/Application Support/Claude/claude_desktop_config.json,Windows 是%APPDATA%/Claude/claude_desktop_config.json。
注意:Key 只放环境变量或本地配置文件,别提交到 git。MCP 配置文件里用
env字段传,不要硬编码进源码。
拿 Key 的入口我建议直接走 API Keys 页面,创建后复制一次,后面所有配置都用它。接入文档里有各语言的调用示例,MCP 服务器里我们只用它的 base URL 和 Key。
3. 可复制配置:MCP 配置文件骨架
这一节是全文核心,给你一份能直接改改就用的骨架。分三块:MCP 服务器代码骨架、Claude Desktop 配置、环境变量。
3.1 MCP 服务器代码骨架
先建项目:
mkdir mcp-project-manager && cd mcp-project-manager npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node ts-nodetsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }package.json脚本:
{ "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts" } }核心服务器代码src/index.ts,这里把「统一 Key 通道」和「文件管理」两件事都放进去:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import * as fs from "fs/promises"; import * as path from "path"; const ALLOWED_DIR = process.env.MCP_ALLOWED_DIR || process.cwd(); const API_BASE = process.env.TAOTOKEN_API_BASE || "https://taotoken.net/api"; const API_KEY = process.env.TAOTOKEN_API_KEY || ""; function isPathSafe(filePath: string): boolean { const resolved = path.resolve(filePath); return resolved.startsWith(path.resolve(ALLOWED_DIR)); } const server = new McpServer({ name: "project-manager-mcp", version: "1.0.0", }); server.tool( "read_file", z.object({ path: z.string().describe("要读取的文件路径") }), async ({ path: filePath }) => { if (!isPathSafe(filePath)) { return { content: [{ type: "text", text: "Access denied" }], isError: true }; } try { const content = await fs.readFile(filePath, "utf-8"); return { content: [{ type: "text", text: content }] }; } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true }; } } ); server.tool( "list_dir", z.object({ path: z.string().describe("要列出的目录路径") }), async ({ path: dirPath }) => { if (!isPathSafe(dirPath)) { return { content: [{ type: "text", text: "Access denied" }], isError: true }; } const entries = await fs.readdir(dirPath, { withFileTypes: true }); const list = entries.map((e) => `${e.isDirectory() ? "[D]" : "[F]"} ${e.name}`).join("\n"); return { content: [{ type: "text", text: list }] }; } ); server.tool( "write_file", z.object({ path: z.string().describe("要写入的文件路径"), content: z.string().describe("文件内容"), }), async ({ path: filePath, content }) => { if (!isPathSafe(filePath)) { return { content: [{ type: "text", text: "Access denied" }], isError: true }; } await fs.writeFile(filePath, content, "utf-8"); return { content: [{ type: "text", text: `Written: ${filePath}` }] }; } ); server.tool("channel_info", {}, async () => { return { content: [ { type: "text", text: JSON.stringify( { apiBase: API_BASE, keyLoaded: API_KEY.length > 0, allowedDir: ALLOWED_DIR }, null, 2 ), }, ], }; }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error(`MCP Server running. Allowed dir: ${ALLOWED_DIR}`); } main().catch(console.error);这段代码里channel_info工具是专门用来验证统一 Key 通道是否加载成功的,后面验证环节会用到。
3.2 Claude Desktop 配置骨架
编辑claude_desktop_config.json,把 MCP 服务器挂上去,同时把 TaoToken 的 Key 和 API 地址通过env注入:
{ "mcpServers": { "project-manager": { "command": "node", "args": ["/absolute/path/to/mcp-project-manager/dist/index.js"], "env": { "MCP_ALLOWED_DIR": "/Users/yourname/projects/my-app", "TAOTOKEN_API_BASE": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "你的Key" } } } }三个关键点:args里必须是编译后dist/index.js的绝对路径;MCP_ALLOWED_DIR是你允许 Claude 访问的项目根目录,别写/或用户主目录;TAOTOKEN_API_KEY填你从 console 拿到的 Key。
注意:Windows 路径用双反斜杠或正斜杠,比如
C:/Users/yourname/projects/my-app,别用单反斜杠,JSON 会解析失败。
3.3 编译并确认产物
npm run build ls dist/index.js看到dist/index.js存在就说明编译通过。这一步不做,Claude Desktop 启动时会因为找不到入口文件而静默失败,你只会看到工具列表是空的。
4. 验证请求:确认 MCP 加载与文件读写
配置写完不算完,得验证。我分两步:先确认 MCP 工具加载成功,再确认文件读写正常。
4.1 确认 MCP 工具加载成功
重启 Claude Desktop,完全退出再打开。然后在对话框里输入:
用 channel_info 工具告诉我当前通道信息如果配置正确,Claude 会调用channel_info并返回类似:
{ "apiBase": "https://taotoken.net/api", "keyLoaded": true, "allowedDir": "/Users/yourname/projects/my-app" }keyLoaded: true说明统一 Key 通道注入成功,allowedDir是你配的目录。如果 Claude 说「没有这个工具」,说明 MCP 服务器没加载,去第 5 节排查。
4.2 确认项目文件读写正常
接着验证文件操作。先列目录:
用 list_dir 列出项目根目录应该返回[D] src、[F] package.json这类条目。再读一个文件:
用 read_file 读一下 package.json 的前 20 行Claude 会把内容贴出来。最后验证写入,让它建一个测试文件:
用 write_file 在项目根目录创建 mcp-test.txt,内容写 "hello from mcp"然后你在终端cat mcp-test.txt,看到hello from mcp就说明读写链路全通。验证完记得删掉测试文件。
4.3 一次完整的项目查询动作
把上面串起来,问 Claude:
列出 src 目录,找出所有包含 TODO 的文件,按文件分组告诉我它会先list_dir,再逐个read_file,最后给你分组结果。这就是「让 Claude 管理项目」的最小可用形态。整个过程它只在你授权的MCP_ALLOWED_DIR里活动,越界会被isPathSafe拦掉。
5. 本篇常见错排查
配置 MCP 最容易卡在几个地方,我按出现频率排一下。
工具列表为空,Claude 说没有工具。九成是配置文件路径或 JSON 格式问题。先确认你编辑的是 Claude Desktop 真正读取的那个文件,macOS 和 Windows 路径不一样,别搞混。再用python -m json.tool claude_desktop_config.json校验 JSON 合法性,少个逗号都会导致整个配置被忽略。改完必须完全退出 Claude Desktop 再重启,不是关窗口。
服务器启动即退出。在终端手动跑node dist/index.js,看报什么错。常见的是 Node 版本低于 18、依赖没装全、dist/index.js不存在(忘了npm run build)。手动跑能起来,说明是 Claude Desktop 的args路径写错了,检查是不是绝对路径。
Access denied。说明isPathSafe拦住了。检查MCP_ALLOWED_DIR和你请求的路径是不是同一个根,相对路径会被path.resolve解析成当前工作目录,容易对不上。统一用绝对路径最省心。
keyLoaded 是 false。说明TAOTOKEN_API_KEY没注入进去。检查env字段拼写,Key 有没有多余空格,以及是不是把 Key 写到了args里而不是env里。Key 管理去 API Keys 页面重新复制一次,接入细节看接入文档。
改了代码但行为没变。MCP 服务器是编译后运行的,改src/index.ts后必须重新npm run build,再重启 Claude Desktop。只重启不重新编译,跑的还是旧dist。
6. 把通道收口,把骨架留下
这套东西跑通之后,我最大的感受是「配置骨架比代码本身值钱」。代码你可以按需加工具,但骨架一旦固定——统一 Key 走环境变量、MCP 服务器只认TAOTOKEN_API_BASE和TAOTOKEN_API_KEY、访问范围锁在MCP_ALLOWED_DIR——后面加多少工具都不会乱。
如果你只是想让 Claude 验证模型输出,直接去模型对话页试就行,不用搭 MCP。如果你要长期用 Claude 做编码、跑 Agent 任务,建议把 Coding Plan 用起来,配合这套 MCP 骨架,Key 和通道都收口到一处,换环境只改env三行。接入文档里有完整的参数说明,照着改不会错。
最后留个实用技巧:把claude_desktop_config.json和 MCP 项目的env模板一起放进项目仓库的.example文件里,Key 留空,新人 clone 下来填自己的 Key 就能跑。这样团队里每个人的 Key 独立,通道统一,谁也不用翻别人的配置。