@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/README.md +76 -2
- package/dist/bin/mcp-bridge.js +3 -1
- package/dist/src/catalog-client.d.ts +102 -0
- package/dist/src/catalog-client.js +194 -0
- package/dist/src/config.d.ts +32 -0
- package/dist/src/config.js +196 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +5 -1
- package/dist/src/recipe-cache.d.ts +28 -0
- package/dist/src/recipe-cache.js +64 -0
- 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 +23 -0
- 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/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
|
+
}
|
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
|
@@ -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": "
|
|
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
|
}
|