构建 MCP 服务器
本教程将引导你从零开始构建一个功能完整的 MCP 服务器,帮助你理解 MCP 协议的核心概念和实现细节。
为什么需要 MCP 服务器
MCP(Model Context Protocol)服务器是连接 AI 助手与外部数据、工具的桥梁。通过构建 MCP 服务器,你可以:
- 扩展 AI 能力:让 Claude 等 AI 助手访问你的数据和服务
- 保持数据主权:数据无需发送到第三方,AI 通过协议访问
- 统一接口:一次开发,多种 AI 应用都能使用
适用场景
| 应用场景 | 说明 |
|---|---|
| 自定义 AI 应用 | 为你的应用添加 AI 能力和外部工具 |
| 自动化工作流 | 通过 AI 自动调用各种 API 和服务 |
| 数据分析 | 让 AI 访问你的数据源进行分析 |
| 企业集成 | 将 AI 集成到现有业务系统 |
核心概念
MCP 服务器提供三种主要能力:
- 工具(Tools):可供 LLM 调用的函数,需要用户批准
- 资源(Resources):类似文件的数据,可被客户端读取
- 提示(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.ai?
日志记录注意事项
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 | shpowershell -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.pyTypeScript 实现
系统要求
- 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.jsJava 实现(Spring AI)
系统要求
- 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.jarKotlin 实现
系统要求
- 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.jarC# 实现
系统要求
- .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 runRust 实现
系统要求
- 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 集成
配置文件位置
| 平台 | 配置文件路径 |
|---|---|
| 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 中查找"添加文件、连接器和更多"(+)图标。
验证步骤
- 点击 + 图标
- 悬停在"连接器"菜单上
- 你应该看到你的服务器已列出
测试查询
尝试这些问题测试服务器:
- “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: 自然语言回答
故障排除
服务器未显示
展开查看解决方案
检查清单:
- 验证配置文件语法(有效的 JSON)
- 确认使用绝对路径
- 完全重启 Claude Desktop
- 检查日志文件:
~/Library/Logs/Claude/mcp.log(macOS)
获取详细日志:
# 实时监控日志
tail -f ~/Library/Logs/Claude/mcp*.log工具调用失败
展开查看解决方案
常见原因:
- 坐标不在美国:NWS API 仅支持美国地区
- 速率限制:在请求之间添加延迟
- 网络问题:检查 API 可访问性
调试步骤:
# 添加调试日志
logging.basicConfig(level=logging.DEBUG)Windows 路径问题
展开查看解决方案
在 JSON 配置中,Windows 路径需要使用双反斜杠或正斜杠:
{
"args": [
"C:\\Users\\YourName\\weather", // 正确
"C:/Users/YourName/weather" // 也正确
]
}
