@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/mcpServer.js +278 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
5
5
  "bin": {
6
6
  "ctxce": "bin/ctxce.js",
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
- // High-level MCP client for the remote HTTP /mcp indexer
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
- if (Object.keys(defaultsPayload).length > 1) {
217
- await sendSessionDefaults(indexerClient, defaultsPayload, "indexer");
218
- if (memoryClient) {
219
- await sendSessionDefaults(memoryClient, defaultsPayload, "memory");
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
- const targetClient = selectClientForTool(name, indexerClient, memoryClient);
299
- if (!targetClient) {
300
- throw new Error(`Tool ${name} not available on any configured MCP server`);
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
- const result = await targetClient.callTool({
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) {