@aiwerk/mcp-bridge 2.8.0 → 2.8.3
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 +51 -1
- package/dist/bin/mcp-bridge.js +3 -1
- package/dist/src/catalog-client.d.ts +2 -1
- package/dist/src/catalog-client.js +4 -2
- package/dist/src/config.d.ts +3 -1
- package/dist/src/config.js +37 -4
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/transport-stdio.js +7 -12
- package/dist/src/types.d.ts +18 -0
- package/dist/src/validate-recipe.js +87 -0
- package/package.json +1 -1
- package/scripts/install-server.sh +6 -1
- package/scripts/verify-signatures.mjs +121 -0
- package/servers/apify/recipe.json +3 -3
- package/servers/chrome-devtools/recipe.json +4 -4
- package/servers/firecrawl/recipe.json +3 -3
- package/servers/google-maps/recipe.json +4 -4
- package/servers/hostinger/recipe.json +3 -3
- package/servers/imap-email/recipe.json +3 -3
- package/servers/index.json +32 -6
- package/servers/linear/recipe.json +3 -3
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Most AI agents connect to MCP servers one-by-one. With 10+ servers, that's 10+ c
|
|
|
27
27
|
- **Graceful shutdown**: clean process termination and connection cleanup
|
|
28
28
|
- **Direct mode**: all tools registered individually with automatic prefixing
|
|
29
29
|
- **3 transports**: stdio, SSE, streamable-http
|
|
30
|
-
- **Built-in catalog**: 14 pre-configured servers, install with one command
|
|
30
|
+
- **Built-in catalog**: 14 pre-configured servers, install with one command (bundled servers deprecated — use [MCP Catalog](https://catalog.aiwerk.ch) with 104+ recipes instead)
|
|
31
31
|
- **Zero config secrets in files**: `${ENV_VAR}` resolution from `.env`
|
|
32
32
|
|
|
33
33
|
## Install
|
|
@@ -109,6 +109,56 @@ const recipe = await client.resolve('todoist');
|
|
|
109
109
|
const results = await client.search('email');
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
### Catalog & Auto-Merge Options
|
|
113
|
+
|
|
114
|
+
Two config options control catalog behavior:
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Description |
|
|
117
|
+
|--------|------|---------|-------------|
|
|
118
|
+
| `catalog` | `boolean` | `true` | Whether `bootstrapCatalog()` fetches recipes from the remote catalog |
|
|
119
|
+
| `autoMerge` | `boolean` | `false` | Whether `mergeRecipesIntoConfig()` auto-merges cached recipes into your config |
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"catalog": true,
|
|
124
|
+
"autoMerge": true,
|
|
125
|
+
"servers": { ... }
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- **`autoMerge` defaults to `false`** (opt-in) — cached recipes are **not** automatically added to your server list unless you explicitly enable it. This prevents servers without required credentials from being silently activated.
|
|
130
|
+
- **`catalog` defaults to `true`** — recipe discovery from [catalog.aiwerk.ch](https://catalog.aiwerk.ch) is enabled by default. Set to `false` to skip all remote fetching.
|
|
131
|
+
|
|
132
|
+
> **Breaking change (v2.9.0):** Previously, all cached recipes whose env vars were present were auto-merged. Now you must set `"autoMerge": true` to restore that behavior.
|
|
133
|
+
|
|
134
|
+
### Multiple instances of the same server
|
|
135
|
+
|
|
136
|
+
Auto-discovery uses the recipe name as the config key (e.g., `gohighlevel`). If you need **multiple instances** of the same server with different credentials (e.g., two GoHighLevel subaccounts), configure them manually:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
// config.json or openclaw.json
|
|
140
|
+
{
|
|
141
|
+
"ghl-client-a": {
|
|
142
|
+
"transport": "streamable-http",
|
|
143
|
+
"url": "https://services.leadconnectorhq.com/mcp/",
|
|
144
|
+
"headers": {
|
|
145
|
+
"Authorization": "Bearer ${GHL_TOKEN_A}",
|
|
146
|
+
"locationId": "${GHL_LOCATION_A}"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"ghl-client-b": {
|
|
150
|
+
"transport": "streamable-http",
|
|
151
|
+
"url": "https://services.leadconnectorhq.com/mcp/",
|
|
152
|
+
"headers": {
|
|
153
|
+
"Authorization": "Bearer ${GHL_TOKEN_B}",
|
|
154
|
+
"locationId": "${GHL_LOCATION_B}"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Use **unique env var names** (e.g., `GHL_TOKEN_A` instead of `GHL_PIT_TOKEN`) to prevent auto-discovery from adding a duplicate third entry. Manual config always takes priority over auto-discovered recipes.
|
|
161
|
+
|
|
112
162
|
> **Note**: The bundled `servers/` directory is deprecated and will be removed in v3.0.0.
|
|
113
163
|
|
|
114
164
|
## Use with Cursor / Windsurf
|
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join, dirname, resolve, extname } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { platform, homedir } from "os";
|
|
6
6
|
import { execFileSync } from "child_process";
|
|
7
|
-
import { loadConfig, initConfigDir } from "../src/config.js";
|
|
7
|
+
import { loadConfig, initConfigDir, warnDeprecatedBundledRecipes } 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";
|
|
@@ -554,6 +554,8 @@ async function cmdServe(args, logger) {
|
|
|
554
554
|
logger.error("HTTP auth not configured. Set http.auth in config or use stdio mode.");
|
|
555
555
|
process.exit(1);
|
|
556
556
|
}
|
|
557
|
+
// Warn about deprecated bundled recipes (v2.8.0+)
|
|
558
|
+
warnDeprecatedBundledRecipes(logger);
|
|
557
559
|
const server = new StandaloneServer(config, logger);
|
|
558
560
|
// Graceful shutdown
|
|
559
561
|
const shutdown = async () => {
|
|
@@ -78,6 +78,7 @@ export declare class CatalogClient {
|
|
|
78
78
|
limit?: number;
|
|
79
79
|
category?: string;
|
|
80
80
|
sort?: string;
|
|
81
|
+
hostedSafe?: boolean;
|
|
81
82
|
}): Promise<{
|
|
82
83
|
results: CatalogSearchResult[];
|
|
83
84
|
total: number;
|
|
@@ -93,7 +94,7 @@ export declare class CatalogClient {
|
|
|
93
94
|
* Bootstrap by downloading the top N most popular recipes.
|
|
94
95
|
* Skips already-cached recipes unless they are stale.
|
|
95
96
|
*/
|
|
96
|
-
bootstrap(limit?: number): Promise<string[]>;
|
|
97
|
+
bootstrap(limit?: number, hostedSafe?: boolean): Promise<string[]>;
|
|
97
98
|
/** Synchronously read a recipe from local cache. Returns null if not cached. */
|
|
98
99
|
getCached(name: string): CatalogRecipe | null;
|
|
99
100
|
/** List all recipe names in the local cache. */
|
|
@@ -110,6 +110,8 @@ export class CatalogClient {
|
|
|
110
110
|
params.set("category", opts.category);
|
|
111
111
|
if (opts?.sort)
|
|
112
112
|
params.set("sort", opts.sort);
|
|
113
|
+
if (opts?.hostedSafe)
|
|
114
|
+
params.set("hostedSafe", "true");
|
|
113
115
|
const qs = params.toString();
|
|
114
116
|
return this.fetchJson(`/api/recipes${qs ? `?${qs}` : ""}`);
|
|
115
117
|
}
|
|
@@ -147,8 +149,8 @@ export class CatalogClient {
|
|
|
147
149
|
* Bootstrap by downloading the top N most popular recipes.
|
|
148
150
|
* Skips already-cached recipes unless they are stale.
|
|
149
151
|
*/
|
|
150
|
-
async bootstrap(limit = 15) {
|
|
151
|
-
const { results } = await this.list({ limit, sort: "popular" });
|
|
152
|
+
async bootstrap(limit = 15, hostedSafe = false) {
|
|
153
|
+
const { results } = await this.list({ limit, sort: "popular", hostedSafe });
|
|
152
154
|
const names = [];
|
|
153
155
|
const toDownload = [];
|
|
154
156
|
for (const entry of results) {
|
package/dist/src/config.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare function loadConfig(options?: LoadConfigOptions): BridgeConfig;
|
|
|
29
29
|
* In v2.8.0, bundled servers/ recipes are deprecated in favor of catalog.
|
|
30
30
|
* They will be removed in v3.0.0.
|
|
31
31
|
*/
|
|
32
|
-
export declare function warnDeprecatedBundledRecipes(
|
|
32
|
+
export declare function warnDeprecatedBundledRecipes(logger: Logger): void;
|
|
33
33
|
/** Get the default config directory path. */
|
|
34
34
|
export declare function getConfigDir(configPath?: string): string;
|
|
35
35
|
/** Initialize the config directory with template files. */
|
|
@@ -45,6 +45,8 @@ export declare function bootstrapCatalog(options?: {
|
|
|
45
45
|
catalogUrl?: string;
|
|
46
46
|
limit?: number;
|
|
47
47
|
force?: boolean;
|
|
48
|
+
requireCleanAudit?: boolean;
|
|
49
|
+
catalog?: boolean;
|
|
48
50
|
}): Promise<string[]>;
|
|
49
51
|
/**
|
|
50
52
|
* Merge cached catalog recipes into a BridgeConfig.
|
package/dist/src/config.js
CHANGED
|
@@ -159,9 +159,7 @@ export function loadConfig(options = {}) {
|
|
|
159
159
|
* In v2.8.0, bundled servers/ recipes are deprecated in favor of catalog.
|
|
160
160
|
* They will be removed in v3.0.0.
|
|
161
161
|
*/
|
|
162
|
-
export function warnDeprecatedBundledRecipes(
|
|
163
|
-
// Check if install-server.sh was used (it writes source: 'local' or references servers/ path)
|
|
164
|
-
// For now, just log a general deprecation notice on first run
|
|
162
|
+
export function warnDeprecatedBundledRecipes(logger) {
|
|
165
163
|
const catalogClient = new CatalogClient({ logger });
|
|
166
164
|
const cached = catalogClient.listCached();
|
|
167
165
|
if (cached.length === 0) {
|
|
@@ -227,14 +225,31 @@ const noopLogger = {
|
|
|
227
225
|
error: () => { },
|
|
228
226
|
debug: () => { },
|
|
229
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
|
+
}
|
|
230
238
|
/**
|
|
231
239
|
* Bootstrap the local recipe cache from the catalog.
|
|
232
240
|
* Downloads top N popular recipes if cache is empty or force=true.
|
|
233
241
|
* Returns array of recipe names now cached. Never throws on network errors.
|
|
234
242
|
*/
|
|
235
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
|
+
}
|
|
236
250
|
const logger = options?.logger ?? noopLogger;
|
|
237
251
|
const cacheDir = options?.cacheDir ?? join(homedir(), ".mcp-bridge", "recipes");
|
|
252
|
+
const requireCleanAudit = options?.requireCleanAudit ?? false;
|
|
238
253
|
const client = new CatalogClient({
|
|
239
254
|
baseUrl: options?.catalogUrl,
|
|
240
255
|
cacheDir,
|
|
@@ -249,7 +264,7 @@ export async function bootstrapCatalog(options) {
|
|
|
249
264
|
}
|
|
250
265
|
}
|
|
251
266
|
try {
|
|
252
|
-
return await client.bootstrap(options?.limit ?? 15);
|
|
267
|
+
return await client.bootstrap(options?.limit ?? 15, requireCleanAudit);
|
|
253
268
|
}
|
|
254
269
|
catch (err) {
|
|
255
270
|
logger.warn(`Catalog unreachable during bootstrap: ${err instanceof Error ? err.message : err}`);
|
|
@@ -266,12 +281,18 @@ export async function bootstrapCatalog(options) {
|
|
|
266
281
|
*/
|
|
267
282
|
export function mergeRecipesIntoConfig(config, options) {
|
|
268
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
|
+
}
|
|
269
289
|
const cacheDir = options?.cacheDir ?? join(homedir(), ".mcp-bridge", "recipes");
|
|
270
290
|
const client = new CatalogClient({ cacheDir, logger });
|
|
271
291
|
const names = client.listCached();
|
|
272
292
|
if (names.length === 0)
|
|
273
293
|
return config;
|
|
274
294
|
const servers = { ...config.servers };
|
|
295
|
+
const requireCleanAudit = config.security?.requireCleanAudit ?? false;
|
|
275
296
|
for (const name of names) {
|
|
276
297
|
// Never overwrite manually configured servers
|
|
277
298
|
if (servers[name])
|
|
@@ -279,6 +300,18 @@ export function mergeRecipesIntoConfig(config, options) {
|
|
|
279
300
|
const recipe = client.getCached(name);
|
|
280
301
|
if (!recipe)
|
|
281
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
|
+
}
|
|
282
315
|
const converted = recipeToServerConfig(recipe);
|
|
283
316
|
if (!converted) {
|
|
284
317
|
logger.debug(`Skipping recipe "${name}": unsupported format`);
|
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, bootstrapCatalog, mergeRecipesIntoConfig } from "./config.js";
|
|
21
|
+
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir, bootstrapCatalog, mergeRecipesIntoConfig, warnDeprecatedBundledRecipes } 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";
|
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, bootstrapCatalog, mergeRecipesIntoConfig } from "./config.js";
|
|
23
|
+
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir, bootstrapCatalog, mergeRecipesIntoConfig, warnDeprecatedBundledRecipes } from "./config.js";
|
|
24
24
|
export { nextRequestId } from "./types.js";
|
|
25
25
|
// Tool naming
|
|
26
26
|
export { pickRegisteredToolName } from "./tool-naming.js";
|
|
@@ -132,20 +132,15 @@ export class StdioTransport extends BaseTransport {
|
|
|
132
132
|
reject(error);
|
|
133
133
|
};
|
|
134
134
|
const onFirstData = (chunk) => {
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
135
|
+
// Some MCP servers print banner text to stdout before they start speaking JSON-RPC.
|
|
136
|
+
// We already parse and ignore non-JSON lines later in processStdoutBuffer(), so any
|
|
137
|
+
// stdout activity means the process is alive and its pipes are working. Resolve here
|
|
138
|
+
// and let initializeProtocol() validate the connection with an actual initialize request.
|
|
138
139
|
const text = chunk.toString().trim();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (text === "" || text.startsWith("{") || text.startsWith("Content-Length")) {
|
|
142
|
-
settleResolve();
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
this.logger.warn(`[mcp-bridge] Stdio process sent non-JSON data on stdout: ${text.substring(0, 80)}`);
|
|
146
|
-
// Still listen for valid data — don't reject yet, the next chunk might be valid
|
|
147
|
-
this.process?.stdout?.once("data", onFirstData);
|
|
140
|
+
if (text && !text.startsWith("{") && !text.startsWith("Content-Length")) {
|
|
141
|
+
this.logger.warn(`[mcp-bridge] Stdio process sent banner/non-JSON data on stdout before ready: ${text.substring(0, 80)}`);
|
|
148
142
|
}
|
|
143
|
+
settleResolve();
|
|
149
144
|
};
|
|
150
145
|
const onProcessError = (error) => settleReject(error);
|
|
151
146
|
const onProcessExit = () => settleReject(new Error("MCP server exited before stdout became ready"));
|
package/dist/src/types.d.ts
CHANGED
|
@@ -165,4 +165,22 @@ export interface BridgeConfig extends McpClientConfig {
|
|
|
165
165
|
http?: {
|
|
166
166
|
auth?: HttpAuthConfig;
|
|
167
167
|
};
|
|
168
|
+
security?: {
|
|
169
|
+
/**
|
|
170
|
+
* When true, only load MCP servers whose depAudit is "clean" or "not-applicable".
|
|
171
|
+
* Servers with "has-advisories", "skip", or other values are blocked at startup.
|
|
172
|
+
* Default: false (advisories are logged as info, not blocked).
|
|
173
|
+
*/
|
|
174
|
+
requireCleanAudit?: boolean;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Whether bootstrapCatalog() fetches recipes from the remote catalog.
|
|
178
|
+
* Default: true (catalog discovery is enabled).
|
|
179
|
+
*/
|
|
180
|
+
catalog?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Whether mergeRecipesIntoConfig() auto-merges cached recipes into config.
|
|
183
|
+
* Default: false (opt-in). Set to true to enable automatic recipe merging.
|
|
184
|
+
*/
|
|
185
|
+
autoMerge?: boolean;
|
|
168
186
|
}
|
|
@@ -7,6 +7,8 @@ const KNOWN_CATEGORIES = new Set([
|
|
|
7
7
|
"development",
|
|
8
8
|
"communication",
|
|
9
9
|
"data",
|
|
10
|
+
"database",
|
|
11
|
+
"crm",
|
|
10
12
|
"finance",
|
|
11
13
|
"infrastructure",
|
|
12
14
|
"analytics",
|
|
@@ -151,6 +153,26 @@ export function validateRecipe(recipe) {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
// ── §7.2 Warnings ──────────────────────────────────────────────────────────
|
|
156
|
+
// Warning: metadata.verification field validation
|
|
157
|
+
const verification = recipe.metadata?.verification;
|
|
158
|
+
if (verification && typeof verification === 'object') {
|
|
159
|
+
if ('tier1' in verification && verification.tier1 !== 'pass') {
|
|
160
|
+
warnings.push(`metadata.verification.tier1 should be "pass", got: "${verification.tier1}"`);
|
|
161
|
+
}
|
|
162
|
+
if ('tier2' in verification) {
|
|
163
|
+
const tier2 = verification.tier2;
|
|
164
|
+
if (tier2 !== 'pass' && tier2 !== 'skip') {
|
|
165
|
+
warnings.push(`metadata.verification.tier2 should be "pass" or "skip", got: "${tier2}"`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if ('depAudit' in verification) {
|
|
169
|
+
const depAudit = verification.depAudit;
|
|
170
|
+
const validDepAudit = new Set(['clean', 'has-advisories', 'not-applicable', 'skip']);
|
|
171
|
+
if (!validDepAudit.has(depAudit)) {
|
|
172
|
+
warnings.push(`metadata.verification.depAudit should be "clean", "has-advisories", "not-applicable", or "skip", got: "${depAudit}"`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
154
176
|
// Warning: metadata.lastVerified older than 90 days
|
|
155
177
|
if (typeof recipe.metadata?.lastVerified === "string") {
|
|
156
178
|
const lastVerified = new Date(recipe.metadata.lastVerified);
|
|
@@ -187,6 +209,71 @@ export function validateRecipe(recipe) {
|
|
|
187
209
|
if (recipe.metadata?.maturity === "deprecated") {
|
|
188
210
|
warnings.push("metadata.maturity is set to 'deprecated'");
|
|
189
211
|
}
|
|
212
|
+
// ── §2.9 Origin cross-check ────────────────────────────────────────────
|
|
213
|
+
// If origin is "official", verify that metadata.author matches the npm package scope/maintainer hint.
|
|
214
|
+
// This is a heuristic — not all packages have matching names — so it's a warning, not an error.
|
|
215
|
+
const origin = recipe.metadata?.origin;
|
|
216
|
+
const author = recipe.metadata?.author;
|
|
217
|
+
const installPkg = recipe.install?.package;
|
|
218
|
+
if (origin === "official" && typeof author === "string" && typeof installPkg === "string") {
|
|
219
|
+
// Extract npm scope (e.g., "@cloudflare/mcp-server-cloudflare" → "cloudflare")
|
|
220
|
+
const scopeMatch = installPkg.match(/^@([^/]+)\//);
|
|
221
|
+
const scope = scopeMatch ? scopeMatch[1].toLowerCase() : null;
|
|
222
|
+
const authorLower = author.toLowerCase();
|
|
223
|
+
if (scope) {
|
|
224
|
+
// Check if the scope relates to the author (heuristic: scope contains author or vice versa)
|
|
225
|
+
// Known mappings for official packages where scope ≠ author name
|
|
226
|
+
const knownOfficialMappings = {
|
|
227
|
+
"playwright": ["microsoft"],
|
|
228
|
+
"browserbasehq": ["browserbase"],
|
|
229
|
+
"anthropic-ai": ["anthropic"],
|
|
230
|
+
"twilio-alpha": ["twilio"],
|
|
231
|
+
"perplexity-ai": ["perplexity"],
|
|
232
|
+
"pinecone-database": ["pinecone"],
|
|
233
|
+
"neondatabase": ["neon"],
|
|
234
|
+
"doist": ["todoist"],
|
|
235
|
+
"webflow-bot": ["webflow"],
|
|
236
|
+
};
|
|
237
|
+
const knownAliases = knownOfficialMappings[scope] ?? [];
|
|
238
|
+
const scopeMatchesAuthor = authorLower.includes(scope) ||
|
|
239
|
+
scope.includes(authorLower.replace(/[^a-z0-9]/g, "")) ||
|
|
240
|
+
knownAliases.some(alias => authorLower.includes(alias));
|
|
241
|
+
if (!scopeMatchesAuthor) {
|
|
242
|
+
// Also check common patterns: "modelcontextprotocol" scope = Anthropic community, not official
|
|
243
|
+
const communityScopes = new Set([
|
|
244
|
+
"modelcontextprotocol",
|
|
245
|
+
]);
|
|
246
|
+
if (communityScopes.has(scope)) {
|
|
247
|
+
warnings.push(`origin is "official" but package scope @${scope} is a community/ecosystem scope — verify that ${author} actually maintains this package`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
warnings.push(`origin is "official" but npm scope @${scope} does not obviously match author "${author}" — verify npm maintainers`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// No scope — unscoped packages are harder to verify, just note it
|
|
256
|
+
warnings.push(`origin is "official" but package "${installPkg}" is unscoped — verify npm maintainers match "${author}"`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (origin === "community" && typeof installPkg === "string") {
|
|
260
|
+
// If community but package scope matches a well-known official org, warn
|
|
261
|
+
const scopeMatch = installPkg.match(/^@([^/]+)\//);
|
|
262
|
+
if (scopeMatch) {
|
|
263
|
+
const scope = scopeMatch[1].toLowerCase();
|
|
264
|
+
const officialScopes = new Set([
|
|
265
|
+
"cloudflare", "stripe", "mongodb", "sentry", "datadog",
|
|
266
|
+
"supabase", "notion-email", "slack", "linear", "playwright",
|
|
267
|
+
"brave", "hubspot", "twilio-alpha", "perplexity-ai", "upstash",
|
|
268
|
+
"pinecone-database", "grafana", "e2b", "browserbasehq", "brightdata",
|
|
269
|
+
"letta-ai", "sanity", "contentful", "neondatabase",
|
|
270
|
+
"shortcut", "webflow-bot", "doist", "replicate",
|
|
271
|
+
]);
|
|
272
|
+
if (officialScopes.has(scope)) {
|
|
273
|
+
warnings.push(`origin is "community" but package scope @${scope} looks like an official org — verify origin`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
190
277
|
// ── Build result ───────────────────────────────────────────────────────────
|
|
191
278
|
const valid = errors.length === 0;
|
|
192
279
|
const result = { valid, errors, warnings };
|
package/package.json
CHANGED
|
@@ -58,10 +58,15 @@ if [[ -f "$RECIPE_CACHE_DIR/recipe.json" ]]; then
|
|
|
58
58
|
elif command -v curl &>/dev/null; then
|
|
59
59
|
echo "[mcp-bridge] Fetching recipe from catalog..."
|
|
60
60
|
mkdir -p "$RECIPE_CACHE_DIR"
|
|
61
|
-
|
|
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
|
|
62
66
|
echo "[mcp-bridge] ✓ Recipe downloaded from catalog"
|
|
63
67
|
SERVER_DIR="$RECIPE_CACHE_DIR"
|
|
64
68
|
else
|
|
69
|
+
rm -f "$RECIPE_CACHE_DIR/recipe.json"
|
|
65
70
|
echo "[mcp-bridge] Catalog unavailable, using bundled recipe"
|
|
66
71
|
# Fall through to existing SERVER_DIR
|
|
67
72
|
fi
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verify Ed25519 signatures on all recipes in servers/.
|
|
4
|
+
* Usage: node scripts/verify-signatures.mjs [--ci]
|
|
5
|
+
*
|
|
6
|
+
* --ci: exit with code 1 if any signature is invalid (for CI/pre-commit)
|
|
7
|
+
*
|
|
8
|
+
* Requires: public key in keys/aiwerk-public.pem (or set AIWERK_PUBLIC_KEY env var)
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { verify, createPublicKey } from 'node:crypto';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const ROOT = join(__dirname, '..');
|
|
17
|
+
const SERVERS_DIR = join(ROOT, 'servers');
|
|
18
|
+
|
|
19
|
+
const SIGNED_FIELDS = ['id', 'name', 'description', 'transports', 'auth', 'install', 'metadata'];
|
|
20
|
+
|
|
21
|
+
function stableStringify(obj) {
|
|
22
|
+
if (obj === null || obj === undefined) return JSON.stringify(obj);
|
|
23
|
+
if (typeof obj !== 'object') return JSON.stringify(obj);
|
|
24
|
+
if (Array.isArray(obj)) {
|
|
25
|
+
return '[' + obj.map(item => stableStringify(item)).join(',') + ']';
|
|
26
|
+
}
|
|
27
|
+
const sorted = Object.keys(obj).sort();
|
|
28
|
+
const entries = sorted.map(key => JSON.stringify(key) + ':' + stableStringify(obj[key]));
|
|
29
|
+
return '{' + entries.join(',') + '}';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canonicalPayload(recipe) {
|
|
33
|
+
const subset = {};
|
|
34
|
+
for (const field of SIGNED_FIELDS) {
|
|
35
|
+
if (field in recipe) {
|
|
36
|
+
subset[field] = recipe[field];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return stableStringify(subset);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadPublicKey() {
|
|
43
|
+
// Try env var first
|
|
44
|
+
if (process.env.AIWERK_PUBLIC_KEY) {
|
|
45
|
+
const pem = Buffer.from(process.env.AIWERK_PUBLIC_KEY, 'base64').toString('utf-8');
|
|
46
|
+
return createPublicKey(pem);
|
|
47
|
+
}
|
|
48
|
+
// Try keys/ dir
|
|
49
|
+
const keyPath = join(ROOT, 'keys', 'aiwerk-public.pem');
|
|
50
|
+
if (existsSync(keyPath)) {
|
|
51
|
+
return createPublicKey(readFileSync(keyPath, 'utf-8'));
|
|
52
|
+
}
|
|
53
|
+
// Try catalog repo
|
|
54
|
+
const catalogKeyPath = join(ROOT, '..', 'mcp-catalog', 'keys', 'aiwerk-public.pem');
|
|
55
|
+
if (existsSync(catalogKeyPath)) {
|
|
56
|
+
return createPublicKey(readFileSync(catalogKeyPath, 'utf-8'));
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function verifySignature(recipe, signature, publicKey) {
|
|
62
|
+
try {
|
|
63
|
+
const payload = canonicalPayload(recipe);
|
|
64
|
+
return verify(null, Buffer.from(payload, 'utf-8'), publicKey, Buffer.from(signature.value, 'base64'));
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ciMode = process.argv.includes('--ci');
|
|
71
|
+
const publicKey = loadPublicKey();
|
|
72
|
+
|
|
73
|
+
if (!publicKey) {
|
|
74
|
+
console.error('⚠️ No public key found. Skipping signature verification.');
|
|
75
|
+
console.error(' Set AIWERK_PUBLIC_KEY env var or place keys/aiwerk-public.pem');
|
|
76
|
+
process.exit(ciMode ? 1 : 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries = readdirSync(SERVERS_DIR, { withFileTypes: true })
|
|
80
|
+
.filter(e => e.isDirectory())
|
|
81
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
82
|
+
|
|
83
|
+
let total = 0;
|
|
84
|
+
let valid = 0;
|
|
85
|
+
let invalid = 0;
|
|
86
|
+
let unsigned = 0;
|
|
87
|
+
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const recipePath = join(SERVERS_DIR, entry.name, 'recipe.json');
|
|
90
|
+
if (!existsSync(recipePath)) continue;
|
|
91
|
+
|
|
92
|
+
total++;
|
|
93
|
+
const recipe = JSON.parse(readFileSync(recipePath, 'utf-8'));
|
|
94
|
+
|
|
95
|
+
if (!recipe.signature) {
|
|
96
|
+
unsigned++;
|
|
97
|
+
console.log(`⚠️ ${entry.name}: UNSIGNED`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isValid = verifySignature(recipe, recipe.signature, publicKey);
|
|
102
|
+
if (isValid) {
|
|
103
|
+
valid++;
|
|
104
|
+
console.log(`✅ ${entry.name}: valid`);
|
|
105
|
+
} else {
|
|
106
|
+
invalid++;
|
|
107
|
+
console.log(`❌ ${entry.name}: INVALID SIGNATURE`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`\n${total} recipes: ${valid} valid, ${unsigned} unsigned, ${invalid} invalid`);
|
|
112
|
+
|
|
113
|
+
if (invalid > 0) {
|
|
114
|
+
console.error(`\n🚨 ${invalid} recipe(s) have invalid signatures!`);
|
|
115
|
+
console.error(' Run: cd ../mcp-catalog && npx tsx scripts/sign-recipe.ts <recipe.json> --output <recipe.json>');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (unsigned > 0 && ciMode) {
|
|
120
|
+
console.error(`\n⚠️ ${unsigned} recipe(s) are unsigned`);
|
|
121
|
+
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"pricing": "byok",
|
|
37
37
|
"maturity": "stable",
|
|
38
38
|
"subcategory": "scraping",
|
|
39
|
-
"origin": "
|
|
39
|
+
"origin": "official",
|
|
40
40
|
"countries": [
|
|
41
41
|
"global"
|
|
42
42
|
],
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"signature": {
|
|
55
55
|
"algorithm": "ed25519",
|
|
56
56
|
"publisherId": "aiwerk",
|
|
57
|
-
"value": "
|
|
57
|
+
"value": "00uinemhL7iVz+e7EZ1mFhYwXB3SHhFeyKQ6nop2FhCqqykSuojLriDjx/sX8M+xAAG/C4QOPfL8qJCgNxq+Bw==",
|
|
58
58
|
"signedFields": [
|
|
59
59
|
"id",
|
|
60
60
|
"name",
|
|
@@ -64,6 +64,6 @@
|
|
|
64
64
|
"install",
|
|
65
65
|
"metadata"
|
|
66
66
|
],
|
|
67
|
-
"signedAt": "2026-03-
|
|
67
|
+
"signedAt": "2026-03-24T18:32:28.206Z"
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"pricing": "free",
|
|
42
42
|
"maturity": "stable",
|
|
43
43
|
"subcategory": "monitoring",
|
|
44
|
-
"origin": "
|
|
44
|
+
"origin": "official",
|
|
45
45
|
"countries": [
|
|
46
46
|
"global"
|
|
47
47
|
],
|
|
48
48
|
"audience": "developer",
|
|
49
49
|
"selfHosted": true,
|
|
50
|
-
"sideEffects": "read-
|
|
50
|
+
"sideEffects": "read-write",
|
|
51
51
|
"authSummary": "none"
|
|
52
52
|
},
|
|
53
53
|
"capabilities": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"signature": {
|
|
60
60
|
"algorithm": "ed25519",
|
|
61
61
|
"publisherId": "aiwerk",
|
|
62
|
-
"value": "
|
|
62
|
+
"value": "pjNR/z4SN6fffrfYbT8rFak6dyqqoQHu5FzHraRf3Lq1eTwEt6Gu82vo2Fa0lpbeTkrZ4wS6LDRaKQpWl2SXBg==",
|
|
63
63
|
"signedFields": [
|
|
64
64
|
"id",
|
|
65
65
|
"name",
|
|
@@ -69,6 +69,6 @@
|
|
|
69
69
|
"install",
|
|
70
70
|
"metadata"
|
|
71
71
|
],
|
|
72
|
-
"signedAt": "2026-03-
|
|
72
|
+
"signedAt": "2026-03-24T18:33:00.640Z"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"global"
|
|
52
52
|
],
|
|
53
53
|
"audience": "developer",
|
|
54
|
-
"selfHosted":
|
|
54
|
+
"selfHosted": false,
|
|
55
55
|
"sideEffects": "read-only",
|
|
56
56
|
"authSummary": "api-key"
|
|
57
57
|
},
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"signature": {
|
|
65
65
|
"algorithm": "ed25519",
|
|
66
66
|
"publisherId": "aiwerk",
|
|
67
|
-
"value": "
|
|
67
|
+
"value": "eQvGxgLGN6NVFD5KnjYWTN37ulE0rwTamMkLSHnK3Z2b8uDJz0gGIatCaN6qTCX707g75zY+COQqbpltf23NAw==",
|
|
68
68
|
"signedFields": [
|
|
69
69
|
"id",
|
|
70
70
|
"name",
|
|
@@ -74,6 +74,6 @@
|
|
|
74
74
|
"install",
|
|
75
75
|
"metadata"
|
|
76
76
|
],
|
|
77
|
-
"signedAt": "2026-03-
|
|
77
|
+
"signedAt": "2026-03-24T18:33:33.007Z"
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"id": "google-maps",
|
|
4
4
|
"name": "Google Maps",
|
|
5
|
-
"description": "Google Maps integration for places search, geocoding, directions, and distance matrix via the
|
|
5
|
+
"description": "Google Maps integration for places search, geocoding, directions, and distance matrix via the @modelcontextprotocol MCP server",
|
|
6
6
|
"repository": "https://github.com/modelcontextprotocol/servers",
|
|
7
7
|
"transports": [
|
|
8
8
|
{
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"pricing": "byok",
|
|
46
46
|
"maturity": "stable",
|
|
47
47
|
"subcategory": "search-engine",
|
|
48
|
-
"origin": "
|
|
48
|
+
"origin": "community",
|
|
49
49
|
"countries": [
|
|
50
50
|
"global"
|
|
51
51
|
],
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"signature": {
|
|
64
64
|
"algorithm": "ed25519",
|
|
65
65
|
"publisherId": "aiwerk",
|
|
66
|
-
"value": "
|
|
66
|
+
"value": "xS+v0iMoG9mJ7RJErekW+Kww/7p18SNL5VxnxGz/haObs2hqrg85CJgTvgXKqnVkdRD9OuQRu0oRxtQgAzzOCw==",
|
|
67
67
|
"signedFields": [
|
|
68
68
|
"id",
|
|
69
69
|
"name",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"install",
|
|
74
74
|
"metadata"
|
|
75
75
|
],
|
|
76
|
-
"signedAt": "2026-03-
|
|
76
|
+
"signedAt": "2026-03-24T18:34:05.383Z"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"pricing": "byok",
|
|
45
45
|
"maturity": "stable",
|
|
46
46
|
"subcategory": "cloud",
|
|
47
|
-
"origin": "
|
|
47
|
+
"origin": "official",
|
|
48
48
|
"countries": [
|
|
49
49
|
"global"
|
|
50
50
|
],
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"signature": {
|
|
64
64
|
"algorithm": "ed25519",
|
|
65
65
|
"publisherId": "aiwerk",
|
|
66
|
-
"value": "
|
|
66
|
+
"value": "7OV7oq85Z/TQkt4GzU82cAUDul+MvWRAqVaNBXY8xjRRsOwBkGTQcWxFo+8ZYbmEH+D8C0wRoragBxSonhXPBQ==",
|
|
67
67
|
"signedFields": [
|
|
68
68
|
"id",
|
|
69
69
|
"name",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"install",
|
|
74
74
|
"metadata"
|
|
75
75
|
],
|
|
76
|
-
"signedAt": "2026-03-
|
|
76
|
+
"signedAt": "2026-03-24T18:34:37.697Z"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"homepage": "https://github.com/AIWerk/mcp-server-imap",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"pricing": "free",
|
|
20
|
-
"maturity": "
|
|
20
|
+
"maturity": "stable",
|
|
21
21
|
"subcategory": "email",
|
|
22
22
|
"origin": "aiwerk",
|
|
23
23
|
"countries": [
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"signature": {
|
|
89
89
|
"algorithm": "ed25519",
|
|
90
90
|
"publisherId": "aiwerk",
|
|
91
|
-
"value": "
|
|
91
|
+
"value": "m6cRcAiimoEXyt9wl+69O5z0JeLWo9bUzgfo+/33H8WJamz9RTacywypQasBbBNDBjYn2QrXSnvEDOVJa1/JBg==",
|
|
92
92
|
"signedFields": [
|
|
93
93
|
"id",
|
|
94
94
|
"name",
|
|
@@ -98,6 +98,6 @@
|
|
|
98
98
|
"install",
|
|
99
99
|
"metadata"
|
|
100
100
|
],
|
|
101
|
-
"signedAt": "2026-03-
|
|
101
|
+
"signedAt": "2026-03-24T18:35:06.321Z"
|
|
102
102
|
}
|
|
103
103
|
}
|
package/servers/index.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"description": "Web scraping and automation platform with 3000+ ready-made actors for data extraction, browser automation, and crawling",
|
|
8
8
|
"category": "automation",
|
|
9
9
|
"subcategory": "scraping",
|
|
10
|
-
"origin": "
|
|
10
|
+
"origin": "official",
|
|
11
11
|
"countries": [
|
|
12
12
|
"global"
|
|
13
13
|
],
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"description": "Control and inspect a live Chrome browser for automation, debugging, and performance analysis via the Chrome DevTools Protocol",
|
|
58
58
|
"category": "development",
|
|
59
59
|
"subcategory": "monitoring",
|
|
60
|
-
"origin": "
|
|
60
|
+
"origin": "official",
|
|
61
61
|
"countries": [
|
|
62
62
|
"global"
|
|
63
63
|
],
|
|
@@ -104,10 +104,10 @@
|
|
|
104
104
|
},
|
|
105
105
|
"google-maps": {
|
|
106
106
|
"name": "Google Maps",
|
|
107
|
-
"description": "Google Maps integration for places search, geocoding, directions, and distance matrix via the
|
|
107
|
+
"description": "Google Maps integration for places search, geocoding, directions, and distance matrix via the @modelcontextprotocol MCP server",
|
|
108
108
|
"category": "data",
|
|
109
109
|
"subcategory": "search-engine",
|
|
110
|
-
"origin": "
|
|
110
|
+
"origin": "community",
|
|
111
111
|
"countries": [
|
|
112
112
|
"global"
|
|
113
113
|
],
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"description": "Web hosting management via Hostinger API — manage domains, hosting plans, email accounts, and website settings",
|
|
158
158
|
"category": "infrastructure",
|
|
159
159
|
"subcategory": "cloud",
|
|
160
|
-
"origin": "
|
|
160
|
+
"origin": "official",
|
|
161
161
|
"countries": [
|
|
162
162
|
"global"
|
|
163
163
|
],
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"description": "Linear project management — create, update, and search issues, projects, and teams via the Linear API",
|
|
207
207
|
"category": "productivity",
|
|
208
208
|
"subcategory": "project-management",
|
|
209
|
-
"origin": "
|
|
209
|
+
"origin": "community",
|
|
210
210
|
"countries": [
|
|
211
211
|
"global"
|
|
212
212
|
],
|
|
@@ -375,6 +375,32 @@
|
|
|
375
375
|
"maturity": "stable",
|
|
376
376
|
"toolCount": null,
|
|
377
377
|
"authSummary": "none"
|
|
378
|
+
},
|
|
379
|
+
"firecrawl": {
|
|
380
|
+
"name": "Firecrawl",
|
|
381
|
+
"description": "Web scraping, crawling, search, and structured data extraction — turn any website into clean markdown or structured data for AI agents",
|
|
382
|
+
"category": "data",
|
|
383
|
+
"subcategory": "web-scraping",
|
|
384
|
+
"origin": "official",
|
|
385
|
+
"countries": [
|
|
386
|
+
"global"
|
|
387
|
+
],
|
|
388
|
+
"tags": [
|
|
389
|
+
"scraping",
|
|
390
|
+
"crawling",
|
|
391
|
+
"web",
|
|
392
|
+
"extraction",
|
|
393
|
+
"search",
|
|
394
|
+
"markdown"
|
|
395
|
+
],
|
|
396
|
+
"authType": "api-key",
|
|
397
|
+
"transports": [
|
|
398
|
+
"stdio"
|
|
399
|
+
],
|
|
400
|
+
"installMethod": "npx",
|
|
401
|
+
"maturity": "stable",
|
|
402
|
+
"toolCount": null,
|
|
403
|
+
"authSummary": "api-key"
|
|
378
404
|
}
|
|
379
405
|
}
|
|
380
406
|
}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"pricing": "byok",
|
|
46
46
|
"maturity": "stable",
|
|
47
47
|
"subcategory": "project-management",
|
|
48
|
-
"origin": "
|
|
48
|
+
"origin": "community",
|
|
49
49
|
"countries": [
|
|
50
50
|
"global"
|
|
51
51
|
],
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"signature": {
|
|
64
64
|
"algorithm": "ed25519",
|
|
65
65
|
"publisherId": "aiwerk",
|
|
66
|
-
"value": "
|
|
66
|
+
"value": "qmaBgrxT3lNFEjQ4MOBeyDC0Cs7pCG487so8PU89JU6MqHUj3foZHGH40BLVcb+nIjp5N1P503LphZCQIC0jBw==",
|
|
67
67
|
"signedFields": [
|
|
68
68
|
"id",
|
|
69
69
|
"name",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"install",
|
|
74
74
|
"metadata"
|
|
75
75
|
],
|
|
76
|
-
"signedAt": "2026-03-
|
|
76
|
+
"signedAt": "2026-03-24T18:35:38.635Z"
|
|
77
77
|
}
|
|
78
78
|
}
|