从零构建MCP服务器:扩展AI助手能力的实战指南
1. 项目概述从零构建你的第一个MCP服务器最近在AI应用开发圈里MCPModel Context Protocol这个词的热度越来越高。简单来说它就像是为你的AI助手比如Claude、Cursor等安装了一个“应用商店”和“插件系统”。传统的AI助手虽然聪明但它的知识库是静态的无法直接操作你的数据库、读取你本地的文件或者调用特定的API。而MCP就是为了解决这个“最后一公里”的问题而生的。它定义了一套标准协议让开发者可以轻松地为AI助手创建各种“工具”Tools让AI不仅能思考还能动手操作。今天要聊的vivy-yi/mcp-tutorial这个项目就是一个绝佳的MCP入门实战指南。它不是一个复杂的概念讲解而是一个手把手、从零开始的构建教程。如果你是一名开发者对如何让AI更深度地融入你的工作流感兴趣或者你正在为团队构建一个智能化的内部工具那么这个教程就是你需要的“第一块砖”。它不要求你有多深的AI背景但需要你具备基本的编程知识比如会用Python以及一颗愿意动手尝试的心。通过这个项目你将学会如何创建一个最简单的MCP服务器并让Claude Desktop这样的客户端成功调用它完成一次从想法到可运行工具的完整闭环。2. MCP核心概念与项目设计思路拆解在开始敲代码之前我们必须先搞清楚MCP到底在解决什么问题以及vivy-yi/mcp-tutorial这个项目是如何围绕核心问题来设计的。2.1 为什么需要MCP传统AI助手的局限性想象一下你正在用Claude分析一份销售数据报告。Claude可以很好地理解你上传的CSV文件内容并给出分析建议。但如果你想让它“去数据库里把上个月的数据拉出来和这份报告做个对比”它就无能为力了。因为Claude本身无法连接你的数据库它没有这个“手”和“脚”。这就是传统AI助手的主要局限上下文隔离。AI模型运行在一个受控的、安全的沙箱环境中无法直接访问外部系统如本地文件系统、网络资源、数据库、API。所有交互都必须通过“上传”文件或“复制粘贴”文本来完成过程繁琐且无法实现动态操作。MCP的出现就是为了打破这堵墙。它定义了一套基于JSON-RPC的通信协议在AI客户端如Claude Desktop和外部资源服务器之间建立了一座标准化的桥梁。这座桥梁允许AI安全、可控地调用外部工具极大地扩展了其能力边界。2.2 MCP协议的核心组件与交互流程理解MCP的架构对后续开发至关重要。整个体系主要包含三个角色MCP 客户端 这是AI应用本身比如Claude Desktop、Cursor IDE或者任何集成了MCP SDK的应用。它的职责是管理用户对话并在需要时向MCP服务器请求可用的工具列表或执行某个工具。MCP 服务器 这就是我们要构建的东西。它是一个独立的进程负责暴露一组定义好的“工具”Tools或“资源”Resources。每个工具都对应一个具体的函数比如“读取文件”、“查询数据库”、“发送邮件”。服务器需要实现MCP协议以便客户端能发现和调用这些工具。MCP 传输层 负责客户端和服务器之间的通信。主要有两种方式stdio标准输入输出 最常见的方式客户端通过子进程启动服务器并通过管道进行通信。这种方式简单、跨平台适合大多数本地工具。SSEServer-Sent Events 基于HTTP的通信方式服务器可以主动向客户端推送信息。更适合需要实时更新或远程访问的场景。vivy-yi/mcp-tutorial项目选择了最经典、最易上手的stdio传输 工具Tools模型作为切入点。整个交互流程可以简化为客户端启动服务器进程。客户端发送initialize请求与服务器握手。客户端发送tools/list请求获取服务器提供的所有工具列表。用户在客户端对话中提出需求如“帮我看看当前目录”。客户端判断需要调用某个工具如list_directory于是发送tools/call请求给服务器。服务器执行对应的函数并将结果返回给客户端。客户端将结果呈现给用户。2.3 教程项目的技术选型与设计目标基于以上理解我们来看这个教程项目的设计思路语言选择Python。 Python是AI生态中最流行的语言拥有丰富的库和极低的入门门槛。使用Python构建MCP服务器可以让开发者快速上手专注于协议逻辑而非语言细节。项目依赖的核心库是mcp这是Anthropic官方维护的Python SDK封装了协议细节让我们可以像写普通函数一样创建工具。核心目标极简与可运行。 教程没有一上来就讲解复杂的概念而是采用“做中学”的方式。它的首要目标是让你在10分钟内拥有一个可以实际运行、能与Claude对话的MCP服务器。因此它选择实现一个最简单的工具列出当前目录的文件。这个工具功能明确无需外部依赖能直观地展示MCP的完整工作流程。项目结构清晰。 一个典型的MCP服务器项目通常包含以下部分协议实现与工具注册主要逻辑。服务器启动脚本。客户端配置告诉Claude Desktop如何找到你的服务器。 这个教程完美地覆盖了这三部分并提供了详细的配置说明。注意 虽然教程以Python为例但MCP是一个语言无关的协议。你可以用Node.js、Go、Rust等任何语言来实现服务器。选择Python只是因为其生态和上手速度在教程场景下最具优势。3. 环境准备与核心依赖解析工欲善其事必先利其器。在开始编码前我们需要搭建好开发环境。这个过程本身也会加深你对MCP部署方式的理解。3.1 基础开发环境搭建首先确保你的系统已经安装了Python。MCP对Python版本有一定要求建议使用Python 3.8或更高版本。你可以通过以下命令检查python3 --version # 或 python --version接下来我们需要一个独立的Python环境来管理项目依赖避免污染系统环境。强烈推荐使用venvPython内置或conda。使用 venv 创建虚拟环境# 在项目目录下 python3 -m venv .venv # 激活虚拟环境 # 在 macOS/Linux 上 source .venv/bin/activate # 在 Windows 上 .venv\Scripts\activate激活后你的命令行提示符前通常会显示(.venv)表示已进入虚拟环境。3.2 关键依赖库安装与作用剖析项目最核心的依赖就是mcp库。我们通过pip安装pip install mcp这个mcp库具体为我们做了什么它不是一个运行时框架而是一个协议实现辅助库。它帮你处理了所有JSON-RPC格式的序列化/反序列化、请求路由、错误处理等底层细节。你不用自己去拼装{jsonrpc: 2.0, method: tools/call, params: ...}这样的字符串只需要像写一个普通的Python类一样用装饰器来声明工具库会帮你搞定一切。除了mcp教程可能还会用到typing等标准库来完善类型提示让代码更健壮。一个完整的requirements.txt文件可能看起来非常简单mcp0.3.0实操心得 在开发MCP服务器时务必注意mcp库的版本。MCP协议本身还在快速发展中不同版本间的API可能有细微变动。建议在requirements.txt中固定一个已知可用的版本如mcp0.3.0以避免未来更新带来的意外问题。你可以通过pip show mcp来查看当前安装的版本详情。3.3 Claude Desktop客户端的安装与配置服务器写好了还需要一个能调用它的客户端。我们选择Claude Desktop作为测试客户端因为它是目前对MCP支持最友好、应用最广泛的AI桌面应用。下载与安装 前往Anthropic官网下载对应你操作系统macOS/Windows的Claude Desktop安装包并完成安装。定位配置文件 Claude Desktop通过一个JSON配置文件来加载MCP服务器。这个文件的位置因系统而异macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.json如果该文件或目录不存在你需要手动创建它。这个配置文件是我们连接自定义服务器与Claude客户端的“桥梁”其结构我们将在下一章详细解析。4. 手把手实现第一个MCP工具目录列表器现在让我们进入最核心的编码环节。我们将一步步实现一个名为list_directory的工具它接收一个可选的路径参数返回该路径下的文件和文件夹列表。4.1 创建服务器主文件与初始化App首先在你的项目目录下创建一个Python文件例如mcp_server.py。# mcp_server.py import asyncio from typing import Any from mcp import ClientSession, StdioServerParameters from mcp.server import Server from mcp.server.models import Tool import os # 创建MCP服务器实例 app Server(tutorial-server)这里我们从mcp.server导入Server类并实例化。Server类是核心它管理着所有的工具和资源。实例化时传入的tutorial-server是服务器的名字用于在日志中标识可以按需修改。4.2 使用装饰器定义工具函数接下来我们使用app.list_tools()装饰器来声明服务器提供的工具列表并使用app.call_tool()装饰器来处理具体的工具调用。# 继续在 mcp_server.py 中编写 app.list_tools() async def list_tools() - list[Tool]: 返回服务器提供的工具列表 return [ Tool( namelist_directory, description列出指定目录下的文件和文件夹。如果未提供路径则列出当前工作目录。, inputSchema{ type: object, properties: { path: { type: string, description: 要列出的目录路径。默认为当前目录。, } }, }, ) ]list_tools函数返回一个Tool对象的列表。每个Tool对象定义了一个工具name: 工具的唯一标识符客户端将通过这个名字来调用它。description: 工具的详细描述。这个描述至关重要AI客户端如Claude会阅读这个描述来理解工具的功能和用途从而决定在什么场景下调用它。描述应清晰、准确。inputSchema: 定义工具的输入参数遵循JSON Schema格式。这里我们定义了一个可选参数path类型是字符串。4.3 实现工具的核心逻辑定义了工具之后我们需要实现它的具体逻辑。# 继续在 mcp_server.py 中编写 app.call_tool() async def call_tool(name: str, arguments: dict[str, Any]) - list[dict[str, Any]]: 处理工具调用请求 if name list_directory: return await handle_list_directory(arguments) else: # 如果收到未知的工具名抛出错误 raise ValueError(f未知的工具: {name}) async def handle_list_directory(arguments: dict[str, Any]) - list[dict[str, Any]]: 处理 list_directory 工具的具体逻辑 path arguments.get(path, .) abs_path os.path.abspath(path) # 检查路径是否存在且为目录 if not os.path.exists(abs_path): return [{ type: text, text: f错误路径 {abs_path} 不存在。 }] if not os.path.isdir(abs_path): return [{ type: text, text: f错误{abs_path} 不是一个目录。 }] # 获取目录列表 try: entries os.listdir(abs_path) # 简单排序文件夹在前 entries.sort(keylambda x: (not os.path.isdir(os.path.join(abs_path, x)), x.lower())) # 格式化输出 result_lines [f目录内容: {abs_path}, ---] for entry in entries: entry_path os.path.join(abs_path, entry) if os.path.isdir(entry_path): result_lines.append(f[目录] {entry}/) else: size os.path.getsize(entry_path) result_lines.append(f[文件] {entry} ({size} 字节)) result_text \n.join(result_lines) return [{ type: text, text: result_text }] except PermissionError: return [{ type: text, text: f错误没有权限读取目录 {abs_path}。 }] except Exception as e: return [{ type: text, text: f读取目录时发生未知错误: {e} }]call_tool函数是一个路由器它根据传入的name参数将请求分发给对应的处理函数这里是handle_list_directory。handle_list_directory函数包含了所有业务逻辑参数解析、路径验证、文件列表获取、结果格式化以及异常处理。返回的结果必须是一个列表列表中的每个元素是一个字典。最常用的类型是{type: text, text: 你的结果字符串}。MCP也支持返回图片、HTML等内容但文本是最通用和简单的。4.4 编写服务器启动脚本最后我们需要一个异步入口点来启动服务器。# 继续在 mcp_server.py 末尾添加 async def main(): # 使用 stdio 作为传输层 async with app.run_stdio_server() as (read_stream, write_stream): session ClientSession(read_stream, write_stream) async with session: await session.initialize() await session.run_until_complete() if __name__ __main__: asyncio.run(main())这段代码是服务器的“发动机”。app.run_stdio_server()创建了一个基于标准输入输出的服务器传输通道。ClientSession用于管理客户端会话。session.initialize()执行与客户端的初始化握手。session.run_until_complete()则让服务器进入事件循环持续监听客户端的请求。至此一个功能完整的MCP服务器就编写完成了。你可以通过python mcp_server.py来运行它但它现在只会等待标准输入因为我们还没有配置客户端来连接它。注意事项 在开发调试阶段你可能会直接运行服务器脚本然后发现它启动后立刻退出或挂起。这是正常现象因为stdio服务器设计为被另一个进程即MCP客户端调用并通过管道进行通信。单独运行它没有客户端连接它自然无事可做。正确的测试方式是通过配置好的Claude Desktop来触发调用。5. 连接与测试配置Claude Desktop调用自定义工具服务器准备好了下一步就是让Claude Desktop知道它的存在并学会调用它。这需要通过编辑Claude Desktop的配置文件来完成。5.1 解析Claude Desktop的MCP配置文件创建一个名为claude_desktop_config.json的文件内容如下。我们将其放在项目根目录下方便管理但最终需要复制到上一节提到的Claude配置目录中。{ mcpServers: { tutorial-file-server: { command: /absolute/path/to/your/.venv/bin/python, args: [ /absolute/path/to/your/project/mcp_server.py ], env: { PYTHONUNBUFFERED: 1 } } } }让我们拆解这个配置的每个部分mcpServers: 顶层对象可以配置多个服务器。tutorial-file-server: 你给这个服务器起的任意名字用于在Claude内部标识。command: 启动服务器进程的命令。这里指向的是你虚拟环境中的Python解释器绝对路径。这是关键不能只用python必须用绝对路径确保Claude Desktop能准确找到它。args: 传递给命令的参数列表。这里只有一个参数即我们服务器脚本的绝对路径。env: 设置环境变量。PYTHONUNBUFFERED1确保Python的输出是实时的便于调试和日志查看。如何获取绝对路径macOS/Linux: 在终端中进入项目目录运行which python在激活的虚拟环境中获取Python路径运行pwd获取项目目录路径。Windows: 在PowerShell中激活虚拟环境后运行Get-Command python | Select-Object -ExpandProperty Source获取Python路径运行pwd获取项目目录路径。5.2 配置文件的放置与客户端加载现在你需要将上面编辑好的claude_desktop_config.json文件移动或复制到Claude Desktop的配置目录macOS:~/Library/Application Support/Claude/Windows:%APPDATA%\Claude\重要步骤如果目标目录不存在请先创建它。放置配置文件后必须完全重启Claude Desktop应用程序。它只在启动时读取一次配置文件。5.3 在Claude Desktop中进行首次测试重启Claude Desktop后你就可以开始测试了。验证连接 打开Claude Desktop新建一个对话。如果配置正确Claude通常会在界面某个位置如输入框上方提示“已连接至自定义工具”或类似信息。有时可能没有明显提示但工具已经可用。触发工具调用 直接在对话中输入指令。例如“请帮我列出当前工作目录下的文件。”“使用list_directory工具看看我的项目根目录。”“查看一下/Users/YourName/Downloads这个文件夹里有什么。”观察过程 Claude会理解你的指令识别出需要调用list_directory工具并在后台向你的服务器发送请求。你会看到它“思考”的过程然后返回格式化好的目录列表。如果Claude回复说“我不知道如何列出目录”或者没有反应说明工具没有被正确加载。请按以下步骤排查。6. 全链路问题排查与调试技巧实录开发过程中遇到问题是常态。下面是我在多次搭建MCP环境时踩过的坑和总结的排查方法希望能帮你快速定位问题。6.1 常见问题速查表问题现象可能原因排查步骤Claude无任何工具提示或明确说没有工具。1. 配置文件路径错误。2. 配置文件格式错误JSON语法。3. 未重启Claude Desktop。1. 确认配置文件在正确的OS特定目录下。2. 使用JSON验证工具检查配置文件。3. 彻底关闭并重启Claude。Claude尝试调用工具但失败提示“无法连接”或“服务器错误”。1.command或args中的路径错误。2. Python脚本本身有语法错误。3. 虚拟环境未激活或依赖未安装。1. 在终端中手动执行配置中的完整命令看能否启动脚本。2. 单独运行python mcp_server.py检查Python错误。3. 确认使用的Python路径在虚拟环境中并已安装mcp库。工具被调用但返回错误结果或异常。1. 工具函数逻辑有bug。2. 参数处理错误。3. 权限问题。1. 在工具函数内添加打印语句需配合PYTHONUNBUFFERED1查看。2. 检查arguments的解析逻辑。3. 检查目标路径的读写权限。工具调用成功但Claude显示的内容格式混乱。返回的结果格式不符合MCP协议要求。确保call_tool返回的是一个列表列表内是{type: text, text: ...}字典。6.2 高级调试技巧日志与进程检查当基础排查无效时需要更深入的手段。1. 启用服务器端日志在mcp_server.py的main()函数或工具函数中添加print语句是最简单的调试方式。由于我们设置了PYTHONUNBUFFERED1这些打印信息会实时输出到Claude Desktop的后台通常你无法直接看到。为了捕获它们一个更有效的方法是重定向输出到文件。修改你的配置文件claude_desktop_config.json在args中稍作调整不推荐用于生产但调试极佳args: [ /absolute/path/to/your/project/mcp_server.py, , /tmp/mcp_server.log, 21 ]注意这种方法依赖于系统的shell且和21可能在所有环境下不兼容。更可靠的方式是在Python脚本内部配置logging模块将日志写入指定文件。2. 检查进程是否启动配置完成后Claude启动时你的服务器进程应该会被自动拉起。macOS/Linux: 在终端运行ps aux | grep mcp_server.py查看是否有对应的Python进程。Windows: 在任务管理器中查看Python进程。如果进程不存在说明Claude启动服务器失败问题一定出在配置文件或脚本的启动阶段。3. 使用MCP Inspector进行独立测试推荐这是最专业的调试方法。Anthropic提供了一个官方调试工具叫MCP Inspector。你可以通过NPM安装它npx modelcontextprotocol/inspector python /absolute/path/to/your/project/mcp_server.py它会启动一个本地网页你可以在这个界面中手动发送tools/list和tools/call请求并看到原始的协议通信数据这对于验证服务器逻辑是否正确、协议是否符合规范非常有帮助。6.3 性能优化与安全考量初步当你的第一个工具跑通后可能会考虑更实际的问题。性能避免阻塞 工具函数call_tool是async的。如果你的工具涉及网络请求如调用API、数据库查询等I/O操作务必使用异步库如aiohttp,asyncpg避免使用阻塞式库否则会拖慢整个服务器的响应影响其他工具调用。超时设置 考虑在工具函数中添加超时逻辑防止某个耗时操作永远不返回。可以使用asyncio.wait_for。安全参数校验 教程中的list_directory工具直接使用了用户输入的path。这存在安全风险如路径遍历攻击../../../etc/passwd。在生产环境中必须对输入路径进行严格的校验和规范化例如将其限制在某个安全的基础目录下。权限最小化 服务器进程应该以最低必要的系统权限运行。不要用root或管理员权限运行你的MCP服务器。工具暴露范围 仔细评估每个工具的描述和功能。不要暴露危险的操作如rm -rf、任意命令执行。MCP的核心思想是授予AI可控的、特定的能力而非完全的系统访问权。7. 从教程到实战扩展你的MCP服务器能力掌握了基础框架后你就可以像搭积木一样为你的服务器添加更多强大的工具打造专属的AI工作流。7.1 添加更多实用工具示例以下是一些可以轻松集成到你的服务器中的工具想法1. 时间与日期工具app.list_tools() async def list_tools() - list[Tool]: return [ # ... 已有的 list_directory 工具 ... Tool( nameget_current_time, description获取当前的系统日期和时间以及对应的星期几。, inputSchema{type: object, properties: {}}, # 无参数 ), Tool( namecalculate_date_diff, description计算两个日期之间的天数差。, inputSchema{ type: object, properties: { date1: {type: string, description: 第一个日期格式为YYYY-MM-DD。}, date2: {type: string, description: 第二个日期格式为YYYY-MM-DD。}, }, required: [date1, date2] }, ), ] app.call_tool() async def call_tool(name: str, arguments: dict[str, Any]) - list[dict[str, Any]]: if name list_directory: return await handle_list_directory(arguments) elif name get_current_time: from datetime import datetime now datetime.now() return [{type: text, text: f当前时间{now.strftime(%Y-%m-%d %H:%M:%S)}星期{[一,二,三,四,五,六,日][now.weekday()]}}] elif name calculate_date_diff: from datetime import datetime try: d1 datetime.strptime(arguments[date1], %Y-%m-%d) d2 datetime.strptime(arguments[date2], %Y-%m-%d) diff abs((d2 - d1).days) return [{type: text, text: f日期 {arguments[date1]} 和 {arguments[date2]} 之间相差 {diff} 天。}] except ValueError as e: return [{type: text, text: f日期格式错误请使用YYYY-MM-DD格式。错误信息{e}}] else: raise ValueError(f未知的工具: {name})2. 简单的网络请求工具查询天气# 需要安装 aiohttp: pip install aiohttp import aiohttp app.list_tools() async def list_tools() - list[Tool]: return [ # ... 其他工具 ... Tool( nameget_weather, description根据城市名称查询当前的天气情况。, inputSchema{ type: object, properties: { city: {type: string, description: 城市名称例如Beijing, Shanghai, New York。}, }, required: [city] }, ), ] async def handle_get_weather(arguments: dict[str, Any]) - list[dict[str, Any]]: city arguments.get(city) # 这里使用一个假设的免费天气API实际使用时请替换为真实的API和密钥 url fhttps://api.weatherapi.com/v1/current.json?keyYOUR_API_KEYq{city} try: async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status 200: data await response.json() temp data[current][temp_c] condition data[current][condition][text] return [{type: text, text: f{city}的天气{condition}温度{temp}摄氏度。}] else: return [{type: text, text: f获取天气失败HTTP状态码{response.status}}] except Exception as e: return [{type: text, text: f请求天气API时出错{e}}] # 记得在 call_tool 函数中添加对应的路由7.2 探索MCP高级特性资源Resources与提示Prompts除了工具ToolsMCP协议还定义了另外两个核心概念它们能实现更动态、更强大的交互。资源 可以理解为“只读的工具”或“动态上下文”。例如你可以定义一个file://资源让AI客户端直接读取文件内容而无需通过工具调用的方式。这对于提供大型的、结构化的参考数据如项目文档、API说明非常有用。提示 预定义的提示模板。你可以将一些复杂的、需要多步工具调用的操作流程封装成一个“提示”用户只需触发这个提示AI就会按照预设的步骤执行。这极大地简化了复杂任务的操作。vivy-yi/mcp-tutorial项目从工具入手是因为它最直观。当你熟练后强烈建议阅读MCP官方文档探索资源和提示的用法这将把你的MCP服务器从“函数库”升级为“智能助理平台”。7.3 项目工程化与部署思考当工具越来越多代码就需要更好的组织。模块化 不要把所有工具都写在mcp_server.py一个文件里。可以按功能拆分例如file_tools.py,network_tools.py,utility_tools.py然后在主文件中导入并注册。配置管理 将API密钥、服务器地址等敏感信息从代码中剥离使用环境变量或配置文件如.env文件管理。错误处理与日志 使用Python的logging模块替代print为不同级别的信息DEBUG, INFO, ERROR配置输出便于线上运维。考虑使用现有框架 社区已经出现了一些基于MCP的框架它们提供了更便捷的工具注册、生命周期管理等功能。如果你的项目变得复杂可以评估使用这些框架。这个教程项目vivy-yi/mcp-tutorial就像给你一把钥匙打开了MCP世界的大门。门后的世界有多大取决于你如何利用这套协议将AI的能力与你熟悉的环境、数据和系统无缝连接起来。从自动化文件管理、数据分析、到监控告警、内部系统操作可能性是无限的。最关键的是你现在已经拥有了开始构建这一切的起点。