ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建MCP Server:为AI Agent打造统一工具接口的实战指南

从零构建MCP Server:为AI Agent打造统一工具接口的实战指南 最近只要在折腾AI Agent大概率绕不开MCP Server这个词。我自己的体会是MCP出现前的工具接入就像早年手机充电口——各家有各家的线MCP就是那个被大家默认接受下来的USB-C接口标准协议统一插上就能用坏了也只换线不换设备。这篇东西不是概念科普是一份能直接照着敲的实战记录目标是在30分钟内从零跑起来一个真正能用的MCP Server并接进AI客户端里让Agent真正调用它。全文按我实际操作的顺序来写先讲清楚MCP为什么值得学再给环境准备、代码实现、客户端接入、排查故障最后聊到远程部署和鉴权。适合两类人看一是刚接触AI Agent开发、想给Agent接工具但不知道从哪里起步的新手二是已经写过Function Calling或OpenAPI插件、想换一套更通用接入方式的开发者。下面直接上干货。1. 为什么MCP会被叫成“Agent的USB-C”接口乱局与统一标准1.1 我过去接AI工具的真实体验一地鸡毛先说个具体场景。去年我给一个内部AI助手接“查数据库、读本地文件、调内部API”三个能力分别写了三套胶水代码数据库那边用一套SQL封装文件系统那边写了独立的读取接口内部API又得按照OpenAPI规范走一遍鉴权和路由。三个能力各连各的客户端逻辑越来越肿每次新增一个工具都要重新部署、重新调试。后来社区里开始推MCPModel Context Protocol我才意识到问题不在“工具多”而在“没有统一标准”。MCP做的事情很简单把工具、数据资源、提示词模板全部规范成一种协议描述客户端只要实现一套协议就能连接所有支持MCP的Server。这正是USB-C的思路——接口统一设备端自己去适配。1.2 MCP协议到底在做什么三句话能讲清楚MCP基于JSON-RPC 2.0做消息交换核心抽象只有三个东西Tool能被Agent调用的函数需要声明输入输出的Schema相当于给Agent装上手。Resource可供读取的数据或上下文用URI标识相当于给Agent一双眼睛。Prompt可复用的提示词模板相当于预先把经验写成“套路”。AI Agent通过MCP客户端连接ServerServer暴露以上三种原语模型在对话过程中自主决定“现在需要调用哪个工具”。整个过程对用户是透明的体验上就是“你问一句Agent自己把活干了”。1.3 和其它接入方式的对比我做了一张对比表方便你判断MCP和传统方案的区别接入方式连接成本适用范围生态情况MCP Server实现一次协议任意客户端通用本地工具、外部API、资源读取均可覆盖生态快速膨胀官方SDK成熟Function Calling每次调用都要写独立封装只随特定模型API走封闭迁移成本高OpenAPI / Plugin需要维护一份OpenAPI描述适合公开HTTP API依赖客户端支持各平台不互通直接SDK调用代码侵入最重单一应用内改需求就要改代码从表里能看出来MCP最大的价值不是“功能更强”而是“连接方式被统一了”。你写好一个ServerClaude能用、别的支持MCP的客户端也能用迁移成本几乎为零。1.4 什么时候不值得上MCP也不是所有场景都需要MCP。如果只是单机脚本里调用两三个函数或者你明确只对接某一个模型API直接用SDK写反而更快。MCP适合“工具可能被多个客户端复用”“未来还会不断加新能力”“需要远程暴露给其他Agent”这三类场景。本文的demo虽然简单但背后的抽象方式会跟着你进入生产级项目值得花半小时上手。2. 开工前先想清楚环境、Schema与Server暴露的能力边界2.1 一句话结论环境准备其实只有两步MCP Server可以用Python、TypeScript、Go等多种语言写主流社区示例以Python和TypeScript为主。我选了Python不是因为别的而是官方SDK的FastMCP封装非常省事装饰器一挂、方法一跑一个Server就出来了很适合快速验证想法。准备工作两步安装Python 3.10以上版本保证python3 --version能正常输出。安装uv。uv是目前我用下来最顺手的Python包管理器创建虚拟环境、锁依赖都比pip快很多很多MCP官方示例也默认用uv。如果你不想用uv用pip install mcp或者pip install mcp[cli]也完全可以后面配置客户端时把命令换成python解释器的绝对路径就行。创建项目uv init mcp-demo cd mcp-demo uv add mcp[cli]提示mcp[cli]会额外带上命令行工具后面调试要用。如果安装网络慢可以先只装mcp但调试体验会差一截。2.2 动手前最重要的30秒先画能力边界很多人一上来就写代码结果Server暴露了一大堆危险能力比如任意文件读写、任意路径遍历、无鉴权访问内部接口。我的建议是写代码前先花30秒在纸上把三个问题列出答案我这个Server要给Agent提供哪几个能力每个能力的输入输出是什么类型参数约束有哪些哪些路径、接口、数据必须禁止访问本文的demo选三个能力获取当前时间、列出指定目录下的文件、统计一段文本的字数词数。这三个能力覆盖了“无参数工具”“带路径参数工具”“结构化返回工具”三类常见形态足够演示MCP的核心用法又不至于引入安全性雷区。2.3 工具Schema提前决定了Agent能不能用对MCP每个Tool都有JSON Schema描述Agent在决定调用哪个工具时会读这些Schema来匹配参数。所以函数签名里的参数名、类型注解、docstring不能随便写它们会被自动转换成Schema直接决定Agent“能不能听懂这个工具怎么用”。比如在FastMCP里你只要这样写from mcp.server.fastmcp import FastMCP mcp FastMCP(local-demo) mcp.tool() def get_current_time() - str: 获取服务器当前的日期和时间返回格式为 年-月-日 时:分:秒 from datetime import datetime return datetime.now().strftime(%Y-%m-%d %H:%M:%S)FastMCP会自动把函数名、参数类型、docstring转换成Tool定义返回给客户端。注意docstring不要写“该函数用于xxx”这种废话直接说明返回格式、单位、约束条件Agent会拿这些文字辅助判断。3. 15分钟写一个能跑的FastMCP Server代码、运行与本地验证3.1 完整代码三个工具加一个资源我先给你一版可以直接抄的完整main.py。它包含我上面说的三个能力另外加了一个Resource用来展示“可读取数据”是怎么暴露的import os from datetime import datetime from mcp.server.fastmcp import FastMCP mcp FastMCP(local-demo) # 只允许访问当前工作目录及子目录防止路径穿越 BASE_DIR os.path.abspath(.) mcp.tool() def get_current_time() - str: 获取服务器当前的日期和时间返回格式为 年-月-日 时:分:秒 return datetime.now().strftime(%Y-%m-%d %H:%M:%S) mcp.tool() def list_directory(relative_path: str ) - list[str]: 列出当前工作目录下指定相对路径中的文件和文件夹不递归 target os.path.abspath(os.path.join(BASE_DIR, relative_path)) if not target.startswith(BASE_DIR): raise ValueError(f禁止访问基准目录之外的路径: {relative_path}) return sorted(os.listdir(target)) mcp.tool() def text_stats(text: str) - dict: 统计一段文本的字符数、词数和行数返回dict return { chars: len(text), words: len(text.split()), lines: len(text.splitlines()), } mcp.resource(config://app) def get_config() - str: 返回当前MCP Server的基础配置信息 return namelocal-demo\nversion0.1.0\nbase_dir{}.format(BASE_DIR) if __name__ __main__: mcp.run(transportstdio)这段代码里有几个值得解释的细节mcp.tool()装饰器把函数注册成ToolFastMCP会根据类型注解生成Schema。list_directory的参数relative_path默认值是空字符串表示“查看当前目录”。我做了一层路径前缀检查防止Agent胡乱传入../../etc这类危险路径。mcp.resource(config://app)注册的是Resource客户端可以通过config://app这个URI读到内容。Resource适合放“Agent需要但不直接调用函数的信息”比如配置项、说明文档、上下文数据。返回值尽量只用基本类型、list、dict。MCP底层是JSON-RPC自定义对象需要额外序列化处理没必要给自己找麻烦。3.2 跑起来三种验证方式写完代码后先别急着接AI客户端本地跑通再说。推荐用官方自带的调试工具一行命令uv run mcp dev main.py这条命令会启动MCP开发面板通常会自动打开一个浏览器页面里面有工具列表、资源列表、调用测试入口可以直接手动调用get_current_time试试。我第一次用的时候才发现原来MCP协议调试比市面上大多数接口调试工具还方便Schema自动生成调用结果也看得清清楚楚。如果不想开浏览器也可以直接用命令行跑uv run mcp run main.py这种模式会走stdio传输进程启动后等待客户端消息。你可以再开一个终端用一个很小的Python客户端发起一次调用感受一下真实协议交互import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params StdioServerParameters(commanduv, args[run, main.py]) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print(Server上的工具, [t.name for t in tools.tools]) result await session.call_tool(text_stats, {text: hello world, MCP!}) print(text_stats结果, result) asyncio.run(main())能看到工具列表和调用结果就说明协议链路通了。这30分钟里写代码大概占15分钟后面全是验证和接线的时间。3.3 三个我踩过的坑调试输出、相对路径、TypeError先说自己踩过的第一个坑在FastMCP代码里用print()输出调试信息。stdio模式下print出来的内容会混进JSON-RPC数据流客户端直接解析失败报各种奇怪的TypeError。正确做法是用logging模块日志走stderr不会污染协议数据。第二个坑是相对路径。list_directory里的BASE_DIR用的是os.path.abspath(.)这个值依赖进程的工作目录。如果通过客户端配置启动工作目录可能和你在终端里手动跑不一样路径就容易对不上。后来我在Server启动时打印了日志或者直接把要暴露的根目录写成配置项才不会排查半天。第三个坑是返回类型。如果Tool函数返回set、bytes或者自定义对象FastMCP会报序列化错误。要把返回值提前转成list、dict等可JSON序列化的基本结构。这些小问题单看都不难但合在一起确实会耗掉新手不少时间。4. 接进Claude Desktop配置文件、真实Agent调用与排错清单4.1 配置文件不懂路径就卡死半小时本地验证通过后接下来是最有成就感的一步——把Server接进AI客户端让Agent在对话中自动调用工具。以Claude Desktop为例配置文件在macOS~/Library/Application Support/Claude/claude_desktop_config.jsonWindows%APPDATA%\Claude\claude_desktop_config.json文件内容如下{ mcpServers: { local-demo: { command: uv, args: [run, --directory, /绝对路径/mcp-demo, main.py] } } }一个很关键的细节command和args必须拆成数组不能写成uv run main.py这种完整字符串。很多MCP客户端启动失败罪魁祸首就是这里把shell命令当成完整命令传给系统了。另外如果系统环境变量里没有uv或者客户端不是从终端启动的建议直接写python解释器的绝对路径{ mcpServers: { local-demo: { command: /usr/local/bin/python3, args: [/绝对路径/mcp-demo/main.py] } } }怎么确认python绝对路径which python3或者where python3把结果填进去。4.2 配置完成后会发生什么配置好之后重启Claude Desktop在对话框里输入一句带工具意图的话比如“帮我看看当前目录下有哪些文件顺便统计一下main.py文件里的代码行数。”正常流程是Agent识别到需要文件操作能力选择调用list_directory再读取对应文件内容然后把结果整理成人话回复你。整个过程你只需要观察不用干预。这就是MCPAI Agent组合的典型体验。如果用完发现Agent总是回答“我没有这个工具”先去设置界面或日志确认MCP Server有没有出现在已连接列表里。连上了但找不到工具多半是Server启动时报错工具列表没加载出来。4.3 排错清单照着这张表排查现象原因处理方式客户端日志出现command not found启动路径不对或环境变量缺失改成绝对路径确认python或uv已安装Server一直connecting然后超时Server启动慢或启动后崩溃先手动执行uv run mcp run main.py验证是否能秒启动工具调用结果混乱、报序列化错误返回值不可序列化或print污染了数据流清理print改用logging检查返回类型Agent找不到某个工具Server加载失败或配置文件语法错误先跑uv run mcp run main.py看报错再检查JSON逗号和引号路径相关工具总是说找不到目录工作目录不在预期位置在配置里指定--directory或Server启动时写死根路径关于第一条我再多啰嗦一句客户端GUI应用从Finder/桌面启动时可能不会加载shell里的~/.zshrc或~/.bashrc所以uv不在PATH里非常常见。能用绝对路径就别省那几行字。4.4 接入后的一个进阶思路让Agent定义自己的“技能包”MCP除了Tool之外还有Prompt很多教程不重视但Agent操作中它很实用。你可以把“总结代码改动”“生成周报草稿”这类高频任务预制成Prompt模板Client端调用时自动填充。这样做的好处是不需要每个用户都在对话框里描述一遍完整诉求AI Agent直接通过Prompt模板理解上下文输出质量和稳定性都会上一个台阶。FastMCP里注册一个Prompt也很简单mcp.prompt() def weekly_report(project_name: str) - str: return f请为项目 {project_name} 生成一份本周工作周报要求包含目标完成度、遇到的问题、下周计划。加上这个之后当用户输入“生成XX项目周报”时Agent会优先匹配这个Prompt模板而不是自由发挥。MCP三种原语里Prompt常常被低估但在真实生产环境里它是控制Agent行为最轻量有效的工具。5. 从本地玩到生产远程传输、鉴权与Server聚合的取舍5.1 把stdio改成Streamable HTTP让远程Agent也能调用本地玩够了之后下一步就是“别人也能连”。stdio适合同一个机器上的进程通信一旦Server要跑在另一台服务器上就要换传输方式。官方现在主推Streamable HTTP上手极其简单只需要把最后一行改掉if __name__ __main__: # 监听所有网卡上的8000端口允许远程连接 mcp.run(transportstreamable-http, host0.0.0.0, port8000)启动后Server会暴露一个HTTP端点客户端侧把连接方式从stdio_client换成streamablehttp_client填上http://服务器IP:8000/mcp就能连接。Streamable HTTP对比老的SSE方案最大改进是支持请求响应复用连接生命周期更清晰客户端实现也更简单。注意一旦监听0.0.0.0意味着同一网络里的任何人都能访问到这个MCP端点。没有鉴权就把Server暴露到公网基本等于给陌生人递了一把手电筒照进你的数据目录。5.2 无认证裸奔的后果与最小鉴权方案MCP的HTTP传输协议本身没有定义鉴权方式这不代表不需要做。生产环境至少要做一层API Key校验。最小实现可以在Server外面套一个ASGI中间件收到请求时检查请求头里有没有预期的x-api-keyfrom fastapi import FastAPI, Request, HTTPException app FastAPI() EXPECTED_KEY sk-demo-123456 app.middleware(http) async def check_api_key(request: Request, call_next): if request.method OPTIONS: return await call_next(request) token request.headers.get(x-api-key, ) if token ! EXPECTED_KEY: raise HTTPException(status_code401, detailinvalid api key) return await call_next(request)当然FastMCP启动时会自己挂一个ASGI应用你需要在外部把两者粘起来或者直接用网关反代。不同版本API细节有差异所以我更建议的思路是让一小时内的demo先跑在内网等真要上公网时把鉴权统一交给API网关做Server本身保持纯业务这样安全逻辑和服务逻辑各自独立排查问题也快。5.3 与其重复造轮子不如直接用现成ServerMCP生态现在最不缺的就是现成Server。社区里已经有人把Chrome浏览器操作、Office Word文档生成、数据库查询、GitHub、Slack、Notion这些常见工具封装成了MCP Server装好直接添到配置里就能用。如果你发现某个工具已经有人做好就别自己写一个把精力省下来关注“我的场景到底需要哪个”。我说一个亲身经历有一次团队需要让Agent自动操作浏览器填表单我本来准备用Playwright从零写一套浏览器控制服务后来发现社区已有成熟的Chrome MCP Server直接配置进去半小时不到就打通了“Agent读取页面内容、点击按钮、填写输入框”的完整链路。这也印证了标题里USB-C接口的比喻接口统一之后外设厂商越来越多你不需要自己造显示器只需要会插线。5.4 多Server聚合Meta-Server的取舍Server多了之后会遇到一个新问题客户端虽然支持多Server连接但每个Server各自独立Agent无法跨Server组合调用。比如“从数据库读到一份订单再用Word插件生成报告”这种任务需要Agent同时访问两个Server的数据。有两种解决路径客户端层面多连每个Server独立暴露一堆工具Agent根据任务动态选择。简单但工具数量多了之后Agent容易选错。服务端聚合单独写一个聚合层Server把多个子Server的工具统一收集后重新命名、重新描述对外只暴露必要的工具。聚合Server的实现思路是在这个Server的tool方法内部启动一个子MCP客户端去连真正的数据源Server拿到结果再转出来。相当于USB-C Hub上再接一个扩展坞对终端用户来说还是同一个口。但代价是链路变长、调试变难如果子Server挂了聚合层也要跟着做容错。我的建议是刚开始不要追求聚合能少一个环节就少一个环节。5.5 最后那根安全弦给Agent的工具等于给人手权限整个流程跑通之后你可能会很兴奋地加一堆工具但请记住一个原则MCP Tool给Agent的是“手”不是“嘴”。让Agent能读什么、能写哪里、能调哪台服务器本质上就是把对应权限交给了模型。模型再聪明也可能因为提示词注入而执行意外操作。之前做生产部署时我们内部定过几条规则现在分享给你参考工具只暴露最小必要权限能不开放写就只开放读。所有带路径、URL、SQL等外部输入的工具必须做白名单校验。工具说明里明确写清“不要删除”“不要覆盖”等约束Agent会读这些约束。启用审计日志每条工具调用都记录参数和执行结果。这套规则看着基础但在多少次实操里帮我躲过坑。MCP让连接变简单了但也意味着攻击面统一了——一个端口暴露出来所有工具都可能被外部触发。安全设计从第一天就得跟上。最后分享一点个人心得。MCP这套协议从设计上解决了一个很实际的问题过去AI Agent接一个能力就要写一套胶水代码全部耦合在主程序里现在所有能力都被拆成独立Server每个Server只做好一件事新功能就是“写个函数、挂个装饰器、重启一下”。我在实际使用中最大的感受是真正值钱的不是写Server的过程而是你愿不愿意把日常重复操作抽象成一个个可以被Agent调用的原子能力。这个抽象能力不会随着某个协议版本更迭而过时哪怕未来MCP竞争不过其他协议你练出来的拆分思路和接口意识也照样能用。30分钟入门但值得长期投入。
返回列表