open-meteo-mcp-server 1.3.1 → 1.5.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/README.md +21 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +236 -146
- package/dist/index.js.map +1 -1
- package/dist/security.d.ts +40 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +115 -0
- package/dist/security.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +148 -25
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +76 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +84 -5
- package/dist/types.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -167,6 +167,19 @@ TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
|
|
|
167
167
|
|
|
168
168
|
This starts an Express server on the specified port (default: 3000) with the MCP endpoint at `/mcp`. The HTTP transport supports session management with unique session IDs per client.
|
|
169
169
|
|
|
170
|
+
For production deployments, enable authentication and rate limiting:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
API_KEY=your-secret-key RATE_LIMIT_RPM=60 TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Clients must then include the key in every request:
|
|
177
|
+
```
|
|
178
|
+
Authorization: Bearer your-secret-key
|
|
179
|
+
# or
|
|
180
|
+
X-API-Key: your-secret-key
|
|
181
|
+
```
|
|
182
|
+
|
|
170
183
|
#### Using npm scripts
|
|
171
184
|
|
|
172
185
|
```bash
|
|
@@ -284,6 +297,12 @@ All environment variables are optional and have sensible defaults:
|
|
|
284
297
|
- `TRANSPORT` - Transport mode: `http` for Streamable HTTP, omit for stdio (default: stdio)
|
|
285
298
|
- `PORT` - HTTP server port when using HTTP transport (default: 3000)
|
|
286
299
|
|
|
300
|
+
#### HTTP Transport Security (optional)
|
|
301
|
+
|
|
302
|
+
- `API_KEY` - When set, all requests to `/mcp` must include this key via `Authorization: Bearer <key>` or `X-API-Key: <key>`. Leave unset for open access (local/dev mode).
|
|
303
|
+
- `RATE_LIMIT_RPM` - Maximum requests per minute per IP (default: `60`). HTTP transport only.
|
|
304
|
+
- `TRUSTED_PROXIES` - Comma-separated list of trusted proxy IPs or CIDR ranges (e.g. `10.0.0.0/8,172.16.0.0/12`). When set, `X-Forwarded-For` is honoured only for requests originating from these addresses. Leave unset to always use the direct connection IP.
|
|
305
|
+
|
|
287
306
|
## Usage Examples
|
|
288
307
|
|
|
289
308
|
### Geocoding and Location Search
|
|
@@ -443,7 +462,8 @@ src/
|
|
|
443
462
|
├── index.ts # MCP server entry point
|
|
444
463
|
├── client.ts # HTTP client for Open-Meteo API
|
|
445
464
|
├── tools.ts # MCP tool definitions
|
|
446
|
-
|
|
465
|
+
├── types.ts # Zod validation schemas
|
|
466
|
+
└── security.ts # Auth middleware, rate limiter, IP extraction
|
|
447
467
|
```
|
|
448
468
|
|
|
449
469
|
## API Coverage
|
|
@@ -485,7 +505,6 @@ The server provides comprehensive error handling with detailed error messages fo
|
|
|
485
505
|
## Performance
|
|
486
506
|
|
|
487
507
|
- Efficient HTTP client with connection pooling
|
|
488
|
-
- Request caching for repeated queries
|
|
489
508
|
- Optimized data serialization
|
|
490
509
|
- Minimal memory footprint
|
|
491
510
|
|
package/dist/client.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare class OpenMeteoClient {
|
|
|
9
9
|
private geocodingClient;
|
|
10
10
|
private floodClient;
|
|
11
11
|
private climateClient;
|
|
12
|
-
constructor(baseURL?: string);
|
|
12
|
+
constructor(baseURL?: string, version?: string);
|
|
13
13
|
private buildParams;
|
|
14
14
|
getForecast(params: ForecastParams): Promise<WeatherResponse>;
|
|
15
15
|
getArchive(params: ArchiveParams): Promise<WeatherResponse>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAgB;IACvC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,aAAa,CAAgB;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAgB;IACvC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,aAAa,CAAgB;gBAGnC,OAAO,GAAE,MAAuE,EAChF,OAAO,SAAY;IAqCrB,OAAO,CAAC,WAAW;IAgBb,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7D,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC;IAO3D,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO5D,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAOxD,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAOhE,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC;IAOvD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAOxD,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO1D,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAOxD,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAOjE,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IAOzD,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7D,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAOjE,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC;IAOvD,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7D,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC;IAO3D,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAMxE"}
|
package/dist/client.js
CHANGED
|
@@ -9,12 +9,12 @@ export class OpenMeteoClient {
|
|
|
9
9
|
geocodingClient;
|
|
10
10
|
floodClient;
|
|
11
11
|
climateClient;
|
|
12
|
-
constructor(baseURL = process.env.OPEN_METEO_API_URL || 'https://api.open-meteo.com') {
|
|
12
|
+
constructor(baseURL = process.env.OPEN_METEO_API_URL || 'https://api.open-meteo.com', version = 'unknown') {
|
|
13
13
|
const config = {
|
|
14
14
|
timeout: 30000,
|
|
15
15
|
headers: {
|
|
16
16
|
Accept: 'application/json',
|
|
17
|
-
'User-Agent':
|
|
17
|
+
'User-Agent': `Open-Meteo-MCP-Server/${version}`,
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
// Utilisation des variables d'environnement avec des valeurs par défaut
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAA6B,MAAM,OAAO,CAAC;AAkBlD,MAAM,OAAO,eAAe;IAClB,MAAM,CAAgB;IACtB,gBAAgB,CAAgB;IAChC,YAAY,CAAgB;IAC5B,aAAa,CAAgB;IAC7B,cAAc,CAAgB;IAC9B,cAAc,CAAgB;IAC9B,eAAe,CAAgB;IAC/B,WAAW,CAAgB;IAC3B,aAAa,CAAgB;IAErC,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
|
+
export declare class OpenMeteoMCPServer {
|
|
4
|
+
private client;
|
|
5
|
+
private sessionServers;
|
|
6
|
+
private static readonly SESSION_TTL_MS;
|
|
7
|
+
private static readonly MAX_SESSIONS;
|
|
8
|
+
private static readonly CLEANUP_INTERVAL_MS;
|
|
9
|
+
constructor();
|
|
10
|
+
private createServer;
|
|
11
|
+
private getSession;
|
|
12
|
+
private startCleanupTimer;
|
|
13
|
+
private buildExpressApp;
|
|
14
|
+
private startHttpTransport;
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
3
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AAgDvB,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,cAAc,CAGR;IAEd,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAkB;IACxD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAO;IAC3C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;;IAO5D,OAAO,CAAC,YAAY;IAsIpB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,eAAe;IAkQvB,OAAO,CAAC,kBAAkB;IAapB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAiB3B"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
9
9
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
10
10
|
import express from 'express';
|
|
11
11
|
import { OpenMeteoClient } from './client.js';
|
|
12
|
+
import { createAuthMiddleware, createRateLimiter, generateSessionId, getClientIp, sanitizeErrorMessage, } from './security.js';
|
|
12
13
|
import { ALL_TOOLS } from './tools.js';
|
|
13
14
|
import { AirQualityParamsSchema, ArchiveParamsSchema, ClimateParamsSchema, EcmwfParamsSchema, ElevationParamsSchema, EnsembleParamsSchema, FloodParamsSchema, ForecastParamsSchema, GeocodingParamsSchema, MarineParamsSchema, SeasonalParamsSchema, } from './types.js';
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -18,17 +19,7 @@ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-
|
|
|
18
19
|
function log(level, event, data = {}) {
|
|
19
20
|
process.stderr.write(`${JSON.stringify({ timestamp: new Date().toISOString(), level, event, ...data })}\n`);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
function getClientIp(req) {
|
|
23
|
-
const xff = req.headers['x-forwarded-for'];
|
|
24
|
-
if (xff) {
|
|
25
|
-
const first = Array.isArray(xff) ? xff[0] : xff;
|
|
26
|
-
const raw = first ?? '';
|
|
27
|
-
return (raw.split(',')[0] ?? raw).trim();
|
|
28
|
-
}
|
|
29
|
-
return req.ip ?? req.socket?.remoteAddress ?? 'unknown';
|
|
30
|
-
}
|
|
31
|
-
class OpenMeteoMCPServer {
|
|
22
|
+
export class OpenMeteoMCPServer {
|
|
32
23
|
client;
|
|
33
24
|
sessionServers = new Map();
|
|
34
25
|
static SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour idle timeout
|
|
@@ -36,7 +27,7 @@ class OpenMeteoMCPServer {
|
|
|
36
27
|
static CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // run cleanup every 5 minutes
|
|
37
28
|
constructor() {
|
|
38
29
|
const baseURL = process.env.OPEN_METEO_API_URL || 'https://api.open-meteo.com';
|
|
39
|
-
this.client = new OpenMeteoClient(baseURL);
|
|
30
|
+
this.client = new OpenMeteoClient(baseURL, pkg.version);
|
|
40
31
|
}
|
|
41
32
|
createServer() {
|
|
42
33
|
const server = new Server({
|
|
@@ -183,168 +174,267 @@ class OpenMeteoMCPServer {
|
|
|
183
174
|
// Don't keep the process alive just for cleanup
|
|
184
175
|
timer.unref();
|
|
185
176
|
}
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
tokens.push(value);
|
|
208
|
-
normalized.add(value);
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
ensureHeader('application/json');
|
|
212
|
-
ensureHeader('text/event-stream');
|
|
213
|
-
req.headers.accept = tokens.join(', ');
|
|
214
|
-
next();
|
|
215
|
-
});
|
|
216
|
-
// Generate unique session IDs for each client
|
|
217
|
-
const sessionIdGenerator = () => {
|
|
218
|
-
const timestamp = Date.now().toString(36);
|
|
219
|
-
const random1 = Math.random().toString(36).substring(2, 15);
|
|
220
|
-
const random2 = Math.random().toString(36).substring(2, 15);
|
|
221
|
-
const random3 = Math.random().toString(36).substring(2, 15);
|
|
222
|
-
return `${timestamp}-${random1}-${random2}-${random3}`;
|
|
177
|
+
buildExpressApp() {
|
|
178
|
+
const app = express();
|
|
179
|
+
app.use(express.json());
|
|
180
|
+
// Health check endpoint
|
|
181
|
+
app.get('/health', (_req, res) => {
|
|
182
|
+
res.status(200).json({ status: 'ok' });
|
|
183
|
+
});
|
|
184
|
+
app.use((req, _res, next) => {
|
|
185
|
+
const acceptHeader = req.headers.accept;
|
|
186
|
+
const tokens = acceptHeader
|
|
187
|
+
? acceptHeader
|
|
188
|
+
.split(',')
|
|
189
|
+
.map((value) => value.trim())
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
: [];
|
|
192
|
+
const normalized = new Set(tokens.map((value) => value.toLowerCase()));
|
|
193
|
+
const ensureHeader = (value) => {
|
|
194
|
+
if (!normalized.has(value)) {
|
|
195
|
+
tokens.push(value);
|
|
196
|
+
normalized.add(value);
|
|
197
|
+
}
|
|
223
198
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
199
|
+
ensureHeader('application/json');
|
|
200
|
+
ensureHeader('text/event-stream');
|
|
201
|
+
req.headers.accept = tokens.join(', ');
|
|
202
|
+
next();
|
|
203
|
+
});
|
|
204
|
+
// GET /mcp — SSE streaming for server-to-client notifications
|
|
205
|
+
app.get('/mcp', async (req, res) => {
|
|
206
|
+
const remoteIp = getClientIp(req);
|
|
207
|
+
const userAgent = req.headers['user-agent'] ?? 'unknown';
|
|
208
|
+
try {
|
|
209
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
210
|
+
log('info', 'http_request', {
|
|
211
|
+
method: 'GET',
|
|
212
|
+
session_id: sessionId ? sessionId.substring(0, 8) : null,
|
|
213
|
+
remote_ip: remoteIp,
|
|
214
|
+
user_agent: userAgent,
|
|
215
|
+
});
|
|
216
|
+
if (!sessionId) {
|
|
217
|
+
res.status(400).json({
|
|
218
|
+
jsonrpc: '2.0',
|
|
219
|
+
error: { code: -32600, message: 'Invalid Request: Session ID required' },
|
|
220
|
+
id: null,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const session = this.getSession(sessionId);
|
|
225
|
+
if (!session) {
|
|
226
|
+
log('warn', 'session_not_found', {
|
|
227
|
+
session_id: sessionId.substring(0, 8),
|
|
228
|
+
remote_ip: remoteIp,
|
|
229
|
+
});
|
|
230
|
+
res.status(404).json({
|
|
231
|
+
jsonrpc: '2.0',
|
|
232
|
+
error: { code: -32600, message: 'Session not found' },
|
|
233
|
+
id: null,
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
await session.transport.handleRequest(req, res);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
241
|
+
log('error', 'request_error', { error: errorMessage, remote_ip: remoteIp });
|
|
242
|
+
if (!res.headersSent) {
|
|
243
|
+
res.status(500).json({
|
|
244
|
+
jsonrpc: '2.0',
|
|
245
|
+
error: { code: -32603, message: sanitizeErrorMessage(err) },
|
|
246
|
+
id: null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// DELETE /mcp — session termination
|
|
252
|
+
app.delete('/mcp', async (req, res) => {
|
|
253
|
+
const remoteIp = getClientIp(req);
|
|
254
|
+
const userAgent = req.headers['user-agent'] ?? 'unknown';
|
|
255
|
+
try {
|
|
256
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
257
|
+
log('info', 'http_request', {
|
|
258
|
+
method: 'DELETE',
|
|
259
|
+
session_id: sessionId ? sessionId.substring(0, 8) : null,
|
|
260
|
+
remote_ip: remoteIp,
|
|
261
|
+
user_agent: userAgent,
|
|
262
|
+
});
|
|
263
|
+
if (!sessionId) {
|
|
264
|
+
res.status(400).json({
|
|
265
|
+
jsonrpc: '2.0',
|
|
266
|
+
error: { code: -32600, message: 'Invalid Request: Session ID required' },
|
|
267
|
+
id: null,
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Use direct map access — lastActivity is irrelevant for a session about to be destroyed
|
|
272
|
+
const session = this.sessionServers.get(sessionId);
|
|
273
|
+
if (!session) {
|
|
274
|
+
log('warn', 'session_not_found', {
|
|
275
|
+
session_id: sessionId.substring(0, 8),
|
|
234
276
|
remote_ip: remoteIp,
|
|
235
|
-
user_agent: userAgent,
|
|
236
277
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
278
|
+
res.status(404).json({
|
|
279
|
+
jsonrpc: '2.0',
|
|
280
|
+
error: { code: -32600, message: 'Session not found' },
|
|
281
|
+
id: null,
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await session.transport.close();
|
|
286
|
+
res.status(200).json({ message: 'Session terminated' });
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
290
|
+
log('error', 'request_error', { error: errorMessage, remote_ip: remoteIp });
|
|
291
|
+
if (!res.headersSent) {
|
|
292
|
+
res.status(500).json({
|
|
293
|
+
jsonrpc: '2.0',
|
|
294
|
+
error: { code: -32603, message: sanitizeErrorMessage(err) },
|
|
295
|
+
id: null,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
app.use(createRateLimiter());
|
|
301
|
+
app.use(createAuthMiddleware());
|
|
302
|
+
app.post('/mcp', async (req, res) => {
|
|
303
|
+
const remoteIp = getClientIp(req);
|
|
304
|
+
const userAgent = req.headers['user-agent'] ?? 'unknown';
|
|
305
|
+
try {
|
|
306
|
+
const method = req.body?.method || 'unknown';
|
|
307
|
+
// Extract session ID from headers (Express normalises headers to lowercase)
|
|
308
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
309
|
+
log('info', 'http_request', {
|
|
310
|
+
method,
|
|
311
|
+
session_id: sessionId ? sessionId.substring(0, 8) : null,
|
|
312
|
+
remote_ip: remoteIp,
|
|
313
|
+
user_agent: userAgent,
|
|
314
|
+
});
|
|
315
|
+
// If no session ID and it's an initialize request, create a new session
|
|
316
|
+
if (!sessionId && req.body?.method === 'initialize') {
|
|
317
|
+
if (this.sessionServers.size >= OpenMeteoMCPServer.MAX_SESSIONS) {
|
|
318
|
+
log('warn', 'session_limit_reached', {
|
|
319
|
+
current: this.sessionServers.size,
|
|
320
|
+
max: OpenMeteoMCPServer.MAX_SESSIONS,
|
|
321
|
+
remote_ip: remoteIp,
|
|
322
|
+
});
|
|
323
|
+
res.status(503).json({
|
|
324
|
+
jsonrpc: '2.0',
|
|
325
|
+
error: { code: -32603, message: 'Server at session capacity, try again later' },
|
|
326
|
+
id: req.body?.id || null,
|
|
260
327
|
});
|
|
261
|
-
server.oninitialized = () => {
|
|
262
|
-
log('info', 'session_initialized', { session_id: newSessionId.substring(0, 8) });
|
|
263
|
-
};
|
|
264
|
-
server.onclose = () => {
|
|
265
|
-
this.sessionServers.delete(newSessionId);
|
|
266
|
-
log('info', 'session_closed', { session_id: newSessionId.substring(0, 8) });
|
|
267
|
-
};
|
|
268
|
-
await server.connect(transport);
|
|
269
|
-
this.sessionServers.set(newSessionId, { server, transport, lastActivity: Date.now() });
|
|
270
|
-
// Set session ID in response header before handling request
|
|
271
|
-
res.setHeader('mcp-session-id', newSessionId);
|
|
272
|
-
// Handle the initialize request
|
|
273
|
-
await transport.handleRequest(req, res, req.body);
|
|
274
328
|
return;
|
|
275
329
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
330
|
+
// Generate a new session ID
|
|
331
|
+
const newSessionId = generateSessionId();
|
|
332
|
+
log('info', 'session_created', { session_id: newSessionId.substring(0, 8) });
|
|
333
|
+
// Create server and transport for this new session
|
|
334
|
+
const server = this.createServer();
|
|
335
|
+
const transport = new StreamableHTTPServerTransport({
|
|
336
|
+
enableJsonResponse: true,
|
|
337
|
+
sessionIdGenerator: () => newSessionId,
|
|
338
|
+
});
|
|
339
|
+
server.oninitialized = () => {
|
|
340
|
+
log('info', 'session_initialized', { session_id: newSessionId.substring(0, 8) });
|
|
341
|
+
};
|
|
342
|
+
server.onclose = () => {
|
|
343
|
+
this.sessionServers.delete(newSessionId);
|
|
344
|
+
log('info', 'session_closed', { session_id: newSessionId.substring(0, 8) });
|
|
345
|
+
};
|
|
346
|
+
await server.connect(transport);
|
|
347
|
+
this.sessionServers.set(newSessionId, { server, transport, lastActivity: Date.now() });
|
|
348
|
+
// Set session ID in response header before handling request
|
|
349
|
+
res.setHeader('mcp-session-id', newSessionId);
|
|
350
|
+
// Handle the initialize request
|
|
351
|
+
await transport.handleRequest(req, res, req.body);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (sessionId) {
|
|
355
|
+
const session = this.getSession(sessionId);
|
|
356
|
+
if (!session) {
|
|
357
|
+
log('warn', 'session_not_found', {
|
|
358
|
+
session_id: sessionId.substring(0, 8),
|
|
298
359
|
remote_ip: remoteIp,
|
|
299
360
|
});
|
|
300
|
-
res.status(
|
|
361
|
+
res.status(404).json({
|
|
301
362
|
jsonrpc: '2.0',
|
|
302
|
-
error: {
|
|
303
|
-
code: -32600,
|
|
304
|
-
message: 'Invalid Request: Session ID required for non-initialize requests',
|
|
305
|
-
},
|
|
363
|
+
error: { code: -32600, message: 'Session not found' },
|
|
306
364
|
id: req.body?.id || null,
|
|
307
365
|
});
|
|
366
|
+
return;
|
|
308
367
|
}
|
|
368
|
+
const { transport } = session;
|
|
369
|
+
await transport.handleRequest(req, res, req.body);
|
|
309
370
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
log('
|
|
313
|
-
|
|
371
|
+
else {
|
|
372
|
+
// No session ID and not an initialize request - error
|
|
373
|
+
log('warn', 'invalid_request', {
|
|
374
|
+
reason: 'missing_session_id',
|
|
375
|
+
method,
|
|
376
|
+
remote_ip: remoteIp,
|
|
377
|
+
});
|
|
378
|
+
res.status(400).json({
|
|
314
379
|
jsonrpc: '2.0',
|
|
315
380
|
error: {
|
|
316
|
-
code: -
|
|
317
|
-
message:
|
|
381
|
+
code: -32600,
|
|
382
|
+
message: 'Invalid Request: Session ID required for non-initialize requests',
|
|
318
383
|
},
|
|
319
384
|
id: req.body?.id || null,
|
|
320
385
|
});
|
|
321
386
|
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
390
|
+
log('error', 'request_error', { error: errorMessage, remote_ip: remoteIp });
|
|
391
|
+
res.status(500).json({
|
|
392
|
+
jsonrpc: '2.0',
|
|
393
|
+
error: {
|
|
394
|
+
code: -32603,
|
|
395
|
+
message: sanitizeErrorMessage(err),
|
|
396
|
+
},
|
|
397
|
+
id: req.body?.id || null,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return app;
|
|
402
|
+
}
|
|
403
|
+
startHttpTransport() {
|
|
404
|
+
const app = this.buildExpressApp();
|
|
405
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
406
|
+
app
|
|
407
|
+
.listen(port, () => {
|
|
408
|
+
log('info', 'server_start', { transport: 'http', port });
|
|
409
|
+
})
|
|
410
|
+
.on('error', (err) => {
|
|
411
|
+
log('error', 'server_error', { error: err instanceof Error ? err.message : String(err) });
|
|
412
|
+
process.exit(1);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async run() {
|
|
416
|
+
const transport = process.env.TRANSPORT || 'stdio';
|
|
417
|
+
if (transport === 'http') {
|
|
418
|
+
this.startCleanupTimer();
|
|
419
|
+
this.startHttpTransport();
|
|
332
420
|
}
|
|
333
421
|
else {
|
|
334
422
|
// For stdio mode, create a single server instance
|
|
335
423
|
const server = this.createServer();
|
|
336
|
-
const
|
|
424
|
+
const stdioTransport = new StdioServerTransport();
|
|
337
425
|
server.oninitialized = () => {
|
|
338
426
|
log('info', 'session_initialized', { transport: 'stdio' });
|
|
339
427
|
};
|
|
340
|
-
await server.connect(
|
|
428
|
+
await server.connect(stdioTransport);
|
|
341
429
|
log('info', 'server_start', { transport: 'stdio' });
|
|
342
430
|
}
|
|
343
431
|
}
|
|
344
432
|
}
|
|
345
|
-
|
|
346
|
-
server
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
433
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
434
|
+
const server = new OpenMeteoMCPServer();
|
|
435
|
+
server.run().catch((err) => {
|
|
436
|
+
log('error', 'server_error', { error: err instanceof Error ? err.message : String(err) });
|
|
437
|
+
process.exit(1);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
350
440
|
//# sourceMappingURL=index.js.map
|