@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: discover all tools from all servers
248
- await this.discoverDirectTools();
249
- const tools = this.directTools.map(t => ({
250
- name: t.registeredName,
251
- description: t.description,
252
- inputSchema: t.inputSchema
253
- }));
254
- return { jsonrpc: "2.0", id, result: { tools } };
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
- const entry = this.directTools.find(t => t.registeredName === toolName);
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
- const conn = this.directConnections.get(entry.serverName);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.8.26",
3
+ "version": "2.8.28",
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",