@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.
- package/package.json +1 -1
- package/src/mcpServer.js +241 -62
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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;
|