@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.
- package/dist/bin/mcp-bridge.js +65 -24
- package/dist/src/config.d.ts +6 -1
- package/dist/src/config.js +2 -2
- package/dist/src/mcp-router.d.ts +9 -0
- package/dist/src/mcp-router.js +30 -4
- package/dist/src/transport-stdio.js +7 -12
- package/dist/src/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
-
|
|
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(
|
|
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);
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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[];
|
package/dist/src/config.js
CHANGED
|
@@ -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)) {
|
package/dist/src/mcp-router.d.ts
CHANGED
|
@@ -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";
|
package/dist/src/mcp-router.js
CHANGED
|
@@ -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 "
|
|
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 `
|
|
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 {
|
|
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
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
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
|
-
|
|
140
|
-
|
|
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"));
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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;
|