@aiwerk/mcp-bridge 2.8.27 → 2.8.29

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)
@@ -255,20 +258,43 @@ export class StandaloneServer {
255
258
  }));
256
259
  return { jsonrpc: "2.0", id, result: { tools } };
257
260
  }
258
- // Lazy: generate tool entries from server config (no connect yet)
259
- const placeholderTools = [];
261
+ // Lazy: try cache first, then placeholder
262
+ const lazyTools = [];
260
263
  const globalNames = new Set();
261
264
  for (const [serverName, serverConfig] of Object.entries(this.config.servers)) {
262
- const desc = serverConfig.description || serverName;
263
- const registeredName = pickRegisteredToolName(serverName, "call", this.config.toolPrefix, new Set(), globalNames, this.logger);
264
- globalNames.add(registeredName);
265
- placeholderTools.push({
266
- name: registeredName,
267
- description: `[${serverName}] ${desc} tools will be discovered on first call. Use this tool to trigger discovery.`,
268
- inputSchema: { type: "object", properties: { _discover: { type: "boolean", description: "Set to true to discover all tools from this server" } } }
269
- });
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
+ }
270
296
  }
271
- return { jsonrpc: "2.0", id, result: { tools: placeholderTools } };
297
+ return { jsonrpc: "2.0", id, result: { tools: lazyTools } };
272
298
  }
273
299
  async handleToolsCall(id, params) {
274
300
  const toolName = params?.name;
@@ -313,22 +339,23 @@ export class StandaloneServer {
313
339
  }
314
340
  // Direct mode: find and call the tool
315
341
  let entry = this.directTools.find(t => t.registeredName === toolName);
316
- // Lazy discovery: if tool not found, maybe we haven't connected yet
342
+ // Lazy discovery: if tool not found, discover only the relevant server (not all)
317
343
  if (!entry) {
318
- // Check if this looks like a placeholder tool (serverName_call pattern) or just unknown
319
- // Try full discovery if we haven't done it yet
320
- if (this.directTools.length === 0 || toolArgs?._discover) {
321
- this.logger.info(`[mcp-bridge] Lazy discovery triggered by tool call: ${toolName}`);
322
- await this.discoverDirectTools();
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);
323
349
  entry = this.directTools.find(t => t.registeredName === toolName);
324
- // If the original call was a placeholder, send back the discovered tools list
350
+ // If the original call was a placeholder (_discover), return discovered tools
325
351
  if (!entry && toolArgs?._discover) {
326
- const discovered = this.directTools.map(t => `${t.registeredName}: ${t.description}`);
352
+ const serverTools = this.directTools.filter(t => t.serverName === serverName);
353
+ const discovered = serverTools.map(t => `${t.registeredName}: ${t.description}`);
327
354
  return {
328
355
  jsonrpc: "2.0",
329
356
  id,
330
357
  result: {
331
- content: [{ type: "text", text: `Discovered ${this.directTools.length} tools:\n${discovered.join("\n")}` }]
358
+ content: [{ type: "text", text: `Discovered ${serverTools.length} tools from ${serverName}:\n${discovered.join("\n")}` }]
332
359
  }
333
360
  };
334
361
  }
@@ -470,13 +497,83 @@ export class StandaloneServer {
470
497
  }
471
498
  }
472
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
+ }
473
570
  nextRequestId() {
474
571
  return nextRequestId(this.requestIdState);
475
572
  }
476
573
  createTransport(serverName, serverConfig) {
477
574
  const onReconnected = async () => {
478
575
  this.logger.info(`[mcp-bridge] ${serverName} reconnected, refreshing tools`);
479
- await this.discoverDirectTools(true);
576
+ await this.discoverSingleServer(serverName);
480
577
  };
481
578
  switch (serverConfig.transport) {
482
579
  case "sse":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.8.27",
3
+ "version": "2.8.29",
4
4
  "description": "Standalone MCP server that multiplexes multiple MCP servers into one interface",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",