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