切换语言
切换主题

MCP Server 开发入门:从零搭建你的第一个 MCP 服务

导语: 你在 Cursor 里写代码时,有没有想让 AI 直接查一下项目依赖的最新版本?或者在 Claude 里分析数据时,希望它能读取你数据库里的信息?如果分别实现,你得为每个 AI 工具写一套适配代码。有了 MCP Server,你只需要写一次,所有支持 MCP 的客户端都能用。这篇文章会带你从零开始,用 TypeScript 手写一个完整的 MCP Server。

30 分钟
上手时间
从零到运行
3 种
核心能力
Tools/Resources/Prompts
1000+
MCP 服务器
GitHub 开源社区
数据来源: MCP 官方数据 (2025 年)

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,知道 interfaceasync/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 SDK
  • zod:TypeScript 运行时类型校验,用来定义工具参数的 schema

TypeScript 配置要点

如果你用 bun inittsconfig.json 已经配好了。手动配置的话,注意这几个选项:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true
  }
}

moduleResolution: "bundler" 对 ESM 模块很重要,不然你可能遇到”xxx is not defined”的错误。


实战:手写天气查询 MCP Server

这个教程会带你完成一个完整的 MCP Server,它能:

  1. 接收 AI 的调用请求
  2. 查询 OpenWeatherMap API 获取实时天气
  3. 返回格式化的结果

项目结构设计

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 提供的核心类,你需要传入 nameversiontool() 方法用来注册一个工具,第一个参数是工具名,第二个是描述,第三个是参数 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,
      };
    }
  }
);

注意:

  1. API Key 从环境变量读取:永远不要把密钥写死在代码里
  2. 错误处理:返回 isError: true 让客户端知道调用失败了
  3. 类型定义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() 的返回值是一个消息数组,每个消息有 rolecontent。这样 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

  1. 重启 Claude Desktop / Cursor
  2. 在对话框中输入:“帮我查一下北京的天气”
  3. 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 buildtsc 编译
权限错误配置文件权限确保配置文件可读

""


扩展与部署建议

添加更多工具

天气查询只是个开始。你可以:

  • 历史天气查询:调用历史数据 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 的内置限流。

日志:使用 pinowinston 记录工具调用,方便排查问题:

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

现在你可以:

  1. 为你常用的 API 构建 MCP 包装器(GitHub、Slack、Notion 等)
  2. 创建内部业务系统的 MCP 接口(CRM、数据库)
  3. 探索 MCP 社区已有的成果

进阶学习资源

如果你想深入了解 MCP 协议的原理,可以阅读深入了解 MCP 协议原理这篇文章。

常见问题

MCP Server 开发需要什么基础?
需要 JavaScript/TypeScript 基础。本文使用 MCP TypeScript SDK,熟悉 async/await 和类型定义即可上手。
MCP Server 和 FastMCP 有什么区别?
FastMCP 是 Python 框架,适合 Python 开发者。本文使用 TypeScript 原生 SDK,适合前端/全栈开发者。两者功能等价,选择取决于技术栈偏好。
如何测试 MCP Server 是否正常工作?
配置 Claude Desktop 后,在对话框中输入自然语言请求(如'查一下北京天气'),如果 Claude 自动调用工具并返回结果,说明 Server 工作正常。
MCP Server 可以部署到远程服务器吗?
可以。本文使用 stdio 传输适合本地开发。生产环境可使用 HTTP/SSE 传输,需实现 OAuth 认证和限流保护。

9 分钟阅读 · 发布于: 2026年3月19日 · 修改于: 2026年3月19日

评论

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

相关文章