@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.
- package/dist/index.js +72 -45
- 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
|
-
// ───
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
99
|
+
console.error("[demo] Sandbox provision: unexpected response");
|
|
83
100
|
return false;
|
|
84
101
|
}
|
|
85
102
|
catch (err) {
|
|
86
|
-
console.error("[demo]
|
|
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
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 ??
|
|
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:
|
|
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
|
|
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");
|