@context-engine-bridge/context-engine-mcp-bridge 0.0.2 → 0.0.4
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/package.json +2 -2
- package/src/cli.js +52 -2
- package/src/mcpServer.js +155 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@context-engine-bridge/context-engine-mcp-bridge",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Context Engine MCP bridge (stdio proxy combining indexer + memory servers)",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ctxce": "bin/ctxce.js",
|
|
7
7
|
"ctxce-bridge": "bin/ctxce.js"
|
package/src/cli.js
CHANGED
|
@@ -3,12 +3,62 @@
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { runMcpServer } from "./mcpServer.js";
|
|
6
|
+
import { runMcpServer, runHttpMcpServer } from "./mcpServer.js";
|
|
7
7
|
|
|
8
8
|
export async function runCli() {
|
|
9
9
|
const argv = process.argv.slice(2);
|
|
10
10
|
const cmd = argv[0];
|
|
11
11
|
|
|
12
|
+
if (cmd === "mcp-http-serve") {
|
|
13
|
+
const args = argv.slice(1);
|
|
14
|
+
let workspace = process.cwd();
|
|
15
|
+
let indexerUrl = process.env.CTXCE_INDEXER_URL || "http://localhost:8003/mcp";
|
|
16
|
+
let memoryUrl = process.env.CTXCE_MEMORY_URL || null;
|
|
17
|
+
let port = Number.parseInt(process.env.CTXCE_HTTP_PORT || "30810", 10) || 30810;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
20
|
+
const a = args[i];
|
|
21
|
+
if (a === "--workspace" || a === "--path") {
|
|
22
|
+
if (i + 1 < args.length) {
|
|
23
|
+
workspace = args[i + 1];
|
|
24
|
+
i += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (a === "--indexer-url") {
|
|
29
|
+
if (i + 1 < args.length) {
|
|
30
|
+
indexerUrl = args[i + 1];
|
|
31
|
+
i += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (a === "--memory-url") {
|
|
36
|
+
if (i + 1 < args.length) {
|
|
37
|
+
memoryUrl = args[i + 1];
|
|
38
|
+
i += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (a === "--port") {
|
|
43
|
+
if (i + 1 < args.length) {
|
|
44
|
+
const parsed = Number.parseInt(args[i + 1], 10);
|
|
45
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
46
|
+
port = parsed;
|
|
47
|
+
}
|
|
48
|
+
i += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.error(
|
|
56
|
+
`[ctxce] Starting HTTP MCP bridge: workspace=${workspace}, port=${port}, indexerUrl=${indexerUrl}, memoryUrl=${memoryUrl || "disabled"}`,
|
|
57
|
+
);
|
|
58
|
+
await runHttpMcpServer({ workspace, indexerUrl, memoryUrl, port });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
12
62
|
if (cmd === "mcp-serve") {
|
|
13
63
|
// Minimal flag parsing for PoC: allow passing workspace/root and indexer URL.
|
|
14
64
|
// Supported flags:
|
|
@@ -59,7 +109,7 @@ export async function runCli() {
|
|
|
59
109
|
|
|
60
110
|
// eslint-disable-next-line no-console
|
|
61
111
|
console.error(
|
|
62
|
-
`Usage: ${binName} mcp-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>]`,
|
|
112
|
+
`Usage: ${binName} mcp-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] | ${binName} mcp-http-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] [--port <port>]`,
|
|
63
113
|
);
|
|
64
114
|
process.exit(1);
|
|
65
115
|
}
|
package/src/mcpServer.js
CHANGED
|
@@ -94,6 +94,22 @@ function withTimeout(promise, ms, label) {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function getBridgeToolTimeoutMs() {
|
|
98
|
+
try {
|
|
99
|
+
const raw = process.env.CTXCE_TOOL_TIMEOUT_MSEC;
|
|
100
|
+
if (!raw) {
|
|
101
|
+
return 300000;
|
|
102
|
+
}
|
|
103
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
104
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
105
|
+
return 300000;
|
|
106
|
+
}
|
|
107
|
+
return parsed;
|
|
108
|
+
} catch {
|
|
109
|
+
return 300000;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
97
113
|
function selectClientForTool(name, indexerClient, memoryClient) {
|
|
98
114
|
if (!name) {
|
|
99
115
|
return indexerClient;
|
|
@@ -112,13 +128,15 @@ import process from "node:process";
|
|
|
112
128
|
import fs from "node:fs";
|
|
113
129
|
import path from "node:path";
|
|
114
130
|
import { execSync } from "node:child_process";
|
|
131
|
+
import { createServer } from "node:http";
|
|
115
132
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
116
133
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
134
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
117
135
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
118
136
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
119
137
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
120
138
|
|
|
121
|
-
|
|
139
|
+
async function createBridgeServer(options) {
|
|
122
140
|
const workspace = options.workspace || process.cwd();
|
|
123
141
|
const indexerUrl = options.indexerUrl;
|
|
124
142
|
const memoryUrl = options.memoryUrl;
|
|
@@ -164,7 +182,6 @@ export async function runMcpServer(options) {
|
|
|
164
182
|
await indexerClient.connect(indexerTransport);
|
|
165
183
|
} catch (err) {
|
|
166
184
|
debugLog("[ctxce] Failed to connect MCP HTTP client to indexer: " + String(err));
|
|
167
|
-
throw err;
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
let memoryClient = null;
|
|
@@ -299,15 +316,147 @@ export async function runMcpServer(options) {
|
|
|
299
316
|
throw new Error(`Tool ${name} not available on any configured MCP server`);
|
|
300
317
|
}
|
|
301
318
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
319
|
+
const timeoutMs = getBridgeToolTimeoutMs();
|
|
320
|
+
const result = await targetClient.callTool(
|
|
321
|
+
{
|
|
322
|
+
name,
|
|
323
|
+
arguments: args,
|
|
324
|
+
},
|
|
325
|
+
undefined,
|
|
326
|
+
{ timeout: timeoutMs },
|
|
327
|
+
);
|
|
306
328
|
return result;
|
|
307
329
|
});
|
|
308
330
|
|
|
331
|
+
return server;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function runMcpServer(options) {
|
|
335
|
+
const server = await createBridgeServer(options);
|
|
309
336
|
const transport = new StdioServerTransport();
|
|
310
337
|
await server.connect(transport);
|
|
338
|
+
|
|
339
|
+
const exitOnStdinClose = process.env.CTXCE_EXIT_ON_STDIN_CLOSE !== "0";
|
|
340
|
+
if (exitOnStdinClose) {
|
|
341
|
+
const handleStdioClosed = () => {
|
|
342
|
+
try {
|
|
343
|
+
debugLog("[ctxce] Stdio transport closed; exiting MCP bridge process.");
|
|
344
|
+
} catch {
|
|
345
|
+
// ignore
|
|
346
|
+
}
|
|
347
|
+
// Allow any in-flight logs to flush, then exit.
|
|
348
|
+
setTimeout(() => {
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}, 10).unref();
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (process.stdin && typeof process.stdin.on === "function") {
|
|
354
|
+
process.stdin.on("end", handleStdioClosed);
|
|
355
|
+
process.stdin.on("close", handleStdioClosed);
|
|
356
|
+
process.stdin.on("error", handleStdioClosed);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function runHttpMcpServer(options) {
|
|
362
|
+
const server = await createBridgeServer(options);
|
|
363
|
+
const port =
|
|
364
|
+
typeof options.port === "number"
|
|
365
|
+
? options.port
|
|
366
|
+
: Number.parseInt(process.env.CTXCE_HTTP_PORT || "30810", 10) || 30810;
|
|
367
|
+
|
|
368
|
+
const transport = new StreamableHTTPServerTransport({
|
|
369
|
+
sessionIdGenerator: undefined,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await server.connect(transport);
|
|
373
|
+
|
|
374
|
+
const httpServer = createServer((req, res) => {
|
|
375
|
+
try {
|
|
376
|
+
if (!req.url || !req.url.startsWith("/mcp")) {
|
|
377
|
+
res.statusCode = 404;
|
|
378
|
+
res.setHeader("Content-Type", "application/json");
|
|
379
|
+
res.end(
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
jsonrpc: "2.0",
|
|
382
|
+
error: { code: -32000, message: "Not found" },
|
|
383
|
+
id: null,
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (req.method !== "POST") {
|
|
390
|
+
res.statusCode = 405;
|
|
391
|
+
res.setHeader("Content-Type", "application/json");
|
|
392
|
+
res.end(
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
jsonrpc: "2.0",
|
|
395
|
+
error: { code: -32000, message: "Method not allowed" },
|
|
396
|
+
id: null,
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let body = "";
|
|
403
|
+
req.on("data", (chunk) => {
|
|
404
|
+
body += chunk;
|
|
405
|
+
});
|
|
406
|
+
req.on("end", async () => {
|
|
407
|
+
let parsed;
|
|
408
|
+
try {
|
|
409
|
+
parsed = body ? JSON.parse(body) : {};
|
|
410
|
+
} catch (err) {
|
|
411
|
+
debugLog("[ctxce] Failed to parse HTTP MCP request body: " + String(err));
|
|
412
|
+
res.statusCode = 400;
|
|
413
|
+
res.setHeader("Content-Type", "application/json");
|
|
414
|
+
res.end(
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
jsonrpc: "2.0",
|
|
417
|
+
error: { code: -32700, message: "Invalid JSON" },
|
|
418
|
+
id: null,
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await transport.handleRequest(req, res, parsed);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
debugLog("[ctxce] Error handling HTTP MCP request: " + String(err));
|
|
428
|
+
if (!res.headersSent) {
|
|
429
|
+
res.statusCode = 500;
|
|
430
|
+
res.setHeader("Content-Type", "application/json");
|
|
431
|
+
res.end(
|
|
432
|
+
JSON.stringify({
|
|
433
|
+
jsonrpc: "2.0",
|
|
434
|
+
error: { code: -32603, message: "Internal server error" },
|
|
435
|
+
id: null,
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
} catch (err) {
|
|
442
|
+
debugLog("[ctxce] Unexpected error in HTTP MCP server: " + String(err));
|
|
443
|
+
if (!res.headersSent) {
|
|
444
|
+
res.statusCode = 500;
|
|
445
|
+
res.setHeader("Content-Type", "application/json");
|
|
446
|
+
res.end(
|
|
447
|
+
JSON.stringify({
|
|
448
|
+
jsonrpc: "2.0",
|
|
449
|
+
error: { code: -32603, message: "Internal server error" },
|
|
450
|
+
id: null,
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
httpServer.listen(port, () => {
|
|
458
|
+
debugLog(`[ctxce] HTTP MCP bridge listening on port ${port}`);
|
|
459
|
+
});
|
|
311
460
|
}
|
|
312
461
|
|
|
313
462
|
function loadConfig(startDir) {
|