1. 项目概述:为什么MCP突然火了?
最近在技术社区和面试准备中,一个词的出现频率越来越高:MCP,全称 Model Context Protocol。如果你正在关注大模型应用开发,或者准备相关岗位的面试,却对这个概念感到既熟悉又陌生,那你来对地方了。简单来说,MCP 是一个旨在解决大模型与外部工具、数据源之间“连接”问题的标准化协议。它不是某个具体的工具或框架,而是一套“通信规则”。
想象一下,你开发了一个强大的 AI 助手,你想让它能帮你查天气、读数据库、操作文件系统。传统做法是,你需要为每个功能写一套特定的代码,告诉模型如何调用这些 API,处理各种不同的数据格式和错误。这个过程繁琐、不通用,且难以维护。MCP 的出现,就是为了定义一套统一的“语言”,让大模型(如 Claude、GPT)能够以一种标准化的方式发现、描述并调用任何外部资源(服务器、工具、数据源),而无需为每个资源编写特定的适配代码。这就像为互联网世界定义了 HTTP 协议一样,MCP 试图为大模型的“工具使用”世界建立一个通用的通信基础。
从面试角度看,理解 MCP 意味着你抓住了当前大模型应用架构的一个核心演进方向:从封闭的、定制化的智能体,转向开放的、可插拔的智能体平台。它考察的不仅是协议本身,更是你对大模型能力边界、扩展性设计以及未来生态发展的思考。
2. MCP 核心概念与架构拆解
要理解 MCP,不能只停留在“协议”这个词上。我们需要把它拆解成几个核心组成部分,看看它到底是如何工作的。
2.1 核心角色:Server, Client 与 Resources
MCP 的架构非常清晰,主要包含三个角色:
MCP 服务器 (Server):这是提供能力的“服务方”。一个 MCP 服务器可以暴露多种“资源”给模型使用。这些资源可以是:
- 工具 (Tools):可执行的操作,例如“搜索网络”、“执行SQL查询”、“发送邮件”。每个工具都有名称、描述、输入参数定义。
- 提示词模板 (Prompts):预定义的、参数化的提示词片段,模型可以调用并填充具体参数,用于生成更精准的指令或内容。
- 数据资源 (Resources):只读的数据源,例如一个数据库表的模式定义、一份项目文档、一个实时股票行情数据流。它们以 URI 的形式标识,内容可以是文本、JSON 或其他格式。
MCP 客户端 (Client):这是消费能力的“调用方”。通常,这就是我们使用的 AI 应用平台本身,例如 Claude Desktop、Cursor IDE,或者任何集成了 MCP 客户端库的应用。客户端的核心职责是:
- 连接并管理一个或多个 MCP 服务器。
- 将服务器提供的资源列表“呈现”给内部的大模型。
- 接收模型的指令,将其转化为对特定服务器工具的调用,并将结果返回给模型。
- 处理资源(如提示词、数据)的读取请求。
大模型 (LLM):虽然模型本身不是 MCP 协议的直接参与方,但它是整个流程的“大脑”。客户端将 MCP 服务器提供的工具和资源描述作为上下文(Context)的一部分提供给模型。模型根据用户的问题,决定是否需要、以及如何调用这些工具,并生成包含工具调用请求的回复。
这个架构的关键在于“解耦”。工具提供者(Server)和工具消费者(Client/LLM)通过 MCP 这个标准接口连接,彼此独立演化。开发者可以专注于编写好用的工具服务器,而 AI 应用开发者则可以轻松集成海量工具,无需关心其内部实现。
2.2 协议基石:SSE 与 JSON-RPC
MCP 不是一个全新的网络协议,它巧妙地建立在现有成熟技术之上,这降低了实现和使用的门槛。
通信层:SSE (Server-Sent Events):MCP 服务器和客户端之间通常使用 SSE 进行通信。这是一种基于 HTTP 的轻量级协议,允许服务器主动向客户端推送数据流。对于 MCP 来说,这非常合适,因为服务器需要主动通知客户端新资源的可用性(例如,文件系统监视器发现新文件),而客户端也需要向服务器发送请求并接收异步响应。相比 WebSocket,SSE 更简单,天然支持 HTTP 生态,对于工具调用这种请求-响应模式为主,辅以少量服务器推送的场景,是更优雅的选择。
消息层:JSON-RPC 2.0:在 SSE 的数据流中,传输的具体消息格式遵循 JSON-RPC 2.0 规范。这是一个非常轻量级的远程过程调用协议。每一条消息都是一个 JSON 对象,包含
jsonrpc: “2.0”、method(方法名)、params(参数)和id(请求ID)等字段。- 客户端调用工具时,会发送一个
request消息,method为tools/call。 - 服务器执行工具后,会返回一个
response消息,包含结果或错误信息。 - 服务器也可以主动发送
notification消息(无id)来通知客户端资源列表的变更。
- 客户端调用工具时,会发送一个
这种设计意味着,只要你熟悉 HTTP 和 JSON,实现或理解一个基本的 MCP 服务器/客户端在技术上并不复杂。协议本身的复杂性被转移到了“资源模型”的定义和语义上。
2.3 与相似概念的对比:Plugin, Function Calling, Skill
面试中常被问到:“MCP 和 OpenAI 的 Function Calling、ChatGPT Plugins 有什么区别?” 厘清这个概念至关重要。
OpenAI Function Calling / Assistant API Tools:这是 OpenAI 为其模型定义的一套工具调用规范。它规定了模型如何理解工具、如何输出工具调用请求。然而,它没有规定工具从哪里来、如何被发现、如何被管理。它更像是模型侧的“接口定义语言”(IDL)。MCP 可以看作是这套规范在工具供给和管理层面的补充和标准化。一个 MCP 客户端可以将服务器提供的工具,转换成 OpenAI 的 Function Calling 格式,再交给 GPT 模型使用。
ChatGPT Plugins / GPTs:这是 OpenAI 在其 ChatGPT 产品中构建的封闭生态。开发者需要遵循 OpenAI 的审核和发布流程,将工具(以特定 API 格式描述)集成到 ChatGPT 中。这是一个平台锁定的解决方案。MCP 则是一个开放协议,任何兼容 MCP 的客户端(如 Claude Desktop, Cursor)都可以连接任何 MCP 服务器,实现了跨平台的工具共享。
Claude Skills (已弃用):Anthropic 最初为其 Claude 模型设计了 Skills 系统,功能上与早期的插件类似,但同样局限于 Claude 生态。MCP 实际上是 Anthropic 推出的、用于取代 Skills 的、更开放的方案。MCP 不是 Claude 专属的,它是一个开放标准,理论上任何大模型平台都可以采用。
简单总结:Function Calling 是“怎么叫”,MCP 是“叫什么”和“从哪叫”。Plugins/Skills 是“苹果商店”,MCP 是“USB-C 接口标准”。
3. 从零实现一个简易 MCP 服务器
理解了概念,最好的巩固方式就是动手实现。我们将用 Node.js 实现一个最简单的 MCP 服务器,它提供一个“查询当前时间”的工具和一个“获取服务器信息”的只读资源。
3.1 环境准备与项目初始化
首先,确保你的系统安装了 Node.js (版本 18 或以上)。我们使用@modelcontextprotocol/sdk这个官方 SDK,它能极大简化开发。
# 1. 创建项目目录并初始化 mkdir my-first-mcp-server cd my-first-mcp-server npm init -y # 2. 安装 MCP SDK 和必要的依赖 npm install @modelcontextprotocol/sdk创建一个入口文件server.js。
3.2 构建服务器骨架与工具定义
MCP SDK 采用了基于类(Class)的清晰结构。我们首先导入 SDK,然后创建一个继承自Server的类。
// server.js const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } = require('@modelcontextprotocol/sdk/types.js'); class MyFirstMCPServer { constructor() { // 1. 初始化 MCP Server 实例 this.server = new Server( { name: 'my-first-mcp-server', version: '1.0.0', }, { capabilities: { // 声明本服务器支持的能力:工具、资源、提示词等 tools: {}, resources: {}, // prompts: {}, // 本例暂不实现提示词 }, } ); // 2. 设置请求处理器 this.setupRequestHandlers(); // 3. 初始化工具和资源列表(模拟数据) this.tools = [ { name: 'get_current_time', description: '获取服务器的当前日期和时间', inputSchema: { type: 'object', properties: { // 这个工具不需要输入参数,所以 properties 为空对象 }, }, }, ]; this.resources = [ { uri: 'file:///server/info', name: '服务器信息', description: '关于这个 MCP 服务器的基本信息', mimeType: 'text/plain', }, ]; } setupRequestHandlers() { // 处理客户端查询工具列表的请求 this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, })), })); // 处理客户端查询资源列表的请求 this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: this.resources.map(resource => ({ uri: resource.uri, name: resource.name, description: resource.description, mimeType: resource.mimeType, })), })); // 处理客户端调用工具的请求 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'get_current_time') { // 执行工具逻辑 const now = new Date(); const timeString = now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, }); return { content: [ { type: 'text', text: `当前服务器时间(北京时间)是:${timeString}`, }, ], }; } // 如果收到未知的工具调用请求,抛出错误 throw new Error(`未知的工具:${name}`); }); // 处理客户端读取资源的请求 this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; if (uri === 'file:///server/info') { const infoText = `服务器名称:${this.server.serverInfo.name}\n版本:${this.server.serverInfo.version}\n这是一个用于演示的简易 MCP 服务器。`; return { contents: [ { uri: uri, mimeType: 'text/plain', text: infoText, }, ], }; } throw new Error(`资源未找到:${uri}`); }); } async run() { // 使用标准输入输出(stdio)作为传输层。 // 这是 MCP 服务器最常见的运行方式,由客户端(如 Claude Desktop)启动并管理其生命周期。 const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('MCP 服务器已启动,正在通过 stdio 通信...'); } } // 启动服务器 const serverInstance = new MyFirstMCPServer(); serverInstance.run().catch(console.error);代码解析与注意事项:
- 能力声明 (Capabilities):在
Server初始化时,必须明确声明服务器支持哪些功能(tools,resources,prompts)。即使初始为空对象也要声明,这是协议的要求。 - 请求处理器 (Request Handlers):这是服务器的核心。我们为四种标准请求设置了处理器:
listTools,listResources,callTool,readResource。SDK 提供了强类型的 Schema 来确保我们处理正确的请求格式。 - 工具定义:每个工具都需要
name(唯一标识符)、description(供模型理解用途)和inputSchema(定义输入参数的 JSON Schema)。即使没有参数,inputSchema也必须是一个有效的 JSON Schema 对象。 - 资源定义:资源通过
uri唯一标识。mimeType告诉客户端如何解释内容。资源是只读的,服务器可以主动通知客户端资源列表的变化(本例未演示)。 - 传输层 (Transport):
StdioServerTransport是最常用的方式。这意味着服务器通过命令行标准输入/输出与客户端通信。客户端(如 Claude Desktop)会以子进程形式启动这个服务器脚本,并通过管道进行通信。这种方式部署简单,无需处理网络端口。
3.3 配置客户端(以 Claude Desktop 为例)
要让 Claude Desktop 使用我们的服务器,需要创建一个配置文件。
找到 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
- macOS:
编辑(或创建)
claude_desktop_config.json文件,添加mcpServers配置项:
{ "mcpServers": { "my-time-server": { "command": "node", "args": [ "/ABSOLUTE/PATH/TO/YOUR/my-first-mcp-server/server.js" ], "env": { "NODE_ENV": "development" } } } }重要提示:
command必须是能在系统 PATH 中找到的命令(如node,python)。args中的路径必须使用绝对路径。Windows 用户注意路径分隔符(\)可能需要转义或使用正斜杠(/)。
- 保存配置文件,并完全重启 Claude Desktop 应用(不是关闭聊天窗口,而是退出整个应用再重新打开)。
3.4 测试与验证
重启 Claude Desktop 后,新建一个对话。如果配置成功,你应该能在输入框上方或模型回复中,看到 Claude 已经识别到了新的工具。你可以尝试提问:“现在几点了?” 或者 “使用 get_current_time 工具”。Claude 应该会理解你的意图,并调用我们编写的服务器工具,返回当前时间。
你也可以尝试让 Claude “读取服务器信息”,它会调用readResource来获取我们定义的file:///server/info资源内容。
常见启动问题排查:
- “Failed to start server”:检查配置文件 JSON 格式是否正确,路径是否存在且为绝对路径,Node.js 是否已安装并可在命令行中运行。
- 工具不出现:检查 Claude Desktop 是否已完全重启。查看 Claude Desktop 的应用日志(通常可在配置目录找到或通过开发者工具查看),里面会有 MCP 服务器启动失败的详细错误信息。
- 权限问题:确保脚本文件有可执行权限,并且 Node 命令可用。
4. 深入进阶:构建一个实用的文件系统 MCP 服务器
一个“查询时间”的服务器是很好的入门,但缺乏实用性。接下来,我们构建一个更贴近真实场景的服务器:一个文件系统浏览器。它将提供工具(如列出目录、读取文件)和资源(以file://URI 形式暴露文件内容)。
4.1 设计工具集与资源模型
我们的文件系统服务器将提供以下功能:
工具 (Tools):
list_directory:列出指定路径下的文件和子目录。- 输入参数:
path(字符串,目录路径)。 - 输出:结构化的列表,包含名称、类型(文件/目录)、大小等。
- 输入参数:
read_file:读取指定文件的内容。- 输入参数:
path(字符串,文件路径)。 - 输出:文件内容的文本。
- 输入参数:
search_files:在指定目录下递归搜索包含特定文本的文件。- 输入参数:
directory(字符串,搜索根目录),query(字符串,搜索关键词)。 - 输出:匹配的文件路径列表。
- 输入参数:
资源 (Resources):
- 我们将文件系统映射为资源。例如,文件
/home/user/document.txt可以作为一个资源,其 URI 为file:///home/user/document.txt。当客户端(模型)需要引用某个文件内容时,可以直接通过这个 URI 请求,服务器返回文件内容。
4.2 实现细节与安全考量
实现这个服务器,核心挑战在于安全性和错误处理。我们绝不能允许模型通过服务器任意访问整个文件系统。
// file_server.js const fs = require('fs/promises'); const path = require('path'); const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } = require('@modelcontextprotocol/sdk/types.js'); class FileSystemMCPServer { constructor(allowedBasePath) { // 将允许访问的根路径解析为绝对路径,并规范化 this.allowedBasePath = path.resolve(allowedBasePath); // 安全检查:确保 basePath 存在且是一个目录 fs.access(this.allowedBasePath, fs.constants.R_OK) .then(() => fs.stat(this.allowedBasePath)) .then(stats => { if (!stats.isDirectory()) { throw new Error(`配置的路径 ${this.allowedBasePath} 不是一个目录`); } }) .catch(err => { console.error(`初始化错误:无法访问基础路径 ${this.allowedBasePath}`, err); process.exit(1); }); this.server = new Server( { name: 'file-system-server', version: '1.0.0', }, { capabilities: { tools: {}, resources: { subscribe: false }, // 我们不支持资源动态订阅 }, } ); this.setupRequestHandlers(); } // 关键安全函数:将用户提供的路径解析并限制在 allowedBasePath 下 resolveSafePath(userPath) { if (!userPath || typeof userPath !== 'string') { throw new Error('路径参数无效'); } // 解析用户路径(可能是相对路径或绝对路径) const requestedPath = path.resolve(this.allowedBasePath, userPath); const normalizedRequested = path.normalize(requestedPath); // 安全检查:确保解析后的路径仍在允许的根目录之下 if (!normalizedRequested.startsWith(this.allowedBasePath + path.sep) && normalizedRequested !== this.allowedBasePath) { throw new Error(`访问被拒绝:路径 ${userPath} 超出了允许的范围 (${this.allowedBasePath})`); } return normalizedRequested; } setupRequestHandlers() { // 列出工具 this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'list_directory', description: '列出指定目录下的文件和子文件夹', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '要列出的目录路径(相对于服务器配置的根目录)', }, }, required: ['path'], }, }, { name: 'read_file', description: '读取指定文本文件的内容', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '要读取的文件路径(相对于服务器配置的根目录)', }, }, required: ['path'], }, }, { name: 'search_files', description: '在指定目录下递归搜索包含特定文本的文件(仅限文本文件)', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: '搜索的起始目录路径', }, query: { type: 'string', description: '要搜索的文本内容', }, }, required: ['directory', 'query'], }, }, ], })); // 列出资源(这里我们动态生成:将 allowedBasePath 下的文件视为资源) // 注意:对于大型目录树,动态列出所有资源不现实。通常这里返回一个“根”资源或空列表。 // 更常见的做法是,当模型通过工具发现某个文件后,再通过其 file:// URI 来读取。 this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: `file://${this.allowedBasePath}`, name: '文件系统根目录', description: `可访问的文件系统根目录:${this.allowedBasePath}`, mimeType: 'application/json', // 目录列表可以用JSON表示 }, ], })); // 调用工具 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'list_directory': { const safePath = this.resolveSafePath(args.path); const stats = await fs.stat(safePath); if (!stats.isDirectory()) { throw new Error(`路径 ${args.path} 不是一个目录`); } const items = await fs.readdir(safePath, { withFileTypes: true }); const list = await Promise.all( items.map(async (item) => { const itemPath = path.join(safePath, item.name); const itemStats = await fs.stat(itemPath); return { name: item.name, type: item.isDirectory() ? 'directory' : 'file', size: itemStats.size, modified: itemStats.mtime.toISOString(), }; }) ); return { content: [{ type: 'text', text: `目录 ${args.path} 的内容:\n` + list.map(it => `- [${it.type}] ${it.name} (${it.size} bytes)`).join('\n'), }], }; } case 'read_file': { const safePath = this.resolveSafePath(args.path); const stats = await fs.stat(safePath); if (!stats.isFile()) { throw new Error(`路径 ${args.path} 不是一个文件`); } // 简单检查文件大小,避免读取超大文件 if (stats.size > 1024 * 1024) { // 1MB throw new Error(`文件过大(${stats.size} bytes),出于安全考虑拒绝读取`); } const content = await fs.readFile(safePath, 'utf-8'); return { content: [{ type: 'text', text: `文件 ${args.path} 的内容:\n---\n${content}\n---`, }], }; } case 'search_files': { const safeDir = this.resolveSafePath(args.directory); const results = []; // 一个简单的递归搜索函数(注意:对于深层目录,可能需要优化或限制深度) const search = async (dir, query) => { const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); try { if (item.isDirectory()) { // 递归搜索子目录 await search(fullPath, query); } else if (item.isFile()) { // 检查文件扩展名,只读取可能的文本文件 const ext = path.extname(item.name).toLowerCase(); const textExtensions = ['.txt', '.md', '.json', '.js', '.py', '.html', '.css']; if (textExtensions.includes(ext)) { const stats = await fs.stat(fullPath); if (stats.size < 1024 * 1024) { // 小于1MB const content = await fs.readFile(fullPath, 'utf-8'); if (content.includes(query)) { // 计算相对路径,便于用户理解 const relativePath = path.relative(this.allowedBasePath, fullPath); results.push(relativePath); } } } } } catch (err) { // 忽略无权限访问的文件 console.error(`搜索时跳过 ${fullPath}:`, err.message); } } }; await search(safeDir, args.query); return { content: [{ type: 'text', text: `在目录 ${args.directory} 中搜索 "${args.query}" 的结果(${results.length} 个匹配项):\n` + (results.length > 0 ? results.map(r => `- ${r}`).join('\n') : '未找到匹配文件。'), }], }; } default: throw new Error(`未知的工具:${name}`); } } catch (error) { // 统一错误处理,返回给客户端 return { content: [{ type: 'text', text: `执行工具 "${name}" 时出错:${error.message}`, }], isError: true, }; } }); // 读取资源(处理 file:// URI) this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; // 检查是否是我们的 file:// 资源 if (uri.startsWith('file://')) { // 移除 file:// 协议头,得到文件系统路径 const filePath = uri.slice(7); // 注意:这里简化处理,实际应处理不同操作系统的路径和编码 try { const safePath = this.resolveSafePath(filePath); const stats = await fs.stat(safePath); if (!stats.isFile()) { throw new Error(`URI ${uri} 不是一个文件`); } if (stats.size > 1024 * 1024) { throw new Error(`文件过大,拒绝读取`); } const content = await fs.readFile(safePath, 'utf-8'); // 尝试根据文件扩展名推断 MIME 类型 const ext = path.extname(safePath).toLowerCase(); const mimeMap = { '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.js': 'application/javascript', '.html': 'text/html', '.css': 'text/css', }; const mimeType = mimeMap[ext] || 'text/plain'; return { contents: [{ uri: uri, mimeType: mimeType, text: content, }], }; } catch (error) { throw new Error(`读取资源 ${uri} 失败:${error.message}`); } } throw new Error(`不支持的资源 URI 模式:${uri}`); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`文件系统 MCP 服务器已启动,根目录:${this.allowedBasePath}`); } } // 从环境变量或命令行参数获取允许访问的根目录 const allowedPath = process.env.ALLOWED_PATH || process.argv[2] || process.cwd(); const server = new FileSystemMCPServer(allowedPath); server.run().catch(console.error);关键实现解析与避坑指南:
路径安全解析 (
resolveSafePath):这是整个服务器的安全基石。必须将用户输入的路径解析并严格限制在预设的allowedBasePath之下。使用path.resolve和path.normalize处理..等相对路径符号,然后检查解析后的路径是否以allowedBasePath开头。绝对不要直接使用用户输入的路径访问文件系统。错误处理与用户反馈:所有文件操作(
fs.stat,fs.readdir)都必须用try...catch包裹。错误信息应清晰反馈给用户(通过模型),但避免泄露服务器内部路径等敏感信息。MCP 响应中的isError: true字段可以标记这是一个错误响应。资源与工具的协同:在这个设计中,工具(如
list_directory)用于发现和操作,而资源(file://URI)用于让模型直接引用文件内容。当模型通过list_directory工具看到一个文件后,它可以在后续对话中直接说“请分析file:///project/README.md的内容”,客户端会自动发起readResource请求。性能与限制:
search_files工具是递归的,对于大型目录树可能很慢甚至导致服务器无响应。在实际产品中,必须添加深度限制、超时机制,或使用更高效的搜索库。读取文件也应有大小限制,防止服务器内存被耗尽。配置化:通过环境变量 (
ALLOWED_PATH) 或命令行参数来指定可访问的根目录,使得服务器更灵活、更安全。
4.3 部署与客户端配置进阶
现在,我们可以用更安全的方式配置 Claude Desktop 来使用这个服务器:
{ "mcpServers": { "my-file-server": { "command": "node", "args": [ "/path/to/your/file_server.js", "/Users/YourName/SafeProjectFolder" // 只允许访问这个文件夹 ] } } }这样,模型就只能在你指定的SafeProjectFolder内进行操作,无法越界。
5. MCP 生态、最佳实践与面试思考
5.1 现有生态与工具
MCP 虽然年轻,但生态正在快速成长。了解这些现有工具能帮你更好地理解其应用场景:
官方与社区服务器:
brave-search-mcp/tavily-mcp:网络搜索服务器。为模型提供实时网络搜索能力。filesystem:Anthropic 官方提供的文件系统服务器,功能比我们上面实现的更完善。github:连接 GitHub API,管理仓库、Issue、PR等。sql:连接数据库,执行安全的 SQL 查询。postgres/mysql:特定数据库的服务器。- 你可以在 npm 或 PyPI 上搜索
mcp-*或mcp-server-*找到大量社区项目。
支持 MCP 的客户端:
- Claude Desktop:最主流的客户端,开箱即用。
- Cursor IDE:代码编辑器,深度集成 MCP,可以直接在编辑器内使用各种工具。
- Windsurf/Continue:其他 AI 编程助手。
- 任何应用都可以通过集成 MCP 客户端 SDK 来获得连接 MCP 服务器的能力。
5.2 开发 MCP 服务器的最佳实践
- 安全第一:始终假设传入的参数是恶意、未经校验的。实施严格的输入验证、路径限制、权限检查和资源配额(如最大文件大小、超时时间)。
- 清晰的工具描述:
description和inputSchema中的参数描述要尽可能清晰、具体。模型依赖这些描述来理解工具的用途和如何调用。好的描述能极大提升工具调用的准确率。 - 结构化输出:工具返回的内容,优先使用结构化的
text或考虑使用image、embedding等类型。清晰的格式有助于模型理解和进一步处理。 - 错误信息友好:返回的错误信息应对最终用户(通过模型)友好,能指导其更正输入,同时避免暴露内部细节。
- 处理异步操作:如果工具执行时间很长(如调用一个慢速 API),应实现异步通知机制。MCP 支持
progress通知,可以向客户端发送执行进度更新。 - 利用资源(Resources):对于只读的、可能被多次引用的数据,将其定义为资源(
Resources)而非工具(Tools)的返回结果,更符合协议设计,也能让模型更自然地将其作为上下文引用。
5.3 面试视角下的深度思考题
如果你在面试中被问到 MCP,面试官可能不会只满足于让你解释概念。他们更想考察你的理解和思考深度。以下是一些可能的深入问题及回答思路:
Q1: MCP 解决了 Function Calling 的什么问题?它的核心优势是什么?A:Function Calling 定义了模型“如何请求调用”,但没有定义工具“如何被提供和管理”。这导致每个 AI 应用都需要自己构建一套工具集成、发现和生命周期管理的框架,造成重复劳动和生态割裂。MCP 的核心优势在于标准化和解耦。它定义了一个统一的工具供给协议,使得工具开发者、模型提供者和 AI 应用开发者可以各自独立工作,通过标准接口连接,极大地促进了工具生态的繁荣和互操作性。
Q2: 在设计一个 MCP 服务器时,你会如何保证其安全性?A:这是一个系统工程,我会从多个层面考虑:
- 认证与授权:虽然基础 MCP over stdio 通常依赖客户端(如 Claude Desktop)的信任,但在网络传输场景下,必须实现服务器和客户端的双向认证(如 TLS 证书)。
- 输入验证与沙箱:对所有输入参数进行严格的类型、范围和语义验证。对于执行代码或系统命令的工具,必须在隔离的沙箱环境(如 Docker 容器、无权限用户)中运行。
- 资源隔离与限额:像我们的文件服务器一样,严格限制可访问的文件系统范围。对内存、CPU 时间、网络请求次数、文件读取大小设置硬性上限。
- 审计与日志:记录所有工具调用请求和结果(脱敏后),用于监控异常行为和事后审计。
- 最小权限原则:服务器进程本身应以最低必要的系统权限运行。
Q3: MCP 的局限性或挑战是什么?A:目前看来有几个方面:
- 协议成熟度:协议仍在快速发展中,一些高级特性(如双向流、复杂资源订阅)的客户端支持可能不完善。
- 工具描述的局限性:当前主要依靠文本描述(
description)和 JSON Schema,对于复杂工具,模型可能仍无法准确理解其使用场景和边界。 - 工具组合与编排:MCP 定义了单个工具的调用,但多个工具之间的顺序执行、条件判断、结果传递等“工作流”编排,目前落在客户端或模型逻辑上,缺乏协议层面的原生支持。
- 状态管理:服务器通常被设计为无状态的,但对于需要会话状态或复杂配置的工具,状态管理会变得棘手。
Q4: 对比其他类似协议(如 OpenAI 的 Assistants API Tools、LangChain Tools),MCP 的定位有何不同?A:OpenAI Assistants API Tools是 OpenAI 平台生态内的一套解决方案,它包含了工具定义、调用和执行环境(如代码解释器),但它是平台绑定的。LangChain Tools是一个 Python/JS 框架内的抽象层,它定义了工具接口,并提供了大量现成工具实现,但它主要服务于使用 LangChain 框架构建的应用。MCP 的定位更底层、更通用。它不关心你用什么框架(可以用 LangChain 实现 MCP 服务器),也不绑定特定平台。它是一个跨平台、跨框架的“通信协议标准”,旨在成为大模型工具生态的“基础连接件”。你可以用 LangChain 的工具包装成一个 MCP 服务器,然后被任何兼容 MCP 的客户端使用。
从写一个简单的“Hello World”式服务器,到一个具备基本安全意识的文件系统服务器,我们走完了 MCP 从概念到代码实现的核心路径。关键在于理解其“协议”的本质——它是一套约定,规定了工具提供者和消费者之间如何对话。这种解耦的设计,正是其生命力的源泉。在实际开发中,比起从头造轮子,更推荐基于成熟的 SDK 和社区项目进行扩展。多看看 Anthropic 官方和社区的服务器实现,是快速提升理解的最佳方式。最后,安全永远是悬在头上的达摩克利斯之剑,赋予模型操作现实世界的能力时,必须用最谨慎的态度去设计每一道边界。