@mrfentmen/spaceweather-mcp 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,33 @@
1
- # Temporary Holding Version
1
+ # Space Weather
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ NOAA space weather: planetary Kp index, aurora probability for a place and active alerts from SWPC - keyless.
4
+
5
+ Keyless — no signup, no API key, no environment variables.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx -y @mrfentmen/spaceweather-mcp
11
+ ```
12
+
13
+ Or add it to your MCP client:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "spaceweather-mcp": { "command": "npx", "args": ["-y", "@mrfentmen/spaceweather-mcp"] }
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Tools
24
+
25
+ | Tool | What it does |
26
+ |---|---|
27
+ | `kp_index` | Recent planetary Kp values (3-hourly) and the geomagnetic storm level they imply. |
28
+ | `aurora_forecast` | Aurora visibility probability from the OVATION model, at a location or as a global summary. |
29
+ | `alerts` | The latest SWPC alerts and warnings with issue times and message text. |
30
+
31
+ ## Data source
32
+
33
+ NOAA Space Weather Prediction Center JSON feeds (services.swpc.noaa.gov), no key required
package/dist/api.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export declare class SourceError extends Error {
2
+ }
3
+ export declare function getJson(path: string, accept?: string, timeoutMs?: number): Promise<any>;
4
+ export declare function clampLimit(n: number | undefined, fallback: number, max: number): number;
5
+ export declare function clean(s: unknown): string;
6
+ export declare function kpIndex(a: {
7
+ hours?: number;
8
+ }): Promise<string>;
9
+ export declare function auroraForecast(a: {
10
+ lat?: number;
11
+ lon?: number;
12
+ }): Promise<string>;
13
+ export declare function alerts(a: {
14
+ limit?: number;
15
+ }): Promise<string>;
package/dist/api.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Space Weather — keyless public API client.
3
+ */
4
+ const BASE = "https://services.swpc.noaa.gov";
5
+ const SOURCE = "Space Weather";
6
+ export class SourceError extends Error {
7
+ }
8
+ async function request(path, accept = "application/json", timeoutMs = 25000) {
9
+ const ctrl = new AbortController();
10
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
11
+ try {
12
+ const res = await fetch(path.startsWith("http") ? path : BASE + path, {
13
+ signal: ctrl.signal,
14
+ headers: { "user-agent": "spaceweather-mcp", accept },
15
+ });
16
+ if (!res.ok)
17
+ throw new SourceError(`${SOURCE} responded ${res.status}`);
18
+ return res;
19
+ }
20
+ catch (e) {
21
+ if (e instanceof SourceError)
22
+ throw e;
23
+ if (e.name === "AbortError")
24
+ throw new SourceError(`${SOURCE} request timed out`);
25
+ throw new SourceError(`${SOURCE} request failed: ${e.message}`);
26
+ }
27
+ finally {
28
+ clearTimeout(timer);
29
+ }
30
+ }
31
+ export async function getJson(path, accept = "application/json", timeoutMs = 25000) {
32
+ const res = await request(path, accept, timeoutMs);
33
+ const body = await res.text();
34
+ try {
35
+ return JSON.parse(body);
36
+ }
37
+ catch {
38
+ throw new SourceError(`${SOURCE} returned malformed JSON`);
39
+ }
40
+ }
41
+ export function clampLimit(n, fallback, max) {
42
+ const v = n ?? fallback;
43
+ if (!Number.isInteger(v) || v < 1 || v > max)
44
+ throw new SourceError(`limit must be between 1 and ${max}`);
45
+ return v;
46
+ }
47
+ export function clean(s) {
48
+ return String(s ?? "").replace(/\s+/g, " ").trim();
49
+ }
50
+ function stormLevel(kp) {
51
+ if (kp >= 9)
52
+ return "G5 extreme storm";
53
+ if (kp >= 8)
54
+ return "G4 severe storm";
55
+ if (kp >= 7)
56
+ return "G3 strong storm";
57
+ if (kp >= 6)
58
+ return "G2 moderate storm";
59
+ if (kp >= 5)
60
+ return "G1 minor storm";
61
+ if (kp >= 4)
62
+ return "active";
63
+ return "quiet";
64
+ }
65
+ export async function kpIndex(a) {
66
+ const hours = clampLimit(a.hours, 8, 24);
67
+ const data = await getJson("/products/noaa-planetary-k-index.json", "application/json", 25000);
68
+ if (!Array.isArray(data) || !data.length)
69
+ throw new SourceError("SWPC returned no Kp data");
70
+ const rows = data.slice(-hours);
71
+ const latest = rows[rows.length - 1];
72
+ const kp = Number(latest?.Kp);
73
+ const maxKp = Math.max(...rows.map((r) => Number(r?.Kp)).filter(Number.isFinite));
74
+ return [
75
+ `Latest Kp: ${kp.toFixed(2)} (${stormLevel(kp)}) at ${clean(latest?.time_tag)} UTC`,
76
+ `Highest in the last ${rows.length} entries: ${maxKp.toFixed(2)}`,
77
+ "",
78
+ "time (UTC) Kp level",
79
+ ...rows.map((r) => {
80
+ const v = Number(r?.Kp);
81
+ return `${clean(r?.time_tag).padEnd(20)} ${v.toFixed(2).padStart(5)} ${stormLevel(v)}`;
82
+ }),
83
+ "",
84
+ "Kp is the 3-hour planetary geomagnetic index; 5 or above means a storm and visible aurora at lower latitudes.",
85
+ ].join("\n");
86
+ }
87
+ export async function auroraForecast(a) {
88
+ const data = await getJson("/json/ovation_aurora_latest.json", "application/json", 30000);
89
+ const coords = Array.isArray(data?.coordinates) ? data.coordinates : [];
90
+ if (!coords.length)
91
+ throw new SourceError("SWPC returned no aurora grid");
92
+ const observed = clean(data["Observation Time"]);
93
+ const forecast = clean(data["Forecast Time"]);
94
+ const hasPoint = a.lat !== undefined && a.lon !== undefined;
95
+ const lines = [
96
+ `Aurora model: observed ${observed}, forecast for ${forecast}`,
97
+ "",
98
+ ];
99
+ if (hasPoint) {
100
+ const lat = Math.round(a.lat);
101
+ const lon = Math.round(a.lon);
102
+ let best = null;
103
+ for (const [x, y, v] of coords) {
104
+ const dLat = Math.abs(Number(y) - lat);
105
+ // Longitude wraps at 360, so -22 is the same column as 338.
106
+ const dLonRaw = Math.abs(Number(x) - lon);
107
+ const dLon = Math.min(dLonRaw, 360 - dLonRaw);
108
+ const score = dLat + dLon;
109
+ if (!best || score < best.score)
110
+ best = { lat: Number(y), lon: Number(x), value: Number(v), score };
111
+ }
112
+ const value = best ? best.value : 0;
113
+ lines.push(`At ${lat}, ${lon} (nearest grid cell ${best?.lat}, ${best?.lon}): aurora probability ${value}%`, "", value >= 50 ? "Aurora is likely overhead if skies are clear." :
114
+ value >= 20 ? "Aurora is possible overhead, better chance looking toward the pole." :
115
+ value >= 5 ? "Aurora is unlikely overhead; look north for a glow." :
116
+ "No aurora expected at this location.", "", "The model is a 30-90 minute forecast; clouds and moonlight decide what you actually see.");
117
+ }
118
+ else {
119
+ const cells = coords.map(([x, y, v]) => ({ lat: Number(y), lon: Number(x), value: Number(v) }));
120
+ for (const hemisphere of [{ name: "north", test: (c) => c.lat > 0 }, { name: "south", test: (c) => c.lat < 0 }]) {
121
+ const subset = cells.filter(hemisphere.test);
122
+ const strong = subset.filter((c) => c.value >= 50);
123
+ const mild = subset.filter((c) => c.value >= 10);
124
+ const lowestLat = strong.length ? Math.min(...strong.map((c) => Math.abs(c.lat))) : null;
125
+ lines.push(`${hemisphere.name}: ${strong.length} cell(s) with 50%+ probability, ${mild.length} with 10%+` +
126
+ (lowestLat !== null ? ` - aurora reaching down to about ${lowestLat} degrees latitude` : " - no strong aurora belt right now"));
127
+ }
128
+ lines.push("", "Pass lat and lon to check one place.");
129
+ }
130
+ return lines.join("\n");
131
+ }
132
+ export async function alerts(a) {
133
+ const limit = clampLimit(a.limit, 5, 20);
134
+ const data = await getJson("/products/alerts.json", "application/json", 25000);
135
+ if (!Array.isArray(data) || !data.length)
136
+ return "SWPC has no active alerts or warnings right now.";
137
+ const rows = data.slice(0, limit);
138
+ return [
139
+ `${data.length} alert(s) on the feed, newest first (showing ${rows.length}):`,
140
+ "",
141
+ ...rows.map((al, i) => {
142
+ const message = clean(al?.message).replace(/\s+/g, " ");
143
+ return `${i + 1}. [${clean(al?.product_id)}] ${clean(al?.issue_datetime)} UTC\n ${message.slice(0, 240)}${message.length > 240 ? " ..." : ""}`;
144
+ }),
145
+ ].join("\n");
146
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ async function main() {
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ console.error("spaceweather-mcp running on stdio");
8
+ }
9
+ main().catch((err) => {
10
+ console.error("Fatal error:", err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,44 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { alerts, auroraForecast, kpIndex, SourceError } from "./api.js";
4
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
5
+ function errorMessage(e) {
6
+ if (e instanceof SourceError || e instanceof Error)
7
+ return `Error: ${e.message}`;
8
+ return `Error: ${String(e)}`;
9
+ }
10
+ export function createServer() {
11
+ const server = new McpServer({ name: "spaceweather-mcp", version: "1.0.0" });
12
+ server.tool("kp_index", "Recent planetary Kp values (3-hourly) and the geomagnetic storm level they imply.", {
13
+ hours: z.number().int().min(1).max(24).default(8).describe("How many recent 3-hour entries to list")
14
+ }, async (a) => {
15
+ try {
16
+ return text(await kpIndex(a));
17
+ }
18
+ catch (e) {
19
+ return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
20
+ }
21
+ });
22
+ server.tool("aurora_forecast", "Aurora visibility probability from the OVATION model, at a location or as a global summary.", {
23
+ lat: z.number().min(-90).max(90).optional().describe("Latitude to look up, e.g. 60"),
24
+ lon: z.number().min(-180).max(180).optional().describe("Longitude to look up, e.g. 19")
25
+ }, async (a) => {
26
+ try {
27
+ return text(await auroraForecast(a));
28
+ }
29
+ catch (e) {
30
+ return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
31
+ }
32
+ });
33
+ server.tool("alerts", "The latest SWPC alerts and warnings with issue times and message text.", {
34
+ limit: z.number().int().min(1).max(20).default(5).describe("How many recent alerts to show")
35
+ }, async (a) => {
36
+ try {
37
+ return text(await alerts(a));
38
+ }
39
+ catch (e) {
40
+ return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
41
+ }
42
+ });
43
+ return server;
44
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@mrfentmen/spaceweather-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "NOAA space weather: planetary Kp index, aurora probability for a place and active alerts from SWPC - keyless.",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/spaceweather-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mrfentmen/awesome-servers.git"
10
+ },
11
+ "bin": {
12
+ "spaceweather-mcp": "./dist/index.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "start": "node dist/index.js"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "space",
25
+ "weather",
26
+ "aurora",
27
+ "solar",
28
+ "noaa",
29
+ "keyless"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.4",
34
+ "zod": "^3.23.8"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "typescript": "^5.6.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ }
43
+ }