@aiwerk/mcp-bridge 1.0.0 → 1.0.1
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.d.ts +2 -0
- package/dist/bin/mcp-bridge.js +320 -0
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +145 -0
- package/{src/index.ts → dist/src/index.d.ts} +1 -30
- package/dist/src/index.js +21 -0
- package/dist/src/mcp-router.d.ts +65 -0
- package/dist/src/mcp-router.js +271 -0
- package/dist/src/protocol.d.ts +4 -0
- package/dist/src/protocol.js +58 -0
- package/dist/src/schema-convert.d.ts +11 -0
- package/dist/src/schema-convert.js +150 -0
- package/dist/src/standalone-server.d.ts +30 -0
- package/dist/src/standalone-server.js +312 -0
- package/dist/src/tool-naming.d.ts +3 -0
- package/dist/src/tool-naming.js +38 -0
- package/dist/src/transport-base.d.ts +76 -0
- package/dist/src/transport-base.js +163 -0
- package/dist/src/transport-sse.d.ts +16 -0
- package/dist/src/transport-sse.js +207 -0
- package/dist/src/transport-stdio.d.ts +20 -0
- package/dist/src/transport-stdio.js +281 -0
- package/dist/src/transport-streamable-http.d.ts +11 -0
- package/dist/src/transport-streamable-http.js +164 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +4 -0
- package/dist/src/update-checker.d.ts +25 -0
- package/dist/src/update-checker.js +132 -0
- package/package.json +19 -4
- package/scripts/install-server.ps1 +25 -58
- package/scripts/install-server.sh +37 -90
- package/servers/apify/README.md +6 -6
- package/servers/github/README.md +6 -6
- package/servers/google-maps/README.md +6 -6
- package/servers/hetzner/README.md +6 -6
- package/servers/hostinger/README.md +6 -6
- package/servers/linear/README.md +6 -6
- package/servers/miro/README.md +6 -6
- package/servers/notion/README.md +6 -6
- package/servers/stripe/README.md +6 -6
- package/servers/tavily/README.md +6 -6
- package/servers/todoist/README.md +6 -6
- package/servers/wise/README.md +6 -6
- package/bin/mcp-bridge.js +0 -9
- package/bin/mcp-bridge.ts +0 -335
- package/src/config.ts +0 -168
- package/src/mcp-router.ts +0 -366
- package/src/protocol.ts +0 -69
- package/src/schema-convert.ts +0 -178
- package/src/standalone-server.ts +0 -385
- package/src/tool-naming.ts +0 -51
- package/src/transport-base.ts +0 -199
- package/src/transport-sse.ts +0 -230
- package/src/transport-stdio.ts +0 -312
- package/src/transport-streamable-http.ts +0 -188
- package/src/types.ts +0 -88
- package/src/update-checker.ts +0 -155
- package/tests/collision.test.ts +0 -60
- package/tests/env-resolve.test.ts +0 -68
- package/tests/mcp-router.test.ts +0 -301
- package/tests/schema-convert.test.ts +0 -70
- package/tests/transport-base.test.ts +0 -214
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join, dirname, resolve } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { loadConfig, initConfigDir } from "../src/config.js";
|
|
7
|
+
import { StandaloneServer } from "../src/standalone-server.js";
|
|
8
|
+
import { PACKAGE_VERSION } from "../src/protocol.js";
|
|
9
|
+
import { checkForUpdate, runUpdate } from "../src/update-checker.js";
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
// After tsc, this file lives at dist/bin/mcp-bridge.js.
|
|
13
|
+
// Package root is two levels up: dist/bin/ -> dist/ -> package root.
|
|
14
|
+
const PACKAGE_ROOT = join(__dirname, "..", "..");
|
|
15
|
+
function createLogger(level) {
|
|
16
|
+
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
17
|
+
const threshold = levels[level];
|
|
18
|
+
const ts = () => new Date().toISOString().replace("T", " ").replace("Z", "");
|
|
19
|
+
return {
|
|
20
|
+
error: (...args) => {
|
|
21
|
+
if (threshold >= 0)
|
|
22
|
+
process.stderr.write(`[${ts()}] [ERROR] ${args.map(String).join(" ")}\n`);
|
|
23
|
+
},
|
|
24
|
+
warn: (...args) => {
|
|
25
|
+
if (threshold >= 1)
|
|
26
|
+
process.stderr.write(`[${ts()}] [WARN] ${args.map(String).join(" ")}\n`);
|
|
27
|
+
},
|
|
28
|
+
info: (...args) => {
|
|
29
|
+
if (threshold >= 2)
|
|
30
|
+
process.stderr.write(`[${ts()}] [INFO] ${args.map(String).join(" ")}\n`);
|
|
31
|
+
},
|
|
32
|
+
debug: (...args) => {
|
|
33
|
+
if (threshold >= 3)
|
|
34
|
+
process.stderr.write(`[${ts()}] [DEBUG] ${args.map(String).join(" ")}\n`);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const args = {
|
|
40
|
+
command: "serve",
|
|
41
|
+
sse: false,
|
|
42
|
+
http: false,
|
|
43
|
+
port: 3000,
|
|
44
|
+
verbose: false,
|
|
45
|
+
debug: false,
|
|
46
|
+
positional: [],
|
|
47
|
+
checkOnly: false,
|
|
48
|
+
offline: false,
|
|
49
|
+
};
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < argv.length) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
switch (arg) {
|
|
54
|
+
case "--sse":
|
|
55
|
+
args.sse = true;
|
|
56
|
+
break;
|
|
57
|
+
case "--http":
|
|
58
|
+
args.http = true;
|
|
59
|
+
break;
|
|
60
|
+
case "--port":
|
|
61
|
+
i++;
|
|
62
|
+
args.port = parseInt(argv[i], 10);
|
|
63
|
+
if (isNaN(args.port)) {
|
|
64
|
+
process.stderr.write("Error: --port requires a number\n");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case "--config":
|
|
69
|
+
i++;
|
|
70
|
+
args.configPath = resolve(argv[i]);
|
|
71
|
+
break;
|
|
72
|
+
case "--verbose":
|
|
73
|
+
args.verbose = true;
|
|
74
|
+
break;
|
|
75
|
+
case "--debug":
|
|
76
|
+
args.debug = true;
|
|
77
|
+
break;
|
|
78
|
+
case "--version":
|
|
79
|
+
args.command = "version";
|
|
80
|
+
break;
|
|
81
|
+
case "--help":
|
|
82
|
+
case "-h":
|
|
83
|
+
args.command = "help";
|
|
84
|
+
break;
|
|
85
|
+
case "--check":
|
|
86
|
+
args.checkOnly = true;
|
|
87
|
+
break;
|
|
88
|
+
case "--offline":
|
|
89
|
+
args.offline = true;
|
|
90
|
+
break;
|
|
91
|
+
case "init":
|
|
92
|
+
args.command = "init";
|
|
93
|
+
break;
|
|
94
|
+
case "install":
|
|
95
|
+
args.command = "install";
|
|
96
|
+
break;
|
|
97
|
+
case "catalog":
|
|
98
|
+
args.command = "catalog";
|
|
99
|
+
break;
|
|
100
|
+
case "servers":
|
|
101
|
+
args.command = "servers";
|
|
102
|
+
break;
|
|
103
|
+
case "search":
|
|
104
|
+
args.command = "search";
|
|
105
|
+
break;
|
|
106
|
+
case "update":
|
|
107
|
+
args.command = "update";
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
if (!arg.startsWith("-")) {
|
|
111
|
+
args.positional.push(arg);
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
return args;
|
|
118
|
+
}
|
|
119
|
+
// -- Commands -------------------------------------------------------------
|
|
120
|
+
function printVersion() {
|
|
121
|
+
process.stdout.write(`mcp-bridge ${PACKAGE_VERSION}\n`);
|
|
122
|
+
}
|
|
123
|
+
function printHelp() {
|
|
124
|
+
process.stdout.write(`
|
|
125
|
+
mcp-bridge v${PACKAGE_VERSION} — MCP server multiplexer
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
mcp-bridge Start in stdio mode (default)
|
|
129
|
+
mcp-bridge --sse --port 3000 Start as SSE server
|
|
130
|
+
mcp-bridge --http --port 3000 Start as streamable-http server
|
|
131
|
+
mcp-bridge init Create ~/.mcp-bridge/ with config template
|
|
132
|
+
mcp-bridge install <server> Install a server from the catalog
|
|
133
|
+
mcp-bridge catalog [--offline] List available servers
|
|
134
|
+
mcp-bridge servers List configured servers
|
|
135
|
+
mcp-bridge search <query> Search catalog by keyword
|
|
136
|
+
mcp-bridge update [--check] Check for / install updates
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
--config PATH Custom config file (default: ~/.mcp-bridge/config.json)
|
|
140
|
+
--verbose Info-level logs to stderr
|
|
141
|
+
--debug Full protocol-level logs to stderr
|
|
142
|
+
--version Print version
|
|
143
|
+
--help Show this help
|
|
144
|
+
|
|
145
|
+
All logs go to stderr. Stdout is reserved for the MCP protocol (stdio mode).
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
function cmdInit(logger) {
|
|
149
|
+
initConfigDir(logger);
|
|
150
|
+
}
|
|
151
|
+
function cmdCatalog(logger, offline) {
|
|
152
|
+
const catalogPath = join(PACKAGE_ROOT, "servers", "index.json");
|
|
153
|
+
if (!existsSync(catalogPath)) {
|
|
154
|
+
logger.error("Server catalog not found");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const catalog = JSON.parse(readFileSync(catalogPath, "utf-8"));
|
|
158
|
+
const servers = catalog.servers || {};
|
|
159
|
+
process.stdout.write("\nAvailable servers:\n\n");
|
|
160
|
+
process.stdout.write(" Server Transport Description\n");
|
|
161
|
+
process.stdout.write(" " + "─".repeat(60) + "\n");
|
|
162
|
+
for (const [name, info] of Object.entries(servers)) {
|
|
163
|
+
const padded = name.padEnd(16);
|
|
164
|
+
const transport = (info.transport || "stdio").padEnd(13);
|
|
165
|
+
process.stdout.write(` ${padded}${transport}${info.description || ""}\n`);
|
|
166
|
+
}
|
|
167
|
+
process.stdout.write("\n");
|
|
168
|
+
}
|
|
169
|
+
function cmdServers(logger, configPath) {
|
|
170
|
+
try {
|
|
171
|
+
const config = loadConfig({ configPath, logger });
|
|
172
|
+
const servers = config.servers || {};
|
|
173
|
+
const entries = Object.entries(servers);
|
|
174
|
+
if (entries.length === 0) {
|
|
175
|
+
process.stdout.write("No servers configured.\n");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
process.stdout.write("\nConfigured servers:\n\n");
|
|
179
|
+
process.stdout.write(" Server Transport Description\n");
|
|
180
|
+
process.stdout.write(" " + "─".repeat(60) + "\n");
|
|
181
|
+
for (const [name, serverConfig] of entries) {
|
|
182
|
+
const padded = name.padEnd(16);
|
|
183
|
+
const transport = serverConfig.transport.padEnd(13);
|
|
184
|
+
process.stdout.write(` ${padded}${transport}${serverConfig.description || ""}\n`);
|
|
185
|
+
}
|
|
186
|
+
process.stdout.write("\n");
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function cmdSearch(query, logger) {
|
|
194
|
+
const catalogPath = join(PACKAGE_ROOT, "servers", "index.json");
|
|
195
|
+
if (!existsSync(catalogPath)) {
|
|
196
|
+
logger.error("Server catalog not found");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
const catalog = JSON.parse(readFileSync(catalogPath, "utf-8"));
|
|
200
|
+
const servers = catalog.servers || {};
|
|
201
|
+
const lowerQuery = query.toLowerCase();
|
|
202
|
+
const matches = Object.entries(servers).filter(([name, info]) => {
|
|
203
|
+
return name.toLowerCase().includes(lowerQuery) ||
|
|
204
|
+
(info.description || "").toLowerCase().includes(lowerQuery);
|
|
205
|
+
});
|
|
206
|
+
if (matches.length === 0) {
|
|
207
|
+
process.stdout.write(`No servers matching "${query}"\n`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
process.stdout.write(`\nSearch results for "${query}":\n\n`);
|
|
211
|
+
for (const [i, [name, info]] of matches.entries()) {
|
|
212
|
+
process.stdout.write(` ${i + 1} ${name.padEnd(16)}${info.description || ""}\n`);
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write("\n");
|
|
215
|
+
}
|
|
216
|
+
function cmdInstall(serverName, logger) {
|
|
217
|
+
const scriptPath = join(PACKAGE_ROOT, "scripts", "install-server.sh");
|
|
218
|
+
if (!existsSync(scriptPath)) {
|
|
219
|
+
logger.error("Install script not found");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
execSync(`bash "${scriptPath}" "${serverName}"`, { stdio: "inherit" });
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function cmdUpdate(logger, checkOnly) {
|
|
230
|
+
if (checkOnly) {
|
|
231
|
+
const info = await checkForUpdate(logger);
|
|
232
|
+
if (info.updateAvailable) {
|
|
233
|
+
process.stdout.write(`Update available: ${info.currentVersion} → ${info.latestVersion}\n`);
|
|
234
|
+
process.stdout.write(`Run 'mcp-bridge update' to install.\n`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
process.stdout.write(`mcp-bridge ${info.currentVersion} is up to date.\n`);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const result = await runUpdate(logger);
|
|
242
|
+
process.stdout.write(result + "\n");
|
|
243
|
+
}
|
|
244
|
+
async function cmdServe(args, logger) {
|
|
245
|
+
let config;
|
|
246
|
+
try {
|
|
247
|
+
config = loadConfig({ configPath: args.configPath, logger });
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
// HTTP modes: require auth
|
|
254
|
+
if ((args.sse || args.http) && !config.http?.auth?.token) {
|
|
255
|
+
logger.error("HTTP auth not configured. Set http.auth in config or use stdio mode.");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
const server = new StandaloneServer(config, logger);
|
|
259
|
+
// Graceful shutdown
|
|
260
|
+
const shutdown = async () => {
|
|
261
|
+
await server.shutdown();
|
|
262
|
+
process.exit(0);
|
|
263
|
+
};
|
|
264
|
+
process.on("SIGTERM", shutdown);
|
|
265
|
+
process.on("SIGINT", shutdown);
|
|
266
|
+
if (args.sse || args.http) {
|
|
267
|
+
// SSE/HTTP mode: not yet implemented in standalone, show message
|
|
268
|
+
logger.error("SSE and HTTP server modes are not yet implemented. Use stdio mode (default).");
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
// Default: stdio mode
|
|
272
|
+
await server.startStdio();
|
|
273
|
+
}
|
|
274
|
+
// -- Main -----------------------------------------------------------------
|
|
275
|
+
async function main() {
|
|
276
|
+
const args = parseArgs(process.argv.slice(2));
|
|
277
|
+
const logLevel = args.debug ? "debug" : args.verbose ? "info" : "warn";
|
|
278
|
+
const logger = createLogger(logLevel);
|
|
279
|
+
switch (args.command) {
|
|
280
|
+
case "version":
|
|
281
|
+
printVersion();
|
|
282
|
+
break;
|
|
283
|
+
case "help":
|
|
284
|
+
printHelp();
|
|
285
|
+
break;
|
|
286
|
+
case "init":
|
|
287
|
+
cmdInit(logger);
|
|
288
|
+
break;
|
|
289
|
+
case "catalog":
|
|
290
|
+
cmdCatalog(logger, args.offline);
|
|
291
|
+
break;
|
|
292
|
+
case "servers":
|
|
293
|
+
cmdServers(logger, args.configPath);
|
|
294
|
+
break;
|
|
295
|
+
case "search":
|
|
296
|
+
if (args.positional.length === 0) {
|
|
297
|
+
process.stderr.write("Usage: mcp-bridge search <query>\n");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
cmdSearch(args.positional[0], logger);
|
|
301
|
+
break;
|
|
302
|
+
case "install":
|
|
303
|
+
if (args.positional.length === 0) {
|
|
304
|
+
process.stderr.write("Usage: mcp-bridge install <server>\n");
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
cmdInstall(args.positional[0], logger);
|
|
308
|
+
break;
|
|
309
|
+
case "update":
|
|
310
|
+
await cmdUpdate(logger, args.checkOnly);
|
|
311
|
+
break;
|
|
312
|
+
case "serve":
|
|
313
|
+
await cmdServe(args, logger);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
main().catch(err => {
|
|
318
|
+
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BridgeConfig, Logger } from "./types.js";
|
|
2
|
+
/** Parse a simple KEY=VALUE .env file (no npm dependency). */
|
|
3
|
+
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
4
|
+
export interface LoadConfigOptions {
|
|
5
|
+
configPath?: string;
|
|
6
|
+
logger?: Logger;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load and validate bridge config.
|
|
10
|
+
* 1. Read ~/.mcp-bridge/config.json (or custom path)
|
|
11
|
+
* 2. Parse ~/.mcp-bridge/.env
|
|
12
|
+
* 3. Resolve ${ENV_VAR} in config values
|
|
13
|
+
* 4. Validate required fields
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadConfig(options?: LoadConfigOptions): BridgeConfig;
|
|
16
|
+
/** Get the default config directory path. */
|
|
17
|
+
export declare function getConfigDir(configPath?: string): string;
|
|
18
|
+
/** Initialize the config directory with template files. */
|
|
19
|
+
export declare function initConfigDir(logger: Logger): void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, chmodSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { resolveEnvVars } from "./transport-base.js";
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
6
|
+
const DEFAULT_CONFIG_DIR = join(homedir(), ".mcp-bridge");
|
|
7
|
+
const DEFAULT_CONFIG_FILE = "config.json";
|
|
8
|
+
const DEFAULT_ENV_FILE = ".env";
|
|
9
|
+
/** Parse a simple KEY=VALUE .env file (no npm dependency). */
|
|
10
|
+
export function parseEnvFile(content) {
|
|
11
|
+
const env = {};
|
|
12
|
+
for (const line of content.split("\n")) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
15
|
+
continue;
|
|
16
|
+
const eqIdx = trimmed.indexOf("=");
|
|
17
|
+
if (eqIdx === -1)
|
|
18
|
+
continue;
|
|
19
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
20
|
+
let value = trimmed.substring(eqIdx + 1).trim();
|
|
21
|
+
// Strip surrounding quotes
|
|
22
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
23
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
24
|
+
value = value.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
if (key)
|
|
27
|
+
env[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return env;
|
|
30
|
+
}
|
|
31
|
+
/** Recursively resolve ${VAR} placeholders in a JSON-compatible value. */
|
|
32
|
+
function resolveConfigValue(value, extraEnv) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
return resolveEnvVars(value, "config value", extraEnv);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value.map(item => resolveConfigValue(item, extraEnv));
|
|
38
|
+
}
|
|
39
|
+
if (value !== null && typeof value === "object") {
|
|
40
|
+
const resolved = {};
|
|
41
|
+
for (const [k, v] of Object.entries(value)) {
|
|
42
|
+
resolved[k] = resolveConfigValue(v, extraEnv);
|
|
43
|
+
}
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Load and validate bridge config.
|
|
50
|
+
* 1. Read ~/.mcp-bridge/config.json (or custom path)
|
|
51
|
+
* 2. Parse ~/.mcp-bridge/.env
|
|
52
|
+
* 3. Resolve ${ENV_VAR} in config values
|
|
53
|
+
* 4. Validate required fields
|
|
54
|
+
*/
|
|
55
|
+
export function loadConfig(options = {}) {
|
|
56
|
+
const configDir = options.configPath
|
|
57
|
+
? join(options.configPath, "..") // If a file path is given, derive directory
|
|
58
|
+
: DEFAULT_CONFIG_DIR;
|
|
59
|
+
const configPath = options.configPath || join(DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE);
|
|
60
|
+
const envPath = join(options.configPath ? join(options.configPath, "..") : DEFAULT_CONFIG_DIR, DEFAULT_ENV_FILE);
|
|
61
|
+
if (!existsSync(configPath)) {
|
|
62
|
+
throw new Error(`Config file not found: ${configPath}\nRun 'mcp-bridge init' to set up.`);
|
|
63
|
+
}
|
|
64
|
+
// Load .env file
|
|
65
|
+
let dotEnv = {};
|
|
66
|
+
if (existsSync(envPath)) {
|
|
67
|
+
try {
|
|
68
|
+
dotEnv = parseEnvFile(readFileSync(envPath, "utf-8"));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
options.logger?.warn(`[mcp-bridge] Failed to parse .env file: ${err instanceof Error ? err.message : err}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Merge .env into process.env (don't overwrite existing)
|
|
75
|
+
for (const [key, value] of Object.entries(dotEnv)) {
|
|
76
|
+
if (process.env[key] === undefined) {
|
|
77
|
+
process.env[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Read and parse config
|
|
81
|
+
const rawConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
82
|
+
// Resolve ${VAR} placeholders using .env + process.env
|
|
83
|
+
const mergedEnv = { ...dotEnv };
|
|
84
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
85
|
+
if (mergedEnv[k] === undefined)
|
|
86
|
+
mergedEnv[k] = v;
|
|
87
|
+
}
|
|
88
|
+
let config;
|
|
89
|
+
try {
|
|
90
|
+
config = resolveConfigValue(rawConfig, mergedEnv);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
throw new Error(`Config resolution failed: ${err instanceof Error ? err.message : err}`);
|
|
94
|
+
}
|
|
95
|
+
// Validate required fields
|
|
96
|
+
if (!config.servers || typeof config.servers !== "object") {
|
|
97
|
+
throw new Error("Config must have a 'servers' object");
|
|
98
|
+
}
|
|
99
|
+
return config;
|
|
100
|
+
}
|
|
101
|
+
/** Get the default config directory path. */
|
|
102
|
+
export function getConfigDir(configPath) {
|
|
103
|
+
return configPath ? join(configPath, "..") : DEFAULT_CONFIG_DIR;
|
|
104
|
+
}
|
|
105
|
+
/** Initialize the config directory with template files. */
|
|
106
|
+
export function initConfigDir(logger) {
|
|
107
|
+
const dir = DEFAULT_CONFIG_DIR;
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
109
|
+
// Set directory permissions (Linux/macOS)
|
|
110
|
+
try {
|
|
111
|
+
chmodSync(dir, 0o700);
|
|
112
|
+
}
|
|
113
|
+
catch { /* Windows doesn't support chmod */ }
|
|
114
|
+
const configPath = join(dir, DEFAULT_CONFIG_FILE);
|
|
115
|
+
if (!existsSync(configPath)) {
|
|
116
|
+
const template = {
|
|
117
|
+
mode: "router",
|
|
118
|
+
servers: {},
|
|
119
|
+
toolPrefix: true,
|
|
120
|
+
connectionTimeoutMs: 5000,
|
|
121
|
+
requestTimeoutMs: 60000,
|
|
122
|
+
routerIdleTimeoutMs: 600000,
|
|
123
|
+
routerMaxConcurrent: 5,
|
|
124
|
+
http: {
|
|
125
|
+
auth: {
|
|
126
|
+
type: "bearer",
|
|
127
|
+
token: "${MCP_BRIDGE_TOKEN}"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n");
|
|
132
|
+
logger.info(`Created config: ${configPath}`);
|
|
133
|
+
}
|
|
134
|
+
const envPath = join(dir, DEFAULT_ENV_FILE);
|
|
135
|
+
if (!existsSync(envPath)) {
|
|
136
|
+
const token = randomBytes(32).toString("hex");
|
|
137
|
+
writeFileSync(envPath, `# MCP Bridge environment variables\nMCP_BRIDGE_TOKEN=${token}\n`);
|
|
138
|
+
try {
|
|
139
|
+
chmodSync(envPath, 0o600);
|
|
140
|
+
}
|
|
141
|
+
catch { /* Windows */ }
|
|
142
|
+
logger.info(`Created .env: ${envPath} (with generated token)`);
|
|
143
|
+
}
|
|
144
|
+
logger.info(`Config directory ready: ${dir}`);
|
|
145
|
+
}
|
|
@@ -1,44 +1,15 @@
|
|
|
1
|
-
// Core exports for @aiwerk/mcp-bridge
|
|
2
|
-
|
|
3
|
-
// Transport classes
|
|
4
1
|
export { BaseTransport, resolveEnvVars, resolveEnvRecord, resolveArgs, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
5
2
|
export { StdioTransport } from "./transport-stdio.js";
|
|
6
3
|
export { SseTransport } from "./transport-sse.js";
|
|
7
4
|
export { StreamableHttpTransport } from "./transport-streamable-http.js";
|
|
8
|
-
|
|
9
|
-
// Router
|
|
10
5
|
export { McpRouter } from "./mcp-router.js";
|
|
11
6
|
export type { RouterToolHint, RouterServerStatus, RouterDispatchResponse, RouterTransportRefs } from "./mcp-router.js";
|
|
12
|
-
|
|
13
|
-
// Schema conversion
|
|
14
7
|
export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
|
|
15
|
-
|
|
16
|
-
// Protocol helpers
|
|
17
8
|
export { initializeProtocol, fetchToolsList, PACKAGE_VERSION } from "./protocol.js";
|
|
18
|
-
|
|
19
|
-
// Config
|
|
20
9
|
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir } from "./config.js";
|
|
21
|
-
|
|
22
|
-
// Types
|
|
23
|
-
export type {
|
|
24
|
-
Logger,
|
|
25
|
-
McpServerConfig,
|
|
26
|
-
McpClientConfig,
|
|
27
|
-
McpTool,
|
|
28
|
-
McpRequest,
|
|
29
|
-
McpResponse,
|
|
30
|
-
McpTransport,
|
|
31
|
-
McpServerConnection,
|
|
32
|
-
BridgeConfig,
|
|
33
|
-
} from "./types.js";
|
|
10
|
+
export type { Logger, McpServerConfig, McpClientConfig, McpTool, McpRequest, McpResponse, McpTransport, McpServerConnection, BridgeConfig, } from "./types.js";
|
|
34
11
|
export { nextRequestId } from "./types.js";
|
|
35
|
-
|
|
36
|
-
// Tool naming
|
|
37
12
|
export { pickRegisteredToolName } from "./tool-naming.js";
|
|
38
|
-
|
|
39
|
-
// Standalone server
|
|
40
13
|
export { StandaloneServer } from "./standalone-server.js";
|
|
41
|
-
|
|
42
|
-
// Update checker
|
|
43
14
|
export { checkForUpdate, getUpdateNotice, runUpdate, resetNoticeFlag } from "./update-checker.js";
|
|
44
15
|
export type { UpdateInfo } from "./update-checker.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Core exports for @aiwerk/mcp-bridge
|
|
2
|
+
// Transport classes
|
|
3
|
+
export { BaseTransport, resolveEnvVars, resolveEnvRecord, resolveArgs, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
4
|
+
export { StdioTransport } from "./transport-stdio.js";
|
|
5
|
+
export { SseTransport } from "./transport-sse.js";
|
|
6
|
+
export { StreamableHttpTransport } from "./transport-streamable-http.js";
|
|
7
|
+
// Router
|
|
8
|
+
export { McpRouter } from "./mcp-router.js";
|
|
9
|
+
// Schema conversion
|
|
10
|
+
export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
|
|
11
|
+
// Protocol helpers
|
|
12
|
+
export { initializeProtocol, fetchToolsList, PACKAGE_VERSION } from "./protocol.js";
|
|
13
|
+
// Config
|
|
14
|
+
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir } from "./config.js";
|
|
15
|
+
export { nextRequestId } from "./types.js";
|
|
16
|
+
// Tool naming
|
|
17
|
+
export { pickRegisteredToolName } from "./tool-naming.js";
|
|
18
|
+
// Standalone server
|
|
19
|
+
export { StandaloneServer } from "./standalone-server.js";
|
|
20
|
+
// Update checker
|
|
21
|
+
export { checkForUpdate, getUpdateNotice, runUpdate, resetNoticeFlag } from "./update-checker.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { McpClientConfig, McpServerConfig, McpTransport } from "./types.js";
|
|
2
|
+
type RouterErrorCode = "unknown_server" | "unknown_tool" | "connection_failed" | "mcp_error" | "invalid_params";
|
|
3
|
+
export interface RouterToolHint {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
requiredParams: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface RouterServerStatus {
|
|
9
|
+
name: string;
|
|
10
|
+
transport: string;
|
|
11
|
+
status: "connected" | "idle" | "disconnected";
|
|
12
|
+
tools: number;
|
|
13
|
+
lastUsed?: string;
|
|
14
|
+
}
|
|
15
|
+
export type RouterDispatchResponse = {
|
|
16
|
+
server: string;
|
|
17
|
+
action: "list";
|
|
18
|
+
tools: RouterToolHint[];
|
|
19
|
+
} | {
|
|
20
|
+
server: string;
|
|
21
|
+
action: "refresh";
|
|
22
|
+
refreshed: true;
|
|
23
|
+
tools: RouterToolHint[];
|
|
24
|
+
} | {
|
|
25
|
+
server: string;
|
|
26
|
+
action: "call";
|
|
27
|
+
tool: string;
|
|
28
|
+
result: any;
|
|
29
|
+
} | {
|
|
30
|
+
action: "status";
|
|
31
|
+
servers: RouterServerStatus[];
|
|
32
|
+
} | {
|
|
33
|
+
error: RouterErrorCode;
|
|
34
|
+
message: string;
|
|
35
|
+
available?: string[];
|
|
36
|
+
code?: number;
|
|
37
|
+
};
|
|
38
|
+
export interface RouterTransportRefs {
|
|
39
|
+
sse: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: any, onReconnected?: () => Promise<void>) => McpTransport;
|
|
40
|
+
stdio: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: any, onReconnected?: () => Promise<void>) => McpTransport;
|
|
41
|
+
streamableHttp: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: any, onReconnected?: () => Promise<void>) => McpTransport;
|
|
42
|
+
}
|
|
43
|
+
export declare class McpRouter {
|
|
44
|
+
private readonly servers;
|
|
45
|
+
private readonly clientConfig;
|
|
46
|
+
private readonly logger;
|
|
47
|
+
private readonly transportRefs;
|
|
48
|
+
private readonly idleTimeoutMs;
|
|
49
|
+
private readonly maxConcurrent;
|
|
50
|
+
private readonly states;
|
|
51
|
+
constructor(servers: Record<string, McpServerConfig>, clientConfig: McpClientConfig, logger: any, transportRefs?: Partial<RouterTransportRefs>);
|
|
52
|
+
static generateDescription(servers: Record<string, McpServerConfig>): string;
|
|
53
|
+
dispatch(server?: string, action?: string, tool?: string, params?: any): Promise<RouterDispatchResponse>;
|
|
54
|
+
getToolList(server: string): Promise<RouterToolHint[]>;
|
|
55
|
+
private getStatus;
|
|
56
|
+
disconnectAll(): Promise<void>;
|
|
57
|
+
private ensureConnected;
|
|
58
|
+
private enforceMaxConcurrent;
|
|
59
|
+
private disconnectServer;
|
|
60
|
+
private markUsed;
|
|
61
|
+
private createTransport;
|
|
62
|
+
private extractRequiredParams;
|
|
63
|
+
private error;
|
|
64
|
+
}
|
|
65
|
+
export {};
|