@pipeworx/mcp-us-iso-grid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pipeworx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # mcp-us-iso-grid
2
+
3
+ US ISO Grid MCP — real-time electricity generation, fuel mix, demand,
4
+
5
+ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 965+ live data sources.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+
12
+ ## Quick Start
13
+
14
+ Add to your MCP client (Claude Desktop, Cursor, Windsurf, etc.):
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "us-iso-grid": {
20
+ "url": "https://gateway.pipeworx.io/us-iso-grid/mcp"
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ Or connect to the full Pipeworx gateway for access to all 965+ data sources:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "pipeworx": {
32
+ "url": "https://gateway.pipeworx.io/mcp"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Using with ask_pipeworx
39
+
40
+ Instead of calling tools directly, you can ask questions in plain English:
41
+
42
+ ```
43
+ ask_pipeworx({ question: "your question about Us Iso Grid data" })
44
+ ```
45
+
46
+ The gateway picks the right tool and fills the arguments automatically.
47
+
48
+ ## More
49
+
50
+ - [All tools and guides](https://github.com/pipeworx-io/examples)
51
+ - [pipeworx.io](https://pipeworx.io)
52
+
53
+ ## License
54
+
55
+ MIT
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@pipeworx/mcp-us-iso-grid",
3
+ "version": "0.1.0",
4
+ "description": "US ISO Grid MCP — real-time electricity generation, fuel mix, demand,",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "keywords": ["mcp", "mcp-server", "model-context-protocol", "pipeworx", "us-iso-grid"],
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/pipeworx-io/mcp-us-iso-grid"
13
+ },
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.7.0"
19
+ }
20
+ }
package/server.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.pipeworx-io/us-iso-grid",
4
+ "title": "Us Iso Grid",
5
+ "description": "US ISO Grid MCP — real-time electricity generation, fuel mix, demand,",
6
+ "version": "0.1.0",
7
+ "websiteUrl": "https://pipeworx.io/packs/us-iso-grid",
8
+ "repository": {
9
+ "url": "https://github.com/pipeworx-io/mcp-us-iso-grid",
10
+ "source": "github"
11
+ },
12
+ "remotes": [
13
+ {
14
+ "type": "streamable-http",
15
+ "url": "https://gateway.pipeworx.io/us-iso-grid/mcp"
16
+ }
17
+ ]
18
+ }
package/src/index.ts ADDED
@@ -0,0 +1,355 @@
1
+ interface McpToolDefinition {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: 'object';
6
+ properties: Record<string, unknown>;
7
+ required?: string[];
8
+ };
9
+ }
10
+
11
+ interface McpToolExport {
12
+ tools: McpToolDefinition[];
13
+ callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
14
+ meter?: { credits: number };
15
+ cost?: Record<string, unknown>;
16
+ provider?: string;
17
+ }
18
+
19
+ /**
20
+ * US ISO Grid MCP — real-time electricity generation, fuel mix, demand,
21
+ * and locational marginal prices (LMPs) for the major United States
22
+ * regional transmission operators / independent system operators.
23
+ *
24
+ * V1 covers the three ISOs with public no-auth endpoints:
25
+ * - CAISO (California ISO) outlook CSVs at caiso.com/outlook/current
26
+ * - ERCOT (Texas, Electric Reliability Council of Texas) dashboard JSONs at ercot.com
27
+ * - NYISO (New York ISO) MIS CSV reports at mis.nyiso.com/public/csv
28
+ *
29
+ * Pairs with energy-charts (EU) for combined US + EU grid coverage.
30
+ *
31
+ * Notes:
32
+ * - CAISO outlook CSVs hold the running current-day data, 5-minute
33
+ * granularity. The first column is the minute-of-day; future minutes
34
+ * are blank or 0. We filter to intervals that actually have data and
35
+ * return the most recent N.
36
+ * - ERCOT dashboards are publicly served JSON files; they lag the live
37
+ * system by 5-15 minutes but cover fuel mix + supply/demand cleanly.
38
+ * - NYISO MIS reports are published per UTC day; today's file may not
39
+ * exist until late evening NY time. Tools default to yesterday so the
40
+ * call almost always succeeds without an explicit date arg.
41
+ */
42
+
43
+
44
+ const UA = 'pipeworx-mcp-us-iso-grid/1.0 (+https://pipeworx.io)';
45
+
46
+ const tools: McpToolExport['tools'] = [
47
+ {
48
+ name: 'caiso_fuel_mix',
49
+ description:
50
+ 'CAISO (California) real-time grid fuel mix for today: solar, wind, geothermal, biomass, biogas, small hydro, coal, nuclear, natural gas, large hydro, batteries, imports per 5-minute interval. Returns timestamped MW values per fuel type. Use for "what is California using to make electricity right now", "solar curtailment today", "battery contribution this evening".',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ limit: { type: 'number', description: 'Latest N intervals to return (default 12 = most-recent hour at 5-min granularity).' },
55
+ },
56
+ },
57
+ },
58
+ {
59
+ name: 'caiso_demand',
60
+ description:
61
+ 'CAISO (California) real-time electricity demand vs. forecast for today. Returns 5-minute demand series + day-ahead forecast + hour-ahead forecast (MW). Use for "is California load tracking forecast", peak-demand timing, demand-response triggers.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ limit: { type: 'number', description: 'Latest N intervals (default 12).' },
66
+ },
67
+ },
68
+ },
69
+ {
70
+ name: 'caiso_renewables',
71
+ description:
72
+ 'CAISO (California) real-time renewable generation breakdown for today: solar, wind, geothermal, biomass, biogas, small hydro, nuclear, large hydro, batteries (MW per 5-min interval). Subset of caiso_fuel_mix focused on zero-/low-carbon sources. Use for "California renewable share right now", "duck curve today".',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ limit: { type: 'number', description: 'Latest N intervals (default 12).' },
77
+ },
78
+ },
79
+ },
80
+ {
81
+ name: 'caiso_co2',
82
+ description:
83
+ 'CAISO (California) real-time grid carbon intensity for today: metric tons CO2 emitted per 5-minute interval, plus marginal CO2 (lbs/MWh). Use for "what is California grid carbon intensity right now", climate-aware load shifting, hourly emissions reporting.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ limit: { type: 'number', description: 'Latest N intervals (default 12).' },
88
+ },
89
+ },
90
+ },
91
+ {
92
+ name: 'ercot_fuel_mix',
93
+ description:
94
+ 'ERCOT (Texas) real-time grid fuel mix: coal/lignite, natural gas, nuclear, hydro, wind, solar, power storage, other (MW). Returns the most recent 5-minute snapshot plus monthly installed capacity per fuel type, so percent-of-installed can be computed. Use for "Texas grid mix right now", "ERCOT wind output", "is Texas burning coal today".',
95
+ inputSchema: { type: 'object', properties: {} },
96
+ },
97
+ {
98
+ name: 'ercot_supply_demand',
99
+ description:
100
+ 'ERCOT (Texas) real-time supply (capacity) vs. demand: hourly capacity, current demand, ERCOT load forecast (MW). Returns the recent day plus today\'s forecast. Use for "is ERCOT close to its reserve margin", "Texas demand vs. capacity", emergency-alert proximity during heatwaves.',
101
+ inputSchema: { type: 'object', properties: {} },
102
+ },
103
+ {
104
+ name: 'nyiso_lmp_zonal',
105
+ description:
106
+ 'NYISO (New York) zonal LMP (locational marginal price, $/MWh) for the most recent published day, broken down by load zone (CAPITL, CENTRL, DUNWOD, GENESE, HUD VL, LONGIL, MHK VL, MILLWD, N.Y.C., NORTH, WEST). Returns LBMP, marginal cost of losses, marginal cost of congestion per 5-min interval. Use for NY wholesale electricity prices, zonal congestion analysis.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ date: { type: 'string', description: 'YYYY-MM-DD (NY local). Defaults to yesterday — today\'s file may not be published yet.' },
111
+ zone: { type: 'string', description: 'Optional zone filter, e.g. "N.Y.C." or "CAPITL". Default: all 11 zones.' },
112
+ },
113
+ },
114
+ },
115
+ {
116
+ name: 'nyiso_fuel_mix',
117
+ description:
118
+ 'NYISO (New York) real-time grid fuel mix for the most recent published day: dual fuel (gas/oil-capable), natural gas, nuclear, other fossil fuels, hydro, wind, solar, other renewables (MW per 5-min interval). Use for "New York grid mix yesterday", NYC clean-energy progress tracking.',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ date: { type: 'string', description: 'YYYY-MM-DD. Defaults to yesterday.' },
123
+ },
124
+ },
125
+ },
126
+ {
127
+ name: 'nyiso_load',
128
+ description:
129
+ 'NYISO (New York) actual zonal load (MW) per 5-minute interval for the most recent published day, by zone (CAPITL, CENTRL, DUNWOD, GENESE, HUD VL, LONGIL, MHK VL, MILLWD, N.Y.C., NORTH, WEST). Use for NY zonal demand analysis, peak-time identification, NYC load profile vs. upstate.',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ date: { type: 'string', description: 'YYYY-MM-DD. Defaults to yesterday.' },
134
+ zone: { type: 'string', description: 'Optional zone filter. Default: all 11 zones.' },
135
+ },
136
+ },
137
+ },
138
+ {
139
+ name: 'nyiso_load_forecast',
140
+ description:
141
+ 'NYISO (New York) ISO load forecast (ISOLF) for the most recent published day, per zone, MW. Use for "what does NYISO expect demand to be today", forecast-error tracking vs. nyiso_load.',
142
+ inputSchema: {
143
+ type: 'object',
144
+ properties: {
145
+ date: { type: 'string', description: 'YYYY-MM-DD. Defaults to yesterday.' },
146
+ zone: { type: 'string', description: 'Optional zone filter. Default: all 11 zones.' },
147
+ },
148
+ },
149
+ },
150
+ ];
151
+
152
+ // ── CAISO ────────────────────────────────────────────────────────────
153
+
154
+ async function caisoOutlook(file: string): Promise<{ headers: string[]; rows: Record<string, string | number>[] }> {
155
+ const url = `https://www.caiso.com/outlook/current/${file}.csv`;
156
+ const res = await fetch(url, { headers: { Accept: 'text/csv', 'User-Agent': UA } });
157
+ if (!res.ok) throw new Error(`CAISO ${file}: ${res.status}`);
158
+ const text = await res.text();
159
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
160
+ if (lines.length < 2) return { headers: [], rows: [] };
161
+ const headers = lines[0].split(',').map((h) => h.trim());
162
+ const rows: Record<string, string | number>[] = [];
163
+ for (const line of lines.slice(1)) {
164
+ const cells = line.split(',');
165
+ // Filter intervals where every non-time cell is empty/0 (future).
166
+ let hasData = false;
167
+ for (let i = 1; i < cells.length; i++) {
168
+ if (cells[i] && cells[i] !== '0') { hasData = true; break; }
169
+ }
170
+ if (!hasData) continue;
171
+ const row: Record<string, string | number> = {};
172
+ for (let i = 0; i < headers.length; i++) {
173
+ const v = (cells[i] ?? '').trim();
174
+ row[headers[i]] = i === 0 ? v : (v === '' ? 0 : Number(v));
175
+ }
176
+ rows.push(row);
177
+ }
178
+ return { headers, rows };
179
+ }
180
+
181
+ async function caisoLimited(file: string, limit: number) {
182
+ const { headers, rows } = await caisoOutlook(file);
183
+ const tail = rows.slice(-Math.max(1, Math.min(288, limit))); // 288 = full day at 5-min
184
+ return { source: 'CAISO outlook', file, columns: headers, count: tail.length, intervals: tail };
185
+ }
186
+
187
+ // ── ERCOT ────────────────────────────────────────────────────────────
188
+
189
+ async function ercotDashboard(name: string): Promise<unknown> {
190
+ const url = `https://www.ercot.com/api/1/services/read/dashboards/${name}.json`;
191
+ const res = await fetch(url, { headers: { Accept: 'application/json', 'User-Agent': UA } });
192
+ if (!res.ok) throw new Error(`ERCOT ${name}: ${res.status}`);
193
+ return res.json();
194
+ }
195
+
196
+ // ── NYISO ────────────────────────────────────────────────────────────
197
+
198
+ function nyisoDate(input: string | undefined): string {
199
+ if (input && /^\d{4}-\d{2}-\d{2}$/.test(input)) return input.replace(/-/g, '');
200
+ // Default: yesterday in UTC (close enough; NYISO publishes after midnight ET).
201
+ const d = new Date(Date.now() - 24 * 60 * 60 * 1000);
202
+ return `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, '0')}${String(d.getUTCDate()).padStart(2, '0')}`;
203
+ }
204
+
205
+ // NYISO directory and file basename are not always identical (e.g. dir
206
+ // "/realtime/" → file "<date>realtime_zone.csv"). Pass both explicitly.
207
+ async function nyisoCsv(dir: string, base: string, yyyymmdd: string): Promise<{ headers: string[]; rows: string[][] }> {
208
+ const url = `http://mis.nyiso.com/public/csv/${dir}/${yyyymmdd}${base}.csv`;
209
+ const res = await fetch(url, { headers: { Accept: 'text/csv', 'User-Agent': UA } });
210
+ if (res.status === 404) throw new Error(`NYISO ${dir} not yet published for ${yyyymmdd}. Try the previous day.`);
211
+ if (!res.ok) throw new Error(`NYISO ${dir}: ${res.status}`);
212
+ return parseQuotedCsv(await res.text());
213
+ }
214
+
215
+ // Minimal RFC-4180-ish parser: quoted fields, embedded commas, escaped quotes.
216
+ function parseQuotedCsv(text: string): { headers: string[]; rows: string[][] } {
217
+ const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
218
+ if (lines.length < 1) return { headers: [], rows: [] };
219
+ const parseLine = (line: string): string[] => {
220
+ const out: string[] = [];
221
+ let buf = '';
222
+ let inQ = false;
223
+ for (let i = 0; i < line.length; i++) {
224
+ const c = line[i];
225
+ if (inQ) {
226
+ if (c === '"') {
227
+ if (line[i + 1] === '"') { buf += '"'; i++; } else { inQ = false; }
228
+ } else buf += c;
229
+ } else {
230
+ if (c === '"') inQ = true;
231
+ else if (c === ',') { out.push(buf); buf = ''; }
232
+ else buf += c;
233
+ }
234
+ }
235
+ out.push(buf);
236
+ return out;
237
+ };
238
+ const headers = parseLine(lines[0]).map((h) => h.trim());
239
+ const rows = lines.slice(1).map(parseLine);
240
+ return { headers, rows };
241
+ }
242
+
243
+ // ── Tool dispatch ────────────────────────────────────────────────────
244
+
245
+ async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
246
+ switch (name) {
247
+ case 'caiso_fuel_mix':
248
+ return caisoLimited('fuelsource', (args.limit as number | undefined) ?? 12);
249
+ case 'caiso_demand':
250
+ return caisoLimited('demand', (args.limit as number | undefined) ?? 12);
251
+ case 'caiso_renewables':
252
+ return caisoLimited('renewables', (args.limit as number | undefined) ?? 12);
253
+ case 'caiso_co2':
254
+ return caisoLimited('co2', (args.limit as number | undefined) ?? 12);
255
+
256
+ case 'ercot_fuel_mix': {
257
+ const raw = (await ercotDashboard('fuel-mix')) as {
258
+ lastUpdated?: string;
259
+ monthlyCapacity?: Record<string, number>;
260
+ types?: string[];
261
+ data?: Record<string, Record<string, Record<string, { gen?: number }>>>;
262
+ };
263
+ const days = raw.data ?? {};
264
+ const dayKeys = Object.keys(days).sort();
265
+ const lastDay = dayKeys[dayKeys.length - 1];
266
+ const intervals = lastDay ? Object.keys(days[lastDay]).sort() : [];
267
+ const lastInterval = intervals[intervals.length - 1];
268
+ const snapshot = lastDay && lastInterval ? days[lastDay][lastInterval] : null;
269
+ return {
270
+ source: 'ERCOT fuel-mix dashboard',
271
+ last_updated: raw.lastUpdated,
272
+ monthly_installed_capacity_mw: raw.monthlyCapacity ?? null,
273
+ latest_timestamp: lastInterval ?? null,
274
+ latest_generation_mw: snapshot
275
+ ? Object.fromEntries(Object.entries(snapshot).map(([k, v]) => [k, v.gen ?? null]))
276
+ : null,
277
+ };
278
+ }
279
+ case 'ercot_supply_demand': {
280
+ const raw = (await ercotDashboard('supply-demand')) as {
281
+ lastUpdated?: string;
282
+ data?: Array<Record<string, number | string>>;
283
+ forecast?: Array<Record<string, number | string>>;
284
+ };
285
+ return {
286
+ source: 'ERCOT supply-demand dashboard',
287
+ last_updated: raw.lastUpdated,
288
+ recent: (raw.data ?? []).slice(-24),
289
+ forecast: (raw.forecast ?? []).slice(0, 24),
290
+ };
291
+ }
292
+
293
+ case 'nyiso_lmp_zonal': {
294
+ const date = nyisoDate(args.date as string | undefined);
295
+ const zone = (args.zone as string | undefined)?.trim();
296
+ const { headers, rows } = await nyisoCsv('realtime', 'realtime_zone', date);
297
+ const filtered = zone ? rows.filter((r) => r[1] === zone) : rows;
298
+ const intervals = filtered.map((r) => ({
299
+ timestamp: r[0],
300
+ zone: r[1],
301
+ ptid: Number(r[2]),
302
+ lbmp_per_mwh: Number(r[3]),
303
+ loss_cost_per_mwh: Number(r[4]),
304
+ congestion_cost_per_mwh: Number(r[5]),
305
+ }));
306
+ return { source: 'NYISO realtime LBMP', report_date: date, columns: headers, count: intervals.length, intervals };
307
+ }
308
+ case 'nyiso_fuel_mix': {
309
+ const date = nyisoDate(args.date as string | undefined);
310
+ const { headers, rows } = await nyisoCsv('rtfuelmix', 'rtfuelmix', date);
311
+ const intervals = rows.map((r) => ({
312
+ timestamp: r[0],
313
+ time_zone: r[1],
314
+ fuel_category: r[2],
315
+ generation_mw: Number(r[3]),
316
+ }));
317
+ return { source: 'NYISO real-time fuel mix', report_date: date, columns: headers, count: intervals.length, intervals };
318
+ }
319
+ case 'nyiso_load': {
320
+ const date = nyisoDate(args.date as string | undefined);
321
+ const zone = (args.zone as string | undefined)?.trim();
322
+ const { headers, rows } = await nyisoCsv('pal', 'pal', date);
323
+ const filtered = zone ? rows.filter((r) => r[2] === zone) : rows;
324
+ const intervals = filtered.map((r) => ({
325
+ timestamp: r[0],
326
+ time_zone: r[1],
327
+ zone: r[2],
328
+ ptid: Number(r[3]),
329
+ load_mw: Number(r[4]),
330
+ }));
331
+ return { source: 'NYISO real-time zonal load', report_date: date, columns: headers, count: intervals.length, intervals };
332
+ }
333
+ case 'nyiso_load_forecast': {
334
+ const date = nyisoDate(args.date as string | undefined);
335
+ const zone = (args.zone as string | undefined)?.trim();
336
+ const { headers, rows } = await nyisoCsv('isolf', 'isolf', date);
337
+ const zoneIdx = zone ? headers.findIndex((h) => h.toLowerCase() === zone.toLowerCase()) : -1;
338
+ const intervals = rows.map((r) => {
339
+ const out: Record<string, string | number> = { timestamp: r[0] };
340
+ if (zoneIdx > 0) {
341
+ out[headers[zoneIdx]] = Number(r[zoneIdx]);
342
+ } else {
343
+ for (let i = 1; i < headers.length; i++) out[headers[i]] = Number(r[i]);
344
+ }
345
+ return out;
346
+ });
347
+ return { source: 'NYISO ISO load forecast (ISOLF)', report_date: date, columns: headers, count: intervals.length, intervals };
348
+ }
349
+
350
+ default:
351
+ throw new Error(`Unknown tool: ${name}`);
352
+ }
353
+ }
354
+
355
+ export default { tools, callTool, meter: { credits: 1 } } satisfies McpToolExport;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }