grande-weather-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/bin/cli.js +59 -0
  4. package/index.js +402 -0
  5. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # Weather MCP Server ๐ŸŒค๏ธ
2
+
3
+ A Model Context Protocol (MCP) server that provides weather data from OpenWeather API. Supports both **STDIO** and **SSE** transport modes.
4
+
5
+ [English](#english) | [ไธญๆ–‡](#ไธญๆ–‡)
6
+
7
+ ---
8
+
9
+ ## English
10
+
11
+ ### Features
12
+
13
+ - ๐ŸŒก๏ธ **Real-time Weather** - Get current weather for any city
14
+ - ๐Ÿ“… **5-Day Forecast** - Get weather forecast up to 5 days
15
+ - ๐ŸŒ **Bilingual Support** - Works with English and Chinese city names
16
+ - ๐Ÿ”Œ **Dual Transport** - Supports both STDIO and SSE modes
17
+ - ๐Ÿ”‘ **Flexible Auth** - API key via URL, Header, or environment variable
18
+
19
+ ### Installation
20
+
21
+ #### Option 1: NPX (Recommended for STDIO)
22
+
23
+ No installation needed! Just configure your MCP client:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "weather": {
29
+ "command": "npx",
30
+ "args": ["-y", "weather-mcp-server"],
31
+ "env": {
32
+ "OPENWEATHER_API_KEY": "your_api_key_here"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ #### Option 2: Global Install
40
+
41
+ ```bash
42
+ npm install -g weather-mcp-server
43
+ ```
44
+
45
+ Then configure:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "weather": {
51
+ "command": "weather-mcp",
52
+ "env": {
53
+ "OPENWEATHER_API_KEY": "your_api_key_here"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ #### Option 3: SSE Mode (Remote Server)
61
+
62
+ If deployed to a server (e.g., Render, Railway):
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "weather": {
68
+ "transport": "sse",
69
+ "url": "https://your-server.com/sse?key=your_api_key"
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### Available Tools
76
+
77
+ | Tool | Description |
78
+ |------|-------------|
79
+ | `get-weather` | Get real-time weather for a city |
80
+ | `get-forecast` | Get 5-day weather forecast |
81
+
82
+ ### Examples
83
+
84
+ Ask your AI assistant:
85
+
86
+ - "What's the weather in Tokyo?"
87
+ - "Get me the 5-day forecast for New York"
88
+ - "ๅŒ—ไบฌไปŠๅคฉๅคฉๆฐ”ๆ€Žไนˆๆ ท๏ผŸ"
89
+ - "ไธŠๆตทๆœชๆฅไธ‰ๅคฉๅคฉๆฐ”้ข„ๆŠฅ"
90
+
91
+ ### Self-Hosting (SSE Mode)
92
+
93
+ ```bash
94
+ # Clone the repo
95
+ git clone https://github.com/your-username/weather-mcp-server
96
+ cd weather-mcp-server
97
+ npm install
98
+
99
+ # Start SSE server
100
+ npm run start:sse
101
+
102
+ # Or with custom port
103
+ PORT=8080 npm run start:sse
104
+ ```
105
+
106
+ ### API Key
107
+
108
+ Get your free API key from [OpenWeather](https://openweathermap.org/api).
109
+
110
+ ---
111
+
112
+ ## ไธญๆ–‡
113
+
114
+ ### ๅŠŸ่ƒฝ็‰น็‚น
115
+
116
+ - ๐ŸŒก๏ธ **ๅฎžๆ—ถๅคฉๆฐ”** - ่Žทๅ–ไปปๆ„ๅŸŽๅธ‚็š„ๅฝ“ๅ‰ๅคฉๆฐ”
117
+ - ๐Ÿ“… **5ๅคฉ้ข„ๆŠฅ** - ่Žทๅ–ๆœ€ๅคš5ๅคฉ็š„ๅคฉๆฐ”้ข„ๆŠฅ
118
+ - ๐ŸŒ **ๅŒ่ฏญๆ”ฏๆŒ** - ๆ”ฏๆŒไธญ่‹ฑๆ–‡ๅŸŽๅธ‚ๅ
119
+ - ๐Ÿ”Œ **ๅŒไผ ่พ“ๆจกๅผ** - ๅŒๆ—ถๆ”ฏๆŒ STDIO ๅ’Œ SSE ๆจกๅผ
120
+ - ๐Ÿ”‘ **็ตๆดป่ฎค่ฏ** - ๆ”ฏๆŒ URLใ€Header ๆˆ–็Žฏๅขƒๅ˜้‡ไผ ้€’ API Key
121
+
122
+ ### ๅฎ‰่ฃ…ไฝฟ็”จ
123
+
124
+ #### ๆ–นๅผ1: NPX ็›ดๆŽฅ่ฟ่กŒ๏ผˆๆŽจ่๏ผ‰
125
+
126
+ ๆ— ้œ€ๅฎ‰่ฃ…๏ผŒ็›ดๆŽฅ้…็ฝฎ MCP ๅฎขๆˆท็ซฏ๏ผš
127
+
128
+ ```json
129
+ {
130
+ "mcpServers": {
131
+ "weather": {
132
+ "command": "npx",
133
+ "args": ["-y", "weather-mcp-server"],
134
+ "env": {
135
+ "OPENWEATHER_API_KEY": "ไฝ ็š„APIๅฏ†้’ฅ"
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ #### ๆ–นๅผ2: ๅ…จๅฑ€ๅฎ‰่ฃ…
143
+
144
+ ```bash
145
+ npm install -g weather-mcp-server
146
+ ```
147
+
148
+ ็„ถๅŽ้…็ฝฎ๏ผš
149
+
150
+ ```json
151
+ {
152
+ "mcpServers": {
153
+ "weather": {
154
+ "command": "weather-mcp",
155
+ "env": {
156
+ "OPENWEATHER_API_KEY": "ไฝ ็š„APIๅฏ†้’ฅ"
157
+ }
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ #### ๆ–นๅผ3: SSE ่ฟœ็จ‹ๆœๅŠก
164
+
165
+ ๅฆ‚ๆžœๅทฒ้ƒจ็ฝฒๅˆฐๆœๅŠกๅ™จ๏ผš
166
+
167
+ ```json
168
+ {
169
+ "mcpServers": {
170
+ "weather": {
171
+ "transport": "sse",
172
+ "url": "https://your-server.com/sse?key=ไฝ ็š„APIๅฏ†้’ฅ"
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### ๅฏ็”จๅทฅๅ…ท
179
+
180
+ | ๅทฅๅ…ท | ๆ่ฟฐ |
181
+ |------|------|
182
+ | `get-weather` | ่Žทๅ–ๅŸŽๅธ‚ๅฎžๆ—ถๅคฉๆฐ” |
183
+ | `get-forecast` | ่Žทๅ–5ๅคฉๅคฉๆฐ”้ข„ๆŠฅ |
184
+
185
+ ### ไฝฟ็”จ็คบไพ‹
186
+
187
+ ๅ‘ AI ๅŠฉๆ‰‹ๆ้—ฎ๏ผš
188
+
189
+ - "ไธœไบฌ็Žฐๅœจๅคฉๆฐ”ๆ€Žไนˆๆ ท๏ผŸ"
190
+ - "ๅธฎๆˆ‘ๆŸฅไธ€ไธ‹็บฝ็บฆๆœชๆฅ5ๅคฉ็š„ๅคฉๆฐ”"
191
+ - "ๅŒ—ไบฌไปŠๅคฉ็ƒญไธ็ƒญ๏ผŸ"
192
+ - "ๆทฑๅœณๆ˜Žๅคฉไผšไธ‹้›จๅ—๏ผŸ"
193
+
194
+ ### ่Žทๅ– API Key
195
+
196
+ ๅ‰ๅพ€ [OpenWeather](https://openweathermap.org/api) ๅ…่ดนๆณจๅ†Œ่Žทๅ–ใ€‚
197
+
198
+ ---
199
+
200
+ ## License
201
+
202
+ MIT ยฉ Grande350
package/bin/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Weather MCP Server CLI
5
+ *
6
+ * Usage:
7
+ * weather-mcp # STDIO mode (default)
8
+ * weather-mcp stdio # STDIO mode
9
+ * weather-mcp sse # SSE HTTP server mode
10
+ */
11
+
12
+ const path = require('path');
13
+ const { spawn } = require('child_process');
14
+
15
+ // ่Žทๅ–ๅ‘ฝไปค่กŒๅ‚ๆ•ฐ
16
+ const args = process.argv.slice(2);
17
+ const mode = args[0] || 'stdio';
18
+
19
+ // ้ชŒ่ฏๆจกๅผ
20
+ if (!['stdio', 'sse'].includes(mode)) {
21
+ console.error(`
22
+ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
23
+ โ•‘ Weather MCP Server - CLI Help โ•‘
24
+ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
25
+ โ•‘ โ•‘
26
+ โ•‘ Usage: โ•‘
27
+ โ•‘ weather-mcp [mode] โ•‘
28
+ โ•‘ โ•‘
29
+ โ•‘ Modes: โ•‘
30
+ โ•‘ stdio STDIO transport (default) โ•‘
31
+ โ•‘ sse SSE HTTP server โ•‘
32
+ โ•‘ โ•‘
33
+ โ•‘ Examples: โ•‘
34
+ โ•‘ weather-mcp โ•‘
35
+ โ•‘ weather-mcp stdio โ•‘
36
+ โ•‘ weather-mcp sse โ•‘
37
+ โ•‘ โ•‘
38
+ โ•‘ Environment Variables: โ•‘
39
+ โ•‘ OPENWEATHER_API_KEY Your OpenWeather API key โ•‘
40
+ โ•‘ PORT HTTP port for SSE (def: 3000) โ•‘
41
+ โ•‘ โ•‘
42
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
43
+ `);
44
+ process.exit(1);
45
+ }
46
+
47
+ // ่Žทๅ– server.js ่ทฏๅพ„
48
+ const serverPath = path.join(__dirname, '..', 'index.js');
49
+
50
+ // ๅฆ‚ๆžœๆ˜ฏ STDIO ๆจกๅผ๏ผŒ็›ดๆŽฅ require ๆ‰ง่กŒ
51
+ if (mode === 'stdio') {
52
+ // STDIO ๆจกๅผ้œ€่ฆไฟๆŒ stdin/stdout ๅนฒๅ‡€
53
+ process.argv = [process.argv[0], serverPath, 'stdio'];
54
+ require(serverPath);
55
+ } else {
56
+ // SSE ๆจกๅผๅฏไปฅ spawn ๅญ่ฟ›็จ‹ๆˆ–็›ดๆŽฅ่ฟ่กŒ
57
+ process.argv = [process.argv[0], serverPath, 'sse'];
58
+ require(serverPath);
59
+ }
package/index.js ADDED
@@ -0,0 +1,402 @@
1
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
2
+ const {
3
+ StdioServerTransport,
4
+ } = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const {
6
+ SSEServerTransport,
7
+ } = require("@modelcontextprotocol/sdk/server/sse.js");
8
+ const { z } = require("zod");
9
+ const axios = require("axios");
10
+ const http = require("http");
11
+ const url = require("url");
12
+
13
+ // ============ ้…็ฝฎ ============
14
+ const DEFAULT_API_KEY =
15
+ process.env.OPENWEATHER_API_KEY || "";
16
+ const BASE_URL = "https://api.openweathermap.org/data/2.5";
17
+ const GEO_URL = "https://api.openweathermap.org/geo/1.0/direct";
18
+
19
+ // Session ๅญ˜ๅ‚จ
20
+ const sessions = new Map();
21
+
22
+ // ============ ๅˆ›ๅปบ MCP Server ============
23
+ function createServer(apiKeyGetter) {
24
+ const server = new McpServer({
25
+ name: "weather-mcp-server",
26
+ version: "2.3.0",
27
+ });
28
+
29
+ // ๅทฅๅ…ท: ่Žทๅ–ๅฝ“ๅ‰ๅคฉๆฐ”
30
+ server.tool(
31
+ "get-weather",
32
+ "Get real-time weather for a city / ่Žทๅ–ๅŸŽๅธ‚ๅฎžๆ—ถๅคฉๆฐ”",
33
+ { city: z.string().describe("City name, e.g.: Beijing, London / ๅŸŽๅธ‚ๅ") },
34
+ async ({ city }) => {
35
+ const apiKey = apiKeyGetter();
36
+ console.error(`[Tool] get-weather | City: ${city}`);
37
+
38
+ try {
39
+ const geoRes = await axios.get(GEO_URL, {
40
+ params: { q: city, limit: 1, appid: apiKey },
41
+ timeout: 10000,
42
+ });
43
+
44
+ if (!geoRes.data?.length) {
45
+ return {
46
+ isError: true,
47
+ content: [{ type: "text", text: `City not found: "${city}"` }],
48
+ };
49
+ }
50
+
51
+ const { lat, lon, name, local_names, country } = geoRes.data[0];
52
+
53
+ const weatherRes = await axios.get(`${BASE_URL}/weather`, {
54
+ params: { lat, lon, appid: apiKey, units: "metric", lang: "zh_cn" },
55
+ timeout: 10000,
56
+ });
57
+
58
+ const d = weatherRes.data;
59
+ const displayName = local_names?.zh || local_names?.en || name;
60
+
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: JSON.stringify(
66
+ {
67
+ location: { name: displayName, country, lat, lon },
68
+ weather: {
69
+ description: d.weather[0].description,
70
+ temperature: `${d.main.temp}ยฐC`,
71
+ feels_like: `${d.main.feels_like}ยฐC`,
72
+ humidity: `${d.main.humidity}%`,
73
+ wind_speed: `${d.wind.speed} m/s`,
74
+ },
75
+ summary: `${displayName}: ${d.weather[0].description}, ${d.main.temp}ยฐC`,
76
+ },
77
+ null,
78
+ 2
79
+ ),
80
+ },
81
+ ],
82
+ };
83
+ } catch (error) {
84
+ console.error(`[Error] get-weather:`, error.message);
85
+ return {
86
+ isError: true,
87
+ content: [{ type: "text", text: `Query failed: ${error.message}` }],
88
+ };
89
+ }
90
+ }
91
+ );
92
+
93
+ // ๅทฅๅ…ท: ่Žทๅ–ๅคฉๆฐ”้ข„ๆŠฅ
94
+ server.tool(
95
+ "get-forecast",
96
+ "Get 5-day weather forecast / ่Žทๅ–5ๅคฉๅคฉๆฐ”้ข„ๆŠฅ",
97
+ {
98
+ city: z.string().describe("City name / ๅŸŽๅธ‚ๅ"),
99
+ days: z.number().min(1).max(5).default(3).optional(),
100
+ },
101
+ async ({ city, days = 3 }) => {
102
+ const apiKey = apiKeyGetter();
103
+ console.error(`[Tool] get-forecast | City: ${city} | Days: ${days}`);
104
+
105
+ try {
106
+ const geoRes = await axios.get(GEO_URL, {
107
+ params: { q: city, limit: 1, appid: apiKey },
108
+ timeout: 10000,
109
+ });
110
+
111
+ if (!geoRes.data?.length) {
112
+ return {
113
+ isError: true,
114
+ content: [{ type: "text", text: `City not found: "${city}"` }],
115
+ };
116
+ }
117
+
118
+ const { lat, lon, name, local_names, country } = geoRes.data[0];
119
+
120
+ const forecastRes = await axios.get(`${BASE_URL}/forecast`, {
121
+ params: {
122
+ lat,
123
+ lon,
124
+ appid: apiKey,
125
+ units: "metric",
126
+ lang: "zh_cn",
127
+ cnt: days * 8,
128
+ },
129
+ timeout: 10000,
130
+ });
131
+
132
+ const displayName = local_names?.zh || name;
133
+ const dailyForecasts = [];
134
+ const seenDates = new Set();
135
+
136
+ for (const item of forecastRes.data.list) {
137
+ const date = item.dt_txt.split(" ")[0];
138
+ if (!seenDates.has(date) && seenDates.size < days) {
139
+ seenDates.add(date);
140
+ dailyForecasts.push({
141
+ date,
142
+ weather: item.weather[0].description,
143
+ temp_max: item.main.temp_max,
144
+ temp_min: item.main.temp_min,
145
+ humidity: item.main.humidity,
146
+ });
147
+ }
148
+ }
149
+
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: JSON.stringify(
155
+ {
156
+ location: { name: displayName, country },
157
+ forecast: dailyForecasts,
158
+ },
159
+ null,
160
+ 2
161
+ ),
162
+ },
163
+ ],
164
+ };
165
+ } catch (error) {
166
+ console.error(`[Error] get-forecast:`, error.message);
167
+ return {
168
+ isError: true,
169
+ content: [
170
+ { type: "text", text: `Forecast failed: ${error.message}` },
171
+ ],
172
+ };
173
+ }
174
+ }
175
+ );
176
+
177
+ return server;
178
+ }
179
+
180
+ // ============ ่พ…ๅŠฉๅ‡ฝๆ•ฐ ============
181
+ function extractApiKey(req) {
182
+ const parsedUrl = url.parse(req.url, true);
183
+ return (
184
+ req.headers["x-api-key"] ||
185
+ req.headers["authorization"]?.replace("Bearer ", "") ||
186
+ parsedUrl.query.key ||
187
+ null
188
+ );
189
+ }
190
+
191
+ function setCorsHeaders(res) {
192
+ res.setHeader("Access-Control-Allow-Origin", "*");
193
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
194
+ res.setHeader(
195
+ "Access-Control-Allow-Headers",
196
+ "Content-Type, Authorization, X-API-Key, Mcp-Session-Id"
197
+ );
198
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
199
+ }
200
+
201
+ function sendJson(res, statusCode, data) {
202
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
203
+ res.end(JSON.stringify(data));
204
+ }
205
+
206
+ // ============ ไธปๅฏๅŠจ้€ป่พ‘ ============
207
+ async function main() {
208
+ const mode = process.argv[2] || "stdio";
209
+
210
+ if (mode === "sse") {
211
+ // ไฝฟ็”จๅŽŸ็”Ÿ http ๆจกๅ—๏ผŒ้ฟๅ… Express ไธญ้—ดไปถ้—ฎ้ข˜
212
+ const server = http.createServer(async (req, res) => {
213
+ const parsedUrl = url.parse(req.url, true);
214
+ const pathname = parsedUrl.pathname;
215
+
216
+ console.error(`[${new Date().toISOString()}] ${req.method} ${pathname}`);
217
+
218
+ // ่ฎพ็ฝฎ CORS
219
+ setCorsHeaders(res);
220
+
221
+ // ๅค„็† OPTIONS ้ข„ๆฃ€่ฏทๆฑ‚
222
+ if (req.method === "OPTIONS") {
223
+ res.writeHead(204);
224
+ res.end();
225
+ return;
226
+ }
227
+
228
+ // -------- ๅฅๅบทๆฃ€ๆŸฅ --------
229
+ if (pathname === "/health" && req.method === "GET") {
230
+ return sendJson(res, 200, {
231
+ status: "ok",
232
+ version: "2.3.0",
233
+ transport: "SSE (native http)",
234
+ activeSessions: sessions.size,
235
+ uptime: process.uptime(),
236
+ });
237
+ }
238
+
239
+ // -------- ่ฐƒ่ฏ•็ซฏ็‚น --------
240
+ if (pathname === "/debug" && req.method === "GET") {
241
+ const sessionsInfo = [];
242
+ for (const [id, session] of sessions) {
243
+ sessionsInfo.push({
244
+ id,
245
+ apiKeyPrefix: session.apiKey?.substring(0, 6) + "***",
246
+ createdAt: session.createdAt,
247
+ });
248
+ }
249
+ return sendJson(res, 200, {
250
+ activeSessions: sessions.size,
251
+ sessions: sessionsInfo,
252
+ });
253
+ }
254
+
255
+ // -------- ๅทฅๅ…ทๅˆ—่กจ --------
256
+ if (pathname === "/tools" && req.method === "GET") {
257
+ return sendJson(res, 200, {
258
+ tools: ["get-weather", "get-forecast"],
259
+ usage: "Connect via SSE at /sse",
260
+ });
261
+ }
262
+
263
+ // -------- API ๆต‹่ฏ• --------
264
+ if (pathname === "/test-api" && req.method === "GET") {
265
+ const apiKey = extractApiKey(req) || DEFAULT_API_KEY;
266
+ try {
267
+ const geoRes = await axios.get(GEO_URL, {
268
+ params: { q: "London", limit: 1, appid: apiKey },
269
+ timeout: 10000,
270
+ });
271
+ return sendJson(res, 200, {
272
+ status: "ok",
273
+ keyUsed: apiKey.substring(0, 6) + "***",
274
+ found: geoRes.data?.length > 0,
275
+ });
276
+ } catch (error) {
277
+ return sendJson(res, 500, {
278
+ status: "error",
279
+ error: error.response?.data || error.message,
280
+ });
281
+ }
282
+ }
283
+
284
+ // -------- SSE ่ฟžๆŽฅ็ซฏ็‚น --------
285
+ if (pathname === "/sse" && req.method === "GET") {
286
+ const userKey = extractApiKey(req);
287
+ const effectiveKey = userKey || DEFAULT_API_KEY;
288
+
289
+ console.error(
290
+ `[SSE] New connection | Key: ${effectiveKey.substring(0, 6)}***`
291
+ );
292
+
293
+ // ๅˆ›ๅปบ transport
294
+ const transport = new SSEServerTransport("/messages", res);
295
+ const sessionId = transport.sessionId;
296
+
297
+ // ๅˆ›ๅปบ server
298
+ const mcpServer = createServer(() => {
299
+ return sessions.get(sessionId)?.apiKey || DEFAULT_API_KEY;
300
+ });
301
+
302
+ // ๅญ˜ๅ‚จ session
303
+ sessions.set(sessionId, {
304
+ transport,
305
+ server: mcpServer,
306
+ apiKey: effectiveKey,
307
+ createdAt: new Date(),
308
+ });
309
+
310
+ console.error(`[SSE] Session created: ${sessionId}`);
311
+
312
+ // ็›‘ๅฌ่ฟžๆŽฅๅ…ณ้—ญ
313
+ res.on("close", () => {
314
+ sessions.delete(sessionId);
315
+ console.error(`[SSE] Session closed: ${sessionId}`);
316
+ });
317
+
318
+ // ่ฟžๆŽฅ
319
+ await mcpServer.connect(transport);
320
+ return;
321
+ }
322
+
323
+ // -------- ๆถˆๆฏๅค„็†็ซฏ็‚น --------
324
+ if (pathname === "/messages" && req.method === "POST") {
325
+ const sessionId = parsedUrl.query.sessionId;
326
+ console.error(`[Messages] POST | sessionId: ${sessionId}`);
327
+
328
+ if (!sessionId) {
329
+ return sendJson(res, 400, { error: "Missing sessionId" });
330
+ }
331
+
332
+ const session = sessions.get(sessionId);
333
+ if (!session) {
334
+ console.error(
335
+ `[Messages] Session not found. Active: ${Array.from(
336
+ sessions.keys()
337
+ ).join(", ")}`
338
+ );
339
+ return sendJson(res, 404, { error: "Session not found" });
340
+ }
341
+
342
+ try {
343
+ // ๅ…ณ้”ฎ๏ผš็›ดๆŽฅไผ ้€’ๅŽŸๅง‹ req, res๏ผŒไธๅšไปปไฝ•้ข„ๅค„็†
344
+ await session.transport.handlePostMessage(req, res);
345
+ } catch (error) {
346
+ console.error(`[Messages] Error: ${error.message}`);
347
+ if (!res.writableEnded) {
348
+ sendJson(res, 500, { error: error.message });
349
+ }
350
+ }
351
+ return;
352
+ }
353
+
354
+ // -------- 404 --------
355
+ sendJson(res, 404, { error: "Not found" });
356
+ });
357
+
358
+ // Session ๆธ…็†ๅฎšๆ—ถๅ™จ
359
+ setInterval(() => {
360
+ const now = Date.now();
361
+ const timeout = 30 * 60 * 1000;
362
+ for (const [id, session] of sessions) {
363
+ if (now - session.createdAt.getTime() > timeout) {
364
+ sessions.delete(id);
365
+ console.error(`[Cleanup] Expired session: ${id}`);
366
+ }
367
+ }
368
+ }, 5 * 60 * 1000);
369
+
370
+ const PORT = process.env.PORT || 3000;
371
+ server.listen(PORT, () => {
372
+ console.error(`
373
+ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
374
+ โ•‘ Weather MCP Server v2.3.0 โ•‘
375
+ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
376
+ โ•‘ Transport: SSE (native http - no Express) โ•‘
377
+ โ•‘ Port: ${PORT} โ•‘
378
+ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
379
+ โ•‘ Endpoints: โ•‘
380
+ โ•‘ GET /health - Health check โ•‘
381
+ โ•‘ GET /debug - Debug info โ•‘
382
+ โ•‘ GET /test-api - Test API key โ•‘
383
+ โ•‘ GET /tools - List tools โ•‘
384
+ โ•‘ GET /sse - SSE connection โ•‘
385
+ โ•‘ POST /messages - Message handler โ•‘
386
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
387
+ `);
388
+ });
389
+ } else {
390
+ // STDIO ๆจกๅผ
391
+ console.error("[STDIO] Starting...");
392
+ const server = createServer(() => DEFAULT_API_KEY);
393
+ const transport = new StdioServerTransport();
394
+ await server.connect(transport);
395
+ console.error("[STDIO] Connected");
396
+ }
397
+ }
398
+
399
+ main().catch((error) => {
400
+ console.error("[Fatal]", error);
401
+ process.exit(1);
402
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "grande-weather-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server for OpenWeather API - supports both SSE and STDIO modes / ๅคฉๆฐ” MCP ๆœๅŠกๅ™จ",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "start:sse": "node index.js sse",
9
+ "start:stdio": "node index.js stdio",
10
+ "test": "echo \"No tests yet\" && exit 0",
11
+ "prepublishOnly": "echo 'Ready to publish!'"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "ai",
17
+ "weather",
18
+ "openweather",
19
+ "context",
20
+ "server"
21
+ ],
22
+ "author": "511456916@qq.com",
23
+ "license": "ISC",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/123liu950/weather-mcp"
27
+ },
28
+ "homepage": "https://github.com/123liu950/weather-mcp/blob/main/README.md",
29
+ "bugs": {
30
+ "url": "https://github.com/123liu950/weather-mcp/issues"
31
+ },
32
+ "bin": {
33
+ "weather-mcp": "./bin/cli.js",
34
+ "weather-mcp-server": "./bin/cli.js"
35
+ },
36
+ "files": [
37
+ "index.js",
38
+ "bin/",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.25.3",
44
+ "axios": "^1.13.3",
45
+ "eventsource": "^4.1.0",
46
+ "express": "^5.2.1",
47
+ "zod": "^3.25.76"
48
+ },
49
+ "devDependencies": {
50
+ "nodemon": "^3.0.1"
51
+ }
52
+ }