构建 MCP 服务器

构建 MCP 服务器

本教程将引导你从零开始构建一个功能完整的 MCP 服务器,帮助你理解 MCP 协议的核心概念和实现细节。

为什么需要 MCP 服务器

MCP(Model Context Protocol)服务器是连接 AI 助手与外部数据、工具的桥梁。通过构建 MCP 服务器,你可以:

  • 扩展 AI 能力:让 Claude 等 AI 助手访问你的数据和服务
  • 保持数据主权:数据无需发送到第三方,AI 通过协议访问
  • 统一接口:一次开发,多种 AI 应用都能使用

适用场景

应用场景说明
自定义 AI 应用为你的应用添加 AI 能力和外部工具
自动化工作流通过 AI 自动调用各种 API 和服务
数据分析让 AI 访问你的数据源进行分析
企业集成将 AI 集成到现有业务系统

核心概念

MCP 服务器提供三种主要能力:

  1. 工具(Tools):可供 LLM 调用的函数,需要用户批准
  2. 资源(Resources):类似文件的数据,可被客户端读取
  3. 提示(Prompts):预定义的提示模板,帮助用户完成特定任务
  flowchart LR
    subgraph AI_Client["AI 客户端"]
        Claude["Claude 等"]
    end

    subgraph MCP_Server["MCP 服务器"]
        Tools["工具<br/>函数调用"]
        Resources["资源<br/>数据读取"]
        Prompts["提示<br/>模板"]
    end

    subgraph Data_Source["数据源"]
        API["外部 API"]
        DB["数据库"]
        Files["文件系统"]
    end

    Claude <--> MCP_Server
    MCP_Server --> Data_Source

    style Tools fill:#e3f2fd
    style Resources fill:#e8f5e9
    style Prompts fill:#fff3e0

我们要构建什么

我们将构建一个天气服务器作为示例,它提供两个工具:

  • get_alerts:获取美国各州的天气警报
  • get_forecast:获取指定位置的天气预报
服务器可以连接到任何客户端。虽然我们这里使用 Claude Desktop 作为示例,但你也可以构建自己的客户端或探索其他可用客户端。
为什么选择 Claude Desktop 而不是 Claude.ai?
因为服务器在本地运行,MCP 目前仅支持桌面主机。远程主机正在积极开发中。

日志记录注意事项

STDIO 传输的服务器严禁向标准输出(stdout)写入日志。这会破坏 JSON-RPC 消息格式。

使用 logging 模块写入 stderr 或文件。

# @error: STDIO 下的错误方式
print("Processing request")  # 会破坏 JSON-RPC

# @correct: STDIO 下的正确方式
import logging
logging.info("Processing request")  # 写入 stderr

语言特定实现

Python 实现

系统要求

  • Python 3.10 或更高版本
  • Python MCP SDK 1.2.0 或更高版本
  • 熟悉 Python 和 async/await

环境设置

安装 uv(包管理器)
curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

创建项目:

# 创建项目目录
uv init weather
cd weather

# 创建并激活虚拟环境
uv venv
source .venv/bin/activate  # macOS/Linux
# .venv\Scripts\activate   # Windows

# 安装依赖
uv add "mcp[cli]" httpx

# 创建服务器文件
touch weather.py

服务器实现

在 weather.py 中添加这些导入和常量:

from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# 初始化 FastMCP 服务器
mcp = FastMCP("weather")

# 常量
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"

添加辅助函数:

async def make_nws_request(url: str) -> dict[str, Any] | None:
    """向 NWS API 发送请求,并进行错误处理。"""
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None


def format_alert(feature: dict) -> str:
    """将天气警报特征格式化为可读文本。"""
    props = feature["properties"]
    return f"""
事件: {props.get("event", "Unknown")}
区域: {props.get("areaDesc", "Unknown")}
严重程度: {props.get("severity", "Unknown")}
描述: {props.get("description", "No description available")}
指导: {props.get("instruction", "No specific instructions provided")}
""".strip()

定义工具:

@mcp.tool()
async def get_alerts(state: str) -> str:
    """获取美国某个州的天气警报。

    Args:
        state: 两字母州代码(例如 CA, NY)
    """
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "无法获取警报或未找到警报。"

    if not data["features"]:
        return f"{state} 当前没有活跃警报。"

    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """获取指定位置的天气预报。

    Args:
        latitude: 位置纬度 (-90 到 90)
        longitude: 位置经度 (-180 到 180)
    """
    # 先获取预报网格端点
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return "无法获取此位置的预报数据。"

    # 从点响应中获取预报 URL
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "无法获取详细预报。"

    # 格式化预报时段
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # 仅显示前 5 个时段
        forecast = f"""
{period["name"]}:
温度: {period["temperature"]}°{period["temperatureUnit"]}
风: {period["windSpeed"]} {period["windDirection"]}
预报: {period["detailedForecast"]}
""".strip()
        forecasts.append(forecast)

    return "\n---\n".join(forecasts)

运行服务器:

def main():
    # 初始化并运行服务器
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()

启动服务器:

uv run weather.py

TypeScript 实现

系统要求

  • Node.js 16 或更高版本
  • 熟悉 TypeScript 和 async/await

环境设置

# 创建项目目录
mkdir weather
cd weather

# 初始化 npm 项目
npm init -y

# 安装依赖
npm install @modelcontextprotocol/sdk zod@3
npm install -D @types/node typescript

# 创建 src 目录
mkdir src
touch src/index.ts

更新 package.json:

{
  "type": "module",
  "bin": {
    "weather": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  },
  "files": ["build"]
}

创建 tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

服务器实现

在 src/index.ts 中添加:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

// 创建服务器实例
const server = new McpServer({
  name: "weather",
  version: "1.0.0",
});

添加辅助函数和接口:

// 用于发出 NWS API 请求的辅助函数
async function makeNWSRequest<T>(url: string): Promise<T | null> {
  const headers = {
    "User-Agent": USER_AGENT,
    Accept: "application/geo+json",
  };

  try {
    const response = await fetch(url, { headers });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return (await response.json()) as T;
  } catch (error) {
    console.error("发出 NWS 请求时出错:", error);
    return null;
  }
}

interface AlertFeature {
  properties: {
    event?: string;
    areaDesc?: string;
    severity?: string;
    status?: string;
    headline?: string;
  };
}

// 格式化警报数据
function formatAlert(feature: AlertFeature): string {
  const props = feature.properties;
  return [
    `事件: ${props.event || "Unknown"}`,
    `区域: ${props.areaDesc || "Unknown"}`,
    `严重程度: ${props.severity || "Unknown"}`,
    `状态: ${props.status || "Unknown"}`,
    `标题: ${props.headline || "No headline"}`,
    "---",
  ].join("\n");
}

interface ForecastPeriod {
  name?: string;
  temperature?: number;
  temperatureUnit?: string;
  windSpeed?: string;
  windDirection?: string;
  shortForecast?: string;
}

interface AlertsResponse {
  features: AlertFeature[];
}

interface PointsResponse {
  properties: {
    forecast?: string;
  };
}

interface ForecastResponse {
  properties: {
    periods: ForecastPeriod[];
  };
}

注册工具:

// 注册天气工具

server.registerTool(
  "get_alerts",
  {
    description: "获取某个州的天气警报",
    inputSchema: {
      state: z
        .string()
        .length(2)
        .describe("两字母州代码(例如 CA, NY)"),
    },
  },
  async ({ state }) => {
    const stateCode = state.toUpperCase();
    const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
    const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);

    if (!alertsData) {
      return {
        content: [
          {
            type: "text",
            text: "检索警报数据失败",
          },
        ],
      };
    }

    const features = alertsData.features || [];
    if (features.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `${stateCode} 没有活跃警报`,
          },
        ],
      };
    }

    const formattedAlerts = features.map(formatAlert);
    const alertsText = `${stateCode} 的活跃警报:\n\n${formattedAlerts.join("\n")}`;

    return {
      content: [
        {
          type: "text",
          text: alertsText,
        },
      ],
    };
  },
);

server.registerTool(
  "get_forecast",
  {
    description: "获取某个位置的天气预报",
    inputSchema: {
      latitude: z
        .number()
        .min(-90)
        .max(90)
        .describe("位置纬度"),
      longitude: z
        .number()
        .min(-180)
        .max(180)
        .describe("位置经度"),
    },
  },
  async ({ latitude, longitude }) => {
    // 获取网格点数据
    const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
    const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);

    if (!pointsData) {
      return {
        content: [
          {
            type: "text",
            text: `无法检索坐标 ${latitude}, ${longitude} 的网格点数据。此位置可能不受 NWS API 支持(仅支持美国位置)。`,
          },
        ],
      };
    }

    const forecastUrl = pointsData.properties?.forecast;
    if (!forecastUrl) {
      return {
        content: [
          {
            type: "text",
            text: "无法从网格点数据获取预报 URL",
          },
        ],
      };
    }

    // 获取预报数据
    const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
    if (!forecastData) {
      return {
        content: [
          {
            type: "text",
            text: "无法检索预报数据",
          },
        ],
      };
    }

    const periods = forecastData.properties?.periods || [];
    if (periods.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: "没有可用的预报时段",
          },
        ],
      };
    }

    // 格式化预报时段
    const formattedForecast = periods.map((period: ForecastPeriod) =>
      [
        `${period.name || "Unknown"}:`,
        `温度: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
        `风: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
        `${period.shortForecast || "No forecast available"}`,
        "---",
      ].join("\n"),
    );

    const forecastText = `${latitude}, ${longitude} 的预报:\n\n${formattedForecast.join("\n")}`;

    return {
      content: [
        {
          type: "text",
          text: forecastText,
        },
      ],
    };
  },
);

运行服务器:

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("天气 MCP 服务器运行在 stdio 上");
}

main().catch((error) => {
  console.error("main() 中的致命错误:", error);
  process.exit(1);
});

构建并运行:

npm run build
node build/index.js

Java 实现(Spring AI)

此快速入门使用 Spring AI MCP 自动配置和 boot starter。有关手动服务器实现,请参阅 Java SDK Server 文档。

系统要求

  • Java 17 或更高版本
  • Spring Boot 3.3.x 或更高版本

环境设置

使用 Spring Initializer 创建包含以下依赖的新项目:

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>
</dependencies>

配置应用属性:

spring.main.bannerMode=off
logging.pattern.console=

服务器实现

创建天气服务:

@Service
public class WeatherService {

    private final RestClient restClient;

    public WeatherService() {
        this.restClient = RestClient.builder()
            .baseUrl("https://api.weather.gov")
            .defaultHeader("Accept", "application/geo+json")
            .defaultHeader("User-Agent", "WeatherApiClient/1.0")
            .build();
    }

    @Tool(description = "获取某个位置的天气预报")
    public String getWeatherForecast(
        double latitude,
        double longitude
    ) {
        // 返回详细预报,包括温度、风和描述
    }

    @Tool(description = "获取美国某个州的天气警报")
    public String getAlerts(
        @ToolParam(description = "两字母州代码") String state
    ) {
        // 返回活跃警报,包括事件类型、严重程度和指导
    }
}

创建主应用:

@SpringBootApplication
public class McpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return MethodToolCallbackProvider.builder()
            .toolObjects(weatherService)
            .build();
    }
}

构建并运行:

./mvnw clean install
java -jar target/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar

Kotlin 实现

系统要求

  • Java 17 或更高版本
  • Kotlin 编译器
  • Gradle

环境设置

# 创建项目
mkdir weather
cd weather
gradle init

选择 Application 作为项目类型,Kotlin 作为语言,Java 17 作为版本。

更新 build.gradle.kts:

val mcpVersion = "0.4.0"
val ktorVersion = "3.1.1"

dependencies {
    implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
    implementation("org.slf4j:slf4j-nop:2.0.9")
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}

plugins {
    kotlin("plugin.serialization") version "2.1.0"
    id("com.gradleup.shadow") version "8.3.9"
}

服务器实现

import io.modelcontextprotocol.sdk.*
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*

// 创建服务器实例
val server = Server(
    Implementation(name = "weather", version = "1.0.0"),
    ServerOptions(capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)))
)

// 创建 HTTP 客户端
val httpClient = HttpClient {
    defaultRequest {
        url("https://api.weather.gov")
        headers {
            append("Accept", "application/geo+json")
            append("User-Agent", "WeatherApiClient/1.0")
        }
    }
    install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}

// 注册 get_alerts 工具
server.addTool(
    name = "get_alerts",
    description = "获取美国某个州的天气警报。输入为两字母州代码(例如 CA, NY)",
    inputSchema = Tool.Input(
        properties = buildJsonObject {
            putJsonObject("state") {
                put("type", "string")
                put("description", "两字母州代码(例如 CA, NY)")
            }
        },
        required = listOf("state")
    )
) { request ->
    val state = request.arguments["state"]?.jsonPrimitive?.content
    if (state == null) {
        return@addTool CallToolResult(content = listOf(TextContent("'state' 参数是必需的。")))
    }

    // 获取并格式化警报
    CallToolResult(content = alerts.map { TextContent(it) })
}

// 注册 get_forecast 工具
server.addTool(
    name = "get_forecast",
    description = "获取特定纬度/经度的天气预报",
    inputSchema = Tool.Input(
        properties = buildJsonObject {
            putJsonObject("latitude") { put("type", "number") }
            putJsonObject("longitude") { put("type", "number") }
        },
        required = listOf("latitude", "longitude")
    )
) { request ->
    // 获取并格式化预报
    CallToolResult(content = forecast.map { TextContent(it) })
}

// 运行服务器
fun main() = runBlocking {
    val transport = StdioServerTransport(System.`in`.asInput(), System.out.asSink().buffered())
    server.connect(transport)
    server.onClose { /* 处理关闭 */ }
}

构建并运行:

./gradlew build
java -jar build/libs/weather-0.1.0-all.jar

C# 实现

系统要求

  • .NET 8 SDK 或更高版本
  • C# 开发环境

环境设置

# 创建项目
dotnet new console -o weather
cd weather

# 添加依赖
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting

服务器实现

更新 Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol;
using System.Net.Http.Headers;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);

builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

builder.Services.AddSingleton(_ =>
{
    var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
    client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
    return client;
});

var app = builder.Build();
await app.RunAsync();
使用 CreateEmptyApplicationBuilder 而不是 CreateDefaultBuilder 以避免为 STDIO 传输写入额外的控制台输出。

创建用于 JSON 处理的扩展类:

using System.Text.Json;

internal static class HttpClientExt
{
    public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
    {
        using var response = await client.GetAsync(requestUri);
        response.EnsureSuccessStatusCode();
        return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
    }
}

定义工具处理器:

using ModelContextProtocol.Server;
using System.ComponentModel;

namespace QuickstartWeatherServer.Tools;

[McpServerToolType]
public static class WeatherTools
{
    [McpServerTool, Description("获取美国某个州代码的天气警报。")]
    public static async Task<string> GetAlerts(
        HttpClient client,
        [Description("要获取警报的州代码。")] string state)
    {
        using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
        var jsonElement = jsonDocument.RootElement;
        var alerts = jsonElement.GetProperty("features").EnumerateArray();

        if (!alerts.Any())
        {
            return "该州当前没有活跃警报。";
        }

        return string.Join("\n--\n", alerts.Select(alert =>
        {
            JsonElement properties = alert.GetProperty("properties");
            return $"""
                    事件: {properties.GetProperty("event").GetString()}
                    区域: {properties.GetProperty("areaDesc").GetString()}
                    严重程度: {properties.GetProperty("severity").GetString()}
                    描述: {properties.GetProperty("description").GetString()}
                    指导: {properties.GetProperty("instruction").GetString()}
                    """;
        }));
    }

    [McpServerTool, Description("获取某个位置的天气预报。")]
    public static async Task<string> GetForecast(
        HttpClient client,
        [Description("位置的纬度。")] double latitude,
        [Description("位置的经度。")] double longitude)
    {
        var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
        using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
        var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
            ?? throw new Exception($"未提供预报 URL");

        using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
        var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();

        return string.Join("\n---\n", periods.Select(period => $"""
                {period.GetProperty("name").GetString()}
                温度: {period.GetProperty("temperature").GetInt32()}°F
                风: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
                预报: {period.GetProperty("detailedForecast").GetString()}
                """));
    }
}

构建并运行:

dotnet run

Rust 实现

系统要求

  • Rust 1.70 或更高版本
  • Cargo(随 Rust 一起安装)

环境设置

# 安装 Rust(如果尚未安装)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 创建项目
cargo new weather
cd weather

更新 Cargo.toml:

[package]
name = "weather"
version = "0.1.0"
edition = "2021"

[dependencies]
rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
tokio = { version = "1.46", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }

服务器实现

在 src/main.rs 中添加:

use anyhow::Result;
use rmcp::{
    ServerHandler, ServiceExt,
    handler::server::{router::tool::ToolRouter, tool::Parameters},
    model::*,
    schemars, tool, tool_handler, tool_router,
};
use serde::Deserialize;
use serde::de::DeserializeOwned;

const NWS_API_BASE: &str = "https://api.weather.gov";
const USER_AGENT: &str = "weather-app/1.0";

定义数据结构:

#[derive(Debug, Deserialize)]
struct AlertsResponse {
    features: Vec<AlertFeature>,
}

#[derive(Debug, Deserialize)]
struct AlertFeature {
    properties: AlertProperties,
}

#[derive(Debug, Deserialize)]
struct AlertProperties {
    event: Option<String>,
    #[serde(rename = "areaDesc")]
    area_desc: Option<String>,
    severity: Option<String>,
    description: Option<String>,
    instruction: Option<String>,
}

#[derive(Debug, Deserialize)]
struct PointsResponse {
    properties: PointsProperties,
}

#[derive(Debug, Deserialize)]
struct PointsProperties {
    forecast: String,
}

#[derive(Debug, Deserialize)]
struct ForecastResponse {
    properties: ForecastProperties,
}

#[derive(Debug, Deserialize)]
struct ForecastProperties {
    periods: Vec<ForecastPeriod>,
}

#[derive(Debug, Deserialize)]
struct ForecastPeriod {
    name: String,
    temperature: i32,
    #[serde(rename = "temperatureUnit")]
    temperature_unit: String,
    #[serde(rename = "windSpeed")]
    wind_speed: String,
    #[serde(rename = "windDirection")]
    wind_direction: String,
    #[serde(rename = "detailedForecast")]
    detailed_forecast: String,
}

#[derive(serde::Deserialize, schemars::JsonSchema)]
pub struct MCPForecastRequest {
    latitude: f32,
    longitude: f32,
}

#[derive(serde::Deserialize, schemars::JsonSchema)]
pub struct MCPAlertRequest {
    state: String,
}

添加辅助函数:

async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
    let client = reqwest::Client::new();
    let rsp = client
        .get(url)
        .header(reqwest::header::USER_AGENT, USER_AGENT)
        .header(reqwest::header::ACCEPT, "application/geo+json")
        .send()
        .await?
        .error_for_status()?;
    Ok(rsp.json::<T>().await?)
}

fn format_alert(feature: &AlertFeature) -> String {
    let props = &feature.properties;
    format!(
        "事件: {}\n区域: {}\n严重程度: {}\n描述: {}\n指导: {}",
        props.event.as_deref().unwrap_or("Unknown"),
        props.area_desc.as_deref().unwrap_or("Unknown"),
        props.severity.as_deref().unwrap_or("Unknown"),
        props.description.as_deref().unwrap_or("无可用描述"),
        props.instruction.as_deref().unwrap_or("无具体指导")
    )
}

fn format_period(period: &ForecastPeriod) -> String {
    format!(
        "{}:\n温度: {}°{}\n风: {} {}\n预报: {}",
        period.name,
        period.temperature,
        period.temperature_unit,
        period.wind_speed,
        period.wind_direction,
        period.detailed_forecast
    )
}

实现服务器:

pub struct Weather {
    tool_router: ToolRouter<Weather>,
}

#[tool_router]
impl Weather {
    fn new() -> Self {
        Self {
            tool_router: Self::tool_router(),
        }
    }

    #[tool(description = "获取美国某个州的天气警报。")]
    async fn get_alerts(
        &self,
        Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
    ) -> String {
        let url = format!(
            "{}/alerts/active/area/{}",
            NWS_API_BASE,
            state.to_uppercase()
        );

        match make_nws_request::<AlertsResponse>(&url).await {
            Ok(data) => {
                if data.features.is_empty() {
                    "该州当前没有活跃警报。".to_string()
                } else {
                    data.features
                        .iter()
                        .map(format_alert)
                        .collect::<Vec<_>>()
                        .join("\n---\n")
                }
            }
            Err(_) => "无法获取警报或未找到警报。".to_string(),
        }
    }

    #[tool(description = "获取某个位置的天气预报。")]
    async fn get_forecast(
        &self,
        Parameters(MCPForecastRequest {
            latitude,
            longitude,
        }): Parameters<MCPForecastRequest>,
    ) -> String {
        let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
        let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
            return "无法获取此位置的预报数据。".to_string();
        };

        let forecast_url = points_data.properties.forecast;

        let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
            return "无法获取此位置的预报数据。".to_string();
        };

        let periods = &forecast_data.properties.periods;
        let forecast_summary: String = periods
            .iter()
            .take(5)
            .map(format_period)
            .collect::<Vec<String>>()
            .join("\n---\n");
        forecast_summary
    }
}

#[tool_handler]
impl ServerHandler for Weather {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            ..Default::default()
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let transport = (tokio::io::stdin(), tokio::io::stdout());
    let service = Weather::new().serve(transport).await?;
    service.waiting().await?;
    Ok(())
}

构建并运行:

cargo build --release
./target/release/weather

与 Claude Desktop 集成

Claude Desktop 尚未在 Linux 上提供。Linux 用户可以继续阅读构建客户端教程。

配置文件位置

平台配置文件路径
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json

配置示例

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/weather",
        "run",
        "weather.py"
      ]
    }
  }
}
{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/ABSOLUTE/PATH/TO/weather/build/index.js"]
    }
  }
}
{
  "mcpServers": {
    "spring-ai-mcp-weather": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-jar",
        "/ABSOLUTE/PATH/TO/mcp-weather-stdio-server.jar"
      ]
    }
  }
}
{
  "mcpServers": {
    "weather": {
      "command": "java",
      "args": [
        "-jar",
        "/ABSOLUTE/PATH/TO/weather/build/libs/weather-0.1.0-all.jar"
      ]
    }
  }
}
{
  "mcpServers": {
    "weather": {
      "command": "dotnet",
      "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
    }
  }
}
{
  "mcpServers": {
    "weather": {
      "command": "/ABSOLUTE/PATH/TO/weather/target/release/weather"
    }
  }
}

必须使用绝对路径。相对路径可能导致服务器无法启动。

在 Windows 上,在路径中使用双反斜杠(\)或正斜杠(/)。

重启 Claude Desktop

配置完成后,必须完全重启 Claude Desktop:

  • macOS:Cmd+Q 或菜单栏选择"退出 Claude"
  • Windows:右键系统托盘图标,选择"退出"

仅关闭窗口不会完全退出应用。

测试服务器

成功配置后,在 Claude 中查找"添加文件、连接器和更多"(+)图标。

验证步骤

  1. 点击 + 图标
  2. 悬停在"连接器"菜单上
  3. 你应该看到你的服务器已列出

测试查询

尝试这些问题测试服务器:

  • “Sacramento 的天气怎么样?”
  • “德克萨斯州当前有哪些天气警报?”
由于这里使用美国国家气象局,查询仅对美国位置有效。

工作原理

  sequenceDiagram
    participant User
    participant Claude
    participant MCP_Client as MCP 客户端
    participant MCP_Server as MCP 服务器
    participant API as NWS API

    User->>Claude: 提问
    Claude->>MCP_Client: 分析可用工具
    MCP_Client->>MCP_Server: 请求工具列表
    MCP_Server-->>MCP_Client: 返回工具定义
    Claude->>MCP_Client: 调用 get_forecast
    MCP_Client->>MCP_Server: 执行工具调用
    MCP_Server->>API: 请求天气数据
    API-->>MCP_Server: 返回数据
    MCP_Server-->>MCP_Client: 返回格式化结果
    MCP_Client-->>Claude: 返回结果
    Claude-->>User: 自然语言回答

故障排除

服务器未显示

展开查看解决方案

检查清单:

  1. 验证配置文件语法(有效的 JSON)
  2. 确认使用绝对路径
  3. 完全重启 Claude Desktop
  4. 检查日志文件:~/Library/Logs/Claude/mcp.log(macOS)

获取详细日志:

# 实时监控日志
tail -f ~/Library/Logs/Claude/mcp*.log

工具调用失败

展开查看解决方案

常见原因:

  1. 坐标不在美国:NWS API 仅支持美国地区
  2. 速率限制:在请求之间添加延迟
  3. 网络问题:检查 API 可访问性

调试步骤:

# 添加调试日志
logging.basicConfig(level=logging.DEBUG)

Windows 路径问题

展开查看解决方案

在 JSON 配置中,Windows 路径需要使用双反斜杠或正斜杠:

{
  "args": [
    "C:\\Users\\YourName\\weather",  // 正确
    "C:/Users/YourName/weather"      // 也正确
  ]
}

下一步