到今天为止,你一直是租客:你安装别人写的 MCP,你的 AI 使用它们。挺好。但总有一刻,这不再够用——你有一个内部 API、一个数据库、一段只有你懂的脚本,而你想让 Claude 直接碰它,而不是把输出复制粘贴到聊天里。就在那里,你从消费转向发布。一个 MCP 服务器就是那个标准插座:你把你的工具描述一次,任何讲这门协议的 AI 都能像调用自己的一样调用它们。FastMCP 是那个包裹官方 MCP SDK(Anthropic 开源的那个)的 TypeScript 框架,它替你卸下样板代码的负担——就像 Express 在 Node 的 http 模块之上给你的那一跃。在这份指南里,我们从十五分钟里一个走 stdio 的 'add' 工具,走到带 Zod schema、自我校验的工具、暴露数据的 resources、可复用的 prompts,最后是真正的生产:认证、HTTP streaming、会话和 edge。到最后,你会拥有一个 Claude Code 真会调用的、属于你自己的 MCP——不是一个 demo,是一根连着你世界的、属于你的管道。
你已经把 AI 连着各种工具用了好几周。你让它打开浏览器,它就开;你让它读个仓库,它就读。这些全都是别人写的 MCP——某人发布、你安装的标准插座。它们能用,是因为大家讲一门共同语言:Model Context Protocol,一个开放标准,让任何 AI 都能发现并调用外部工具,而不必和其中任何一个耦合。
那个时刻会在你需要的东西不存在为 MCP 时到来。你有个内部端点会返回订单状态。一张只有你会读的电子表格。一段 40 行、算着你领域里某个特定东西的脚本。今天,每次你想让 AI 用它,你都得跳一遍跑腿舞:你自己执行,复制输出,粘到聊天里,等待。做一次没问题。到第十次,你会开始纳闷,为什么 AI 不自己来。
这份资源合上了一个闭环。在 agent-browser 里,你学会了给 AI 一种外部能力去消费世界——看页面、点击、抽取。这里,你把镜头转过来:你学会发布你自己的能力,让别的 AI 去消费。从用户到作者。同一个协议,只是从电线的另一头看过去。
当你第一次打开官方 MCP SDK,那种感觉就像掀开一辆赛车的引擎盖:一切都在那儿,一切都正确,但没有一样是显而易见的。你得手动注册 handler,映射协议的 request,把响应序列化成客户端期望的确切格式,管理连接周期,自己解析输入参数并校验。就为了暴露一个把两个数字相加的工具,你要在真正做加法那一行之前,写下几十行管道代码。
这份痛从哪来?来自官方 SDK 有意做成低层。它是构建之上的基座,不是最终的人体工学——就像 Node 的 http 模块虽然正确,但没人会直接拿它写 Web 服务器,大家用 Express。SDK 给你对协议的完全掌控;代价是所有样板代码都压在你身上。对构建框架的人来说这没问题。对只想插上自己 API 的人来说,它并不友好。
FastMCP 的存在正是为了抹掉那些样板代码。它是一个 TypeScript 框架,包裹着官方 MCP SDK——不是取代它,而是站在它之上——给你一套声明式 API:你描述工具、它的 schema 和它的函数,剩下的 FastMCP 全包(注册、序列化、连接周期、校验)。那个相加的工具,从那团管道代码缩到十来行出头。而且都是重要的那几行:真正做加法的那些。
用于构建 MCP 服务器、免去样板代码的 TypeScript 框架。它站在官方 MCP SDK(@modelcontextprotocol/sdk,Anthropic 开源的那个)之上,加上了带 schema 校验的工具(Zod 及其他 Standard Schema)、resources、prompts、认证、HTTP streaming、会话,以及一个开发用的 CLI。这是从一个想法走到 Claude 真正会调用的 MCP 服务器最快的方式。
发布一个 MCP 服务器不是那种做一次就忘的事。它是一种在特定时刻被触发的反射。学会识别它们,你就永远知道,什么时候值得花那点时间去构建它。
FastMCP 像任何 npm 包一样安装。它跑在现代 Node 上,带 TypeScript。一个最小的新项目,一个文件夹里三个文件就够了。
# En una carpeta nueva para tu servidor mkdir mi-mcp && cd mi-mcp npm init -y # FastMCP + Zod (para los esquemas de entrada) + tsx (para correr TS sin compilar) npm install fastmcp zod npm install -D tsx typescript
a 是数字,email 是符合邮箱格式的字符串,edad 是可选的整数。FastMCP 拿着这个 schema,免费帮你做两件事:(1) 告诉客户端你的工具期望哪些参数,好让 AI 把它们填对;(2) 在输入到达你代码之前就校验它。如果 AI 发来垃圾,拦下它的是 schema,不是你的函数。它是门口一个你不必自己写的守卫。(FastMCP 也接受同一标准下的其他校验器——ArkType、Valibot——但 Zod 是走得最多的那条路。)我们从 MCP 的“hello world”开始:一个只有一个工具、把两个数字相加的服务器。它故意很蠢——重点不是加法,而是看清整条电线:定义服务器、注册一个带 schema 的工具、把它启动起来,并让 Claude 调用它。一旦你有了这副骨架,其余一切都是变体。
// server.ts
import { FastMCP } from "fastmcp";
import { z } from "zod";
const server = new FastMCP({
name: "Mi Servidor",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Suma dos números",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async (args) => {
return String(args.a + args.b);
},
});
server.start({
transportType: "stdio",
});从上到下读一遍,因为这个模式会在你构建的一切里重复。new FastMCP 用名字和版本创建服务器——那就是 Claude 会在连接列表里看到的。addTool 注册一个工具:一个名字(AI 用它来调用)、一段描述(AI 读它来决定何时使用——写好它,这是面向机器的定向营销)、作为 Zod schema 的 parameters,以及 execute,那个真正运行的函数。你返回一个字符串,FastMCP 帮你把它包进协议格式。
transportType: "stdio" 意思是服务器通过标准输入和输出说话——就是终端程序接收和发出文本的那同一个通道。它是最简单的传输方式:客户端(Claude Code)把你的服务器当作一个子进程启动,并通过那根管子和它对话。零网络,零端口,零配置。非常适合你自己的本地工具。当你想把它暴露到互联网上时,你会把这个传输换成 HTTP——但作为开头,stdio 就是你需要的全部。在把它插到 Claude 之前,先隔离测它。FastMCP 自带一个 CLI,能启动你的服务器并让你和它对话,外加官方的 MCP Inspector,让你在可视化界面里看它。这是你的开发循环:改代码,在这里测,只有当它能用时才接到真正的客户端上。
# Arranca tu servidor en modo desarrollo (interactúas con los tools) npx fastmcp dev server.ts # Ábrelo en el MCP Inspector (interfaz visual para inspeccionar tools/resources/prompts) npx fastmcp inspect server.ts
npx fastmcp dev 当你的工作台:在那儿你能看到原始的报错、确切的输出、你服务器发布出来的 schema 本身。只有当工具做到你想要的样子时,你才迈出把它连到客户端那一步。就像你不会为了测一个 if 而部署到生产——先在本地测。就在这里,它从一次练习变成真事。一个 MCP 客户端(Claude Desktop、Claude Code)把你的服务器当子进程启动,并通过 stdio 和它说话。它只需要知道怎么启动它:什么命令、什么参数。这活在一个配置文件里,里面是 MCP 服务器的清单。
{
"mcpServers": {
"mi-servidor": {
"command": "npx",
"args": ["tsx", "/ruta/absoluta/a/mi-mcp/server.ts"]
}
}
}command 是启动你服务器的可执行文件,args 是它的参数——这里我们用 npx tsx 直接跑 TypeScript,不必编译。用绝对路径:客户端不知道你从哪个文件夹启动它。保存并重启客户端后,你的服务器就出现在列表里,当你让 Claude “把 128 和 45 相加”,你会看到它调用你的 add 工具,而不是自己心算。那个时刻——你第一个工具被 AI 自己调用——就是让人上瘾的那一刻。
args 里放了相对路径(./server.ts)。客户端从它自己的工作目录启动子进程,不是你的,所以 ./server.ts 指向虚无,服务器起不来——通常还是无声无息。如果你的 MCP 没出现或连接失败,先查这个:绝对路径,永远。add 工具用 Zod 只做了最起码的事。但真正的力量,出现在真实工具上——那里输入有形状、有规则。想象一个创建用户的工具:email 必须符合邮箱格式,年龄必须是正整数,角色只能是一个列表里的某一个。用 Zod,你把这一切描述出来,FastMCP 会在无效输入触碰你代码之前把它拒掉。你永远不用写一句 if (!email.includes('@'))。schema 就是那个守卫。
server.addTool({
name: "crear_usuario",
description: "Crea un usuario en el sistema con validación completa",
parameters: z.object({
nombre: z.string().min(2),
email: z.string().email(),
edad: z.number().int().positive().optional(),
rol: z.enum(["admin", "editor", "lector"]),
}),
execute: async (args) => {
// Si llegaste aquí, args YA está validado: email es email, rol es válido.
const usuario = await miBaseDeDatos.insertar(args);
return `Usuario ${usuario.id} creado con rol ${args.rol}`;
},
});execute 里,args 带着 TypeScript 类型进来,还有自动补全。一个代码块,又校验、又文档、又类型。这正是官方 SDK 逼你手写三遍的东西。工具是个动词——AI 执行它来让某件事发生。但有时你不想要一个动作,你想给出上下文:一个日志文件、一份配置文档、一个 README 的内容。为此存在着 resources。它们是你服务器暴露、AI 在需要时可以读取的数据,由一个 URI 标识。把工具想成遥控器上的按钮,把 resources 想成展示信息的屏幕。
server.addResource({
uri: "file:///logs/app.log",
name: "Logs de la Aplicación",
mimeType: "text/plain",
async load() {
const contenido = await fs.readFile("/var/log/app.log", "utf-8");
return { text: contenido };
},
});uri 是 resource 的唯一标识符——客户端用它来请求。mimeType 告诉 AI 内容是什么类型(纯文本、JSON、markdown),好让它正确解读。而 load 是有人请求时把内容取来的那个函数——设计上是惰性的:不到需要时不读文件。对于二进制数据,你返回 base64 的 { blob } 而不是 { text }。一个 resource 是干净地说“AI,这里有新鲜数据,等你需要时来取”的方式,直到那个确切时刻才在 prompt 里消耗它们。
协议的第三种原料是 prompts。如果你有一份一直在用的模板——“根据这个 diff 生成一条 commit 信息”“用这个格式总结这场会议”——MCP 的一个 prompt 就把它连同一个名字和几个参数打包,任何客户端都能调用它。你不再复制粘贴同一长串指令:你把它当作服务器的又一种能力提供出来。
server.addPrompt({
name: "git-commit",
description: "Genera un mensaje de commit a partir de un diff",
arguments: [
{
name: "changes",
description: "El diff de git o una descripción de los cambios",
required: true,
},
],
load: async (args) => {
return `Genera un mensaje de commit conciso para estos cambios:\n${args.changes}`;
},
});上面这一切都跑在 stdio 上:本地的、你自己的、你机器上的一个子进程。作为开头很完美。但当你想让别人用你的 MCP——你的团队、一个客户、你自己从好几个地方——你需要把它暴露到互联网上。这时 HTTP streaming 传输就登场了:不再是一根本地管子,你的服务器监听一个端口,通过 HTTP 用事件流响应。你工具的代码一行都不变;你只改它怎么启动。
server.start({
transportType: "httpStream",
httpStream: {
port: 8080,
endpoint: "/mcp", // opcional, por defecto es /mcp
},
});那就是全部的开关。同样的 tools、同样的 resources、同样的 prompts——现在通过 HTTP 服务在 8080 端口、/mcp 端点上。一个远程客户端指向 https://tu-dominio/mcp 就插上了。就在这里,你的 MCP 从一个个人工具变成了一项服务。也在这里,那个把玩具和产品分开的问题出现了:谁能调用它?
一个没有认证、走 HTTP 的 MCP,是一扇敞开的门:任何知道 URL 的人都能调用你的工具、碰你的数据库、耗你的资源。FastMCP 用一个 authenticate 选项解决这个:一个在每次连接时运行的函数,它检视请求(headers、API key、token),决定放不放行。如果放行,它返回一个会话对象,这个对象会在每个工具内部可用。
const server = new FastMCP({
name: "Mi Servidor",
version: "1.0.0",
authenticate: (request) => {
const apiKey = request.headers["x-api-key"];
if (apiKey !== process.env.MCP_API_KEY) {
throw new Response(null, { status: 401, statusText: "Unauthorized" });
}
return { id: 1, role: "user" }; // esto se vuelve la sesión
},
});
server.addTool({
name: "sayHello",
execute: async (args, { session }) => {
return `Hola, usuario ${session.id}!`; // session viene de authenticate
},
});authenticate 校验的那个 token。它们不是对手:一个生产级 MCP 是带锁的管道。没有锁,你的管道就是一个谁都能拧开的公共水龙头。process.env.MCP_API_KEY。这个 key 绝不在代码里写死,也不上传到 git——这是密钥那份资源里同一条黄金法则:出仓库,放在环境变量或密钥管理器里。一个把 key 硬编码的 MCP,是一扇门上漆着“已锁”、却根本没有真锁的 MCP。任何看到代码的人,都能进。真实的工具不是瞬时的。一个处理大文件、调用慢 API 或生成内容的工具要花上几秒。FastMCP 在 execute 内部给你一个上下文,带着一些工具,让那个工具呼吸:上报进度、流式发出内容、记录日志,并访问已认证用户的会话。
server.addTool({
name: "procesar_lote",
description: "Procesa un lote reportando progreso en vivo",
parameters: z.object({ total: z.number() }),
annotations: { streamingHint: true },
execute: async (args, { streamContent, reportProgress, log, session }) => {
log.info(`Iniciando lote para ${session.id}`);
await streamContent({ type: "text", text: "Empezando..." });
await reportProgress({ progress: 50, total: 100 });
// ... trabajo real ...
return "Completado";
},
});reportProgress({ progress, total }) 告诉客户端“我进行到 50% 了”,AI(和人)于是看到一条进度条,而不是一片令人焦虑的沉默。streamContent 随着内容生成一块块地发出输出,而不是等到最后——这是看着文字浮现,和盯着一个转圈圈之间的差别。log 留下结构化的痕迹供调试。而 session 是 authenticate 校验过的那个用户,于是每个工具都知道谁在调用它。有了这些,你的工具不再是耗时的黑盒,而变成可观察的过程。
这就是你唯一需要保存的 prompt。与其手写服务器,你把它描述给你的 AI——它面前已经有了这份指南里 FastMCP 的全部上下文——它就会生成完整的骨架,测试过的、可以直接连接的。用你真实的情况填好方括号,让它去构建。只有一个 prompt,不是五个:这一个就把整个项目启动起来。
扮演一名资深 MCP 工程师。我们要用 FastMCP(那个包裹官方 MCP SDK、Anthropic 开源的那个框架;用 `npm install fastmcp zod` 安装)在 TypeScript 里构建一个 MCP 服务器。
我的服务器:
- 名称:[你服务器的名称,例如 "CRM Interno"]
- 它暴露什么:[用 2 行描述你想让 AI 碰的工具/数据]
我需要的 TOOLS(AI 会执行的动作):
1. [nombre_tool] — [做什么] — 输入:[字段及其类型/规则]
2. [nombre_tool] — [做什么] — 输入:[字段及其类型/规则]
RESOURCES(AI 会读取的数据,如适用):
- [nombre] — [暴露什么数据,例如日志、配置]
模式:[开发时从 "stdio local" 开始 | 之后为了生产迁移到 "httpStream con autenticación por API key"]
请构建:
1. 完整的 server.ts:带名称和版本的 `new FastMCP`,每个工具用 `addTool`(name、清晰的、面向让 AI 知道何时使用它的 description、作为真正会校验的 Zod schema 的 parameters——该用的地方用 .email()、.int().positive()、.enum(),可选项用 .optional(),以及带类型的 execute)。
2. 如果我要了,用 `addResource` 写 resources(uri、name、mimeType、返回 {text} 或 {blob} 的惰性 load)。
3. 用我选的传输方式写 `server.start`(stdio,或带 port/endpoint 的 httpStream)。
4. 如果我要了生产:`authenticate` 选项,从 `process.env` 读 API key(绝不硬编码),不匹配时抛 `new Response(null, {status: 401})` 并返回会话,外加一个使用 `session` 的工具。
5. 用来把它连到 Claude Code 的 `mcpServers` JSON 块,用 `npx tsx` 和绝对路径。
6. 确切的命令:安装、用 `npx fastmcp dev server.ts` 测试、用 `npx fastmcp inspect server.ts` 检视。
约束:代码里零密钥;工具的描述要写成 让 AI 能很好地决定何时调用它们;用 Zod 校验所有输入,不要手写 if。在代码前用一行向我解释每个工具。npx fastmcp dev 能测的最小量。当它能用、Claude 真的会调用它时,再回到同一个 prompt,把模式换成带认证的生产。这样你是分层地、可验证地构建,而不是吐出一个没测过的巨型服务器。并应用一贯的习惯:先给能用的骨架来一个 commit,再来其余的。发布一个 MCP,根据你站在哪儿有两条路,值得知道各自何时该走。
server.ts,带 Zod、传输方式和配置。mcpServers 块,以及 fastmcp dev 的命令,让你在插上之前先测。npx fastmcp inspect server.ts 打开可视化的 Inspector——在那儿你像客户端一样看到你的 tools、resources 和 prompts。退后一步,看看这条路。你从消费别人写的 MCP 开始——你的 AI 用着别人的工具。你用 agent-browser 学会给它一种能力去看、去碰世界。而在这里,用 FastMCP,你把镜头转过来:现在你发布你自己的能力,让任何 AI 去消费。一个十五分钟的 add 工具,变成了带 Zod、resources、prompts、认证和 HTTP 的工具。你合上了用户→作者的弧线。你不再是 MCP 生态的租客:你是一个插座的主人。
Join 4,200+ builders. No credit card. Build your first app with AI in minutes.