@aiwerk/mcp-bridge 2.7.6 → 2.8.2

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/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 } 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";
@@ -30,3 +30,7 @@ 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";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * RecipeCache — local file cache for downloaded catalog recipes.
3
+ *
4
+ * Default cache dir: ~/.mcp-bridge/recipes/
5
+ * Each recipe is stored as recipes/<name>/recipe.json with metadata.
6
+ */
7
+ import type { CatalogRecipe } from "./catalog-client.js";
8
+ export interface CachedRecipe {
9
+ recipe: CatalogRecipe;
10
+ downloadedAt: string;
11
+ catalogVersion?: string;
12
+ }
13
+ export declare class RecipeCache {
14
+ private cacheDir;
15
+ constructor(cacheDir?: string);
16
+ private recipePath;
17
+ private ensureDir;
18
+ /** Get a cached recipe by name, or undefined if not cached. */
19
+ get(name: string): CachedRecipe | undefined;
20
+ /** Cache a recipe. */
21
+ put(name: string, recipe: CatalogRecipe, catalogVersion?: string): void;
22
+ /** List all cached recipe names. */
23
+ list(): string[];
24
+ /** Check if a recipe is cached. */
25
+ has(name: string): boolean;
26
+ /** Remove all cached recipes. */
27
+ clear(): void;
28
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * RecipeCache — local file cache for downloaded catalog recipes.
3
+ *
4
+ * Default cache dir: ~/.mcp-bridge/recipes/
5
+ * Each recipe is stored as recipes/<name>/recipe.json with metadata.
6
+ */
7
+ import { existsSync, mkdirSync, readdirSync, rmSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ export class RecipeCache {
11
+ cacheDir;
12
+ constructor(cacheDir) {
13
+ this.cacheDir = cacheDir ?? join(homedir(), ".mcp-bridge", "recipes");
14
+ }
15
+ recipePath(name) {
16
+ return join(this.cacheDir, name, "recipe.json");
17
+ }
18
+ ensureDir(name) {
19
+ const dir = join(this.cacheDir, name);
20
+ if (!existsSync(dir)) {
21
+ mkdirSync(dir, { recursive: true });
22
+ }
23
+ }
24
+ /** Get a cached recipe by name, or undefined if not cached. */
25
+ get(name) {
26
+ const path = this.recipePath(name);
27
+ if (!existsSync(path))
28
+ return undefined;
29
+ try {
30
+ return JSON.parse(readFileSync(path, "utf-8"));
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ /** Cache a recipe. */
37
+ put(name, recipe, catalogVersion) {
38
+ this.ensureDir(name);
39
+ const entry = {
40
+ recipe,
41
+ downloadedAt: new Date().toISOString(),
42
+ catalogVersion,
43
+ };
44
+ writeFileSync(this.recipePath(name), JSON.stringify(entry, null, 2), "utf-8");
45
+ }
46
+ /** List all cached recipe names. */
47
+ list() {
48
+ if (!existsSync(this.cacheDir))
49
+ return [];
50
+ return readdirSync(this.cacheDir, { withFileTypes: true })
51
+ .filter((d) => d.isDirectory() && existsSync(join(this.cacheDir, d.name, "recipe.json")))
52
+ .map((d) => d.name);
53
+ }
54
+ /** Check if a recipe is cached. */
55
+ has(name) {
56
+ return existsSync(this.recipePath(name));
57
+ }
58
+ /** Remove all cached recipes. */
59
+ clear() {
60
+ if (existsSync(this.cacheDir)) {
61
+ rmSync(this.cacheDir, { recursive: true, force: true });
62
+ }
63
+ }
64
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.7.6",
3
+ "version": "2.8.2",
4
4
  "description": "Standalone MCP server that multiplexes multiple MCP servers into one interface",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -49,6 +49,29 @@ while [[ $# -gt 0 ]]; do
49
49
  done
50
50
 
51
51
  SERVER_DIR="$SCRIPT_DIR/../servers/$SERVER_NAME"
52
+
53
+ # v2.8.0: Try catalog first, fall back to bundled servers/
54
+ RECIPE_CACHE_DIR="$HOME/.mcp-bridge/recipes/$SERVER_NAME"
55
+ if [[ -f "$RECIPE_CACHE_DIR/recipe.json" ]]; then
56
+ echo "[mcp-bridge] Using cached catalog recipe for $SERVER_NAME"
57
+ SERVER_DIR="$RECIPE_CACHE_DIR"
58
+ elif command -v curl &>/dev/null; then
59
+ echo "[mcp-bridge] Fetching recipe from catalog..."
60
+ mkdir -p "$RECIPE_CACHE_DIR"
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
74
+
52
75
  if [[ ! -d "$SERVER_DIR" ]]; then
53
76
  echo "Error: Server '$SERVER_NAME' not found."
54
77
  usage
@@ -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": "community",
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": "2eHbBy6v0MMBTdnIusU9oNReuNjg1CKKA7iknkS7FxQNTuisjZFAW2x5DFjUgN9FqjWmlwwhSLnk/qhwfV42Cw==",
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-20T14:24:25.196Z"
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": "community",
44
+ "origin": "official",
45
45
  "countries": [
46
46
  "global"
47
47
  ],
48
48
  "audience": "developer",
49
49
  "selfHosted": true,
50
- "sideEffects": "read-only",
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": "fo/w/oYgd8T4ecaOnSUq3vE5nl82RVB31Mj+2v5W/j6qM+wK17xstVMdiZuiqH+a6YDQXXe30S9enRjLK0y/CA==",
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-20T14:24:32.876Z"
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": true,
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": "s2daVrlfMhaE5+Tk/Y0CnXtG/S6qIVzP0/iHobQ4edC2RmfZFOfiaBUfOL+p6745VSoiiO8nMLDhuiRrUGQrDQ==",
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-20T14:24:35.540Z"
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 official MCP server",
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": "official",
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": "nc+tSwZmwP+MHs9z8VtyL5ZNf/iAsItoHdlygRezoZ5TjR2H++3nOlMmvp5JKC+Fq61P3iYFDRXTM0/VO6a3DA==",
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-20T14:24:43.129Z"
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": "community",
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": "Btxn49bVCENtq5hgKDhSvYjU8KtbcxFKIAjclBfxl2W87YDFH68q2vkXzkqd1B9QHE74P0djp8KWt5hR0m7lBA==",
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-20T14:25:53.457Z"
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": "beta",
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": "IJQAle97X6wb3x+b6tPne74fwOUhif34GLkd4QII9ziwPyYI7r4Tv6eiV8gBc06d+H2mxMcqn/LE7qxMrxpDDg==",
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-20T15:15:39.408Z"
101
+ "signedAt": "2026-03-24T18:35:06.321Z"
102
102
  }
103
103
  }
@@ -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": "community",
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": "community",
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 official MCP server",
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": "official",
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": "community",
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": "official",
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
  }