游乐游手机版
首页/AI热点日报/热点详情

循序渐进构建MCP服务器教程

类型:热点整理2026-07-02
这次咱们来拆解一个实际项目:如何基于 TypeScript 构建一个完整的 MCP 服务器。别担心,整个过程会一步步拆开揉碎了讲,从环境搭建到代码实现,再到集成 Claude Desktop 进行测试,一条龙说清楚。 为了不让这个教程显得太干,我们会用一个非常接地气的场景——**天气查询服务**——

这次咱们来拆解一个实际项目:如何基于 TypeScript 构建一个完整的 MCP 服务器。别担心,整个过程会一步步拆开揉碎了讲,从环境搭建到代码实现,再到集成 Claude Desktop 进行测试,一条龙说清楚。

为了不让这个教程显得太干,我们会用一个非常接地气的场景——**天气查询服务**——来贯穿始终。具体来说,我们会搭建一个 MCP 天气服务器,它能把当前天气数据作为“资源”暴露出来,同时还能让 Claude 通过调用“工具”来获取未来的天气预报。

既然是做天气服务,数据源就是重中之重。这里我们会用到 OpenWeatherMap 的 API[2]。你只需要去它的官网注册一个账号,然后在 API keys[3] 页面,就能免费领到一个 API 密钥,非常方便。

环境准备

工欲善其事,必先利其器。搞 TypeScript 项目,Node.js 和 NPM 是绕不开的。先把它们检查一下:

# 检查 Node.js 版本,需要 v18 或更高版本
node --version

# 检查 npm 版本
npm --version

环境到位后,生成 MCP 服务器的脚手架就简单多了。官方贴心地准备了一个工具,直接一行命令搞定:

$ npx @modelcontextprotocol/create-server weather-server
Need to install the following packages:
@modelcontextprotocol/create-server@0.3.1
Ok to proceed? (y) y

? What is the name of your MCP server? y
? What is the description of your server? A Model Context Protocol server
? Would you like to install this server for Claude.app? Yes
✔ MCP server created successfully!
✓ Successfully added MCP server to Claude.app configuration

Next steps:
  cd weather-server
  npm install
  npm run build  # or: npm run watch
  npm link       # optional, to make a vailable globally

$ cd weather-server

脚手架生成后,进到项目目录,顺手再装两个我们接下来会用到的依赖包:

npm install --sa ve axios dotenv

因为要调用外部 API,自然少不了环境变量。在项目根目录下创建一个 .env 文件,把 API 密钥放进去:

OPENWEATHER_API_KEY=your-api-key-here

别忘了,这个 .env 里藏着你的私钥,一定要记得把它加到 .gitignore 文件里,避免泄漏到代码仓库中。

项目创建完毕后,它的结构大致如下:

模板分析

脚手架生成的模板,其实已经是一个麻雀虽小、五脏俱全的 MCP 服务器示例了。它自带了一个简单的笔记系统,用来演示 MCP 的几个核心概念:

  • 将笔记作为“资源”列出
  • 读取特定的笔记内容
  • 通过“工具”创建新笔记
  • 通过“提示词”总结所有笔记

在动笔写我们自己的天气服务器之前,花点时间把这个模板吃透,绝对是一笔划算的投入。理解它的运行机制,能让你在后面写代码时思路更清晰。

先看看它导入了哪些东西:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

代码第一行,从 MCP 的 SDK 里导入了 Server 对象。可以把它理解成一个“MCP 服务器”的化身,它会自动响应来自客户端的初始化流程。后面导入的那些 ListResourcesRequestSchemaReadResourceRequestSchema 等,分别对应了 MCP 协议中定义的各类请求类型。

接着,模板通过 Server 创建了一个服务器实例:

const server = new Server(
  {
    name: "weather-server",
    version: "0.1.0",
  },
  {
    capabilities: {
      resources: {},
      tools: {},
      prompts: {},
    },
  }
);

这个实例具备资源、工具和提示词三种能力,并且指定了服务器的名称和版本号。

服务器就绪后,核心工作就是通过 Server 对象的 setRequestHandler 方法来注册处理器。这和写 HTTP 服务器时,针对不同的 API 端点注册处理逻辑是一模一样的道理。当协议对象收到对应类型的请求时,就会自动调用你注册好的函数。

接下来,我们逐一拆解模板中注册的几个处理器。

资源(Resources)

先聊聊 Resources(资源)。在 MCP 协议里,“资源”是一个核心概念。你可以把服务器上的各种数据和内容——比如笔记、文件、数据库记录——都包装成“资源”暴露给 LLM。然后 LLM 就可以通过工具来操作这些资源。

不过有一点需要注意:**资源本质上是由应用程序(客户端)控制的**。这意味着客户端应用可以决定何时以及如何使用这些资源。不同的 MCP 客户端对此处理方式也不同,比如:

  • Claude Desktop 目前要求用户在使用前手动选择资源。
  • 其他客户端则可能会自动帮用户选择。
  • 甚至有些实现允许 AI 模型自己决定要使用哪些资源。

作为服务器开发者,你在实现资源支持时,必须考虑到各种交互模式。如果你的目标是让模型能自动调用数据,那么更合适的方式是利用“工具”(Tools)这类由模型控制的原语。

笼统地说,任何类型的数据都可以成为资源:

  • 文件内容
  • 数据库记录
  • API 响应
  • 实时系统数据
  • 截图和图片
  • 日志文件
  • 等等

每个资源都由唯一的 URI 来标识,并且可以包含文本或二进制数据。

在模板里,它就把所有笔记作为资源暴露出来。通过 setRequestHandler 注册了一个处理器,专门处理客户端的 resources/list 请求:

/**
 * 用于列出可用笔记作为资源的处理程序。
 * 每个笔记都作为具有以下特征的资源公开:
 * - note:// URI scheme
 * - MIME 类型
 * - 人类可读的名称和描述(包括笔记标题)
 */
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: Object.entries(notes).map(([id, note]) => ({
      uri: `note:///${id}`,
      mimeType: "text/plain",
      name: note.title,
      description: `A text note: ${note.title}`,
    })),
  };
});

资源 URI

资源的 URI 遵循 <协议>://<主机>/<路径> 这样的格式:

:///

举几个例子:

  • file:///home/user/documents/report.pdf
  • postgres://database/customers/schema
  • screen://localhost/display1

这里的 protocolpath 结构完全由 MCP 服务器的实现者(也就是你)来定义,灵活性极高。

资源类型

资源的内容类型分为两种:

文本资源

包含 UTF-8 编码的文本数据。适用于:

  • 源代码
  • 配置文件
  • 日志文件
  • JSON/XML 数据
  • 纯文本

二进制资源

包含以 base64 编码的原始二进制数据。适用于:

  • 图片
  • PDF 文件
  • 音频文件
  • 视频文件
  • 其他非文本格式

资源发现

客户端可以通过两种主要方式来发现可用资源:

直接资源

服务器通过 resources/list 端点,直接暴露一个具体的资源列表。每个资源包含:

{
  uri: string;           // 资源的唯一标识符
  name: string;          // 人类可读的名称
  description?: string;  // 可选描述
  mimeType?: string;     // 可选的 MIME 类型
}

资源模板

对于动态资源,服务器可以暴露 URI 模板,客户端可以利用这个模板来构造有效的资源 URI:

{
  uriTemplate: string;   // 遵循 RFC 6570 的 URI 模板
  name: string;          // 该类型的人类可读名称
  description?: string;  // 可选描述
  mimeType?: string;     // 所有匹配资源的可选 MIME 类型
}

读取资源

要读取一个资源,客户端会用资源的 URI 发起 resources/read 请求。服务器响应如下所示:

{
  contents: [
    {
      uri: string;        // 资源的 URI
      mimeType?: string;  // 可选的 MIME 类型
      // 其中之一:
      text?: string;      // 对于文本资源
      blob?: string;      // 对于二进制资源 (base64 编码)
    }
  ]
}

有趣的是,服务器在一次 resources/read 请求中,可以返回多个资源。比如,当你请求读取一个目录时,服务器可以返回该目录下的所有文件列表。

模板里读取笔记的处理器是这样实现的:

/**
 * 用于读取指定笔记内容的处理程序。
 * 接受一个 note:// URI 并返回笔记内容作为纯文本。
 */
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const url = new URL(request.params.uri);
  const id = url.pathname.replace(/^//, "");
  const note = notes[id];

  if (!note) {
    throw new Error(`Note ${id} not found`);
  }

  return {
    contents: [
      {
        uri: request.params.uri,
        mimeType: "text/plain",
        text: note.content,
      },
    ],
  };
});

资源更新

MCP 提供了两种方式来支持资源的实时更新:

列表变更

当可用的资源列表发生变化时(比如新增或删除了一个资源),服务器可以通过发送 notification/resources/list_changed 通知来告知客户端。

内容变更

客户端可以订阅特定资源的更新:

  • 客户端使用资源 URI 发送 resources/subscribe 请求
  • 当该资源的内容发生变化时,服务器发送 notification/resources/update 通知
  • 客户端收到通知后,再通过 resources/read 来获取最新内容
  • 客户端也可以随时取消订阅

工具(Tools)

接下来看 Tools(工具)。如果说资源是“数据”,那么工具就是“操作”。工具让 LLM 能够通过你的服务器执行具体的动作。它们使服务器能够向客户端暴露可执行的函数,AI 模型可以(在获得用户批准后)自动调用这些函数,去与外部系统交互、执行计算、或者在现实世界中执行任务。

MCP 中的工具主要有以下几个维度:

  • 发现:客户端通过 tools/list 端点来查看有哪些可用的工具。
  • 调用:客户端调用 tools/call 端点来执行一个工具,服务器执行操作并返回结果。
  • 灵活性:工具可以是简单的计算,也可以是复杂的 API 交互。

与资源类似,工具也由唯一的名称标识,并可以附带描述来说明其用途。但关键区别在于,工具通常代表着可以修改状态或与外部系统交互的**动态操作**。

每个工具的结构定义如下:

{
  name: string;          // 工具的唯一标识符
  description?: string;  // 人类可读的描述
  inputSchema: { // 工具参数的 JSON Schema
    type: "object";
    properties: { ... } // 工具特定参数
  }
}

看模板代码就清楚了。它先通过 setRequestHandler 注册了一个工具列表处理器:

/**
 * 用于列出可用工具的处理程序。
 * 暴露一个 "create_note" 工具,让客户端创建新笔记。
 */
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "create_note",
        description: "Create a new note",
        inputSchema: {
          type: "object",
          properties: {
            title: {
              type: "string",
              description: "Title of the note",
            },
            content: {
              type: "string",
              description: "Text content of the note",
            },
          },
          required: ["title", "content"],
        },
      },
    ],
  };
});

上面这段代码定义了一个名为 create_note 的工具。它的作用不言而喻,就是让 LLM 能创建新笔记。它还清晰地声明了自己需要的参数:title(笔记标题)和 content(笔记内容),并且这两个参数都是必须的。这样一来,客户端(比如 Claude)就知道有这么一个工具可以调用,并且知道调用它时需要传什么参数。

不过,光是列出工具还不够。要让工具真正“动”起来,还得注册一个工具调用处理器:

/**
 * 用于创建新笔记的处理程序。
 * 使用提供的标题和内容创建新笔记,并返回成功消息。
 */
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    case "create_note": {
      const title = String(request.params.arguments?.title);
      const content = String(request.params.arguments?.content);
      if (!title || !content) {
        throw new Error("Title and content are required");
      }

      const id = String(Object.keys(notes).length + 1);
      notes[id] = { title, content };

      return {
        content: [
          {
            type: "text",
            text: `Created note ${id}: ${title}`,
          },
        ],
      };
    }

    default:
      throw new Error("Unknown tool");
  }
});

逻辑非常直白:根据请求中传来的工具名称 create_note,拿到参数,创建一条新笔记,然后返回一条成功消息。当客户端调用 create_note 这个工具时,就会触发这段代码。

提示词(Prompts)

最后来看 Prompts(提示词)。在 MCP 协议里,提示词是一种用于定义可重复使用的提示词模板和工作流程的机制。客户端可以利用这些模板,轻松地向用户和 LLM 展示复杂的任务。与资源不同,提示词被设计为由**用户控制**,它们从服务器暴露给客户端,让用户能显式地选择使用它们。

MCP 中的提示词是预定义的模板,它们可以:

  • 接受动态参数
  • 从资源中包含上下文
  • 链式引导多个交互
  • 指导特定工作流程
  • 在 UI 中以斜杠命令等形式呈现

每个提示词的结构如下:

{
  name: string;              // 提示词的唯一标识符
  description?: string;      // 人类可读的描述
  arguments?: [              // 可选的参数列表
    {
      name: string;          // 参数的标识符
      description?: string;  // 参数的描述
      required?: boolean;    // 是否必须
    }
  ]
}

客户端可以通过 prompts/list 端点来发现所有可用的提示词:

// Request
{
  method: "prompts/list"
}

// Response
{
  prompts: [
    {
      name: "analyze-code",
      description: "Analyze code for potential improvements",
      arguments: [
        {
          name: "language",
          description: "Programming language",
          required: true
        }
      ]
    }
  ]
}

然后通过 prompts/get 端点来获取指定提示词的详细信息:

// Request
{
  "method": "prompts/get",
  "params": {
    "name": "analyze-code",
    "arguments": {
      "language": "python"
    }
  }
}

// Response
{
  "description": "Analyze Python code for potential improvements",
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Please analyze the following Python code for potential improvements:nn```pythonndef calculate_sum(numbers):n    total = 0n    for num in numbers:n        total = total + numn    return totalnnresult = calculate_sum([1, 2, 3, 4, 5])nprint(result)n```"
      }
    }
  ]
}

模板代码中,也通过 setRequestHandler 注册了一个提示词列表处理器:

/**
 * 用于列出可用提示词的处理程序。
 * 暴露一个 "summarize_notes" 提示词,用于总结所有笔记。
 */
server.setRequestHandler(ListPromptsRequestSchema, async () => {
  return {
    prompts: [
      {
        name: "summarize_notes",
        description: "Summarize all notes",
      },
    ],
  };
});

这里注册了一个名为 summarize_notes 的提示词,用于总结笔记。值得注意的是,它并没有定义任何参数,所以客户端调用时无需传入额外信息。

紧接着,它注册了获取该提示词详细信息的处理器:

/**
 * 用于总结所有笔记的提示词处理器。
 * 返回一个提示词,请求总结所有笔记,并将笔记内容作为资源嵌入。
 */
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  if (request.params.name !== "summarize_notes") {
    throw new Error("Unknown prompt");
  }

  const embeddedNotes = Object.entries(notes).map(([id, note]) => ({
    type: "resource" as const,
    resource: {
      uri: `note:///${id}`,
      mimeType: "text/plain",
      text: note.content,
    },
  }));

  return {
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: "Please summarize the following notes:",
        },
      },
      ...embeddedNotes.map((note) => ({
        role: "user" as const,
        content: note,
      })),
      {
        role: "user",
        content: {
          type: "text",
          text: "Provide a concise summary of all the notes above.",
        },
      },
    ],
  };
});

从这段代码能看出来,它在生成提示词时,会把所有笔记的内容都嵌入进去,这样 LLM 在接收到提示词时,就已经有了完整的笔记上下文,可以直接进行总结。

启动服务器

处理器都注册好了,服务器也准备就绪,就差临门一脚——启动它。模板中使用了 stdio 传输来启动:

/**
 * 使用 stdio 传输启动服务器。
 * 允许服务器通过标准输入/输出流进行通信。
 */
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

stdio 传输通过标准输入输出流进行通信,非常适合本地集成和命令行工具。在以下场景中,它非常顺手:

  • 构建命令行工具
  • 实施本地集成
  • 需要简单的进程通信
  • 使用 shell 脚本

当然,MCP 也支持基于 HTTP 的 SSE(服务器发送事件)传输,它通过 HTTP POST 进行客户端到服务器的通信。在你需要服务器到客户端的流式传输、或网络受限的环境下,SSE 是更好的选择。

编写代码

模板分析完毕,理论武装到位,是时候动真格的了。我们的目标是构建一个天气查询服务,思路很清晰:把天气数据作为“资源”暴露,再提供一个查询天气的“工具”。

首先,定义好天气数据相关的类型,这能让我们在后续编码中如鱼得水:

// src/types/weather.ts
export interface OpenWeatherResponse {
  main: {
    temp: number;
    humidity: number;
  };
  weather: Array<{
    description: string;
  }>;
  wind: {
    speed: number;
  };
  dt_txt?: string;
}

export interface WeatherData {
  temperature: number;
  conditions: string;
  humidity: number;
  wind_speed: number;
  timestamp: string;
}

export interface ForecastDay {
  date: string;
  temperature: number;
  conditions: string;
}

export interface GetForecastArgs {
  city: string;
  days?: number;
}

// 类型保护函数,用于检查 GetForecastArgs 类型
export function isValidForecastArgs(args: any): args is GetForecastArgs {
  return (
    typeof args === "object" &&
    args !== null &&
    "city" in args &&
    typeof args.city === "string" &&
    (args.days === undefined || typeof args.days === "number")
  );
}

这些类型定义几乎完全对照了 OpenWeather API 的返回数据结构。

接着,编写核心代码,将整个 src/index.ts 文件内容替换掉:

// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import dotenv from "dotenv";
import {
  WeatherData,
  ForecastDay,
  OpenWeatherResponse,
  isValidForecastArgs,
} from "./types.js";

dotenv.config();

const API_KEY = process.env.OPENWEATHER_API_KEY;
if (!API_KEY) {
  throw new Error("OPENWEATHER_API_KEY environment variable is required");
}

const API_CONFIG = {
  BASE_URL: "http://api.openweathermap.org/data/2.5",
  DEFAULT_CITY: "San Francisco",
  ENDPOINTS: {
    CURRENT: "weather",
    FORECAST: "forecast",
  },
} as const;

class WeatherServer {
  private server: Server;
  private axiosInstance;

  constructor() {
    this.server = new Server(
      {
        name: "weather-server",
        version: "0.1.0",
      },
      {
        capabilities: {
          resources: {},
          tools: {},
        },
      }
    );

    // 配置 axios 实例
    this.axiosInstance = axios.create({
      baseURL: API_CONFIG.BASE_URL,
      params: {
        appid: API_KEY,
        units: "metric",
      },
    });

    this.setupHandlers();
    this.setupErrorHandling();
  }

  private setupErrorHandling(): void {
    this.server.onerror = (error) => {
      console.error("[MCP Error]", error);
    };

    process.on("SIGINT", async () => {
      await this.server.close();
      process.exit(0);
    });
  }

  private setupHandlers(): void {
    this.setupResourceHandlers();
    this.setupToolHandlers();
  }

  private setupResourceHandlers(): void {
    // TODO: 实现资源处理器
  }

  private setupToolHandlers(): void {
    // TODO: 实现工具处理器
  }

  async run(): Promise {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);

    console.error("Weather MCP server running on stdio");
  }
}

const server = new WeatherServer();
server.run().catch(console.error);

这段代码在模板的基础上做了一层封装,使用类的方式来组织逻辑。核心步骤包括:

  • 定义天气资源的数据类型
  • 初始化 MCP 服务器实例
  • 预留注册资源和工具处理器的空方法(以 TODO 标记)
  • 启动服务器

接下来,我们只要把两个 TODO 空方法填上即可。

实现资源处理器

setupResourceHandlers 方法中,完整实现资源处理的逻辑:

private setupResourceHandlers(): void {
  this.server.setRequestHandler(
    ListResourcesRequestSchema,
    async () => ({
      resources: [{
        uri: `weather://${API_CONFIG.DEFAULT_CITY}/current`,
        name: `Current weather in ${API_CONFIG.DEFAULT_CITY}`,
        mimeType: "application/json",
        description: "Real-time weather data including temperature, conditions, humidity, and wind speed"
      }]
    })
  );

  this.server.setRequestHandler(
    ReadResourceRequestSchema,
    async (request) => {
      const city = API_CONFIG.DEFAULT_CITY;
      if (request.params.uri !== `weather://${city}/current`) {
        throw new McpError(
          ErrorCode.InvalidRequest,
          `Unknown resource: ${request.params.uri}`
        );
      }

      try {
        const response = await this.axiosInstance.get(
          API_CONFIG.ENDPOINTS.CURRENT,
          {
            params: { q: city }
          }
        );

        const weatherData: WeatherData = {
          temperature: response.data.main.temp,
          conditions: response.data.weather[0].description,
          humidity: response.data.main.humidity,
          wind_speed: response.data.wind.speed,
          timestamp: new Date().toISOString()
        };

        return {
          contents: [{
            uri: request.params.uri,
            mimeType: "application/json",
            text: JSON.stringify(weatherData, null, 2)
          }]
        };
      } catch (error) {
        if (axios.isAxiosError(error)) {
          throw new McpError(
            ErrorCode.InternalError,
            `Weather API error: ${error.response?.data.message ?? error.message}`
          );
        }
        throw error;
      }
    }
  );
}

列出资源的处理器很简单,我们自定义了 weather 协议,数据类型设定为 JSON。在读取资源时,通过 axios 请求 OpenWeather API 获取实时数据,然后将其转换为 WeatherData 类型返回。

实现工具处理器

资源处理器搞定后,再来实现工具处理器。这里我们要实现一个用于查询未来天气预报的工具:

private setupToolHandlers(): void {
  this.server.setRequestHandler(
    ListToolsRequestSchema,
    async () => ({
      tools: [{
        name: "get_forecast",
        description: "Get weather forecast for a city",
        inputSchema: {
          type: "object",
          properties: {
            city: {
              type: "string",
              description: "City name"
            },
            days: {
              type: "number",
              description: "Number of days (1-5)",
              minimum: 1,
              maximum: 5
            }
          },
          required: ["city"]
        }
      }]
    })
  );

  this.server.setRequestHandler(
    CallToolRequestSchema,
    async (request) => {
      if (request.params.name !== "get_forecast") {
        throw new McpError(
          ErrorCode.MethodNotFound,
          `Unknown tool: ${request.params.name}`
        );
      }

      if (!isValidForecastArgs(request.params.arguments)) {
        throw new McpError(
          ErrorCode.InvalidParams,
          "Invalid forecast arguments"
        );
      }

      const city = request.params.arguments.city;
      const days = Math.min(request.params.arguments.days || 3, 5);

      try {
        const response = await this.axiosInstance.get<{
          list: OpenWeatherResponse[]
        }>(API_CONFIG.ENDPOINTS.FORECAST, {
          params: {
            q: city,
            cnt: days * 8 // API 返回 3 小时间隔的数据
          }
        });

        const forecasts: ForecastDay[] = [];
        for (let i = 0; i < response.data.list.length; i += 8) {
          const dayData = response.data.list[i];
          forecasts.push({
            date: dayData.dt_txt?.split(' ')[0] ?? new Date().toISOString().split('T')[0],
            temperature: dayData.main.temp,
            conditions: dayData.weather[0].description
          });
        }

        return {
          content: [{
            type: "text",
            text: JSON.stringify(forecasts, null, 2)
          }]
        };
      } catch (error) {
        if (axios.isAxiosError(error)) {
          return {
            content: [{
              type: "text",
              text: `Weather API error: ${error.response?.data.message ?? error.message}`
            }],
            isError: true,
          }
        }
        throw error;
      }
    }
  );
}

同样,先列出可用的工具(这里只有一个 get_forecast),再实现具体的调用逻辑。它需要两个参数:城市名称 city(必填)和查询天数 days(可选,默认3天)。数据依然是通过请求 OpenWeather API 来获取的。

有一个小细节值得注意:其实我们上面定义的资源,完全可以通过一个工具来动态获取,因为数据源头都是 OpenWeather API。这里之所以单独定义资源,主要是为了演示 MCP 中“资源”和“工具”这两种不同的原语及其使用方法。

测试

编码完成,接下来就是见证奇迹的时刻——测试。

首先构建项目:

npm run build

然后更新 Claude Desktop 的配置文件:

code ~/Library/Application Support/Claude/claude_desktop_config.json

将我们的天气服务添加进去:

{
  "mcpServers": {
    //...... 其他服务器配置
    "weather": {
      "command": "node",
      "args": ["/Users/cnych/src/weather-server/build/index.js"],
      "env": {
        "OPENWEATHER_API_KEY": "your_openweather_api_key"
      }
    }
  }
}

注意 args 要替换成你本地实际的构建文件路径,env 里填上你自己的 OpenWeather API Key。配置搞定后,重启 Claude Desktop。

测试时,点击 Claude Desktop 输入框右下角的数字小图标,就能看到我们定义的 get_forecast 工具了。

接下来,你就可以用自然语言向 Claude 提问了,比如:

Can you get me a 5-day forecast for Beijing and tell me if I should pack an umbrella?

你就可以看到 Claude 调用了 get_forecast 工具(需要你授权),并返回了结果。

调试

测试过程中,如果遇到“水土不服”的情况,别慌,有几种调试手段可以用。

1. 查看 MCP 日志

这是最直接的排查方式:

# 实时查看日志
tail -n 20 -f ~/Library/Logs/Claude/mcp*.log

日志里会包含服务器连接事件、配置问题、运行时错误和消息交换记录等信息,问题出在哪一目了然。

2. 使用 Chrome DevTools

Claude Desktop 也提供了一套开发工具。先在配置文件 ~/Library/Application Support/Claude/developer_settings.json 中开启:

{
  "allowDevTools": true
}

然后按下快捷键 Command+Option+Shift+i,就能唤醒 DevTools,操作方式跟调试网页一模一样。

3. 官方 MCP Inspector 工具

这是官方专门为 MCP 服务器提供的交互式调试工具,极其好用。直接用 npx 命令就可以启动,无需安装:

npx @modelcontextprotocol/inspector 

如果服务器包来自 NPM,可以这样:

npx -y @modelcontextprotocol/inspector npx  
# 例如
npx -y @modelcontextprotocol/inspector npx server-postgres postgres://127.0.0.1/testdb

如果是本地构建的包,则这样:

npx @modelcontextprotocol/inspector node path/to/server/index.js args...

针对我们的天气服务:

npx @modelcontextprotocol/inspector node /Users/cnych/src/weather-server/build/index.js

Inspector 启动后,会在 localhost:5173 开启一个 Web 页面。在这里,你需要先点击右侧的 Environment Variables 按钮,填入 OPENWEATHER_API_KEY 环境变量,然后点击 Connect 连接服务。

连接成功后,右侧主窗口就会列出天气服务的资源和工具。你可以逐个点击测试:点击 List Resources 查看资源列表,选中后点击即可读取内容并展示;点击 List Tools 查看工具,然后选中某个工具、填入参数、点击 Run Tool,结果一目了然。

当然,除了资源和工具,Inspector 还支持测试 Prompts、Sampling 等高级功能。

至此,一个功能完整的天气 MCP 服务器就大功告成了。整个过程走下来,你应该对 MCP 协议中的资源、工具和提示词这几个核心概念有了更直观和深入的理解。下次再遇到类似的需求,就可以举一反三,快速搭起自己的服务了。

来源:https://www.53ai.com/news/finetuning/2025030382017.html

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。