@context-engine-bridge/context-engine-mcp-bridge 0.0.4 → 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 +241 -62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.4",
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
@@ -120,6 +120,118 @@ function selectClientForTool(name, indexerClient, memoryClient) {
120
120
  }
121
121
  return indexerClient;
122
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
+ }
123
235
  // MCP stdio server implemented using the official MCP TypeScript SDK.
124
236
  // Acts as a low-level proxy for tools, forwarding tools/list and tools/call
125
237
  // to the remote qdrant-indexer MCP server while adding a local `ping` tool.
@@ -162,52 +274,8 @@ async function createBridgeServer(options) {
162
274
  );
163
275
  }
164
276
 
165
- // High-level MCP client for the remote HTTP /mcp indexer
166
- const indexerTransport = new StreamableHTTPClientTransport(indexerUrl);
167
- const indexerClient = new Client(
168
- {
169
- name: "ctx-context-engine-bridge-http-client",
170
- version: "0.0.1",
171
- },
172
- {
173
- capabilities: {
174
- tools: {},
175
- resources: {},
176
- prompts: {},
177
- },
178
- },
179
- );
180
-
181
- try {
182
- await indexerClient.connect(indexerTransport);
183
- } catch (err) {
184
- debugLog("[ctxce] Failed to connect MCP HTTP client to indexer: " + String(err));
185
- }
186
-
277
+ let indexerClient = null;
187
278
  let memoryClient = null;
188
- if (memoryUrl) {
189
- try {
190
- const memoryTransport = new StreamableHTTPClientTransport(memoryUrl);
191
- memoryClient = new Client(
192
- {
193
- name: "ctx-context-engine-bridge-memory-client",
194
- version: "0.0.1",
195
- },
196
- {
197
- capabilities: {
198
- tools: {},
199
- resources: {},
200
- prompts: {},
201
- },
202
- },
203
- );
204
- await memoryClient.connect(memoryTransport);
205
- debugLog(`[ctxce] Connected memory MCP client: ${memoryUrl}`);
206
- } catch (err) {
207
- debugLog("[ctxce] Failed to connect memory MCP client: " + String(err));
208
- memoryClient = null;
209
- }
210
- }
211
279
 
212
280
  // Derive a simple session identifier for this bridge process. In the
213
281
  // future this can be made user-aware (e.g. from auth), but for now we
@@ -229,13 +297,81 @@ async function createBridgeServer(options) {
229
297
  defaultsPayload.under = defaultUnder;
230
298
  }
231
299
 
232
- if (Object.keys(defaultsPayload).length > 1) {
233
- await sendSessionDefaults(indexerClient, defaultsPayload, "indexer");
234
- if (memoryClient) {
235
- 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
+ }
236
370
  }
237
371
  }
238
372
 
373
+ await initializeRemoteClients(false);
374
+
239
375
  const server = new Server( // TODO: marked as depreciated
240
376
  {
241
377
  name: "ctx-context-engine-bridge",
@@ -253,6 +389,10 @@ async function createBridgeServer(options) {
253
389
  let remote;
254
390
  try {
255
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
+ }
256
396
  remote = await withTimeout(
257
397
  indexerClient.listTools(),
258
398
  10000,
@@ -311,21 +451,60 @@ async function createBridgeServer(options) {
311
451
  return indexerResult;
312
452
  }
313
453
 
314
- const targetClient = selectClientForTool(name, indexerClient, memoryClient);
315
- if (!targetClient) {
316
- throw new Error(`Tool ${name} not available on any configured MCP server`);
317
- }
454
+ await initializeRemoteClients(false);
318
455
 
319
456
  const timeoutMs = getBridgeToolTimeoutMs();
320
- const result = await targetClient.callTool(
321
- {
322
- name,
323
- arguments: args,
324
- },
325
- undefined,
326
- { timeout: timeoutMs },
327
- );
328
- return result;
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
+ }
505
+ }
506
+
507
+ throw lastError || new Error("Unknown MCP tools/call error");
329
508
  });
330
509
 
331
510
  return server;