Skip to content

Instantly share code, notes, and snippets.

@flying3615
Created March 5, 2025 22:39
Show Gist options
  • Select an option

  • Save flying3615/2461bf8e7de1a3670391ca3390db0eda to your computer and use it in GitHub Desktop.

Select an option

Save flying3615/2461bf8e7de1a3670391ca3390db0eda to your computer and use it in GitHub Desktop.
MCP tutorial
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import axios from "axios";
import dotenv from "dotenv";
import {ForecastDay, isValidForecastArgs, OpenWeatherResponse, WeatherData} from "./types/weather.js";
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema, ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/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 {
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<OpenWeatherResponse>(
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;
}
}
);
}
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;
}
}
);
}
async run(): Promise<void> {
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);
// 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")
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment