@aiwerk/mcp-bridge 1.0.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/mcp-bridge.js +9 -0
- package/bin/mcp-bridge.ts +335 -0
- package/package.json +42 -0
- package/scripts/install-server.ps1 +300 -0
- package/scripts/install-server.sh +357 -0
- package/servers/apify/README.md +40 -0
- package/servers/apify/config.json +13 -0
- package/servers/apify/env_vars +1 -0
- package/servers/apify/install.ps1 +3 -0
- package/servers/apify/install.sh +4 -0
- package/servers/candidates.md +13 -0
- package/servers/github/README.md +40 -0
- package/servers/github/config.json +21 -0
- package/servers/github/env_vars +1 -0
- package/servers/github/install.ps1 +3 -0
- package/servers/github/install.sh +4 -0
- package/servers/google-maps/README.md +40 -0
- package/servers/google-maps/config.json +17 -0
- package/servers/google-maps/env_vars +1 -0
- package/servers/google-maps/install.ps1 +3 -0
- package/servers/google-maps/install.sh +4 -0
- package/servers/hetzner/README.md +41 -0
- package/servers/hetzner/config.json +16 -0
- package/servers/hetzner/env_vars +1 -0
- package/servers/hetzner/install.ps1 +3 -0
- package/servers/hetzner/install.sh +4 -0
- package/servers/hostinger/README.md +40 -0
- package/servers/hostinger/config.json +17 -0
- package/servers/hostinger/env_vars +1 -0
- package/servers/hostinger/install.ps1 +3 -0
- package/servers/hostinger/install.sh +4 -0
- package/servers/index.json +125 -0
- package/servers/linear/README.md +40 -0
- package/servers/linear/config.json +16 -0
- package/servers/linear/env_vars +1 -0
- package/servers/linear/install.ps1 +3 -0
- package/servers/linear/install.sh +4 -0
- package/servers/miro/README.md +40 -0
- package/servers/miro/config.json +19 -0
- package/servers/miro/env_vars +1 -0
- package/servers/miro/install.ps1 +3 -0
- package/servers/miro/install.sh +4 -0
- package/servers/notion/README.md +42 -0
- package/servers/notion/config.json +17 -0
- package/servers/notion/env_vars +1 -0
- package/servers/notion/install.ps1 +3 -0
- package/servers/notion/install.sh +4 -0
- package/servers/stripe/README.md +40 -0
- package/servers/stripe/config.json +19 -0
- package/servers/stripe/env_vars +1 -0
- package/servers/stripe/install.ps1 +3 -0
- package/servers/stripe/install.sh +4 -0
- package/servers/tavily/README.md +40 -0
- package/servers/tavily/config.json +17 -0
- package/servers/tavily/env_vars +1 -0
- package/servers/tavily/install.ps1 +3 -0
- package/servers/tavily/install.sh +4 -0
- package/servers/todoist/README.md +40 -0
- package/servers/todoist/config.json +17 -0
- package/servers/todoist/env_vars +1 -0
- package/servers/todoist/install.ps1 +3 -0
- package/servers/todoist/install.sh +4 -0
- package/servers/wise/README.md +41 -0
- package/servers/wise/config.json +16 -0
- package/servers/wise/env_vars +1 -0
- package/servers/wise/install.ps1 +3 -0
- package/servers/wise/install.sh +4 -0
- package/src/config.ts +168 -0
- package/src/index.ts +44 -0
- package/src/mcp-router.ts +366 -0
- package/src/protocol.ts +69 -0
- package/src/schema-convert.ts +178 -0
- package/src/standalone-server.ts +385 -0
- package/src/tool-naming.ts +51 -0
- package/src/transport-base.ts +199 -0
- package/src/transport-sse.ts +230 -0
- package/src/transport-stdio.ts +312 -0
- package/src/transport-streamable-http.ts +188 -0
- package/src/types.ts +88 -0
- package/src/update-checker.ts +155 -0
- package/tests/collision.test.ts +60 -0
- package/tests/env-resolve.test.ts +68 -0
- package/tests/mcp-router.test.ts +301 -0
- package/tests/schema-convert.test.ts +70 -0
- package/tests/transport-base.test.ts +214 -0
- package/tsconfig.json +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AIWerk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @aiwerk/mcp-bridge
|
|
2
|
+
|
|
3
|
+
Standalone MCP server that multiplexes multiple MCP servers into one interface. Works with Claude Desktop, Cursor, Windsurf, Cline, or any MCP client.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @aiwerk/mcp-bridge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Initialize config directory
|
|
15
|
+
mcp-bridge init
|
|
16
|
+
|
|
17
|
+
# Edit your config
|
|
18
|
+
vi ~/.mcp-bridge/config.json
|
|
19
|
+
|
|
20
|
+
# Install a server from the catalog
|
|
21
|
+
mcp-bridge install todoist
|
|
22
|
+
|
|
23
|
+
# Start (stdio mode for MCP clients)
|
|
24
|
+
mcp-bridge
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
Config location: `~/.mcp-bridge/config.json`
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mode": "router",
|
|
34
|
+
"servers": {
|
|
35
|
+
"todoist": {
|
|
36
|
+
"transport": "stdio",
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@doist/todoist-ai"],
|
|
39
|
+
"env": {
|
|
40
|
+
"TODOIST_API_KEY": "${TODOIST_API_TOKEN}"
|
|
41
|
+
},
|
|
42
|
+
"description": "Task management"
|
|
43
|
+
},
|
|
44
|
+
"github": {
|
|
45
|
+
"transport": "stdio",
|
|
46
|
+
"command": "docker",
|
|
47
|
+
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
|
|
48
|
+
"env": {
|
|
49
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
|
50
|
+
},
|
|
51
|
+
"description": "GitHub repos, issues, PRs"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"toolPrefix": true,
|
|
55
|
+
"connectionTimeoutMs": 5000,
|
|
56
|
+
"requestTimeoutMs": 60000
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Environment variables go in `~/.mcp-bridge/.env`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
TODOIST_API_TOKEN=your-token-here
|
|
64
|
+
GITHUB_TOKEN=ghp_xxxxx
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Modes
|
|
68
|
+
|
|
69
|
+
**Router mode** (default) — exposes a single `mcp` meta-tool. The agent calls `mcp(server="todoist", action="list")` to discover tools, then `mcp(server="todoist", tool="get_tasks", params={...})` to call them. ~99% reduction in tool registration tokens.
|
|
70
|
+
|
|
71
|
+
**Direct mode** — registers all tools from all servers individually (`todoist_get_tasks`, `github_list_repos`, etc.). Better for few servers or simpler agents.
|
|
72
|
+
|
|
73
|
+
## Claude Desktop
|
|
74
|
+
|
|
75
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"mcp-bridge": {
|
|
81
|
+
"command": "mcp-bridge",
|
|
82
|
+
"args": []
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## CLI
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
mcp-bridge # stdio mode (default)
|
|
92
|
+
mcp-bridge --verbose # info-level logs to stderr
|
|
93
|
+
mcp-bridge --debug # full protocol logs to stderr
|
|
94
|
+
mcp-bridge --config ./config.json # custom config file
|
|
95
|
+
|
|
96
|
+
mcp-bridge init # create ~/.mcp-bridge/ with template
|
|
97
|
+
mcp-bridge install <server> # install from catalog
|
|
98
|
+
mcp-bridge catalog # list available servers
|
|
99
|
+
mcp-bridge servers # list configured servers
|
|
100
|
+
mcp-bridge search <query> # search catalog
|
|
101
|
+
mcp-bridge update --check # check for updates
|
|
102
|
+
mcp-bridge update # install updates
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Library Usage
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { McpRouter, StandaloneServer, loadConfig } from "@aiwerk/mcp-bridge";
|
|
109
|
+
|
|
110
|
+
const config = loadConfig({ configPath: "./config.json" });
|
|
111
|
+
const server = new StandaloneServer(config, console);
|
|
112
|
+
await server.startStdio();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Bootstrap: run the TypeScript entry point via tsx
|
|
4
|
+
import { register } from "node:module";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
register("tsx/esm", pathToFileURL("./"));
|
|
8
|
+
|
|
9
|
+
const { default: _ } = await import("./mcp-bridge.ts");
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { join, dirname, resolve } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { loadConfig, initConfigDir, getConfigDir } from "../src/config.js";
|
|
8
|
+
import { StandaloneServer } from "../src/standalone-server.js";
|
|
9
|
+
import { PACKAGE_VERSION } from "../src/protocol.js";
|
|
10
|
+
import { checkForUpdate, runUpdate } from "../src/update-checker.js";
|
|
11
|
+
import type { Logger } from "../src/types.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// -- Logger ---------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
type LogLevel = "error" | "warn" | "info" | "debug";
|
|
19
|
+
|
|
20
|
+
function createLogger(level: LogLevel): Logger {
|
|
21
|
+
const levels: Record<LogLevel, number> = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
22
|
+
const threshold = levels[level];
|
|
23
|
+
const ts = () => new Date().toISOString().replace("T", " ").replace("Z", "");
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
error: (...args: unknown[]) => {
|
|
27
|
+
if (threshold >= 0) process.stderr.write(`[${ts()}] [ERROR] ${args.map(String).join(" ")}\n`);
|
|
28
|
+
},
|
|
29
|
+
warn: (...args: unknown[]) => {
|
|
30
|
+
if (threshold >= 1) process.stderr.write(`[${ts()}] [WARN] ${args.map(String).join(" ")}\n`);
|
|
31
|
+
},
|
|
32
|
+
info: (...args: unknown[]) => {
|
|
33
|
+
if (threshold >= 2) process.stderr.write(`[${ts()}] [INFO] ${args.map(String).join(" ")}\n`);
|
|
34
|
+
},
|
|
35
|
+
debug: (...args: unknown[]) => {
|
|
36
|
+
if (threshold >= 3) process.stderr.write(`[${ts()}] [DEBUG] ${args.map(String).join(" ")}\n`);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// -- Arg parsing ----------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
interface CliArgs {
|
|
44
|
+
command: "serve" | "init" | "install" | "catalog" | "servers" | "search" | "update" | "version" | "help";
|
|
45
|
+
sse: boolean;
|
|
46
|
+
http: boolean;
|
|
47
|
+
port: number;
|
|
48
|
+
configPath?: string;
|
|
49
|
+
verbose: boolean;
|
|
50
|
+
debug: boolean;
|
|
51
|
+
positional: string[];
|
|
52
|
+
checkOnly: boolean;
|
|
53
|
+
offline: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
57
|
+
const args: CliArgs = {
|
|
58
|
+
command: "serve",
|
|
59
|
+
sse: false,
|
|
60
|
+
http: false,
|
|
61
|
+
port: 3000,
|
|
62
|
+
verbose: false,
|
|
63
|
+
debug: false,
|
|
64
|
+
positional: [],
|
|
65
|
+
checkOnly: false,
|
|
66
|
+
offline: false,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let i = 0;
|
|
70
|
+
while (i < argv.length) {
|
|
71
|
+
const arg = argv[i];
|
|
72
|
+
switch (arg) {
|
|
73
|
+
case "--sse": args.sse = true; break;
|
|
74
|
+
case "--http": args.http = true; break;
|
|
75
|
+
case "--port":
|
|
76
|
+
i++;
|
|
77
|
+
args.port = parseInt(argv[i], 10);
|
|
78
|
+
if (isNaN(args.port)) { process.stderr.write("Error: --port requires a number\n"); process.exit(1); }
|
|
79
|
+
break;
|
|
80
|
+
case "--config":
|
|
81
|
+
i++;
|
|
82
|
+
args.configPath = resolve(argv[i]);
|
|
83
|
+
break;
|
|
84
|
+
case "--verbose": args.verbose = true; break;
|
|
85
|
+
case "--debug": args.debug = true; break;
|
|
86
|
+
case "--version": args.command = "version"; break;
|
|
87
|
+
case "--help": case "-h": args.command = "help"; break;
|
|
88
|
+
case "--check": args.checkOnly = true; break;
|
|
89
|
+
case "--offline": args.offline = true; break;
|
|
90
|
+
case "init": args.command = "init"; break;
|
|
91
|
+
case "install": args.command = "install"; break;
|
|
92
|
+
case "catalog": args.command = "catalog"; break;
|
|
93
|
+
case "servers": args.command = "servers"; break;
|
|
94
|
+
case "search": args.command = "search"; break;
|
|
95
|
+
case "update": args.command = "update"; break;
|
|
96
|
+
default:
|
|
97
|
+
if (!arg.startsWith("-")) {
|
|
98
|
+
args.positional.push(arg);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return args;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// -- Commands -------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
function printVersion(): void {
|
|
111
|
+
process.stdout.write(`mcp-bridge ${PACKAGE_VERSION}\n`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function printHelp(): void {
|
|
115
|
+
process.stdout.write(`
|
|
116
|
+
mcp-bridge v${PACKAGE_VERSION} — MCP server multiplexer
|
|
117
|
+
|
|
118
|
+
Usage:
|
|
119
|
+
mcp-bridge Start in stdio mode (default)
|
|
120
|
+
mcp-bridge --sse --port 3000 Start as SSE server
|
|
121
|
+
mcp-bridge --http --port 3000 Start as streamable-http server
|
|
122
|
+
mcp-bridge init Create ~/.mcp-bridge/ with config template
|
|
123
|
+
mcp-bridge install <server> Install a server from the catalog
|
|
124
|
+
mcp-bridge catalog [--offline] List available servers
|
|
125
|
+
mcp-bridge servers List configured servers
|
|
126
|
+
mcp-bridge search <query> Search catalog by keyword
|
|
127
|
+
mcp-bridge update [--check] Check for / install updates
|
|
128
|
+
|
|
129
|
+
Options:
|
|
130
|
+
--config PATH Custom config file (default: ~/.mcp-bridge/config.json)
|
|
131
|
+
--verbose Info-level logs to stderr
|
|
132
|
+
--debug Full protocol-level logs to stderr
|
|
133
|
+
--version Print version
|
|
134
|
+
--help Show this help
|
|
135
|
+
|
|
136
|
+
All logs go to stderr. Stdout is reserved for the MCP protocol (stdio mode).
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cmdInit(logger: Logger): void {
|
|
141
|
+
initConfigDir(logger);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function cmdCatalog(logger: Logger, offline: boolean): void {
|
|
145
|
+
const catalogPath = join(__dirname, "..", "servers", "index.json");
|
|
146
|
+
if (!existsSync(catalogPath)) {
|
|
147
|
+
logger.error("Server catalog not found");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const catalog = JSON.parse(readFileSync(catalogPath, "utf-8"));
|
|
152
|
+
const servers = catalog.servers || {};
|
|
153
|
+
|
|
154
|
+
process.stdout.write("\nAvailable servers:\n\n");
|
|
155
|
+
process.stdout.write(" Server Transport Description\n");
|
|
156
|
+
process.stdout.write(" " + "─".repeat(60) + "\n");
|
|
157
|
+
|
|
158
|
+
for (const [name, info] of Object.entries(servers) as [string, any][]) {
|
|
159
|
+
const padded = name.padEnd(16);
|
|
160
|
+
const transport = (info.transport || "stdio").padEnd(13);
|
|
161
|
+
process.stdout.write(` ${padded}${transport}${info.description || ""}\n`);
|
|
162
|
+
}
|
|
163
|
+
process.stdout.write("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cmdServers(logger: Logger, configPath?: string): void {
|
|
167
|
+
try {
|
|
168
|
+
const config = loadConfig({ configPath, logger });
|
|
169
|
+
const servers = config.servers || {};
|
|
170
|
+
const entries = Object.entries(servers);
|
|
171
|
+
|
|
172
|
+
if (entries.length === 0) {
|
|
173
|
+
process.stdout.write("No servers configured.\n");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
process.stdout.write("\nConfigured servers:\n\n");
|
|
178
|
+
process.stdout.write(" Server Transport Description\n");
|
|
179
|
+
process.stdout.write(" " + "─".repeat(60) + "\n");
|
|
180
|
+
|
|
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
|
+
} catch (err) {
|
|
188
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cmdSearch(query: string, logger: Logger): void {
|
|
194
|
+
const catalogPath = join(__dirname, "..", "servers", "index.json");
|
|
195
|
+
if (!existsSync(catalogPath)) {
|
|
196
|
+
logger.error("Server catalog not found");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const catalog = JSON.parse(readFileSync(catalogPath, "utf-8"));
|
|
201
|
+
const servers = catalog.servers || {};
|
|
202
|
+
const lowerQuery = query.toLowerCase();
|
|
203
|
+
|
|
204
|
+
const matches = Object.entries(servers).filter(([name, info]: [string, any]) => {
|
|
205
|
+
return name.toLowerCase().includes(lowerQuery) ||
|
|
206
|
+
(info.description || "").toLowerCase().includes(lowerQuery);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (matches.length === 0) {
|
|
210
|
+
process.stdout.write(`No servers matching "${query}"\n`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.stdout.write(`\nSearch results for "${query}":\n\n`);
|
|
215
|
+
for (const [i, [name, info]] of matches.entries() as any) {
|
|
216
|
+
process.stdout.write(` ${i + 1} ${name.padEnd(16)}${(info as any).description || ""}\n`);
|
|
217
|
+
}
|
|
218
|
+
process.stdout.write("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cmdInstall(serverName: string, logger: Logger): void {
|
|
222
|
+
const scriptPath = join(__dirname, "..", "scripts", "install-server.sh");
|
|
223
|
+
if (!existsSync(scriptPath)) {
|
|
224
|
+
logger.error("Install script not found");
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
execSync(`bash "${scriptPath}" "${serverName}"`, { stdio: "inherit" });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function cmdUpdate(logger: Logger, checkOnly: boolean): Promise<void> {
|
|
236
|
+
if (checkOnly) {
|
|
237
|
+
const info = await checkForUpdate(logger);
|
|
238
|
+
if (info.updateAvailable) {
|
|
239
|
+
process.stdout.write(`Update available: ${info.currentVersion} → ${info.latestVersion}\n`);
|
|
240
|
+
process.stdout.write(`Run 'mcp-bridge update' to install.\n`);
|
|
241
|
+
} else {
|
|
242
|
+
process.stdout.write(`mcp-bridge ${info.currentVersion} is up to date.\n`);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = await runUpdate(logger);
|
|
248
|
+
process.stdout.write(result + "\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function cmdServe(args: CliArgs, logger: Logger): Promise<void> {
|
|
252
|
+
let config;
|
|
253
|
+
try {
|
|
254
|
+
config = loadConfig({ configPath: args.configPath, logger });
|
|
255
|
+
} catch (err) {
|
|
256
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// HTTP modes: require auth
|
|
261
|
+
if ((args.sse || args.http) && !config.http?.auth?.token) {
|
|
262
|
+
logger.error("HTTP auth not configured. Set http.auth in config or use stdio mode.");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const server = new StandaloneServer(config, logger);
|
|
267
|
+
|
|
268
|
+
// Graceful shutdown
|
|
269
|
+
const shutdown = async () => {
|
|
270
|
+
await server.shutdown();
|
|
271
|
+
process.exit(0);
|
|
272
|
+
};
|
|
273
|
+
process.on("SIGTERM", shutdown);
|
|
274
|
+
process.on("SIGINT", shutdown);
|
|
275
|
+
|
|
276
|
+
if (args.sse || args.http) {
|
|
277
|
+
// SSE/HTTP mode: not yet implemented in standalone, show message
|
|
278
|
+
logger.error("SSE and HTTP server modes are not yet implemented. Use stdio mode (default).");
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Default: stdio mode
|
|
283
|
+
await server.startStdio();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// -- Main -----------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
async function main(): Promise<void> {
|
|
289
|
+
const args = parseArgs(process.argv.slice(2));
|
|
290
|
+
const logLevel: LogLevel = args.debug ? "debug" : args.verbose ? "info" : "warn";
|
|
291
|
+
const logger = createLogger(logLevel);
|
|
292
|
+
|
|
293
|
+
switch (args.command) {
|
|
294
|
+
case "version":
|
|
295
|
+
printVersion();
|
|
296
|
+
break;
|
|
297
|
+
case "help":
|
|
298
|
+
printHelp();
|
|
299
|
+
break;
|
|
300
|
+
case "init":
|
|
301
|
+
cmdInit(logger);
|
|
302
|
+
break;
|
|
303
|
+
case "catalog":
|
|
304
|
+
cmdCatalog(logger, args.offline);
|
|
305
|
+
break;
|
|
306
|
+
case "servers":
|
|
307
|
+
cmdServers(logger, args.configPath);
|
|
308
|
+
break;
|
|
309
|
+
case "search":
|
|
310
|
+
if (args.positional.length === 0) {
|
|
311
|
+
process.stderr.write("Usage: mcp-bridge search <query>\n");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
cmdSearch(args.positional[0], logger);
|
|
315
|
+
break;
|
|
316
|
+
case "install":
|
|
317
|
+
if (args.positional.length === 0) {
|
|
318
|
+
process.stderr.write("Usage: mcp-bridge install <server>\n");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
cmdInstall(args.positional[0], logger);
|
|
322
|
+
break;
|
|
323
|
+
case "update":
|
|
324
|
+
await cmdUpdate(logger, args.checkOnly);
|
|
325
|
+
break;
|
|
326
|
+
case "serve":
|
|
327
|
+
await cmdServe(args, logger);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
main().catch(err => {
|
|
333
|
+
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aiwerk/mcp-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Standalone MCP server that multiplexes multiple MCP servers into one interface",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-bridge": "./bin/mcp-bridge.js"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "AIWerk <kontakt@aiwerk.ch>",
|
|
12
|
+
"homepage": "https://github.com/AIWerk/mcp-bridge#readme",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/AIWerk/mcp-bridge"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"bridge",
|
|
21
|
+
"multiplexer",
|
|
22
|
+
"ai",
|
|
23
|
+
"agent",
|
|
24
|
+
"tools"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"test": "node --import tsx --test tests/*.test.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@sinclair/typebox": "^0.34.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"tsx": "^4.0.0",
|
|
40
|
+
"typescript": "^5.7.0"
|
|
41
|
+
}
|
|
42
|
+
}
|