公司动态

MCP协议实战:为AI编程助手构建代码库记忆宫殿

📅 2026/7/28 8:11:33
MCP协议实战:为AI编程助手构建代码库记忆宫殿
最近在尝试让 AI 理解我们公司那个庞大而古老的代码库时真是踩了不少坑。无论是 Claude、Cursor 还是 GitHub Copilot面对一个包含数十万行代码、架构复杂、依赖关系盘根错节的私有项目时它们常常表现得像“失忆”一样上下文窗口一满对项目整体的理解就支离破碎。开发者们普遍面临一个困境如何让 AI 助手真正“记住”并理解整个代码库的上下文而不仅仅是当前打开的几份文件就在这个痛点被反复提及的时候一个名为MCPModel Context Protocol的项目在 GitHub 上迅速走红。它并非一个全新的 AI 模型而是一个旨在解决上述问题的协议和工具集。简单来说MCP 就像是为 AI 大模型搭建的一座“记忆宫殿”或“外部知识库”允许你将整个代码库、文档、API 规范等结构化地“喂”给 AI从而突破其原生上下文长度的限制实现更精准、更连贯的代码理解和生成。本文将深入拆解 MCP 的核心概念、工作原理并提供一个从零开始的实战教程教你如何为自己的私有代码库构建一个 MCP 服务器最终让 Claude、Cursor 等 AI 助手能够“秒懂”你的百万行代码。无论你是想提升现有开发效率还是探索 AI 编程的边界这篇文章都将提供一套完整的落地方案。1. MCP 是什么为什么它能解决 AI 的“代码失忆症”在深入实操之前我们必须先理解 MCP 要解决的根本问题以及它的设计哲学。1.1 核心问题AI 的上下文限制与代码理解碎片化当前主流的 AI 编程助手其能力严重受限于“上下文窗口”Context Window。这个窗口就像 AI 的“短期工作记忆”。例如Claude 3 的上下文可能达到 20 万 token听起来很多但对于一个大型项目来说可能只够放入十分之一的源代码。当你询问一个涉及多个模块、深层依赖的问题时AI 很可能因为“看不到”相关的代码而给出错误或片面的答案。更糟糕的是这种“记忆”是临时的。关闭对话或切换文件后AI 之前“学习”到的项目信息就丢失了下一次又得重新开始。这导致了 AI 对代码库的理解始终是碎片化和表面化的。1.2 MCP 的解决方案外部化、持久化的上下文管理MCPModel Context Protocol由 Anthropic 公司提出它是一个开放协议定义了一套标准化的方式让 AI 应用程序客户端能够与各种数据源服务器进行安全、高效的交互。你可以把 MCP 想象成 AI 的“外接硬盘”和“索引系统”服务器Server 负责管理具体的资源Resources和工具Tools。对于代码库场景一个 MCP 服务器可以扫描你的整个项目为其建立索引如代码符号、依赖图、文档并将这些信息以结构化的方式暴露出来。客户端Client 如 Claude Desktop、Cursor 或任何集成了 MCP 的 IDE 插件。它连接到 MCP 服务器按需查询“这个函数在哪里被调用”或执行操作“在指定位置实现这个接口”。协议Protocol 规定了客户端和服务器之间通信的语言通常是 JSON-RPC over stdio/HTTP确保不同团队开发的服务器和客户端可以无缝协作。MCP 的核心价值在于突破上下文限制 AI 无需将整个代码库塞进提示词。当需要时它通过 MCP 协议向服务器发起一个精准查询服务器返回最相关的代码片段或信息AI 再基于这些“新鲜”的上下文生成回答。实现持久化记忆 服务器端建立的索引是持久化的。AI 每次对话都能访问到同一份完整、一致的项目知识图谱实现了对代码库的“长期记忆”。能力可扩展 MCP 服务器不仅可以提供代码搜索还能集成数据库 Schema、内部 API 文档、部署脚本、甚至执行命令行工具极大扩展了 AI 助手的实操能力边界。2. 环境准备与核心工具在开始构建我们自己的代码库 MCP 服务器之前需要准备好相应的环境和工具。2.1 基础环境要求操作系统 macOS, Linux, 或 Windows (WSL 2 推荐用于 Windows 用户)。Node.js 版本 18 或更高。这是构建和运行大多数 JavaScript/TypeScript 生态 MCP 工具的基础。包管理工具 npm 或 yarn。Python 版本 3.8可选部分索引工具或示例可能用到。Git 用于克隆示例项目和管理你的代码库。2.2 核心工具安装我们将使用由 Anthropic 官方维护的modelcontextprotocol/sdk来快速开发 MCP 服务器。同时为了建立代码索引一个高效且流行的选择是opencodegraph/scip-typescript它基于 SCIPSource Code Intelligence Protocol协议能为 TypeScript/JavaScript 项目生成高质量的符号索引。首先创建一个项目目录并初始化mkdir my-codebase-mcp-server cd my-codebase-mcp-server npm init -y安装必要的依赖npm install modelcontextprotocol/sdk npm install --save-dev typescript ts-node types/node # 安装代码索引器 npm install opencodegraph/scip-typescript初始化 TypeScript 配置npx tsc --init修改生成的tsconfig.json确保设置适合 Node.js 项目{ compilerOptions: { target: ES2022, module: commonjs, lib: [ES2022], outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true }, include: [src/**/*], exclude: [node_modules, dist] }2.3 目标代码库准备为了演示你可以使用任何一个现有的 TypeScript/JavaScript 项目。如果没有可以快速创建一个示例项目或者直接克隆一个开源项目作为测试目标。# 示例创建一个简单的测试项目 mkdir -p /tmp/test-repo/src cd /tmp/test-repo npm init -y npm install express在/tmp/test-repo/src/index.ts中写入一些代码// /tmp/test-repo/src/index.ts import express, { Request, Response } from express; interface User { id: number; name: string; email: string; } const app express(); const PORT 3000; app.use(express.json()); const users: User[] [ { id: 1, name: Alice, email: aliceexample.com }, { id: 2, name: Bob, email: bobexample.com }, ]; /** * 获取所有用户列表 * returns 用户数组 */ export function getAllUsers(): User[] { return users; } /** * 根据ID查找用户 * param id 用户ID * returns 找到的用户或undefined */ export function findUserById(id: number): User | undefined { return users.find(user user.id id); } app.get(/users, (req: Request, res: Response) { res.json(getAllUsers()); }); app.get(/users/:id, (req: Request, res: Response) { const userId parseInt(req.params.id, 10); const user findUserById(userId); if (user) { res.json(user); } else { res.status(404).json({ error: User not found }); } }); app.listen(PORT, () { console.log(Server is running at http://localhost:${PORT}); });这个简单的 Express 应用包含了接口定义、函数、路由足以演示 MCP 的代码理解能力。3. 构建你的第一个代码库 MCP 服务器现在我们来一步步实现一个能够为上述或你自己的代码库提供智能查询的 MCP 服务器。3.1 项目结构与核心文件在我们的my-codebase-mcp-server项目中创建如下结构my-codebase-mcp-server/ ├── src/ │ ├── index.ts # MCP 服务器主入口 │ ├── codebase.ts # 代码库索引与查询逻辑 │ └── types.ts # 类型定义 ├── scripts/ │ └── generate-index.ts # 生成代码索引的脚本 ├── dist/ # TypeScript 编译输出目录 ├── package.json └── tsconfig.json3.2 生成代码索引知识图谱首先我们需要一个脚本利用opencodegraph/scip-typescript来扫描目标代码库并生成一个包含所有符号函数、类、接口、变量等及其位置、关系的索引文件通常是.scip格式。创建scripts/generate-index.ts// scripts/generate-index.ts import { generateIndex } from opencodegraph/scip-typescript; import * as path from path; import * as fs from fs; const TARGET_REPO_PATH /tmp/test-repo; // 替换成你的目标代码库路径 const OUTPUT_INDEX_PATH path.join(__dirname, .., index.scip); async function main() { console.log(开始为代码库生成索引: ${TARGET_REPO_PATH}); try { const index await generateIndex({ projectRoot: TARGET_REPO_PATH, // 可以配置需要排除的文件夹如 node_modules, dist 等 excludePatterns: [**/node_modules/**, **/dist/**, **/*.test.*], }); // 将索引写入文件 fs.writeFileSync(OUTPUT_INDEX_PATH, JSON.stringify(index, null, 2)); console.log(索引已成功生成并保存至: ${OUTPUT_INDEX_PATH}); console.log(共索引了 ${index.documents?.length || 0} 个文档。); } catch (error) { console.error(生成索引时发生错误:, error); process.exit(1); } } main();在package.json中添加一个脚本命令{ scripts: { generate-index: ts-node scripts/generate-index.ts, build: tsc, start: node dist/index.js } }运行索引生成命令npm run generate-index如果成功你会在项目根目录看到一个index.scip文件。这个文件就是你的代码库的“知识图谱”它以一种结构化的格式记录了所有代码符号的信息。3.3 实现 MCP 服务器接下来我们实现 MCP 服务器的核心。创建src/index.ts// src/index.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from modelcontextprotocol/sdk/types.js; import { CodebaseManager } from ./codebase.js; // 初始化代码库管理器加载我们生成的索引 const codebaseManager new CodebaseManager(./index.scip); // 创建 MCP 服务器实例 const server new Server( { name: my-codebase-mcp-server, version: 0.1.0, }, { capabilities: { resources: {}, // 声明我们支持资源 tools: {}, // 声明我们支持工具 }, } ); // 1. 列出可用的资源这里我们可以把整个代码库或特定模块作为资源 server.setRequestHandler(ListResourcesRequestSchema, async () { return { resources: [ { uri: codebase:///, mimeType: application/json, name: My Codebase Index, description: The entire indexed codebase for querying., }, // 未来可以添加更多资源如特定文件、文档等 ], }; }); // 2. 处理读取资源的请求客户端请求获取资源内容 server.setRequestHandler(ReadResourceRequestSchema, async (request) { const { uri } request.params; if (uri codebase:///) { // 当客户端请求“根资源”时我们可以返回索引的摘要信息 const summary codebaseManager.getCodebaseSummary(); return { contents: [ { uri: uri, mimeType: application/json, text: JSON.stringify(summary, null, 2), }, ], }; } // 可以扩展其他 URI 模式的处理逻辑 throw new Error(Resource not found: ${uri}); }); // 3. 列出可用的工具即客户端可以调用的函数 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: search_symbol, description: 在代码库中搜索符号函数、类、接口、变量等。, inputSchema: { type: object, properties: { query: { type: string, description: 要搜索的符号名称或部分名称。, }, kind: { type: string, description: 符号类型过滤如 function, class, interface。可选。, enum: [function, class, interface, variable, type, all], }, limit: { type: number, description: 返回结果的最大数量。, }, }, required: [query], }, }, { name: find_references, description: 查找某个符号在代码库中的所有引用位置。, inputSchema: { type: object, properties: { symbolName: { type: string, description: 需要查找引用的符号全名。, }, filePath: { type: string, description: 符号所在文件的路径有助于精确定位。, }, }, required: [symbolName], }, }, { name: get_function_details, description: 获取函数的详细信息包括签名、参数、返回类型和文档注释。, inputSchema: { type: object, properties: { functionName: { type: string, description: 函数名称。, }, filePath: { type: string, description: 函数所在文件的路径。, }, }, required: [functionName], }, }, ], }; }); // 4. 处理工具调用请求这是核心交互逻辑 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; try { switch (name) { case search_symbol: { const { query, kind all, limit 10 } args as any; const results await codebaseManager.searchSymbol(query, kind, limit); return { content: [ { type: text, text: JSON.stringify(results, null, 2), }, ], }; } case find_references: { const { symbolName, filePath } args as any; const references await codebaseManager.findReferences(symbolName, filePath); return { content: [ { type: text, text: JSON.stringify(references, null, 2), }, ], }; } case get_function_details: { const { functionName, filePath } args as any; const details await codebaseManager.getFunctionDetails(functionName, filePath); return { content: [ { type: text, text: JSON.stringify(details, null, 2), }, ], }; } default: throw new Error(Unknown tool: ${name}); } } catch (error) { console.error(Error executing tool ${name}:, error); return { content: [ { type: text, text: Error: ${(error as Error).message}, }, ], isError: true, }; } }); // 启动服务器使用标准输入输出进行通信这是 MCP 客户端的标准连接方式 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(My Codebase MCP Server is running on stdio...); } runServer().catch((error) { console.error(Server fatal error:, error); process.exit(1); });3.4 实现代码库管理器这是服务器的“大脑”负责加载 SCIP 索引文件并提供查询功能。创建src/codebase.ts// src/codebase.ts import * as fs from fs; import * as path from path; // 简化版的 SCIP 索引类型定义实际应使用 sourcegraph/scip 类型包 interface SCIPIndex { documents?: Array{ relative_path: string; occurrences?: Array{ range: number[]; symbol: string; symbol_roles?: number; }; symbols?: Array{ symbol: string; documentation?: string[]; }; }; // ... 其他字段 } export class CodebaseManager { private index: SCIPIndex | null null; private indexPath: string; constructor(indexPath: string) { this.indexPath path.resolve(indexPath); this.loadIndex(); } private loadIndex(): void { try { const data fs.readFileSync(this.indexPath, utf-8); this.index JSON.parse(data) as SCIPIndex; console.error(Codebase index loaded from ${this.indexPath}); } catch (error) { console.error(Failed to load index from ${this.indexPath}:, error); this.index { documents: [] }; } } public getCodebaseSummary(): any { if (!this.index) { return { error: Index not loaded }; } const docCount this.index.documents?.length || 0; let symbolCount 0; this.index.documents?.forEach(doc { symbolCount doc.occurrences?.length || 0; }); return { documents: docCount, symbols: symbolCount, indexPath: this.indexPath, }; } public async searchSymbol(query: string, kind: string all, limit: number 10): Promiseany[] { // 简化的搜索实现在实际项目中应使用更高效的索引库如 Lunr.js 或 FlexSearch const results: any[] []; if (!this.index?.documents) { return results; } const lowerQuery query.toLowerCase(); for (const doc of this.index.documents) { if (!doc.occurrences) continue; for (const occ of doc.occurrences) { if (occ.symbol.toLowerCase().includes(lowerQuery)) { // 这里可以添加基于 kind 的过滤逻辑需要从符号字符串中解析类型 results.push({ symbol: occ.symbol, file: doc.relative_path, range: occ.range, // [startLine, startChar, endLine, endChar] }); if (results.length limit) { return results; } } } } return results; } public async findReferences(symbolName: string, filePathHint?: string): Promiseany[] { const results: any[] []; if (!this.index?.documents) { return results; } for (const doc of this.index.documents) { if (!doc.occurrences) continue; for (const occ of doc.occurrences) { if (occ.symbol symbolName) { results.push({ file: doc.relative_path, range: occ.range, }); } } } return results; } public async getFunctionDetails(functionName: string, filePathHint?: string): Promiseany { // 这是一个简化示例。完整实现需要解析 SCIP 符号信息中的文档字符串和类型详情。 const searchResults await this.searchSymbol(functionName, function, 1); if (searchResults.length 0) { return { error: Function ${functionName} not found. }; } const result searchResults[0]; // 在实际中这里应该从 this.index 的 symbols 字段中提取更详细的信息 return { name: functionName, location: ${result.file}:${result.range[0] 1}, signature: function ${functionName}(...), // 应从索引中获取真实签名 documentation: Documentation extracted from JSDoc/TSDoc comments., // 应从索引中获取真实文档 }; } }3.5 编译并运行服务器首先编译 TypeScript 代码npm run build然后你可以直接运行编译后的文件npm start如果一切正常服务器将在标准输入输出上启动并等待连接。但通常我们不会直接运行它而是通过 MCP 客户端如 Claude Desktop来连接。4. 连接 MCP 服务器与 AI 客户端以 Claude Desktop 为例目前最成熟的 MCP 客户端是Claude Desktop应用。下面演示如何配置 Claude Desktop 来使用我们刚刚构建的代码库 MCP 服务器。4.1 配置 Claude DesktopClaude Desktop 的配置文件通常位于macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json如果文件不存在请创建它。编辑该文件添加我们的 MCP 服务器配置{ mcpServers: { my-codebase: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/my-codebase-mcp-server/dist/index.js ], env: { // 可以在这里传递环境变量如代码库路径 CODEBASE_PATH: /tmp/test-repo } } } }重要将/ABSOLUTE/PATH/TO/YOUR/my-codebase-mcp-server/dist/index.js替换为你项目dist/index.js的绝对路径。4.2 重启 Claude Desktop 并验证保存配置文件后完全退出并重新启动 Claude Desktop 应用。启动后打开与 Claude 的对话。你可以尝试向 Claude 提问它会自动利用我们配置的 MCP 服务器工具。例如你可以问“请使用search_symbol工具在我的代码库里搜索名为findUserById的函数。”Claude 会识别出这是一个工具调用请求并通过 MCP 协议将请求发送给我们的服务器。服务器执行搜索并返回结果Claude 再将结果整合到回复中。回复可能类似于“我在你的代码库中找到了findUserById函数。它位于文件src/index.ts的大约第 20 行。这是一个根据 ID 查找用户的函数返回User类型或undefined。”你还可以问更复杂的问题“getAllUsers函数在哪些地方被引用了”Claude 会调用find_references工具并告诉你这个函数在/users路由处理程序中被调用。通过这种方式Claude 获得了“透视”整个代码库的能力即使这个代码库有百万行代码它也能通过 MCP 协议快速定位到相关信息从而给出极其精准的回答、建议甚至代码修改。5. 常见问题与排查思路在搭建和使用 MCP 服务器的过程中你可能会遇到以下问题问题现象常见原因解决思路Claude Desktop 启动后没有识别 MCP 服务器1. 配置文件路径或格式错误。2. Node.js 路径或服务器脚本路径错误。3. Claude Desktop 版本过旧。1. 检查配置文件路径和 JSON 语法。2. 确保command和args中的路径是绝对路径且可执行。3. 升级 Claude Desktop 到最新版本。运行npm start后服务器立即退出1. TypeScript 编译错误。2. 依赖未安装。3. 代码中存在未处理的同步错误。1. 运行npm run build查看编译错误。2. 确保已运行npm install。3. 在runServer()函数外添加try-catch并打印日志。服务器已运行但 Claude 调用工具时报错或超时1. MCP 协议通信错误。2. 工具处理函数抛出异常。3. 索引文件损坏或路径不对。1. 检查服务器控制台输出的错误信息。2. 在工具调用处理函数中加强错误捕获和日志。3. 确认index.scip文件存在且是有效的 JSON。搜索符号返回的结果不准确或为空1. 索引生成不完整如排除了某些目录。2. 搜索算法过于简单。3. 目标代码库不是 TypeScript/JavaScript。1. 检查generate-index.ts中的excludePatterns。2. 考虑引入专业的全文检索库如Lunr.js替代简单遍历。3.opencodegraph/scip-typescript只适用于 TS/JS。对于其他语言需寻找或编写对应的 SCIP 索引器。索引生成速度慢对于超大项目卡住目标代码库过大一次性索引内存不足或超时。1. 增加 Node.js 内存限制NODE_OPTIONS--max-old-space-size8192。2. 考虑分模块、分目录进行增量索引。3. 优化excludePatterns排除更多无关文件。6. 进阶优化与最佳实践上面的示例是一个最小可行产品MVP。要让它在生产环境中真正发挥威力需要考虑以下优化点6.1 提升索引质量与性能使用更强大的索引器opencodegraph/scip-typescript是一个好的开始。对于企业级应用可以考虑使用Sourcegraph的本地部署 (src命令) 或Zoekt来建立全局代码搜索索引它们能提供更快、更准确的符号搜索和交叉引用。增量索引 监听代码库的 git 钩子或文件系统事件在代码变更时只更新受影响文件的索引避免全量重建。多语言支持 如果你的代码库是多语言的需要为每种语言配置相应的 SCIP 索引器如scip-java,scip-python等并在 MCP 服务器中合并这些索引。6.2 丰富 MCP 服务器功能更多工具get_file_content: 直接获取指定文件的原始内容。explain_code: 对指定代码块进行解释可结合本地 LLM。suggest_refactor: 基于代码坏味道建议重构。run_tests: 执行与当前文件相关的单元测试。资源分级 不仅提供整个代码库资源还可以提供“模块级”、“文件级”资源让 AI 客户端能更精细地获取上下文。访问控制 在服务器端实现权限校验确保 AI 客户端只能访问被授权的代码库和操作。6.3 工程化部署进程管理 使用pm2或systemd管理 MCP 服务器进程确保其高可用。配置化 将目标代码库路径、索引策略、排除规则等提取到配置文件如config.yaml中。日志与监控 集成 Winston、Pino 等日志库记录所有工具调用和错误便于问题排查和用量分析。容器化 使用 Docker 封装 MCP 服务器及其依赖实现环境一致性和快速部署。6.4 安全边界沙箱化工具执行 对于run_tests、execute_command这类可能执行代码的工具必须在严格的沙箱环境如 Docker 容器、nsjail中运行防止任意命令执行风险。输入验证与净化 对所有从客户端传入的参数进行严格的验证和净化防止路径遍历、命令注入等攻击。最小权限原则 MCP 服务器进程应以最低必要权限运行避免直接使用 root 或高权限账户。MCP 协议为 AI 与开发者工具深度集成打开了一扇新的大门。它不仅仅是让 AI“记住”更多代码更是构建了一个可编程的、标准化的接口层让 AI 能够安全、可控地操作整个软件开发环境。从代码搜索、依赖分析到自动化测试和部署其想象空间巨大。本文提供的实战指南是一个起点你可以在此基础上根据自己团队的技术栈和需求打造出真正专属的、智能化的开发助手让“百万行代码AI 秒懂”成为日常开发的常态。