从零搭建MCP Server:连接AI与外部系统的标准化协议实践
1. 项目概述:为什么我们需要从零搭建一个MCP?
如果你最近在AI开发或者智能体(Agent)的圈子里混,一定频繁听到“MCP”这个词。它不是什么新出的芯片,也不是某个神秘组织,而是Model Context Protocol的缩写,中文可以理解为“模型上下文协议”。简单来说,MCP是一个标准化的“插座”协议,它定义了AI模型(比如Claude、GPT)如何与外部工具、数据源和服务进行安全、高效的对话。
想象一下,你有一个能力超强的AI助手(Claude Code或Cursor里的AI),但它天生只能“思考”,无法直接操作你的文件系统、查询数据库、调用API或者控制浏览器。传统的做法是,每个AI应用都要自己写一套复杂的、定制化的插件系统来连接这些外部能力,这就像给每个电器都定制一个独特的插头,混乱且低效。MCP的出现,就是为了解决这个问题。它制定了一套统一的“插座”标准(协议),任何符合MCP标准的工具(称为MCP Server)都可以被任何支持MCP的AI客户端(称为MCP Client)即插即用。
所以,“从0开始搭建MCP”这个标题,其核心价值在于:让你亲手打造一个能让AI模型安全、可控地访问特定外部资源的桥梁。无论是想让你本地的Claude Desktop读取Obsidian笔记库,还是让Cursor里的AI帮你操作Figma设计稿,亦或是连接公司内部的数据库,你都需要理解并实现一个MCP Server。这个过程不仅能让你深度理解AI与工具集成的未来形态,更能让你获得一项构建下一代AI应用基础设施的核心技能。它适合所有对AI应用开发、自动化工具链构建感兴趣的开发者、技术爱好者和效率追求者。
2. MCP核心概念与生态全景解析
在动手之前,我们必须把几个关键概念和它们之间的关系彻底理清。这能帮助你在后续的搭建和调试中,清楚地知道每一步在全局中的位置。
2.1 MCP、Skill、Function Calling:概念辨析
网络上经常看到MCP、Skill、Agent Skill这些词混用,甚至和传统的Function Calling(函数调用)对比,让人一头雾水。我们来做个清晰的拆解:
MCP(Model Context Protocol): 这是最底层的通信协议标准。它由Anthropic公司牵头制定并开源,定义了一套JSON-RPC格式的消息规范。MCP协议规定了Client和Server之间“如何说话”,比如如何发现工具(Tools)、如何调用工具、如何传递资源(Resources)等。MCP本身不实现任何具体功能,它只是一本“通信手册”。
MCP Server: 这是协议的服务端实现。一个MCP Server就是一个独立的进程或服务,它“会说MCP协议”,并且封装了对某个特定外部系统或能力的访问。例如:
filesystem-mcp: 一个提供本地文件读写能力的Server。sqlite-mcp: 一个可以连接并查询SQLite数据库的Server。brave-search-mcp: 一个提供网络搜索能力的Server。- 你自己写的
my-internal-api-mcp: 连接你公司内部系统的Server。 Server向Client宣告自己提供了哪些“工具”(Tools)和“资源”(Resources)。
MCP Client: 这是协议的客户端实现。通常是AI应用本身,比如Claude Desktop、Cursor Editor、Windsurf等。Client负责启动和管理一个或多个MCP Server进程,并从这些Server中获取工具列表和资源信息,在合适的时机(如用户提问涉及相关能力时)调用这些工具。
Skill(技能)与 Agent Skill: 这是一个更上层的、偏向用户功能的概念。在一些AI平台(如Dify、Coze)中,“Skill”指的是一个封装好的、可复用的AI能力模块。一个Skill内部可能通过调用一个或多个MCP Server来实现其功能。你可以认为Skill是面向业务的“产品功能”,而MCP Server是面向技术的“基础设施组件”。至于“Agent Skill”,通常是指在智能体(Agent)框架中,一个可被智能体规划和调用的子能力单元,其底层同样可能由MCP驱动。
Function Calling(函数调用): 这是大语言模型(LLM)本身提供的一种基础能力。模型在对话中,可以输出一个结构化的请求,表示它想调用某个预定义好的函数。MCP可以看作是Function Calling的“标准化和外部化”。传统的Function Calling需要开发者在每次与AI对话时,手动在代码里定义好函数列表(schema)并传给模型。而MCP将函数的定义、发现和调用过程标准化,并且将这些函数的执行体(Server)与AI应用(Client)解耦,部署在独立的进程中,带来了更好的安全性、可扩展性和可维护性。
注意: 很多教程里说的“给Claude添加MCP”,准确来说是“为Claude Desktop配置MCP Server”。Claude Desktop作为一个MCP Client,本身已经内置了对MCP协议的支持,你需要做的只是告诉它去运行哪些Server。
2.2 MCP 核心组件:Tools 与 Resources
MCP协议主要围绕两大核心组件来组织能力,理解它们是你设计Server的关键。
Tools(工具): 这是主动操作的接口。你可以把它类比为编程中的“函数”或“方法”。一个Tool有名称、描述、参数列表(输入schema)。当AI模型认为需要执行某个操作时(比如“搜索网络”、“创建文件”),它会通过Client调用对应的Tool。
- 示例:
search_web(query: string)工具,接收一个查询字符串,返回搜索结果。
- 示例:
Resources(资源): 这是被动提供信息的接口。你可以把它类比为“只读的URI”或“数据源”。一个Resource有唯一的URI(如
file:///path/to/note.md)和MIME类型。AI模型可以“读取”这些资源来获取上下文信息,但通常不能直接修改它们(修改需要通过Tool)。- 示例: Server可以声明它提供了
file:///home/user/project/README.md这个资源。当用户对话中提到“看看我的README文件”时,Client可以主动读取这个Resource的内容,并将其作为上下文提供给AI模型,而无需模型显式调用一个“read_file”工具。
- 示例: Server可以声明它提供了
一个典型的交互流程:
- Client(如Claude Desktop)启动你配置的
sqlite-mcpServer。 - Server启动后,立即通过MCP协议向Client发送一个
list_tools和list_resources的响应,告知Client:“我这里有query_database工具,可以查询db://sales/data这个资源”。 - 用户在Claude Desktop中输入:“帮我查一下上个月的销售数据。”
- Claude Desktop的AI模型分析请求,发现需要用到“销售数据”,它知道有一个
db://sales/data资源可用,于是通过Client读取该资源的结构信息(如表schema)。 - 模型可能进一步决定需要执行一个查询,于是通过Client调用
query_database工具,并生成SQL查询参数。 - Client将调用请求转发给Server,Server执行SQL,将结果返回给Client,Client再呈现给用户。
3. 从零搭建MCP Server:环境与设计
现在,我们进入实战环节。我们将选择一个最常见的场景来构建我们的第一个MCP Server:一个能够读取和搜索指定目录下Markdown笔记内容的Server。这模拟了连接Obsidian、Logseq等知识库的需求。我们将使用MCP官方推荐的TypeScript/JavaScript SDK进行开发,这是目前生态最完善、文档最清晰的方式。
3.1 开发环境准备与项目初始化
首先,确保你的系统已经安装了Node.js(版本18或以上)和npm/yarn/pnpm等包管理器。
# 1. 创建一个新的项目目录 mkdir my-note-mcp-server cd my-note-mcp-server # 2. 初始化Node.js项目,推荐使用TypeScript以获得更好的类型提示 npm init -y npm install typescript @types/node tsx --save-dev # tsx用于运行TypeScript代码,比ts-node更轻量 # 3. 初始化TypeScript配置 npx tsc --init # 编辑生成的tsconfig.json,确保 `"module": "ESNext"` 和 `"target": "ES2022"` # 4. 安装MCP官方SDK npm install @modelcontextprotocol/sdk接下来,创建项目的基本结构:
my-note-mcp-server/ ├── package.json ├── tsconfig.json ├── src/ │ ├── index.ts # Server主入口文件 │ └── note-store.ts # 笔记读取与搜索的逻辑模块 └── README.md3.2 Server核心逻辑设计
在动手写MCP协议相关的代码前,我们先设计好核心的业务逻辑。在src/note-store.ts中:
// src/note-store.ts import fs from 'fs/promises'; import path from 'path'; import { glob } from 'glob'; // 需要安装: npm install glob export interface Note { uri: string; // 例如:file:///notes/hello.md title: string; content: string; lastModified: Date; } export class NoteStore { private notesDir: string; constructor(notesDir: string) { this.notesDir = path.resolve(notesDir); // 解析为绝对路径 } // 扫描目录,获取所有Markdown文件 async scanNotes(): Promise<Note[]> { const pattern = path.join(this.notesDir, '**/*.md'); const files = await glob(pattern, { nodir: true }); const notes: Note[] = []; for (const file of files) { try { const content = await fs.readFile(file, 'utf-8'); const stats = await fs.stat(file); // 简单从文件内容第一行提取标题,如果没有则使用文件名 const firstLine = content.split('\n')[0] || ''; const titleMatch = firstLine.match(/^#\s+(.+)/); const title = titleMatch ? titleMatch[1] : path.basename(file, '.md'); notes.push({ uri: `file://${file}`, // MCP Resource URI title, content, lastModified: stats.mtime, }); } catch (err) { console.error(`Failed to read note ${file}:`, err); // 可以选择跳过错误文件,或根据需求处理 } } return notes; } // 根据关键词搜索笔记内容 async searchNotes(keyword: string): Promise<Note[]> { const allNotes = await this.scanNotes(); const lowerKeyword = keyword.toLowerCase(); return allNotes.filter(note => note.title.toLowerCase().includes(lowerKeyword) || note.content.toLowerCase().includes(lowerKeyword) ); } // 根据URI获取特定笔记 async getNoteByUri(uri: string): Promise<Note | undefined> { // 将 file:// 开头的URI转换回本地文件路径 if (!uri.startsWith('file://')) { return undefined; } const filePath = uri.slice('file://'.length); // 安全检查:确保请求的文件在notesDir目录下 if (!filePath.startsWith(this.notesDir)) { throw new Error('Access denied: File outside of allowed directory'); } try { const content = await fs.readFile(filePath, 'utf-8'); const stats = await fs.stat(filePath); const firstLine = content.split('\n')[0] || ''; const titleMatch = firstLine.match(/^#\s+(.+)/); const title = titleMatch ? titleMatch[1] : path.basename(filePath, '.md'); return { uri, title, content, lastModified: stats.mtime, }; } catch { return undefined; } } }这个NoteStore类封装了所有与笔记文件系统交互的底层逻辑,它与MCP协议无关,只是纯粹的业务代码。这样做的好处是逻辑清晰,便于测试,也方便未来替换数据源(比如从数据库读取笔记)。
实操心得: 在文件路径处理上,一定要做好规范化和安全性检查。使用
path.resolve()获取绝对路径,避免相对路径导致的歧义。在getNoteByUri中,我们必须检查请求的文件是否在我们声明的notesDir目录下,这是防止Server被恶意利用读取系统任意文件的关键安全措施。MCP协议本身不强制这一点,但作为Server开发者,你必须考虑到。
4. 实现MCP Server:协议对接与工具暴露
有了核心逻辑,我们现在需要创建一个MCP Server,将NoteStore的能力通过MCP协议暴露出去。
4.1 创建Server实例与定义Tools
我们在src/index.ts中创建Server:
// 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 { NoteStore } from './note-store.js'; // 1. 初始化Server和NoteStore const server = new Server( { name: 'my-note-mcp-server', version: '0.1.0', }, { capabilities: { resources: {}, // 声明我们支持Resources tools: {}, // 声明我们支持Tools }, } ); const NOTES_DIR = process.env.NOTES_DIR || './notes'; // 通过环境变量配置笔记目录 const noteStore = new NoteStore(NOTES_DIR); // 2. 处理工具列表请求 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_notes', description: '在笔记库中搜索包含特定关键词的笔记。', inputSchema: { type: 'object', properties: { keyword: { type: 'string', description: '要搜索的关键词', }, }, required: ['keyword'], }, }, { name: 'get_note_statistics', description: '获取笔记库的统计信息,如笔记总数、最近更新等。', inputSchema: { type: 'object', properties: {}, // 此工具不需要参数 }, }, ], }; }); // 3. 处理资源列表请求 server.setRequestHandler(ListResourcesRequestSchema, async () => { // 我们可以选择动态返回资源列表,例如返回最近修改的5篇笔记作为资源 // 但为了简单起见,我们先返回一个空列表,或者一个根资源。 // 更动态的做法是在ReadResource请求时再按需列出。 return { resources: [ { uri: `note://root`, name: '笔记库根目录', description: `位于 ${NOTES_DIR} 的Markdown笔记库`, mimeType: 'text/plain', // 或 application/json }, ], }; }); // 4. 处理读取资源请求 server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; if (uri === 'note://root') { // 当请求根资源时,我们返回一个笔记列表的摘要 const notes = await noteStore.scanNotes(); const summary = notes.map(note => `- ${note.title} (${note.uri})`).join('\n'); return { contents: [ { uri: uri, mimeType: 'text/plain', text: `笔记库总览 (共${notes.length}篇笔记):\n${summary}`, }, ], }; } // 如果请求的是具体的file:// URI,我们委托给NoteStore处理 if (uri.startsWith('file://')) { const note = await noteStore.getNoteByUri(uri); if (note) { return { contents: [ { uri: uri, mimeType: 'text/markdown', // 标记为Markdown格式,AI客户端可能进行特殊渲染 text: note.content, }, ], }; } else { throw new Error(`Note not found: ${uri}`); } } throw new Error(`Unsupported resource URI: ${uri}`); }); // 5. 处理工具调用请求 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'search_notes') { const keyword = args?.keyword; if (typeof keyword !== 'string') { throw new Error('Keyword must be a string'); } const results = await noteStore.searchNotes(keyword); // 将结果格式化为AI易于理解的内容 const content = results.length > 0 ? `找到 ${results.length} 篇相关笔记:\n` + results.map(n => `### ${n.title}\n**URI:** ${n.uri}\n**摘要:** ${n.content.slice(0, 150)}...`).join('\n\n') : `未找到包含“${keyword}”的笔记。`; return { content: [ { type: 'text', text: content, }, ], }; } if (name === 'get_note_statistics') { const notes = await noteStore.scanNotes(); const total = notes.length; const latest = notes.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())[0]; return { content: [ { type: 'text', text: `**笔记库统计**\n- 笔记总数: ${total}\n- 最新笔记: ${latest?.title || '无'}\n- 最新更新时间: ${latest?.lastModified.toLocaleDateString() || 'N/A'}`, }, ], }; } throw new Error(`Unknown tool: ${name}`); }); // 6. 启动Server(使用stdio传输,这是与Claude Desktop等客户端通信的标准方式) async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('My Note MCP Server is running on stdio...'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });4.2 关键实现细节剖析
Server初始化: 创建
Server实例时,需要提供元数据(name, version)和声明支持的Capabilities(这里是resources和tools)。这类似于HTTP服务器的路由声明。请求处理器(Request Handler): SDK的核心是为一类MCP请求设置处理器。我们处理了四种核心请求:
ListToolsRequestSchema: 客户端询问“你有什么工具?”。我们返回search_notes和get_note_statistics两个工具的定义,包括名称、描述和输入参数的JSON Schema。ListResourcesRequestSchema: 客户端询问“你有什么资源?”。这里我们返回了一个静态的根资源note://root。更复杂的实现可以动态扫描文件系统并列出所有笔记文件作为资源。ReadResourceRequestSchema: 客户端请求读取某个资源的内容。我们根据URI进行路由:如果是根URI,返回摘要;如果是file://开头的具体笔记URI,则读取文件内容返回。注意返回的mimeType,设置为text/markdown可以帮助AI客户端更好地理解内容格式。CallToolRequestSchema: 客户端调用某个工具。我们根据工具名name分派到不同的业务逻辑函数,并处理输入参数arguments。
传输层(Transport):
StdioServerTransport是MCP Server最常用的传输方式。Server通过标准输入(stdin)接收JSON-RPC请求,通过标准输出(stdout)发送响应。这使得任何能启动子进程并与之进行标准IO通信的程序(如Claude Desktop、Cursor)都能轻松集成MCP Server。错误处理: 在工具调用和资源读取中,我们对参数进行了基础校验,并对未找到的资源或未知工具抛出了错误。这些错误会被SDK捕获并格式化为标准的MCP错误响应返回给客户端。
注意事项: 在
ReadResourceRequestSchema处理器中,我们直接返回了笔记的原始内容。对于大型文件,这可能会消耗大量上下文令牌。在生产环境中,你可能需要实现更智能的策略,比如只返回文件的前N行,或者提供一个summarize_note工具来让AI主动请求摘要。
5. 构建、测试与配置客户端
5.1 构建与运行独立测试
首先,我们需要编译TypeScript并创建一个可直接运行的脚本。在package.json中添加脚本:
{ "name": "my-note-mcp-server", "version": "0.1.0", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx watch src/index.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "glob": "^11.0.0" }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0", "tsx": "^4.0.0" } }创建一个简单的测试笔记目录和文件:
mkdir -p notes echo '# 项目计划\n这是关于MCP服务器搭建的项目计划。' > notes/project-plan.md echo '# 学习笔记\nMCP协议的核心是Tools和Resources。' > notes/learning.md现在,我们可以用开发模式运行Server,手动模拟客户端发送JSON-RPC请求来测试。但这比较繁琐。更高效的方法是使用MCP SDK自带的测试工具或编写一个简单的测试客户端。这里我们介绍一个实用的手动测试技巧:
- 在一个终端运行Server:
NOTES_DIR=$(pwd)/notes npm run dev - 由于Server使用stdio,它会等待输入。我们可以编写一个简单的Node.js脚本作为测试客户端,或者使用像
nc(netcat) 这样的工具进行简单交互(但这需要处理JSON-RPC帧)。更推荐的方法是使用官方提供的@modelcontextprotocol/sdk中的测试工具,或者直接将其配置到Claude Desktop中进行真实环境测试。
5.2 配置到Claude Desktop
这是最直接的集成测试方式。Claude Desktop是Anthropic官方提供的、天然支持MCP的客户端。
找到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:
编辑配置文件: 如果文件不存在,就创建它。我们需要在
mcpServers字段下添加我们的Server配置。{ "mcpServers": { "my-note-server": { "command": "node", "args": [ "/ABSOLUTE/PATH/TO/YOUR/my-note-mcp-server/dist/index.js" ], "env": { "NOTES_DIR": "/ABSOLUTE/PATH/TO/YOUR/my-note-mcp-server/notes" } } // 你可以在这里继续添加其他MCP Server,如sqlite, brave-search等 } }关键点:
command: 必须是node,因为我们的Server是Node.js脚本。args: 第一个参数必须是编译后的JS文件(dist/index.js)的绝对路径。相对路径很可能导致启动失败。env: 设置环境变量NOTES_DIR,这样我们的Server就知道去哪里找笔记。同样,必须使用绝对路径。my-note-server是这个Server实例的别名,可以自定义。
重启Claude Desktop: 保存配置文件后,完全关闭并重新打开Claude Desktop应用程序。
验证连接: 重启后,Claude Desktop会在后台启动你配置的MCP Server进程。你可以通过查看Claude Desktop的日志(通常在上述配置文件的同级目录或系统标准日志位置)来检查是否有启动错误。更直观的方式是,直接在Claude的聊天框中询问:“你现在可以使用哪些工具?” 或者 “搜索一下关于‘项目’的笔记”。如果配置成功,Claude会调用你的
search_notes工具并返回结果。
5.3 配置到Cursor Editor
Cursor是另一个深度集成MCP的流行代码编辑器。配置方式类似,但入口不同。
- 打开Cursor,进入设置(Settings)。
- 找到“MCP Servers”或“AI”设置部分(具体位置可能随版本更新变化,通常在设置搜索栏输入MCP即可找到)。
- 点击“Add New MCP Server”。
- 在弹出的配置界面中,填写信息:
- Name:
my-note-server(任意名称) - Command:
node - Arguments:
/ABSOLUTE/PATH/TO/YOUR/my-note-mcp-server/dist/index.js - Environment Variables: 点击添加,键为
NOTES_DIR,值为你的笔记目录绝对路径。
- Name:
- 保存并重启Cursor。
重启后,你可以在Cursor的AI聊天界面中测试工具调用。
6. 进阶优化与生产环境考量
一个能跑通的Demo只是第一步。要让你的MCP Server稳定、可用,还需要考虑以下方面。
6.1 性能、安全与错误处理增强
资源列表的动态性与分页: 我们的
ListResourcesRequestSchema处理器目前只返回一个静态根资源。当笔记库很大时,更好的做法是支持动态列出和分页。MCP协议支持在ListResourcesRequest中传递cursor参数来实现分页。你可以修改逻辑,每次返回一部分笔记的URI,并提供一个nextCursor。内容采样与摘要: 直接返回整个大文件的内容会浪费大量AI模型的上下文窗口。可以在
ReadResourceRequestSchema处理器中实现内容采样,例如只返回文件的前1000个字符,或者提供一个get_note_summary工具,利用AI模型或本地摘要算法生成摘要。更严格的安全边界: 除了检查文件路径是否在
NOTES_DIR下,还应考虑:- 符号链接(Symlink)攻击: 使用
fs.realpath()解析符号链接,确保最终路径仍在安全目录内。 - 命令注入: 如果你的工具涉及执行系统命令(例如调用外部程序处理文件),必须对输入参数进行严格的过滤和转义,避免命令注入漏洞。
- 环境隔离: 考虑在Docker容器或沙箱环境中运行Server,尤其是处理不可信输入时。
- 符号链接(Symlink)攻击: 使用
完善的日志与监控: 在生产环境中,需要记录Server的运行日志、工具调用次数、错误信息等。可以使用
winston、pino等日志库,并将日志输出到文件或日志收集系统。在Server的各个请求处理器开头和结尾添加详细的调试日志,对于排查问题至关重要。健壮的错误处理: 当前的错误处理比较基础。应该为不同类型的错误(如文件不存在、权限错误、参数无效)定义清晰的错误码和用户友好的信息,并通过MCP协议的错误响应返回。
6.2 扩展更多工具与能力
我们的Server目前只有搜索和统计两个工具。你可以根据需求轻松扩展:
create_note: 创建新笔记。需要处理文件名冲突、内容写入。update_note: 更新现有笔记内容。注意并发修改的问题。tag_notes: 为笔记添加标签。这可能需要引入一个额外的元数据存储(如一个JSON索引文件)。vector_search_notes: 集成向量数据库(如Chroma、LanceDB),实现基于语义的相似性搜索,而不仅仅是关键词匹配。
每添加一个工具,只需在ListToolsRequestSchema处理器中增加其定义,并在CallToolRequestSchema处理器中添加对应的分支逻辑即可。
6.3 调试技巧与常见问题排查
在开发过程中,你肯定会遇到各种问题。以下是一些实用的调试技巧:
查看客户端日志: Claude Desktop和Cursor通常都有开发者控制台或日志文件。启动时加上
--verbose或--debug标志(如果支持)可以输出更详细的MCP通信日志。这是定位连接和协议问题的第一手资料。独立运行并模拟请求: 编写一个简单的测试脚本,模拟MCP客户端向你的Server发送JSON-RPC请求。这可以帮助你隔离问题,确定是Server逻辑错误还是客户端集成错误。
// test-client.mjs import { spawn } from 'child_process'; const serverProcess = spawn('node', ['dist/index.js'], { env: { ...process.env, NOTES_DIR: './notes' }, stdio: ['pipe', 'pipe', 'inherit'] // 接管 stdin/stdout }); // 手动构造一个 list_tools 请求并写入 serverProcess.stdin...使用MCP Inspector工具: Anthropic提供了一个名为MCP Inspector的调试工具。它是一个图形化界面,可以连接到任何MCP Server,查看其暴露的工具和资源,并手动调用工具,是开发和调试的利器。可以通过
npm install -g @modelcontextprotocol/inspector安装,然后运行mcp-inspector。常见问题速查表:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Claude Desktop/Cursor 启动后无新工具 | 1. 配置文件路径错误。 2. Server启动失败。 3. Server未正确声明工具。 | 1. 检查配置文件路径和JSON格式。 2. 查看客户端/系统日志,看Server进程是否报错退出。 3. 用MCP Inspector连接Server,验证工具列表。 |
| 调用工具时报“Unknown tool” | 工具名称拼写不一致。ListTools返回的名称与CallTool处理器中判断的名称不匹配。 | 仔细检查两处的name字段是否完全一致(大小写敏感)。 |
| 读取资源返回空或错误 | 1. URI格式不正确。 2. 文件路径权限问题。 3. 安全检查阻止了访问。 | 1. 确保URI以file://开头,且路径是绝对路径。2. 确保Node.js进程有权限读取目标文件。 3. 调试 getNoteByUri中的路径检查和解析逻辑。 |
| Server进程立即退出 | 1. 代码中存在未捕获的异常。 2. 依赖未安装。 3. TypeScript未编译,直接运行了ts文件。 | 1. 在main()函数外包裹try-catch,打印错误。2. 运行 npm install。3. 确保运行的是 dist/index.js或使用tsx。 |
| 通信超时或无响应 | Server的请求处理器是异步的,但没有正确返回Promise或发生了死循环。 | 确保所有async请求处理器都使用了await,并且最终会return或throw。使用调试器检查执行流。 |
7. 生态集成与未来展望
成功搭建并运行你自己的MCP Server后,你就获得了连接AI世界与真实世界数据/服务的一把钥匙。但这仅仅是开始。
融入MCP生态: 你可以将你的Server开源,发布到npm上,并提交到官方的 MCP Server Registry (如果存在)或社区列表。这样,其他开发者就可以通过一行配置轻松使用你的my-note-mcp-server。在发布时,记得编写清晰的README.md,说明功能、配置方法和注意事项。
探索更复杂的Server: 本文的笔记Server是一个文件系统类Server。你可以尝试更复杂的类型:
- 数据库Server: 连接MySQL/PostgreSQL,让AI直接安全地查询业务数据。
- API聚合Server: 封装公司内部多个API,提供统一的、自然语言可访问的接口。
- 浏览器自动化Server: 集成Playwright或Puppeteer,让AI可以控制浏览器完成网页操作、数据抓取等任务(需要极其注意安全边界)。
Skill与MCP的协同: 在Dify、Coze等平台上,你可以将你的MCP Server作为一个后端能力,在其上构建更面向业务的Skill。例如,一个“周报生成Skill”可以调用你的笔记Server搜索本周工作笔记,调用数据库Server查询任务数据,再调用LLM生成周报草稿。
MCP协议本身的演进: MCP协议仍在快速发展中。关注其官方GitHub仓库,了解新特性,如更丰富的资源类型、流式响应(用于长内容生成)、双向通信(Server主动推送通知给Client)等。作为Server开发者,及时跟进协议更新能让你的工具保持兼容性和先进性。
从我个人的实践经验来看,MCP的价值在于它提供了一种标准化、解耦、安全的扩展AI能力的方式。它降低了为不同AI客户端重复开发适配层的成本,让开发者可以专注于实现核心业务逻辑。虽然初期搭建会遇到配置路径、环境变量、协议细节等“坑”,但一旦跑通,你会发现为AI构建工具变得前所未有的清晰和高效。未来的AI应用,很可能就是一个强大的MCP Client,配合一个由无数专业MCP Server组成的“工具网络”。而你现在所做的,正是在为这个网络添加一个节点。