@iflow-mcp/kitfunso-luminus 0.2.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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +488 -0
  5. package/dist/lib/audit.d.ts +3 -0
  6. package/dist/lib/audit.js +66 -0
  7. package/dist/lib/auth.d.ts +26 -0
  8. package/dist/lib/auth.js +199 -0
  9. package/dist/lib/cache.d.ts +25 -0
  10. package/dist/lib/cache.js +38 -0
  11. package/dist/lib/cli.d.ts +1 -0
  12. package/dist/lib/cli.js +10 -0
  13. package/dist/lib/corine.d.ts +31 -0
  14. package/dist/lib/corine.js +137 -0
  15. package/dist/lib/eea-natura2000.d.ts +7 -0
  16. package/dist/lib/eea-natura2000.js +53 -0
  17. package/dist/lib/entsoe-client.d.ts +22 -0
  18. package/dist/lib/entsoe-client.js +69 -0
  19. package/dist/lib/gis-sources.d.ts +33 -0
  20. package/dist/lib/gis-sources.js +392 -0
  21. package/dist/lib/natural-england.d.ts +27 -0
  22. package/dist/lib/natural-england.js +105 -0
  23. package/dist/lib/neso-gsp.d.ts +18 -0
  24. package/dist/lib/neso-gsp.js +113 -0
  25. package/dist/lib/overpass.d.ts +13 -0
  26. package/dist/lib/overpass.js +193 -0
  27. package/dist/lib/profiles.d.ts +23 -0
  28. package/dist/lib/profiles.js +149 -0
  29. package/dist/lib/schema-guard.d.ts +22 -0
  30. package/dist/lib/schema-guard.js +38 -0
  31. package/dist/lib/tool-handler.d.ts +15 -0
  32. package/dist/lib/tool-handler.js +95 -0
  33. package/dist/lib/xml-parser.d.ts +4 -0
  34. package/dist/lib/xml-parser.js +34 -0
  35. package/dist/lib/zone-codes.d.ts +12 -0
  36. package/dist/lib/zone-codes.js +127 -0
  37. package/dist/tools/acer-remit.d.ts +60 -0
  38. package/dist/tools/acer-remit.js +154 -0
  39. package/dist/tools/agricultural-land.d.ts +31 -0
  40. package/dist/tools/agricultural-land.js +210 -0
  41. package/dist/tools/ancillary-prices.d.ts +27 -0
  42. package/dist/tools/ancillary-prices.js +70 -0
  43. package/dist/tools/auctions.d.ts +15 -0
  44. package/dist/tools/auctions.js +89 -0
  45. package/dist/tools/balancing-actions.d.ts +22 -0
  46. package/dist/tools/balancing-actions.js +151 -0
  47. package/dist/tools/balancing.d.ts +21 -0
  48. package/dist/tools/balancing.js +56 -0
  49. package/dist/tools/carbon.d.ts +21 -0
  50. package/dist/tools/carbon.js +68 -0
  51. package/dist/tools/commodity-prices.d.ts +26 -0
  52. package/dist/tools/commodity-prices.js +100 -0
  53. package/dist/tools/compare-sites.d.ts +41 -0
  54. package/dist/tools/compare-sites.js +237 -0
  55. package/dist/tools/demand-forecast.d.ts +21 -0
  56. package/dist/tools/demand-forecast.js +56 -0
  57. package/dist/tools/elexon-bmrs.d.ts +72 -0
  58. package/dist/tools/elexon-bmrs.js +117 -0
  59. package/dist/tools/energi-data.d.ts +72 -0
  60. package/dist/tools/energi-data.js +170 -0
  61. package/dist/tools/energy-charts.d.ts +103 -0
  62. package/dist/tools/energy-charts.js +411 -0
  63. package/dist/tools/entsog.d.ts +71 -0
  64. package/dist/tools/entsog.js +159 -0
  65. package/dist/tools/era5-weather.d.ts +39 -0
  66. package/dist/tools/era5-weather.js +117 -0
  67. package/dist/tools/eu-gas-price.d.ts +38 -0
  68. package/dist/tools/eu-gas-price.js +110 -0
  69. package/dist/tools/fingrid.d.ts +39 -0
  70. package/dist/tools/fingrid.js +158 -0
  71. package/dist/tools/flood-risk.d.ts +33 -0
  72. package/dist/tools/flood-risk.js +166 -0
  73. package/dist/tools/flows.d.ts +23 -0
  74. package/dist/tools/flows.js +61 -0
  75. package/dist/tools/frequency.d.ts +10 -0
  76. package/dist/tools/frequency.js +35 -0
  77. package/dist/tools/gas-storage.d.ts +18 -0
  78. package/dist/tools/gas-storage.js +72 -0
  79. package/dist/tools/generation.d.ts +17 -0
  80. package/dist/tools/generation.js +80 -0
  81. package/dist/tools/grid-connection-intelligence.d.ts +42 -0
  82. package/dist/tools/grid-connection-intelligence.js +122 -0
  83. package/dist/tools/grid-connection-queue.d.ts +64 -0
  84. package/dist/tools/grid-connection-queue.js +198 -0
  85. package/dist/tools/grid-proximity.d.ts +38 -0
  86. package/dist/tools/grid-proximity.js +123 -0
  87. package/dist/tools/hydro-inflows.d.ts +34 -0
  88. package/dist/tools/hydro-inflows.js +114 -0
  89. package/dist/tools/hydro.d.ts +18 -0
  90. package/dist/tools/hydro.js +85 -0
  91. package/dist/tools/imbalance-prices.d.ts +21 -0
  92. package/dist/tools/imbalance-prices.js +56 -0
  93. package/dist/tools/intraday-prices.d.ts +21 -0
  94. package/dist/tools/intraday-prices.js +57 -0
  95. package/dist/tools/intraday-spread.d.ts +24 -0
  96. package/dist/tools/intraday-spread.js +55 -0
  97. package/dist/tools/land-constraints.d.ts +25 -0
  98. package/dist/tools/land-constraints.js +148 -0
  99. package/dist/tools/land-cover.d.ts +18 -0
  100. package/dist/tools/land-cover.js +64 -0
  101. package/dist/tools/lng-terminals.d.ts +22 -0
  102. package/dist/tools/lng-terminals.js +75 -0
  103. package/dist/tools/net-positions.d.ts +19 -0
  104. package/dist/tools/net-positions.js +74 -0
  105. package/dist/tools/nordpool-prices.d.ts +29 -0
  106. package/dist/tools/nordpool-prices.js +80 -0
  107. package/dist/tools/outages.d.ts +28 -0
  108. package/dist/tools/outages.js +107 -0
  109. package/dist/tools/power-plants.d.ts +26 -0
  110. package/dist/tools/power-plants.js +224 -0
  111. package/dist/tools/price-spread-analysis.d.ts +27 -0
  112. package/dist/tools/price-spread-analysis.js +97 -0
  113. package/dist/tools/prices.d.ts +23 -0
  114. package/dist/tools/prices.js +79 -0
  115. package/dist/tools/realtime-generation.d.ts +19 -0
  116. package/dist/tools/realtime-generation.js +141 -0
  117. package/dist/tools/ree-esios.d.ts +78 -0
  118. package/dist/tools/ree-esios.js +216 -0
  119. package/dist/tools/regelleistung.d.ts +28 -0
  120. package/dist/tools/regelleistung.js +71 -0
  121. package/dist/tools/remit-messages.d.ts +23 -0
  122. package/dist/tools/remit-messages.js +110 -0
  123. package/dist/tools/renewable-forecast.d.ts +23 -0
  124. package/dist/tools/renewable-forecast.js +75 -0
  125. package/dist/tools/rte-france.d.ts +72 -0
  126. package/dist/tools/rte-france.js +147 -0
  127. package/dist/tools/screen-site.d.ts +50 -0
  128. package/dist/tools/screen-site.js +288 -0
  129. package/dist/tools/site-revenue.d.ts +50 -0
  130. package/dist/tools/site-revenue.js +147 -0
  131. package/dist/tools/smard-data.d.ts +34 -0
  132. package/dist/tools/smard-data.js +155 -0
  133. package/dist/tools/solar.d.ts +23 -0
  134. package/dist/tools/solar.js +69 -0
  135. package/dist/tools/stormglass.d.ts +56 -0
  136. package/dist/tools/stormglass.js +172 -0
  137. package/dist/tools/terna.d.ts +69 -0
  138. package/dist/tools/terna.js +159 -0
  139. package/dist/tools/terrain-analysis.d.ts +19 -0
  140. package/dist/tools/terrain-analysis.js +120 -0
  141. package/dist/tools/transfer-capacity.d.ts +22 -0
  142. package/dist/tools/transfer-capacity.js +61 -0
  143. package/dist/tools/transmission.d.ts +29 -0
  144. package/dist/tools/transmission.js +159 -0
  145. package/dist/tools/uk-carbon.d.ts +51 -0
  146. package/dist/tools/uk-carbon.js +109 -0
  147. package/dist/tools/uk-grid.d.ts +28 -0
  148. package/dist/tools/uk-grid.js +70 -0
  149. package/dist/tools/us-gas.d.ts +30 -0
  150. package/dist/tools/us-gas.js +100 -0
  151. package/dist/tools/verify-gis-sources.d.ts +25 -0
  152. package/dist/tools/verify-gis-sources.js +119 -0
  153. package/dist/tools/weather.d.ts +27 -0
  154. package/dist/tools/weather.js +120 -0
  155. package/package.json +62 -0
@@ -0,0 +1,193 @@
1
+ const OVERPASS_ENDPOINTS = [
2
+ "https://overpass-api.de/api/interpreter",
3
+ "https://overpass.kumi.systems/api/interpreter",
4
+ "https://overpass.private.coffee/api/interpreter",
5
+ ];
6
+ const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
7
+ const MAX_CONCURRENT = 2;
8
+ const MAX_PER_WINDOW = 10;
9
+ const WINDOW_MS = 60_000;
10
+ const REQUEST_TIMEOUT_MS = 30_000;
11
+ const INITIAL_BACKOFF_MS = 500;
12
+ const MAX_BACKOFF_MS = 4_000;
13
+ const FALLBACK_DELAY_MS = 200;
14
+ const state = {
15
+ windowTimestamps: [],
16
+ inFlight: 0,
17
+ queue: [],
18
+ };
19
+ let windowDrainTimer = null;
20
+ /** Exposed for testing only. */
21
+ export function _getOverpassState() {
22
+ return {
23
+ windowTimestamps: state.windowTimestamps,
24
+ inFlight: state.inFlight,
25
+ queueLength: state.queue.length,
26
+ maxConcurrent: MAX_CONCURRENT,
27
+ maxPerWindow: MAX_PER_WINDOW,
28
+ windowMs: WINDOW_MS,
29
+ requestTimeoutMs: REQUEST_TIMEOUT_MS,
30
+ };
31
+ }
32
+ /** Exposed for testing: reset internal state between test runs. */
33
+ export function _resetOverpassState() {
34
+ state.windowTimestamps.length = 0;
35
+ state.inFlight = 0;
36
+ state.queue.length = 0;
37
+ if (windowDrainTimer) {
38
+ clearTimeout(windowDrainTimer);
39
+ windowDrainTimer = null;
40
+ }
41
+ }
42
+ function pruneWindow(now) {
43
+ const cutoff = now - WINDOW_MS;
44
+ while (state.windowTimestamps.length > 0 && state.windowTimestamps[0] < cutoff) {
45
+ state.windowTimestamps.shift();
46
+ }
47
+ }
48
+ function canProceed(now) {
49
+ pruneWindow(now);
50
+ return state.inFlight < MAX_CONCURRENT && state.windowTimestamps.length < MAX_PER_WINDOW;
51
+ }
52
+ function scheduleWindowDrain() {
53
+ if (state.queue.length === 0 || windowDrainTimer) {
54
+ return;
55
+ }
56
+ const now = Date.now();
57
+ pruneWindow(now);
58
+ if (state.windowTimestamps.length < MAX_PER_WINDOW) {
59
+ return;
60
+ }
61
+ const oldest = state.windowTimestamps[0];
62
+ const delayMs = Math.max(1, oldest + WINDOW_MS - now);
63
+ windowDrainTimer = setTimeout(() => {
64
+ windowDrainTimer = null;
65
+ drainQueue();
66
+ }, delayMs);
67
+ }
68
+ function drainQueue() {
69
+ if (windowDrainTimer) {
70
+ clearTimeout(windowDrainTimer);
71
+ windowDrainTimer = null;
72
+ }
73
+ while (state.queue.length > 0 && canProceed(Date.now())) {
74
+ const next = state.queue.shift();
75
+ next?.();
76
+ }
77
+ scheduleWindowDrain();
78
+ }
79
+ async function acquireSlot() {
80
+ if (canProceed(Date.now())) {
81
+ state.inFlight++;
82
+ state.windowTimestamps.push(Date.now());
83
+ return;
84
+ }
85
+ // Wait in queue until a slot opens
86
+ await new Promise((resolve) => {
87
+ state.queue.push(() => {
88
+ state.inFlight++;
89
+ state.windowTimestamps.push(Date.now());
90
+ resolve();
91
+ });
92
+ scheduleWindowDrain();
93
+ });
94
+ }
95
+ function releaseSlot() {
96
+ state.inFlight--;
97
+ drainQueue();
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Error classification
101
+ // ---------------------------------------------------------------------------
102
+ const EXPENSIVE_QUERY_PATTERNS = [
103
+ "runtime error",
104
+ "out of memory",
105
+ "timed out",
106
+ "Query run out of memory",
107
+ "Query timed out",
108
+ "The server is probably too busy",
109
+ "load average",
110
+ ];
111
+ function isQueryTooExpensive(body) {
112
+ const lower = body.toLowerCase();
113
+ return EXPENSIVE_QUERY_PATTERNS.some((p) => lower.includes(p.toLowerCase()));
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Helpers
117
+ // ---------------------------------------------------------------------------
118
+ function sleep(ms) {
119
+ return new Promise((resolve) => setTimeout(resolve, ms));
120
+ }
121
+ function backoffMs(attempt) {
122
+ return Math.min(INITIAL_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS);
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // Public API (signature unchanged)
126
+ // ---------------------------------------------------------------------------
127
+ export async function fetchOverpassJson(query) {
128
+ await acquireSlot();
129
+ try {
130
+ return await fetchWithFallbacks(query);
131
+ }
132
+ finally {
133
+ releaseSlot();
134
+ }
135
+ }
136
+ async function fetchWithFallbacks(query) {
137
+ const errors = [];
138
+ for (let i = 0; i < OVERPASS_ENDPOINTS.length; i++) {
139
+ const endpoint = OVERPASS_ENDPOINTS[i];
140
+ // Brief delay between fallback endpoint attempts (not before the first)
141
+ if (i > 0) {
142
+ await sleep(FALLBACK_DELAY_MS);
143
+ }
144
+ // Exponential backoff: attempt index drives the delay
145
+ if (i > 0) {
146
+ await sleep(backoffMs(i - 1));
147
+ }
148
+ const controller = new AbortController();
149
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
150
+ try {
151
+ const response = await fetch(endpoint, {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
154
+ body: `data=${encodeURIComponent(query)}`,
155
+ signal: controller.signal,
156
+ });
157
+ clearTimeout(timeout);
158
+ if (response.ok) {
159
+ return (await response.json());
160
+ }
161
+ const body = await response.text();
162
+ errors.push(`${endpoint} -> ${response.status}: ${body.slice(0, 180)}`);
163
+ // Non-retryable status: throw immediately, no fallback
164
+ if (!RETRYABLE_STATUS.has(response.status)) {
165
+ if (isQueryTooExpensive(body)) {
166
+ throw new Error(`Overpass query too expensive (${response.status} from ${endpoint}): ${body.slice(0, 300)}`);
167
+ }
168
+ throw new Error(`Overpass API returned ${response.status}: ${body.slice(0, 300)}`);
169
+ }
170
+ }
171
+ catch (err) {
172
+ clearTimeout(timeout);
173
+ // Re-throw non-retryable errors (our own thrown errors above)
174
+ if (err instanceof Error && err.message.startsWith("Overpass")) {
175
+ throw err;
176
+ }
177
+ // AbortController timeout
178
+ if (err instanceof DOMException && err.name === "AbortError") {
179
+ errors.push(`${endpoint} -> timeout after ${REQUEST_TIMEOUT_MS}ms`);
180
+ continue;
181
+ }
182
+ // AbortError can also come as a plain Error in some runtimes
183
+ if (err instanceof Error && err.name === "AbortError") {
184
+ errors.push(`${endpoint} -> timeout after ${REQUEST_TIMEOUT_MS}ms`);
185
+ continue;
186
+ }
187
+ // Network error or other transient failure
188
+ const msg = err instanceof Error ? err.message : String(err);
189
+ errors.push(`${endpoint} -> network error: ${msg}`);
190
+ }
191
+ }
192
+ throw new Error(`Overpass API failed across all endpoints:\n ${errors.join("\n ")}`);
193
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tool profiles for conditional MCP tool registration.
3
+ *
4
+ * Each profile groups tools by use case so callers can register
5
+ * only the subset they need, cutting context window cost from a large
6
+ * all-tools surface to a few hundred tokens for focused profiles.
7
+ */
8
+ /** Profile name -> list of tool names that belong to it. */
9
+ export declare const PROFILES: Readonly<Record<string, readonly string[]>>;
10
+ /**
11
+ * Resolve a profile name to its tool list.
12
+ * Returns `null` for "full" (meaning all tools should be registered).
13
+ * Returns `undefined` if the profile name is unknown.
14
+ */
15
+ export declare function resolveProfile(name: string): string[] | null | undefined;
16
+ /** Return all known profile names (excluding "full"). */
17
+ export declare function getProfileNames(): string[];
18
+ /** One-line description for a profile. Falls back to empty string for unknown profiles. */
19
+ export declare function getProfileDescription(name: string): string;
20
+ /** Check whether a profile name is valid (including "full"). */
21
+ export declare function isValidProfile(name: string): boolean;
22
+ /** Total number of unique tools (derived from all profile entries + unassigned). */
23
+ export declare const TOTAL_TOOLS: number;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Tool profiles for conditional MCP tool registration.
3
+ *
4
+ * Each profile groups tools by use case so callers can register
5
+ * only the subset they need, cutting context window cost from a large
6
+ * all-tools surface to a few hundred tokens for focused profiles.
7
+ */
8
+ /** Profile name -> list of tool names that belong to it. */
9
+ export const PROFILES = {
10
+ trader: [
11
+ 'get_day_ahead_prices',
12
+ 'get_intraday_prices',
13
+ 'get_balancing_prices',
14
+ 'get_imbalance_prices',
15
+ 'get_intraday_da_spread',
16
+ 'get_commodity_prices',
17
+ 'get_price_spread_analysis',
18
+ 'get_nordpool_prices',
19
+ ],
20
+ grid: [
21
+ 'get_cross_border_flows',
22
+ 'get_net_positions',
23
+ 'get_transfer_capacities',
24
+ 'get_outages',
25
+ 'get_eu_frequency',
26
+ 'get_transmission_lines',
27
+ 'get_power_plants',
28
+ 'get_auction_results',
29
+ 'get_remit_messages',
30
+ 'get_acer_remit',
31
+ 'get_grid_connection_queue',
32
+ ],
33
+ generation: [
34
+ 'get_generation_mix',
35
+ 'get_realtime_generation',
36
+ 'get_carbon_intensity',
37
+ 'get_renewable_forecast',
38
+ 'get_demand_forecast',
39
+ 'get_hydro_reservoir',
40
+ ],
41
+ gas: [
42
+ 'get_gas_storage',
43
+ 'get_lng_terminals',
44
+ 'get_us_gas_data',
45
+ 'get_eu_gas_price',
46
+ 'get_entsog_data',
47
+ ],
48
+ renewables: [
49
+ 'get_renewable_forecast',
50
+ 'get_solar_irradiance',
51
+ 'get_weather_forecast',
52
+ 'get_era5_weather',
53
+ 'get_hydro_inflows',
54
+ ],
55
+ uk: [
56
+ 'get_uk_carbon_intensity',
57
+ 'get_uk_grid_demand',
58
+ 'get_elexon_bmrs',
59
+ ],
60
+ bess: [
61
+ 'get_price_spread_analysis',
62
+ 'get_ancillary_prices',
63
+ 'get_balancing_actions',
64
+ 'get_day_ahead_prices',
65
+ 'get_intraday_prices',
66
+ 'estimate_site_revenue',
67
+ ],
68
+ regional: [
69
+ 'get_energy_charts',
70
+ 'get_smard_data',
71
+ 'get_rte_france',
72
+ 'get_energi_data',
73
+ 'get_fingrid_data',
74
+ 'get_regelleistung',
75
+ 'get_terna_data',
76
+ 'get_ree_esios',
77
+ ],
78
+ weather: [
79
+ 'get_weather_forecast',
80
+ 'get_solar_irradiance',
81
+ 'get_era5_weather',
82
+ 'get_stormglass',
83
+ 'get_hydro_inflows',
84
+ ],
85
+ gis: [
86
+ 'get_solar_irradiance',
87
+ 'get_transmission_lines',
88
+ 'get_terrain_analysis',
89
+ 'get_grid_proximity',
90
+ 'get_grid_connection_queue',
91
+ 'get_land_constraints',
92
+ 'get_land_cover',
93
+ 'get_agricultural_land',
94
+ 'get_flood_risk',
95
+ 'get_grid_connection_intelligence',
96
+ 'screen_site',
97
+ 'verify_gis_sources',
98
+ 'compare_sites',
99
+ 'estimate_site_revenue',
100
+ ],
101
+ };
102
+ /** Short descriptions for each profile, used by the discovery tool. */
103
+ const PROFILE_DESCRIPTIONS = {
104
+ trader: 'Day-ahead, intraday, balancing, and commodity prices for energy trading',
105
+ grid: 'Cross-border flows, outages, transmission, auctions, connection-register signals, and grid infrastructure',
106
+ generation: 'Generation mix, real-time output, carbon intensity, and demand forecasts',
107
+ gas: 'Gas storage, LNG terminals, US gas data, EU gas prices, and ENTSOG pipelines',
108
+ renewables: 'Wind/solar forecasts, irradiance, ERA5 reanalysis, and hydro inflows',
109
+ uk: 'UK-specific carbon intensity, grid demand, and Elexon BMRS data',
110
+ bess: 'Battery storage arbitrage: spreads, ancillary prices, and balancing actions',
111
+ regional: 'Country-specific sources: Energy Charts, SMARD, RTE, Energinet, Fingrid, Terna, REE',
112
+ weather: 'Weather forecasts, solar irradiance, ERA5 reanalysis, and marine/offshore data',
113
+ gis: 'GIS site prospecting: solar resource, terrain, grid proximity, GB transmission connection-register signals, GB grid connection intelligence (GSP + TEC + substations), land constraints, land cover, agricultural-land screening, flood-risk screening, composite site screening (GB + EU), multi-site comparison (GB + EU), and source verification',
114
+ full: 'All 54 data tools registered by default, plus 2 meta-tools',
115
+ };
116
+ /**
117
+ * Resolve a profile name to its tool list.
118
+ * Returns `null` for "full" (meaning all tools should be registered).
119
+ * Returns `undefined` if the profile name is unknown.
120
+ */
121
+ export function resolveProfile(name) {
122
+ if (name === 'full')
123
+ return null;
124
+ const tools = PROFILES[name];
125
+ if (!tools)
126
+ return undefined;
127
+ return [...tools];
128
+ }
129
+ /** Return all known profile names (excluding "full"). */
130
+ export function getProfileNames() {
131
+ return Object.keys(PROFILES);
132
+ }
133
+ /** One-line description for a profile. Falls back to empty string for unknown profiles. */
134
+ export function getProfileDescription(name) {
135
+ return PROFILE_DESCRIPTIONS[name] ?? '';
136
+ }
137
+ /** Check whether a profile name is valid (including "full"). */
138
+ export function isValidProfile(name) {
139
+ return name === 'full' || name in PROFILES;
140
+ }
141
+ /** Total number of unique tools (derived from all profile entries + unassigned). */
142
+ export const TOTAL_TOOLS = (() => {
143
+ const allTools = new Set();
144
+ for (const tools of Object.values(PROFILES)) {
145
+ for (const t of tools)
146
+ allTools.add(t);
147
+ }
148
+ return allTools.size;
149
+ })();
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Defensive schema-drift guards for GIS tools that rely on external
3
+ * ArcGIS/API field names. Validates expected fields exist in API responses
4
+ * before parsing, so upstream renames fail loudly instead of silently
5
+ * returning bad data.
6
+ */
7
+ /**
8
+ * Validate that an ArcGIS feature response contains the expected fields.
9
+ * Checks the first feature's `attributes` object. Skips validation when
10
+ * the features array is empty (no data is not a schema problem).
11
+ *
12
+ * @throws Error with provider name and missing field when validation fails.
13
+ */
14
+ export declare function guardArcGisFields(features: Array<{
15
+ attributes: Record<string, unknown>;
16
+ }>, expectedFields: readonly string[], provider: string): void;
17
+ /**
18
+ * Validate that a JSON object has the expected top-level keys.
19
+ *
20
+ * @throws Error with provider name and missing field when validation fails.
21
+ */
22
+ export declare function guardJsonFields(data: Record<string, unknown>, expectedFields: readonly string[], provider: string): void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Defensive schema-drift guards for GIS tools that rely on external
3
+ * ArcGIS/API field names. Validates expected fields exist in API responses
4
+ * before parsing, so upstream renames fail loudly instead of silently
5
+ * returning bad data.
6
+ */
7
+ /**
8
+ * Validate that an ArcGIS feature response contains the expected fields.
9
+ * Checks the first feature's `attributes` object. Skips validation when
10
+ * the features array is empty (no data is not a schema problem).
11
+ *
12
+ * @throws Error with provider name and missing field when validation fails.
13
+ */
14
+ export function guardArcGisFields(features, expectedFields, provider) {
15
+ if (features.length === 0)
16
+ return;
17
+ const attributes = features[0].attributes;
18
+ if (!attributes) {
19
+ throw new Error(`Schema drift detected in ${provider}: first feature has no "attributes" object. The upstream service may have changed its schema.`);
20
+ }
21
+ for (const field of expectedFields) {
22
+ if (!(field in attributes)) {
23
+ throw new Error(`Schema drift detected in ${provider}: expected field "${field}" not found in response. The upstream service may have changed its schema.`);
24
+ }
25
+ }
26
+ }
27
+ /**
28
+ * Validate that a JSON object has the expected top-level keys.
29
+ *
30
+ * @throws Error with provider name and missing field when validation fails.
31
+ */
32
+ export function guardJsonFields(data, expectedFields, provider) {
33
+ for (const field of expectedFields) {
34
+ if (!(field in data)) {
35
+ throw new Error(`Schema drift detected in ${provider}: expected field "${field}" not found in response. The upstream service may have changed its schema.`);
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,15 @@
1
+ import { type z } from "zod";
2
+ interface ToolResult {
3
+ [key: string]: unknown;
4
+ content: Array<{
5
+ type: "text";
6
+ text: string;
7
+ }>;
8
+ isError?: boolean;
9
+ }
10
+ /**
11
+ * Wrap an MCP tool handler with schema validation and consistent error formatting.
12
+ * Keeps tool registration terse while making user-facing failures less cryptic.
13
+ */
14
+ export declare function toolHandler<T extends z.ZodType>(schema: T, handler: (params: z.infer<T>) => Promise<unknown>): (params: unknown) => Promise<ToolResult>;
15
+ export {};
@@ -0,0 +1,95 @@
1
+ import { ZodError } from "zod";
2
+ const DEBUG_ENV_VAR = "LUMINUS_DEBUG";
3
+ const HELP_URL = "https://github.com/kitfunso/luminus#troubleshooting";
4
+ function isDebugEnabled() {
5
+ return process.env[DEBUG_ENV_VAR] === "1";
6
+ }
7
+ function formatZodIssues(error) {
8
+ return error.issues
9
+ .map((issue) => {
10
+ const path = issue.path.length > 0 ? issue.path.join(".") : "input";
11
+ return `${path}: ${issue.message}`;
12
+ })
13
+ .join("; ");
14
+ }
15
+ function normalizeError(error) {
16
+ if (error instanceof ZodError) {
17
+ return {
18
+ summary: `Invalid parameters. ${formatZodIssues(error)}`,
19
+ details: error.message,
20
+ };
21
+ }
22
+ const raw = error instanceof Error ? error.message : String(error);
23
+ const message = raw.trim() || "Unknown error";
24
+ const lower = message.toLowerCase();
25
+ if (lower.includes("environment variable is required") || lower.includes("api key")) {
26
+ return {
27
+ summary: "Configuration error. Add the required API key to your MCP env or .env file, then retry.",
28
+ details: message,
29
+ };
30
+ }
31
+ if (lower.includes("unknown zone") || lower.includes("unknown corridor")) {
32
+ return {
33
+ summary: "Invalid market identifier. Check the zone, corridor, or EIC code and retry.",
34
+ details: message,
35
+ };
36
+ }
37
+ if (lower.includes("no ") && lower.includes(" data")) {
38
+ return {
39
+ summary: "No data returned for that request. Check the market, date range, and source coverage.",
40
+ details: message,
41
+ };
42
+ }
43
+ if (lower.includes("fetch failed") ||
44
+ lower.includes("timeout") ||
45
+ lower.includes("timed out") ||
46
+ lower.includes("econnreset") ||
47
+ lower.includes("socket hang up")) {
48
+ return {
49
+ summary: "Upstream request failed or timed out. Retry shortly. If it keeps happening, enable debug output and inspect the source-specific details.",
50
+ details: message,
51
+ };
52
+ }
53
+ if (lower.includes("returned 4") || lower.includes("returned 5") || lower.includes("status")) {
54
+ return {
55
+ summary: "Upstream source returned an error. The request was valid, but the data provider rejected or failed it.",
56
+ details: message,
57
+ };
58
+ }
59
+ return {
60
+ summary: "Unexpected server error. Retry once. If it persists, enable debug output and inspect the raw error details.",
61
+ details: message,
62
+ };
63
+ }
64
+ function renderErrorMessage(error) {
65
+ const normalized = normalizeError(error);
66
+ const debug = isDebugEnabled();
67
+ if (debug) {
68
+ return `${normalized.summary}\n\nRaw error: ${normalized.details ?? "n/a"}`;
69
+ }
70
+ return `${normalized.summary}\n\nSet ${DEBUG_ENV_VAR}=1 for raw error details. Troubleshooting: ${HELP_URL}`;
71
+ }
72
+ /**
73
+ * Wrap an MCP tool handler with schema validation and consistent error formatting.
74
+ * Keeps tool registration terse while making user-facing failures less cryptic.
75
+ */
76
+ export function toolHandler(schema, handler) {
77
+ return async (params) => {
78
+ try {
79
+ const parsed = schema.parse(params);
80
+ const result = await handler(parsed);
81
+ return {
82
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
83
+ };
84
+ }
85
+ catch (error) {
86
+ if (isDebugEnabled()) {
87
+ console.error("[luminus] tool error", error);
88
+ }
89
+ return {
90
+ content: [{ type: "text", text: renderErrorMessage(error) }],
91
+ isError: true,
92
+ };
93
+ }
94
+ };
95
+ }
@@ -0,0 +1,4 @@
1
+ /** Parse ENTSO-E XML response into JS object */
2
+ export declare function parseXml(xml: string): Record<string, unknown>;
3
+ /** Ensure a value is always an array (handles ENTSO-E single-vs-array quirk) */
4
+ export declare function ensureArray<T>(value: T | T[] | undefined | null): T[];
@@ -0,0 +1,34 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ const parser = new XMLParser({
3
+ ignoreAttributes: false,
4
+ attributeNamePrefix: "@_",
5
+ // ENTSO-E quirk: single-element arrays come back as objects
6
+ // Force arrays for known list elements
7
+ isArray: (_name, jpath) => {
8
+ const arrayPaths = [
9
+ "GL_MarketDocument.TimeSeries",
10
+ "Publication_MarketDocument.TimeSeries",
11
+ "Imbalance_MarketDocument.TimeSeries",
12
+ "Balancing_MarketDocument.TimeSeries",
13
+ "Unavailability_MarketDocument.TimeSeries",
14
+ "UnavailabilityMarketDocument.TimeSeries",
15
+ "TimeSeries.Period",
16
+ "Period.Point",
17
+ "TimeSeries.Available_Period",
18
+ "Available_Period.Point",
19
+ "TimeSeries.MktPSRType.MktGeneratingUnit",
20
+ ];
21
+ const jp = String(jpath);
22
+ return arrayPaths.some((p) => jp.endsWith(p));
23
+ },
24
+ });
25
+ /** Parse ENTSO-E XML response into JS object */
26
+ export function parseXml(xml) {
27
+ return parser.parse(xml);
28
+ }
29
+ /** Ensure a value is always an array (handles ENTSO-E single-vs-array quirk) */
30
+ export function ensureArray(value) {
31
+ if (value == null)
32
+ return [];
33
+ return Array.isArray(value) ? value : [value];
34
+ }
@@ -0,0 +1,12 @@
1
+ /** ENTSO-E EIC area codes for European bidding zones */
2
+ export declare const ZONE_CODES: Record<string, string>;
3
+ /** Resolve a zone string to an EIC code. Accepts both "DE" and raw EIC codes. */
4
+ export declare function resolveZone(zone: string): string;
5
+ /** Resolve zone for price queries (uses bidding zone overrides) */
6
+ export declare function resolvePriceZone(zone: string): string;
7
+ /** All available zone keys for tool descriptions */
8
+ export declare const AVAILABLE_ZONES: string;
9
+ /** Neighbouring zones for net position calculation (cross-border flow pairs) */
10
+ export declare const ZONE_NEIGHBOURS: Record<string, string[]>;
11
+ /** Approximate bounding boxes [lat_min, lon_min, lat_max, lon_max] for Overpass API queries */
12
+ export declare const COUNTRY_BBOXES: Record<string, [number, number, number, number]>;