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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/bin/cli.js +59 -0
- package/index.js +402 -0
- 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
|
+
}
|