@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/dist/src/mcp-router.js
CHANGED
|
@@ -13,8 +13,6 @@ import { ToolResolver } from "./tool-resolution.js";
|
|
|
13
13
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
14
14
|
import { FileTokenStore } from "./token-store.js";
|
|
15
15
|
import { RateLimiter } from "./rate-limiter.js";
|
|
16
|
-
import { CatalogClient } from "./catalog-client.js";
|
|
17
|
-
import { recipeToServerConfig, collectRequiredEnvVars } from "./config.js";
|
|
18
16
|
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
19
17
|
const DEFAULT_CONNECT_ERROR_COOLDOWN_MS = 10 * 1000;
|
|
20
18
|
const DEFAULT_MAX_CONCURRENT = 5;
|
|
@@ -35,7 +33,6 @@ export class McpRouter {
|
|
|
35
33
|
tokenManager;
|
|
36
34
|
rateLimiter;
|
|
37
35
|
requestIdState = { value: 0 };
|
|
38
|
-
catalogClient;
|
|
39
36
|
intentRouter = null;
|
|
40
37
|
promotion = null;
|
|
41
38
|
constructor(servers, clientConfig, logger, transportRefs) {
|
|
@@ -61,7 +58,6 @@ export class McpRouter {
|
|
|
61
58
|
this.toolResolver = new ToolResolver(Object.keys(servers));
|
|
62
59
|
this.tokenManager = new OAuth2TokenManager(logger, new FileTokenStore());
|
|
63
60
|
this.rateLimiter = new RateLimiter();
|
|
64
|
-
this.catalogClient = new CatalogClient({ logger });
|
|
65
61
|
if (clientConfig.adaptivePromotion?.enabled) {
|
|
66
62
|
this.promotion = new AdaptivePromotion(clientConfig.adaptivePromotion, logger);
|
|
67
63
|
}
|
|
@@ -69,7 +65,7 @@ export class McpRouter {
|
|
|
69
65
|
static generateDescription(servers) {
|
|
70
66
|
const serverNames = Object.keys(servers);
|
|
71
67
|
if (serverNames.length === 0) {
|
|
72
|
-
return "MCP server manager with no servers configured yet.
|
|
68
|
+
return "MCP server manager with no servers configured yet.";
|
|
73
69
|
}
|
|
74
70
|
const serverList = serverNames
|
|
75
71
|
.map((name) => {
|
|
@@ -77,7 +73,7 @@ export class McpRouter {
|
|
|
77
73
|
return desc ? `${name} (${desc})` : name;
|
|
78
74
|
})
|
|
79
75
|
.join(", ");
|
|
80
|
-
return `MCP server manager with ${serverNames.length} connected servers: ${serverList}. Actions: 'call' to execute a tool, 'list' to discover tools on a server, 'batch' for multiple calls in one round-trip, 'status' to check connections, 'refresh' to re-discover tools.
|
|
76
|
+
return `MCP server manager with ${serverNames.length} connected servers: ${serverList}. Actions: 'call' to execute a tool, 'list' to discover tools on a server, 'batch' for multiple calls in one round-trip, 'status' to check connections, 'refresh' to re-discover tools. Use 'set-env' with params.key and params.value to configure API keys (stored in ~/.mcp-bridge/.env). If the user mentions a specific tool by name, the call action auto-connects and works without listing first.`;
|
|
81
77
|
}
|
|
82
78
|
async dispatch(server, action = "call", tool, params) {
|
|
83
79
|
try {
|
|
@@ -98,137 +94,6 @@ export class McpRouter {
|
|
|
98
94
|
}
|
|
99
95
|
return this.resolveIntent(intent);
|
|
100
96
|
}
|
|
101
|
-
// Search catalog
|
|
102
|
-
if (normalizedAction === "search") {
|
|
103
|
-
const query = params?.query || server || tool;
|
|
104
|
-
if (!query) {
|
|
105
|
-
return this.error("invalid_params", "query is required for action=search (pass as params.query or server field)");
|
|
106
|
-
}
|
|
107
|
-
if (!this.catalogClient) {
|
|
108
|
-
return this.error("mcp_error", "Catalog client not available");
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
const searchResponse = await this.catalogClient.search(query);
|
|
112
|
-
// search() may return array or {results: [...]} depending on catalog API version
|
|
113
|
-
const results = Array.isArray(searchResponse) ? searchResponse : searchResponse.results || [];
|
|
114
|
-
return {
|
|
115
|
-
action: "search",
|
|
116
|
-
query,
|
|
117
|
-
results: results.map((r) => ({
|
|
118
|
-
id: r.name,
|
|
119
|
-
name: r.name,
|
|
120
|
-
description: r.description || "",
|
|
121
|
-
category: r.category,
|
|
122
|
-
auth: r.authSummary || (r.authRequired ? "required" : "none"),
|
|
123
|
-
origin: r.origin,
|
|
124
|
-
maturity: r.maturity,
|
|
125
|
-
sideEffects: r.sideEffects,
|
|
126
|
-
pricing: r.pricing,
|
|
127
|
-
signed: Array.isArray(r.badges) ? r.badges.includes("signed") : false,
|
|
128
|
-
}))
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
return this.error("mcp_error", `Catalog search failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// Browse catalog
|
|
136
|
-
if (normalizedAction === "catalog") {
|
|
137
|
-
if (!this.catalogClient) {
|
|
138
|
-
return this.error("mcp_error", "Catalog client not available");
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const recipeList = await this.catalogClient.list({ limit: 200 });
|
|
142
|
-
return {
|
|
143
|
-
action: "catalog",
|
|
144
|
-
recipes: recipeList.results.map((r) => ({
|
|
145
|
-
id: r.name,
|
|
146
|
-
name: r.name,
|
|
147
|
-
description: r.description || "",
|
|
148
|
-
category: r.category,
|
|
149
|
-
auth: r.authSummary || (r.authRequired ? "required" : "none"),
|
|
150
|
-
origin: r.origin,
|
|
151
|
-
maturity: r.maturity,
|
|
152
|
-
sideEffects: r.sideEffects,
|
|
153
|
-
pricing: r.pricing,
|
|
154
|
-
signed: Array.isArray(r.badges) ? r.badges.includes("signed") : false,
|
|
155
|
-
}))
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
return this.error("mcp_error", `Catalog browse failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Install server from catalog (runtime + persisted to config file)
|
|
163
|
-
if (normalizedAction === "install") {
|
|
164
|
-
const serverName = server || params?.server || params?.name;
|
|
165
|
-
if (!serverName) {
|
|
166
|
-
return this.error("invalid_params", "server name is required for action=install (pass as server field or params.name)");
|
|
167
|
-
}
|
|
168
|
-
// Sanitize serverName: only lowercase alphanumeric + hyphens (matches recipe spec id format)
|
|
169
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(serverName)) {
|
|
170
|
-
return this.error("invalid_params", `Invalid server name "${serverName}". Must match /^[a-z0-9][a-z0-9-]*$/ (lowercase alphanumeric + hyphens).`);
|
|
171
|
-
}
|
|
172
|
-
if (this.servers[serverName]) {
|
|
173
|
-
return { action: "install", server: serverName, installed: true, message: `Server "${serverName}" is already configured.` };
|
|
174
|
-
}
|
|
175
|
-
if (!this.catalogClient) {
|
|
176
|
-
return this.error("mcp_error", "Catalog client not available");
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
const recipe = await this.catalogClient.resolve(serverName);
|
|
180
|
-
const serverConfig = recipeToServerConfig(recipe);
|
|
181
|
-
if (!serverConfig) {
|
|
182
|
-
return this.error("mcp_error", `Unsupported recipe format for "${serverName}"`);
|
|
183
|
-
}
|
|
184
|
-
// Check env vars
|
|
185
|
-
const requiredVars = collectRequiredEnvVars(recipe);
|
|
186
|
-
const missing = requiredVars.filter(v => !process.env[v]);
|
|
187
|
-
// Add to runtime config
|
|
188
|
-
this.servers[serverName] = serverConfig;
|
|
189
|
-
this.clientConfig.servers[serverName] = serverConfig;
|
|
190
|
-
// Persist to config file
|
|
191
|
-
try {
|
|
192
|
-
const os = await import("os");
|
|
193
|
-
const fs = await import("fs");
|
|
194
|
-
const path = await import("path");
|
|
195
|
-
const configPath = path.join(os.homedir(), ".mcp-bridge", "config.json");
|
|
196
|
-
if (fs.existsSync(configPath)) {
|
|
197
|
-
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
198
|
-
if (!raw.servers)
|
|
199
|
-
raw.servers = {};
|
|
200
|
-
if (!raw.servers[serverName]) {
|
|
201
|
-
raw.servers[serverName] = serverConfig;
|
|
202
|
-
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
203
|
-
this.logger.info(`Persisted "${serverName}" to ${configPath}`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (persistErr) {
|
|
208
|
-
this.logger.warn(`Could not persist "${serverName}" to config: ${persistErr instanceof Error ? persistErr.message : String(persistErr)}`);
|
|
209
|
-
}
|
|
210
|
-
const credUrl = recipe.auth?.credentialsUrl;
|
|
211
|
-
if (missing.length > 0) {
|
|
212
|
-
return {
|
|
213
|
-
action: "install",
|
|
214
|
-
server: serverName,
|
|
215
|
-
installed: true,
|
|
216
|
-
message: `Server "${serverName}" installed. Missing env vars: ${missing.join(", ")}. Set them before calling.`,
|
|
217
|
-
missingEnvVars: missing,
|
|
218
|
-
...(credUrl ? { credentialsUrl: credUrl } : {}),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
action: "install",
|
|
223
|
-
server: serverName,
|
|
224
|
-
installed: true,
|
|
225
|
-
message: `Server "${serverName}" installed and ready to use.`
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
catch (err) {
|
|
229
|
-
return this.error("mcp_error", `Install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
97
|
if (normalizedAction === "remove") {
|
|
233
98
|
const serverName = server || params?.server || params?.name;
|
|
234
99
|
if (!serverName) {
|
|
@@ -464,7 +329,7 @@ export class McpRouter {
|
|
|
464
329
|
}
|
|
465
330
|
}
|
|
466
331
|
if (normalizedAction !== "call") {
|
|
467
|
-
return this.error("invalid_params", `action must be one of: list, call, batch, refresh, schema, intent, status, promotions,
|
|
332
|
+
return this.error("invalid_params", `action must be one of: list, call, batch, refresh, schema, intent, status, promotions, remove, set-mode, set-env`);
|
|
468
333
|
}
|
|
469
334
|
if (!tool) {
|
|
470
335
|
return this.error("invalid_params", "tool is required for action=call");
|
|
@@ -228,7 +228,7 @@ export class StandaloneServer {
|
|
|
228
228
|
type: "object",
|
|
229
229
|
properties: {
|
|
230
230
|
server: { type: "string", description: "Server name" },
|
|
231
|
-
action: { type: "string", description: "list | call | batch | refresh | status | intent | schema | promotions |
|
|
231
|
+
action: { type: "string", description: "list | call | batch | refresh | status | intent | schema | promotions | remove | set-mode | set-env" },
|
|
232
232
|
tool: { type: "string", description: "Tool name for action=call/schema" },
|
|
233
233
|
params: { type: "object", description: "Tool arguments" },
|
|
234
234
|
calls: {
|
|
@@ -409,15 +409,13 @@ export class StandaloneServer {
|
|
|
409
409
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: `Failed to set env var: ${err instanceof Error ? err.message : String(err)}` }] } };
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
-
if (action === "
|
|
412
|
+
if (action === "set-mode" || action === "remove") {
|
|
413
413
|
// Delegate to router dispatch (create a temporary router for management actions)
|
|
414
414
|
if (!this.router) {
|
|
415
415
|
const { McpRouter } = await import("./mcp-router.js");
|
|
416
416
|
this.router = new McpRouter(this.config.servers, this.config, this.logger);
|
|
417
417
|
}
|
|
418
418
|
const params = {};
|
|
419
|
-
if (query)
|
|
420
|
-
params.query = query;
|
|
421
419
|
if (server)
|
|
422
420
|
params.server = server;
|
|
423
421
|
if (server)
|
|
@@ -425,13 +423,9 @@ export class StandaloneServer {
|
|
|
425
423
|
if (toolArgs?.mode)
|
|
426
424
|
params.mode = toolArgs.mode;
|
|
427
425
|
const result = await this.router.dispatch(server, action, undefined, params);
|
|
428
|
-
// If install succeeded, notify about new tools
|
|
429
|
-
if (action === "install" && "installed" in result && result.installed) {
|
|
430
|
-
this.sendToolsChanged();
|
|
431
|
-
}
|
|
432
426
|
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(result) }] } };
|
|
433
427
|
}
|
|
434
|
-
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "Unknown action. Use:
|
|
428
|
+
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: "Unknown action. Use: remove, status, servers, discover, set-mode, set-env" }] } };
|
|
435
429
|
}
|
|
436
430
|
// Handle mcp_discover tool (legacy, kept for backward compatibility)
|
|
437
431
|
if (toolName === "mcp_discover") {
|
|
@@ -607,13 +601,12 @@ export class StandaloneServer {
|
|
|
607
601
|
const serverNames = Object.keys(this.config.servers);
|
|
608
602
|
return {
|
|
609
603
|
name: "mcp_manage",
|
|
610
|
-
description: `MCP server manager. Actions: '
|
|
604
|
+
description: `MCP server manager. Actions: 'remove' to remove a server, 'status' to check connections, 'servers' to list configured servers, 'discover' to connect a server and discover its tools, 'set-mode' to switch between router/direct mode, 'set-env' to configure API keys in ~/.mcp-bridge/.env. Connected servers: ${serverNames.join(", ") || "none"}.`,
|
|
611
605
|
inputSchema: {
|
|
612
606
|
type: "object",
|
|
613
607
|
properties: {
|
|
614
|
-
action: { type: "string", description: "
|
|
615
|
-
server: { type: "string", description: "Server name (for
|
|
616
|
-
query: { type: "string", description: "Search query (for action=search)" },
|
|
608
|
+
action: { type: "string", description: "remove | status | servers | discover | set-mode | set-env", enum: ["remove", "status", "servers", "discover", "set-mode", "set-env"] },
|
|
609
|
+
server: { type: "string", description: "Server name (for remove, discover)" },
|
|
617
610
|
mode: { type: "string", description: "Mode (for action=set-mode): router or direct", enum: ["router", "direct"] },
|
|
618
611
|
key: { type: "string", description: "Environment variable name for action=set-env (e.g. TODOIST_API_TOKEN)" },
|
|
619
612
|
value: { type: "string", description: "Environment variable value for action=set-env" }
|
package/dist/src/types.d.ts
CHANGED
|
@@ -175,14 +175,4 @@ export interface BridgeConfig extends McpClientConfig {
|
|
|
175
175
|
*/
|
|
176
176
|
requireCleanAudit?: boolean;
|
|
177
177
|
};
|
|
178
|
-
/**
|
|
179
|
-
* Whether bootstrapCatalog() fetches recipes from the remote catalog.
|
|
180
|
-
* Default: true (catalog discovery is enabled).
|
|
181
|
-
*/
|
|
182
|
-
catalog?: boolean;
|
|
183
|
-
/**
|
|
184
|
-
* Whether mergeRecipesIntoConfig() auto-merges cached recipes into config.
|
|
185
|
-
* Default: false (opt-in). Set to true to enable automatic recipe merging.
|
|
186
|
-
*/
|
|
187
|
-
autoMerge?: boolean;
|
|
188
178
|
}
|
|
@@ -274,6 +274,68 @@ export function validateRecipe(recipe) {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
+
// ── v2 spec field acceptance (Universal Recipe Spec v2, fields added since 2.8.x) ──
|
|
278
|
+
//
|
|
279
|
+
// These are typo-guarded boolean/string checks. Recipes shipped from the
|
|
280
|
+
// hosted bridge use them; the standalone runtime mostly informs the user
|
|
281
|
+
// and does not enforce hosted-only semantics (e.g. localOnly is not a
|
|
282
|
+
// gate locally — every recipe runs on the user's machine anyway).
|
|
283
|
+
// localOnly: top-level boolean. Hosted bridge refuses these recipes;
|
|
284
|
+
// standalone accepts them and lets the user spawn locally.
|
|
285
|
+
if (recipe.localOnly !== undefined && typeof recipe.localOnly !== "boolean") {
|
|
286
|
+
errors.push(`localOnly must be boolean, got ${typeof recipe.localOnly}`);
|
|
287
|
+
}
|
|
288
|
+
// multiInstance + instanceNameHint: hosted-only semantics today. Standalone
|
|
289
|
+
// accepts the fields without enforcing multi-instance install behavior.
|
|
290
|
+
if (recipe.multiInstance !== undefined && typeof recipe.multiInstance !== "boolean") {
|
|
291
|
+
errors.push(`multiInstance must be boolean, got ${typeof recipe.multiInstance}`);
|
|
292
|
+
}
|
|
293
|
+
if (recipe.instanceNameHint !== undefined && typeof recipe.instanceNameHint !== "string") {
|
|
294
|
+
errors.push(`instanceNameHint must be string, got ${typeof recipe.instanceNameHint}`);
|
|
295
|
+
}
|
|
296
|
+
// auth.options[]: multi-auth picker. If present, must be an array of objects
|
|
297
|
+
// with an id (string), label (string), and type (string).
|
|
298
|
+
const authOptions = recipe.auth?.options;
|
|
299
|
+
if (authOptions !== undefined) {
|
|
300
|
+
if (!Array.isArray(authOptions)) {
|
|
301
|
+
errors.push("auth.options must be an array");
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
for (let i = 0; i < authOptions.length; i++) {
|
|
305
|
+
const opt = authOptions[i];
|
|
306
|
+
if (!opt || typeof opt !== "object") {
|
|
307
|
+
errors.push(`auth.options[${i}]: must be an object`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (typeof opt.id !== "string" || opt.id.trim().length === 0) {
|
|
311
|
+
errors.push(`auth.options[${i}]: id is required (non-empty string)`);
|
|
312
|
+
}
|
|
313
|
+
if (typeof opt.label !== "string" || opt.label.trim().length === 0) {
|
|
314
|
+
errors.push(`auth.options[${i}]: label is required (non-empty string)`);
|
|
315
|
+
}
|
|
316
|
+
if (typeof opt.type !== "string" || opt.type.trim().length === 0) {
|
|
317
|
+
errors.push(`auth.options[${i}]: type is required (non-empty string)`);
|
|
318
|
+
}
|
|
319
|
+
if (opt.recommended !== undefined && typeof opt.recommended !== "boolean") {
|
|
320
|
+
errors.push(`auth.options[${i}]: recommended must be boolean if present`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// oauth2.envBinding: env var name to bind the OAuth access_token at spawn time.
|
|
326
|
+
// oauth2.credentialsFileType: marks recipes that need a credentials file
|
|
327
|
+
// written to disk (e.g. workspace-mcp). Standalone reads but does not yet
|
|
328
|
+
// write the file; will be plumbed through OAuth2 token manager when the
|
|
329
|
+
// feature is needed locally.
|
|
330
|
+
const oauth2 = recipe.auth?.oauth2;
|
|
331
|
+
if (oauth2 && typeof oauth2 === "object") {
|
|
332
|
+
if (oauth2.envBinding !== undefined && typeof oauth2.envBinding !== "string") {
|
|
333
|
+
errors.push(`auth.oauth2.envBinding must be string, got ${typeof oauth2.envBinding}`);
|
|
334
|
+
}
|
|
335
|
+
if (oauth2.credentialsFileType !== undefined && typeof oauth2.credentialsFileType !== "string") {
|
|
336
|
+
errors.push(`auth.oauth2.credentialsFileType must be string, got ${typeof oauth2.credentialsFileType}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
277
339
|
// ── Build result ───────────────────────────────────────────────────────────
|
|
278
340
|
const valid = errors.length === 0;
|
|
279
341
|
const result = { valid, errors, warnings };
|
package/package.json
CHANGED
|
@@ -50,30 +50,17 @@ done
|
|
|
50
50
|
|
|
51
51
|
SERVER_DIR="$SCRIPT_DIR/../servers/$SERVER_NAME"
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
ENCODED_NAME="$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$SERVER_NAME" 2>/dev/null || echo "$SERVER_NAME")"
|
|
62
|
-
if curl -sf --connect-timeout 5 --max-time 15 \
|
|
63
|
-
"https://catalog.aiwerk.ch/api/recipes/$ENCODED_NAME/download" \
|
|
64
|
-
-o "$RECIPE_CACHE_DIR/recipe.json" 2>/dev/null \
|
|
65
|
-
&& [[ -s "$RECIPE_CACHE_DIR/recipe.json" ]]; then
|
|
66
|
-
echo "[mcp-bridge] ✓ Recipe downloaded from catalog"
|
|
67
|
-
SERVER_DIR="$RECIPE_CACHE_DIR"
|
|
68
|
-
else
|
|
69
|
-
rm -f "$RECIPE_CACHE_DIR/recipe.json"
|
|
70
|
-
echo "[mcp-bridge] Catalog unavailable, using bundled recipe"
|
|
71
|
-
# Fall through to existing SERVER_DIR
|
|
72
|
-
fi
|
|
73
|
-
fi
|
|
53
|
+
# Historical note (2026-04-10): earlier versions of this script fetched
|
|
54
|
+
# recipes over HTTP from catalog.aiwerk.ch (and briefly from
|
|
55
|
+
# bridge.aiwerk.ch) before falling back to the bundled servers/ directory.
|
|
56
|
+
# That coupling has been removed — mcp-bridge is a pure router and the
|
|
57
|
+
# bundled servers/ tree is the only recipe source. Users who want to add
|
|
58
|
+
# their own server should either copy a bundled recipe into servers/ as a
|
|
59
|
+
# starting point, or write their own ~/.mcp-bridge/config.json entry
|
|
60
|
+
# directly and point it to any MCP server they want.
|
|
74
61
|
|
|
75
62
|
if [[ ! -d "$SERVER_DIR" ]]; then
|
|
76
|
-
echo "Error: Server '$SERVER_NAME' not found
|
|
63
|
+
echo "Error: Server '$SERVER_NAME' not found in $SCRIPT_DIR/../servers/"
|
|
77
64
|
usage
|
|
78
65
|
fi
|
|
79
66
|
|