@xnetjs/plugins 0.0.2
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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/index.d.ts +2047 -0
- package/dist/index.js +2640 -0
- package/dist/services/node.d.ts +583 -0
- package/dist/services/node.js +1055 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
// src/services/local-api.ts
|
|
2
|
+
import { timingSafeEqual } from "crypto";
|
|
3
|
+
import { createServer } from "http";
|
|
4
|
+
import { URL } from "url";
|
|
5
|
+
function constantTimeCompare(a, b) {
|
|
6
|
+
if (a.length !== b.length) return false;
|
|
7
|
+
const bufA = Buffer.from(a);
|
|
8
|
+
const bufB = Buffer.from(b);
|
|
9
|
+
return timingSafeEqual(bufA, bufB);
|
|
10
|
+
}
|
|
11
|
+
var EventBuffer = class {
|
|
12
|
+
events = [];
|
|
13
|
+
maxEvents;
|
|
14
|
+
constructor(maxEvents = 1e3) {
|
|
15
|
+
this.maxEvents = maxEvents;
|
|
16
|
+
}
|
|
17
|
+
push(event) {
|
|
18
|
+
this.events.push(event);
|
|
19
|
+
if (this.events.length > this.maxEvents) {
|
|
20
|
+
this.events.shift();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
getSince(timestamp, schemaId) {
|
|
24
|
+
return this.events.filter((e) => {
|
|
25
|
+
if (e.timestamp <= timestamp) return false;
|
|
26
|
+
if (schemaId && e.node?.schemaId !== schemaId) return false;
|
|
27
|
+
return true;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var LocalAPIServer = class {
|
|
32
|
+
server = null;
|
|
33
|
+
config;
|
|
34
|
+
eventBuffer;
|
|
35
|
+
unsubscribe;
|
|
36
|
+
constructor(config) {
|
|
37
|
+
this.config = {
|
|
38
|
+
port: config.port ?? 31415,
|
|
39
|
+
host: config.host ?? "127.0.0.1",
|
|
40
|
+
token: config.token,
|
|
41
|
+
allowedOrigins: config.allowedOrigins ?? [],
|
|
42
|
+
store: config.store,
|
|
43
|
+
schemas: config.schemas
|
|
44
|
+
};
|
|
45
|
+
this.eventBuffer = new EventBuffer();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Start the API server
|
|
49
|
+
*/
|
|
50
|
+
async start() {
|
|
51
|
+
if (this.server) {
|
|
52
|
+
throw new Error("Server already running");
|
|
53
|
+
}
|
|
54
|
+
this.unsubscribe = this.config.store.subscribe((event) => {
|
|
55
|
+
const type = this.getEventType(event.change.type);
|
|
56
|
+
if (type) {
|
|
57
|
+
this.eventBuffer.push({
|
|
58
|
+
type,
|
|
59
|
+
node: event.node,
|
|
60
|
+
timestamp: Date.now()
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.server = createServer((req, res) => {
|
|
65
|
+
this.handleRequest(req, res).catch((err) => {
|
|
66
|
+
console.error("[LocalAPI] Request error:", err);
|
|
67
|
+
this.sendError(res, 500, "Internal server error");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
72
|
+
console.log(`[LocalAPI] Server listening on http://${this.config.host}:${this.config.port}`);
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
this.server.on("error", reject);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Stop the API server
|
|
80
|
+
*/
|
|
81
|
+
async stop() {
|
|
82
|
+
if (this.unsubscribe) {
|
|
83
|
+
this.unsubscribe();
|
|
84
|
+
this.unsubscribe = void 0;
|
|
85
|
+
}
|
|
86
|
+
if (!this.server) return;
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
this.server.close(() => {
|
|
89
|
+
console.log("[LocalAPI] Server stopped");
|
|
90
|
+
this.server = null;
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the server port
|
|
97
|
+
*/
|
|
98
|
+
get port() {
|
|
99
|
+
return this.config.port;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if server is running
|
|
103
|
+
*/
|
|
104
|
+
get isRunning() {
|
|
105
|
+
return this.server !== null;
|
|
106
|
+
}
|
|
107
|
+
// ─── Request Handling ────────────────────────────────────────────────────────
|
|
108
|
+
async handleRequest(req, res) {
|
|
109
|
+
const origin = req.headers.origin;
|
|
110
|
+
if (this.config.allowedOrigins.length > 0 && origin) {
|
|
111
|
+
const isAllowed = this.config.allowedOrigins.includes("*") || this.config.allowedOrigins.includes(origin);
|
|
112
|
+
if (isAllowed) {
|
|
113
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
114
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
115
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
116
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (req.method === "OPTIONS") {
|
|
120
|
+
res.writeHead(204);
|
|
121
|
+
res.end();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (this.config.token) {
|
|
125
|
+
const auth = req.headers.authorization;
|
|
126
|
+
const expected = `Bearer ${this.config.token}`;
|
|
127
|
+
if (!auth || !constantTimeCompare(auth, expected)) {
|
|
128
|
+
this.sendError(res, 401, "Unauthorized");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const url = new URL(req.url ?? "/", `http://${this.config.host}:${this.config.port}`);
|
|
133
|
+
const pathname = url.pathname;
|
|
134
|
+
const method = req.method ?? "GET";
|
|
135
|
+
try {
|
|
136
|
+
if (pathname === "/health" && method === "GET") {
|
|
137
|
+
return this.handleHealth(res);
|
|
138
|
+
}
|
|
139
|
+
if (pathname.startsWith("/api/v1/")) {
|
|
140
|
+
const path = pathname.slice("/api/v1".length);
|
|
141
|
+
if (path === "/nodes" && method === "GET") {
|
|
142
|
+
return await this.handleListNodes(url, res);
|
|
143
|
+
}
|
|
144
|
+
if (path === "/nodes" && method === "POST") {
|
|
145
|
+
return await this.handleCreateNode(req, res);
|
|
146
|
+
}
|
|
147
|
+
if (path.match(/^\/nodes\/[^/]+$/) && method === "GET") {
|
|
148
|
+
const id = path.slice("/nodes/".length);
|
|
149
|
+
return await this.handleGetNode(id, res);
|
|
150
|
+
}
|
|
151
|
+
if (path.match(/^\/nodes\/[^/]+$/) && method === "PATCH") {
|
|
152
|
+
const id = path.slice("/nodes/".length);
|
|
153
|
+
return await this.handleUpdateNode(id, req, res);
|
|
154
|
+
}
|
|
155
|
+
if (path.match(/^\/nodes\/[^/]+$/) && method === "DELETE") {
|
|
156
|
+
const id = path.slice("/nodes/".length);
|
|
157
|
+
return await this.handleDeleteNode(id, res);
|
|
158
|
+
}
|
|
159
|
+
if (path === "/query" && method === "POST") {
|
|
160
|
+
return await this.handleQuery(req, res);
|
|
161
|
+
}
|
|
162
|
+
if (path === "/events" && method === "GET") {
|
|
163
|
+
return this.handleGetEvents(url, res);
|
|
164
|
+
}
|
|
165
|
+
if (path === "/schemas" && method === "GET") {
|
|
166
|
+
return await this.handleListSchemas(res);
|
|
167
|
+
}
|
|
168
|
+
if (path.match(/^\/schemas\//) && method === "GET") {
|
|
169
|
+
const iri = decodeURIComponent(path.slice("/schemas/".length));
|
|
170
|
+
return await this.handleGetSchema(iri, res);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.sendError(res, 404, "Not found");
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error("[LocalAPI] Handler error:", err);
|
|
176
|
+
this.sendError(res, 500, err instanceof Error ? err.message : "Internal server error");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ─── Health ──────────────────────────────────────────────────────────────────
|
|
180
|
+
handleHealth(res) {
|
|
181
|
+
this.sendJSON(res, 200, {
|
|
182
|
+
status: "ok",
|
|
183
|
+
version: "1.0.0",
|
|
184
|
+
timestamp: Date.now()
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// ─── Nodes ───────────────────────────────────────────────────────────────────
|
|
188
|
+
async handleListNodes(url, res) {
|
|
189
|
+
const schemaId = url.searchParams.get("schema") ?? void 0;
|
|
190
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
191
|
+
const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
|
|
192
|
+
const nodes = await this.config.store.list({ schemaId, limit, offset });
|
|
193
|
+
this.sendJSON(res, 200, { nodes, count: nodes.length, limit, offset });
|
|
194
|
+
}
|
|
195
|
+
async handleGetNode(id, res) {
|
|
196
|
+
const node = await this.config.store.get(id);
|
|
197
|
+
if (!node) {
|
|
198
|
+
this.sendError(res, 404, `Node not found: ${id}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.sendJSON(res, 200, node);
|
|
202
|
+
}
|
|
203
|
+
async handleCreateNode(req, res) {
|
|
204
|
+
const body = await this.parseBody(req);
|
|
205
|
+
if (!body.schema || typeof body.schema !== "string") {
|
|
206
|
+
this.sendError(res, 400, "Missing required field: schema");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!body.properties || typeof body.properties !== "object") {
|
|
210
|
+
this.sendError(res, 400, "Missing required field: properties");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const node = await this.config.store.create({
|
|
214
|
+
schemaId: body.schema,
|
|
215
|
+
properties: body.properties
|
|
216
|
+
});
|
|
217
|
+
this.sendJSON(res, 201, node);
|
|
218
|
+
}
|
|
219
|
+
async handleUpdateNode(id, req, res) {
|
|
220
|
+
const existing = await this.config.store.get(id);
|
|
221
|
+
if (!existing) {
|
|
222
|
+
this.sendError(res, 404, `Node not found: ${id}`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const body = await this.parseBody(req);
|
|
226
|
+
if (!body || typeof body !== "object") {
|
|
227
|
+
this.sendError(res, 400, "Request body must be an object");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const node = await this.config.store.update(id, {
|
|
231
|
+
properties: body
|
|
232
|
+
});
|
|
233
|
+
this.sendJSON(res, 200, node);
|
|
234
|
+
}
|
|
235
|
+
async handleDeleteNode(id, res) {
|
|
236
|
+
const existing = await this.config.store.get(id);
|
|
237
|
+
if (!existing) {
|
|
238
|
+
this.sendError(res, 404, `Node not found: ${id}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
await this.config.store.delete(id);
|
|
242
|
+
this.sendJSON(res, 200, { success: true });
|
|
243
|
+
}
|
|
244
|
+
// ─── Query ───────────────────────────────────────────────────────────────────
|
|
245
|
+
async handleQuery(req, res) {
|
|
246
|
+
const body = await this.parseBody(req);
|
|
247
|
+
if (!body.schema || typeof body.schema !== "string") {
|
|
248
|
+
this.sendError(res, 400, "Missing required field: schema");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const limit = typeof body.limit === "number" ? body.limit : 50;
|
|
252
|
+
const offset = typeof body.offset === "number" ? body.offset : 0;
|
|
253
|
+
const nodes = await this.config.store.list({
|
|
254
|
+
schemaId: body.schema,
|
|
255
|
+
limit,
|
|
256
|
+
offset
|
|
257
|
+
});
|
|
258
|
+
this.sendJSON(res, 200, { nodes, count: nodes.length });
|
|
259
|
+
}
|
|
260
|
+
// ─── Events ──────────────────────────────────────────────────────────────────
|
|
261
|
+
handleGetEvents(url, res) {
|
|
262
|
+
const since = parseInt(url.searchParams.get("since") ?? "0", 10);
|
|
263
|
+
const schemaId = url.searchParams.get("schema") ?? void 0;
|
|
264
|
+
const events = this.eventBuffer.getSince(since, schemaId);
|
|
265
|
+
this.sendJSON(res, 200, {
|
|
266
|
+
events,
|
|
267
|
+
timestamp: Date.now()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// ─── Schemas ─────────────────────────────────────────────────────────────────
|
|
271
|
+
async handleListSchemas(res) {
|
|
272
|
+
const iris = this.config.schemas.getAllIRIs();
|
|
273
|
+
const schemas = await Promise.all(
|
|
274
|
+
iris.map(async (iri) => {
|
|
275
|
+
const schema = await this.config.schemas.get(iri);
|
|
276
|
+
return schema ? {
|
|
277
|
+
iri,
|
|
278
|
+
name: schema.name,
|
|
279
|
+
properties: schema.properties
|
|
280
|
+
} : null;
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
this.sendJSON(res, 200, {
|
|
284
|
+
schemas: schemas.filter(Boolean)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async handleGetSchema(iri, res) {
|
|
288
|
+
const schema = await this.config.schemas.get(iri);
|
|
289
|
+
if (!schema) {
|
|
290
|
+
this.sendError(res, 404, `Schema not found: ${iri}`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.sendJSON(res, 200, schema);
|
|
294
|
+
}
|
|
295
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
296
|
+
async parseBody(req) {
|
|
297
|
+
return new Promise((resolve, reject) => {
|
|
298
|
+
let body = "";
|
|
299
|
+
req.on("data", (chunk) => body += chunk);
|
|
300
|
+
req.on("end", () => {
|
|
301
|
+
try {
|
|
302
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
303
|
+
resolve(parsed);
|
|
304
|
+
} catch {
|
|
305
|
+
reject(new Error("Invalid JSON body"));
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
req.on("error", reject);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
sendJSON(res, status, data) {
|
|
312
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
313
|
+
res.end(JSON.stringify(data));
|
|
314
|
+
}
|
|
315
|
+
sendError(res, status, message) {
|
|
316
|
+
this.sendJSON(res, status, { error: message });
|
|
317
|
+
}
|
|
318
|
+
getEventType(changeType) {
|
|
319
|
+
if (changeType === "node-change") {
|
|
320
|
+
return "updated";
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
function createLocalAPI(config) {
|
|
326
|
+
return new LocalAPIServer(config);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/services/mcp-server.ts
|
|
330
|
+
var MCPServer = class {
|
|
331
|
+
config;
|
|
332
|
+
tools = /* @__PURE__ */ new Map();
|
|
333
|
+
running = false;
|
|
334
|
+
constructor(config) {
|
|
335
|
+
this.config = {
|
|
336
|
+
store: config.store,
|
|
337
|
+
schemas: config.schemas,
|
|
338
|
+
name: config.name ?? "xnet",
|
|
339
|
+
version: config.version ?? "1.0.0"
|
|
340
|
+
};
|
|
341
|
+
this.registerTools();
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get server info for MCP initialize response
|
|
345
|
+
*/
|
|
346
|
+
getServerInfo() {
|
|
347
|
+
return {
|
|
348
|
+
name: this.config.name,
|
|
349
|
+
version: this.config.version
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get server capabilities
|
|
354
|
+
*/
|
|
355
|
+
getCapabilities() {
|
|
356
|
+
return {
|
|
357
|
+
tools: {},
|
|
358
|
+
resources: {}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Get all registered tools
|
|
363
|
+
*/
|
|
364
|
+
getTools() {
|
|
365
|
+
return Array.from(this.tools.values());
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Get available resources
|
|
369
|
+
*/
|
|
370
|
+
getResources() {
|
|
371
|
+
return [
|
|
372
|
+
{
|
|
373
|
+
uri: "xnet://nodes",
|
|
374
|
+
name: "All Nodes",
|
|
375
|
+
description: "List of all nodes in the local store",
|
|
376
|
+
mimeType: "application/json"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
uri: "xnet://schemas",
|
|
380
|
+
name: "All Schemas",
|
|
381
|
+
description: "List of all available schemas",
|
|
382
|
+
mimeType: "application/json"
|
|
383
|
+
}
|
|
384
|
+
];
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Handle an MCP request
|
|
388
|
+
*/
|
|
389
|
+
async handleRequest(request) {
|
|
390
|
+
const { id, method, params } = request;
|
|
391
|
+
try {
|
|
392
|
+
let result;
|
|
393
|
+
switch (method) {
|
|
394
|
+
case "initialize":
|
|
395
|
+
result = {
|
|
396
|
+
protocolVersion: "2024-11-05",
|
|
397
|
+
serverInfo: this.getServerInfo(),
|
|
398
|
+
capabilities: this.getCapabilities()
|
|
399
|
+
};
|
|
400
|
+
break;
|
|
401
|
+
case "tools/list":
|
|
402
|
+
result = { tools: this.getTools() };
|
|
403
|
+
break;
|
|
404
|
+
case "tools/call":
|
|
405
|
+
result = await this.handleToolCall(params);
|
|
406
|
+
break;
|
|
407
|
+
case "resources/list":
|
|
408
|
+
result = { resources: this.getResources() };
|
|
409
|
+
break;
|
|
410
|
+
case "resources/read":
|
|
411
|
+
result = await this.handleResourceRead(params);
|
|
412
|
+
break;
|
|
413
|
+
case "notifications/initialized":
|
|
414
|
+
result = {};
|
|
415
|
+
break;
|
|
416
|
+
default:
|
|
417
|
+
return {
|
|
418
|
+
jsonrpc: "2.0",
|
|
419
|
+
id,
|
|
420
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return { jsonrpc: "2.0", id, result };
|
|
424
|
+
} catch (err) {
|
|
425
|
+
return {
|
|
426
|
+
jsonrpc: "2.0",
|
|
427
|
+
id,
|
|
428
|
+
error: {
|
|
429
|
+
code: -32e3,
|
|
430
|
+
message: err instanceof Error ? err.message : "Unknown error"
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Start the MCP server in stdio mode.
|
|
437
|
+
* Reads JSON-RPC requests from stdin and writes responses to stdout.
|
|
438
|
+
*/
|
|
439
|
+
async startStdio() {
|
|
440
|
+
if (this.running) {
|
|
441
|
+
throw new Error("Server already running");
|
|
442
|
+
}
|
|
443
|
+
this.running = true;
|
|
444
|
+
const readline = await import("readline");
|
|
445
|
+
const rl = readline.createInterface({
|
|
446
|
+
input: process.stdin,
|
|
447
|
+
output: process.stdout,
|
|
448
|
+
terminal: false
|
|
449
|
+
});
|
|
450
|
+
for await (const line of rl) {
|
|
451
|
+
if (!this.running) break;
|
|
452
|
+
try {
|
|
453
|
+
const request = JSON.parse(line);
|
|
454
|
+
const response = await this.handleRequest(request);
|
|
455
|
+
console.log(JSON.stringify(response));
|
|
456
|
+
} catch {
|
|
457
|
+
console.log(
|
|
458
|
+
JSON.stringify({
|
|
459
|
+
jsonrpc: "2.0",
|
|
460
|
+
id: null,
|
|
461
|
+
error: { code: -32700, message: "Parse error" }
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Stop the stdio server
|
|
469
|
+
*/
|
|
470
|
+
stop() {
|
|
471
|
+
this.running = false;
|
|
472
|
+
}
|
|
473
|
+
// ─── Tool Registration ─────────────────────────────────────────────────────
|
|
474
|
+
registerTools() {
|
|
475
|
+
this.tools.set("xnet_query", {
|
|
476
|
+
name: "xnet_query",
|
|
477
|
+
description: "Query nodes by schema and optional filter. Returns matching nodes with their properties.",
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: "object",
|
|
480
|
+
properties: {
|
|
481
|
+
schema: {
|
|
482
|
+
type: "string",
|
|
483
|
+
description: "Schema IRI to query (e.g., xnet://xnet.dev/Task)"
|
|
484
|
+
},
|
|
485
|
+
limit: {
|
|
486
|
+
type: "number",
|
|
487
|
+
description: "Maximum number of results to return (default: 20)"
|
|
488
|
+
},
|
|
489
|
+
offset: {
|
|
490
|
+
type: "number",
|
|
491
|
+
description: "Number of results to skip for pagination"
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
required: ["schema"]
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
this.tools.set("xnet_get", {
|
|
498
|
+
name: "xnet_get",
|
|
499
|
+
description: "Get a single node by its ID.",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
nodeId: {
|
|
504
|
+
type: "string",
|
|
505
|
+
description: "The unique ID of the node to retrieve"
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
required: ["nodeId"]
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
this.tools.set("xnet_create", {
|
|
512
|
+
name: "xnet_create",
|
|
513
|
+
description: "Create a new node with the given schema and properties.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: {
|
|
517
|
+
schema: {
|
|
518
|
+
type: "string",
|
|
519
|
+
description: "Schema IRI for the new node"
|
|
520
|
+
},
|
|
521
|
+
properties: {
|
|
522
|
+
type: "object",
|
|
523
|
+
description: "Initial property values for the node"
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
required: ["schema", "properties"]
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
this.tools.set("xnet_update", {
|
|
530
|
+
name: "xnet_update",
|
|
531
|
+
description: "Update properties of an existing node.",
|
|
532
|
+
inputSchema: {
|
|
533
|
+
type: "object",
|
|
534
|
+
properties: {
|
|
535
|
+
nodeId: {
|
|
536
|
+
type: "string",
|
|
537
|
+
description: "ID of the node to update"
|
|
538
|
+
},
|
|
539
|
+
properties: {
|
|
540
|
+
type: "object",
|
|
541
|
+
description: "Properties to update (only specified properties will change)"
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
required: ["nodeId", "properties"]
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
this.tools.set("xnet_delete", {
|
|
548
|
+
name: "xnet_delete",
|
|
549
|
+
description: "Delete a node by its ID.",
|
|
550
|
+
inputSchema: {
|
|
551
|
+
type: "object",
|
|
552
|
+
properties: {
|
|
553
|
+
nodeId: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "ID of the node to delete"
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
required: ["nodeId"]
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
this.tools.set("xnet_schemas", {
|
|
562
|
+
name: "xnet_schemas",
|
|
563
|
+
description: "List all available schemas with their properties. Useful for understanding what types of nodes can be created.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
// ─── Tool Execution ────────────────────────────────────────────────────────
|
|
571
|
+
async handleToolCall(params) {
|
|
572
|
+
const { name, arguments: args } = params;
|
|
573
|
+
const toolArgs = args ?? {};
|
|
574
|
+
let result;
|
|
575
|
+
switch (name) {
|
|
576
|
+
case "xnet_query": {
|
|
577
|
+
const schemaId = toolArgs.schema;
|
|
578
|
+
const limit = toolArgs.limit ?? 20;
|
|
579
|
+
const offset = toolArgs.offset ?? 0;
|
|
580
|
+
const nodes = await this.config.store.list({ schemaId, limit, offset });
|
|
581
|
+
result = { nodes, count: nodes.length };
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case "xnet_get": {
|
|
585
|
+
const nodeId = toolArgs.nodeId;
|
|
586
|
+
const node = await this.config.store.get(nodeId);
|
|
587
|
+
if (!node) {
|
|
588
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
589
|
+
}
|
|
590
|
+
result = node;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case "xnet_create": {
|
|
594
|
+
const schema = toolArgs.schema;
|
|
595
|
+
const properties = toolArgs.properties;
|
|
596
|
+
const node = await this.config.store.create({
|
|
597
|
+
schemaId: schema,
|
|
598
|
+
properties
|
|
599
|
+
});
|
|
600
|
+
result = node;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case "xnet_update": {
|
|
604
|
+
const nodeId = toolArgs.nodeId;
|
|
605
|
+
const properties = toolArgs.properties;
|
|
606
|
+
const existing = await this.config.store.get(nodeId);
|
|
607
|
+
if (!existing) {
|
|
608
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
609
|
+
}
|
|
610
|
+
const node = await this.config.store.update(nodeId, { properties });
|
|
611
|
+
result = node;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
case "xnet_delete": {
|
|
615
|
+
const nodeId = toolArgs.nodeId;
|
|
616
|
+
const existing = await this.config.store.get(nodeId);
|
|
617
|
+
if (!existing) {
|
|
618
|
+
throw new Error(`Node not found: ${nodeId}`);
|
|
619
|
+
}
|
|
620
|
+
await this.config.store.delete(nodeId);
|
|
621
|
+
result = { success: true, nodeId };
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case "xnet_schemas": {
|
|
625
|
+
const iris = this.config.schemas.getAllIRIs();
|
|
626
|
+
const schemas = await Promise.all(
|
|
627
|
+
iris.map(async (iri) => {
|
|
628
|
+
const schema = await this.config.schemas.get(iri);
|
|
629
|
+
return schema ? {
|
|
630
|
+
iri,
|
|
631
|
+
name: schema.name,
|
|
632
|
+
properties: schema.properties
|
|
633
|
+
} : null;
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
result = { schemas: schemas.filter(Boolean) };
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
default:
|
|
640
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
// ─── Resource Handling ─────────────────────────────────────────────────────
|
|
647
|
+
async handleResourceRead(params) {
|
|
648
|
+
const { uri } = params;
|
|
649
|
+
let content;
|
|
650
|
+
if (uri === "xnet://nodes") {
|
|
651
|
+
const nodes = await this.config.store.list({ limit: 100 });
|
|
652
|
+
content = { nodes, count: nodes.length };
|
|
653
|
+
} else if (uri === "xnet://schemas") {
|
|
654
|
+
const iris = this.config.schemas.getAllIRIs();
|
|
655
|
+
const schemas = await Promise.all(
|
|
656
|
+
iris.map(async (iri) => {
|
|
657
|
+
const schema = await this.config.schemas.get(iri);
|
|
658
|
+
return schema ? { iri, name: schema.name } : null;
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
content = { schemas: schemas.filter(Boolean) };
|
|
662
|
+
} else {
|
|
663
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
contents: [
|
|
667
|
+
{
|
|
668
|
+
uri,
|
|
669
|
+
mimeType: "application/json",
|
|
670
|
+
text: JSON.stringify(content, null, 2)
|
|
671
|
+
}
|
|
672
|
+
]
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
function createMCPServer(config) {
|
|
677
|
+
return new MCPServer(config);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/services/process-manager.ts
|
|
681
|
+
function isNodeEnvironment() {
|
|
682
|
+
return typeof process !== "undefined" && typeof process.versions !== "undefined" && typeof process.versions.node !== "undefined";
|
|
683
|
+
}
|
|
684
|
+
var ManagedProcess = class {
|
|
685
|
+
constructor(definition) {
|
|
686
|
+
this.definition = definition;
|
|
687
|
+
this.status = {
|
|
688
|
+
id: definition.id,
|
|
689
|
+
state: "stopped",
|
|
690
|
+
restartCount: 0
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
process = null;
|
|
694
|
+
status;
|
|
695
|
+
restartTimer = null;
|
|
696
|
+
healthCheckTimer = null;
|
|
697
|
+
listeners = { status: [], output: [] };
|
|
698
|
+
/**
|
|
699
|
+
* Start the service process
|
|
700
|
+
*/
|
|
701
|
+
async start() {
|
|
702
|
+
if (this.status.state === "running" || this.status.state === "starting") {
|
|
703
|
+
return this.status;
|
|
704
|
+
}
|
|
705
|
+
if (!isNodeEnvironment()) {
|
|
706
|
+
throw new Error("ProcessManager can only run in Node.js environment");
|
|
707
|
+
}
|
|
708
|
+
const { spawn } = await import("child_process");
|
|
709
|
+
this.updateState("starting");
|
|
710
|
+
const { command, args = [], cwd, env, shell } = this.definition.process;
|
|
711
|
+
const { protocol } = this.definition.communication;
|
|
712
|
+
try {
|
|
713
|
+
this.process = spawn(command, args, {
|
|
714
|
+
cwd,
|
|
715
|
+
env: { ...process.env, ...env },
|
|
716
|
+
shell: shell ?? false,
|
|
717
|
+
stdio: protocol === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
|
|
718
|
+
});
|
|
719
|
+
this.status.pid = this.process.pid;
|
|
720
|
+
this.process.stdout?.on("data", (data) => {
|
|
721
|
+
this.emitOutput("stdout", data.toString());
|
|
722
|
+
});
|
|
723
|
+
this.process.stderr?.on("data", (data) => {
|
|
724
|
+
this.emitOutput("stderr", data.toString());
|
|
725
|
+
});
|
|
726
|
+
this.process.on("exit", (code, signal) => {
|
|
727
|
+
this.handleExit(code, signal);
|
|
728
|
+
});
|
|
729
|
+
this.process.on("error", (err) => {
|
|
730
|
+
this.status.lastError = err.message;
|
|
731
|
+
this.updateState("error");
|
|
732
|
+
});
|
|
733
|
+
await this.waitForHealthy();
|
|
734
|
+
this.updateState("running");
|
|
735
|
+
this.status.startedAt = Date.now();
|
|
736
|
+
this.startHealthCheck();
|
|
737
|
+
return this.getStatus();
|
|
738
|
+
} catch (err) {
|
|
739
|
+
this.status.lastError = err instanceof Error ? err.message : String(err);
|
|
740
|
+
this.updateState("error");
|
|
741
|
+
throw err;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Stop the service process
|
|
746
|
+
*/
|
|
747
|
+
async stop() {
|
|
748
|
+
if (this.status.state === "stopped" || this.status.state === "stopping") {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
this.updateState("stopping");
|
|
752
|
+
this.clearTimers();
|
|
753
|
+
if (!this.process) {
|
|
754
|
+
this.updateState("stopped");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const shutdownTimeout = this.definition.lifecycle.shutdownTimeoutMs ?? 5e3;
|
|
758
|
+
return new Promise((resolve) => {
|
|
759
|
+
const timeout = setTimeout(() => {
|
|
760
|
+
this.process?.kill("SIGKILL");
|
|
761
|
+
this.process = null;
|
|
762
|
+
this.updateState("stopped");
|
|
763
|
+
resolve();
|
|
764
|
+
}, shutdownTimeout);
|
|
765
|
+
this.process.on("exit", () => {
|
|
766
|
+
clearTimeout(timeout);
|
|
767
|
+
this.process = null;
|
|
768
|
+
this.updateState("stopped");
|
|
769
|
+
resolve();
|
|
770
|
+
});
|
|
771
|
+
this.process.kill("SIGTERM");
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Get current status
|
|
776
|
+
*/
|
|
777
|
+
getStatus() {
|
|
778
|
+
const status = { ...this.status };
|
|
779
|
+
if (status.state === "running" && status.startedAt) {
|
|
780
|
+
status.uptime = Date.now() - status.startedAt;
|
|
781
|
+
}
|
|
782
|
+
return status;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Subscribe to status changes
|
|
786
|
+
*/
|
|
787
|
+
onStatus(callback) {
|
|
788
|
+
this.listeners.status.push(callback);
|
|
789
|
+
return () => {
|
|
790
|
+
const idx = this.listeners.status.indexOf(callback);
|
|
791
|
+
if (idx >= 0) this.listeners.status.splice(idx, 1);
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Subscribe to output events
|
|
796
|
+
*/
|
|
797
|
+
onOutput(callback) {
|
|
798
|
+
this.listeners.output.push(callback);
|
|
799
|
+
return () => {
|
|
800
|
+
const idx = this.listeners.output.indexOf(callback);
|
|
801
|
+
if (idx >= 0) this.listeners.output.splice(idx, 1);
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
// ─── Private Methods ─────────────────────────────────────────────────────────
|
|
805
|
+
updateState(state) {
|
|
806
|
+
const previousState = this.status.state;
|
|
807
|
+
this.status.state = state;
|
|
808
|
+
const event = {
|
|
809
|
+
serviceId: this.definition.id,
|
|
810
|
+
status: this.getStatus(),
|
|
811
|
+
previousState
|
|
812
|
+
};
|
|
813
|
+
for (const listener of this.listeners.status) {
|
|
814
|
+
try {
|
|
815
|
+
listener(event);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
console.error("[ManagedProcess] Status listener error:", err);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
emitOutput(stream, data) {
|
|
822
|
+
const event = {
|
|
823
|
+
serviceId: this.definition.id,
|
|
824
|
+
stream,
|
|
825
|
+
data,
|
|
826
|
+
timestamp: Date.now()
|
|
827
|
+
};
|
|
828
|
+
for (const listener of this.listeners.output) {
|
|
829
|
+
try {
|
|
830
|
+
listener(event);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
console.error("[ManagedProcess] Output listener error:", err);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
handleExit(code, signal) {
|
|
837
|
+
if (this.status.state === "stopping") {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const { restart, maxRestarts = 5, restartDelayMs = 1e3 } = this.definition.lifecycle;
|
|
841
|
+
const shouldRestart = restart === "always" || restart === "on-failure" && code !== 0;
|
|
842
|
+
if (shouldRestart && this.status.restartCount < maxRestarts) {
|
|
843
|
+
this.status.restartCount++;
|
|
844
|
+
this.status.lastError = `Exited with code ${code}, signal ${signal}. Restarting...`;
|
|
845
|
+
this.updateState("starting");
|
|
846
|
+
this.restartTimer = setTimeout(() => {
|
|
847
|
+
this.start().catch((err) => {
|
|
848
|
+
console.error(`[ManagedProcess] Restart failed for ${this.definition.id}:`, err);
|
|
849
|
+
});
|
|
850
|
+
}, restartDelayMs);
|
|
851
|
+
} else {
|
|
852
|
+
this.status.lastError = `Exited with code ${code}, signal ${signal}`;
|
|
853
|
+
this.updateState("error");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async waitForHealthy() {
|
|
857
|
+
const { healthCheck, startTimeoutMs = 1e4 } = this.definition.lifecycle;
|
|
858
|
+
const start = Date.now();
|
|
859
|
+
while (Date.now() - start < startTimeoutMs) {
|
|
860
|
+
if (!healthCheck) {
|
|
861
|
+
await this.delay(500);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const healthy = await this.checkHealth();
|
|
865
|
+
if (healthy) return;
|
|
866
|
+
await this.delay(500);
|
|
867
|
+
}
|
|
868
|
+
throw new Error(`Service '${this.definition.id}' health check timed out`);
|
|
869
|
+
}
|
|
870
|
+
async checkHealth() {
|
|
871
|
+
const { healthCheck } = this.definition.lifecycle;
|
|
872
|
+
if (!healthCheck) return true;
|
|
873
|
+
try {
|
|
874
|
+
switch (healthCheck.type) {
|
|
875
|
+
case "http": {
|
|
876
|
+
if (!healthCheck.url) return false;
|
|
877
|
+
const controller = new AbortController();
|
|
878
|
+
const timeout = setTimeout(() => controller.abort(), healthCheck.timeoutMs ?? 5e3);
|
|
879
|
+
try {
|
|
880
|
+
const res = await fetch(healthCheck.url, { signal: controller.signal });
|
|
881
|
+
return res.ok;
|
|
882
|
+
} finally {
|
|
883
|
+
clearTimeout(timeout);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
case "tcp": {
|
|
887
|
+
const port = healthCheck.port ?? this.definition.communication.port;
|
|
888
|
+
if (!port) return false;
|
|
889
|
+
const { createConnection } = await import("net");
|
|
890
|
+
return new Promise((resolve) => {
|
|
891
|
+
const socket = createConnection(
|
|
892
|
+
{ port, host: this.definition.communication.host ?? "127.0.0.1" },
|
|
893
|
+
() => {
|
|
894
|
+
socket.destroy();
|
|
895
|
+
resolve(true);
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
socket.on("error", () => resolve(false));
|
|
899
|
+
socket.setTimeout(healthCheck.timeoutMs ?? 5e3, () => {
|
|
900
|
+
socket.destroy();
|
|
901
|
+
resolve(false);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
case "stdout": {
|
|
906
|
+
return this.process?.pid !== void 0;
|
|
907
|
+
}
|
|
908
|
+
default:
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
startHealthCheck() {
|
|
916
|
+
const { healthCheck } = this.definition.lifecycle;
|
|
917
|
+
if (!healthCheck || !healthCheck.intervalMs) return;
|
|
918
|
+
this.healthCheckTimer = setInterval(async () => {
|
|
919
|
+
if (this.status.state !== "running") return;
|
|
920
|
+
const healthy = await this.checkHealth();
|
|
921
|
+
if (!healthy) {
|
|
922
|
+
this.status.lastError = "Health check failed";
|
|
923
|
+
}
|
|
924
|
+
}, healthCheck.intervalMs);
|
|
925
|
+
}
|
|
926
|
+
clearTimers() {
|
|
927
|
+
if (this.restartTimer) {
|
|
928
|
+
clearTimeout(this.restartTimer);
|
|
929
|
+
this.restartTimer = null;
|
|
930
|
+
}
|
|
931
|
+
if (this.healthCheckTimer) {
|
|
932
|
+
clearInterval(this.healthCheckTimer);
|
|
933
|
+
this.healthCheckTimer = null;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
delay(ms) {
|
|
937
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
var ProcessManager = class {
|
|
941
|
+
processes = /* @__PURE__ */ new Map();
|
|
942
|
+
listeners = {
|
|
943
|
+
"service:status": [],
|
|
944
|
+
"service:output": [],
|
|
945
|
+
"service:error": []
|
|
946
|
+
};
|
|
947
|
+
/**
|
|
948
|
+
* Start a service
|
|
949
|
+
*/
|
|
950
|
+
async start(definition) {
|
|
951
|
+
if (this.processes.has(definition.id)) {
|
|
952
|
+
throw new Error(`Service '${definition.id}' is already registered`);
|
|
953
|
+
}
|
|
954
|
+
const managed = new ManagedProcess(definition);
|
|
955
|
+
this.processes.set(definition.id, managed);
|
|
956
|
+
managed.onStatus((event) => {
|
|
957
|
+
this.emit("service:status", event);
|
|
958
|
+
});
|
|
959
|
+
managed.onOutput((event) => {
|
|
960
|
+
this.emit("service:output", event);
|
|
961
|
+
});
|
|
962
|
+
try {
|
|
963
|
+
return await managed.start();
|
|
964
|
+
} catch (err) {
|
|
965
|
+
this.emit("service:error", {
|
|
966
|
+
serviceId: definition.id,
|
|
967
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
968
|
+
});
|
|
969
|
+
throw err;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Stop a service
|
|
974
|
+
*/
|
|
975
|
+
async stop(serviceId) {
|
|
976
|
+
const managed = this.processes.get(serviceId);
|
|
977
|
+
if (!managed) return;
|
|
978
|
+
await managed.stop();
|
|
979
|
+
this.processes.delete(serviceId);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Restart a service
|
|
983
|
+
*/
|
|
984
|
+
async restart(serviceId) {
|
|
985
|
+
const managed = this.processes.get(serviceId);
|
|
986
|
+
if (!managed) {
|
|
987
|
+
throw new Error(`Service '${serviceId}' not found`);
|
|
988
|
+
}
|
|
989
|
+
await managed.stop();
|
|
990
|
+
return managed.start();
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Get service status
|
|
994
|
+
*/
|
|
995
|
+
getStatus(serviceId) {
|
|
996
|
+
return this.processes.get(serviceId)?.getStatus();
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Get all service statuses
|
|
1000
|
+
*/
|
|
1001
|
+
getAllStatuses() {
|
|
1002
|
+
return [...this.processes.values()].map((p) => p.getStatus());
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Stop all services
|
|
1006
|
+
*/
|
|
1007
|
+
async stopAll() {
|
|
1008
|
+
await Promise.all([...this.processes.keys()].map((id) => this.stop(id)));
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Subscribe to events
|
|
1012
|
+
*/
|
|
1013
|
+
on(event, listener) {
|
|
1014
|
+
this.listeners[event].push(listener);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Unsubscribe from events
|
|
1018
|
+
*/
|
|
1019
|
+
off(event, listener) {
|
|
1020
|
+
const listeners = this.listeners[event];
|
|
1021
|
+
const idx = listeners.indexOf(listener);
|
|
1022
|
+
if (idx >= 0) listeners.splice(idx, 1);
|
|
1023
|
+
}
|
|
1024
|
+
emit(event, ...args) {
|
|
1025
|
+
const listeners = this.listeners[event];
|
|
1026
|
+
for (const listener of listeners) {
|
|
1027
|
+
try {
|
|
1028
|
+
const fn = listener;
|
|
1029
|
+
fn(...args);
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
console.error(`[ProcessManager] Event listener error for '${event}':`, err);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// src/services/client.ts
|
|
1038
|
+
var SERVICE_IPC_CHANNELS = {
|
|
1039
|
+
START: "xnet:service:start",
|
|
1040
|
+
STOP: "xnet:service:stop",
|
|
1041
|
+
RESTART: "xnet:service:restart",
|
|
1042
|
+
STATUS: "xnet:service:status",
|
|
1043
|
+
LIST_ALL: "xnet:service:list-all",
|
|
1044
|
+
CALL: "xnet:service:call",
|
|
1045
|
+
STATUS_UPDATE: "xnet:service:status-update",
|
|
1046
|
+
OUTPUT: "xnet:service:output"
|
|
1047
|
+
};
|
|
1048
|
+
export {
|
|
1049
|
+
LocalAPIServer,
|
|
1050
|
+
MCPServer,
|
|
1051
|
+
ProcessManager,
|
|
1052
|
+
SERVICE_IPC_CHANNELS,
|
|
1053
|
+
createLocalAPI,
|
|
1054
|
+
createMCPServer
|
|
1055
|
+
};
|