@aiwerk/mcp-bridge 2.8.44 → 2.9.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/README.md +37 -2
- package/dist/bin/mcp-bridge.js +97 -134
- package/dist/src/catalog-client.d.ts +51 -23
- package/dist/src/catalog-client.js +119 -52
- package/dist/src/config.d.ts +2 -35
- package/dist/src/config.js +2 -133
- package/dist/src/index.d.ts +1 -5
- package/dist/src/index.js +1 -5
- package/dist/src/mcp-router.d.ts +0 -37
- package/dist/src/mcp-router.js +3 -138
- package/dist/src/standalone-server.js +6 -13
- package/dist/src/types.d.ts +0 -10
- package/dist/src/validate-recipe.js +62 -0
- package/package.json +1 -1
- package/scripts/install-server.sh +9 -22
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://github.com/AIWerk/mcp-bridge/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@aiwerk/mcp-bridge)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](#status)
|
|
6
7
|
|
|
7
8
|
**Your AI, Connected to Everything.** Multiplex multiple MCP servers into one interface. One config, one connection, all your tools.
|
|
8
9
|
|
|
@@ -10,6 +11,32 @@
|
|
|
10
11
|
|
|
11
12
|
Works with **Claude Code**, **Codex (OpenAI)**, **Claude Desktop**, **Cursor**, **Windsurf**, **Cline**, **OpenClaw**, or any MCP client.
|
|
12
13
|
|
|
14
|
+
## Status
|
|
15
|
+
|
|
16
|
+
`@aiwerk/mcp-bridge` is **actively developed** as of 2026-05-03.
|
|
17
|
+
|
|
18
|
+
The primary use case is the **local install path for the AIWerk catalog** at
|
|
19
|
+
[aiwerkmcp.com](https://aiwerkmcp.com). Some recipes (those marked
|
|
20
|
+
`localOnly: true`, e.g. `chrome-devtools`) need a browser, display, USB
|
|
21
|
+
device or user-specific local path that the hosted bridge cannot reach;
|
|
22
|
+
this package is the recommended way to run them.
|
|
23
|
+
|
|
24
|
+
It also serves:
|
|
25
|
+
|
|
26
|
+
- **OpenClaw plugin users** via [`@aiwerk/openclaw-mcp-bridge`](https://github.com/AIWerk/openclaw-mcp-bridge),
|
|
27
|
+
which embeds this library and ships its own recipe-install tooling.
|
|
28
|
+
- **Self-hosted / offline deployments** where the hosted service is not an
|
|
29
|
+
option.
|
|
30
|
+
- **Library consumers** that import `McpRouter`, transports and the OAuth2
|
|
31
|
+
token manager directly.
|
|
32
|
+
|
|
33
|
+
Active development tracks the **Universal Recipe Spec v2** alongside the
|
|
34
|
+
hosted bridge — new fields like `localOnly`, `multiInstance`,
|
|
35
|
+
`auth.options[]` and `envBinding` are being ported, and the bundled
|
|
36
|
+
`servers/` directory is being kept in sync with the catalog. The install
|
|
37
|
+
helper does not fetch recipes over HTTP — recipes come from the bundled
|
|
38
|
+
`servers/` directory only.
|
|
39
|
+
|
|
13
40
|
## Why?
|
|
14
41
|
|
|
15
42
|
Most AI agents connect to MCP servers one-by-one. With 10+ servers, that's 10+ connections, 200+ tools in context, and thousands of wasted tokens.
|
|
@@ -27,7 +54,7 @@ Most AI agents connect to MCP servers one-by-one. With 10+ servers, that's 10+ c
|
|
|
27
54
|
- **Graceful shutdown**: clean process termination and connection cleanup
|
|
28
55
|
- **Direct mode**: all tools registered individually with automatic prefixing
|
|
29
56
|
- **3 transports**: stdio, SSE, streamable-http
|
|
30
|
-
- **
|
|
57
|
+
- **Bundled recipe examples**: 14+ reference server recipes in `servers/` — copy one as a starting point for your own config, or bring your own MCP server entirely
|
|
31
58
|
- **Zero config secrets in files**: `${ENV_VAR}` resolution from `.env`
|
|
32
59
|
|
|
33
60
|
## Install
|
|
@@ -565,6 +592,12 @@ NOTION_TOKEN=ntn_xxxxx
|
|
|
565
592
|
|
|
566
593
|
Use `${VAR_NAME}` in config — resolved from `.env` + system env.
|
|
567
594
|
|
|
595
|
+
You can also set env vars via CLI or at runtime:
|
|
596
|
+
```bash
|
|
597
|
+
mcp-bridge set-env TODOIST_API_TOKEN your-token-here
|
|
598
|
+
```
|
|
599
|
+
Or via the agent: `mcp(action="set-env", params={key: "TODOIST_API_TOKEN", value: "..."})`
|
|
600
|
+
|
|
568
601
|
## CLI Reference
|
|
569
602
|
|
|
570
603
|
```bash
|
|
@@ -581,8 +614,9 @@ mcp-bridge init --register codex # Init + register with Codex
|
|
|
581
614
|
mcp-bridge init --register cursor # Init + register with Cursor
|
|
582
615
|
mcp-bridge init --register windsurf # Init + register with Windsurf
|
|
583
616
|
mcp-bridge install <server> # Install from online catalog
|
|
617
|
+
mcp-bridge set-env <KEY> <value> # Set an API key in ~/.mcp-bridge/.env
|
|
584
618
|
mcp-bridge catalog # Browse 100+ available servers
|
|
585
|
-
mcp-bridge servers # List configured servers
|
|
619
|
+
mcp-bridge servers # List configured servers and current mode
|
|
586
620
|
mcp-bridge search <query> # Search catalog by keyword
|
|
587
621
|
mcp-bridge update [--check] # Check for / install updates
|
|
588
622
|
mcp-bridge --version # Print version
|
|
@@ -599,6 +633,7 @@ When connected to an MCP client (Claude Code, Codex, Cursor, etc.), the bridge e
|
|
|
599
633
|
```
|
|
600
634
|
mcp(action="search", params={query: "task management"}) # Search catalog
|
|
601
635
|
mcp(action="install", params={name: "todoist"}) # Install server (persisted to config)
|
|
636
|
+
mcp(action="set-env", params={key: "TODOIST_API_TOKEN", value: "your-key"}) # Set API key
|
|
602
637
|
mcp(action="catalog") # Browse all servers
|
|
603
638
|
mcp(action="list", server="todoist") # Discover tools on a server
|
|
604
639
|
mcp(action="call", server="todoist", tool="find-tasks", params={query: "today"})
|
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -4,14 +4,14 @@ import { join, dirname, resolve, extname } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { execFileSync, execSync } from "child_process";
|
|
7
|
-
import { loadConfig, initConfigDir,
|
|
7
|
+
import { loadConfig, initConfigDir, recipeToServerConfig, collectRequiredEnvVars } from "../src/config.js";
|
|
8
8
|
import { StandaloneServer } from "../src/standalone-server.js";
|
|
9
9
|
import { PACKAGE_VERSION } from "../src/protocol.js";
|
|
10
10
|
import { checkForUpdate, runUpdate } from "../src/update-checker.js";
|
|
11
11
|
import { FileTokenStore } from "../src/token-store.js";
|
|
12
12
|
import { performAuthCodeLogin, performDeviceCodeLogin } from "../src/cli-auth.js";
|
|
13
13
|
import { RateLimiter } from "../src/rate-limiter.js";
|
|
14
|
-
import { CatalogClient } from "../src/catalog-client.js";
|
|
14
|
+
import { CatalogClient, CatalogError, CatalogSignatureError } from "../src/catalog-client.js";
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = dirname(__filename);
|
|
17
17
|
// After tsc, this file lives at dist/bin/mcp-bridge.js.
|
|
@@ -136,6 +136,12 @@ function parseArgs(argv) {
|
|
|
136
136
|
case "install":
|
|
137
137
|
args.command = "install";
|
|
138
138
|
break;
|
|
139
|
+
case "search":
|
|
140
|
+
args.command = "search";
|
|
141
|
+
break;
|
|
142
|
+
case "catalog":
|
|
143
|
+
args.command = "catalog";
|
|
144
|
+
break;
|
|
139
145
|
case "remove":
|
|
140
146
|
case "uninstall":
|
|
141
147
|
args.command = "remove";
|
|
@@ -143,15 +149,9 @@ function parseArgs(argv) {
|
|
|
143
149
|
case "set-env":
|
|
144
150
|
args.command = "set-env";
|
|
145
151
|
break;
|
|
146
|
-
case "catalog":
|
|
147
|
-
args.command = "catalog";
|
|
148
|
-
break;
|
|
149
152
|
case "servers":
|
|
150
153
|
args.command = "servers";
|
|
151
154
|
break;
|
|
152
|
-
case "search":
|
|
153
|
-
args.command = "search";
|
|
154
|
-
break;
|
|
155
155
|
case "update":
|
|
156
156
|
args.command = "update";
|
|
157
157
|
break;
|
|
@@ -195,12 +195,12 @@ Usage:
|
|
|
195
195
|
mcp-bridge --sse --port 3000 Start as SSE server
|
|
196
196
|
mcp-bridge --http --port 3000 Start as streamable-http server
|
|
197
197
|
mcp-bridge init [--register <client>] [--mode router|direct] Create config + optionally register
|
|
198
|
-
mcp-bridge install <server> Install a server from the catalog
|
|
198
|
+
mcp-bridge install <server> Install a server from the AIWerk catalog (signature-verified)
|
|
199
|
+
mcp-bridge catalog [--offline] List available servers from the catalog
|
|
200
|
+
mcp-bridge search <query> Search the catalog by keyword
|
|
199
201
|
mcp-bridge remove <server> Remove a configured server
|
|
200
202
|
mcp-bridge set-env <KEY> <value> Set an API key in ~/.mcp-bridge/.env
|
|
201
|
-
mcp-bridge
|
|
202
|
-
mcp-bridge servers List configured servers
|
|
203
|
-
mcp-bridge search <query> Search catalog by keyword
|
|
203
|
+
mcp-bridge servers List configured servers and current mode
|
|
204
204
|
mcp-bridge usage Show current per-server call usage
|
|
205
205
|
mcp-bridge limit <server> [--daily N] [--monthly N]
|
|
206
206
|
Set per-server rate limits (0 = unlimited)
|
|
@@ -362,28 +362,6 @@ function registerClient(client, bridgeCmd, bridgeArgs, cmd) {
|
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
|
-
async function cmdCatalog(logger, offline) {
|
|
366
|
-
const client = new CatalogClient({ logger });
|
|
367
|
-
try {
|
|
368
|
-
const result = await client.list({ limit: 200 });
|
|
369
|
-
const recipes = result.results || [];
|
|
370
|
-
process.stdout.write(`\nAvailable servers (${recipes.length} from catalog.aiwerk.ch):\n\n`);
|
|
371
|
-
process.stdout.write(" Server Auth Category Description\n");
|
|
372
|
-
process.stdout.write(" " + "─".repeat(90) + "\n");
|
|
373
|
-
for (const r of recipes) {
|
|
374
|
-
const name = (r.name || "").padEnd(22);
|
|
375
|
-
const auth = (r.authSummary || "none").padEnd(12);
|
|
376
|
-
const cat = (r.category || "").padEnd(17);
|
|
377
|
-
const desc = (r.description || "").slice(0, 60);
|
|
378
|
-
process.stdout.write(` ${name}${auth}${cat}${desc}\n`);
|
|
379
|
-
}
|
|
380
|
-
process.stdout.write("\n");
|
|
381
|
-
}
|
|
382
|
-
catch (err) {
|
|
383
|
-
logger.error(`Failed to fetch catalog: ${err instanceof Error ? err.message : String(err)}`);
|
|
384
|
-
process.exit(1);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
365
|
function cmdServers(logger, configPath) {
|
|
388
366
|
try {
|
|
389
367
|
const config = loadConfig({ configPath, logger });
|
|
@@ -411,27 +389,6 @@ function cmdServers(logger, configPath) {
|
|
|
411
389
|
process.exit(1);
|
|
412
390
|
}
|
|
413
391
|
}
|
|
414
|
-
async function cmdSearch(query, logger) {
|
|
415
|
-
const client = new CatalogClient({ logger });
|
|
416
|
-
try {
|
|
417
|
-
const searchResponse = await client.search(query);
|
|
418
|
-
const results = Array.isArray(searchResponse) ? searchResponse : searchResponse.results || [];
|
|
419
|
-
if (results.length === 0) {
|
|
420
|
-
process.stdout.write(`No servers matching "${query}"\n`);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
process.stdout.write(`\nSearch results for "${query}" (${results.length} found):\n\n`);
|
|
424
|
-
for (const [i, r] of results.entries()) {
|
|
425
|
-
const auth = r.authSummary || (r.authRequired ? "required" : "none");
|
|
426
|
-
process.stdout.write(` ${i + 1} ${(r.name || "").padEnd(22)}[${auth}] ${r.description || ""}\n`);
|
|
427
|
-
}
|
|
428
|
-
process.stdout.write("\n");
|
|
429
|
-
}
|
|
430
|
-
catch (err) {
|
|
431
|
-
logger.error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
432
|
-
process.exit(1);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
392
|
function resolveConfigPath(configPath) {
|
|
436
393
|
if (!configPath) {
|
|
437
394
|
return join(homedir(), ".mcp-bridge", "config.json");
|
|
@@ -547,27 +504,22 @@ function cmdLimit(args, logger) {
|
|
|
547
504
|
async function cmdInstall(serverName, args, logger) {
|
|
548
505
|
const configPath = resolveConfigPath(args.configPath);
|
|
549
506
|
const configDir = dirname(configPath);
|
|
550
|
-
// Ensure config dir exists
|
|
551
507
|
if (!existsSync(configDir)) {
|
|
552
508
|
mkdirSync(configDir, { recursive: true });
|
|
553
509
|
}
|
|
554
|
-
// Ensure config file exists
|
|
555
510
|
if (!existsSync(configPath)) {
|
|
556
511
|
writeFileSync(configPath, JSON.stringify({ servers: {} }, null, 2) + "\n", "utf-8");
|
|
557
512
|
logger.info(`Created config: ${configPath}`);
|
|
558
513
|
}
|
|
559
|
-
// Read current config
|
|
560
514
|
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
561
515
|
if (!raw.servers)
|
|
562
516
|
raw.servers = {};
|
|
563
|
-
// Check if already configured
|
|
564
517
|
if (raw.servers[serverName]) {
|
|
565
518
|
process.stdout.write(`Server "${serverName}" is already configured.\n`);
|
|
566
519
|
process.stdout.write(`Config: ${configPath}\n`);
|
|
567
520
|
return;
|
|
568
521
|
}
|
|
569
|
-
|
|
570
|
-
process.stdout.write(`Fetching recipe for ${serverName}...\n`);
|
|
522
|
+
process.stdout.write(`Fetching recipe for ${serverName} from bridge.aiwerk.ch...\n`);
|
|
571
523
|
const cacheDir = join(configDir, "recipes");
|
|
572
524
|
const client = new CatalogClient({ cacheDir, logger });
|
|
573
525
|
let recipe;
|
|
@@ -575,74 +527,100 @@ async function cmdInstall(serverName, args, logger) {
|
|
|
575
527
|
recipe = await client.resolve(serverName);
|
|
576
528
|
}
|
|
577
529
|
catch (err) {
|
|
578
|
-
|
|
530
|
+
if (err instanceof CatalogSignatureError) {
|
|
531
|
+
logger.error(err.message);
|
|
532
|
+
process.stderr.write(`\nThe recipe was rejected because its Ed25519 signature did not match the AIWerk catalog public key. This means either the catalog was tampered with in transit, or your local cache was modified. Nothing has been written to your config.\n`);
|
|
533
|
+
}
|
|
534
|
+
else if (err instanceof CatalogError) {
|
|
535
|
+
logger.error(err.message);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
logger.error(`Failed to fetch recipe: ${err instanceof Error ? err.message : String(err)}`);
|
|
539
|
+
}
|
|
579
540
|
process.exit(1);
|
|
580
541
|
}
|
|
581
|
-
|
|
542
|
+
if (recipe.localOnly) {
|
|
543
|
+
process.stdout.write(`\nNote: "${serverName}" is marked localOnly — it needs your local environment (browser, display, USB, or user-specific paths) and cannot run on the hosted bridge. You are installing it locally, which is the right place.\n\n`);
|
|
544
|
+
}
|
|
582
545
|
const serverConfig = recipeToServerConfig(recipe);
|
|
583
546
|
if (!serverConfig) {
|
|
584
547
|
logger.error(`Unsupported recipe format for "${serverName}"`);
|
|
585
548
|
process.exit(1);
|
|
586
549
|
}
|
|
587
|
-
// Check required env vars
|
|
588
550
|
const requiredVars = collectRequiredEnvVars(recipe);
|
|
589
|
-
const missing = requiredVars.filter(v => !process.env[v]);
|
|
590
|
-
// Add to config
|
|
551
|
+
const missing = requiredVars.filter((v) => v !== "__AUTH_REQUIRED__" && !process.env[v]);
|
|
591
552
|
raw.servers[serverName] = serverConfig;
|
|
592
553
|
writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
593
|
-
process.stdout.write(
|
|
554
|
+
process.stdout.write(`✓ Added "${serverName}" to ${configPath}\n\n`);
|
|
594
555
|
if (missing.length > 0) {
|
|
595
|
-
process.stdout.write(
|
|
556
|
+
process.stdout.write(`Missing environment variables:\n`);
|
|
596
557
|
for (const v of missing) {
|
|
597
558
|
process.stdout.write(` ${v}\n`);
|
|
598
559
|
}
|
|
599
|
-
// Show credentials URL if available
|
|
600
560
|
const credUrl = recipe.auth?.credentialsUrl;
|
|
601
|
-
if (credUrl) {
|
|
561
|
+
if (typeof credUrl === "string") {
|
|
602
562
|
process.stdout.write(`\nGet credentials: ${credUrl}\n`);
|
|
603
563
|
}
|
|
604
564
|
process.stdout.write(`\nSet them in your environment or ~/.mcp-bridge/.env before starting the bridge.\n`);
|
|
605
565
|
}
|
|
606
|
-
else {
|
|
566
|
+
else if (requiredVars.length > 0) {
|
|
607
567
|
process.stdout.write(`All required environment variables are set. Ready to use.\n`);
|
|
608
568
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
if (transport) {
|
|
629
|
-
await transport.connect();
|
|
630
|
-
await initializeProtocol(transport, PACKAGE_VERSION);
|
|
631
|
-
const tools = await fetchToolsList(transport);
|
|
632
|
-
// Save to cache
|
|
633
|
-
const cacheToolDir = join(configDir, "cache");
|
|
634
|
-
mkdirSync(cacheToolDir, { recursive: true });
|
|
635
|
-
writeFileSync(join(cacheToolDir, `${serverName}-tools.json`), JSON.stringify(tools, null, 2), "utf-8");
|
|
636
|
-
process.stdout.write(`✓ Cached ${tools.length} tools from "${serverName}"\n`);
|
|
637
|
-
// Disconnect
|
|
638
|
-
await transport.disconnect().catch(() => { });
|
|
639
|
-
}
|
|
569
|
+
else {
|
|
570
|
+
process.stdout.write(`No credentials required. Ready to use.\n`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function cmdCatalog(args, logger) {
|
|
574
|
+
const configPath = resolveConfigPath(args.configPath);
|
|
575
|
+
const cacheDir = join(dirname(configPath), "recipes");
|
|
576
|
+
const client = new CatalogClient({ cacheDir, logger });
|
|
577
|
+
if (args.offline) {
|
|
578
|
+
const cached = client.listCached();
|
|
579
|
+
if (cached.length === 0) {
|
|
580
|
+
process.stdout.write(`No cached recipes. Run 'mcp-bridge install <server>' to fetch one.\n`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
process.stdout.write(`\nCached recipes (${cached.length}):\n\n`);
|
|
584
|
+
for (const name of cached.sort()) {
|
|
585
|
+
const r = client.getCached(name);
|
|
586
|
+
const desc = r?.description ?? "";
|
|
587
|
+
process.stdout.write(` ${name.padEnd(24)}${desc.slice(0, 60)}\n`);
|
|
640
588
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
const { results } = await client.list({ limit: 100 });
|
|
593
|
+
process.stdout.write(`\nAvailable servers (${results.length} from bridge.aiwerk.ch):\n\n`);
|
|
594
|
+
for (const r of results) {
|
|
595
|
+
const desc = (r.description ?? "").slice(0, 60);
|
|
596
|
+
process.stdout.write(` ${r.name.padEnd(24)}${desc}\n`);
|
|
645
597
|
}
|
|
598
|
+
process.stdout.write(`\nInstall: mcp-bridge install <name>\n`);
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
logger.error(`Failed to fetch catalog: ${err instanceof Error ? err.message : String(err)}`);
|
|
602
|
+
process.stderr.write(`Use 'mcp-bridge catalog --offline' to list cached recipes.\n`);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function cmdSearch(query, logger) {
|
|
607
|
+
const client = new CatalogClient({ logger });
|
|
608
|
+
try {
|
|
609
|
+
const results = await client.search(query);
|
|
610
|
+
if (results.length === 0) {
|
|
611
|
+
process.stdout.write(`No matches for "${query}".\n`);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
process.stdout.write(`\nMatches for "${query}" (${results.length}):\n\n`);
|
|
615
|
+
for (const r of results) {
|
|
616
|
+
const desc = (r.description ?? "").slice(0, 60);
|
|
617
|
+
process.stdout.write(` ${r.name.padEnd(24)}${desc}\n`);
|
|
618
|
+
}
|
|
619
|
+
process.stdout.write(`\nInstall: mcp-bridge install <name>\n`);
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
logger.error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
623
|
+
process.exit(1);
|
|
646
624
|
}
|
|
647
625
|
}
|
|
648
626
|
function cmdRemove(serverName, args, logger) {
|
|
@@ -812,23 +790,10 @@ async function cmdAuth(args, logger) {
|
|
|
812
790
|
process.stdout.write(` ${name.padEnd(maxName)}${label.padEnd(14)}${envStatus.slice(0, 27).padEnd(28)}${status}\n`);
|
|
813
791
|
// Show help for missing env vars
|
|
814
792
|
if (status === "missing env vars") {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const recipe = await helpClient.resolve(name);
|
|
819
|
-
const recipeAuth = recipe?.auth;
|
|
820
|
-
if (recipeAuth?.credentialsUrl) {
|
|
821
|
-
process.stdout.write(` ${" ".repeat(maxName)}Get credentials: ${recipeAuth.credentialsUrl}\n`);
|
|
822
|
-
}
|
|
823
|
-
if (recipeAuth?.instructions) {
|
|
824
|
-
process.stdout.write(` ${" ".repeat(maxName)}${recipeAuth.instructions}\n`);
|
|
825
|
-
}
|
|
826
|
-
const missingKeys = envKeys.filter(k => !envVars.has(k) && !process.env[k]);
|
|
827
|
-
if (missingKeys.length > 0) {
|
|
828
|
-
process.stdout.write(` ${" ".repeat(maxName)}Set with: mcp-bridge set-env ${missingKeys[0]} <your-key>\n`);
|
|
829
|
-
}
|
|
793
|
+
const missingKeys = envKeys.filter(k => !envVars.has(k) && !process.env[k]);
|
|
794
|
+
if (missingKeys.length > 0) {
|
|
795
|
+
process.stdout.write(` ${" ".repeat(maxName)}Set with: mcp-bridge set-env ${missingKeys[0]} <your-key>\n`);
|
|
830
796
|
}
|
|
831
|
-
catch { /* recipe not in cache, skip help */ }
|
|
832
797
|
}
|
|
833
798
|
shown.add(name);
|
|
834
799
|
}
|
|
@@ -928,8 +893,6 @@ async function cmdServe(args, logger) {
|
|
|
928
893
|
logger.error("HTTP auth not configured. Set http.auth in config or use stdio mode.");
|
|
929
894
|
process.exit(1);
|
|
930
895
|
}
|
|
931
|
-
// Warn about deprecated bundled recipes (v2.8.0+)
|
|
932
|
-
warnDeprecatedBundledRecipes(logger);
|
|
933
896
|
const server = new StandaloneServer(config, logger);
|
|
934
897
|
// Graceful shutdown
|
|
935
898
|
const shutdown = async () => {
|
|
@@ -961,19 +924,9 @@ async function main() {
|
|
|
961
924
|
case "init":
|
|
962
925
|
cmdInit(logger, args.register, args.mode);
|
|
963
926
|
break;
|
|
964
|
-
case "catalog":
|
|
965
|
-
await cmdCatalog(logger, args.offline);
|
|
966
|
-
break;
|
|
967
927
|
case "servers":
|
|
968
928
|
cmdServers(logger, args.configPath);
|
|
969
929
|
break;
|
|
970
|
-
case "search":
|
|
971
|
-
if (args.positional.length === 0) {
|
|
972
|
-
process.stderr.write("Usage: mcp-bridge search <query>\n");
|
|
973
|
-
process.exit(1);
|
|
974
|
-
}
|
|
975
|
-
await cmdSearch(args.positional[0], logger);
|
|
976
|
-
break;
|
|
977
930
|
case "usage":
|
|
978
931
|
cmdUsage(args.configPath, logger);
|
|
979
932
|
break;
|
|
@@ -987,6 +940,16 @@ async function main() {
|
|
|
987
940
|
}
|
|
988
941
|
await cmdInstall(args.positional[0], args, logger);
|
|
989
942
|
break;
|
|
943
|
+
case "catalog":
|
|
944
|
+
await cmdCatalog(args, logger);
|
|
945
|
+
break;
|
|
946
|
+
case "search":
|
|
947
|
+
if (args.positional.length === 0) {
|
|
948
|
+
process.stderr.write("Usage: mcp-bridge search <query>\n");
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
await cmdSearch(args.positional.join(" "), logger);
|
|
952
|
+
break;
|
|
990
953
|
case "remove":
|
|
991
954
|
if (args.positional.length === 0) {
|
|
992
955
|
process.stderr.write("Usage: mcp-bridge remove <server>\n");
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CatalogClient — REST client for the AIWerk MCP
|
|
2
|
+
* CatalogClient — REST client for the AIWerk MCP catalog at bridge.aiwerk.ch.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Restored 2026-05-03 after maintenance mode ended. Endpoint moved from the
|
|
5
|
+
* historical catalog.aiwerk.ch to bridge.aiwerk.ch/api/recipes/<name>/download.
|
|
6
|
+
* Every fetched recipe is Ed25519-verified against the bundled AIWerk public
|
|
7
|
+
* key before it is cached or returned. Unsigned or tampered recipes are
|
|
8
|
+
* refused.
|
|
6
9
|
*/
|
|
7
10
|
import type { Logger } from "./types.js";
|
|
8
11
|
export interface CatalogSearchResult {
|
|
@@ -12,6 +15,13 @@ export interface CatalogSearchResult {
|
|
|
12
15
|
quality?: number;
|
|
13
16
|
[key: string]: unknown;
|
|
14
17
|
}
|
|
18
|
+
export interface RecipeSignature {
|
|
19
|
+
algorithm: string;
|
|
20
|
+
publisherId: string;
|
|
21
|
+
value: string;
|
|
22
|
+
signedFields: string[];
|
|
23
|
+
signedAt: string;
|
|
24
|
+
}
|
|
15
25
|
export interface CatalogRecipe {
|
|
16
26
|
name: string;
|
|
17
27
|
description?: string;
|
|
@@ -30,6 +40,9 @@ export interface CatalogRecipe {
|
|
|
30
40
|
headers?: Record<string, string>;
|
|
31
41
|
}>;
|
|
32
42
|
install?: {
|
|
43
|
+
method?: string;
|
|
44
|
+
package?: string;
|
|
45
|
+
version?: string;
|
|
33
46
|
npm?: {
|
|
34
47
|
package: string;
|
|
35
48
|
version?: string;
|
|
@@ -44,34 +57,49 @@ export interface CatalogRecipe {
|
|
|
44
57
|
envVars?: string[];
|
|
45
58
|
credentialsUrl?: string;
|
|
46
59
|
};
|
|
60
|
+
signature?: RecipeSignature;
|
|
61
|
+
localOnly?: boolean;
|
|
47
62
|
[key: string]: unknown;
|
|
48
63
|
}
|
|
49
64
|
export declare class CatalogError extends Error {
|
|
50
65
|
constructor(message: string);
|
|
51
66
|
}
|
|
67
|
+
export declare class CatalogSignatureError extends CatalogError {
|
|
68
|
+
constructor(message: string);
|
|
69
|
+
}
|
|
52
70
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
* Verify the Ed25519 signature on a recipe against the bundled AIWerk public
|
|
72
|
+
* key. Throws CatalogSignatureError on any failure (missing signature, wrong
|
|
73
|
+
* algorithm, tampered payload, key mismatch).
|
|
74
|
+
*/
|
|
75
|
+
export declare function verifyRecipeSignature(recipe: CatalogRecipe): void;
|
|
76
|
+
export interface CatalogClientOptions {
|
|
77
|
+
baseUrl?: string;
|
|
78
|
+
cacheDir?: string;
|
|
79
|
+
logger?: Logger;
|
|
80
|
+
staleDays?: number;
|
|
81
|
+
/** Skip signature verification (testing only). */
|
|
82
|
+
skipSignatureVerify?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* REST client for the AIWerk MCP catalog. File I/O is intentionally
|
|
86
|
+
* synchronous — fine for CLI tools and bridge startup. Signature verification
|
|
87
|
+
* runs on every fetch (including cache reads) so a tampered cache cannot
|
|
88
|
+
* silently slip through.
|
|
58
89
|
*/
|
|
59
90
|
export declare class CatalogClient {
|
|
60
91
|
private baseUrl;
|
|
61
92
|
private cacheDir;
|
|
62
93
|
private logger;
|
|
63
94
|
private staleMs;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cacheDir?: string;
|
|
67
|
-
logger?: Logger;
|
|
68
|
-
staleDays?: number;
|
|
69
|
-
});
|
|
95
|
+
private skipSignatureVerify;
|
|
96
|
+
constructor(opts?: CatalogClientOptions);
|
|
70
97
|
private fetchJson;
|
|
71
98
|
private cachePath;
|
|
72
99
|
private readCache;
|
|
73
100
|
private writeCache;
|
|
74
101
|
private isCacheStale;
|
|
102
|
+
private verifyOrThrow;
|
|
75
103
|
/** Search for recipes by keyword. */
|
|
76
104
|
search(query: string): Promise<CatalogSearchResult[]>;
|
|
77
105
|
/** List recipes with optional filtering. */
|
|
@@ -79,23 +107,23 @@ export declare class CatalogClient {
|
|
|
79
107
|
limit?: number;
|
|
80
108
|
category?: string;
|
|
81
109
|
sort?: string;
|
|
82
|
-
hostedSafe?: boolean;
|
|
83
110
|
}): Promise<{
|
|
84
111
|
results: CatalogSearchResult[];
|
|
85
112
|
total: number;
|
|
86
113
|
}>;
|
|
87
|
-
/** Download a recipe from the catalog and cache it locally. */
|
|
88
|
-
download(name: string): Promise<CatalogRecipe>;
|
|
89
114
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
115
|
+
* Download a recipe from the catalog, verify its signature, and cache it
|
|
116
|
+
* locally. Throws CatalogSignatureError if the recipe is unsigned or
|
|
117
|
+
* tampered — nothing is written to the cache in that case.
|
|
92
118
|
*/
|
|
93
|
-
|
|
119
|
+
download(name: string): Promise<CatalogRecipe>;
|
|
94
120
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
121
|
+
* Resolve a recipe by name. Returns the cached copy if fresh; otherwise
|
|
122
|
+
* fetches from the catalog. Falls back to a stale cache if the network is
|
|
123
|
+
* unreachable. Signature is verified on both fresh and cached paths so a
|
|
124
|
+
* tampered cache cannot slip through.
|
|
97
125
|
*/
|
|
98
|
-
|
|
126
|
+
resolve(name: string): Promise<CatalogRecipe>;
|
|
99
127
|
/** Synchronously read a recipe from local cache. Returns null if not cached. */
|
|
100
128
|
getCached(name: string): CatalogRecipe | null;
|
|
101
129
|
/** List all recipe names in the local cache. */
|