@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:
|
|
259
|
-
const
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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:
|
|
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,
|
|
342
|
+
// Lazy discovery: if tool not found, discover only the relevant server (not all)
|
|
317
343
|
if (!entry) {
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
this.logger.info(`[mcp-bridge] Lazy discovery triggered by
|
|
322
|
-
await this.
|
|
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,
|
|
350
|
+
// If the original call was a placeholder (_discover), return discovered tools
|
|
325
351
|
if (!entry && toolArgs?._discover) {
|
|
326
|
-
const
|
|
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 ${
|
|
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.
|
|
576
|
+
await this.discoverSingleServer(serverName);
|
|
480
577
|
};
|
|
481
578
|
switch (serverConfig.transport) {
|
|
482
579
|
case "sse":
|