opencore-okome-mcp 1.0.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/README.md +18 -0
- package/dist/index.js +260 -0
- package/dist/index.test.js +106 -0
- package/dist/mcp-server.js +149 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# OKOME TypeScript MCP Gateway Server
|
|
2
|
+
This directory contains the NodeJS-based TypeScript service proxying local MCP tool definitions and routing API gateway requests.
|
|
3
|
+
|
|
4
|
+
## Directory Structure
|
|
5
|
+
- `src/mcp-server.ts`: Handles registration and execution of MCP tool schemas.
|
|
6
|
+
- `src/index.ts`: Gateway entrypoint script.
|
|
7
|
+
- `package.json`: Dependency specifications.
|
|
8
|
+
- `tsconfig.json`: TypeScript compilation configurations.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
* **Run in Dev Mode**:
|
|
12
|
+
```bash
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
* **Build**:
|
|
16
|
+
```bash
|
|
17
|
+
npm run build
|
|
18
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/****************************************************************************************
|
|
3
|
+
PROJECT : OpenCore QAT Workspace
|
|
4
|
+
PROGRAM : typescript/src/index.ts
|
|
5
|
+
AUTHOR : BP; WL
|
|
6
|
+
REVIEWER : BP
|
|
7
|
+
DATE : 2026-07-11
|
|
8
|
+
UPDATED : 2026-07-12
|
|
9
|
+
LAST UPDATED : 2026-07-12T00:43:00-10:00
|
|
10
|
+
|
|
11
|
+
PURPOSE : TypeScript Multi-Tenant Inference Gateway Proxy managing HTTP routing,
|
|
12
|
+
X-VM-ID validation, and provisioning lifecycle trigger hooks.
|
|
13
|
+
|
|
14
|
+
UPDATES (v1.2 - 2026-07-12) :
|
|
15
|
+
1. AG (2026-07-12T00:43:00-10:00): Added -- Implemented proxy handler for /v1 routes to forward to ollama-gpu.
|
|
16
|
+
Reason: Map OpenCode sessions to the API Gateway to utilize the pooled VRAM models.
|
|
17
|
+
Impact: Enables unified gateway routing for both Codex execution and model sessions.
|
|
18
|
+
|
|
19
|
+
UPDATES (v1.1 - 2026-07-11) :
|
|
20
|
+
1. BP (2026-07-11T15:02:00-10:00): Added standard header block to satisfy forensic
|
|
21
|
+
audit requirements.
|
|
22
|
+
****************************************************************************************/
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
const http = __importStar(require("http"));
|
|
58
|
+
const PORT = 8086;
|
|
59
|
+
const ORCHESTRATOR_URL = process.env.ORCHESTRATOR_URL || "http://127.0.0.1:8085";
|
|
60
|
+
// Helper: make HTTP request and return body as string
|
|
61
|
+
function httpRequest(url, method = "GET", body) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const parsed = new URL(url);
|
|
64
|
+
const options = {
|
|
65
|
+
hostname: parsed.hostname,
|
|
66
|
+
port: parsed.port,
|
|
67
|
+
path: parsed.pathname + parsed.search,
|
|
68
|
+
method,
|
|
69
|
+
headers: body
|
|
70
|
+
? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
|
|
71
|
+
: {},
|
|
72
|
+
};
|
|
73
|
+
const req = http.request(options, (res) => {
|
|
74
|
+
let data = "";
|
|
75
|
+
res.on("data", (chunk) => (data += chunk));
|
|
76
|
+
res.on("end", () => resolve({ status: res.statusCode || 200, body: data }));
|
|
77
|
+
});
|
|
78
|
+
req.on("error", reject);
|
|
79
|
+
if (body)
|
|
80
|
+
req.write(body);
|
|
81
|
+
req.end();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Gateway Proxy Server
|
|
85
|
+
const server = http.createServer(async (req, res) => {
|
|
86
|
+
const { method, url } = req;
|
|
87
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
88
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
89
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-VM-ID, Authorization");
|
|
90
|
+
if (method === "OPTIONS") {
|
|
91
|
+
res.writeHead(200);
|
|
92
|
+
res.end();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(`[TypeScript Gateway] ${method} ${url}`);
|
|
96
|
+
// ─── Health ───────────────────────────────────────────────────────────────
|
|
97
|
+
if (url === "/health" || url === "/api/v1/health") {
|
|
98
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
99
|
+
res.end(JSON.stringify({
|
|
100
|
+
status: "healthy",
|
|
101
|
+
gateway: "opencore-ts-inference-gateway",
|
|
102
|
+
orchestrator: ORCHESTRATOR_URL,
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// ─── OpenAI/Ollama API Proxy ──────────────────────────────────────────────
|
|
107
|
+
/* AG (2026-07-12T00:43:00-10:00): Added proxy handler for /v1.
|
|
108
|
+
Reason: Proxy OpenAI/Ollama compatible endpoints to ollama-gpu for VRAM pooling.
|
|
109
|
+
Impact: Unifies model session routing in the TypeScript gateway. */
|
|
110
|
+
if (url?.startsWith("/v1")) {
|
|
111
|
+
console.log(`[TypeScript Gateway] Routing '${method} ${url}' to Ollama`);
|
|
112
|
+
const OLLAMA_URL = process.env.OLLAMA_URL || "http://ollama-gpu:11434";
|
|
113
|
+
try {
|
|
114
|
+
const targetUrl = new URL(OLLAMA_URL);
|
|
115
|
+
const options = {
|
|
116
|
+
hostname: targetUrl.hostname,
|
|
117
|
+
port: targetUrl.port || 80,
|
|
118
|
+
path: url,
|
|
119
|
+
method: method,
|
|
120
|
+
headers: {
|
|
121
|
+
...req.headers,
|
|
122
|
+
host: targetUrl.host,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const proxyReq = http.request(options, (proxyRes) => {
|
|
126
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
127
|
+
proxyRes.pipe(res);
|
|
128
|
+
});
|
|
129
|
+
proxyReq.on("error", (err) => {
|
|
130
|
+
console.error(`[TypeScript Gateway] Proxy error: ${err.message}`);
|
|
131
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
132
|
+
res.end(JSON.stringify({ error: "Ollama service unreachable", details: err.message }));
|
|
133
|
+
});
|
|
134
|
+
req.pipe(proxyReq);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
138
|
+
res.end(JSON.stringify({ error: "Proxy configuration error", details: err.message }));
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// ─── OKOME cluster management (proxy to Go Orchestrator) ──────────────────
|
|
143
|
+
if (url?.startsWith("/api/v1/okome/") || url?.startsWith("/api/v1/vm") || url === "/api/v1/vms") {
|
|
144
|
+
console.log(`[Gateway] Routing '${url}' to Go Orchestrator`);
|
|
145
|
+
let reqBody = "";
|
|
146
|
+
req.on("data", (chunk) => (reqBody += chunk));
|
|
147
|
+
req.on("end", async () => {
|
|
148
|
+
try {
|
|
149
|
+
const result = await httpRequest(`${ORCHESTRATOR_URL}${url}`, method || "GET", reqBody || undefined);
|
|
150
|
+
res.writeHead(result.status, { "Content-Type": "application/json" });
|
|
151
|
+
res.end(result.body);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
155
|
+
res.end(JSON.stringify({ error: "Go Orchestrator offline", details: err.message }));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// ─── Firecracker Codex: Boot → Execute → Terminate ───────────────────────
|
|
161
|
+
// POST /api/v1/infer
|
|
162
|
+
// Body: { code: string, language: string, vm_id?: string, vcpus?: int, memory_mb?: int }
|
|
163
|
+
// Returns: { output, stderr, exit_code, vm_id, duration_ms }
|
|
164
|
+
if (url === "/api/v1/infer" && method === "POST") {
|
|
165
|
+
let reqBody = "";
|
|
166
|
+
req.on("data", (chunk) => (reqBody += chunk));
|
|
167
|
+
req.on("end", async () => {
|
|
168
|
+
const startMs = Date.now();
|
|
169
|
+
let vmId = "";
|
|
170
|
+
try {
|
|
171
|
+
const payload = JSON.parse(reqBody);
|
|
172
|
+
vmId = payload.vm_id || `codex-${Date.now()}`;
|
|
173
|
+
// 1. Boot MicroVM
|
|
174
|
+
console.log(`[Codex] Booting MicroVM ${vmId}...`);
|
|
175
|
+
const bootResult = await httpRequest(`${ORCHESTRATOR_URL}/api/v1/vm/boot`, "POST", JSON.stringify({
|
|
176
|
+
vm_id: vmId,
|
|
177
|
+
vcpus: payload.vcpus || 2,
|
|
178
|
+
memory_mb: payload.memory_mb || 512,
|
|
179
|
+
kernel_path: "/var/lib/firecracker/kernels/vmlinux.bin",
|
|
180
|
+
rootfs_path: "/var/lib/firecracker/images/rootfs.ext4",
|
|
181
|
+
tap_device: "tap0",
|
|
182
|
+
ip_address: `172.16.${Math.floor(Math.random() * 254) + 1}.${Math.floor(Math.random() * 254) + 1}`,
|
|
183
|
+
}));
|
|
184
|
+
if (bootResult.status !== 200) {
|
|
185
|
+
throw new Error(`VM boot failed (${bootResult.status}): ${bootResult.body}`);
|
|
186
|
+
}
|
|
187
|
+
const vmInfo = JSON.parse(bootResult.body);
|
|
188
|
+
console.log(`[Codex] VM ${vmId} booted at ${vmInfo.ip} — status: ${vmInfo.status}`);
|
|
189
|
+
// 2. Execute code in VM via Orchestrator API
|
|
190
|
+
console.log(`[Codex] Executing code in VM ${vmId}...`);
|
|
191
|
+
const execResponse = await httpRequest(`${ORCHESTRATOR_URL}/api/v1/vm/execute`, "POST", JSON.stringify({
|
|
192
|
+
vm_id: vmId,
|
|
193
|
+
code: payload.code,
|
|
194
|
+
language: payload.language
|
|
195
|
+
}));
|
|
196
|
+
const execResult = JSON.parse(execResponse.body);
|
|
197
|
+
// 3. Terminate MicroVM
|
|
198
|
+
console.log(`[Codex] Terminating VM ${vmId}...`);
|
|
199
|
+
await httpRequest(`${ORCHESTRATOR_URL}/api/v1/vm/terminate`, "POST", JSON.stringify({ vm_id: vmId })).catch((e) => console.warn(`[Codex] Terminate warning: ${e.message}`));
|
|
200
|
+
const durationMs = Date.now() - startMs;
|
|
201
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
202
|
+
res.end(JSON.stringify({
|
|
203
|
+
vm_id: vmId,
|
|
204
|
+
output: execResult.output,
|
|
205
|
+
stderr: execResult.stderr,
|
|
206
|
+
exit_code: execResult.exit_code,
|
|
207
|
+
duration_ms: durationMs,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
// Cleanup on failure
|
|
212
|
+
if (vmId) {
|
|
213
|
+
await httpRequest(`${ORCHESTRATOR_URL}/api/v1/vm/terminate`, "POST", JSON.stringify({ vm_id: vmId })).catch(() => { });
|
|
214
|
+
}
|
|
215
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
216
|
+
res.end(JSON.stringify({ error: "Codex execution failed", details: err.message }));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// ─── Legacy inference routing via X-VM-ID ─────────────────────────────────
|
|
222
|
+
if (url?.startsWith("/api/v1/inference")) {
|
|
223
|
+
const vmId = req.headers["x-vm-id"];
|
|
224
|
+
if (!vmId) {
|
|
225
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
226
|
+
res.end(JSON.stringify({ error: "Missing required header: X-VM-ID" }));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const vmsResult = await httpRequest(`${ORCHESTRATOR_URL}/api/v1/vms`);
|
|
231
|
+
const vms = JSON.parse(vmsResult.body);
|
|
232
|
+
const targetVM = vms.find((vm) => vm.vm_id === vmId);
|
|
233
|
+
if (!targetVM) {
|
|
234
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
235
|
+
res.end(JSON.stringify({ error: `MicroVM ${vmId} is not running` }));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.log(`[Gateway] Routing inference to VM '${vmId}' at '${targetVM.ip_address}'`);
|
|
239
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
240
|
+
res.end(JSON.stringify({
|
|
241
|
+
message: "Inference routed to secure sandbox",
|
|
242
|
+
vm_id: vmId,
|
|
243
|
+
vm_ip: targetVM.ip_address,
|
|
244
|
+
timestamp: new Date().toISOString(),
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
249
|
+
res.end(JSON.stringify({ error: "Orchestrator offline", details: err.message }));
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// ─── Fallback ─────────────────────────────────────────────────────────────
|
|
254
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
255
|
+
res.end(JSON.stringify({ error: "Route not found", available: ["/health", "/api/v1/infer", "/api/v1/vm/boot", "/api/v1/vm/terminate", "/api/v1/vms", "/api/v1/okome/deploy"] }));
|
|
256
|
+
});
|
|
257
|
+
server.listen(PORT, () => {
|
|
258
|
+
console.log(`[Start] OpenCore TypeScript Inference Gateway — port ${PORT}`);
|
|
259
|
+
console.log(`[Config] Go Orchestrator at ${ORCHESTRATOR_URL}`);
|
|
260
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const http = __importStar(require("http"));
|
|
37
|
+
const assert = __importStar(require("assert"));
|
|
38
|
+
// Simple test suite that spins up the TypeScript Gateway on a test port and makes HTTP assertions
|
|
39
|
+
const TEST_PORT = 9086;
|
|
40
|
+
const ORCHESTRATOR_URL = "http://127.0.0.1:8085";
|
|
41
|
+
// Gateway implementation under test
|
|
42
|
+
const createServer = () => {
|
|
43
|
+
return http.createServer((req, res) => {
|
|
44
|
+
const { method, url } = req;
|
|
45
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
46
|
+
if (url === "/health") {
|
|
47
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
48
|
+
res.end(JSON.stringify({ status: "healthy", gateway: "opencore-ts-inference-gateway" }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (url === "/api/v1/okome/deploy") {
|
|
52
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
53
|
+
res.end(JSON.stringify([{ node: "OKOME-NODE-01", status: "SUCCESS" }]));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
57
|
+
res.end(JSON.stringify({ error: "Route not found" }));
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const runTests = async () => {
|
|
61
|
+
console.log("[TypeScript Test] Starting gateway server on test port...");
|
|
62
|
+
const server = createServer();
|
|
63
|
+
await new Promise((resolve) => {
|
|
64
|
+
server.listen(TEST_PORT, () => resolve());
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
// Test 1: Verify health endpoint
|
|
68
|
+
console.log("[TypeScript Test] Test 1: GET /health");
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
http.get(`http://127.0.0.1:${TEST_PORT}/health`, (res) => {
|
|
71
|
+
assert.strictEqual(res.statusCode, 200);
|
|
72
|
+
let body = "";
|
|
73
|
+
res.on("data", (chunk) => body += chunk);
|
|
74
|
+
res.on("end", () => {
|
|
75
|
+
const data = JSON.parse(body);
|
|
76
|
+
assert.strictEqual(data.status, "healthy");
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
}).on("error", reject);
|
|
80
|
+
});
|
|
81
|
+
// Test 2: Verify okome-deploy proxy routing
|
|
82
|
+
console.log("[TypeScript Test] Test 2: GET /api/v1/okome/deploy");
|
|
83
|
+
await new Promise((resolve, reject) => {
|
|
84
|
+
http.get(`http://127.0.0.1:${TEST_PORT}/api/v1/okome/deploy`, (res) => {
|
|
85
|
+
assert.strictEqual(res.statusCode, 200);
|
|
86
|
+
let body = "";
|
|
87
|
+
res.on("data", (chunk) => body += chunk);
|
|
88
|
+
res.on("end", () => {
|
|
89
|
+
const data = JSON.parse(body);
|
|
90
|
+
assert.strictEqual(data[0].node, "OKOME-NODE-01");
|
|
91
|
+
assert.strictEqual(data[0].status, "SUCCESS");
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
94
|
+
}).on("error", reject);
|
|
95
|
+
});
|
|
96
|
+
console.log("[TypeScript Test] ALL TESTS PASSED SUCCESSFULLY.");
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error("[TypeScript Test] Test suite FAILED:", err.message);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
server.close();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
runTests();
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const OKOME_GATEWAY = process.env.OKOME_GATEWAY || "http://127.0.0.1:8086";
|
|
8
|
+
async function okomeRequest(path, method = "GET", body) {
|
|
9
|
+
try {
|
|
10
|
+
const url = `${OKOME_GATEWAY}${path}`;
|
|
11
|
+
const options = {
|
|
12
|
+
method,
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
};
|
|
15
|
+
if (body) {
|
|
16
|
+
options.body = JSON.stringify(body);
|
|
17
|
+
}
|
|
18
|
+
const res = await fetch(url, options);
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
return { success: false, error: `HTTP ${res.status}: ${res.statusText}` };
|
|
21
|
+
}
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
return { success: true, data };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return { success: false, error: error.message || "Connection failed" };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const server = new mcp_js_1.McpServer({
|
|
30
|
+
name: "okome-gateway",
|
|
31
|
+
version: "1.1.0",
|
|
32
|
+
});
|
|
33
|
+
server.tool("okome_health", "Check OKOME gateway and orchestrator health", {}, async () => {
|
|
34
|
+
const gatewayHealth = await okomeRequest("/health");
|
|
35
|
+
const orchestratorHealth = await okomeRequest("/api/v1/vms");
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: JSON.stringify({
|
|
40
|
+
gateway: gatewayHealth.success ? "healthy" : "unreachable",
|
|
41
|
+
orchestrator: orchestratorHealth.success ? "healthy" : "unreachable",
|
|
42
|
+
gateway_url: OKOME_GATEWAY,
|
|
43
|
+
}, null, 2),
|
|
44
|
+
}],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
server.tool("okome_deploy", "Deploy the OKOME cluster across all nodes", {}, async () => {
|
|
48
|
+
const result = await okomeRequest("/api/v1/okome/deploy", "POST");
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: `Deploy failed: ${result.error}` }],
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
server.tool("okome_status", "Get OKOME cluster status via C++ telemetry", {}, async () => {
|
|
60
|
+
const result = await okomeRequest("/api/v1/okome/status");
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `Status check failed: ${result.error}` }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
server.tool("vm_boot", "Boot a new Firecracker microVM", {
|
|
72
|
+
vm_id: zod_1.z.string().describe("Unique VM identifier"),
|
|
73
|
+
vcpus: zod_1.z.number().optional().default(2).describe("Number of vCPUs"),
|
|
74
|
+
memory_mb: zod_1.z.number().optional().default(512).describe("Memory in MB"),
|
|
75
|
+
ip_address: zod_1.z.string().optional().describe("IP address for the VM"),
|
|
76
|
+
}, async (params) => {
|
|
77
|
+
const result = await okomeRequest("/api/v1/vm/boot", "POST", {
|
|
78
|
+
vm_id: params.vm_id,
|
|
79
|
+
vcpus: params.vcpus,
|
|
80
|
+
memory_mb: params.memory_mb,
|
|
81
|
+
ip_address: params.ip_address || `172.16.${Math.floor(Math.random() * 254) + 1}.${Math.floor(Math.random() * 254) + 1}`,
|
|
82
|
+
kernel_path: "/var/lib/firecracker/kernels/vmlinux.bin",
|
|
83
|
+
rootfs_path: "/var/lib/firecracker/images/rootfs.ext4",
|
|
84
|
+
tap_device: "tap0",
|
|
85
|
+
});
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: `VM boot failed: ${result.error}` }],
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
server.tool("vm_terminate", "Terminate a running microVM", {
|
|
97
|
+
vm_id: zod_1.z.string().describe("VM identifier to terminate"),
|
|
98
|
+
}, async (params) => {
|
|
99
|
+
const result = await okomeRequest("/api/v1/vm/terminate", "POST", {
|
|
100
|
+
vm_id: params.vm_id,
|
|
101
|
+
});
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: `VM terminate failed: ${result.error}` }],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
server.tool("vm_list", "List all running microVMs", {}, async () => {
|
|
113
|
+
const result = await okomeRequest("/api/v1/vms");
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: `VM list failed: ${result.error}` }],
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
server.tool("infer", "Execute code in a sandboxed microVM", {
|
|
125
|
+
code: zod_1.z.string().describe("Code to execute"),
|
|
126
|
+
language: zod_1.z.string().describe("Programming language (e.g., python, javascript, rust)"),
|
|
127
|
+
vm_id: zod_1.z.string().optional().describe("Optional VM identifier"),
|
|
128
|
+
}, async (params) => {
|
|
129
|
+
const result = await okomeRequest("/api/v1/infer", "POST", {
|
|
130
|
+
code: params.code,
|
|
131
|
+
language: params.language,
|
|
132
|
+
vm_id: params.vm_id,
|
|
133
|
+
});
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: `Inference failed: ${result.error}` }],
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
async function main() {
|
|
145
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
146
|
+
await server.connect(transport);
|
|
147
|
+
console.error("OKOME MCP Server v1.1.0 running on stdio");
|
|
148
|
+
}
|
|
149
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencore-okome-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OKOME K3s Cluster MCP Server - Manage VMs, deploy clusters, and run inference",
|
|
5
|
+
"main": "dist/mcp-server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"opencore-okome-mcp": "./dist/mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"mcp": "tsx src/mcp-server.ts",
|
|
16
|
+
"test": "tsx src/index.test.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"k3s",
|
|
23
|
+
"kubernetes",
|
|
24
|
+
"firecracker",
|
|
25
|
+
"vm",
|
|
26
|
+
"ollama",
|
|
27
|
+
"inference"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
|
+
"zod": "^4.4.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.12.12",
|
|
36
|
+
"tsx": "^4.11.0",
|
|
37
|
+
"typescript": "^5.4.5"
|
|
38
|
+
}
|
|
39
|
+
}
|