@aiwerk/mcp-bridge 2.8.26 → 2.8.28
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.
|
@@ -30,6 +30,14 @@ export declare class StandaloneServer {
|
|
|
30
30
|
/** Connect to all backend servers and discover their tools (direct mode). */
|
|
31
31
|
private discoverDirectTools;
|
|
32
32
|
private _doDiscovery;
|
|
33
|
+
/** Extract server name from a tool name like "todoist_call" or "github_call" */
|
|
34
|
+
private guessServerFromToolName;
|
|
35
|
+
/** Discover tools from a single server (lazy, per-server) */
|
|
36
|
+
private discoverSingleServer;
|
|
37
|
+
/** Save discovered tools to disk cache */
|
|
38
|
+
private saveToolCache;
|
|
39
|
+
/** Load cached tools from disk */
|
|
40
|
+
private loadToolCache;
|
|
33
41
|
private nextRequestId;
|
|
34
42
|
private createTransport;
|
|
35
43
|
/** Graceful shutdown: disconnect all backend servers. */
|
|
@@ -7,6 +7,9 @@ import { StdioTransport } from "./transport-stdio.js";
|
|
|
7
7
|
import { StreamableHttpTransport } from "./transport-streamable-http.js";
|
|
8
8
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
9
9
|
import { FileTokenStore } from "./token-store.js";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
10
13
|
/**
|
|
11
14
|
* Standalone MCP server that wraps the router.
|
|
12
15
|
* Implements the MCP protocol (initialize, tools/list, tools/call)
|
|
@@ -244,14 +247,54 @@ export class StandaloneServer {
|
|
|
244
247
|
}
|
|
245
248
|
};
|
|
246
249
|
}
|
|
247
|
-
// Direct mode:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
// Direct mode: return tools lazily
|
|
251
|
+
// If already discovered, use real tool list. Otherwise, generate placeholder
|
|
252
|
+
// tools from config descriptions (no child process startup on tools/list).
|
|
253
|
+
if (this.directTools.length > 0) {
|
|
254
|
+
const tools = this.directTools.map(t => ({
|
|
255
|
+
name: t.registeredName,
|
|
256
|
+
description: t.description,
|
|
257
|
+
inputSchema: t.inputSchema
|
|
258
|
+
}));
|
|
259
|
+
return { jsonrpc: "2.0", id, result: { tools } };
|
|
260
|
+
}
|
|
261
|
+
// Lazy: try cache first, then placeholder
|
|
262
|
+
const lazyTools = [];
|
|
263
|
+
const globalNames = new Set();
|
|
264
|
+
for (const [serverName, serverConfig] of Object.entries(this.config.servers)) {
|
|
265
|
+
const cached = this.loadToolCache(serverName);
|
|
266
|
+
if (cached && cached.length > 0) {
|
|
267
|
+
// Use cached tools (real tool names + descriptions, no child process)
|
|
268
|
+
const localNames = new Set();
|
|
269
|
+
for (const tool of cached) {
|
|
270
|
+
const registeredName = pickRegisteredToolName(serverName, tool.name, this.config.toolPrefix, localNames, globalNames, this.logger);
|
|
271
|
+
localNames.add(registeredName);
|
|
272
|
+
globalNames.add(registeredName);
|
|
273
|
+
lazyTools.push({ name: registeredName, description: tool.description, inputSchema: tool.inputSchema });
|
|
274
|
+
// Also populate directTools so call works with lazy connect
|
|
275
|
+
this.directTools.push({
|
|
276
|
+
serverName, originalName: tool.name, registeredName,
|
|
277
|
+
description: tool.description, inputSchema: tool.inputSchema
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// No cache: single placeholder per server
|
|
283
|
+
const desc = serverConfig.description || serverName;
|
|
284
|
+
const registeredName = pickRegisteredToolName(serverName, "call", this.config.toolPrefix, new Set(), globalNames, this.logger);
|
|
285
|
+
globalNames.add(registeredName);
|
|
286
|
+
lazyTools.push({
|
|
287
|
+
name: registeredName,
|
|
288
|
+
description: `[${serverName}] ${desc} — call this tool to discover all available tools from this server.`,
|
|
289
|
+
inputSchema: { type: "object", properties: { _discover: { type: "boolean", description: "Set to true to discover all tools" } } }
|
|
290
|
+
});
|
|
291
|
+
this.directTools.push({
|
|
292
|
+
serverName, originalName: "call", registeredName,
|
|
293
|
+
description: `[${serverName}] ${desc}`, inputSchema: {}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { jsonrpc: "2.0", id, result: { tools: lazyTools } };
|
|
255
298
|
}
|
|
256
299
|
async handleToolsCall(id, params) {
|
|
257
300
|
const toolName = params?.name;
|
|
@@ -295,7 +338,29 @@ export class StandaloneServer {
|
|
|
295
338
|
};
|
|
296
339
|
}
|
|
297
340
|
// Direct mode: find and call the tool
|
|
298
|
-
|
|
341
|
+
let entry = this.directTools.find(t => t.registeredName === toolName);
|
|
342
|
+
// Lazy discovery: if tool not found, discover only the relevant server (not all)
|
|
343
|
+
if (!entry) {
|
|
344
|
+
// Extract server name from tool name (prefix_toolname pattern)
|
|
345
|
+
const serverName = this.guessServerFromToolName(toolName);
|
|
346
|
+
if (serverName) {
|
|
347
|
+
this.logger.info(`[mcp-bridge] Lazy discovery for server: ${serverName} (triggered by ${toolName})`);
|
|
348
|
+
await this.discoverSingleServer(serverName);
|
|
349
|
+
entry = this.directTools.find(t => t.registeredName === toolName);
|
|
350
|
+
// If the original call was a placeholder (_discover), return discovered tools
|
|
351
|
+
if (!entry && toolArgs?._discover) {
|
|
352
|
+
const serverTools = this.directTools.filter(t => t.serverName === serverName);
|
|
353
|
+
const discovered = serverTools.map(t => `${t.registeredName}: ${t.description}`);
|
|
354
|
+
return {
|
|
355
|
+
jsonrpc: "2.0",
|
|
356
|
+
id,
|
|
357
|
+
result: {
|
|
358
|
+
content: [{ type: "text", text: `Discovered ${serverTools.length} tools from ${serverName}:\n${discovered.join("\n")}` }]
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
299
364
|
if (!entry) {
|
|
300
365
|
return {
|
|
301
366
|
jsonrpc: "2.0",
|
|
@@ -308,7 +373,32 @@ export class StandaloneServer {
|
|
|
308
373
|
};
|
|
309
374
|
}
|
|
310
375
|
try {
|
|
311
|
-
|
|
376
|
+
let conn = this.directConnections.get(entry.serverName);
|
|
377
|
+
// Lazy connect: if server not connected yet, connect now
|
|
378
|
+
if (!conn || !conn.transport.isConnected()) {
|
|
379
|
+
const serverConfig = this.config.servers[entry.serverName];
|
|
380
|
+
if (serverConfig) {
|
|
381
|
+
try {
|
|
382
|
+
this.logger.info(`[mcp-bridge] Lazy connecting to ${entry.serverName}...`);
|
|
383
|
+
const transport = this.createTransport(entry.serverName, serverConfig);
|
|
384
|
+
await transport.connect();
|
|
385
|
+
await initializeProtocol(transport, PACKAGE_VERSION);
|
|
386
|
+
conn = { transport, initialized: true };
|
|
387
|
+
this.directConnections.set(entry.serverName, conn);
|
|
388
|
+
}
|
|
389
|
+
catch (connErr) {
|
|
390
|
+
return {
|
|
391
|
+
jsonrpc: "2.0",
|
|
392
|
+
id,
|
|
393
|
+
error: {
|
|
394
|
+
code: -32001,
|
|
395
|
+
message: `Failed to connect to ${entry.serverName}: ${connErr instanceof Error ? connErr.message : String(connErr)}`,
|
|
396
|
+
data: { errorType: "connection_failed", server: entry.serverName, retriable: true }
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
312
402
|
if (!conn || !conn.transport.isConnected()) {
|
|
313
403
|
return {
|
|
314
404
|
jsonrpc: "2.0",
|
|
@@ -407,6 +497,76 @@ export class StandaloneServer {
|
|
|
407
497
|
}
|
|
408
498
|
}
|
|
409
499
|
}
|
|
500
|
+
/** Extract server name from a tool name like "todoist_call" or "github_call" */
|
|
501
|
+
guessServerFromToolName(toolName) {
|
|
502
|
+
// Try exact match with placeholder pattern: serverName_call
|
|
503
|
+
for (const serverName of Object.keys(this.config.servers)) {
|
|
504
|
+
if (toolName.startsWith(serverName + "_") || toolName === serverName) {
|
|
505
|
+
return serverName;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
/** Discover tools from a single server (lazy, per-server) */
|
|
511
|
+
async discoverSingleServer(serverName) {
|
|
512
|
+
const serverConfig = this.config.servers[serverName];
|
|
513
|
+
if (!serverConfig)
|
|
514
|
+
return;
|
|
515
|
+
// Skip if already connected
|
|
516
|
+
const existing = this.directConnections.get(serverName);
|
|
517
|
+
if (existing?.initialized)
|
|
518
|
+
return;
|
|
519
|
+
try {
|
|
520
|
+
const transport = this.createTransport(serverName, serverConfig);
|
|
521
|
+
await transport.connect();
|
|
522
|
+
await initializeProtocol(transport, PACKAGE_VERSION);
|
|
523
|
+
this.directConnections.set(serverName, { transport, initialized: true });
|
|
524
|
+
const tools = await fetchToolsList(transport);
|
|
525
|
+
const globalNames = new Set(this.directTools.map(t => t.registeredName));
|
|
526
|
+
const localNames = new Set();
|
|
527
|
+
// Remove placeholder entries for this server
|
|
528
|
+
this.directTools = this.directTools.filter(t => t.serverName !== serverName);
|
|
529
|
+
for (const tool of tools) {
|
|
530
|
+
const registeredName = pickRegisteredToolName(serverName, tool.name, this.config.toolPrefix, localNames, globalNames, this.logger);
|
|
531
|
+
localNames.add(registeredName);
|
|
532
|
+
globalNames.add(registeredName);
|
|
533
|
+
this.directTools.push({
|
|
534
|
+
serverName,
|
|
535
|
+
originalName: tool.name,
|
|
536
|
+
registeredName,
|
|
537
|
+
description: tool.description,
|
|
538
|
+
inputSchema: tool.inputSchema
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
// Cache tools to disk
|
|
542
|
+
this.saveToolCache(serverName, tools);
|
|
543
|
+
this.logger.info(`[mcp-bridge] Discovered ${tools.length} tools from ${serverName}`);
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
this.logger.error(`[mcp-bridge] Failed to discover ${serverName}:`, err);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/** Save discovered tools to disk cache */
|
|
550
|
+
saveToolCache(serverName, tools) {
|
|
551
|
+
try {
|
|
552
|
+
const cacheDir = join(homedir(), ".mcp-bridge", "cache");
|
|
553
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
554
|
+
const cachePath = join(cacheDir, `${serverName}-tools.json`);
|
|
555
|
+
writeFileSync(cachePath, JSON.stringify(tools, null, 2), "utf-8");
|
|
556
|
+
}
|
|
557
|
+
catch { /* ignore cache write errors */ }
|
|
558
|
+
}
|
|
559
|
+
/** Load cached tools from disk */
|
|
560
|
+
loadToolCache(serverName) {
|
|
561
|
+
try {
|
|
562
|
+
const cachePath = join(homedir(), ".mcp-bridge", "cache", `${serverName}-tools.json`);
|
|
563
|
+
if (existsSync(cachePath)) {
|
|
564
|
+
return JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch { /* ignore cache read errors */ }
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
410
570
|
nextRequestId() {
|
|
411
571
|
return nextRequestId(this.requestIdState);
|
|
412
572
|
}
|