@context-engine-bridge/context-engine-mcp-bridge 0.0.3 → 0.0.5
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 +1 -1
- package/src/mcpServer.js +278 -57
package/package.json
CHANGED
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;
|
|
@@ -104,6 +120,118 @@ function selectClientForTool(name, indexerClient, memoryClient) {
|
|
|
104
120
|
}
|
|
105
121
|
return indexerClient;
|
|
106
122
|
}
|
|
123
|
+
|
|
124
|
+
function isSessionError(error) {
|
|
125
|
+
try {
|
|
126
|
+
const msg =
|
|
127
|
+
(error && typeof error.message === "string" && error.message) ||
|
|
128
|
+
(typeof error === "string" ? error : String(error || ""));
|
|
129
|
+
if (!msg) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
msg.includes("No valid session ID") ||
|
|
134
|
+
msg.includes("Mcp-Session-Id header is required") ||
|
|
135
|
+
msg.includes("Server not initialized") ||
|
|
136
|
+
msg.includes("Session not found")
|
|
137
|
+
);
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getBridgeRetryAttempts() {
|
|
144
|
+
try {
|
|
145
|
+
const raw = process.env.CTXCE_TOOL_RETRY_ATTEMPTS;
|
|
146
|
+
if (!raw) {
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
150
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
return parsed;
|
|
154
|
+
} catch {
|
|
155
|
+
return 2;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getBridgeRetryDelayMs() {
|
|
160
|
+
try {
|
|
161
|
+
const raw = process.env.CTXCE_TOOL_RETRY_DELAY_MSEC;
|
|
162
|
+
if (!raw) {
|
|
163
|
+
return 200;
|
|
164
|
+
}
|
|
165
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
166
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
} catch {
|
|
171
|
+
return 200;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isTransientToolError(error) {
|
|
176
|
+
try {
|
|
177
|
+
const msg =
|
|
178
|
+
(error && typeof error.message === "string" && error.message) ||
|
|
179
|
+
(typeof error === "string" ? error : String(error || ""));
|
|
180
|
+
if (!msg) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const lower = msg.toLowerCase();
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
lower.includes("timed out") ||
|
|
187
|
+
lower.includes("timeout") ||
|
|
188
|
+
lower.includes("time-out")
|
|
189
|
+
) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
lower.includes("econnreset") ||
|
|
195
|
+
lower.includes("econnrefused") ||
|
|
196
|
+
lower.includes("etimedout") ||
|
|
197
|
+
lower.includes("enotfound") ||
|
|
198
|
+
lower.includes("ehostunreach") ||
|
|
199
|
+
lower.includes("enetunreach")
|
|
200
|
+
) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
lower.includes("bad gateway") ||
|
|
206
|
+
lower.includes("gateway timeout") ||
|
|
207
|
+
lower.includes("service unavailable") ||
|
|
208
|
+
lower.includes(" 502 ") ||
|
|
209
|
+
lower.includes(" 503 ") ||
|
|
210
|
+
lower.includes(" 504 ")
|
|
211
|
+
) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (lower.includes("network error")) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (typeof error.code === "number" && error.code === -32001 && !isSessionError(error)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
typeof error.code === "string" &&
|
|
224
|
+
error.code.toLowerCase &&
|
|
225
|
+
error.code.toLowerCase().includes("timeout")
|
|
226
|
+
) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return false;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
107
235
|
// MCP stdio server implemented using the official MCP TypeScript SDK.
|
|
108
236
|
// Acts as a low-level proxy for tools, forwarding tools/list and tools/call
|
|
109
237
|
// to the remote qdrant-indexer MCP server while adding a local `ping` tool.
|
|
@@ -146,52 +274,8 @@ async function createBridgeServer(options) {
|
|
|
146
274
|
);
|
|
147
275
|
}
|
|
148
276
|
|
|
149
|
-
|
|
150
|
-
const indexerTransport = new StreamableHTTPClientTransport(indexerUrl);
|
|
151
|
-
const indexerClient = new Client(
|
|
152
|
-
{
|
|
153
|
-
name: "ctx-context-engine-bridge-http-client",
|
|
154
|
-
version: "0.0.1",
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
capabilities: {
|
|
158
|
-
tools: {},
|
|
159
|
-
resources: {},
|
|
160
|
-
prompts: {},
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
await indexerClient.connect(indexerTransport);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
debugLog("[ctxce] Failed to connect MCP HTTP client to indexer: " + String(err));
|
|
169
|
-
}
|
|
170
|
-
|
|
277
|
+
let indexerClient = null;
|
|
171
278
|
let memoryClient = null;
|
|
172
|
-
if (memoryUrl) {
|
|
173
|
-
try {
|
|
174
|
-
const memoryTransport = new StreamableHTTPClientTransport(memoryUrl);
|
|
175
|
-
memoryClient = new Client(
|
|
176
|
-
{
|
|
177
|
-
name: "ctx-context-engine-bridge-memory-client",
|
|
178
|
-
version: "0.0.1",
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
capabilities: {
|
|
182
|
-
tools: {},
|
|
183
|
-
resources: {},
|
|
184
|
-
prompts: {},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
|
-
await memoryClient.connect(memoryTransport);
|
|
189
|
-
debugLog(`[ctxce] Connected memory MCP client: ${memoryUrl}`);
|
|
190
|
-
} catch (err) {
|
|
191
|
-
debugLog("[ctxce] Failed to connect memory MCP client: " + String(err));
|
|
192
|
-
memoryClient = null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
279
|
|
|
196
280
|
// Derive a simple session identifier for this bridge process. In the
|
|
197
281
|
// future this can be made user-aware (e.g. from auth), but for now we
|
|
@@ -213,13 +297,81 @@ async function createBridgeServer(options) {
|
|
|
213
297
|
defaultsPayload.under = defaultUnder;
|
|
214
298
|
}
|
|
215
299
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
300
|
+
async function initializeRemoteClients(forceRecreate = false) {
|
|
301
|
+
if (!forceRecreate && indexerClient) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (forceRecreate) {
|
|
306
|
+
try {
|
|
307
|
+
debugLog("[ctxce] Reinitializing remote MCP clients after session error.");
|
|
308
|
+
} catch {
|
|
309
|
+
// ignore logging failures
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let nextIndexerClient = null;
|
|
314
|
+
try {
|
|
315
|
+
const indexerTransport = new StreamableHTTPClientTransport(indexerUrl);
|
|
316
|
+
const client = new Client(
|
|
317
|
+
{
|
|
318
|
+
name: "ctx-context-engine-bridge-http-client",
|
|
319
|
+
version: "0.0.1",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
capabilities: {
|
|
323
|
+
tools: {},
|
|
324
|
+
resources: {},
|
|
325
|
+
prompts: {},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
await client.connect(indexerTransport);
|
|
330
|
+
nextIndexerClient = client;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
debugLog("[ctxce] Failed to connect MCP HTTP client to indexer: " + String(err));
|
|
333
|
+
nextIndexerClient = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let nextMemoryClient = null;
|
|
337
|
+
if (memoryUrl) {
|
|
338
|
+
try {
|
|
339
|
+
const memoryTransport = new StreamableHTTPClientTransport(memoryUrl);
|
|
340
|
+
const client = new Client(
|
|
341
|
+
{
|
|
342
|
+
name: "ctx-context-engine-bridge-memory-client",
|
|
343
|
+
version: "0.0.1",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
capabilities: {
|
|
347
|
+
tools: {},
|
|
348
|
+
resources: {},
|
|
349
|
+
prompts: {},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
await client.connect(memoryTransport);
|
|
354
|
+
debugLog(`[ctxce] Connected memory MCP client: ${memoryUrl}`);
|
|
355
|
+
nextMemoryClient = client;
|
|
356
|
+
} catch (err) {
|
|
357
|
+
debugLog("[ctxce] Failed to connect memory MCP client: " + String(err));
|
|
358
|
+
nextMemoryClient = null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
indexerClient = nextIndexerClient;
|
|
363
|
+
memoryClient = nextMemoryClient;
|
|
364
|
+
|
|
365
|
+
if (Object.keys(defaultsPayload).length > 1 && indexerClient) {
|
|
366
|
+
await sendSessionDefaults(indexerClient, defaultsPayload, "indexer");
|
|
367
|
+
if (memoryClient) {
|
|
368
|
+
await sendSessionDefaults(memoryClient, defaultsPayload, "memory");
|
|
369
|
+
}
|
|
220
370
|
}
|
|
221
371
|
}
|
|
222
372
|
|
|
373
|
+
await initializeRemoteClients(false);
|
|
374
|
+
|
|
223
375
|
const server = new Server( // TODO: marked as depreciated
|
|
224
376
|
{
|
|
225
377
|
name: "ctx-context-engine-bridge",
|
|
@@ -237,6 +389,10 @@ async function createBridgeServer(options) {
|
|
|
237
389
|
let remote;
|
|
238
390
|
try {
|
|
239
391
|
debugLog("[ctxce] tools/list: fetching tools from indexer");
|
|
392
|
+
await initializeRemoteClients(false);
|
|
393
|
+
if (!indexerClient) {
|
|
394
|
+
throw new Error("Indexer MCP client not initialized");
|
|
395
|
+
}
|
|
240
396
|
remote = await withTimeout(
|
|
241
397
|
indexerClient.listTools(),
|
|
242
398
|
10000,
|
|
@@ -295,16 +451,60 @@ async function createBridgeServer(options) {
|
|
|
295
451
|
return indexerResult;
|
|
296
452
|
}
|
|
297
453
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
454
|
+
await initializeRemoteClients(false);
|
|
455
|
+
|
|
456
|
+
const timeoutMs = getBridgeToolTimeoutMs();
|
|
457
|
+
const maxAttempts = getBridgeRetryAttempts();
|
|
458
|
+
const retryDelayMs = getBridgeRetryDelayMs();
|
|
459
|
+
let sessionRetried = false;
|
|
460
|
+
let lastError;
|
|
461
|
+
|
|
462
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
463
|
+
if (attempt > 0 && retryDelayMs > 0) {
|
|
464
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const targetClient = selectClientForTool(name, indexerClient, memoryClient);
|
|
468
|
+
if (!targetClient) {
|
|
469
|
+
throw new Error(`Tool ${name} not available on any configured MCP server`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const result = await targetClient.callTool(
|
|
474
|
+
{
|
|
475
|
+
name,
|
|
476
|
+
arguments: args,
|
|
477
|
+
},
|
|
478
|
+
undefined,
|
|
479
|
+
{ timeout: timeoutMs },
|
|
480
|
+
);
|
|
481
|
+
return result;
|
|
482
|
+
} catch (err) {
|
|
483
|
+
lastError = err;
|
|
484
|
+
|
|
485
|
+
if (isSessionError(err) && !sessionRetried) {
|
|
486
|
+
debugLog(
|
|
487
|
+
"[ctxce] tools/call: detected remote MCP session error; reinitializing clients and retrying once: " +
|
|
488
|
+
String(err),
|
|
489
|
+
);
|
|
490
|
+
await initializeRemoteClients(true);
|
|
491
|
+
sessionRetried = true;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!isTransientToolError(err) || attempt === maxAttempts - 1) {
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
debugLog(
|
|
500
|
+
`[ctxce] tools/call: transient error (attempt ${attempt + 1}/${maxAttempts}), retrying: ` +
|
|
501
|
+
String(err),
|
|
502
|
+
);
|
|
503
|
+
// Loop will retry
|
|
504
|
+
}
|
|
301
505
|
}
|
|
302
506
|
|
|
303
|
-
|
|
304
|
-
name,
|
|
305
|
-
arguments: args,
|
|
306
|
-
});
|
|
307
|
-
return result;
|
|
507
|
+
throw lastError || new Error("Unknown MCP tools/call error");
|
|
308
508
|
});
|
|
309
509
|
|
|
310
510
|
return server;
|
|
@@ -314,6 +514,27 @@ export async function runMcpServer(options) {
|
|
|
314
514
|
const server = await createBridgeServer(options);
|
|
315
515
|
const transport = new StdioServerTransport();
|
|
316
516
|
await server.connect(transport);
|
|
517
|
+
|
|
518
|
+
const exitOnStdinClose = process.env.CTXCE_EXIT_ON_STDIN_CLOSE !== "0";
|
|
519
|
+
if (exitOnStdinClose) {
|
|
520
|
+
const handleStdioClosed = () => {
|
|
521
|
+
try {
|
|
522
|
+
debugLog("[ctxce] Stdio transport closed; exiting MCP bridge process.");
|
|
523
|
+
} catch {
|
|
524
|
+
// ignore
|
|
525
|
+
}
|
|
526
|
+
// Allow any in-flight logs to flush, then exit.
|
|
527
|
+
setTimeout(() => {
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}, 10).unref();
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
if (process.stdin && typeof process.stdin.on === "function") {
|
|
533
|
+
process.stdin.on("end", handleStdioClosed);
|
|
534
|
+
process.stdin.on("close", handleStdioClosed);
|
|
535
|
+
process.stdin.on("error", handleStdioClosed);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
317
538
|
}
|
|
318
539
|
|
|
319
540
|
export async function runHttpMcpServer(options) {
|