@aiwerk/mcp-bridge 2.8.39 → 2.8.41

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.
@@ -729,11 +729,6 @@ async function cmdAuth(args, logger) {
729
729
  const shown = new Set();
730
730
  if (config) {
731
731
  for (const [name, serverConfig] of Object.entries(config.servers)) {
732
- const authType = serverConfig.auth?.type ?? "none";
733
- const grantType = serverConfig.auth?.type === "oauth2" && "grantType" in serverConfig.auth
734
- ? serverConfig.auth.grantType
735
- : serverConfig.auth?.type === "oauth2" ? "client_credentials" : "";
736
- const label = authType === "oauth2" ? `oauth2 (${grantType})` : authType;
737
732
  // Find required env vars from raw config (before env var resolution)
738
733
  const envKeys = [];
739
734
  try {
@@ -747,6 +742,11 @@ async function cmdAuth(args, logger) {
747
742
  }
748
743
  }
749
744
  catch { /* ignore */ }
745
+ const authType = serverConfig.auth?.type ?? (envKeys.length > 0 ? "api-key" : "none");
746
+ const grantType = serverConfig.auth?.type === "oauth2" && "grantType" in serverConfig.auth
747
+ ? serverConfig.auth.grantType
748
+ : serverConfig.auth?.type === "oauth2" ? "client_credentials" : "";
749
+ const label = authType === "oauth2" ? `oauth2 (${grantType})` : authType;
750
750
  let envStatus = "-";
751
751
  if (envKeys.length > 0) {
752
752
  const setKeys = envKeys.filter(k => envVars.has(k) || process.env[k]);
@@ -1,6 +1,6 @@
1
1
  import { McpClientConfig, McpServerConfig, McpTransport, Logger } from "./types.js";
2
2
  import { OAuth2TokenManager } from "./oauth2-token-manager.js";
3
- type RouterErrorCode = "unknown_server" | "unknown_tool" | "connection_failed" | "mcp_error" | "invalid_params";
3
+ type RouterErrorCode = "unknown_server" | "unknown_tool" | "connection_failed" | "mcp_error" | "invalid_params" | "not_found";
4
4
  interface DebugMetadata {
5
5
  server: string;
6
6
  tool: string;
@@ -110,6 +110,11 @@ export type RouterDispatchResponse = {
110
110
  message: string;
111
111
  missingEnvVars?: string[];
112
112
  credentialsUrl?: string;
113
+ } | {
114
+ action: "remove";
115
+ server: string;
116
+ removed: boolean;
117
+ message: string;
113
118
  } | {
114
119
  action: "set-mode";
115
120
  mode: string;
@@ -229,6 +229,54 @@ export class McpRouter {
229
229
  return this.error("mcp_error", `Install failed: ${err instanceof Error ? err.message : String(err)}`);
230
230
  }
231
231
  }
232
+ if (normalizedAction === "remove") {
233
+ const serverName = server || params?.server || params?.name;
234
+ if (!serverName) {
235
+ return this.error("invalid_params", "server name is required for action=remove");
236
+ }
237
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(serverName)) {
238
+ return this.error("invalid_params", `Invalid server name "${serverName}". Must match /^[a-z0-9][a-z0-9-]*$/.`);
239
+ }
240
+ if (!this.servers[serverName]) {
241
+ return this.error("not_found", `Server "${serverName}" is not configured.`);
242
+ }
243
+ // Disconnect if connected
244
+ const state = this.states.get(serverName);
245
+ if (state?.transport?.isConnected()) {
246
+ try {
247
+ await state.transport.disconnect();
248
+ }
249
+ catch { /* ignore */ }
250
+ }
251
+ this.states.delete(serverName);
252
+ // Remove from runtime config
253
+ delete this.servers[serverName];
254
+ delete this.clientConfig.servers[serverName];
255
+ // Remove from config file
256
+ try {
257
+ const os = await import("os");
258
+ const fs = await import("fs");
259
+ const path = await import("path");
260
+ const configPath = path.join(os.homedir(), ".mcp-bridge", "config.json");
261
+ if (fs.existsSync(configPath)) {
262
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
263
+ if (raw.servers?.[serverName]) {
264
+ delete raw.servers[serverName];
265
+ fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
266
+ this.logger.info(`Removed "${serverName}" from ${configPath}`);
267
+ }
268
+ }
269
+ // Remove tool cache
270
+ const cachePath = path.join(os.homedir(), ".mcp-bridge", "cache", `${serverName}-tools.json`);
271
+ if (fs.existsSync(cachePath)) {
272
+ fs.unlinkSync(cachePath);
273
+ }
274
+ }
275
+ catch (err) {
276
+ this.logger.warn(`Could not persist removal of "${serverName}": ${err instanceof Error ? err.message : String(err)}`);
277
+ }
278
+ return { action: "remove", server: serverName, removed: true, message: `Server "${serverName}" removed.` };
279
+ }
232
280
  if (normalizedAction === "batch") {
233
281
  const calls = params?.calls;
234
282
  if (!Array.isArray(calls) || calls.length === 0) {
@@ -14,6 +14,8 @@ export declare class StandaloneServer {
14
14
  private readonly requestIdState;
15
15
  private directTools;
16
16
  private directConnections;
17
+ private directIdleTimer;
18
+ private static readonly DIRECT_IDLE_TIMEOUT_MS;
17
19
  private stdoutRef;
18
20
  constructor(config: BridgeConfig, logger: Logger);
19
21
  private isRouterMode;
@@ -39,12 +41,15 @@ export declare class StandaloneServer {
39
41
  private guessServerFromToolName;
40
42
  /** Discover tools from a single server (lazy, per-server) */
41
43
  private discoverSingleServer;
42
- /** Save discovered tools to disk cache */
44
+ private static readonly CACHE_TTL_MS;
45
+ /** Save discovered tools to disk cache with timestamp */
43
46
  private saveToolCache;
44
- /** Load cached tools from disk */
47
+ /** Load cached tools from disk (with TTL validation) */
45
48
  private loadToolCache;
46
49
  private nextRequestId;
47
50
  private createTransport;
48
51
  /** Graceful shutdown: disconnect all backend servers. */
52
+ /** Start idle connection cleanup timer for direct mode */
53
+ private startDirectIdleTimer;
49
54
  shutdown(): Promise<void>;
50
55
  }
@@ -26,6 +26,8 @@ export class StandaloneServer {
26
26
  // Direct mode state
27
27
  directTools = [];
28
28
  directConnections = new Map();
29
+ directIdleTimer = null;
30
+ static DIRECT_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
29
31
  stdoutRef = null;
30
32
  constructor(config, logger) {
31
33
  this.config = config;
@@ -481,7 +483,7 @@ export class StandaloneServer {
481
483
  const transport = this.createTransport(entry.serverName, serverConfig);
482
484
  await transport.connect();
483
485
  await initializeProtocol(transport, PACKAGE_VERSION);
484
- conn = { transport, initialized: true };
486
+ conn = { transport, initialized: true, lastUsed: Date.now() };
485
487
  this.directConnections.set(entry.serverName, conn);
486
488
  }
487
489
  catch (connErr) {
@@ -508,6 +510,9 @@ export class StandaloneServer {
508
510
  }
509
511
  };
510
512
  }
513
+ // Mark connection as recently used + start idle timer
514
+ conn.lastUsed = Date.now();
515
+ this.startDirectIdleTimer();
511
516
  const response = await conn.transport.sendRequest({
512
517
  jsonrpc: "2.0",
513
518
  method: "tools/call",
@@ -573,7 +578,7 @@ export class StandaloneServer {
573
578
  const transport = this.createTransport(serverName, serverConfig);
574
579
  await transport.connect();
575
580
  await initializeProtocol(transport, PACKAGE_VERSION);
576
- this.directConnections.set(serverName, { transport, initialized: true });
581
+ this.directConnections.set(serverName, { transport, initialized: true, lastUsed: Date.now() });
577
582
  const tools = await fetchToolsList(transport);
578
583
  const localNames = new Set();
579
584
  for (const tool of tools) {
@@ -646,8 +651,9 @@ export class StandaloneServer {
646
651
  const transport = this.createTransport(serverName, serverConfig);
647
652
  await transport.connect();
648
653
  await initializeProtocol(transport, PACKAGE_VERSION);
649
- this.directConnections.set(serverName, { transport, initialized: true });
650
654
  const tools = await fetchToolsList(transport);
655
+ // Only add to connections AFTER successful tool fetch (prevents leak on partial failure)
656
+ this.directConnections.set(serverName, { transport, initialized: true, lastUsed: Date.now() });
651
657
  const globalNames = new Set(this.directTools.map(t => t.registeredName));
652
658
  const localNames = new Set();
653
659
  // Remove placeholder entries for this server
@@ -672,28 +678,57 @@ export class StandaloneServer {
672
678
  }
673
679
  catch (err) {
674
680
  this.logger.error(`[mcp-bridge] Failed to discover ${serverName}:`, err);
681
+ // Clean up partial connection to allow retry
682
+ const partial = this.directConnections.get(serverName);
683
+ if (partial?.transport) {
684
+ try {
685
+ await partial.transport.disconnect();
686
+ }
687
+ catch { /* ignore */ }
688
+ }
689
+ this.directConnections.delete(serverName);
675
690
  }
676
691
  }
677
- /** Save discovered tools to disk cache */
692
+ static CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
693
+ /** Save discovered tools to disk cache with timestamp */
678
694
  saveToolCache(serverName, tools) {
679
695
  try {
696
+ // Sanitize server name for filesystem safety
697
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(serverName))
698
+ return;
680
699
  const cacheDir = join(homedir(), ".mcp-bridge", "cache");
681
700
  mkdirSync(cacheDir, { recursive: true });
682
701
  const cachePath = join(cacheDir, `${serverName}-tools.json`);
683
- writeFileSync(cachePath, JSON.stringify(tools, null, 2), "utf-8");
702
+ writeFileSync(cachePath, JSON.stringify({ cachedAt: Date.now(), tools }, null, 2), "utf-8");
684
703
  }
685
704
  catch { /* ignore cache write errors */ }
686
705
  }
687
- /** Load cached tools from disk */
706
+ /** Load cached tools from disk (with TTL validation) */
688
707
  loadToolCache(serverName) {
689
708
  try {
709
+ // Sanitize server name for filesystem safety
710
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(serverName))
711
+ return null;
690
712
  const cachePath = join(homedir(), ".mcp-bridge", "cache", `${serverName}-tools.json`);
691
- if (existsSync(cachePath)) {
692
- return JSON.parse(readFileSync(cachePath, "utf-8"));
713
+ if (!existsSync(cachePath))
714
+ return null;
715
+ const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
716
+ // Support both old format (array) and new format ({cachedAt, tools})
717
+ if (Array.isArray(raw))
718
+ return raw; // legacy format, no TTL
719
+ if (raw && typeof raw === "object" && Array.isArray(raw.tools)) {
720
+ // Check TTL
721
+ if (raw.cachedAt && (Date.now() - raw.cachedAt > StandaloneServer.CACHE_TTL_MS)) {
722
+ this.logger.info(`[mcp-bridge] Cache expired for ${serverName}, will re-discover`);
723
+ return null;
724
+ }
725
+ return raw.tools;
693
726
  }
727
+ return null; // malformed cache
728
+ }
729
+ catch {
730
+ return null; // corrupt/unreadable cache
694
731
  }
695
- catch { /* ignore cache read errors */ }
696
- return null;
697
732
  }
698
733
  nextRequestId() {
699
734
  return nextRequestId(this.requestIdState);
@@ -715,8 +750,28 @@ export class StandaloneServer {
715
750
  }
716
751
  }
717
752
  /** Graceful shutdown: disconnect all backend servers. */
753
+ /** Start idle connection cleanup timer for direct mode */
754
+ startDirectIdleTimer() {
755
+ if (this.directIdleTimer)
756
+ return;
757
+ this.directIdleTimer = setInterval(() => {
758
+ const now = Date.now();
759
+ for (const [name, conn] of this.directConnections) {
760
+ if (now - conn.lastUsed > StandaloneServer.DIRECT_IDLE_TIMEOUT_MS) {
761
+ this.logger.info(`[mcp-bridge] Disconnecting idle server: ${name}`);
762
+ conn.transport.disconnect().catch(() => { });
763
+ this.directConnections.delete(name);
764
+ }
765
+ }
766
+ }, 60_000); // Check every minute
767
+ this.directIdleTimer.unref(); // Don't keep process alive
768
+ }
718
769
  async shutdown() {
719
770
  this.logger.info("[mcp-bridge] Shutting down...");
771
+ if (this.directIdleTimer) {
772
+ clearInterval(this.directIdleTimer);
773
+ this.directIdleTimer = null;
774
+ }
720
775
  if (this.router) {
721
776
  await this.router.shutdown(this.config.shutdownTimeoutMs);
722
777
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.8.39",
3
+ "version": "2.8.41",
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",