@aiwerk/mcp-bridge 2.8.3 → 2.8.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.
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync, writeFileSync } from "fs";
2
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
3
3
  import { join, dirname, resolve, extname } from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { platform, homedir } from "os";
6
- import { execFileSync } from "child_process";
7
- import { loadConfig, initConfigDir, warnDeprecatedBundledRecipes } from "../src/config.js";
5
+ import { homedir } from "os";
6
+ import { loadConfig, initConfigDir, warnDeprecatedBundledRecipes, recipeToServerConfig, collectRequiredEnvVars } from "../src/config.js";
8
7
  import { StandaloneServer } from "../src/standalone-server.js";
9
8
  import { PACKAGE_VERSION } from "../src/protocol.js";
10
9
  import { checkForUpdate, runUpdate } from "../src/update-checker.js";
11
10
  import { FileTokenStore } from "../src/token-store.js";
12
11
  import { performAuthCodeLogin, performDeviceCodeLogin } from "../src/cli-auth.js";
13
12
  import { RateLimiter } from "../src/rate-limiter.js";
13
+ import { CatalogClient } from "../src/catalog-client.js";
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = dirname(__filename);
16
16
  // After tsc, this file lives at dist/bin/mcp-bridge.js.
@@ -195,6 +195,26 @@ All logs go to stderr. Stdout is reserved for the MCP protocol (stdio mode).
195
195
  }
196
196
  function cmdInit(logger) {
197
197
  initConfigDir(logger);
198
+ // Check if installed globally
199
+ const isGlobal = __dirname.includes("node_modules") && !__dirname.includes(homedir());
200
+ process.stdout.write(`
201
+ Next step: add mcp-bridge to your MCP client.
202
+
203
+ Add this to your client's MCP server config:
204
+
205
+ {
206
+ "mcp-bridge": {
207
+ "command": "${isGlobal ? "mcp-bridge" : "node"}",
208
+ "args": ${isGlobal ? '["serve"]' : `["${join(__dirname, "..", "bin", "mcp-bridge.js")}", "serve"]`}
209
+ }
210
+ }
211
+
212
+ Supported clients: Claude Code (~/.claude/settings.json),
213
+ Cursor (~/.cursor/mcp.json), Claude Desktop, Windsurf, OpenClaw, etc.
214
+ ${!isGlobal ? "\nTip: Install globally for a cleaner setup:\n npm install -g @aiwerk/mcp-bridge\n" : ""}
215
+ After adding, restart your client. The bridge will appear as an 'mcp' tool
216
+ with search, install, and catalog actions to discover and add MCP servers.
217
+ `);
198
218
  }
199
219
  function cmdCatalog(logger) {
200
220
  const catalogPath = join(PACKAGE_ROOT, "servers", "index.json");
@@ -373,30 +393,68 @@ function cmdLimit(args, logger) {
373
393
  process.exit(1);
374
394
  }
375
395
  }
376
- function cmdInstall(serverName, logger) {
377
- const scriptDir = join(PACKAGE_ROOT, "scripts");
396
+ async function cmdInstall(serverName, args, logger) {
397
+ const configPath = resolveConfigPath(args.configPath);
398
+ const configDir = dirname(configPath);
399
+ // Ensure config dir exists
400
+ if (!existsSync(configDir)) {
401
+ mkdirSync(configDir, { recursive: true });
402
+ }
403
+ // Ensure config file exists
404
+ if (!existsSync(configPath)) {
405
+ writeFileSync(configPath, JSON.stringify({ servers: {} }, null, 2) + "\n", "utf-8");
406
+ logger.info(`Created config: ${configPath}`);
407
+ }
408
+ // Read current config
409
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
410
+ if (!raw.servers)
411
+ raw.servers = {};
412
+ // Check if already configured
413
+ if (raw.servers[serverName]) {
414
+ process.stdout.write(`Server "${serverName}" is already configured.\n`);
415
+ process.stdout.write(`Config: ${configPath}\n`);
416
+ return;
417
+ }
418
+ // Fetch recipe from catalog
419
+ process.stdout.write(`Fetching recipe for ${serverName}...\n`);
420
+ const cacheDir = join(configDir, "recipes");
421
+ const client = new CatalogClient({ cacheDir, logger });
422
+ let recipe;
378
423
  try {
379
- if (platform() === "win32") {
380
- const psScript = join(scriptDir, "install-server.ps1");
381
- if (!existsSync(psScript)) {
382
- logger.error("Install script not found (install-server.ps1)");
383
- process.exit(1);
384
- }
385
- execFileSync("powershell", ["-ExecutionPolicy", "Bypass", "-File", psScript, serverName], { stdio: "inherit" });
386
- }
387
- else {
388
- const scriptPath = join(scriptDir, "install-server.sh");
389
- if (!existsSync(scriptPath)) {
390
- logger.error("Install script not found (install-server.sh)");
391
- process.exit(1);
392
- }
393
- execFileSync("bash", [scriptPath, serverName], { stdio: "inherit" });
394
- }
424
+ recipe = await client.resolve(serverName);
395
425
  }
396
426
  catch (err) {
397
- logger.error("Install failed:", err instanceof Error ? err.message : String(err));
427
+ logger.error(`Recipe not found: ${serverName}`);
428
+ process.exit(1);
429
+ }
430
+ // Convert recipe to server config
431
+ const serverConfig = recipeToServerConfig(recipe);
432
+ if (!serverConfig) {
433
+ logger.error(`Unsupported recipe format for "${serverName}"`);
398
434
  process.exit(1);
399
435
  }
436
+ // Check required env vars
437
+ const requiredVars = collectRequiredEnvVars(recipe);
438
+ const missing = requiredVars.filter(v => !process.env[v]);
439
+ // Add to config
440
+ raw.servers[serverName] = serverConfig;
441
+ writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
442
+ process.stdout.write(`\n✓ Added "${serverName}" to ${configPath}\n\n`);
443
+ if (missing.length > 0) {
444
+ process.stdout.write(`⚠ Missing environment variables:\n`);
445
+ for (const v of missing) {
446
+ process.stdout.write(` ${v}\n`);
447
+ }
448
+ // Show credentials URL if available
449
+ const credUrl = recipe.auth?.credentialsUrl;
450
+ if (credUrl) {
451
+ process.stdout.write(`\nGet credentials: ${credUrl}\n`);
452
+ }
453
+ process.stdout.write(`\nSet them in your environment or ~/.mcp-bridge/.env before starting the bridge.\n`);
454
+ }
455
+ else {
456
+ process.stdout.write(`All required environment variables are set. Ready to use.\n`);
457
+ }
400
458
  }
401
459
  async function cmdUpdate(logger, checkOnly) {
402
460
  if (checkOnly) {
@@ -549,6 +607,9 @@ async function cmdServe(args, logger) {
549
607
  logger.error(err instanceof Error ? err.message : String(err));
550
608
  process.exit(1);
551
609
  }
610
+ if (args.debug) {
611
+ config.debug = true;
612
+ }
552
613
  // HTTP modes: require auth
553
614
  if ((args.sse || args.http) && !config.http?.auth) {
554
615
  logger.error("HTTP auth not configured. Set http.auth in config or use stdio mode.");
@@ -611,7 +672,7 @@ async function main() {
611
672
  process.stderr.write("Usage: mcp-bridge install <server>\n");
612
673
  process.exit(1);
613
674
  }
614
- cmdInstall(args.positional[0], logger);
675
+ await cmdInstall(args.positional[0], args, logger);
615
676
  break;
616
677
  case "update":
617
678
  await cmdUpdate(logger, args.checkOnly);
@@ -1,4 +1,5 @@
1
- import { BridgeConfig, Logger } from "./types.js";
1
+ import { BridgeConfig, Logger, McpServerConfig } from "./types.js";
2
+ import type { CatalogRecipe } from "./catalog-client.js";
2
3
  /**
3
4
  * Load ~/.openclaw/.env as a fallback env source.
4
5
  *
@@ -60,3 +61,7 @@ export declare function mergeRecipesIntoConfig(config: BridgeConfig, options?: {
60
61
  cacheDir?: string;
61
62
  logger?: Logger;
62
63
  }): BridgeConfig;
64
+ /** Convert a catalog recipe JSON to McpServerConfig, or null if unsupported. */
65
+ export declare function recipeToServerConfig(recipe: CatalogRecipe): McpServerConfig | null;
66
+ /** Collect all env var names required by a recipe. */
67
+ export declare function collectRequiredEnvVars(recipe: CatalogRecipe): string[];
@@ -330,7 +330,7 @@ export function mergeRecipesIntoConfig(config, options) {
330
330
  return { ...config, servers };
331
331
  }
332
332
  /** Convert a catalog recipe JSON to McpServerConfig, or null if unsupported. */
333
- function recipeToServerConfig(recipe) {
333
+ export function recipeToServerConfig(recipe) {
334
334
  // v2 recipe: has transports array
335
335
  if (Array.isArray(recipe.transports) && recipe.transports.length > 0) {
336
336
  const t = recipe.transports[0];
@@ -374,7 +374,7 @@ function recipeToServerConfig(recipe) {
374
374
  return null;
375
375
  }
376
376
  /** Collect all env var names required by a recipe. */
377
- function collectRequiredEnvVars(recipe) {
377
+ export function collectRequiredEnvVars(recipe) {
378
378
  const vars = new Set();
379
379
  // From auth.envVars
380
380
  if (Array.isArray(recipe.auth?.envVars)) {
@@ -1,6 +1,13 @@
1
1
  import { McpClientConfig, McpServerConfig, McpTransport, Logger } from "./types.js";
2
2
  import { OAuth2TokenManager } from "./oauth2-token-manager.js";
3
3
  type RouterErrorCode = "unknown_server" | "unknown_tool" | "connection_failed" | "mcp_error" | "invalid_params";
4
+ interface DebugMetadata {
5
+ server: string;
6
+ tool: string;
7
+ transport: string;
8
+ latencyMs: number;
9
+ cached?: boolean;
10
+ }
4
11
  interface RouterBatchResult {
5
12
  server: string;
6
13
  tool: string;
@@ -11,6 +18,7 @@ interface RouterBatchResult {
11
18
  available?: string[];
12
19
  code?: number;
13
20
  };
21
+ _debug?: DebugMetadata;
14
22
  }
15
23
  export interface RouterToolHint {
16
24
  name: string;
@@ -40,6 +48,7 @@ export type RouterDispatchResponse = {
40
48
  result: any;
41
49
  retries?: number;
42
50
  warning?: string;
51
+ _debug?: DebugMetadata;
43
52
  } | {
44
53
  server: string;
45
54
  action: "schema";
@@ -65,7 +65,7 @@ export class McpRouter {
65
65
  static generateDescription(servers) {
66
66
  const serverNames = Object.keys(servers);
67
67
  if (serverNames.length === 0) {
68
- return "Call MCP server tools. No servers configured.";
68
+ return "MCP server multiplexer with no servers configured yet. Use action='search' to find servers in the catalog (100+ available), action='install' to add a server by name, action='catalog' to browse all available servers.";
69
69
  }
70
70
  const serverList = serverNames
71
71
  .map((name) => {
@@ -73,7 +73,7 @@ export class McpRouter {
73
73
  return desc ? `${name} (${desc})` : name;
74
74
  })
75
75
  .join(", ");
76
- return `Call any MCP server tool. Servers: ${serverList}. Use action='list' to discover tools and required parameters, action='call' to execute a tool, action='batch' to execute multiple calls in one round-trip, action='refresh' to clear cache and re-discover tools, and action='status' to check server connection states. If the user mentions a specific tool by name, the call action auto-connects and works without listing first.`;
76
+ return `MCP server multiplexer with ${serverNames.length} connected servers: ${serverList}. Actions: 'call' to execute a tool, 'list' to discover tools on a server, 'batch' for multiple calls in one round-trip, 'status' to check connections, 'refresh' to re-discover tools. To add new servers: 'search' to find servers in the catalog (100+ available), 'install' to add a server by name. If the user mentions a specific tool by name, the call action auto-connects and works without listing first.`;
77
77
  }
78
78
  async dispatch(server, action = "call", tool, params) {
79
79
  try {
@@ -130,7 +130,10 @@ export class McpRouter {
130
130
  results[idx] = {
131
131
  server: callServer,
132
132
  tool: callTool,
133
- result: "result" in response ? response.result : response
133
+ result: "result" in response ? response.result : response,
134
+ ...(this.clientConfig.debug && "result" in response && "_debug" in response ? {
135
+ _debug: response._debug
136
+ } : {})
134
137
  };
135
138
  }
136
139
  }
@@ -208,6 +211,7 @@ export class McpRouter {
208
211
  if (!tool) {
209
212
  return this.error("invalid_params", "tool is required for action=call");
210
213
  }
214
+ const startTime = Date.now();
211
215
  let targetServer = server;
212
216
  if (!targetServer) {
213
217
  await this.primeToolResolutionIndex();
@@ -249,7 +253,21 @@ export class McpRouter {
249
253
  this.promotion.recordCall(server, tool);
250
254
  }
251
255
  this.toolResolver.recordCall(server, tool);
252
- return { server, action: "call", tool, result: cachedResult };
256
+ return {
257
+ server,
258
+ action: "call",
259
+ tool,
260
+ result: cachedResult,
261
+ ...(this.clientConfig.debug ? {
262
+ _debug: {
263
+ server,
264
+ tool,
265
+ transport: serverConfig.transport,
266
+ latencyMs: Date.now() - startTime,
267
+ cached: true,
268
+ }
269
+ } : {})
270
+ };
253
271
  }
254
272
  }
255
273
  // Rate limit: check BEFORE call, increment AFTER success
@@ -280,6 +298,14 @@ export class McpRouter {
280
298
  action: "call",
281
299
  tool,
282
300
  result,
301
+ ...(this.clientConfig.debug ? {
302
+ _debug: {
303
+ server,
304
+ tool,
305
+ transport: serverConfig.transport,
306
+ latencyMs: Date.now() - startTime,
307
+ }
308
+ } : {}),
283
309
  ...(rateLimitIncrement.warning ? { warning: rateLimitIncrement.warning } : {}),
284
310
  ...(callOutcome.retries > 0 ? { retries: callOutcome.retries } : {})
285
311
  };
@@ -101,6 +101,8 @@ export interface McpClientConfig {
101
101
  defaultTtlMs?: number;
102
102
  cacheTtl?: Record<string, number>;
103
103
  };
104
+ /** When true, tool call responses include a _debug object with routing metadata. */
105
+ debug?: boolean;
104
106
  }
105
107
  export interface McpTool {
106
108
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.8.3",
3
+ "version": "2.8.5",
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",