@dangahagan/weather-mcp 1.34.9 → 1.35.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://badge.fury.io/js/@dangahagan%2Fweather-mcp.svg)](https://www.npmjs.com/package/@dangahagan/weather-mcp)
4
4
  [![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.dgahagan/weather-mcp)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Tests](https://img.shields.io/badge/tests-4%2C551%20passing-brightgreen)](./docs/testing/TEST_SUITE_README.md)
6
+ [![Tests](https://img.shields.io/badge/tests-4%2C656%20passing-brightgreen)](./docs/testing/TEST_SUITE_README.md)
7
7
  [![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white)](https://nodejs.org)
8
8
 
9
9
  **Give your AI assistant real weather data — 17 tools, zero API keys, zero signup, zero cost.**
@@ -58,7 +58,7 @@ Choose this one if you want:
58
58
 
59
59
  - **Genuinely free** — every data source is a free public API. No trial that expires, no credit card, no rate-limited "free tier" bait.
60
60
  - **No API keys** — install to first forecast in under a minute. Nothing to configure, nothing to leak into a repo. ([Four optional keys](#optional-api-keys) add extras if you want them; the default configuration needs none.)
61
- - **Fully open source** — MIT licensed, readable TypeScript, 4,551 tests. Audit it, fork it, fix it.
61
+ - **Fully open source** — MIT licensed, readable TypeScript, 4,656 tests. Audit it, fork it, fix it.
62
62
  - **Privacy-respecting** — your queries go directly from your machine to public weather APIs. No middleman server, and no telemetry unless you opt in.
63
63
  - **Breadth** — 17 tools covering weather, safety hazards (lightning, floods, wildfires), marine conditions, air quality, and historical data back to 1940. Most weather MCPs stop at forecasts.
64
64
 
@@ -110,6 +110,7 @@ All 17 tools, documented in detail in **[docs/TOOLS.md](./docs/TOOLS.md)**:
110
110
  - **Imperial or metric** — pick your units server-wide (`WEATHER_UNITS`) or per request (`units: "metric"`), with fine-grained overrides (wind in knots, pressure in hPa, 24-hour clock). Defaults to imperial. See [Units & Localization](#units--localization).
111
111
  - **Built-in caching** — an LRU cache with per-data-type TTLs (5 minutes for alerts, 2 hours for forecasts, forever for finalized historical data) makes repeat queries return in <10ms and cuts upstream API calls by 50–80%.
112
112
  - **Actionable errors** — failures explain what happened and link to the upstream status page instead of dumping a stack trace.
113
+ - **Remote hosting** — besides the local stdio server, the package ships `weather-mcp-http`, which serves the same tools over Streamable HTTP so a claude.ai connector or a client on another machine can reach your own instance. It requires an OAuth authorization server you run (Keycloak, Authentik and similar) and checks every request's token, including its audience. For self-hosters; there is no public instance. See [docs/REMOTE_HOSTING.md](./docs/REMOTE_HOSTING.md).
113
114
 
114
115
  ## Data sources
115
116
 
@@ -180,7 +181,7 @@ npm run build
180
181
 
181
182
  Then point your MCP client at `node /absolute/path/to/weather-mcp/dist/index.js`.
182
183
 
183
- Requires Node.js 18+. No API keys, tokens, or accounts needed — see [Optional API keys](#optional-api-keys) for the four that add optional extras, and [Optional dependency](#optional-dependency) if you want a smaller install.
184
+ Requires Node.js 20+. No API keys, tokens, or accounts needed — see [Optional API keys](#optional-api-keys) for the four that add optional extras, and [Optional dependency](#optional-dependency) if you want a smaller install.
184
185
 
185
186
  ### Works with
186
187
 
@@ -279,6 +280,7 @@ Supported on `get_forecast`, `get_current_conditions`, and `get_historical_weath
279
280
  | `NCEI_API_TOKEN` | — | Optional [free NCEI token](https://www.ncdc.noaa.gov/cdo-web/token) for official NOAA climate normals (US); falls back to Open-Meteo automatically. See [Optional API keys](#optional-api-keys) |
280
281
  | `FIRMS_MAP_KEY` | — | Optional [free FIRMS key](https://firms.modaps.eosdis.nasa.gov/api/map_key/) for targeted wildfire queries and up to 5 days of detection history. See [Optional API keys](#optional-api-keys) |
281
282
  | `GOOGLE_POLLEN_API_KEY` | — | Optional key for pollen outside Europe (incl. the US). **Requires a Google Cloud billing account** — free tier is 5,000 lookups/month. See [Optional API keys](#optional-api-keys) and [the setup guide](./docs/GOOGLE_POLLEN_KEY_SETUP.md) |
283
+ | `WEATHER_HTTP_PUBLIC_URL`, `WEATHER_OAUTH_ISSUER`, … | — | Remote HTTP server (`weather-mcp-http`) only: five variables for its address, public URL and OAuth issuer. See [docs/REMOTE_HOSTING.md](./docs/REMOTE_HOSTING.md#configuration) |
282
284
  | `GOOGLE_WEATHER_API_KEY` | — | Optional key for official weather alerts beyond the US, Canada, Europe, India, the Philippines, Indonesia, and Japan (~45+ more territories). **Requires a Google Cloud billing account.** See [Optional API keys](#optional-api-keys) and [the setup guide](./docs/GOOGLE_WEATHER_KEY_SETUP.md) |
283
285
 
284
286
  For caching architecture details, see [.github/CACHING.md](./.github/CACHING.md).
@@ -391,7 +393,7 @@ Being honest about what free public data can and can't do:
391
393
  ```bash
392
394
  npm run build # Compile TypeScript
393
395
  npm run dev # Run in development mode
394
- npm test # Run all 4,551 tests
396
+ npm test # Run all 4,656 tests
395
397
  npm run test:coverage # Coverage report
396
398
  npm run audit # Dependency vulnerability scan
397
399
  ```
@@ -418,6 +420,7 @@ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
418
420
  - **[Examples](./examples/)** — realistic sessions in cities around the world: prompt → assistant answer → verbatim server output
419
421
  - **[Tool Reference](./docs/TOOLS.md)** — all 17 tools: parameters, examples, sample output
420
422
  - **[Client Setup](./docs/CLIENT_SETUP.md)** — step-by-step for 8 MCP clients
423
+ - **[Remote Hosting](./docs/REMOTE_HOSTING.md)** — run `weather-mcp-http` behind your own OAuth authorization server
421
424
  - **[Error Handling](./docs/ERROR_HANDLING.md)** — how failures are reported
422
425
  - **[Testing Guide](./docs/testing/TESTING_GUIDE.md)** — manual testing procedures
423
426
  - **[Changelog](./CHANGELOG.md)** — version history
@@ -0,0 +1,55 @@
1
+ /**
2
+ * HTTP entry configuration.
3
+ *
4
+ * The one read site of the five variables the HTTP entry (`src/http.ts`,
5
+ * `weather-mcp-http`) takes:
6
+ *
7
+ * WEATHER_HTTP_HOST bind address (default 127.0.0.1)
8
+ * WEATHER_HTTP_PORT bind port, 1-65535 (default 3939)
9
+ * WEATHER_HTTP_PUBLIC_URL required; the externally visible MCP endpoint URL
10
+ * WEATHER_OAUTH_ISSUER required; the OAuth authorization server issuer
11
+ * WEATHER_OAUTH_REQUIRED_SCOPES comma-separated scopes (default none)
12
+ *
13
+ * The module is pure. It takes `env` as a parameter, reads each key by name,
14
+ * and makes no console or logger call. It returns a result instead of warning
15
+ * and falling back, because this server fails closed: a missing or malformed
16
+ * value must stop startup, never degrade to a default that exposes the server
17
+ * differently from what the operator meant. The caller decides how to report
18
+ * the error.
19
+ *
20
+ * Every error message is fixed text that names the variable and never echoes
21
+ * its value: a URL may carry a path the operator treats as secret.
22
+ */
23
+ export interface HttpConfig {
24
+ host: string;
25
+ port: number;
26
+ publicUrl: URL;
27
+ /** The configured issuer string, trimmed. */
28
+ issuer: string;
29
+ issuerUrl: URL;
30
+ requiredScopes: string[];
31
+ }
32
+ export type HttpConfigResult = {
33
+ ok: true;
34
+ config: HttpConfig;
35
+ } | {
36
+ ok: false;
37
+ error: string;
38
+ };
39
+ export declare const DEFAULT_HTTP_HOST = "127.0.0.1";
40
+ export declare const DEFAULT_HTTP_PORT = 3939;
41
+ export declare const ERR_PORT = "WEATHER_HTTP_PORT must be an integer from 1 to 65535";
42
+ export declare const ERR_PUBLIC_URL_REQUIRED = "WEATHER_HTTP_PUBLIC_URL is required";
43
+ export declare const ERR_ISSUER_REQUIRED = "WEATHER_OAUTH_ISSUER is required";
44
+ export declare const ERR_PUBLIC_URL_INVALID = "WEATHER_HTTP_PUBLIC_URL is not a valid URL";
45
+ export declare const ERR_ISSUER_INVALID = "WEATHER_OAUTH_ISSUER is not a valid URL";
46
+ export declare const ERR_PUBLIC_URL_HTTPS = "WEATHER_HTTP_PUBLIC_URL must be an https URL (http is allowed only for localhost, 127.0.0.1 and [::1])";
47
+ export declare const ERR_ISSUER_HTTPS = "WEATHER_OAUTH_ISSUER must be an https URL (http is allowed only for localhost and 127.0.0.1)";
48
+ export declare const ERR_PUBLIC_URL_QUERY = "WEATHER_HTTP_PUBLIC_URL must not carry a query or fragment";
49
+ export declare const ERR_ISSUER_QUERY = "WEATHER_OAUTH_ISSUER must not carry a query or fragment";
50
+ export declare const ERR_PUBLIC_URL_PATH = "WEATHER_HTTP_PUBLIC_URL path may contain only letters, digits, and . _ ~ - /";
51
+ export declare const ERR_PUBLIC_URL_USERINFO = "WEATHER_HTTP_PUBLIC_URL must not carry a username or password";
52
+ export declare const ERR_ISSUER_USERINFO = "WEATHER_OAUTH_ISSUER must not carry a username or password";
53
+ export declare const ERR_PUBLIC_URL_CANONICAL = "WEATHER_HTTP_PUBLIC_URL must be in canonical form (lowercase host, no default port, a path of at least \"/\")";
54
+ export declare function parseHttpConfig(env: NodeJS.ProcessEnv): HttpConfigResult;
55
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/config/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,GAAG,CAAC;IACf,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,GAAG,CAAC;IACf,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAC7C,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,eAAO,MAAM,QAAQ,yDAAyD,CAAC;AAC/E,eAAO,MAAM,uBAAuB,wCAAwC,CAAC;AAC7E,eAAO,MAAM,mBAAmB,qCAAqC,CAAC;AACtE,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AACnF,eAAO,MAAM,kBAAkB,4CAA4C,CAAC;AAC5E,eAAO,MAAM,oBAAoB,2GACyE,CAAC;AAC3G,eAAO,MAAM,gBAAgB,iGACmE,CAAC;AACjG,eAAO,MAAM,oBAAoB,+DAA+D,CAAC;AACjG,eAAO,MAAM,gBAAgB,4DAA4D,CAAC;AAC1F,eAAO,MAAM,mBAAmB,iFACgD,CAAC;AACjF,eAAO,MAAM,uBAAuB,kEAAkE,CAAC;AACvG,eAAO,MAAM,mBAAmB,+DAA+D,CAAC;AAChG,eAAO,MAAM,wBAAwB,kHAC0E,CAAC;AA2ChH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,gBAAgB,CA6DxE"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * HTTP entry configuration.
3
+ *
4
+ * The one read site of the five variables the HTTP entry (`src/http.ts`,
5
+ * `weather-mcp-http`) takes:
6
+ *
7
+ * WEATHER_HTTP_HOST bind address (default 127.0.0.1)
8
+ * WEATHER_HTTP_PORT bind port, 1-65535 (default 3939)
9
+ * WEATHER_HTTP_PUBLIC_URL required; the externally visible MCP endpoint URL
10
+ * WEATHER_OAUTH_ISSUER required; the OAuth authorization server issuer
11
+ * WEATHER_OAUTH_REQUIRED_SCOPES comma-separated scopes (default none)
12
+ *
13
+ * The module is pure. It takes `env` as a parameter, reads each key by name,
14
+ * and makes no console or logger call. It returns a result instead of warning
15
+ * and falling back, because this server fails closed: a missing or malformed
16
+ * value must stop startup, never degrade to a default that exposes the server
17
+ * differently from what the operator meant. The caller decides how to report
18
+ * the error.
19
+ *
20
+ * Every error message is fixed text that names the variable and never echoes
21
+ * its value: a URL may carry a path the operator treats as secret.
22
+ */
23
+ export const DEFAULT_HTTP_HOST = '127.0.0.1';
24
+ export const DEFAULT_HTTP_PORT = 3939;
25
+ export const ERR_PORT = 'WEATHER_HTTP_PORT must be an integer from 1 to 65535';
26
+ export const ERR_PUBLIC_URL_REQUIRED = 'WEATHER_HTTP_PUBLIC_URL is required';
27
+ export const ERR_ISSUER_REQUIRED = 'WEATHER_OAUTH_ISSUER is required';
28
+ export const ERR_PUBLIC_URL_INVALID = 'WEATHER_HTTP_PUBLIC_URL is not a valid URL';
29
+ export const ERR_ISSUER_INVALID = 'WEATHER_OAUTH_ISSUER is not a valid URL';
30
+ export const ERR_PUBLIC_URL_HTTPS = 'WEATHER_HTTP_PUBLIC_URL must be an https URL (http is allowed only for localhost, 127.0.0.1 and [::1])';
31
+ export const ERR_ISSUER_HTTPS = 'WEATHER_OAUTH_ISSUER must be an https URL (http is allowed only for localhost and 127.0.0.1)';
32
+ export const ERR_PUBLIC_URL_QUERY = 'WEATHER_HTTP_PUBLIC_URL must not carry a query or fragment';
33
+ export const ERR_ISSUER_QUERY = 'WEATHER_OAUTH_ISSUER must not carry a query or fragment';
34
+ export const ERR_PUBLIC_URL_PATH = 'WEATHER_HTTP_PUBLIC_URL path may contain only letters, digits, and . _ ~ - /';
35
+ export const ERR_PUBLIC_URL_USERINFO = 'WEATHER_HTTP_PUBLIC_URL must not carry a username or password';
36
+ export const ERR_ISSUER_USERINFO = 'WEATHER_OAUTH_ISSUER must not carry a username or password';
37
+ export const ERR_PUBLIC_URL_CANONICAL = 'WEATHER_HTTP_PUBLIC_URL must be in canonical form (lowercase host, no default port, a path of at least "/")';
38
+ // URL.hostname keeps IPv6 literals bracketed (GOTCHAS G109).
39
+ const PUBLIC_URL_LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
40
+ // Mirrors the SDK's checkIssuerUrl exemption list (server/auth/router.js).
41
+ const ISSUER_LOOPBACK = new Set(['localhost', '127.0.0.1']);
42
+ const PATH_PATTERN = /^[A-Za-z0-9._~/-]*$/;
43
+ function read(env, name) {
44
+ const raw = env[name];
45
+ return typeof raw === 'string' ? raw.trim() : '';
46
+ }
47
+ function parseUrl(raw, loopback, msg) {
48
+ if (raw === '')
49
+ return { ok: false, error: msg.required };
50
+ let url;
51
+ try {
52
+ url = new URL(raw);
53
+ }
54
+ catch {
55
+ return { ok: false, error: msg.invalid };
56
+ }
57
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback.has(url.hostname))) {
58
+ return { ok: false, error: msg.https };
59
+ }
60
+ // A bare trailing "?" or "#" leaves search/hash empty, so check the raw string too.
61
+ if (url.search !== '' || url.hash !== '' || raw.includes('?') || raw.includes('#')) {
62
+ return { ok: false, error: msg.query };
63
+ }
64
+ return { ok: true, url };
65
+ }
66
+ export function parseHttpConfig(env) {
67
+ const host = read(env, 'WEATHER_HTTP_HOST') || DEFAULT_HTTP_HOST;
68
+ const rawPort = read(env, 'WEATHER_HTTP_PORT');
69
+ let port = DEFAULT_HTTP_PORT;
70
+ if (rawPort !== '') {
71
+ // Digits only: Number() alone would accept "0x10" and "1e2".
72
+ const n = /^[0-9]+$/.test(rawPort) ? Number(rawPort) : NaN;
73
+ if (!Number.isInteger(n) || n < 1 || n > 65535)
74
+ return { ok: false, error: ERR_PORT };
75
+ port = n;
76
+ }
77
+ const publicRaw = read(env, 'WEATHER_HTTP_PUBLIC_URL');
78
+ const pub = parseUrl(publicRaw, PUBLIC_URL_LOOPBACK, {
79
+ required: ERR_PUBLIC_URL_REQUIRED,
80
+ invalid: ERR_PUBLIC_URL_INVALID,
81
+ https: ERR_PUBLIC_URL_HTTPS,
82
+ query: ERR_PUBLIC_URL_QUERY,
83
+ });
84
+ if (!pub.ok)
85
+ return pub;
86
+ if (pub.url.username !== '' || pub.url.password !== '') {
87
+ return { ok: false, error: ERR_PUBLIC_URL_USERINFO };
88
+ }
89
+ // Express 5 reads anything else as path-to-regexp syntax (e.g. "/mcp:v2").
90
+ if (!PATH_PATTERN.test(pub.url.pathname))
91
+ return { ok: false, error: ERR_PUBLIC_URL_PATH };
92
+ // The token audience is compared as an exact string against publicUrl.href. A
93
+ // configured value that the URL parser rewrites (mixed-case host, explicit :443,
94
+ // no path) would make every token fail the audience check, so refuse it here and
95
+ // keep "exactly as configured" true. PATH_PATTERN keeps the path free of
96
+ // percent-encoding, so href equality is a clean test.
97
+ if (pub.url.href !== publicRaw)
98
+ return { ok: false, error: ERR_PUBLIC_URL_CANONICAL };
99
+ const issuerRaw = read(env, 'WEATHER_OAUTH_ISSUER');
100
+ const iss = parseUrl(issuerRaw, ISSUER_LOOPBACK, {
101
+ required: ERR_ISSUER_REQUIRED,
102
+ invalid: ERR_ISSUER_INVALID,
103
+ https: ERR_ISSUER_HTTPS,
104
+ query: ERR_ISSUER_QUERY,
105
+ });
106
+ if (!iss.ok)
107
+ return iss;
108
+ // Discovery fetches from this URL, and the startup line logs its origin.
109
+ if (iss.url.username !== '' || iss.url.password !== '') {
110
+ return { ok: false, error: ERR_ISSUER_USERINFO };
111
+ }
112
+ const requiredScopes = read(env, 'WEATHER_OAUTH_REQUIRED_SCOPES')
113
+ .split(',')
114
+ .map((s) => s.trim())
115
+ .filter((s) => s.length > 0);
116
+ return {
117
+ ok: true,
118
+ config: {
119
+ host,
120
+ port,
121
+ publicUrl: pub.url,
122
+ issuer: issuerRaw,
123
+ issuerUrl: iss.url,
124
+ requiredScopes,
125
+ },
126
+ };
127
+ }
128
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/config/http.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Weather MCP Server — the Streamable HTTP entry (`weather-mcp-http`).
4
+ *
5
+ * A second process entry beside the stdio one (`src/index.ts`), never a mode of
6
+ * it: express and jose stay out of the stdio entry's import graph. Both are
7
+ * imported statically here, so a broken install fails at boot (G24).
8
+ *
9
+ * Fail closed. Every check below runs before the port is bound, and any
10
+ * failure logs one fixed reason and exits 1:
11
+ * 1. WebCrypto present (jose needs it; Node 20+)
12
+ * 2. the WEATHER_HTTP_* / WEATHER_OAUTH_* configuration parses
13
+ * 3. the authorization server's metadata is readable and names this issuer
14
+ *
15
+ * Shutdown is SIGTERM and SIGINT only. A container started without `-i` has
16
+ * stdin at EOF from the first tick, so the stdio entry's stdin triggers would
17
+ * stop this server the moment it booted.
18
+ */
19
+ import 'dotenv/config';
20
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,eAAe,CAAC"}
package/dist/http.js ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Weather MCP Server — the Streamable HTTP entry (`weather-mcp-http`).
4
+ *
5
+ * A second process entry beside the stdio one (`src/index.ts`), never a mode of
6
+ * it: express and jose stay out of the stdio entry's import graph. Both are
7
+ * imported statically here, so a broken install fails at boot (G24).
8
+ *
9
+ * Fail closed. Every check below runs before the port is bound, and any
10
+ * failure logs one fixed reason and exits 1:
11
+ * 1. WebCrypto present (jose needs it; Node 20+)
12
+ * 2. the WEATHER_HTTP_* / WEATHER_OAUTH_* configuration parses
13
+ * 3. the authorization server's metadata is readable and names this issuer
14
+ *
15
+ * Shutdown is SIGTERM and SIGINT only. A container started without `-i` has
16
+ * stdin at EOF from the first tick, so the stdio entry's stdin triggers would
17
+ * stop this server the moment it booted.
18
+ */
19
+ // Load environment variables from .env file (for local development)
20
+ import 'dotenv/config';
21
+ import { createServer as createNodeHttpServer } from 'node:http';
22
+ import { createRemoteJWKSet } from 'jose';
23
+ import { LocationStore } from './services/locationStore.js';
24
+ import { blitzortungService } from './services/blitzortung.js';
25
+ import { toolConfig } from './config/tools.js';
26
+ import { parseHttpConfig } from './config/http.js';
27
+ import { describeErrorForLogging, logger, redactUrlForLogging } from './utils/logger.js';
28
+ import { analytics } from './analytics/index.js';
29
+ import { SERVER_VERSION } from './server/weatherServer.js';
30
+ import { createHttpApp } from './server/httpApp.js';
31
+ import { createTokenVerifier } from './server/tokenVerifier.js';
32
+ import { discoverIssuer } from './server/oauthDiscovery.js';
33
+ import { startLightningPrewarm } from './server/lightningPrewarm.js';
34
+ import { createShutdown, SHUTDOWN_DEADLINE_MS } from './server/shutdown.js';
35
+ /** As in src/index.ts: half the budget, leaving room for the MQTT disconnect after it. */
36
+ const ANALYTICS_SHUTDOWN_FLUSH_MS = SHUTDOWN_DEADLINE_MS / 2;
37
+ /**
38
+ * How long `server.close()` may wait for in-flight requests before every
39
+ * connection is destroyed. 400 + the analytics flush (750) + the measured MQTT
40
+ * disconnect (~316) fits the 1500 ms deadline, which still bounds a hung step.
41
+ */
42
+ const HTTP_FORCE_CLOSE_MS = 400;
43
+ const WEBCRYPTO_REFUSAL = 'The HTTP entry needs Node.js 20 or newer (WebCrypto is unavailable)';
44
+ const LISTEN_REFUSAL = 'could not listen on WEATHER_HTTP_HOST:WEATHER_HTTP_PORT';
45
+ let httpServer;
46
+ let lightningPrewarm;
47
+ /** Log one fixed reason and exit 1. Nothing has been bound when this runs. */
48
+ function refuse(reason, metadata) {
49
+ logger.error('HTTP entry refused to start', undefined, { reason, ...metadata });
50
+ process.exit(1);
51
+ }
52
+ /** Stop accepting connections; force the rest closed if they outlast the grace period. */
53
+ function closeHttpServer() {
54
+ const server = httpServer;
55
+ if (!server)
56
+ return Promise.resolve();
57
+ return new Promise((resolve) => {
58
+ const force = setTimeout(() => server.closeAllConnections(), HTTP_FORCE_CLOSE_MS);
59
+ server.close(() => {
60
+ clearTimeout(force);
61
+ resolve();
62
+ });
63
+ server.closeIdleConnections();
64
+ });
65
+ }
66
+ function listen(server, port, host) {
67
+ return new Promise((resolve, reject) => {
68
+ const onError = (error) => reject(error);
69
+ server.once('error', onError);
70
+ server.once('listening', () => {
71
+ server.off('error', onError);
72
+ resolve();
73
+ });
74
+ server.listen(port, host);
75
+ });
76
+ }
77
+ async function main() {
78
+ const shutdown = createShutdown({
79
+ deadlineMs: SHUTDOWN_DEADLINE_MS,
80
+ exit: (code) => process.exit(code),
81
+ steps: [
82
+ { name: 'http', run: closeHttpServer },
83
+ { name: 'lightning-prewarm', run: () => lightningPrewarm?.stop() },
84
+ { name: 'analytics', run: () => analytics.shutdown({ deadlineMs: ANALYTICS_SHUTDOWN_FLUSH_MS }) },
85
+ { name: 'mqtt', run: () => blitzortungService.disconnect() }
86
+ ]
87
+ });
88
+ process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
89
+ process.on('SIGINT', () => { void shutdown('SIGINT'); });
90
+ if (!globalThis.crypto?.subtle)
91
+ refuse(WEBCRYPTO_REFUSAL);
92
+ const parsed = parseHttpConfig(process.env);
93
+ if (!parsed.ok)
94
+ refuse(parsed.error);
95
+ const { config } = parsed;
96
+ const discovered = await discoverIssuer(config.issuerUrl, config.issuer);
97
+ if (!discovered.ok)
98
+ refuse(discovered.error);
99
+ // jose's defaults: a 5 s fetch timeout and a 30 s cooldown between refetches.
100
+ // The first fetch happens on the first request, not here.
101
+ const getKey = createRemoteJWKSet(discovered.jwksUri);
102
+ // One store for every authenticated caller (the design's tenancy decision).
103
+ const locationStore = new LocationStore();
104
+ const verifier = createTokenVerifier({ issuer: config.issuer, audience: config.publicUrl.href, getKey });
105
+ const app = createHttpApp({ config, verifier, oauthMetadata: discovered.metadata, locationStore });
106
+ const server = createNodeHttpServer(app);
107
+ try {
108
+ await listen(server, config.port, config.host);
109
+ }
110
+ catch (error) {
111
+ refuse(LISTEN_REFUSAL, { error: describeErrorForLogging(error) });
112
+ }
113
+ httpServer = server;
114
+ server.on('error', (error) => {
115
+ logger.error('HTTP server error', undefined, { error: describeErrorForLogging(error) });
116
+ });
117
+ // The public URL's path is never logged: an operator may treat it as a secret.
118
+ logger.info('Weather MCP HTTP server listening', {
119
+ version: SERVER_VERSION,
120
+ host: config.host,
121
+ port: config.port,
122
+ issuerOrigin: redactUrlForLogging(config.issuer),
123
+ enabledTools: toolConfig.getEnabledTools().length
124
+ });
125
+ // Once per process, after listen — never per request.
126
+ lightningPrewarm = startLightningPrewarm({
127
+ toolEnabled: toolConfig.isEnabled('get_lightning_activity'),
128
+ optOutValue: process.env.WEATHER_LIGHTNING_PREWARM,
129
+ readSavedLocations: () => Object.values(locationStore.getAll()),
130
+ prewarmLocation: (lat, lon, r, o) => blitzortungService.prewarmLocation(lat, lon, r, o)
131
+ });
132
+ }
133
+ main().catch((error) => {
134
+ logger.error('HTTP entry failed', undefined, { error: describeErrorForLogging(error) });
135
+ process.exit(1);
136
+ });
137
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The HTTP entry's express app: an OAuth 2.1 resource server in front of a
3
+ * stateless Streamable HTTP transport.
4
+ *
5
+ * `createHttpApp()` builds the app and returns it. It is side-effect-free: it
6
+ * binds no port, registers no signal handler, starts no timer and reads no
7
+ * environment. `src/http.ts` starts everything, so a test can build the app and
8
+ * bind it to loopback itself (G61).
9
+ *
10
+ * The stages run in this order, and a new middleware goes into this order:
11
+ *
12
+ * 1. GET /healthz constant body, no upstream probe, ahead of host
13
+ * validation so a proxy or container probe that sends
14
+ * Host: 127.0.0.1 still gets an answer
15
+ * 2. host validation the SDK's DNS-rebinding guard: Host must name the
16
+ * public URL's host, on every other route
17
+ * 3. metadata router RFC 9728 protected-resource document (and the
18
+ * issuer's RFC 8414 mirror), unauthenticated
19
+ * 4. on the MCP route, every method:
20
+ * access log method, status, duration; nothing else
21
+ * missing-token check classifies `missing` for the log; the SDK answers
22
+ * requireBearerAuth 401 with WWW-Authenticate naming the metadata URL,
23
+ * 403 for a missing required scope
24
+ * 5. POST only: a 256 KB JSON body limit, then a fresh Server and transport
25
+ * per request (stateless transports are single-use)
26
+ * 6. any other method: 405, after auth, so the route never answers without a token
27
+ * 7. a final error handler, so express never reaches its default handler,
28
+ * which writes err.stack to stderr through console.error
29
+ *
30
+ * No log line on this path carries the Authorization header, a token, a claim,
31
+ * the subject, a request body, a header or the route path (the path may be an
32
+ * operator's secret-by-obscurity).
33
+ */
34
+ import express from 'express';
35
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
36
+ import type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
37
+ import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';
38
+ import type { HttpConfig } from '../config/http.js';
39
+ import type { LocationStore } from '../services/locationStore.js';
40
+ import { type WeatherServerOptions } from './weatherServer.js';
41
+ /** A tools/call is a few hundred bytes; the SDK's own 4 MiB default is far above any real request. */
42
+ export declare const HTTP_BODY_LIMIT = "256kb";
43
+ export interface HttpAppDeps {
44
+ config: HttpConfig;
45
+ verifier: OAuthTokenVerifier;
46
+ /** The issuer's metadata, as `discoverIssuer` returned it. */
47
+ oauthMetadata: OAuthMetadata;
48
+ locationStore: LocationStore;
49
+ /** Defaults to `createWeatherServer`. Injectable so a test can count the Servers built. */
50
+ createServer?: (options: WeatherServerOptions) => Server;
51
+ }
52
+ export declare function createHttpApp(deps: HttpAppDeps): express.Express;
53
+ //# sourceMappingURL=httpApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpApp.d.ts","sourceRoot":"","sources":["../../src/server/httpApp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,OAA2D,MAAM,SAAS,CAAC;AAClF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAQxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mDAAmD,CAAC;AAC5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAGpF,sGAAsG;AACtG,eAAO,MAAM,eAAe,UAAU,CAAC;AAEvC,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,8DAA8D;IAC9D,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,2FAA2F;IAC3F,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,MAAM,CAAC;CAC1D;AAqBD,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAqHhE"}