@aiwerk/mcp-bridge 2.8.2 → 2.8.4

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.
@@ -373,30 +373,68 @@ function cmdLimit(args, logger) {
373
373
  process.exit(1);
374
374
  }
375
375
  }
376
- function cmdInstall(serverName, logger) {
377
- const scriptDir = join(PACKAGE_ROOT, "scripts");
376
+ async function cmdInstall(serverName, args, logger) {
377
+ const configPath = resolveConfigPath(args.configPath);
378
+ const configDir = dirname(configPath);
379
+ // Ensure config dir exists
380
+ if (!existsSync(configDir)) {
381
+ mkdirSync(configDir, { recursive: true });
382
+ }
383
+ // Ensure config file exists
384
+ if (!existsSync(configPath)) {
385
+ writeFileSync(configPath, JSON.stringify({ servers: {} }, null, 2) + "\n", "utf-8");
386
+ logger.info(`Created config: ${configPath}`);
387
+ }
388
+ // Read current config
389
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
390
+ if (!raw.servers)
391
+ raw.servers = {};
392
+ // Check if already configured
393
+ if (raw.servers[serverName]) {
394
+ process.stdout.write(`Server "${serverName}" is already configured.\n`);
395
+ process.stdout.write(`Config: ${configPath}\n`);
396
+ return;
397
+ }
398
+ // Fetch recipe from catalog
399
+ process.stdout.write(`Fetching recipe for ${serverName}...\n`);
400
+ const cacheDir = join(configDir, "recipes");
401
+ const client = new CatalogClient({ cacheDir, logger });
402
+ let recipe;
378
403
  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
- }
404
+ recipe = await client.resolve(serverName);
395
405
  }
396
406
  catch (err) {
397
- logger.error("Install failed:", err instanceof Error ? err.message : String(err));
407
+ logger.error(`Recipe not found: ${serverName}`);
398
408
  process.exit(1);
399
409
  }
410
+ // Convert recipe to server config
411
+ const serverConfig = recipeToServerConfig(recipe);
412
+ if (!serverConfig) {
413
+ logger.error(`Unsupported recipe format for "${serverName}"`);
414
+ process.exit(1);
415
+ }
416
+ // Check required env vars
417
+ const requiredVars = collectRequiredEnvVars(recipe);
418
+ const missing = requiredVars.filter(v => !process.env[v]);
419
+ // Add to config
420
+ raw.servers[serverName] = serverConfig;
421
+ writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
422
+ process.stdout.write(`\n✓ Added "${serverName}" to ${configPath}\n\n`);
423
+ if (missing.length > 0) {
424
+ process.stdout.write(`⚠ Missing environment variables:\n`);
425
+ for (const v of missing) {
426
+ process.stdout.write(` ${v}\n`);
427
+ }
428
+ // Show credentials URL if available
429
+ const credUrl = recipe.auth?.credentialsUrl;
430
+ if (credUrl) {
431
+ process.stdout.write(`\nGet credentials: ${credUrl}\n`);
432
+ }
433
+ process.stdout.write(`\nSet them in your environment or ~/.mcp-bridge/.env before starting the bridge.\n`);
434
+ }
435
+ else {
436
+ process.stdout.write(`All required environment variables are set. Ready to use.\n`);
437
+ }
400
438
  }
401
439
  async function cmdUpdate(logger, checkOnly) {
402
440
  if (checkOnly) {
@@ -549,6 +587,9 @@ async function cmdServe(args, logger) {
549
587
  logger.error(err instanceof Error ? err.message : String(err));
550
588
  process.exit(1);
551
589
  }
590
+ if (args.debug) {
591
+ config.debug = true;
592
+ }
552
593
  // HTTP modes: require auth
553
594
  if ((args.sse || args.http) && !config.http?.auth) {
554
595
  logger.error("HTTP auth not configured. Set http.auth in config or use stdio mode.");
@@ -611,7 +652,7 @@ async function main() {
611
652
  process.stderr.write("Usage: mcp-bridge install <server>\n");
612
653
  process.exit(1);
613
654
  }
614
- cmdInstall(args.positional[0], logger);
655
+ await cmdInstall(args.positional[0], args, logger);
615
656
  break;
616
657
  case "update":
617
658
  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
  };
@@ -132,20 +132,15 @@ export class StdioTransport extends BaseTransport {
132
132
  reject(error);
133
133
  };
134
134
  const onFirstData = (chunk) => {
135
- // Validate that first data looks like JSON-RPC (starts with { or Content-Length).
136
- // Some servers write banner text to stdout instead of stderr, which would
137
- // cause a false-positive connect (we'd think the transport is ready).
135
+ // Some MCP servers print banner text to stdout before they start speaking JSON-RPC.
136
+ // We already parse and ignore non-JSON lines later in processStdoutBuffer(), so any
137
+ // stdout activity means the process is alive and its pipes are working. Resolve here
138
+ // and let initializeProtocol() validate the connection with an actual initialize request.
138
139
  const text = chunk.toString().trim();
139
- // Accept empty/whitespace readiness signals (common in lightweight stdio MCP servers),
140
- // JSON messages, or LSP framing headers.
141
- if (text === "" || text.startsWith("{") || text.startsWith("Content-Length")) {
142
- settleResolve();
143
- }
144
- else {
145
- this.logger.warn(`[mcp-bridge] Stdio process sent non-JSON data on stdout: ${text.substring(0, 80)}`);
146
- // Still listen for valid data — don't reject yet, the next chunk might be valid
147
- this.process?.stdout?.once("data", onFirstData);
140
+ if (text && !text.startsWith("{") && !text.startsWith("Content-Length")) {
141
+ this.logger.warn(`[mcp-bridge] Stdio process sent banner/non-JSON data on stdout before ready: ${text.substring(0, 80)}`);
148
142
  }
143
+ settleResolve();
149
144
  };
150
145
  const onProcessError = (error) => settleReject(error);
151
146
  const onProcessExit = () => settleReject(new Error("MCP server exited before stdout became ready"));
@@ -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.2",
3
+ "version": "2.8.4",
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",