@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.
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -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]);
|
package/dist/src/mcp-router.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/mcp-router.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|