@energyatit/mcp-server 0.2.3 → 0.3.1

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 (2) hide show
  1. package/dist/index.js +81 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
+ const VERSION = "0.3.1";
5
6
  // ─── Config ────────────────────────────────────────────────────────────────
6
7
  const BASE_URL = (process.env.ENERGYATIT_BASE_URL ??
7
8
  process.env.ENERGYATIT_URL ??
@@ -9,7 +10,7 @@ const BASE_URL = (process.env.ENERGYATIT_BASE_URL ??
9
10
  const API_KEY = process.env.ENERGYATIT_API_KEY ?? "";
10
11
  const TOKEN = process.env.ENERGYATIT_TOKEN ?? "";
11
12
  const JOURNEY_TOKEN = process.env.ENERGYATIT_JOURNEY_TOKEN ?? "";
12
- // ─── Demo Mode ──────────────────────────────────────────────────────────────
13
+ // ─── Auth ──────────────────────────────────────────────────────────────────
13
14
  const demoMode = !API_KEY && !TOKEN && !JOURNEY_TOKEN;
14
15
  let sessionApiKey = "";
15
16
  function authHeaders() {
@@ -24,7 +25,8 @@ function authHeaders() {
24
25
  h["X-API-Key"] = sessionApiKey;
25
26
  return h;
26
27
  }
27
- // Demo path mapping: read-only tools use public /demo/* routes when in demo mode
28
+ // ─── Demo path mapping ─────────────────────────────────────────────────────
29
+ // Read-only tools in demo mode use public /demo/* or already-public endpoints.
28
30
  const DEMO_PATH_MAP = {
29
31
  "/api/sites": "/api/v1/demo/sites",
30
32
  "/api/assets": "/api/v1/demo/assets",
@@ -34,16 +36,32 @@ const DEMO_PATH_MAP = {
34
36
  "/api/v1/dr/events": "/api/v1/demo/dr/events",
35
37
  "/api/v1/integrations/status": "/api/v1/demo/integrations/status",
36
38
  "/api/v1/carbon": "/api/v1/demo/carbon",
39
+ "/api/v1/carbon/verify": "/api/v1/demo/carbon/verify",
40
+ "/api/v1/carbon/certificate": "/api/v1/demo/carbon/certificate",
37
41
  };
42
+ // These paths are already public on the server — no rewrite needed, no auth needed.
43
+ const PUBLIC_PATHS = [
44
+ "/api/health",
45
+ "/api/v1/health",
46
+ "/api/v1/status",
47
+ "/api/v1/pricing",
48
+ "/api/v1/intel/grid/", // prefix — grid capacity + trends are public
49
+ "/api/public/", // prefix — all public routes
50
+ ];
51
+ function isPublicPath(path) {
52
+ const basePath = path.split("?")[0];
53
+ return PUBLIC_PATHS.some(p => basePath === p || basePath.startsWith(p));
54
+ }
38
55
  function resolveDemoPath(path) {
39
56
  if (!demoMode)
40
57
  return path;
41
- // Check exact match first
58
+ if (isPublicPath(path))
59
+ return path; // already public, no rewrite
42
60
  const basePath = path.split("?")[0];
43
61
  const qs = path.includes("?") ? path.slice(path.indexOf("?")) : "";
44
62
  if (DEMO_PATH_MAP[basePath])
45
63
  return DEMO_PATH_MAP[basePath] + qs;
46
- // Check prefix matches (e.g. /api/sites/123)
64
+ // Check prefix matches (e.g. /api/sites/123 → /api/v1/demo/sites/123)
47
65
  for (const [from, to] of Object.entries(DEMO_PATH_MAP)) {
48
66
  if (basePath.startsWith(from + "/")) {
49
67
  return to + basePath.slice(from.length) + qs;
@@ -70,45 +88,64 @@ async function autoProvisionSandbox() {
70
88
  method: "POST",
71
89
  headers: { "Content-Type": "application/json" },
72
90
  });
73
- const json = await res.json();
74
- if (json.success && json.data) {
75
- const data = json.data;
76
- if (data.apiKey && typeof data.apiKey === "string") {
77
- sessionApiKey = data.apiKey;
78
- console.error(`[demo] Auto-provisioned sandbox (key: ${sessionApiKey.slice(0, 20)}...)`);
79
- return true;
80
- }
91
+ if (!res.ok) {
92
+ console.error(`[demo] Sandbox provision HTTP ${res.status}`);
93
+ return false;
94
+ }
95
+ const json = await safeJson(res);
96
+ if (json?.success && json.data?.apiKey) {
97
+ sessionApiKey = json.data.apiKey;
98
+ console.error(`[demo] Auto-provisioned sandbox (key: ${sessionApiKey.slice(0, 20)}...)`);
99
+ return true;
81
100
  }
82
- console.error("[demo] Sandbox provision returned unexpected response:", json);
101
+ console.error("[demo] Sandbox provision: unexpected response");
83
102
  return false;
84
103
  }
85
104
  catch (err) {
86
- console.error("[demo] Failed to auto-provision sandbox:", err);
105
+ console.error("[demo] Sandbox provision failed:", err instanceof Error ? err.message : err);
87
106
  return false;
88
107
  }
89
108
  }
109
+ /** Safely parse JSON — returns null instead of throwing on HTML or malformed responses. */
110
+ async function safeJson(res) {
111
+ const ct = res.headers.get("content-type") ?? "";
112
+ if (!ct.includes("json")) {
113
+ // Server returned HTML (SPA catch-all) or other non-JSON — route doesn't exist or auth redirect
114
+ return null;
115
+ }
116
+ try {
117
+ return await res.json();
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
90
123
  async function apiGet(path) {
91
124
  const resolvedPath = resolveDemoPath(path);
92
- const res = await fetch(`${BASE_URL}${resolvedPath}`, { headers: authHeaders() });
93
- // Auto-provision on 401 in demo mode
125
+ const url = `${BASE_URL}${resolvedPath}`;
126
+ const headers = isPublicPath(resolvedPath) && demoMode ? { "Content-Type": "application/json" } : authHeaders();
127
+ let res = await fetch(url, { headers });
128
+ // Auto-provision on 401 in demo mode (for non-public paths)
94
129
  if (res.status === 401 && demoMode && !sessionApiKey) {
95
130
  const provisioned = await autoProvisionSandbox();
96
131
  if (provisioned) {
97
- // Retry with the new key
98
- const retry = await fetch(`${BASE_URL}${resolvedPath}`, { headers: authHeaders() });
99
- const retryJson = await retry.json();
100
- if (retryJson.success === false)
101
- throw new Error(String(retryJson.error ?? `API error ${retry.status}`));
102
- return retryJson.data ?? retryJson;
132
+ res = await fetch(url, { headers: authHeaders() });
103
133
  }
104
134
  }
105
- const json = await res.json();
135
+ if (!res.ok) {
136
+ const json = await safeJson(res);
137
+ const msg = json?.error ? String(json.error) : `HTTP ${res.status} ${res.statusText}`;
138
+ throw new Error(msg);
139
+ }
140
+ const json = await safeJson(res);
141
+ if (!json)
142
+ throw new Error(`Non-JSON response from ${resolvedPath}`);
106
143
  if (json.success === false)
107
- throw new Error(String(json.error ?? `API error ${res.status}`));
144
+ throw new Error(String(json.error ?? "Unknown API error"));
108
145
  return json.data ?? json;
109
146
  }
110
147
  async function apiPost(path, body) {
111
- const res = await fetch(`${BASE_URL}${path}`, {
148
+ let res = await fetch(`${BASE_URL}${path}`, {
112
149
  method: "POST",
113
150
  headers: authHeaders(),
114
151
  body: body ? JSON.stringify(body) : undefined,
@@ -117,31 +154,23 @@ async function apiPost(path, body) {
117
154
  if (res.status === 401 && demoMode && !sessionApiKey) {
118
155
  const provisioned = await autoProvisionSandbox();
119
156
  if (provisioned) {
120
- const retry = await fetch(`${BASE_URL}${path}`, {
157
+ res = await fetch(`${BASE_URL}${path}`, {
121
158
  method: "POST",
122
159
  headers: authHeaders(),
123
160
  body: body ? JSON.stringify(body) : undefined,
124
161
  });
125
- const retryJson = await retry.json();
126
- if (retryJson.success === false)
127
- throw new Error(String(retryJson.error ?? `API error ${retry.status}`));
128
- return retryJson.data ?? retryJson;
129
162
  }
130
163
  }
131
- const json = await res.json();
132
- if (json.success === false)
133
- throw new Error(String(json.error ?? `API error ${res.status}`));
134
- return json.data ?? json;
135
- }
136
- async function apiPatch(path, body) {
137
- const res = await fetch(`${BASE_URL}${path}`, {
138
- method: "PATCH",
139
- headers: authHeaders(),
140
- body: body ? JSON.stringify(body) : undefined,
141
- });
142
- const json = await res.json();
164
+ if (!res.ok) {
165
+ const json = await safeJson(res);
166
+ const msg = json?.error ? String(json.error) : `HTTP ${res.status} ${res.statusText}`;
167
+ throw new Error(msg);
168
+ }
169
+ const json = await safeJson(res);
170
+ if (!json)
171
+ throw new Error(`Non-JSON response from ${path}`);
143
172
  if (json.success === false)
144
- throw new Error(String(json.error ?? `API error ${res.status}`));
173
+ throw new Error(String(json.error ?? "Unknown API error"));
145
174
  return json.data ?? json;
146
175
  }
147
176
  function text(data) {
@@ -153,7 +182,7 @@ function errorResult(err) {
153
182
  // ─── MCP Server ────────────────────────────────────────────────────────────
154
183
  const server = new McpServer({
155
184
  name: "energyatit",
156
- version: "0.2.1",
185
+ version: VERSION,
157
186
  });
158
187
  // ── Sites ────────────────────────────────────────────────────────────────
159
188
  server.tool("list_sites", "List all energy sites in your tenant", {}, async () => {
@@ -479,7 +508,13 @@ server.tool("create_procurement", "Create an energy procurement request", {
479
508
  region: z.string().optional().describe("Region"),
480
509
  }, async (params) => {
481
510
  try {
482
- return text(await apiPost("/api/v1/procurement", params));
511
+ // Map MCP params to server API format
512
+ const body = {
513
+ region: params.region ?? "UAE",
514
+ requiredMw: Math.round(params.volume_kwh / 1000) / 1000, // kWh → MW
515
+ workloadType: params.type === "ppa" ? "ai_training" : params.type === "rec" ? "general_compute" : "inference",
516
+ };
517
+ return text(await apiPost("/api/v1/procurement", body));
483
518
  }
484
519
  catch (e) {
485
520
  return errorResult(e);
@@ -585,7 +620,7 @@ async function main() {
585
620
  console.error("Running in demo mode — read-only tools use public endpoints, write tools auto-provision sandbox");
586
621
  console.error("Set ENERGYATIT_API_KEY, ENERGYATIT_TOKEN, or ENERGYATIT_JOURNEY_TOKEN for full access.");
587
622
  }
588
- console.error(`EnergyAtIt MCP server v0.2.1 — connecting to ${BASE_URL}`);
623
+ console.error(`EnergyAtIt MCP server v${VERSION} — connecting to ${BASE_URL}`);
589
624
  const transport = new StdioServerTransport();
590
625
  await server.connect(transport);
591
626
  console.error("EnergyAtIt MCP server running on stdio");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energyatit/mcp-server",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "MCP server for EnergyAtIt — connect Claude, GPT, or any MCP client to energy grid data",
5
5
  "type": "module",
6
6
  "bin": {