@energyatit/mcp-server 0.2.3 → 0.3.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 (2) hide show
  1. package/dist/index.js +72 -45
  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.0";
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",
@@ -35,15 +37,29 @@ const DEMO_PATH_MAP = {
35
37
  "/api/v1/integrations/status": "/api/v1/demo/integrations/status",
36
38
  "/api/v1/carbon": "/api/v1/demo/carbon",
37
39
  };
40
+ // These paths are already public on the server — no rewrite needed, no auth needed.
41
+ const PUBLIC_PATHS = [
42
+ "/api/health",
43
+ "/api/v1/health",
44
+ "/api/v1/status",
45
+ "/api/v1/pricing",
46
+ "/api/v1/intel/grid/", // prefix — grid capacity + trends are public
47
+ "/api/public/", // prefix — all public routes
48
+ ];
49
+ function isPublicPath(path) {
50
+ const basePath = path.split("?")[0];
51
+ return PUBLIC_PATHS.some(p => basePath === p || basePath.startsWith(p));
52
+ }
38
53
  function resolveDemoPath(path) {
39
54
  if (!demoMode)
40
55
  return path;
41
- // Check exact match first
56
+ if (isPublicPath(path))
57
+ return path; // already public, no rewrite
42
58
  const basePath = path.split("?")[0];
43
59
  const qs = path.includes("?") ? path.slice(path.indexOf("?")) : "";
44
60
  if (DEMO_PATH_MAP[basePath])
45
61
  return DEMO_PATH_MAP[basePath] + qs;
46
- // Check prefix matches (e.g. /api/sites/123)
62
+ // Check prefix matches (e.g. /api/sites/123 → /api/v1/demo/sites/123)
47
63
  for (const [from, to] of Object.entries(DEMO_PATH_MAP)) {
48
64
  if (basePath.startsWith(from + "/")) {
49
65
  return to + basePath.slice(from.length) + qs;
@@ -70,45 +86,64 @@ async function autoProvisionSandbox() {
70
86
  method: "POST",
71
87
  headers: { "Content-Type": "application/json" },
72
88
  });
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
- }
89
+ if (!res.ok) {
90
+ console.error(`[demo] Sandbox provision HTTP ${res.status}`);
91
+ return false;
92
+ }
93
+ const json = await safeJson(res);
94
+ if (json?.success && json.data?.apiKey) {
95
+ sessionApiKey = json.data.apiKey;
96
+ console.error(`[demo] Auto-provisioned sandbox (key: ${sessionApiKey.slice(0, 20)}...)`);
97
+ return true;
81
98
  }
82
- console.error("[demo] Sandbox provision returned unexpected response:", json);
99
+ console.error("[demo] Sandbox provision: unexpected response");
83
100
  return false;
84
101
  }
85
102
  catch (err) {
86
- console.error("[demo] Failed to auto-provision sandbox:", err);
103
+ console.error("[demo] Sandbox provision failed:", err instanceof Error ? err.message : err);
87
104
  return false;
88
105
  }
89
106
  }
107
+ /** Safely parse JSON — returns null instead of throwing on HTML or malformed responses. */
108
+ async function safeJson(res) {
109
+ const ct = res.headers.get("content-type") ?? "";
110
+ if (!ct.includes("json")) {
111
+ // Server returned HTML (SPA catch-all) or other non-JSON — route doesn't exist or auth redirect
112
+ return null;
113
+ }
114
+ try {
115
+ return await res.json();
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
90
121
  async function apiGet(path) {
91
122
  const resolvedPath = resolveDemoPath(path);
92
- const res = await fetch(`${BASE_URL}${resolvedPath}`, { headers: authHeaders() });
93
- // Auto-provision on 401 in demo mode
123
+ const url = `${BASE_URL}${resolvedPath}`;
124
+ const headers = isPublicPath(resolvedPath) && demoMode ? { "Content-Type": "application/json" } : authHeaders();
125
+ let res = await fetch(url, { headers });
126
+ // Auto-provision on 401 in demo mode (for non-public paths)
94
127
  if (res.status === 401 && demoMode && !sessionApiKey) {
95
128
  const provisioned = await autoProvisionSandbox();
96
129
  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;
130
+ res = await fetch(url, { headers: authHeaders() });
103
131
  }
104
132
  }
105
- const json = await res.json();
133
+ if (!res.ok) {
134
+ const json = await safeJson(res);
135
+ const msg = json?.error ? String(json.error) : `HTTP ${res.status} ${res.statusText}`;
136
+ throw new Error(msg);
137
+ }
138
+ const json = await safeJson(res);
139
+ if (!json)
140
+ throw new Error(`Non-JSON response from ${resolvedPath}`);
106
141
  if (json.success === false)
107
- throw new Error(String(json.error ?? `API error ${res.status}`));
142
+ throw new Error(String(json.error ?? "Unknown API error"));
108
143
  return json.data ?? json;
109
144
  }
110
145
  async function apiPost(path, body) {
111
- const res = await fetch(`${BASE_URL}${path}`, {
146
+ let res = await fetch(`${BASE_URL}${path}`, {
112
147
  method: "POST",
113
148
  headers: authHeaders(),
114
149
  body: body ? JSON.stringify(body) : undefined,
@@ -117,31 +152,23 @@ async function apiPost(path, body) {
117
152
  if (res.status === 401 && demoMode && !sessionApiKey) {
118
153
  const provisioned = await autoProvisionSandbox();
119
154
  if (provisioned) {
120
- const retry = await fetch(`${BASE_URL}${path}`, {
155
+ res = await fetch(`${BASE_URL}${path}`, {
121
156
  method: "POST",
122
157
  headers: authHeaders(),
123
158
  body: body ? JSON.stringify(body) : undefined,
124
159
  });
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
160
  }
130
161
  }
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();
162
+ if (!res.ok) {
163
+ const json = await safeJson(res);
164
+ const msg = json?.error ? String(json.error) : `HTTP ${res.status} ${res.statusText}`;
165
+ throw new Error(msg);
166
+ }
167
+ const json = await safeJson(res);
168
+ if (!json)
169
+ throw new Error(`Non-JSON response from ${path}`);
143
170
  if (json.success === false)
144
- throw new Error(String(json.error ?? `API error ${res.status}`));
171
+ throw new Error(String(json.error ?? "Unknown API error"));
145
172
  return json.data ?? json;
146
173
  }
147
174
  function text(data) {
@@ -153,7 +180,7 @@ function errorResult(err) {
153
180
  // ─── MCP Server ────────────────────────────────────────────────────────────
154
181
  const server = new McpServer({
155
182
  name: "energyatit",
156
- version: "0.2.1",
183
+ version: VERSION,
157
184
  });
158
185
  // ── Sites ────────────────────────────────────────────────────────────────
159
186
  server.tool("list_sites", "List all energy sites in your tenant", {}, async () => {
@@ -585,7 +612,7 @@ async function main() {
585
612
  console.error("Running in demo mode — read-only tools use public endpoints, write tools auto-provision sandbox");
586
613
  console.error("Set ENERGYATIT_API_KEY, ENERGYATIT_TOKEN, or ENERGYATIT_JOURNEY_TOKEN for full access.");
587
614
  }
588
- console.error(`EnergyAtIt MCP server v0.2.1 — connecting to ${BASE_URL}`);
615
+ console.error(`EnergyAtIt MCP server v${VERSION} — connecting to ${BASE_URL}`);
589
616
  const transport = new StdioServerTransport();
590
617
  await server.connect(transport);
591
618
  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.0",
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": {