@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 +7 -4
- package/dist/config/http.d.ts +55 -0
- package/dist/config/http.d.ts.map +1 -0
- package/dist/config/http.js +128 -0
- package/dist/config/http.js.map +1 -0
- package/dist/http.d.ts +20 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +137 -0
- package/dist/http.js.map +1 -0
- package/dist/server/httpApp.d.ts +53 -0
- package/dist/server/httpApp.d.ts.map +1 -0
- package/dist/server/httpApp.js +172 -0
- package/dist/server/httpApp.js.map +1 -0
- package/dist/server/oauthDiscovery.d.ts +44 -0
- package/dist/server/oauthDiscovery.d.ts.map +1 -0
- package/dist/server/oauthDiscovery.js +96 -0
- package/dist/server/oauthDiscovery.js.map +1 -0
- package/dist/server/shutdown.d.ts +2 -2
- package/dist/server/shutdown.js +2 -2
- package/dist/server/tokenVerifier.d.ts +54 -0
- package/dist/server/tokenVerifier.d.ts.map +1 -0
- package/dist/server/tokenVerifier.js +122 -0
- package/dist/server/tokenVerifier.js.map +1 -0
- package/dist/server/weatherServer.d.ts +3 -2
- package/dist/server/weatherServer.d.ts.map +1 -1
- package/dist/server/weatherServer.js +3 -2
- package/dist/server/weatherServer.js.map +1 -1
- package/package.json +15 -9
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@dangahagan/weather-mcp)
|
|
4
4
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.dgahagan/weather-mcp)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](./docs/testing/TEST_SUITE_README.md)
|
|
7
7
|
[](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,
|
|
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
|
|
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,
|
|
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
|
package/dist/http.js.map
ADDED
|
@@ -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"}
|