从零构建MCP Server:为AI Agent打造统一工具接口的实战指南
2026/9/11 11:25:23 网站建设 项目流程

最近只要在折腾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规范走一遍鉴权和路由。三个能力各连各的,客户端逻辑越来越肿,每次新增一个工具都要重新部署、重新调试。

后来社区里开始推MCP(Model 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客户端连接Server,Server暴露以上三种原语,模型在对话过程中自主决定“现在需要调用哪个工具”。整个过程对用户是透明的,体验上就是“你问一句,Agent自己把活干了”。

1.3 和其它接入方式的对比

我做了一张对比表,方便你判断MCP和传统方案的区别:

接入方式连接成本适用范围生态情况
MCP Server实现一次协议,任意客户端通用本地工具、外部API、资源读取均可覆盖生态快速膨胀,官方SDK成熟
Function Calling每次调用都要写独立封装只随特定模型API走封闭,迁移成本高
OpenAPI / Plugin需要维护一份OpenAPI描述适合公开HTTP API依赖客户端支持,各平台不互通
直接SDK调用代码侵入最重单一应用内改需求就要改代码

从表里能看出来,MCP最大的价值不是“功能更强”,而是“连接方式被统一了”。你写好一个Server,Claude能用、别的支持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就出来了,很适合快速验证想法。

准备工作两步:

  1. 安装Python 3.10以上版本,保证python3 --version能正常输出。
  2. 安装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 "name=local-demo\nversion=0.1.0\nbase_dir={}".format(BASE_DIR) if __name__ == "__main__": mcp.run(transport="stdio")

这段代码里有几个值得解释的细节:

  • @mcp.tool()装饰器把函数注册成Tool,FastMCP会根据类型注解生成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(command="uv", 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函数返回setbytes,或者自定义对象,FastMCP会报序列化错误。要把返回值提前转成listdict等可JSON序列化的基本结构。这些小问题单看都不难,但合在一起确实会耗掉新手不少时间。

4. 接进Claude Desktop:配置文件、真实Agent调用与排错清单

4.1 配置文件:不懂路径就卡死半小时

本地验证通过后,接下来是最有成就感的一步——把Server接进AI客户端,让Agent在对话中自动调用工具。以Claude Desktop为例,配置文件在:

  • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows:%APPDATA%\Claude\claude_desktop_config.json

文件内容如下:

{ "mcpServers": { "local-demo": { "command": "uv", "args": ["run", "--directory", "/绝对路径/mcp-demo", "main.py"] } } }

一个很关键的细节:commandargs必须拆成数组,不能写成"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,再读取对应文件内容,然后把结果整理成人话回复你。整个过程你只需要观察,不用干预。这就是MCP+AI 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(transport="streamable-http", host="0.0.0.0", port=8000)

启动后,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-key

from 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_code=401, detail="invalid api key") return await call_next(request)

当然FastMCP启动时会自己挂一个ASGI应用,你需要在外部把两者粘起来,或者直接用网关反代。不同版本API细节有差异,所以我更建议的思路是:让一小时内的demo先跑在内网,等真要上公网时,把鉴权统一交给API网关做,Server本身保持纯业务,这样安全逻辑和服务逻辑各自独立,排查问题也快。

5.3 与其重复造轮子,不如直接用现成Server

MCP生态现在最不缺的就是现成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分钟入门,但值得长期投入。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询