MCP协议开发实战:从零搭建AI Agent工具链## 为什么要搞MCP故事要从一次内部hackathon说起。2025年底,我们公司办了一次hackathon,主题是"让AI更懂我们的业务"。我们组选的题目是做一个内部的AI助手,能帮员工查项目进度、查代码仓库、查部署状态、查会议纪要——把散落在各个系统里的信息用自然语言串起来。听起来很简单对吧?不就是给大模型接几个API嘛。我们一开始也是这么想的。第一天晚上就搭了个原型:用Claude的API,写了一堆function calling的定义,把Jira、GitHub、Confluence的API都包了一层。然后问题就来了。Claude的function calling要求你把所有工具的定义都放在一个请求里。我们一共有14个工具(查Jira issue、创建Jira issue、查PR、合并PR、查Wiki、搜索Wiki……),光工具定义的JSON就有3000多token。每次请求都带着这3000 token的工具定义,既浪费钱又影响响应速度。更大的问题是:这些工具定义是硬编码在代码里的。每加一个新工具,就要改代码、重新部署、重启服务。如果让非技术人员自己加工具?想都别想。hackathon结束后,我们没有放弃这个项目,而是开始认真思考:有没有一种标准化的方式,让AI Agent的工具管理变得像USB一样——即插即用?这时候我们发现了MCP(Model Context Protocol)。## 一、MCP协议是什么### 1.1 一句话解释MCP(Model Context Protocol)是Anthropic在2024年底提出的一个开放协议,目标是标准化AI模型与外部数据源/工具之间的通信方式。打个比方:USB协议让各种外设(键盘、鼠标、硬盘、摄像头)能通过统一的接口连接电脑,不需要每个外设都写一套专属驱动。MCP做的事情类似——它让各种工具(数据库查询、API调用、文件操作)能通过统一的协议连接AI模型,不需要每个工具都写一套专属的集成代码。### 1.2 MCP的核心概念MCP协议里有三个核心概念:| 概念 | 类比 | 说明 ||------|------|------|| MCP Server | USB设备 | 提供具体能力的程序,比如一个"Jira查询Server" || MCP Client | USB控制器 | 集成在AI应用中,负责跟Server通信 || Transport | USB线 | 通信通道,支持stdio(本地)和SSE/HTTP(远程) |一个MCP Server可以暴露三种类型的能力:| 能力类型 | 说明 | 示例 ||---------|------|------|| Tools | 可执行的操作 | 查询Jira issue、创建PR、发送消息 || Resources | 可读取的数据源 | 代码仓库文件、配置文件、文档 || Prompts | 预定义的提示模板 | 代码审查模板、Bug分析模板 |### 1.3 MCP vs 传统Function Calling这是大家最关心的问题:既然已经有function calling了,为什么还要MCP?| 维度 | 传统Function Calling | MCP ||------|---------------------|-----|| 工具定义位置 | 硬编码在应用代码中 | 独立在MCP Server中 || 动态添加工具 | 需要改代码重新部署 | 启动新的MCP Server即可 || 工具复用 | 每个AI应用都要单独集成 | 一个Server可以被多个应用使用 || 上下文开销 | 所有工具定义每次请求都带上 | 按需发现和加载 || 跨模型兼容 | 每个模型的格式略有不同 | 统一协议,模型无关 || 权限管理 | 在应用层实现 | 协议层支持 |最关键的区别是解耦。传统function calling里,工具定义和AI应用紧耦合;MCP把它们分开了,工具变成独立的"服务",AI应用只是"客户端"。## 二、架构设计### 2.1 整体架构我们设计的AI Agent工具链架构如下(用表格描述各层职责):| 层级 | 组件 | 职责 | 技术选型 ||------|------|------|---------|| 用户层 | Web UI / Slack Bot | 用户交互入口 | Next.js + Slack Bolt || Agent层 | MCP Client | 调度AI模型和工具 | Claude 3.5 Sonnet || 协议层 | MCP协议 | 标准化通信 | TypeScript SDK || 工具层 | MCP Servers | 具体业务能力 | 各语言实现 || 数据层 | 外部系统 | 业务数据源 | Jira/GitHub/Confluence/MySQL |数据流是这样的:用户在Slack里发消息"帮我看看PROJ-123这个issue的进度" → Agent层收到消息 → Claude判断需要调用Jira查询工具 → MCP Client通过协议向Jira MCP Server发起请求 → Server调用Jira API → 返回结果 → Claude组织语言 → 回复用户。### 2.2 MCP Server规划我们把工具按业务域拆分成多个独立的MCP Server:| Server名称 | 提供的工具 | 连接的系统 | 传输方式 ||-----------|-----------|-----------|---------|| jira-server | 查询/创建/更新Issue | Jira REST API | stdio || github-server | 查询PR/代码搜索/合并PR | GitHub API | stdio || confluence-server | 搜索/读取Wiki | Confluence API | stdio || deploy-server | 查询部署状态/触发部署 | Jenkins API | SSE || meeting-server | 查询会议/获取纪要 | 腾讯会议API | stdio || db-server | 查询业务数据 | PostgreSQL | stdio || search-server | 全文搜索 | Elasticsearch | SSE |为什么不用一个大Server搞定所有工具?因为:1. 独立部署:每个Server可以独立开发、独立部署、独立扩缩容2. 权限隔离:不同Server有不同的权限范围,db-server能访问数据库但jira-server不能3. 技术栈自由:db-server用Python写(因为有现成的SQL工具库),github-server用TypeScript写(因为官方SDK是TS的)4. 故障隔离:一个Server挂了不影响其他Server## 三、从零实现一个MCP Server### 3.1 环境准备我们以Jira MCP Server为例,用TypeScript实现。需要安装的依赖:bashnpm install @modelcontextprotocol/sdknpm install axios # 调用Jira API项目结构:jira-mcp-server/├── src/│ ├── index.ts # 入口,启动Server│ ├── tools/│ │ ├── searchIssues.ts # 搜索Issue│ │ ├── getIssue.ts # 获取Issue详情│ │ ├── createIssue.ts # 创建Issue│ │ └── updateIssue.ts # 更新Issue│ ├── types.ts # 类型定义│ └── jira-client.ts # Jira API封装├── package.json└── tsconfig.json### 3.2 Jira API客户端封装先封装一个Jira API的客户端,处理认证和请求:typescript// src/jira-client.tsimport axios, { AxiosInstance } from 'axios';export class JiraClient { private client: AxiosInstance; constructor(config: { baseUrl: string; email: string; apiToken: string }) { this.client = axios.create({ baseURL: config.baseUrl, auth: { username: config.email, password: config.apiToken, }, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, timeout: 30000, }); } async searchIssues(jql: string, maxResults: number = 50) { const response = await this.client.post('/rest/api/3/search', { jql, maxResults, fields: ['summary', 'status', 'assignee', 'priority', 'created', 'updated'], }); return response.data; } async getIssue(issueKey: string) { const response = await this.client.get(`/rest/api/3/issue/${issueKey}`); return response.data; } async createIssue(data: { projectKey: string; summary: string; description: string; issueType: string; }) { const response = await this.client.post('/rest/api/3/issue', { fields: { project: { key: data.projectKey }, summary: data.summary, description: { type: 'doc', version: 1, content: [{ type: 'paragraph', content: [{ type: 'text', text: data.description }], }], }, issuetype: { name: data.issueType }, }, }); return response.data; } async updateIssue(issueKey: string, fields: Record<string, unknown>) { const response = await this.client.put(`/rest/api/3/issue/${issueKey}`, { fields, }); return response.data; }}这段代码没什么特别的,就是标准的API客户端封装。重点是下面的MCP Server部分。### 3.3 MCP Server实现typescript// src/index.tsimport { Server } from '@modelcontextprotocol/sdk/server/index.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { CallToolRequestSchema, ListToolsRequestSchema,} from '@modelcontextprotocol/sdk/types.js';import { JiraClient } from './jira-client.js';const jiraClient = new JiraClient({ baseUrl: process.env.JIRA_BASE_URL!, email: process.env.JIRA_EMAIL!, apiToken: process.env.JIRA_API_TOKEN!,});const server = new Server( { name: 'jira-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } });// 注册工具列表server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_issues', description: '搜索Jira Issue。使用JQL(Jira Query Language)语法。', inputSchema: { type: 'object', properties: { jql: { type: 'string', description: 'JQL查询语句,例如: project = PROJ AND status = "In Progress"', }, maxResults: { type: 'number', description: '最大返回数量,默认50', default: 50, }, }, required: ['jql'], }, }, { name: 'get_issue', description: '获取指定Issue的详细信息,包括描述、状态、评论等。', inputSchema: { type: 'object', properties: { issueKey: { type: 'string', description: 'Issue编号,例如: PROJ-123', }, }, required: ['issueKey'], }, }, { name: 'create_issue', description: '在指定项目中创建新的Jira Issue。', inputSchema: { type: 'object', properties: { projectKey: { type: 'string', description: '项目编号,例如: PROJ', }, summary: { type: 'string', description: 'Issue标题', }, description: { type: 'string', description: 'Issue描述', }, issueType: { type: 'string', description: 'Issue类型: Task, Bug, Story, Epic', enum: ['Task', 'Bug', 'Story', 'Epic'], }, }, required: ['projectKey', 'summary', 'issueType'], }, }, { name: 'update_issue_status', description: '更新指定Issue的状态。', inputSchema: { type: 'object', properties: { issueKey: { type: 'string', description: 'Issue编号', }, status: { type: 'string', description: '目标状态: To Do, In Progress, In Review, Done', enum: ['To Do', 'In Progress', 'In Review', 'Done'], }, }, required: ['issueKey', 'status'], }, }, ], };});// 处理工具调用server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'search_issues': { const result = await jiraClient.searchIssues( args!.jql as string, args!.maxResults as number ); const issues = result.issues.map((issue: any) => ({ key: issue.key, summary: issue.fields.summary, status: issue.fields.status.name, assignee: issue.fields.assignee?.displayName || '未分配', priority: issue.fields.priority?.name || '无', })); return { content: [{ type: 'text', text: JSON.stringify({ total: result.total, issues }, null, 2), }], }; } case 'get_issue': { const issue = await jiraClient.getIssue(args!.issueKey as string); return { content: [{ type: 'text', text: JSON.stringify({ key: issue.key, summary: issue.fields.summary, status: issue.fields.status.name, assignee: issue.fields.assignee?.displayName || '未分配', priority: issue.fields.priority?.name, created: issue.fields.created, updated: issue.fields.updated, description: issue.fields.description, }, null, 2), }], }; } case 'create_issue': { const result = await jiraClient.createIssue({ projectKey: args!.projectKey as string, summary: args!.summary as string, description: (args!.description as string) || '', issueType: args!.issueType as string, }); return { content: [{ type: 'text', text: JSON.stringify({ key: result.key, self: result.self, message: `Issue ${result.key} 创建成功`, }, null, 2), }], }; } case 'update_issue_status': { // Jira的状态转换需要通过transition API const transitions = await jiraClient.getTransitions(args!.issueKey as string); const targetTransition = transitions.find( (t: any) => t.name === args!.status ); if (!targetTransition) { return { content: [{ type: 'text', text: `无法转换到状态 "${args!.status}"。可用的状态转换: ${transitions.map((t: any) => t.name).join(', ')}`, }], isError: true, }; } await jiraClient.transitionIssue(args!.issueKey as string, targetTransition.id); return { content: [{ type: 'text', text: `Issue ${args!.issueKey} 状态已更新为 ${args!.status}`, }], }; } default: return { content: [{ type: 'text', text: `未知工具: ${name}` }], isError: true, }; } } catch (error: any) { return { content: [{ type: 'text', text: `工具执行失败: ${error.message}`, }], isError: true, }; }});// 启动Serverasync function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Jira MCP Server started');}main().catch(console.error);这段代码的核心逻辑分两部分:1. ListToolsRequestSchema:告诉Client"我有哪些工具",每个工具的名称、描述、参数schema2. CallToolRequestSchema:处理Client发来的工具调用请求,执行具体逻辑并返回结果注意几个细节:- 工具的 description 非常重要,AI模型根据这个描述来决定什么时候调用这个工具。描述写得越清晰,AI选对工具的概率越高- inputSchema 用的是JSON Schema格式,跟OpenAI的function calling格式兼容- 返回结果统一用 content 数组,支持text、image等多种类型- 错误处理用 isError: true 标记,让AI知道工具调用失败了### 3.4 编译和运行bash# 编译TypeScriptnpx tsc# 设置环境变量export JIRA_BASE_URL="https://yourcompany.atlassian.net"export JIRA_EMAIL="your-email@company.com"export JIRA_API_TOKEN="your-api-token"# 运行Servernode dist/index.jsServer启动后会通过stdio等待Client的连接和请求。它本身不对外提供HTTP接口——通信完全通过标准输入输出进行。## 四、与Claude和Cursor集成### 4.1 与Claude Desktop集成Claude Desktop原生支持MCP。只需要在配置文件里加上你的Server信息即可。配置文件路径:- macOS: ~/Library/Application Support/Claude/claude_desktop_config.json- Windows: %APPDATA%\Claude\claude_desktop_config.json配置内容:json{ "mcpServers": { "jira": { "command": "node", "args": ["/path/to/jira-mcp-server/dist/index.js"], "env": { "JIRA_BASE_URL": "https://yourcompany.atlassian.net", "JIRA_EMAIL": "your-email@company.com", "JIRA_API_TOKEN": "your-api-token" } }, "github": { "command": "node", "args": ["/path/to/github-mcp-server/dist/index.js"], "env": { "GITHUB_TOKEN": "ghp_xxxxxxxxxxxx" } }, "confluence": { "command": "node", "args": ["/path/to/confluence-mcp-server/dist/index.js"], "env": { "CONFLUENCE_BASE_URL": "https://yourcompany.atlassian.net/wiki", "CONFLUENCE_EMAIL": "your-email@company.com", "CONFLUENCE_API_TOKEN": "your-api-token" } } }}配置完成后重启Claude Desktop,它会在启动时自动连接这些MCP Server。然后你就可以在对话中直接使用这些工具了:用户:帮我查一下PROJ项目里所有状态为In Progress的BugClaude:(自动调用jira MCP Server的search_issues工具,JQL: project = PROJ AND status = "In Progress" AND issuetype = Bug)找到了3个符合条件的Issue:1. PROJ-234 - 支付回调偶发性超时 - 负责人: 张三 - 优先级: High2. PROJ-289 - 用户头像上传后显示旋转 - 负责人: 李四 - 优先级: Medium 3. PROJ-301 - 导出Excel时内存溢出 - 负责人: 王五 - 优先级: High需要我查看某个Issue的详情吗?### 4.2 与Cursor集成Cursor从0.42版本开始支持MCP。在Cursor的设置里有一个"MCP Servers"页面,可以添加和管理MCP Server。添加方式跟Claude Desktop类似,填入Server的启动命令和环境变量即可。Cursor会在你使用Chat功能时自动发现可用的MCP工具。一个实际的使用场景:在Cursor里选中一段代码,然后在Chat里说"帮我查一下这段代码相关的Jira issue"——Cursor会调用Jira MCP Server搜索相关issue,然后把issue信息带回来帮你理解代码的业务背景。### 4.3 集成对比| 维度 | Claude Desktop | Cursor ||------|---------------|--------|| MCP支持版本 | 原生支持 | 0.42+ || 工具发现 | 自动 | 自动 || 工具调用透明度 | 不显示调用过程 | 显示调用过程 || 多Server支持 | 支持 | 支持 || SSE传输 | 不支持 | 支持 || 配置方式 | JSON配置文件 | UI界面配置 || 使用体验 | 对话中自然调用 | 编辑器内集成调用 |Claude Desktop的优势是对话体验好,适合做"问答型"的Agent。Cursor的优势是跟代码编辑器深度集成,适合做"开发辅助型"的Agent。## 五、踩过的坑这一节可能是全文最有价值的部分。### 5.1 坑一:工具描述写不好,AI就调用不对MCP工具的 description 字段是AI决定是否调用这个工具的唯一依据。描述写得模糊,AI就会调用错误的工具,或者在不该调用的时候调用。反面示例typescript{ name: 'search', description: '搜索功能', inputSchema: { ... }}这种描述等于没写。AI不知道这是搜索什么的、用什么语法搜索、返回什么结果。正面示例typescript{ name: 'search_issues', description: '搜索Jira Issue。使用JQL(Jira Query Language)语法。常用JQL示例:project = PROJ AND status = "In Progress" 查找进行中的issue;assignee = currentUser() AND priority = High 查找分配给当前用户的高优先级issue。', inputSchema: { ... }}好的描述应该包含三个要素:这个工具做什么、用什么参数、给个例子。把例子写在描述里,AI的调用准确率会显著提升。### 5.2 坑二:stdio传输模式的坑stdio是MCP默认的传输方式——Server通过标准输入输出跟Client通信。这个模式有一个隐蔽的坑:你不能在代码里往stdout写任何非MCP协议的内容。我有一次在代码里加了一行 console.log('调试信息'),结果Claude Desktop直接报错说协议解析失败。原因很简单:MCP协议通过stdout传输JSON-RPC消息,你的console.log也往stdout写,把协议消息搞乱了。解决方案:所有调试日志用 console.error 输出到stderr,不要用 console.log。MCP协议不使用stderr,所以stderr可以随便写。typescript// ❌ 错误:会破坏MCP协议console.log('Server started');// ✅ 正确:输出到stderr,不影响协议console.error('Server started');### 5.3 坑三:工具返回的数据量太大有一次用户问"把PROJ项目所有未关闭的issue列出来",我们的search_issues工具一口气返回了800多个issue的完整信息,包括描述、评论、附件列表……返回的JSON有200KB。Claude收到这200KB的数据后,回复的答案反而不是很好——信息太多了,它在组织答案时丢失了一些关键信息。而且这么大的上下文会显著增加token消耗和响应延迟。解决方案:在工具层做分页和数据裁剪:typescriptcase 'search_issues': { const result = await jiraClient.searchIssues(jql, 20); // 限制20条 // 只返回关键字段,不返回完整内容 const issues = result.issues.map((issue: any) => ({ key: issue.key, summary: issue.fields.summary, status: issue.fields.status.name, assignee: issue.fields.assignee?.displayName || '未分配', priority: issue.fields.priority?.name || '无', updated: issue.fields.updated, })); return { content: [{ type: 'text', text: JSON.stringify({ total: result.total, returned: issues.length, note: result.total > 20 ? `共${result.total}条结果,当前显示前20条。使用更精确的JQL缩小范围。` : undefined, issues, }, null, 2), }], };}关键原则:返回AI需要的信息,不要返回所有信息。如果用户需要某个issue的详情,AI会再调用 get_issue 工具去获取。### 5.4 坑四:环境变量管理每个MCP Server都需要一些配置(API Token、Base URL等)。在本地开发时直接写环境变量没问题,但在生产环境部署时,环境变量的管理就变得复杂了。我们踩过的坑:- 多个Server需要同一个Jira Token,但各自从环境变量读取,改一个要改多处- 开发环境和生产环境的配置混在一起,容易出错- Token轮换时需要逐个Server修改解决方案:统一用 .env 文件管理,Server启动时从同一个 .env 读取:bash# .env.shared - 所有Server共享的配置JIRA_BASE_URL=https://yourcompany.atlassian.netJIRA_EMAIL=bot@company.comJIRA_API_TOKEN=xxxGITHUB_TOKEN=ghp_xxxCONFLUENCE_BASE_URL=https://yourcompany.atlassian.net/wiki``````typescript// 所有Server的入口统一加载环境变量import dotenv from 'dotenv';dotenv.config({ path: process.env.MCP_ENV_FILE || '.env.shared' });### 5.5 坑五:SSE传输模式的连接稳定性我们有一个deploy-server用的是SSE(Server-Sent Events)传输模式,因为它需要部署在远程服务器上而不是本地。SSE模式的问题是连接不够稳定——网络波动、代理超时、防火墙都可能导致连接断开。Claude Desktop在SSE连接断开时不会自动重连,需要手动重启。这在开发阶段还能接受,但在给团队使用时就不可接受了。解决方案:在SSE Server端加心跳机制,在Client端加重连逻辑。或者更好的方案是——如果工具对实时性要求不高,尽量用stdio模式,把Server部署在本地。虽然这限制了远程访问的能力,但稳定性好得多。我们的最终方案是:大部分Server用stdio模式跑在本地,只有deploy-server和search-server(需要访问内网Elasticsearch)用SSE模式跑在远程,并加了完善的断线重连机制。## 六、完整的MCP工具列表最终我们搭建的MCP工具链包含7个Server,共34个工具:| Server | 工具名 | 功能 | 调用频率 ||--------|-------|------|---------|| jira-server | search_issues | 搜索Jira Issue | 高 || | get_issue | 获取Issue详情 | 高 || | create_issue | 创建Issue | 中 || | update_issue_status | 更新Issue状态 | 中 || | get_sprint_info | 获取Sprint信息 | 中 || github-server | search_code | 搜索代码 | 高 || | get_pr | 获取PR详情 | 高 || | list_prs | 列出PR | 中 || | merge_pr | 合并PR | 低 || | get_commit | 获取提交信息 | 中 || confluence-server | search_pages | 搜索Wiki | 高 || | get_page | 获取Wiki页面 | 中 || | create_page | 创建Wiki页面 | 低 || deploy-server | get_deploy_status | 查询部署状态 | 高 || | trigger_deploy | 触发部署 | 低 || | get_deploy_log | 获取部署日志 | 中 || | rollback_deploy | 回滚部署 | 低 || meeting-server | list_meetings | 列出会议 | 中 || | get_meeting_minutes | 获取会议纪要 | 中 || | get_meeting_recording | 获取会议录像链接 | 低 || db-server | query_data | 查询业务数据 | 中 || | get_table_schema | 获取表结构 | 低 || | list_tables | 列出所有表 | 低 || search-server | fulltext_search | 全文搜索 | 高 || | index_document | 索引文档 | 低 |"调用频率"这一列是我们统计了一个月的使用数据后标注的。高频工具(搜索类)占了总调用量的70%以上,低频工具(创建类、合并类)虽然调用少但价值很高——因为它们通常代表了一个重要操作的自动化。## 七、实际使用效果### 7.1 典型使用场景工具链上线后,团队最常用的几个场景:场景一:晨会前的准备以前晨会前要打开Jira看自己的Issue、打开GitHub看有没有待审查的PR、打开Confluence看有没有新的设计文档。现在直接在Claude里问:> “帮我整理一下今天的待办:我在PROJ项目中In Progress的Issue、分配给我审查的PR、以及昨天更新的跟我相关的Wiki页面。“Claude会同时调用3个MCP Server(jira、github、confluence),把结果整合成一个清单。场景二:代码审查前的上下文准备在Cursor里审查一个PR时,选中代码问:“这段代码对应哪个Jira issue?设计文档在哪?“Cursor会调用github-server获取PR的描述(里面通常有issue链接),然后调用jira-server获取issue详情,再调用confluence-server搜索相关设计文档。整个过程几秒钟完成,以前需要手动在三个系统之间来回切换。场景三:故障排查线上出了问题,直接问:“最近一次部署是什么时候?部署了哪些改动?“Claude调用deploy-server获取最近的部署记录,再调用github-server获取那次部署包含的commit,然后总结出部署的时间、内容和可能相关的改动。### 7.2 效能数据| 指标 | 工具链上线前 | 上线后(3个月) | 变化 ||------|------------|---------------|------|| 查询信息平均耗时 | 3-5min(多系统切换) | 10-15秒(一句话) | -95% || 晨会准备时间 | 10min | 1min | -90% || PR审查上下文准备 | 5min | 30秒 | -90% || 新人上手查询频率 | 高(到处问人) | 低(问AI) | -70% || 跨系统信息整合 | 手动 | 自动 | — |## 八、经验总结和未来规划### 8.1 经验总结回顾整个MCP工具链的搭建过程,有几条核心经验:第一,工具描述是第一优先级。 AI能不能正确使用你的工具,90%取决于描述写得好不好。花时间打磨描述,比加更多工具有用得多。第二,从高频低风险的工具开始。 先上搜索类工具(查Issue、查PR、查文档),这些工具只读不写,出问题也不会有太大影响。等团队习惯了再逐步加入写操作工具(创建Issue、合并PR、触发部署)。第三,返回数据要精简。 AI不是数据库,不需要把所有字段都返回。返回关键字段,让AI在需要时再调用详情工具。这既节省token又提高回答质量。第四,权限控制不可忽视。 我们的create_issue和merge_pr工具在上线初期只允许特定人员使用。通过在MCP Server层做用户身份验证和权限检查,确保只有授权用户才能执行写操作。第五,监控和日志很重要。 每个工具的调用次数、成功率、平均耗时都需要监控。我们用Prometheus + Grafana搭了一套监控面板,能实时看到哪个Server出了问题。### 8.2 MCP vs 自建Function Calling的对比最终我们用MCP替代了原来自建的function calling方案,体感差异:| 维度 | 自建Function Calling | MCP方案 ||------|---------------------|---------|| 新增工具开发时间 | 2-3天(含集成测试) | 0.5-1天(只需实现Server) || 工具复用 | 每个AI应用单独集成 | 一次实现,多端使用 || 维护成本 | 高(代码耦合) | 低(独立部署) || 非技术人员加工具 | 不可能 | 配置即可(用通用Server) || 调试体验 | 好(代码里打断点) | 一般(通过日志调试) || 生态 | 自建自用 | 可使用社区Server |MCP最大的优势不是技术上的(function calling也能做到同样的事),而是标准化带来的生态效应。当越来越多的工具都以MCP Server的形式存在时,你可以直接拿来用,不需要自己写集成。### 8.3 未来规划我们接下来打算做几件事:工具市场:搭建一个内部的MCP Server注册中心,让各业务团队可以发布和订阅MCP Server。类似于内部的"工具App Store”。自然语言建工具:用AI帮非技术人员创建简单的MCP Server。比如HR同学描述"我想要一个能查询员工请假记录的工具”,AI自动生成对应的MCP Server代码。多模态支持:目前的工具主要返回文本,后续要支持返回图表、图片等。比如查询到数据后自动生成可视化图表返回给用户。Agent编排:从"单轮工具调用"进化到"多步骤Agent编排”。比如用户说"帮我创建一个Bug的Issue,然后把相关代码链接关联上去,再通知负责人”,这需要多个工具的有序编排,而不只是单个工具的调用。## 九、MCP生态观察与选型建议在我们开发MCP工具链的这几个月里,MCP生态也在快速变化。观察到一些趋势,跟想入坑的同学分享。### 9.1 官方和社区Server的现状Anthropic官方已经发布了一批参考实现,包括文件系统、GitHub、Slack、Google Drive等常用Server。社区里也有人贡献了各种第三方Server——数据库查询、日志搜索、邮件发送等等。我们在选型时做了一个简单的分类梳理:| 来源 | Server类型 | 成熟度 | 适用场景 ||------|-----------|--------|---------|| Anthropic官方 | filesystem, github, slack | 高 | 通用场景,直接可用 || 社区开源 | postgres, mysql, elasticsearch | 中等 | 需要审查代码后使用 || 社区开源 | jira, confluence, linear | 中低 | 功能可能不完整,需自行补充 || 自研 | 内部系统专用 | 定制 | 只适用于本团队 |我们的策略是:能直接用官方的就用官方的(比如github-server直接用了官方实现),社区实现的选择性使用(postgres-server用了一个star数较高的社区版本但做了一些修改),内部系统全部自研。一个重要提醒:使用第三方MCP Server时一定要审查代码。MCP Server本质上是一个能执行操作的程序——如果它里面藏了恶意代码(比如把你的API Key发送到外部服务器),后果很严重。我们定了一个规则:所有第三方Server必须经过代码审查后才能接入生产环境。### 9.2 MCP vs 其他Agent框架市面上还有不少AI Agent框架——LangChain、AutoGPT、CrewAI等等。经常有人问:既然有这些框架了,为什么还要用MCP?我的理解是,它们解决的不是同一个层面的问题:| 框架 | 核心解决的问题 | 与MCP的关系 ||------|--------------|------------|| LangChain | Agent的推理逻辑和工具编排 | 可以用MCP作为工具来源 || AutoGPT | 自主Agent的端到端执行 | 可以通过MCP接入更多工具 || CrewAI | 多Agent协作编排 | MCP提供工具能力,CrewAI负责编排 || MCP | 工具与AI模型的标准化连接 | 底层协议,可被上层框架使用 |MCP不是LangChain的替代品,它更像是"工具层的USB标准”。你完全可以用LangChain做Agent编排,同时用MCP管理工具——两者是互补关系,不是竞争关系。我们团队目前的架构就是这样的:MCP负责工具的注册、发现和执行,Claude负责理解用户意图和选择工具,上层有一个轻量的编排逻辑处理多步骤任务。没有用LangChain,因为我们的编排需求还不复杂,自己写几十行代码就搞定了。但如果后面需要更复杂的多Agent协作,可能会引入LangChain或类似的框架。### 9.3 成本考量搭建MCP工具链的成本主要分三块:| 成本项 | 月费用 | 说明 ||--------|--------|------|| Claude API调用 | ~$200 | 团队日均约200次对话,每次平均消耗2000 token || 服务器费用 | ~$50 | 跑SSE模式Server的两台轻量云服务器 || 开发维护人力 | 0.2 FTE | 一名工程师兼职维护和迭代 || 合计 | ~$250 + 0.2 FTE | |对于15人规模的团队来说,这个成本是完全可以接受的。而且如果按节省的时间来算——团队每周节省的信息查询时间至少有15小时,远超工具链的维护成本。### 9.4 给想入门的开发者的建议如果你看完这篇文章想动手试试MCP,我建议按这个顺序来:第一步:用Claude Desktop跑一个官方Server不需要写任何代码。下载Claude Desktop,在配置文件里加上官方的filesystem-server,然后跟Claude说"帮我读一下桌面上的文件列表”。这个体验能让你直观感受MCP的工作方式。第二步:写一个最简单的自定义Server不用搞Jira、GitHub这些复杂的,就写一个"查询当前天气"的Server。用TypeScript SDK,大概50行代码就能搞定。重点是理解ListTools和CallTool这两个handler的作用。第三步:接入一个真实的业务系统选一个你日常工作最常用的系统——Jira、GitHub、飞书文档、企业内部的知识库都行。给它写一个MCP Server,暴露2-3个查询工具。然后在Claude Desktop或Cursor里实际使用它。第四步:考虑部署和权限如果你打算给团队使用,就需要考虑Server的部署方式(本地还是远程)、权限控制(谁能用哪些工具)、监控告警(Server挂了怎么知道)这些问题。这一步最花时间,但也最重要。## 十、写在最后MCP协议目前还在早期阶段,社区在快速发展,协议本身也在迭代。但它解决的问题——AI模型与外部工具的标准化连接——是一个真实且重要的需求。从我们的实践来看,MCP已经可以用于生产环境,但需要你对协议有足够的理解,并且愿意花时间在工具描述、错误处理、监控告警这些"脏活"上。技术本身的门槛不高,真正难的是把工具设计得好用——让AI能准确理解什么时候该用什么工具,返回什么信息。如果你也在做AI Agent相关的项目,强烈建议试一试MCP。哪怕只是用Claude Desktop加一个简单的Server跑个demo,也能让你对"AI Agent怎么连接外部世界"有一个全新的理解。有问题欢迎评论区交流,我会尽量回复。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐