@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
|
@@ -1,45 +1,121 @@
|
|
|
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 { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
8
11
|
import { join } from "node:path";
|
|
9
12
|
import { homedir } from "node:os";
|
|
10
|
-
|
|
13
|
+
import { createPublicKey, verify } from "node:crypto";
|
|
14
|
+
// ── Public key (Ed25519, AIWerk catalog signer) ──────────────────────────────
|
|
15
|
+
//
|
|
16
|
+
// The hosted bridge signs every recipe with the matching private key kept in
|
|
17
|
+
// pass under aiwerk/mcp-catalog-private-key. The public key is baked into
|
|
18
|
+
// the standalone bundle so a fresh install does not need to fetch it.
|
|
19
|
+
const AIWERK_CATALOG_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
20
|
+
MCowBQYDK2VwAyEAkHESasC8Mbf2+pGe+bhKRQkgOBSPcqGj0ZWGop4TS6k=
|
|
21
|
+
-----END PUBLIC KEY-----
|
|
22
|
+
`;
|
|
23
|
+
const SIGNED_FIELDS = [
|
|
24
|
+
"id",
|
|
25
|
+
"name",
|
|
26
|
+
"description",
|
|
27
|
+
"transports",
|
|
28
|
+
"auth",
|
|
29
|
+
"install",
|
|
30
|
+
"metadata",
|
|
31
|
+
"skill",
|
|
32
|
+
"localOnly",
|
|
33
|
+
];
|
|
34
|
+
// ── Errors ───────────────────────────────────────────────────────────────────
|
|
11
35
|
export class CatalogError extends Error {
|
|
12
36
|
constructor(message) {
|
|
13
37
|
super(message);
|
|
14
38
|
this.name = "CatalogError";
|
|
15
39
|
}
|
|
16
40
|
}
|
|
41
|
+
export class CatalogSignatureError extends CatalogError {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "CatalogSignatureError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
17
47
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
48
|
const TIMEOUT_MS = 5_000;
|
|
49
|
+
const DEFAULT_BASE_URL = "https://bridge.aiwerk.ch";
|
|
19
50
|
const noop = {
|
|
20
51
|
info: () => { },
|
|
21
52
|
warn: () => { },
|
|
22
53
|
error: () => { },
|
|
23
54
|
debug: () => { },
|
|
24
55
|
};
|
|
25
|
-
|
|
56
|
+
function stableStringify(value) {
|
|
57
|
+
if (value === null || value === undefined)
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
if (typeof value !== "object")
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
63
|
+
}
|
|
64
|
+
const sorted = Object.keys(value).sort();
|
|
65
|
+
const entries = sorted.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]));
|
|
66
|
+
return "{" + entries.join(",") + "}";
|
|
67
|
+
}
|
|
68
|
+
function canonicalSignedPayload(recipe) {
|
|
69
|
+
const subset = {};
|
|
70
|
+
for (const field of SIGNED_FIELDS) {
|
|
71
|
+
if (field in recipe) {
|
|
72
|
+
subset[field] = recipe[field];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return stableStringify(subset);
|
|
76
|
+
}
|
|
26
77
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
78
|
+
* Verify the Ed25519 signature on a recipe against the bundled AIWerk public
|
|
79
|
+
* key. Throws CatalogSignatureError on any failure (missing signature, wrong
|
|
80
|
+
* algorithm, tampered payload, key mismatch).
|
|
81
|
+
*/
|
|
82
|
+
export function verifyRecipeSignature(recipe) {
|
|
83
|
+
const sig = recipe.signature;
|
|
84
|
+
if (!sig) {
|
|
85
|
+
throw new CatalogSignatureError("recipe has no signature");
|
|
86
|
+
}
|
|
87
|
+
if (sig.algorithm !== "ed25519") {
|
|
88
|
+
throw new CatalogSignatureError(`unsupported signature algorithm: ${sig.algorithm}`);
|
|
89
|
+
}
|
|
90
|
+
if (typeof sig.value !== "string" || sig.value.length === 0) {
|
|
91
|
+
throw new CatalogSignatureError("recipe signature value is empty");
|
|
92
|
+
}
|
|
93
|
+
const publicKey = createPublicKey(AIWERK_CATALOG_PUBLIC_KEY_PEM);
|
|
94
|
+
const payload = Buffer.from(canonicalSignedPayload(recipe), "utf-8");
|
|
95
|
+
const signatureBytes = Buffer.from(sig.value, "base64");
|
|
96
|
+
const ok = verify(null, payload, publicKey, signatureBytes);
|
|
97
|
+
if (!ok) {
|
|
98
|
+
throw new CatalogSignatureError("recipe signature does not match");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* REST client for the AIWerk MCP catalog. File I/O is intentionally
|
|
103
|
+
* synchronous — fine for CLI tools and bridge startup. Signature verification
|
|
104
|
+
* runs on every fetch (including cache reads) so a tampered cache cannot
|
|
105
|
+
* silently slip through.
|
|
32
106
|
*/
|
|
33
107
|
export class CatalogClient {
|
|
34
108
|
baseUrl;
|
|
35
109
|
cacheDir;
|
|
36
110
|
logger;
|
|
37
111
|
staleMs;
|
|
112
|
+
skipSignatureVerify;
|
|
38
113
|
constructor(opts) {
|
|
39
|
-
this.baseUrl = (opts?.baseUrl ??
|
|
114
|
+
this.baseUrl = (opts?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
40
115
|
this.cacheDir = opts?.cacheDir ?? join(homedir(), ".mcp-bridge", "recipes");
|
|
41
116
|
this.logger = opts?.logger ?? noop;
|
|
42
117
|
this.staleMs = (opts?.staleDays ?? 7) * 24 * 60 * 60 * 1000;
|
|
118
|
+
this.skipSignatureVerify = opts?.skipSignatureVerify ?? false;
|
|
43
119
|
}
|
|
44
120
|
// ── Private helpers ──────────────────────────────────────────────────────
|
|
45
121
|
async fetchJson(path) {
|
|
@@ -95,11 +171,23 @@ export class CatalogClient {
|
|
|
95
171
|
return true;
|
|
96
172
|
}
|
|
97
173
|
}
|
|
174
|
+
verifyOrThrow(recipe, name) {
|
|
175
|
+
if (this.skipSignatureVerify)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
verifyRecipeSignature(recipe);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
182
|
+
throw new CatalogSignatureError(`Recipe "${name}" failed signature verification: ${reason}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
98
185
|
// ── Public API ───────────────────────────────────────────────────────────
|
|
99
186
|
/** Search for recipes by keyword. */
|
|
100
187
|
async search(query) {
|
|
101
188
|
const encoded = encodeURIComponent(query);
|
|
102
|
-
|
|
189
|
+
const result = await this.fetchJson(`/api/search?q=${encoded}`);
|
|
190
|
+
return Array.isArray(result) ? result : (result.results ?? []);
|
|
103
191
|
}
|
|
104
192
|
/** List recipes with optional filtering. */
|
|
105
193
|
async list(opts) {
|
|
@@ -110,75 +198,54 @@ export class CatalogClient {
|
|
|
110
198
|
params.set("category", opts.category);
|
|
111
199
|
if (opts?.sort)
|
|
112
200
|
params.set("sort", opts.sort);
|
|
113
|
-
if (opts?.hostedSafe)
|
|
114
|
-
params.set("hostedSafe", "true");
|
|
115
201
|
const qs = params.toString();
|
|
116
202
|
return this.fetchJson(`/api/recipes${qs ? `?${qs}` : ""}`);
|
|
117
203
|
}
|
|
118
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* Download a recipe from the catalog, verify its signature, and cache it
|
|
206
|
+
* locally. Throws CatalogSignatureError if the recipe is unsigned or
|
|
207
|
+
* tampered — nothing is written to the cache in that case.
|
|
208
|
+
*/
|
|
119
209
|
async download(name) {
|
|
120
210
|
const recipe = await this.fetchJson(`/api/recipes/${encodeURIComponent(name)}/download`);
|
|
211
|
+
this.verifyOrThrow(recipe, name);
|
|
121
212
|
this.writeCache(name, recipe);
|
|
122
213
|
return recipe;
|
|
123
214
|
}
|
|
124
215
|
/**
|
|
125
|
-
* Resolve a recipe
|
|
126
|
-
* Falls back to cache
|
|
216
|
+
* Resolve a recipe by name. Returns the cached copy if fresh; otherwise
|
|
217
|
+
* fetches from the catalog. Falls back to a stale cache if the network is
|
|
218
|
+
* unreachable. Signature is verified on both fresh and cached paths so a
|
|
219
|
+
* tampered cache cannot slip through.
|
|
127
220
|
*/
|
|
128
221
|
async resolve(name) {
|
|
129
222
|
const cached = this.readCache(name);
|
|
130
|
-
if (cached && !this.isCacheStale(name))
|
|
223
|
+
if (cached && !this.isCacheStale(name)) {
|
|
224
|
+
this.verifyOrThrow(cached, name);
|
|
131
225
|
return cached;
|
|
226
|
+
}
|
|
132
227
|
try {
|
|
133
228
|
const recipe = await this.fetchJson(`/api/recipes/${encodeURIComponent(name)}/download`);
|
|
229
|
+
this.verifyOrThrow(recipe, name);
|
|
134
230
|
this.writeCache(name, recipe);
|
|
135
231
|
return recipe;
|
|
136
232
|
}
|
|
137
233
|
catch (err) {
|
|
234
|
+
if (err instanceof CatalogSignatureError)
|
|
235
|
+
throw err;
|
|
138
236
|
if (err instanceof CatalogError && err.message.startsWith("Recipe not found:")) {
|
|
139
237
|
throw err;
|
|
140
238
|
}
|
|
141
239
|
if (cached) {
|
|
240
|
+
// Stale cache fallback: still verify the signature so an offline user
|
|
241
|
+
// does not run a tampered local copy.
|
|
242
|
+
this.verifyOrThrow(cached, name);
|
|
142
243
|
this.logger.warn(`Catalog unreachable for "${name}", using cached version`);
|
|
143
244
|
return cached;
|
|
144
245
|
}
|
|
145
246
|
throw new CatalogError(`Cannot resolve recipe "${name}": catalog unreachable and no local cache`);
|
|
146
247
|
}
|
|
147
248
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Bootstrap by downloading the top N most popular recipes.
|
|
150
|
-
* Skips already-cached recipes unless they are stale.
|
|
151
|
-
*/
|
|
152
|
-
async bootstrap(limit = 15, hostedSafe = false) {
|
|
153
|
-
const { results } = await this.list({ limit, sort: "popular", hostedSafe });
|
|
154
|
-
const names = [];
|
|
155
|
-
const toDownload = [];
|
|
156
|
-
for (const entry of results) {
|
|
157
|
-
const name = entry.name;
|
|
158
|
-
if (!this.isCacheStale(name)) {
|
|
159
|
-
names.push(name);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
toDownload.push(name);
|
|
163
|
-
}
|
|
164
|
-
const BATCH_SIZE = 5;
|
|
165
|
-
for (let i = 0; i < toDownload.length; i += BATCH_SIZE) {
|
|
166
|
-
const batch = toDownload.slice(i, i + BATCH_SIZE);
|
|
167
|
-
const results = await Promise.allSettled(batch.map(async (name) => {
|
|
168
|
-
await this.download(name);
|
|
169
|
-
return name;
|
|
170
|
-
}));
|
|
171
|
-
for (const r of results) {
|
|
172
|
-
if (r.status === "fulfilled") {
|
|
173
|
-
names.push(r.value);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
this.logger.warn(`Failed to download recipe: ${r.reason}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return names;
|
|
181
|
-
}
|
|
182
249
|
/** Synchronously read a recipe from local cache. Returns null if not cached. */
|
|
183
250
|
getCached(name) {
|
|
184
251
|
return this.readCache(name);
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BridgeConfig, Logger, McpServerConfig } from "./types.js";
|
|
2
|
-
import type { CatalogRecipe } from "./catalog-client.js";
|
|
3
2
|
/**
|
|
4
3
|
* Load ~/.openclaw/.env as a fallback env source.
|
|
5
4
|
*
|
|
@@ -25,43 +24,11 @@ export interface LoadConfigOptions {
|
|
|
25
24
|
* 4. Validate required fields
|
|
26
25
|
*/
|
|
27
26
|
export declare function loadConfig(options?: LoadConfigOptions): BridgeConfig;
|
|
28
|
-
/**
|
|
29
|
-
* Warn about deprecated bundled recipes.
|
|
30
|
-
* In v2.8.0, bundled servers/ recipes are deprecated in favor of catalog.
|
|
31
|
-
* They will be removed in v3.0.0.
|
|
32
|
-
*/
|
|
33
|
-
export declare function warnDeprecatedBundledRecipes(logger: Logger): void;
|
|
34
27
|
/** Get the default config directory path. */
|
|
35
28
|
export declare function getConfigDir(configPath?: string): string;
|
|
36
29
|
/** Initialize the config directory with template files. */
|
|
37
30
|
export declare function initConfigDir(logger: Logger): void;
|
|
38
|
-
|
|
39
|
-
* Bootstrap the local recipe cache from the catalog.
|
|
40
|
-
* Downloads top N popular recipes if cache is empty or force=true.
|
|
41
|
-
* Returns array of recipe names now cached. Never throws on network errors.
|
|
42
|
-
*/
|
|
43
|
-
export declare function bootstrapCatalog(options?: {
|
|
44
|
-
logger?: Logger;
|
|
45
|
-
cacheDir?: string;
|
|
46
|
-
catalogUrl?: string;
|
|
47
|
-
limit?: number;
|
|
48
|
-
force?: boolean;
|
|
49
|
-
requireCleanAudit?: boolean;
|
|
50
|
-
catalog?: boolean;
|
|
51
|
-
}): Promise<string[]>;
|
|
52
|
-
/**
|
|
53
|
-
* Merge cached catalog recipes into a BridgeConfig.
|
|
54
|
-
* Only adds recipes whose required env vars are all present in process.env.
|
|
55
|
-
* Never overwrites manually configured servers.
|
|
56
|
-
*
|
|
57
|
-
* IMPORTANT: Must be called AFTER loadConfig() / dotenv, since env var
|
|
58
|
-
* checks rely on process.env being fully populated.
|
|
59
|
-
*/
|
|
60
|
-
export declare function mergeRecipesIntoConfig(config: BridgeConfig, options?: {
|
|
61
|
-
cacheDir?: string;
|
|
62
|
-
logger?: Logger;
|
|
63
|
-
}): BridgeConfig;
|
|
64
|
-
/** Convert a catalog recipe JSON to McpServerConfig, or null if unsupported. */
|
|
31
|
+
import type { CatalogRecipe } from "./catalog-client.js";
|
|
65
32
|
export declare function recipeToServerConfig(recipe: CatalogRecipe): McpServerConfig | null;
|
|
66
|
-
/** Collect all env var names required by a recipe. */
|
|
33
|
+
/** Collect all env var names required by a recipe (auth.envVars + ${VAR} refs). */
|
|
67
34
|
export declare function collectRequiredEnvVars(recipe: CatalogRecipe): string[];
|
package/dist/src/config.js
CHANGED
|
@@ -3,7 +3,6 @@ import { join, extname } from "path";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { resolveEnvVars } from "./transport-base.js";
|
|
5
5
|
import { randomBytes } from "crypto";
|
|
6
|
-
import { CatalogClient } from "./catalog-client.js";
|
|
7
6
|
const DEFAULT_CONFIG_DIR = join(homedir(), ".mcp-bridge");
|
|
8
7
|
const DEFAULT_CONFIG_FILE = "config.json";
|
|
9
8
|
const DEFAULT_ENV_FILE = ".env";
|
|
@@ -154,18 +153,6 @@ export function loadConfig(options = {}) {
|
|
|
154
153
|
}
|
|
155
154
|
return config;
|
|
156
155
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Warn about deprecated bundled recipes.
|
|
159
|
-
* In v2.8.0, bundled servers/ recipes are deprecated in favor of catalog.
|
|
160
|
-
* They will be removed in v3.0.0.
|
|
161
|
-
*/
|
|
162
|
-
export function warnDeprecatedBundledRecipes(logger) {
|
|
163
|
-
const catalogClient = new CatalogClient({ logger });
|
|
164
|
-
const cached = catalogClient.listCached();
|
|
165
|
-
if (cached.length === 0) {
|
|
166
|
-
logger.info('[mcp-bridge] Tip: Run bootstrapCatalog() to fetch recipes from catalog.aiwerk.ch (replaces bundled servers/)');
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
156
|
/** Get the default config directory path. */
|
|
170
157
|
export function getConfigDir(configPath) {
|
|
171
158
|
if (!configPath)
|
|
@@ -218,120 +205,7 @@ export function initConfigDir(logger) {
|
|
|
218
205
|
}
|
|
219
206
|
logger.info(`Config directory ready: ${dir}`);
|
|
220
207
|
}
|
|
221
|
-
// ── Catalog bootstrap ─────────────────────────────────────────────────────────
|
|
222
|
-
const noopLogger = {
|
|
223
|
-
info: () => { },
|
|
224
|
-
warn: () => { },
|
|
225
|
-
error: () => { },
|
|
226
|
-
debug: () => { },
|
|
227
|
-
};
|
|
228
|
-
/**
|
|
229
|
-
* Extract the depAudit value from a catalog recipe's metadata.verification field.
|
|
230
|
-
* Returns null if not present.
|
|
231
|
-
*/
|
|
232
|
-
function getDepAudit(recipe) {
|
|
233
|
-
const meta = recipe.metadata;
|
|
234
|
-
const verification = meta?.verification;
|
|
235
|
-
const depAudit = verification?.depAudit;
|
|
236
|
-
return typeof depAudit === "string" ? depAudit : null;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Bootstrap the local recipe cache from the catalog.
|
|
240
|
-
* Downloads top N popular recipes if cache is empty or force=true.
|
|
241
|
-
* Returns array of recipe names now cached. Never throws on network errors.
|
|
242
|
-
*/
|
|
243
|
-
export async function bootstrapCatalog(options) {
|
|
244
|
-
// If catalog is explicitly disabled, skip fetching
|
|
245
|
-
if (options?.catalog === false) {
|
|
246
|
-
const logger = options?.logger ?? noopLogger;
|
|
247
|
-
logger.info("[mcp-bridge] Catalog discovery disabled (catalog: false), skipping bootstrap");
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
250
|
-
const logger = options?.logger ?? noopLogger;
|
|
251
|
-
const cacheDir = options?.cacheDir ?? join(homedir(), ".mcp-bridge", "recipes");
|
|
252
|
-
const requireCleanAudit = options?.requireCleanAudit ?? false;
|
|
253
|
-
const client = new CatalogClient({
|
|
254
|
-
baseUrl: options?.catalogUrl,
|
|
255
|
-
cacheDir,
|
|
256
|
-
logger,
|
|
257
|
-
});
|
|
258
|
-
// Check if cache already has recipes
|
|
259
|
-
if (!options?.force) {
|
|
260
|
-
const cached = client.listCached();
|
|
261
|
-
if (cached.length > 0) {
|
|
262
|
-
logger.debug(`Recipe cache already has ${cached.length} recipes, skipping bootstrap`);
|
|
263
|
-
return cached;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
try {
|
|
267
|
-
return await client.bootstrap(options?.limit ?? 15, requireCleanAudit);
|
|
268
|
-
}
|
|
269
|
-
catch (err) {
|
|
270
|
-
logger.warn(`Catalog unreachable during bootstrap: ${err instanceof Error ? err.message : err}`);
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Merge cached catalog recipes into a BridgeConfig.
|
|
276
|
-
* Only adds recipes whose required env vars are all present in process.env.
|
|
277
|
-
* Never overwrites manually configured servers.
|
|
278
|
-
*
|
|
279
|
-
* IMPORTANT: Must be called AFTER loadConfig() / dotenv, since env var
|
|
280
|
-
* checks rely on process.env being fully populated.
|
|
281
|
-
*/
|
|
282
|
-
export function mergeRecipesIntoConfig(config, options) {
|
|
283
|
-
const logger = options?.logger ?? noopLogger;
|
|
284
|
-
// autoMerge defaults to false (opt-in) — only merge when explicitly enabled
|
|
285
|
-
if (config.autoMerge !== true) {
|
|
286
|
-
logger.debug("[mcp-bridge] Auto-merge disabled (autoMerge is not true), skipping recipe merge");
|
|
287
|
-
return config;
|
|
288
|
-
}
|
|
289
|
-
const cacheDir = options?.cacheDir ?? join(homedir(), ".mcp-bridge", "recipes");
|
|
290
|
-
const client = new CatalogClient({ cacheDir, logger });
|
|
291
|
-
const names = client.listCached();
|
|
292
|
-
if (names.length === 0)
|
|
293
|
-
return config;
|
|
294
|
-
const servers = { ...config.servers };
|
|
295
|
-
const requireCleanAudit = config.security?.requireCleanAudit ?? false;
|
|
296
|
-
for (const name of names) {
|
|
297
|
-
// Never overwrite manually configured servers
|
|
298
|
-
if (servers[name])
|
|
299
|
-
continue;
|
|
300
|
-
const recipe = client.getCached(name);
|
|
301
|
-
if (!recipe)
|
|
302
|
-
continue;
|
|
303
|
-
// Check depAudit security policy
|
|
304
|
-
const depAudit = getDepAudit(recipe);
|
|
305
|
-
const auditOk = depAudit === null || depAudit === "clean" || depAudit === "not-applicable";
|
|
306
|
-
if (!auditOk) {
|
|
307
|
-
if (requireCleanAudit) {
|
|
308
|
-
logger.warn(`⚠️ Skipping server "${name}": has known security advisories (depAudit: ${depAudit}). Set security.requireCleanAudit=false to allow.`);
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
logger.info(`ℹ️ Server "${name}" has known advisories (depAudit: ${depAudit}). Set security.requireCleanAudit=true to block.`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const converted = recipeToServerConfig(recipe);
|
|
316
|
-
if (!converted) {
|
|
317
|
-
logger.debug(`Skipping recipe "${name}": unsupported format`);
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
// Check that all required env vars are available
|
|
321
|
-
const requiredVars = collectRequiredEnvVars(recipe);
|
|
322
|
-
const missing = requiredVars.filter((v) => !process.env[v]);
|
|
323
|
-
if (missing.length > 0) {
|
|
324
|
-
logger.debug(`Skipping recipe "${name}": missing env vars: ${missing.join(", ")}`);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
servers[name] = converted;
|
|
328
|
-
logger.debug(`Added catalog recipe "${name}" to config`);
|
|
329
|
-
}
|
|
330
|
-
return { ...config, servers };
|
|
331
|
-
}
|
|
332
|
-
/** Convert a catalog recipe JSON to McpServerConfig, or null if unsupported. */
|
|
333
208
|
export function recipeToServerConfig(recipe) {
|
|
334
|
-
// v2 recipe: has transports array
|
|
335
209
|
if (Array.isArray(recipe.transports) && recipe.transports.length > 0) {
|
|
336
210
|
const t = recipe.transports[0];
|
|
337
211
|
if (t.type === "stdio") {
|
|
@@ -353,7 +227,6 @@ export function recipeToServerConfig(recipe) {
|
|
|
353
227
|
}
|
|
354
228
|
return null;
|
|
355
229
|
}
|
|
356
|
-
// v1 recipe: has transport string
|
|
357
230
|
if (recipe.transport === "stdio") {
|
|
358
231
|
return {
|
|
359
232
|
transport: "stdio",
|
|
@@ -373,15 +246,13 @@ export function recipeToServerConfig(recipe) {
|
|
|
373
246
|
}
|
|
374
247
|
return null;
|
|
375
248
|
}
|
|
376
|
-
/** Collect all env var names required by a recipe. */
|
|
249
|
+
/** Collect all env var names required by a recipe (auth.envVars + ${VAR} refs). */
|
|
377
250
|
export function collectRequiredEnvVars(recipe) {
|
|
378
251
|
const vars = new Set();
|
|
379
|
-
// From auth.envVars
|
|
380
252
|
if (Array.isArray(recipe.auth?.envVars)) {
|
|
381
253
|
for (const v of recipe.auth.envVars)
|
|
382
254
|
vars.add(v);
|
|
383
255
|
}
|
|
384
|
-
// From env object: extract ${VAR} references
|
|
385
256
|
const envObj = Array.isArray(recipe.transports)
|
|
386
257
|
? recipe.transports[0]?.env
|
|
387
258
|
: recipe.env;
|
|
@@ -394,10 +265,8 @@ export function collectRequiredEnvVars(recipe) {
|
|
|
394
265
|
}
|
|
395
266
|
}
|
|
396
267
|
}
|
|
397
|
-
// If auth is explicitly required but no env vars were found,
|
|
398
|
-
// return a placeholder to prevent auto-registration without credentials
|
|
399
268
|
if (recipe.auth?.required === true && vars.size === 0) {
|
|
400
269
|
vars.add("__AUTH_REQUIRED__");
|
|
401
270
|
}
|
|
402
|
-
return
|
|
271
|
+
return Array.from(vars);
|
|
403
272
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { ToolResolver } from "./tool-resolution.js";
|
|
|
18
18
|
export type { ToolResolutionResult, ToolResolutionCandidate } from "./tool-resolution.js";
|
|
19
19
|
export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
|
|
20
20
|
export { initializeProtocol, fetchToolsList, PACKAGE_VERSION } from "./protocol.js";
|
|
21
|
-
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir
|
|
21
|
+
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir } from "./config.js";
|
|
22
22
|
export type { Logger, McpServerConfig, McpClientConfig, HttpAuthConfig, RetryConfig, McpTool, McpRequest, McpCallRequest, McpResponse, JsonRpcMessage, McpTransport, McpServerConnection, BridgeConfig, RequestIdState, RequestIdGenerator, } from "./types.js";
|
|
23
23
|
export { nextRequestId } from "./types.js";
|
|
24
24
|
export { pickRegisteredToolName } from "./tool-naming.js";
|
|
@@ -26,7 +26,3 @@ export { StandaloneServer } from "./standalone-server.js";
|
|
|
26
26
|
export { checkForUpdate, checkPluginUpdate, getUpdateNotice, runUpdate, resetNoticeFlag } from "./update-checker.js";
|
|
27
27
|
export type { UpdateInfo, PluginUpdateInfo } from "./update-checker.js";
|
|
28
28
|
export { filterServers, buildFilteredDescription } from "./smart-filter.js";
|
|
29
|
-
export { CatalogClient, CatalogError } from "./catalog-client.js";
|
|
30
|
-
export type { CatalogRecipe, CatalogSearchResult } from "./catalog-client.js";
|
|
31
|
-
export { RecipeCache } from "./recipe-cache.js";
|
|
32
|
-
export type { CachedRecipe } from "./recipe-cache.js";
|
package/dist/src/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, set
|
|
|
20
20
|
// Protocol helpers
|
|
21
21
|
export { initializeProtocol, fetchToolsList, PACKAGE_VERSION } from "./protocol.js";
|
|
22
22
|
// Config
|
|
23
|
-
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir
|
|
23
|
+
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir } from "./config.js";
|
|
24
24
|
export { nextRequestId } from "./types.js";
|
|
25
25
|
// Tool naming
|
|
26
26
|
export { pickRegisteredToolName } from "./tool-naming.js";
|
|
@@ -30,7 +30,3 @@ export { StandaloneServer } from "./standalone-server.js";
|
|
|
30
30
|
export { checkForUpdate, checkPluginUpdate, getUpdateNotice, runUpdate, resetNoticeFlag } from "./update-checker.js";
|
|
31
31
|
// Smart filter
|
|
32
32
|
export { filterServers, buildFilteredDescription } from "./smart-filter.js";
|
|
33
|
-
// Catalog client
|
|
34
|
-
export { CatalogClient, CatalogError } from "./catalog-client.js";
|
|
35
|
-
// Recipe cache
|
|
36
|
-
export { RecipeCache } from "./recipe-cache.js";
|
package/dist/src/mcp-router.d.ts
CHANGED
|
@@ -75,42 +75,6 @@ export type RouterDispatchResponse = {
|
|
|
75
75
|
callCount: number;
|
|
76
76
|
lastCall: string;
|
|
77
77
|
}>;
|
|
78
|
-
} | {
|
|
79
|
-
action: "search";
|
|
80
|
-
query: string;
|
|
81
|
-
results: Array<{
|
|
82
|
-
id: string;
|
|
83
|
-
name: string;
|
|
84
|
-
description: string;
|
|
85
|
-
category?: string;
|
|
86
|
-
auth?: string;
|
|
87
|
-
origin?: string;
|
|
88
|
-
maturity?: string;
|
|
89
|
-
sideEffects?: string;
|
|
90
|
-
pricing?: string;
|
|
91
|
-
signed?: boolean;
|
|
92
|
-
}>;
|
|
93
|
-
} | {
|
|
94
|
-
action: "catalog";
|
|
95
|
-
recipes: Array<{
|
|
96
|
-
id: string;
|
|
97
|
-
name: string;
|
|
98
|
-
description: string;
|
|
99
|
-
category?: string;
|
|
100
|
-
auth?: string;
|
|
101
|
-
origin?: string;
|
|
102
|
-
maturity?: string;
|
|
103
|
-
sideEffects?: string;
|
|
104
|
-
pricing?: string;
|
|
105
|
-
signed?: boolean;
|
|
106
|
-
}>;
|
|
107
|
-
} | {
|
|
108
|
-
action: "install";
|
|
109
|
-
server: string;
|
|
110
|
-
installed: boolean;
|
|
111
|
-
message: string;
|
|
112
|
-
missingEnvVars?: string[];
|
|
113
|
-
credentialsUrl?: string;
|
|
114
78
|
} | {
|
|
115
79
|
action: "remove";
|
|
116
80
|
server: string;
|
|
@@ -173,7 +137,6 @@ export declare class McpRouter {
|
|
|
173
137
|
private readonly tokenManager;
|
|
174
138
|
private readonly rateLimiter;
|
|
175
139
|
private readonly requestIdState;
|
|
176
|
-
private readonly catalogClient;
|
|
177
140
|
private intentRouter;
|
|
178
141
|
private promotion;
|
|
179
142
|
constructor(servers: Record<string, McpServerConfig>, clientConfig: McpClientConfig, logger: Logger, transportRefs?: Partial<RouterTransportRefs>);
|