Created
March 5, 2025 22:39
-
-
Save flying3615/2461bf8e7de1a3670391ca3390db0eda to your computer and use it in GitHub Desktop.
MCP tutorial
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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