MCP Server 开发入门:从零搭建你的第一个 MCP 服务
导语: 你在 Cursor 里写代码时,有没有想让 AI 直接查一下项目依赖的最新版本?或者在 Claude 里分析数据时,希望它能读取你数据库里的信息?如果分别实现,你得为每个 AI 工具写一套适配代码。有了 MCP Server,你只需要写一次,所有支持 MCP 的客户端都能用。这篇文章会带你从零开始,用 TypeScript 手写一个完整的 MCP Server。
MCP 是什么?3 分钟理解核心概念
一个 USB 接口的故事
用过几年前数码产品的人,应该记得那个尴尬的时期:鼠标用圆口,键盘用方口,打印机用并口,每个设备都要找对应的插孔。后来 USB 出现了,一个接口解决所有问题。
MCP(Model Context Protocol)正在成为 AI 工具世界的”USB 标准”。
没有 MCP 的时候,你想让 AI 访问某个数据源,就得为每个 AI 工具单独写适配层:给 Claude 写一个插件,给 Cursor 写一个扩展,给 Windsurf 再写一个……复杂度是 N x M(N 个数据源 x M 个 AI 工具)。
有了 MCP 之后,你只需要写一个 MCP Server,所有支持 MCP 的客户端都能直接调用。复杂度降到了 N+M。
三层架构很简单:
+-------------+ +-------------+ +-------------+
| Host | -> | Client | -> | Server |
| (Claude) | | (MCP 客户端)| | (你的服务) |
+-------------+ +-------------+ +-------------+
- Host:AI 应用本身,比如 Claude Desktop、Cursor
- Client:MCP 客户端,负责和 Host 通信
- Server:你写的服务,提供具体功能
MCP Server 提供的三种能力
MCP Server 可以提供三种不同类型的功能:
| 能力 | 用途 | 示例 |
|---|---|---|
| Tools(工具) | 执行操作 | 查询天气、发送消息、读取数据库 |
| Resources(资源) | 提供数据 | 文件内容、API 返回、配置信息 |
| Prompts(提示) | 预定义模板 | 代码审查模板、日报生成模板 |
把 Tools 理解成”函数”——AI 可以调用它执行某个动作;Resources 是”数据源”——AI 可以读取其中的内容;Prompts 是”模板”——帮助 AI 更快理解任务。
与现有文章的区别:如果你看过其他 MCP 教程,可能见过用 Python 和 FastMCP 实现的版本。本文使用 TypeScript 原生 SDK,更适合前端和全栈开发者。两种实现功能等价,选你熟悉的语言就好。
""
开发环境准备
前置要求
这篇文章假设你已经:
- 安装 Node.js 18+ 或 Bun 1.0+
- 写过 TypeScript,知道
interface、async/await是什么 - 有 Claude Desktop 或支持 MCP 的客户端(Cursor、Windsurf 等)
如果你没用过 Bun,推荐尝试一下——它比 npm 快很多,而且内置了 TypeScript 支持,不用额外配置 ts-node。
初始化项目
# 创建项目目录
mkdir mcp-weather-server && cd mcp-weather-server
# 初始化(使用 Bun 或 npm)
bun init -y
# 或 npm init -y
# 安装 MCP TypeScript SDK
bun add @modelcontextprotocol/sdk zod
# 或 npm install @modelcontextprotocol/sdk zod
这里用到了两个依赖:
@modelcontextprotocol/sdk:MCP 官方 TypeScript SDKzod:TypeScript 运行时类型校验,用来定义工具参数的 schema
TypeScript 配置要点
如果你用 bun init,tsconfig.json 已经配好了。手动配置的话,注意这几个选项:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true
}
}
moduleResolution: "bundler" 对 ESM 模块很重要,不然你可能遇到”xxx is not defined”的错误。
实战:手写天气查询 MCP Server
这个教程会带你完成一个完整的 MCP Server,它能:
- 接收 AI 的调用请求
- 查询 OpenWeatherMap API 获取实时天气
- 返回格式化的结果
项目结构设计
mcp-weather-server/
+-- src/
| +-- index.ts # 入口文件
| +-- weather.ts # 天气工具实现
| +-- resources.ts # 资源定义
+-- package.json
+-- tsconfig.json
实际代码可以都放在 index.ts 里(我们这篇文章就这么做),但拆分成模块更利于维护。
第一步:创建 MCP Server 骨架
从最简单的开始——创建一个能启动的 MCP Server:
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 创建服务器实例
const server = new McpServer({
name: "weather-service",
version: "1.0.0",
});
// 注册工具(Tools)
server.tool(
"get_weather",
"获取指定城市的当前天气信息",
{
city: z.string().describe("城市名称,如:北京、上海"),
},
async ({ city }) => {
// 工具实现将在下一节展开
return { content: [{ type: "text", text: `查询 ${city} 的天气...` }] };
}
);
// 启动服务器
const transport = new StdioServerTransport();
await server.connect(transport);
McpServer 是 SDK 提供的核心类,你需要传入 name 和 version。tool() 方法用来注册一个工具,第一个参数是工具名,第二个是描述,第三个是参数 schema,最后是执行函数。
第二步:实现天气查询工具(核心代码)
现在让工具真正工作起来。我们用 OpenWeatherMap 的免费 API:
// src/weather.ts
import { z } from "zod";
// 定义 OpenWeatherMap API 响应类型
interface WeatherResponse {
name: string;
main: { temp: number; feels_like: number; humidity: number };
weather: [{ description: string }];
wind: { speed: number };
}
// 天气查询工具实现
server.tool(
"get_weather",
"获取指定城市的当前天气信息",
{
city: z.string().describe("城市名称,如:北京、上海"),
},
async ({ city }) => {
const API_KEY = process.env.OPENWEATHER_API_KEY;
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric&lang=zh_cn`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`API 请求失败:${response.status}`);
}
const data: WeatherResponse = await response.json();
// 返回格式化结果
return {
content: [
{
type: "text",
text: JSON.stringify({
city: data.name,
temperature: `${data.main.temp}°C`,
feels_like: `${data.main.feels_like}°C`,
description: data.weather[0].description,
humidity: `${data.main.humidity}%`,
wind_speed: `${data.wind.speed} m/s`,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `查询失败:${error instanceof Error ? error.message : '未知错误'}`,
},
],
isError: true,
};
}
}
);
注意:
- API Key 从环境变量读取:永远不要把密钥写死在代码里
- 错误处理:返回
isError: true让客户端知道调用失败了 - 类型定义:
WeatherResponse接口让 TypeScript 帮你检查数据结构
去 OpenWeatherMap 注册一个免费账号,拿到 API Key 后设置环境变量:
export OPENWEATHER_API_KEY=your_api_key_here
第三步:添加 Resources(可选但推荐)
Resources 让你的 Server 能提供”只读”数据。比如你可以提供一个资源,让 AI 查看服务器状态:
// src/resources.ts
// 提供服务器状态信息
server.resource(
"server-status",
"status://server",
async (uri) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify({
name: "Weather Service",
version: "1.0.0",
status: "running",
timestamp: new Date().toISOString(),
}, null, 2),
},
],
})
);
// 提供 API 文档
server.resource(
"api-docs",
"docs://api",
async (uri) => ({
contents: [
{
uri: uri.href,
text: `
# Weather MCP Server API
## Tools
- get_weather(city: string): 获取指定城市天气
## Resources
- status://server - 服务器状态
- docs://api - API 文档
`.trim(),
},
],
})
);
resource() 的前两个参数是资源名和 URI,第三个是读取函数。URI 可以是任意 scheme,比如 status://、docs://,只要你能区分就行。
第四步:添加 Prompts(进阶功能)
Prompts 是预定义的对话模板。比如你可以定义一个”天气报告”模板,AI 使用时会自动填充城市名:
// 预定义的天气报告模板
server.prompt(
"weather_report",
"生成一份格式化的天气报告",
{
city: z.string().describe("城市名称"),
include_tips: z.boolean().optional().describe("是否包含穿衣建议"),
},
({ city, include_tips }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `请为${city}生成一份天气报告。${include_tips ? "同时提供穿衣建议。" : ""}`,
},
},
],
})
);
prompt() 的返回值是一个消息数组,每个消息有 role 和 content。这样 AI 在使用时会直接获得预设的上下文。
第五步:完善入口文件
把上面所有代码整合到 src/index.ts,加上错误处理:
// src/index.ts (完整版)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "weather-service",
version: "1.0.0",
});
// 注册所有工具、资源、提示
// ...(上述代码)
// 错误处理
process.stdin.on("error", (err) => {
console.error("标准输入错误:", err);
process.exit(1);
});
process.stdout.on("error", (err) => {
console.error("标准输出错误:", err);
process.exit(1);
});
// 优雅退出
process.on("SIGINT", async () => {
await server.close();
process.exit(0);
});
// 启动服务器
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Weather Server 已启动,等待连接...");
StdioServerTransport 使用标准输入输出通信,所以 stdin/stdout 的错误处理很重要。SIGINT 处理让你能用 Ctrl+C 优雅地停止服务。
用 bun run src/index.ts 启动,看到”已启动”提示就说明一切正常。
配置客户端:让 Claude 用上你的 Server
Server 写好了,现在要让 Claude Desktop 或 Cursor 能调用它。
Claude Desktop 配置
找到配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
添加你的 Server 配置:
{
"mcpServers": {
"weather": {
"command": "bun",
"args": ["run", "/absolute/path/to/mcp-weather-server/src/index.ts"],
"env": {
"OPENWEATHER_API_KEY": "你的 API Key"
}
}
}
}
注意:args 中的路径必须是绝对路径。相对路径会导致 Server 启动失败。
Cursor / Windsurf 配置
Cursor 和 Windsurf 的配置方式类似,在 IDE 设置中找到 MCP 配置,添加服务器条目(格式同上)。
Cursor 的配置文件通常在:
- macOS:
~/Library/Application Support/Cursor/User/globalStorage/state.vscdb - 或者在 IDE 内:设置 -> AI -> MCP -> 添加服务器
测试你的 Server
- 重启 Claude Desktop / Cursor
- 在对话框中输入:“帮我查一下北京的天气”
- Claude 应该会自动调用你的 MCP Server
如果看到类似这样的输出,说明成功了:
{
"city": "北京",
"temperature": "18°C",
"feels_like": "16°C",
"description": "多云",
"humidity": "65%",
"wind_speed": "3.2 m/s"
}
常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| Server 未连接 | 路径错误 | 检查 args 中的绝对路径 |
| API Key 无效 | 环境变量未传递 | 确认 env 配置正确 |
| 无响应 | TypeScript 编译错误 | 先用 bun build 或 tsc 编译 |
| 权限错误 | 配置文件权限 | 确保配置文件可读 |
""
扩展与部署建议
添加更多工具
天气查询只是个开始。你可以:
- 历史天气查询:调用历史数据 API,返回过去某天的天气
- 多城市对比:一次性查询多个城市,返回对比表格
- 天气预警订阅:检查是否有恶劣天气预警
这些工具的注册方式和 get_weather 完全一样,只是实现逻辑不同。
部署选项对比
如果你想把 Server 分享给团队用,本地 stdio 传输就不够用了。下面是几种部署方式:
| 部署方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 本地 stdio | 个人使用、开发测试 | 简单、安全 | 无法共享 |
| HTTP/SSE | 团队共享、多用户 | 可远程访问 | 需处理认证 |
| Serverless | 生产环境 | 自动扩缩容 | 冷启动延迟 |
生产环境注意事项
认证:HTTP 部署时必须实现认证。MCP 支持 OAuth 2.1,你也可以用简单的 API Key:
// 检查请求头中的 API Key
const apiKey = request.headers.get("Authorization");
if (apiKey !== `Bearer ${process.env.API_KEY}`) {
return new Response("Unauthorized", { status: 401 });
}
限流:避免恶意调用耗尽你的 API 配额。可以用 express-rate-limit 或 Cloudflare Workers 的内置限流。
日志:使用 pino 或 winston 记录工具调用,方便排查问题:
import pino from "pino";
const logger = pino();
server.tool("get_weather", /* ... */, async ({ city }) => {
logger.info({ city }, "查询天气");
// ...
});
监控:追踪工具调用成功率、响应时间。Prometheus + Grafana 是常用组合。
总结
这篇文章介绍了如何使用 TypeScript 从零开始编写 MCP Server。内容包括:
- 理解 MCP 的核心概念和三层架构
- 用 MCP TypeScript SDK 创建服务器
- 实现天气查询工具(Tools)
- 添加服务器状态资源(Resources)
- 定义天气报告模板(Prompts)
- 配置 Claude Desktop / Cursor 调用 Server
现在你可以:
- 为你常用的 API 构建 MCP 包装器(GitHub、Slack、Notion 等)
- 创建内部业务系统的 MCP 接口(CRM、数据库)
- 探索 MCP 社区已有的成果
进阶学习资源:
如果你想深入了解 MCP 协议的原理,可以阅读深入了解 MCP 协议原理这篇文章。
常见问题
MCP Server 开发需要什么基础?
MCP Server 和 FastMCP 有什么区别?
如何测试 MCP Server 是否正常工作?
MCP Server 可以部署到远程服务器吗?
9 分钟阅读 · 发布于: 2026年3月19日 · 修改于: 2026年3月19日
相关文章
OpenClaw 2026.3 实战进阶:新版本核心功能与最佳实践
OpenClaw 2026.3 实战进阶:新版本核心功能与最佳实践
OpenClaw 实战完全手册:从入门到精通
OpenClaw 实战完全手册:从入门到精通
不做单一模型的囚徒:在 Antigravity 中灵活切换 Gemini 3、Claude 4.5 与 GPT-OSS

评论
使用 GitHub 账号登录后即可评论