@heyclaude/mcp 0.1.2 → 0.3.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/CHANGELOG.md +25 -0
- package/README.md +71 -11
- package/package.json +1 -1
- package/scripts/validate-endpoint.mjs +236 -12
- package/src/package-metadata.js +4 -7
- package/src/registry.d.ts +139 -0
- package/src/registry.js +2154 -61
- package/src/remote-proxy.d.ts +20 -0
- package/src/remote-proxy.js +155 -42
- package/src/schemas.d.ts +15 -0
- package/src/schemas.js +181 -1
- package/src/server.js +40 -1
- package/src/submissions.d.ts +14 -2
- package/src/submissions.js +464 -67
package/src/registry.js
CHANGED
|
@@ -7,25 +7,50 @@ import {
|
|
|
7
7
|
platformFeedSlug,
|
|
8
8
|
SITE_URL,
|
|
9
9
|
} from "./platforms.js";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_REMOTE_MCP_URL,
|
|
12
|
+
normalizeEndpointUrl,
|
|
13
|
+
} from "./endpoint-url.js";
|
|
14
|
+
import { packageName, packageVersion } from "./package-metadata.js";
|
|
10
15
|
import {
|
|
11
16
|
formatZodError,
|
|
12
17
|
jsonSchemaForTool,
|
|
18
|
+
jsonSchemaForToolOutput,
|
|
13
19
|
parseToolArguments,
|
|
14
20
|
} from "./schemas.js";
|
|
15
21
|
import {
|
|
16
22
|
buildSubmissionUrlsFromSpec,
|
|
23
|
+
getSubmissionExamplesFromSpec,
|
|
17
24
|
getCategorySubmissionGuidanceFromSpec,
|
|
25
|
+
prepareSubmissionDraftFromSpec,
|
|
18
26
|
getSubmissionSchemaFromSpec,
|
|
27
|
+
reviewSubmissionDraftFromSpec,
|
|
19
28
|
searchDuplicateEntries,
|
|
20
29
|
validateSubmissionDraftFromSpec,
|
|
21
30
|
} from "./submissions.js";
|
|
22
31
|
|
|
23
|
-
const repoRoot = path.resolve(
|
|
24
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
25
|
-
"../../..",
|
|
26
|
-
);
|
|
27
|
-
const defaultDataDir = path.join(repoRoot, "apps", "web", "public", "data");
|
|
28
32
|
const safePathPartPattern = /^[a-z0-9-]+$/;
|
|
33
|
+
const jsonMimeType = "application/json";
|
|
34
|
+
const DISCOVERY_RESOURCE_LIMIT = 25;
|
|
35
|
+
const DISCOVERY_FETCH_TIMEOUT_MS = 5000;
|
|
36
|
+
|
|
37
|
+
function entryCanonicalUrl(entry) {
|
|
38
|
+
return (
|
|
39
|
+
entry.canonicalUrl ||
|
|
40
|
+
entry.url ||
|
|
41
|
+
`${SITE_URL}/entry/${entry.category}/${entry.slug}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const MCP_PUBLIC_POLICY = {
|
|
46
|
+
apiKeyRequired: false,
|
|
47
|
+
readOnly: true,
|
|
48
|
+
createsIssues: false,
|
|
49
|
+
createsPullRequests: false,
|
|
50
|
+
publishesContent: false,
|
|
51
|
+
writesLocalFiles: false,
|
|
52
|
+
note: "HeyClaude MCP tools only read public registry artifacts or prepare maintainer-reviewed submission drafts.",
|
|
53
|
+
};
|
|
29
54
|
|
|
30
55
|
const platformAliases = new Map([
|
|
31
56
|
["claude", "Claude"],
|
|
@@ -43,7 +68,16 @@ const platformAliases = new Map([
|
|
|
43
68
|
|
|
44
69
|
export const READ_ONLY_TOOL_NAMES = [
|
|
45
70
|
"search_registry",
|
|
71
|
+
"plan_workflow_toolbox",
|
|
72
|
+
"server_info",
|
|
73
|
+
"list_category_entries",
|
|
74
|
+
"get_recent_updates",
|
|
75
|
+
"get_related_entries",
|
|
46
76
|
"get_entry_detail",
|
|
77
|
+
"get_copyable_asset",
|
|
78
|
+
"compare_entries",
|
|
79
|
+
"get_registry_stats",
|
|
80
|
+
"get_client_setup",
|
|
47
81
|
"get_compatibility",
|
|
48
82
|
"get_install_guidance",
|
|
49
83
|
"get_platform_adapter",
|
|
@@ -53,6 +87,12 @@ export const READ_ONLY_TOOL_NAMES = [
|
|
|
53
87
|
"search_duplicate_entries",
|
|
54
88
|
"build_submission_urls",
|
|
55
89
|
"get_category_submission_guidance",
|
|
90
|
+
"prepare_submission_draft",
|
|
91
|
+
"get_submission_examples",
|
|
92
|
+
"review_submission_draft",
|
|
93
|
+
"get_submission_policy",
|
|
94
|
+
"explain_entry_trust",
|
|
95
|
+
"review_entry_safety",
|
|
56
96
|
];
|
|
57
97
|
|
|
58
98
|
export const TOOL_DEFINITIONS = [
|
|
@@ -61,6 +101,50 @@ export const TOOL_DEFINITIONS = [
|
|
|
61
101
|
description:
|
|
62
102
|
"Search read-only HeyClaude registry entries by query, category, and skill platform compatibility.",
|
|
63
103
|
inputSchema: jsonSchemaForTool("search_registry"),
|
|
104
|
+
outputSchema: jsonSchemaForToolOutput("search_registry"),
|
|
105
|
+
annotations: {
|
|
106
|
+
readOnlyHint: true,
|
|
107
|
+
destructiveHint: false,
|
|
108
|
+
idempotentHint: true,
|
|
109
|
+
openWorldHint: false,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "plan_workflow_toolbox",
|
|
114
|
+
description:
|
|
115
|
+
"Plan a read-only Claude or Codex workflow toolbox from ranked HeyClaude registry entries with trust, install, and follow-up guidance.",
|
|
116
|
+
inputSchema: jsonSchemaForTool("plan_workflow_toolbox"),
|
|
117
|
+
outputSchema: jsonSchemaForToolOutput("plan_workflow_toolbox"),
|
|
118
|
+
annotations: {
|
|
119
|
+
readOnlyHint: true,
|
|
120
|
+
destructiveHint: false,
|
|
121
|
+
idempotentHint: true,
|
|
122
|
+
openWorldHint: false,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "server_info",
|
|
127
|
+
description:
|
|
128
|
+
"Fetch read-only HeyClaude MCP package, registry, tool, and public rate-limit metadata.",
|
|
129
|
+
inputSchema: jsonSchemaForTool("server_info"),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "list_category_entries",
|
|
133
|
+
description:
|
|
134
|
+
"List read-only HeyClaude entries with bounded pagination and optional category, platform, tag, and query filters.",
|
|
135
|
+
inputSchema: jsonSchemaForTool("list_category_entries"),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "get_recent_updates",
|
|
139
|
+
description:
|
|
140
|
+
"List recently added or upstream-updated HeyClaude entries from generated registry metadata.",
|
|
141
|
+
inputSchema: jsonSchemaForTool("get_recent_updates"),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "get_related_entries",
|
|
145
|
+
description:
|
|
146
|
+
"Fetch read-only related HeyClaude entries based on category, tags, platforms, keywords, and source metadata.",
|
|
147
|
+
inputSchema: jsonSchemaForTool("get_related_entries"),
|
|
64
148
|
},
|
|
65
149
|
{
|
|
66
150
|
name: "get_entry_detail",
|
|
@@ -68,6 +152,30 @@ export const TOOL_DEFINITIONS = [
|
|
|
68
152
|
"Fetch a read-only HeyClaude registry entry detail payload by category and slug.",
|
|
69
153
|
inputSchema: jsonSchemaForTool("get_entry_detail"),
|
|
70
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: "get_copyable_asset",
|
|
157
|
+
description:
|
|
158
|
+
"Fetch the category-aware copy/install asset for a HeyClaude entry without writing local files.",
|
|
159
|
+
inputSchema: jsonSchemaForTool("get_copyable_asset"),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "compare_entries",
|
|
163
|
+
description:
|
|
164
|
+
"Compare 2-5 read-only HeyClaude entries by fit, category, platforms, source metadata, and install complexity.",
|
|
165
|
+
inputSchema: jsonSchemaForTool("compare_entries"),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "get_registry_stats",
|
|
169
|
+
description:
|
|
170
|
+
"Fetch aggregate read-only registry stats, freshness, category counts, and real source-signal coverage.",
|
|
171
|
+
inputSchema: jsonSchemaForTool("get_registry_stats"),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "get_client_setup",
|
|
175
|
+
description:
|
|
176
|
+
"Fetch read-only MCP client setup snippets for Codex, Claude Desktop, Cursor, Windsurf, or remote HTTP clients.",
|
|
177
|
+
inputSchema: jsonSchemaForTool("get_client_setup"),
|
|
178
|
+
},
|
|
71
179
|
{
|
|
72
180
|
name: "get_compatibility",
|
|
73
181
|
description:
|
|
@@ -95,25 +203,25 @@ export const TOOL_DEFINITIONS = [
|
|
|
95
203
|
{
|
|
96
204
|
name: "get_submission_schema",
|
|
97
205
|
description:
|
|
98
|
-
"Fetch read-only HeyClaude submission schemas
|
|
206
|
+
"Fetch read-only HeyClaude submission schemas for PR-first intake by category.",
|
|
99
207
|
inputSchema: jsonSchemaForTool("get_submission_schema"),
|
|
100
208
|
},
|
|
101
209
|
{
|
|
102
210
|
name: "validate_submission_draft",
|
|
103
211
|
description:
|
|
104
|
-
"Validate a HeyClaude content submission draft locally without creating GitHub issues or publishing content.",
|
|
212
|
+
"Validate a HeyClaude content submission draft locally without creating GitHub issues, pull requests, or publishing content.",
|
|
105
213
|
inputSchema: jsonSchemaForTool("validate_submission_draft"),
|
|
106
214
|
},
|
|
107
215
|
{
|
|
108
216
|
name: "search_duplicate_entries",
|
|
109
217
|
description:
|
|
110
|
-
"Search generated registry artifacts for likely duplicate entries before a user opens a submission
|
|
218
|
+
"Search generated registry artifacts for likely duplicate entries before a user opens a submission PR.",
|
|
111
219
|
inputSchema: jsonSchemaForTool("search_duplicate_entries"),
|
|
112
220
|
},
|
|
113
221
|
{
|
|
114
222
|
name: "build_submission_urls",
|
|
115
223
|
description:
|
|
116
|
-
"Build prefilled HeyClaude submit and
|
|
224
|
+
"Build prefilled HeyClaude submit and review URLs for a validated PR-first submission draft without making write calls.",
|
|
117
225
|
inputSchema: jsonSchemaForTool("build_submission_urls"),
|
|
118
226
|
},
|
|
119
227
|
{
|
|
@@ -122,10 +230,73 @@ export const TOOL_DEFINITIONS = [
|
|
|
122
230
|
"Fetch category-specific HeyClaude contribution guidance, required fields, and review expectations.",
|
|
123
231
|
inputSchema: jsonSchemaForTool("get_category_submission_guidance"),
|
|
124
232
|
},
|
|
233
|
+
{
|
|
234
|
+
name: "prepare_submission_draft",
|
|
235
|
+
description:
|
|
236
|
+
"Build a read-only maintainer-reviewed HeyClaude submission draft with canonical PR text and URLs.",
|
|
237
|
+
inputSchema: jsonSchemaForTool("prepare_submission_draft"),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "get_submission_examples",
|
|
241
|
+
description:
|
|
242
|
+
"Fetch read-only category examples and templates for faster, more accurate HeyClaude submissions.",
|
|
243
|
+
inputSchema: jsonSchemaForTool("get_submission_examples"),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "review_submission_draft",
|
|
247
|
+
description:
|
|
248
|
+
"Review a HeyClaude submission draft locally for schema errors, duplicate risk, and maintainer checklist items without writing to GitHub.",
|
|
249
|
+
inputSchema: jsonSchemaForTool("review_submission_draft"),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "get_submission_policy",
|
|
253
|
+
description:
|
|
254
|
+
"Fetch HeyClaude's read-only submission, artifact, import, and maintainer-review policy for contributors and agents.",
|
|
255
|
+
inputSchema: jsonSchemaForTool("get_submission_policy"),
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "explain_entry_trust",
|
|
259
|
+
description:
|
|
260
|
+
"Explain deterministic trust, source, package, safety, privacy, and review metadata signals for one HeyClaude entry. This is a metadata review only and does not provide malware scanning, automatic safety guarantees, or installation approval.",
|
|
261
|
+
inputSchema: jsonSchemaForTool("explain_entry_trust"),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "review_entry_safety",
|
|
265
|
+
description:
|
|
266
|
+
"Review 1-5 HeyClaude entries for source, package, safety, and privacy metadata fit before install or recommendation. This is a metadata review only and does not provide malware scanning, automatic safety guarantees, or installation approval.",
|
|
267
|
+
inputSchema: jsonSchemaForTool("review_entry_safety"),
|
|
268
|
+
},
|
|
125
269
|
];
|
|
126
270
|
|
|
271
|
+
for (const tool of TOOL_DEFINITIONS) {
|
|
272
|
+
tool.outputSchema ||= jsonSchemaForToolOutput(tool.name);
|
|
273
|
+
tool.annotations ||= {
|
|
274
|
+
readOnlyHint: true,
|
|
275
|
+
destructiveHint: false,
|
|
276
|
+
idempotentHint: true,
|
|
277
|
+
openWorldHint: false,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
127
281
|
function dataDirFromOptions(options = {}) {
|
|
128
|
-
|
|
282
|
+
const envDataDir =
|
|
283
|
+
typeof process !== "undefined" ? process.env?.HEYCLAUDE_DATA_DIR : "";
|
|
284
|
+
if (options.dataDir || envDataDir) {
|
|
285
|
+
return options.dataDir || envDataDir;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const moduleUrl = import.meta.url;
|
|
289
|
+
if (!moduleUrl) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"HEYCLAUDE_DATA_DIR or readTextArtifact is required outside the Node package runtime.",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const repoRoot = path.resolve(
|
|
296
|
+
path.dirname(fileURLToPath(moduleUrl)),
|
|
297
|
+
"../../..",
|
|
298
|
+
);
|
|
299
|
+
return path.join(repoRoot, "apps", "web", "public", "data");
|
|
129
300
|
}
|
|
130
301
|
|
|
131
302
|
function isSafePathPart(value) {
|
|
@@ -180,6 +351,12 @@ function normalizeLimit(value, fallback = 10) {
|
|
|
180
351
|
return Math.max(1, Math.min(25, Math.trunc(numeric)));
|
|
181
352
|
}
|
|
182
353
|
|
|
354
|
+
function normalizeOffset(value) {
|
|
355
|
+
const numeric = Number(value);
|
|
356
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
357
|
+
return Math.max(0, Math.min(5000, Math.trunc(numeric)));
|
|
358
|
+
}
|
|
359
|
+
|
|
183
360
|
function normalizePlatform(value) {
|
|
184
361
|
const normalized = normalizeText(value).replace(/[^a-z0-9]+/g, "-");
|
|
185
362
|
if (!normalized) return "";
|
|
@@ -198,6 +375,8 @@ function entryMatchesQuery(entry, query) {
|
|
|
198
375
|
entry.submittedBy,
|
|
199
376
|
entry.brandName,
|
|
200
377
|
entry.brandDomain,
|
|
378
|
+
...notes(entry.safetyNotes),
|
|
379
|
+
...notes(entry.privacyNotes),
|
|
201
380
|
...(entry.tags || []),
|
|
202
381
|
...(entry.keywords || []),
|
|
203
382
|
]
|
|
@@ -206,12 +385,212 @@ function entryMatchesQuery(entry, query) {
|
|
|
206
385
|
return haystack.includes(query);
|
|
207
386
|
}
|
|
208
387
|
|
|
388
|
+
function searchTokens(query) {
|
|
389
|
+
return normalizeText(query)
|
|
390
|
+
.split(/[^a-z0-9+#.-]+/i)
|
|
391
|
+
.map((token) => token.trim())
|
|
392
|
+
.filter((token) => token.length >= 2)
|
|
393
|
+
.slice(0, 12);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function entrySearchText(entry) {
|
|
397
|
+
return [
|
|
398
|
+
entry.title,
|
|
399
|
+
entry.description,
|
|
400
|
+
entry.cardDescription,
|
|
401
|
+
entry.category,
|
|
402
|
+
entry.slug,
|
|
403
|
+
entry.author,
|
|
404
|
+
entry.submittedBy,
|
|
405
|
+
entry.brandName,
|
|
406
|
+
entry.brandDomain,
|
|
407
|
+
...notes(entry.safetyNotes),
|
|
408
|
+
...notes(entry.privacyNotes),
|
|
409
|
+
...(entry.tags || []),
|
|
410
|
+
...(entry.keywords || []),
|
|
411
|
+
]
|
|
412
|
+
.map(normalizeText)
|
|
413
|
+
.join(" ")
|
|
414
|
+
.toLowerCase();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function scoreSearchEntry(entry, query) {
|
|
418
|
+
const normalizedQuery = normalizeText(query);
|
|
419
|
+
const tokens = searchTokens(normalizedQuery);
|
|
420
|
+
if (!tokens.length) return { score: 0, reasons: [] };
|
|
421
|
+
|
|
422
|
+
const title = normalizeText(entry.title);
|
|
423
|
+
const category = normalizeText(entry.category);
|
|
424
|
+
const tags = new Set((entry.tags || []).map(normalizeText));
|
|
425
|
+
const keywords = new Set((entry.keywords || []).map(normalizeText));
|
|
426
|
+
const haystack = entrySearchText(entry);
|
|
427
|
+
const reasons = new Set();
|
|
428
|
+
let score = 0;
|
|
429
|
+
|
|
430
|
+
if (title.includes(normalizedQuery)) {
|
|
431
|
+
score += 90;
|
|
432
|
+
reasons.add("title phrase");
|
|
433
|
+
}
|
|
434
|
+
if (category === normalizedQuery) {
|
|
435
|
+
score += 45;
|
|
436
|
+
reasons.add("category match");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const token of tokens) {
|
|
440
|
+
if (title.includes(token)) {
|
|
441
|
+
score += 35;
|
|
442
|
+
reasons.add("title term");
|
|
443
|
+
}
|
|
444
|
+
if (tags.has(token)) {
|
|
445
|
+
score += 24;
|
|
446
|
+
reasons.add("tag match");
|
|
447
|
+
}
|
|
448
|
+
if (keywords.has(token)) {
|
|
449
|
+
score += 18;
|
|
450
|
+
reasons.add("keyword match");
|
|
451
|
+
}
|
|
452
|
+
if (category.includes(token)) {
|
|
453
|
+
score += 12;
|
|
454
|
+
reasons.add("category term");
|
|
455
|
+
}
|
|
456
|
+
if (haystack.includes(token)) score += 4;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (entrySourceStatus(entry) === "available") {
|
|
460
|
+
score += 8;
|
|
461
|
+
reasons.add("source-backed");
|
|
462
|
+
}
|
|
463
|
+
if (
|
|
464
|
+
entryPackageTrust(entry) === "first-party" ||
|
|
465
|
+
entry.packageVerified ||
|
|
466
|
+
entry.trustSignals?.packageVerified
|
|
467
|
+
) {
|
|
468
|
+
score += 8;
|
|
469
|
+
reasons.add("trusted package");
|
|
470
|
+
}
|
|
471
|
+
if (notes(entry.safetyNotes).length) {
|
|
472
|
+
score += 4;
|
|
473
|
+
reasons.add("safety notes");
|
|
474
|
+
}
|
|
475
|
+
if (notes(entry.privacyNotes).length) {
|
|
476
|
+
score += 4;
|
|
477
|
+
reasons.add("privacy notes");
|
|
478
|
+
}
|
|
479
|
+
if (entry.claimStatus === "verified" || entry.reviewedBy) {
|
|
480
|
+
score += 4;
|
|
481
|
+
reasons.add("reviewed");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { score, reasons: [...reasons].slice(0, 6) };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function rankSearchEntries(entries, query) {
|
|
488
|
+
return entries
|
|
489
|
+
.map((entry, index) => ({
|
|
490
|
+
entry,
|
|
491
|
+
index,
|
|
492
|
+
...scoreSearchEntry(entry, query),
|
|
493
|
+
}))
|
|
494
|
+
.sort((left, right) => {
|
|
495
|
+
if (left.score !== right.score) return right.score - left.score;
|
|
496
|
+
const dateCompare = String(right.entry.dateAdded || "").localeCompare(
|
|
497
|
+
String(left.entry.dateAdded || ""),
|
|
498
|
+
);
|
|
499
|
+
if (dateCompare !== 0) return dateCompare;
|
|
500
|
+
return left.index - right.index;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
209
504
|
function entryMatchesPlatform(entry, platform) {
|
|
210
505
|
if (!platform) return true;
|
|
211
506
|
return (entry.platforms || []).some((candidate) => candidate === platform);
|
|
212
507
|
}
|
|
213
508
|
|
|
214
|
-
function
|
|
509
|
+
function entryMatchesTag(entry, tag) {
|
|
510
|
+
if (!tag) return true;
|
|
511
|
+
return (entry.tags || []).some(
|
|
512
|
+
(candidate) => normalizeText(candidate) === tag,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function booleanFilterMatches(value, filter = "all") {
|
|
517
|
+
if (!filter || filter === "all") return true;
|
|
518
|
+
return filter === "true" ? Boolean(value) : !value;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function entryPackageTrust(entry) {
|
|
522
|
+
return entry.downloadTrust || (entry.downloadUrl ? "external" : "none");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function entryClaimStatus(entry) {
|
|
526
|
+
return entry.claimStatus || "unclaimed";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function entrySourceStatus(entry) {
|
|
530
|
+
const sourceUrls = [
|
|
531
|
+
entry.documentationUrl,
|
|
532
|
+
entry.repoUrl,
|
|
533
|
+
entry.githubUrl,
|
|
534
|
+
entry.sourceUrl,
|
|
535
|
+
].filter((value) => String(value || "").trim());
|
|
536
|
+
return (
|
|
537
|
+
entry.trustSignals?.sourceStatus ||
|
|
538
|
+
(sourceUrls.length ? "available" : "missing")
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function entryMatchesTrustFilters(entry, args = {}) {
|
|
543
|
+
if (
|
|
544
|
+
!booleanFilterMatches(
|
|
545
|
+
notes(entry.safetyNotes).length > 0,
|
|
546
|
+
args.hasSafetyNotes,
|
|
547
|
+
)
|
|
548
|
+
) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if (
|
|
552
|
+
!booleanFilterMatches(
|
|
553
|
+
notes(entry.privacyNotes).length > 0,
|
|
554
|
+
args.hasPrivacyNotes,
|
|
555
|
+
)
|
|
556
|
+
) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
if (
|
|
560
|
+
args.downloadTrust &&
|
|
561
|
+
args.downloadTrust !== "all" &&
|
|
562
|
+
entryPackageTrust(entry) !== args.downloadTrust
|
|
563
|
+
) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
if (
|
|
567
|
+
args.claimStatus &&
|
|
568
|
+
args.claimStatus !== "all" &&
|
|
569
|
+
entryClaimStatus(entry) !== args.claimStatus
|
|
570
|
+
) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
if (
|
|
574
|
+
args.sourceStatus &&
|
|
575
|
+
args.sourceStatus !== "all" &&
|
|
576
|
+
entrySourceStatus(entry) !== args.sourceStatus
|
|
577
|
+
) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function parsedTrustArgs(args = {}) {
|
|
584
|
+
return {
|
|
585
|
+
hasSafetyNotes: args.hasSafetyNotes || "all",
|
|
586
|
+
hasPrivacyNotes: args.hasPrivacyNotes || "all",
|
|
587
|
+
downloadTrust: args.downloadTrust || "all",
|
|
588
|
+
claimStatus: args.claimStatus || "all",
|
|
589
|
+
sourceStatus: args.sourceStatus || "all",
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function toSearchResult(entry, ranking = null) {
|
|
215
594
|
return {
|
|
216
595
|
key: `${entry.category}:${entry.slug}`,
|
|
217
596
|
category: entry.category,
|
|
@@ -224,12 +603,268 @@ function toSearchResult(entry) {
|
|
|
224
603
|
brandDomain: entry.brandDomain || "",
|
|
225
604
|
submittedBy: entry.submittedBy || "",
|
|
226
605
|
claimStatus: entry.claimStatus || "",
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
606
|
+
downloadTrust: entry.downloadTrust || null,
|
|
607
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
608
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
609
|
+
url: entry.url || entryCanonicalUrl(entry),
|
|
610
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
611
|
+
searchScore: ranking?.score ?? 0,
|
|
612
|
+
searchReasons: ranking?.reasons ?? [],
|
|
613
|
+
trust: entryTrustSummary(entry),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function toEntrySummary(entry) {
|
|
618
|
+
return {
|
|
619
|
+
...toSearchResult(entry),
|
|
620
|
+
dateAdded: entry.dateAdded || "",
|
|
621
|
+
repoUpdatedAt: entry.repoUpdatedAt || null,
|
|
622
|
+
verificationStatus: entry.verificationStatus || "",
|
|
623
|
+
installable: Boolean(entry.installable),
|
|
624
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
625
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
626
|
+
supportLevels: entry.supportLevels || [],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function entryUpdatedAt(entry) {
|
|
631
|
+
return String(
|
|
632
|
+
entry.repoUpdatedAt || entry.updatedAt || entry.dateAdded || "",
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function sourceHost(value) {
|
|
637
|
+
const text = String(value || "").trim();
|
|
638
|
+
if (!text) return "";
|
|
639
|
+
try {
|
|
640
|
+
return new URL(text).hostname.toLowerCase().replace(/^www\./, "");
|
|
641
|
+
} catch {
|
|
642
|
+
return "";
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function entrySourceHosts(entry) {
|
|
647
|
+
return [
|
|
648
|
+
entry.documentationUrl,
|
|
649
|
+
entry.repoUrl,
|
|
650
|
+
entry.url,
|
|
651
|
+
entry.canonicalUrl,
|
|
652
|
+
entry.llmsUrl,
|
|
653
|
+
entry.apiUrl,
|
|
654
|
+
]
|
|
655
|
+
.map(sourceHost)
|
|
656
|
+
.filter(Boolean);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function intersection(left = [], right = [], normalize = normalizeText) {
|
|
660
|
+
const rightValues = new Set((right || []).map(normalize).filter(Boolean));
|
|
661
|
+
return (left || [])
|
|
662
|
+
.map(normalize)
|
|
663
|
+
.filter((value, index, values) => value && values.indexOf(value) === index)
|
|
664
|
+
.filter((value) => rightValues.has(value));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function unique(values = []) {
|
|
668
|
+
return values.filter(
|
|
669
|
+
(value, index, list) => value && list.indexOf(value) === index,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function notes(values) {
|
|
674
|
+
return Array.isArray(values)
|
|
675
|
+
? values.map((value) => String(value || "").trim()).filter(Boolean)
|
|
676
|
+
: [];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function normalizeDateFloor(value) {
|
|
680
|
+
const text = String(value || "").trim();
|
|
681
|
+
if (!text) return "";
|
|
682
|
+
const timestamp = Date.parse(text);
|
|
683
|
+
if (!Number.isFinite(timestamp)) return "";
|
|
684
|
+
return new Date(timestamp).toISOString().slice(0, 10);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function withPublicPolicy(result) {
|
|
688
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
if (result.policy) return result;
|
|
692
|
+
return { ...result, policy: MCP_PUBLIC_POLICY };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function sourceSummary(entry) {
|
|
696
|
+
return {
|
|
697
|
+
repoUrl: entry.repoUrl || entry.githubUrl || "",
|
|
698
|
+
documentationUrl: entry.documentationUrl || "",
|
|
699
|
+
downloadUrl: entry.downloadUrl || "",
|
|
700
|
+
sourceHosts: unique(entrySourceHosts(entry)),
|
|
701
|
+
githubStars:
|
|
702
|
+
typeof entry.githubStars === "number" ? entry.githubStars : null,
|
|
703
|
+
githubForks:
|
|
704
|
+
typeof entry.githubForks === "number" ? entry.githubForks : null,
|
|
705
|
+
repoUpdatedAt: entry.repoUpdatedAt || null,
|
|
706
|
+
downloadTrust: entry.downloadTrust || null,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function entryTrustRecommendations(entry) {
|
|
711
|
+
const recommendations = [];
|
|
712
|
+
const safetyNotes = notes(entry.safetyNotes);
|
|
713
|
+
const privacyNotes = notes(entry.privacyNotes);
|
|
714
|
+
const packageTrust = entryPackageTrust(entry);
|
|
715
|
+
const source = sourceSummary(entry);
|
|
716
|
+
|
|
717
|
+
if (!source.repoUrl && !source.documentationUrl) {
|
|
718
|
+
recommendations.push(
|
|
719
|
+
"Verify a canonical source before relying on this entry.",
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (packageTrust === "external") {
|
|
723
|
+
recommendations.push(
|
|
724
|
+
"Review the upstream package source and checksum before installing.",
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
if (entry.downloadUrl && packageTrust !== "first-party") {
|
|
728
|
+
recommendations.push(
|
|
729
|
+
"Treat the download as external unless maintainers have rebuilt and verified it.",
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (!safetyNotes.length) {
|
|
733
|
+
recommendations.push(
|
|
734
|
+
"No structured safety notes are present; inspect commands and permissions manually.",
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
if (!privacyNotes.length) {
|
|
738
|
+
recommendations.push(
|
|
739
|
+
"No structured privacy notes are present; review file, credential, telemetry, and network behavior manually.",
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
return unique(recommendations).slice(0, 6);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function entryTrustSummary(entry) {
|
|
746
|
+
const safetyNotes = notes(entry.safetyNotes);
|
|
747
|
+
const privacyNotes = notes(entry.privacyNotes);
|
|
748
|
+
const source = sourceSummary(entry);
|
|
749
|
+
const packageTrust = entryPackageTrust(entry);
|
|
750
|
+
const claimStatus = entryClaimStatus(entry);
|
|
751
|
+
return {
|
|
752
|
+
source: {
|
|
753
|
+
status: entrySourceStatus(entry),
|
|
754
|
+
repoUrl: source.repoUrl,
|
|
755
|
+
documentationUrl: source.documentationUrl,
|
|
756
|
+
sourceHosts: source.sourceHosts,
|
|
757
|
+
githubStars: source.githubStars,
|
|
758
|
+
githubForks: source.githubForks,
|
|
759
|
+
repoUpdatedAt: source.repoUpdatedAt,
|
|
760
|
+
},
|
|
761
|
+
package: {
|
|
762
|
+
downloadUrl: source.downloadUrl,
|
|
763
|
+
downloadTrust: packageTrust,
|
|
764
|
+
packageVerified: Boolean(
|
|
765
|
+
entry.packageVerified || entry.trustSignals?.packageVerified,
|
|
766
|
+
),
|
|
767
|
+
checksum:
|
|
768
|
+
entry.checksum ||
|
|
769
|
+
entry.packageChecksum ||
|
|
770
|
+
entry.downloadSha256 ||
|
|
771
|
+
entry.skillPackage?.sha256 ||
|
|
772
|
+
"",
|
|
773
|
+
},
|
|
774
|
+
disclosures: {
|
|
775
|
+
safetyNotes,
|
|
776
|
+
privacyNotes,
|
|
777
|
+
hasSafetyNotes: safetyNotes.length > 0,
|
|
778
|
+
hasPrivacyNotes: privacyNotes.length > 0,
|
|
779
|
+
},
|
|
780
|
+
review: {
|
|
781
|
+
claimStatus,
|
|
782
|
+
reviewedBy: entry.reviewedBy || "",
|
|
783
|
+
reviewedAt: entry.reviewedAt || "",
|
|
784
|
+
submittedBy: entry.submittedBy || "",
|
|
785
|
+
sourceSubmissionUrl: entry.sourceSubmissionUrl || "",
|
|
786
|
+
},
|
|
787
|
+
recommendations: entryTrustRecommendations(entry),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function contentAsset(type, label, content, format = "markdown") {
|
|
792
|
+
const text =
|
|
793
|
+
content && typeof content === "object"
|
|
794
|
+
? JSON.stringify(content, null, 2)
|
|
795
|
+
: String(content || "").trim();
|
|
796
|
+
if (!text) return null;
|
|
797
|
+
return {
|
|
798
|
+
type,
|
|
799
|
+
label,
|
|
800
|
+
format,
|
|
801
|
+
content: text,
|
|
802
|
+
length: text.length,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function categoryPrimaryAsset(entry) {
|
|
807
|
+
const assets = [
|
|
808
|
+
contentAsset(
|
|
809
|
+
"full_content",
|
|
810
|
+
"Full usable entry content",
|
|
811
|
+
entry.fullCopyableContent || entry.copySnippet || entry.body,
|
|
812
|
+
),
|
|
813
|
+
contentAsset(
|
|
814
|
+
"install_command",
|
|
815
|
+
"Install command",
|
|
816
|
+
entry.installCommand,
|
|
817
|
+
"shell",
|
|
818
|
+
),
|
|
819
|
+
contentAsset(
|
|
820
|
+
"config_snippet",
|
|
821
|
+
"Configuration snippet",
|
|
822
|
+
entry.configSnippet,
|
|
823
|
+
"text",
|
|
824
|
+
),
|
|
825
|
+
contentAsset("script", "Script body", entry.scriptBody, "text"),
|
|
826
|
+
contentAsset(
|
|
827
|
+
"command_syntax",
|
|
828
|
+
"Command syntax",
|
|
829
|
+
entry.commandSyntax,
|
|
830
|
+
"text",
|
|
831
|
+
),
|
|
832
|
+
contentAsset("usage", "Usage snippet", entry.usageSnippet, "markdown"),
|
|
833
|
+
contentAsset("items", "Collection items", entry.items, "json"),
|
|
834
|
+
].filter(Boolean);
|
|
835
|
+
|
|
836
|
+
const preferredByCategory = {
|
|
837
|
+
agents: ["full_content", "usage"],
|
|
838
|
+
rules: ["full_content", "script", "usage"],
|
|
839
|
+
hooks: ["config_snippet", "script", "install_command", "usage"],
|
|
840
|
+
mcp: ["config_snippet", "install_command", "usage"],
|
|
841
|
+
skills: ["install_command", "full_content", "usage"],
|
|
842
|
+
statuslines: ["config_snippet", "script", "full_content", "usage"],
|
|
843
|
+
commands: ["command_syntax", "install_command", "full_content", "usage"],
|
|
844
|
+
collections: ["items", "full_content", "usage"],
|
|
845
|
+
guides: ["full_content", "usage"],
|
|
232
846
|
};
|
|
847
|
+
const preferred = preferredByCategory[entry.category] || ["full_content"];
|
|
848
|
+
return (
|
|
849
|
+
preferred
|
|
850
|
+
.map((type) => assets.find((asset) => asset.type === type))
|
|
851
|
+
.find(Boolean) ||
|
|
852
|
+
assets[0] ||
|
|
853
|
+
null
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function entryInstallComplexity(entry) {
|
|
858
|
+
const pieces = [
|
|
859
|
+
entry.installCommand,
|
|
860
|
+
entry.configSnippet,
|
|
861
|
+
entry.downloadUrl,
|
|
862
|
+
entry.prerequisites,
|
|
863
|
+
].filter((value) => String(value || "").trim());
|
|
864
|
+
if (pieces.length >= 3) return "higher";
|
|
865
|
+
if (pieces.length === 2) return "medium";
|
|
866
|
+
if (pieces.length === 1) return "low";
|
|
867
|
+
return "unknown";
|
|
233
868
|
}
|
|
234
869
|
|
|
235
870
|
async function readEntry(category, slug, options = {}) {
|
|
@@ -264,16 +899,19 @@ export async function searchRegistry(args = {}, options = {}) {
|
|
|
264
899
|
const category = normalizeText(args.category);
|
|
265
900
|
const platform = normalizePlatform(args.platform);
|
|
266
901
|
const limit = normalizeLimit(args.limit);
|
|
902
|
+
const trustFilters = parsedTrustArgs(args);
|
|
267
903
|
const searchIndex = unwrapEntries(
|
|
268
904
|
await readJsonArtifact("search-index.json", options),
|
|
269
905
|
);
|
|
270
906
|
|
|
271
|
-
const
|
|
907
|
+
const matched = searchIndex
|
|
272
908
|
.filter((entry) => !category || entry.category === category)
|
|
273
909
|
.filter((entry) => entryMatchesPlatform(entry, platform))
|
|
274
910
|
.filter((entry) => entryMatchesQuery(entry, query))
|
|
911
|
+
.filter((entry) => entryMatchesTrustFilters(entry, trustFilters));
|
|
912
|
+
const entries = rankSearchEntries(matched, query)
|
|
275
913
|
.slice(0, limit)
|
|
276
|
-
.map(toSearchResult);
|
|
914
|
+
.map((item) => toSearchResult(item.entry, item));
|
|
277
915
|
|
|
278
916
|
return {
|
|
279
917
|
ok: true,
|
|
@@ -281,56 +919,1324 @@ export async function searchRegistry(args = {}, options = {}) {
|
|
|
281
919
|
query: args.query || "",
|
|
282
920
|
category: category || "",
|
|
283
921
|
platform: platform || "",
|
|
922
|
+
filters: trustFilters,
|
|
284
923
|
entries,
|
|
285
924
|
};
|
|
286
925
|
}
|
|
287
926
|
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
927
|
+
function selectDiverseRankedEntries(ranked, limit) {
|
|
928
|
+
const selected = [];
|
|
929
|
+
const byCategory = new Map();
|
|
930
|
+
|
|
931
|
+
for (const item of ranked) {
|
|
932
|
+
const category = item.entry.category || "";
|
|
933
|
+
const current = byCategory.get(category) || 0;
|
|
934
|
+
if (current >= 2) continue;
|
|
935
|
+
selected.push(item);
|
|
936
|
+
byCategory.set(category, current + 1);
|
|
937
|
+
if (selected.length >= limit) return selected;
|
|
293
938
|
}
|
|
294
939
|
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
940
|
+
for (const item of ranked) {
|
|
941
|
+
if (selected.includes(item)) continue;
|
|
942
|
+
selected.push(item);
|
|
943
|
+
if (selected.length >= limit) return selected;
|
|
298
944
|
}
|
|
299
945
|
|
|
300
|
-
return
|
|
301
|
-
ok: true,
|
|
302
|
-
key: `${entry.category}:${entry.slug}`,
|
|
303
|
-
canonicalUrl: `${SITE_URL}/${entry.category}/${entry.slug}`,
|
|
304
|
-
entry,
|
|
305
|
-
};
|
|
946
|
+
return selected;
|
|
306
947
|
}
|
|
307
948
|
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
949
|
+
function toolboxFitReasons(entry, ranking) {
|
|
950
|
+
const reasons = [...(ranking.reasons || []).slice(0, 4)];
|
|
951
|
+
if (entry.category) {
|
|
952
|
+
reasons.push(`${entry.category} workflow surface`);
|
|
953
|
+
}
|
|
954
|
+
if (entrySourceStatus(entry) === "available") {
|
|
955
|
+
reasons.push("source-backed metadata");
|
|
956
|
+
}
|
|
957
|
+
if (
|
|
958
|
+
entryPackageTrust(entry) === "first-party" ||
|
|
959
|
+
entry.packageVerified ||
|
|
960
|
+
entry.trustSignals?.packageVerified
|
|
961
|
+
) {
|
|
962
|
+
reasons.push("first-party or verified package signal");
|
|
963
|
+
}
|
|
964
|
+
if (notes(entry.safetyNotes).length && notes(entry.privacyNotes).length) {
|
|
965
|
+
reasons.push("safety and privacy notes present");
|
|
966
|
+
} else if (notes(entry.safetyNotes).length) {
|
|
967
|
+
reasons.push("safety notes present");
|
|
968
|
+
} else if (notes(entry.privacyNotes).length) {
|
|
969
|
+
reasons.push("privacy notes present");
|
|
970
|
+
}
|
|
971
|
+
if (entry.installCommand || entry.downloadUrl || entry.configSnippet) {
|
|
972
|
+
reasons.push("actionable setup surface");
|
|
973
|
+
}
|
|
974
|
+
if ((entry.platforms || []).length) {
|
|
975
|
+
reasons.push(
|
|
976
|
+
`platform compatibility: ${(entry.platforms || []).slice(0, 3).join(", ")}`,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
if ((entry.supportLevels || []).length) {
|
|
980
|
+
reasons.push("support levels documented");
|
|
981
|
+
}
|
|
982
|
+
if (entry.claimStatus === "verified" || entry.reviewedBy) {
|
|
983
|
+
reasons.push("review/provenance metadata");
|
|
984
|
+
}
|
|
985
|
+
return unique(reasons).slice(0, 8);
|
|
986
|
+
}
|
|
312
987
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
988
|
+
function toolboxCaveats(entry) {
|
|
989
|
+
const caveats = [];
|
|
990
|
+
const packageTrust = entryPackageTrust(entry);
|
|
991
|
+
const safetyNotes = notes(entry.safetyNotes);
|
|
992
|
+
const privacyNotes = notes(entry.privacyNotes);
|
|
993
|
+
if (entrySourceStatus(entry) !== "available") {
|
|
994
|
+
caveats.push("Source metadata is missing or incomplete.");
|
|
995
|
+
}
|
|
996
|
+
if (packageTrust === "external") {
|
|
997
|
+
caveats.push("Package/download is external; verify upstream before use.");
|
|
998
|
+
}
|
|
999
|
+
if (entry.downloadUrl && !entryTrustSummary(entry).package.checksum) {
|
|
1000
|
+
caveats.push("Download checksum metadata is not present.");
|
|
1001
|
+
}
|
|
1002
|
+
if (!safetyNotes.length) {
|
|
1003
|
+
caveats.push("No structured safety notes are present.");
|
|
1004
|
+
}
|
|
1005
|
+
if (!privacyNotes.length) {
|
|
1006
|
+
caveats.push("No structured privacy notes are present.");
|
|
1007
|
+
}
|
|
1008
|
+
if (
|
|
1009
|
+
["mcp", "hooks", "commands", "skills", "statuslines"].includes(
|
|
1010
|
+
entry.category,
|
|
1011
|
+
)
|
|
1012
|
+
) {
|
|
1013
|
+
caveats.push(
|
|
1014
|
+
"Risk-bearing workflow surface; inspect commands, permissions, and data access before use.",
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
return unique(caveats).slice(0, 5);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function toolboxNextActions(entry) {
|
|
1021
|
+
return [
|
|
1022
|
+
`Inspect get_entry_detail with category=${entry.category} and slug=${entry.slug}.`,
|
|
1023
|
+
`Run explain_entry_trust with category=${entry.category} and slug=${entry.slug}; this is still metadata review only.`,
|
|
1024
|
+
"Use compare_entries with nearby candidates before recommending a final stack.",
|
|
1025
|
+
`Use get_copyable_asset with category=${entry.category} and slug=${entry.slug} only after trust review.`,
|
|
1026
|
+
];
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function toolboxCategoryMix(entries) {
|
|
1030
|
+
const counts = new Map();
|
|
1031
|
+
for (const entry of entries) {
|
|
1032
|
+
const category = entry.category || "unknown";
|
|
1033
|
+
counts.set(category, (counts.get(category) || 0) + 1);
|
|
316
1034
|
}
|
|
1035
|
+
return [...counts]
|
|
1036
|
+
.map(([category, count]) => ({ category, count }))
|
|
1037
|
+
.sort((left, right) => left.category.localeCompare(right.category));
|
|
1038
|
+
}
|
|
317
1039
|
|
|
1040
|
+
function toolboxTrustSummary(entries) {
|
|
318
1041
|
return {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1042
|
+
sourceBacked: entries.filter(
|
|
1043
|
+
(entry) => entry.trust?.source?.status === "available",
|
|
1044
|
+
).length,
|
|
1045
|
+
firstPartyOrVerifiedPackages: entries.filter(
|
|
1046
|
+
(entry) =>
|
|
1047
|
+
entry.trust?.package?.downloadTrust === "first-party" ||
|
|
1048
|
+
entry.trust?.package?.packageVerified,
|
|
1049
|
+
).length,
|
|
1050
|
+
entriesWithSafetyNotes: entries.filter(
|
|
1051
|
+
(entry) => entry.trust?.disclosures?.hasSafetyNotes,
|
|
1052
|
+
).length,
|
|
1053
|
+
entriesWithPrivacyNotes: entries.filter(
|
|
1054
|
+
(entry) => entry.trust?.disclosures?.hasPrivacyNotes,
|
|
1055
|
+
).length,
|
|
1056
|
+
externalPackages: entries.filter(
|
|
1057
|
+
(entry) => entry.trust?.package?.downloadTrust === "external",
|
|
1058
|
+
).length,
|
|
1059
|
+
missingSource: entries.filter(
|
|
1060
|
+
(entry) => entry.trust?.source?.status !== "available",
|
|
1061
|
+
).length,
|
|
324
1062
|
};
|
|
325
1063
|
}
|
|
326
1064
|
|
|
327
|
-
export async function
|
|
1065
|
+
export async function planWorkflowToolbox(args = {}, options = {}) {
|
|
1066
|
+
const goal = String(args.goal || "").trim();
|
|
1067
|
+
if (goal.length < 2) {
|
|
1068
|
+
return invalid("Planner goal must be at least 2 characters.");
|
|
1069
|
+
}
|
|
1070
|
+
const query = normalizeText(goal);
|
|
328
1071
|
const category = normalizeText(args.category);
|
|
329
|
-
const slug = normalizeText(args.slug);
|
|
330
1072
|
const platform = normalizePlatform(args.platform);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
1073
|
+
const limit = Math.min(10, normalizeLimit(args.limit, 6));
|
|
1074
|
+
const searchIndex = unwrapEntries(
|
|
1075
|
+
await readJsonArtifact("search-index.json", options),
|
|
1076
|
+
);
|
|
1077
|
+
const scoped = searchIndex
|
|
1078
|
+
.filter((entry) => !category || entry.category === category)
|
|
1079
|
+
.filter((entry) => entryMatchesPlatform(entry, platform));
|
|
1080
|
+
let matched = scoped.filter((entry) => entryMatchesQuery(entry, query));
|
|
1081
|
+
const queryTokens = searchTokens(query);
|
|
1082
|
+
if (!matched.length && queryTokens.length) {
|
|
1083
|
+
matched = scoped.filter((entry) =>
|
|
1084
|
+
queryTokens.some((token) => entrySearchText(entry).includes(token)),
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
const ranked = rankSearchEntries(matched, query);
|
|
1088
|
+
const selected = selectDiverseRankedEntries(ranked, limit).map((item) => ({
|
|
1089
|
+
...toEntrySummary(item.entry),
|
|
1090
|
+
searchScore: item.score,
|
|
1091
|
+
searchReasons: item.reasons,
|
|
1092
|
+
toolboxReasons: toolboxFitReasons(item.entry, item),
|
|
1093
|
+
caveats: toolboxCaveats(item.entry),
|
|
1094
|
+
nextActions: toolboxNextActions(item.entry),
|
|
1095
|
+
}));
|
|
1096
|
+
|
|
1097
|
+
return {
|
|
1098
|
+
ok: true,
|
|
1099
|
+
goal,
|
|
1100
|
+
category: category || "",
|
|
1101
|
+
platform: platform || "",
|
|
1102
|
+
count: selected.length,
|
|
1103
|
+
entries: selected,
|
|
1104
|
+
categoryMix: toolboxCategoryMix(selected),
|
|
1105
|
+
trustSummary: toolboxTrustSummary(selected),
|
|
1106
|
+
recommendedNextTools: [
|
|
1107
|
+
"get_entry_detail",
|
|
1108
|
+
"explain_entry_trust",
|
|
1109
|
+
"compare_entries",
|
|
1110
|
+
"get_copyable_asset",
|
|
1111
|
+
],
|
|
1112
|
+
plannerNotes: [
|
|
1113
|
+
"This planner is metadata review only; it is not install approval or malware scanning, and it does not execute or install entries.",
|
|
1114
|
+
"Recommendations are bounded and category-diverse where matching entries allow it.",
|
|
1115
|
+
"Prefer source-backed entries with safety/privacy notes for risk-bearing MCP, hooks, skills, commands, and statuslines.",
|
|
1116
|
+
"Use get_entry_detail, explain_entry_trust, compare_entries, and get_copyable_asset before relying on any entry.",
|
|
1117
|
+
],
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export async function getServerInfo(args = {}, options = {}) {
|
|
1122
|
+
const manifest = await readJsonArtifact("registry-manifest.json", options);
|
|
1123
|
+
return {
|
|
1124
|
+
ok: true,
|
|
1125
|
+
package: {
|
|
1126
|
+
name: packageName,
|
|
1127
|
+
version: packageVersion,
|
|
1128
|
+
},
|
|
1129
|
+
endpoint: {
|
|
1130
|
+
url: DEFAULT_REMOTE_MCP_URL,
|
|
1131
|
+
auth: "none",
|
|
1132
|
+
transport: "streamable-http",
|
|
1133
|
+
stdioBridge: "npx -y @heyclaude/mcp",
|
|
1134
|
+
requestBodyLimitBytes: 64 * 1024,
|
|
1135
|
+
rateLimit: {
|
|
1136
|
+
scope: "mcp-streamable",
|
|
1137
|
+
limit: 60,
|
|
1138
|
+
windowSeconds: 60,
|
|
1139
|
+
binding: "API_MCP_RATE_LIMIT",
|
|
1140
|
+
note: "Cloudflare enforces the durable production limit when the binding is available; local/dev falls back to an in-process limiter.",
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
registry: {
|
|
1144
|
+
schemaVersion: manifest.schemaVersion,
|
|
1145
|
+
generatedAt: manifest.generatedAt,
|
|
1146
|
+
totalEntries: manifest.totalEntries,
|
|
1147
|
+
categories: manifest.categories || {},
|
|
1148
|
+
},
|
|
1149
|
+
tools: READ_ONLY_TOOL_NAMES,
|
|
1150
|
+
policy: MCP_PUBLIC_POLICY,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
export async function listCategoryEntries(args = {}, options = {}) {
|
|
1155
|
+
const category = normalizeText(args.category);
|
|
1156
|
+
const platform = normalizePlatform(args.platform);
|
|
1157
|
+
const tag = normalizeText(args.tag);
|
|
1158
|
+
const query = normalizeText(args.query);
|
|
1159
|
+
const offset = normalizeOffset(args.offset);
|
|
1160
|
+
const limit = normalizeLimit(args.limit, 20);
|
|
1161
|
+
const searchIndex = unwrapEntries(
|
|
1162
|
+
await readJsonArtifact("search-index.json", options),
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
const entries = searchIndex
|
|
1166
|
+
.filter((entry) => !category || entry.category === category)
|
|
1167
|
+
.filter((entry) => entryMatchesPlatform(entry, platform))
|
|
1168
|
+
.filter((entry) => entryMatchesTag(entry, tag))
|
|
1169
|
+
.filter((entry) => entryMatchesQuery(entry, query));
|
|
1170
|
+
const page = entries.slice(offset, offset + limit).map(toEntrySummary);
|
|
1171
|
+
|
|
1172
|
+
return {
|
|
1173
|
+
ok: true,
|
|
1174
|
+
category: category || "",
|
|
1175
|
+
platform: platform || "",
|
|
1176
|
+
tag: tag || "",
|
|
1177
|
+
query: args.query || "",
|
|
1178
|
+
total: entries.length,
|
|
1179
|
+
count: page.length,
|
|
1180
|
+
offset,
|
|
1181
|
+
limit,
|
|
1182
|
+
nextOffset: offset + limit < entries.length ? offset + limit : null,
|
|
1183
|
+
entries: page,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export async function getRecentUpdates(args = {}, options = {}) {
|
|
1188
|
+
const category = normalizeText(args.category);
|
|
1189
|
+
const since = args.since ? normalizeDateFloor(args.since) : "";
|
|
1190
|
+
if (args.since && !since) {
|
|
1191
|
+
return invalid("since must be a parseable date such as 2026-05-01.");
|
|
1192
|
+
}
|
|
1193
|
+
const limit = normalizeLimit(args.limit, 10);
|
|
1194
|
+
const searchIndex = unwrapEntries(
|
|
1195
|
+
await readJsonArtifact("search-index.json", options),
|
|
1196
|
+
);
|
|
1197
|
+
const entries = searchIndex
|
|
1198
|
+
.filter((entry) => !category || entry.category === category)
|
|
1199
|
+
.filter((entry) => !since || entryUpdatedAt(entry) >= since)
|
|
1200
|
+
.slice()
|
|
1201
|
+
.sort((left, right) => {
|
|
1202
|
+
const dateCompare = entryUpdatedAt(right).localeCompare(
|
|
1203
|
+
entryUpdatedAt(left),
|
|
1204
|
+
);
|
|
1205
|
+
if (dateCompare !== 0) return dateCompare;
|
|
1206
|
+
return String(left.title || "").localeCompare(String(right.title || ""));
|
|
1207
|
+
})
|
|
1208
|
+
.slice(0, limit)
|
|
1209
|
+
.map((entry) => ({
|
|
1210
|
+
...toEntrySummary(entry),
|
|
1211
|
+
updatedAt: entryUpdatedAt(entry),
|
|
1212
|
+
updateKind: entry.repoUpdatedAt ? "upstream_update" : "added",
|
|
1213
|
+
}));
|
|
1214
|
+
|
|
1215
|
+
return {
|
|
1216
|
+
ok: true,
|
|
1217
|
+
category: category || "",
|
|
1218
|
+
since,
|
|
1219
|
+
count: entries.length,
|
|
1220
|
+
entries,
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function scoreRelatedEntry(target, candidate) {
|
|
1225
|
+
if (
|
|
1226
|
+
target.category === candidate.category &&
|
|
1227
|
+
target.slug === candidate.slug
|
|
1228
|
+
) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const sharedTags = intersection(target.tags, candidate.tags);
|
|
1233
|
+
const sharedKeywords = intersection(target.keywords, candidate.keywords);
|
|
1234
|
+
const sharedPlatforms = intersection(
|
|
1235
|
+
target.platforms,
|
|
1236
|
+
candidate.platforms,
|
|
1237
|
+
(value) => String(value || ""),
|
|
1238
|
+
);
|
|
1239
|
+
const sharedHosts = intersection(
|
|
1240
|
+
entrySourceHosts(target),
|
|
1241
|
+
entrySourceHosts(candidate),
|
|
1242
|
+
(value) => String(value || ""),
|
|
1243
|
+
);
|
|
1244
|
+
const score =
|
|
1245
|
+
(target.category === candidate.category ? 4 : 0) +
|
|
1246
|
+
sharedTags.length * 3 +
|
|
1247
|
+
Math.min(sharedKeywords.length, 6) +
|
|
1248
|
+
sharedPlatforms.length +
|
|
1249
|
+
sharedHosts.length * 2;
|
|
1250
|
+
|
|
1251
|
+
if (score <= 0) return null;
|
|
1252
|
+
return {
|
|
1253
|
+
score,
|
|
1254
|
+
reasons: [
|
|
1255
|
+
...(target.category === candidate.category ? ["same_category"] : []),
|
|
1256
|
+
...sharedTags.map((tag) => `tag:${tag}`),
|
|
1257
|
+
...sharedPlatforms.map((platform) => `platform:${platform}`),
|
|
1258
|
+
...sharedHosts.map((host) => `source:${host}`),
|
|
1259
|
+
],
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
export async function getRelatedEntries(args = {}, options = {}) {
|
|
1264
|
+
const category = normalizeText(args.category);
|
|
1265
|
+
const slug = normalizeText(args.slug);
|
|
1266
|
+
const limit = normalizeLimit(args.limit, 8);
|
|
1267
|
+
const searchIndex = unwrapEntries(
|
|
1268
|
+
await readJsonArtifact("search-index.json", options),
|
|
1269
|
+
);
|
|
1270
|
+
const target = searchIndex.find(
|
|
1271
|
+
(entry) => entry.category === category && entry.slug === slug,
|
|
1272
|
+
);
|
|
1273
|
+
if (!target) {
|
|
1274
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const graph = await readJsonArtifact("relation-graph.json", options).catch(
|
|
1278
|
+
() => null,
|
|
1279
|
+
);
|
|
1280
|
+
const graphRow = Array.isArray(graph?.entries)
|
|
1281
|
+
? graph.entries.find((entry) => entry.key === `${category}:${slug}`)
|
|
1282
|
+
: null;
|
|
1283
|
+
if (graphRow?.related?.length) {
|
|
1284
|
+
const searchByKey = new Map(
|
|
1285
|
+
searchIndex.map((entry) => [`${entry.category}:${entry.slug}`, entry]),
|
|
1286
|
+
);
|
|
1287
|
+
const entries = graphRow.related
|
|
1288
|
+
.map((relation) => {
|
|
1289
|
+
const entry = searchByKey.get(relation.key);
|
|
1290
|
+
if (!entry) return null;
|
|
1291
|
+
return {
|
|
1292
|
+
...toEntrySummary(entry),
|
|
1293
|
+
relation: relation.relation,
|
|
1294
|
+
relatedScore: relation.score,
|
|
1295
|
+
relatedReasons: relation.reasons || [],
|
|
1296
|
+
};
|
|
1297
|
+
})
|
|
1298
|
+
.filter(Boolean)
|
|
1299
|
+
.slice(0, limit);
|
|
1300
|
+
|
|
1301
|
+
return {
|
|
1302
|
+
ok: true,
|
|
1303
|
+
key: `${target.category}:${target.slug}`,
|
|
1304
|
+
relationGraph: true,
|
|
1305
|
+
count: entries.length,
|
|
1306
|
+
entries,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const entries = searchIndex
|
|
1311
|
+
.map((entry) => {
|
|
1312
|
+
const related = scoreRelatedEntry(target, entry);
|
|
1313
|
+
return related ? { entry, related } : null;
|
|
1314
|
+
})
|
|
1315
|
+
.filter(Boolean)
|
|
1316
|
+
.sort((left, right) => {
|
|
1317
|
+
const scoreCompare = right.related.score - left.related.score;
|
|
1318
|
+
if (scoreCompare !== 0) return scoreCompare;
|
|
1319
|
+
return entryUpdatedAt(right.entry).localeCompare(
|
|
1320
|
+
entryUpdatedAt(left.entry),
|
|
1321
|
+
);
|
|
1322
|
+
})
|
|
1323
|
+
.slice(0, limit)
|
|
1324
|
+
.map(({ entry, related }) => ({
|
|
1325
|
+
...toEntrySummary(entry),
|
|
1326
|
+
relatedScore: related.score,
|
|
1327
|
+
relatedReasons: related.reasons,
|
|
1328
|
+
}));
|
|
1329
|
+
|
|
1330
|
+
return {
|
|
1331
|
+
ok: true,
|
|
1332
|
+
key: `${target.category}:${target.slug}`,
|
|
1333
|
+
count: entries.length,
|
|
1334
|
+
entries,
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
export async function getEntryDetail(args = {}, options = {}) {
|
|
1339
|
+
const category = normalizeText(args.category);
|
|
1340
|
+
const slug = normalizeText(args.slug);
|
|
1341
|
+
if (!category || !slug) {
|
|
1342
|
+
return invalid("category and slug are required.");
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const entry = await readEntry(category, slug, options);
|
|
1346
|
+
if (!entry) {
|
|
1347
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return {
|
|
1351
|
+
ok: true,
|
|
1352
|
+
key: `${entry.category}:${entry.slug}`,
|
|
1353
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
1354
|
+
entry: {
|
|
1355
|
+
...entry,
|
|
1356
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
1357
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
1358
|
+
},
|
|
1359
|
+
trust: entryTrustSummary(entry),
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
export async function getCopyableAsset(args = {}, options = {}) {
|
|
1364
|
+
const category = normalizeText(args.category);
|
|
1365
|
+
const slug = normalizeText(args.slug);
|
|
1366
|
+
const platform = normalizePlatform(args.platform);
|
|
1367
|
+
if (!category || !slug) {
|
|
1368
|
+
return invalid("category and slug are required.");
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const entry = await readEntry(category, slug, options);
|
|
1372
|
+
if (!entry) {
|
|
1373
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const primary = categoryPrimaryAsset(entry);
|
|
1377
|
+
const assets = [
|
|
1378
|
+
contentAsset(
|
|
1379
|
+
"full_content",
|
|
1380
|
+
"Full usable entry content",
|
|
1381
|
+
entry.fullCopyableContent || entry.copySnippet || entry.body,
|
|
1382
|
+
),
|
|
1383
|
+
contentAsset(
|
|
1384
|
+
"install_command",
|
|
1385
|
+
"Install command",
|
|
1386
|
+
entry.installCommand,
|
|
1387
|
+
"shell",
|
|
1388
|
+
),
|
|
1389
|
+
contentAsset(
|
|
1390
|
+
"config_snippet",
|
|
1391
|
+
"Configuration snippet",
|
|
1392
|
+
entry.configSnippet,
|
|
1393
|
+
"text",
|
|
1394
|
+
),
|
|
1395
|
+
contentAsset("script", "Script body", entry.scriptBody, "text"),
|
|
1396
|
+
contentAsset(
|
|
1397
|
+
"command_syntax",
|
|
1398
|
+
"Command syntax",
|
|
1399
|
+
entry.commandSyntax,
|
|
1400
|
+
"text",
|
|
1401
|
+
),
|
|
1402
|
+
contentAsset("usage", "Usage snippet", entry.usageSnippet, "markdown"),
|
|
1403
|
+
contentAsset("items", "Collection items", entry.items, "json"),
|
|
1404
|
+
].filter(Boolean);
|
|
1405
|
+
const compatibility = buildSkillPlatformCompatibility(entry);
|
|
1406
|
+
|
|
1407
|
+
return {
|
|
1408
|
+
ok: true,
|
|
1409
|
+
key: `${entry.category}:${entry.slug}`,
|
|
1410
|
+
category: entry.category,
|
|
1411
|
+
slug: entry.slug,
|
|
1412
|
+
title: entry.title,
|
|
1413
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
1414
|
+
platform: platform || "",
|
|
1415
|
+
primaryAsset: primary,
|
|
1416
|
+
assets,
|
|
1417
|
+
installCommand: entry.installCommand || "",
|
|
1418
|
+
configSnippet: entry.configSnippet || "",
|
|
1419
|
+
usageSnippet: entry.usageSnippet || "",
|
|
1420
|
+
downloadUrl: entry.downloadUrl || "",
|
|
1421
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
1422
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
1423
|
+
platformCompatibility: compatibility,
|
|
1424
|
+
source: sourceSummary(entry),
|
|
1425
|
+
trust: entryTrustSummary(entry),
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
export async function compareEntries(args = {}, options = {}) {
|
|
1430
|
+
const platform = normalizePlatform(args.platform);
|
|
1431
|
+
const entries = [];
|
|
1432
|
+
for (const target of args.entries || []) {
|
|
1433
|
+
const category = normalizeText(target.category);
|
|
1434
|
+
const slug = normalizeText(target.slug);
|
|
1435
|
+
const entry = await readEntry(category, slug, options);
|
|
1436
|
+
if (!entry) {
|
|
1437
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
1438
|
+
}
|
|
1439
|
+
entries.push(entry);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const compared = entries.map((entry) => {
|
|
1443
|
+
const compatibility = buildSkillPlatformCompatibility(entry);
|
|
1444
|
+
const selectedCompatibility = platform
|
|
1445
|
+
? compatibility.find((item) => item.platform === platform) || null
|
|
1446
|
+
: null;
|
|
1447
|
+
return {
|
|
1448
|
+
key: `${entry.category}:${entry.slug}`,
|
|
1449
|
+
category: entry.category,
|
|
1450
|
+
slug: entry.slug,
|
|
1451
|
+
title: entry.title,
|
|
1452
|
+
description: entry.description,
|
|
1453
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
1454
|
+
tags: entry.tags || [],
|
|
1455
|
+
platforms: entry.platforms || [],
|
|
1456
|
+
selectedCompatibility,
|
|
1457
|
+
installComplexity: entryInstallComplexity(entry),
|
|
1458
|
+
copyableAssetTypes: [
|
|
1459
|
+
categoryPrimaryAsset(entry)?.type,
|
|
1460
|
+
entry.configSnippet ? "config_snippet" : "",
|
|
1461
|
+
entry.installCommand ? "install_command" : "",
|
|
1462
|
+
entry.scriptBody ? "script" : "",
|
|
1463
|
+
].filter(Boolean),
|
|
1464
|
+
source: sourceSummary(entry),
|
|
1465
|
+
trust: entryTrustSummary(entry),
|
|
1466
|
+
};
|
|
1467
|
+
});
|
|
1468
|
+
const sharedTags = compared.length
|
|
1469
|
+
? compared
|
|
1470
|
+
.slice(1)
|
|
1471
|
+
.reduce(
|
|
1472
|
+
(tags, entry) => intersection(tags, entry.tags || []),
|
|
1473
|
+
compared[0].tags || [],
|
|
1474
|
+
)
|
|
1475
|
+
: [];
|
|
1476
|
+
|
|
1477
|
+
return {
|
|
1478
|
+
ok: true,
|
|
1479
|
+
platform: platform || "",
|
|
1480
|
+
count: compared.length,
|
|
1481
|
+
sharedTags,
|
|
1482
|
+
entries: compared,
|
|
1483
|
+
comparisonNotes: [
|
|
1484
|
+
"Prefer exact category fit before source popularity.",
|
|
1485
|
+
"Treat GitHub stars/forks as source signals only when present; absence is not a negative ranking.",
|
|
1486
|
+
"Install complexity is derived from available install/config/download/prerequisite metadata.",
|
|
1487
|
+
"Safety/privacy notes are disclosure metadata, not a malware verdict.",
|
|
1488
|
+
],
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
export async function getRegistryStats(args = {}, options = {}) {
|
|
1493
|
+
const [manifest, searchIndexPayload] = await Promise.all([
|
|
1494
|
+
readJsonArtifact("registry-manifest.json", options),
|
|
1495
|
+
readJsonArtifact("search-index.json", options),
|
|
1496
|
+
]);
|
|
1497
|
+
const entries = unwrapEntries(searchIndexPayload);
|
|
1498
|
+
const platformCounts = new Map();
|
|
1499
|
+
const tagCounts = new Map();
|
|
1500
|
+
for (const entry of entries) {
|
|
1501
|
+
for (const platform of entry.platforms || []) {
|
|
1502
|
+
platformCounts.set(platform, (platformCounts.get(platform) || 0) + 1);
|
|
1503
|
+
}
|
|
1504
|
+
for (const tag of entry.tags || []) {
|
|
1505
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return {
|
|
1510
|
+
ok: true,
|
|
1511
|
+
package: {
|
|
1512
|
+
name: packageName,
|
|
1513
|
+
version: packageVersion,
|
|
1514
|
+
},
|
|
1515
|
+
registry: {
|
|
1516
|
+
schemaVersion: manifest.schemaVersion,
|
|
1517
|
+
generatedAt: manifest.generatedAt,
|
|
1518
|
+
totalEntries: manifest.totalEntries,
|
|
1519
|
+
categories: manifest.categories || {},
|
|
1520
|
+
},
|
|
1521
|
+
freshness: {
|
|
1522
|
+
entriesWithRepoUpdatedAt: entries.filter((entry) => entry.repoUpdatedAt)
|
|
1523
|
+
.length,
|
|
1524
|
+
entriesAddedLast30Days: entries.filter((entry) => {
|
|
1525
|
+
const added = Date.parse(entry.dateAdded || "");
|
|
1526
|
+
return (
|
|
1527
|
+
Number.isFinite(added) &&
|
|
1528
|
+
Date.now() - added <= 30 * 24 * 60 * 60 * 1000
|
|
1529
|
+
);
|
|
1530
|
+
}).length,
|
|
1531
|
+
},
|
|
1532
|
+
sourceSignals: {
|
|
1533
|
+
entriesWithGithubStats: entries.filter(
|
|
1534
|
+
(entry) => typeof entry.githubStars === "number",
|
|
1535
|
+
).length,
|
|
1536
|
+
installableEntries: entries.filter((entry) => entry.installable).length,
|
|
1537
|
+
},
|
|
1538
|
+
platforms: Object.fromEntries(
|
|
1539
|
+
[...platformCounts.entries()].sort((left, right) =>
|
|
1540
|
+
left[0].localeCompare(right[0]),
|
|
1541
|
+
),
|
|
1542
|
+
),
|
|
1543
|
+
topTags: [...tagCounts.entries()]
|
|
1544
|
+
.sort(
|
|
1545
|
+
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
|
|
1546
|
+
)
|
|
1547
|
+
.slice(0, 20)
|
|
1548
|
+
.map(([tag, count]) => ({ tag, count })),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
export async function getClientSetup(args = {}) {
|
|
1553
|
+
let endpointUrl;
|
|
1554
|
+
try {
|
|
1555
|
+
const rawEndpointUrl = Object.prototype.hasOwnProperty.call(
|
|
1556
|
+
args,
|
|
1557
|
+
"endpointUrl",
|
|
1558
|
+
)
|
|
1559
|
+
? args.endpointUrl
|
|
1560
|
+
: DEFAULT_REMOTE_MCP_URL;
|
|
1561
|
+
endpointUrl = normalizeEndpointUrl(rawEndpointUrl).toString();
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
return invalid(error?.message || "Invalid endpoint URL.");
|
|
1564
|
+
}
|
|
1565
|
+
const snippets = {
|
|
1566
|
+
codex: {
|
|
1567
|
+
label: "Codex stdio bridge",
|
|
1568
|
+
config: {
|
|
1569
|
+
mcpServers: {
|
|
1570
|
+
heyclaude: {
|
|
1571
|
+
command: "npx",
|
|
1572
|
+
args: ["-y", "@heyclaude/mcp"],
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
"claude-desktop": {
|
|
1578
|
+
label: "Claude Desktop stdio bridge",
|
|
1579
|
+
config: {
|
|
1580
|
+
mcpServers: {
|
|
1581
|
+
heyclaude: {
|
|
1582
|
+
command: "npx",
|
|
1583
|
+
args: ["-y", "@heyclaude/mcp"],
|
|
1584
|
+
},
|
|
1585
|
+
},
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1588
|
+
cursor: {
|
|
1589
|
+
label: "Cursor remote MCP",
|
|
1590
|
+
config: {
|
|
1591
|
+
mcpServers: {
|
|
1592
|
+
heyclaude: {
|
|
1593
|
+
url: endpointUrl,
|
|
1594
|
+
},
|
|
1595
|
+
},
|
|
1596
|
+
},
|
|
1597
|
+
},
|
|
1598
|
+
windsurf: {
|
|
1599
|
+
label: "Windsurf remote MCP",
|
|
1600
|
+
config: {
|
|
1601
|
+
mcpServers: {
|
|
1602
|
+
heyclaude: {
|
|
1603
|
+
serverUrl: endpointUrl,
|
|
1604
|
+
},
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
"remote-http": {
|
|
1609
|
+
label: "Streamable HTTP endpoint",
|
|
1610
|
+
endpointUrl,
|
|
1611
|
+
headers: {
|
|
1612
|
+
accept: "application/json, text/event-stream",
|
|
1613
|
+
"content-type": "application/json",
|
|
1614
|
+
},
|
|
1615
|
+
},
|
|
1616
|
+
};
|
|
1617
|
+
const client = args.client || "";
|
|
1618
|
+
return {
|
|
1619
|
+
ok: true,
|
|
1620
|
+
endpointUrl,
|
|
1621
|
+
apiKeyRequired: false,
|
|
1622
|
+
selectedClient: client,
|
|
1623
|
+
snippets: client ? { [client]: snippets[client] } : snippets,
|
|
1624
|
+
notes: [
|
|
1625
|
+
"The public endpoint is read-only and does not need an API key.",
|
|
1626
|
+
"Submission tools prepare maintainer-reviewed PR-first drafts; they do not open GitHub issues.",
|
|
1627
|
+
"Use --url only when testing a custom preview or deployment.",
|
|
1628
|
+
],
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
export const RESOURCE_TEMPLATES = [
|
|
1633
|
+
{
|
|
1634
|
+
uriTemplate: "heyclaude://entry/{category}/{slug}",
|
|
1635
|
+
name: "HeyClaude entry detail",
|
|
1636
|
+
title: "HeyClaude entry detail",
|
|
1637
|
+
description:
|
|
1638
|
+
"Read a single generated HeyClaude entry detail artifact as JSON.",
|
|
1639
|
+
mimeType: jsonMimeType,
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
uriTemplate: "heyclaude://category/{category}",
|
|
1643
|
+
name: "HeyClaude category entries",
|
|
1644
|
+
title: "HeyClaude category entries",
|
|
1645
|
+
description:
|
|
1646
|
+
"Read generated summary entries for one HeyClaude category as JSON.",
|
|
1647
|
+
mimeType: jsonMimeType,
|
|
1648
|
+
},
|
|
1649
|
+
];
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Static MCP resource descriptors for the bounded discovery surfaces
|
|
1653
|
+
* exposed alongside the directory and category feeds. Appended to
|
|
1654
|
+
* {@link listRegistryResources} output and routed by
|
|
1655
|
+
* {@link readRegistryResource}.
|
|
1656
|
+
*
|
|
1657
|
+
* @type {Array<{ uri: string, name: string, title: string, description: string, mimeType: string }>}
|
|
1658
|
+
*/
|
|
1659
|
+
const DISCOVERY_RESOURCES = [
|
|
1660
|
+
{
|
|
1661
|
+
uri: "heyclaude://registry/recent",
|
|
1662
|
+
name: "HeyClaude recent registry updates",
|
|
1663
|
+
title: "HeyClaude recent registry updates",
|
|
1664
|
+
description:
|
|
1665
|
+
"Bounded list of recently added or upstream-updated HeyClaude entries from the generated search index.",
|
|
1666
|
+
mimeType: jsonMimeType,
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
uri: "heyclaude://registry/trending",
|
|
1670
|
+
name: "HeyClaude trending registry entries",
|
|
1671
|
+
title: "HeyClaude trending registry entries",
|
|
1672
|
+
description:
|
|
1673
|
+
"Bounded list of trending HeyClaude entries from the public /api/registry/trending endpoint; degrades gracefully when dynamic state is unavailable.",
|
|
1674
|
+
mimeType: jsonMimeType,
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
uri: "heyclaude://jobs/active",
|
|
1678
|
+
name: "HeyClaude active jobs",
|
|
1679
|
+
title: "HeyClaude active jobs",
|
|
1680
|
+
description:
|
|
1681
|
+
"Bounded list of active public job listings from the public /api/jobs endpoint; degrades gracefully when dynamic state is unavailable.",
|
|
1682
|
+
mimeType: jsonMimeType,
|
|
1683
|
+
},
|
|
1684
|
+
];
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* Resolve the public HeyClaude API base URL. Prefers an explicit override
|
|
1688
|
+
* on `options.publicApiBaseUrl`, then the `HEYCLAUDE_PUBLIC_API_URL`
|
|
1689
|
+
* environment variable, then falls back to the canonical site URL.
|
|
1690
|
+
*
|
|
1691
|
+
* @param {{ publicApiBaseUrl?: string }} [options]
|
|
1692
|
+
* @returns {string} Base URL used to build `/api/...` requests.
|
|
1693
|
+
*/
|
|
1694
|
+
function publicApiBaseUrl(options = {}) {
|
|
1695
|
+
return (
|
|
1696
|
+
options.publicApiBaseUrl || process.env.HEYCLAUDE_PUBLIC_API_URL || SITE_URL
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Remove trailing slashes without using a potentially expensive regex on
|
|
1702
|
+
* caller-controlled API base URL overrides.
|
|
1703
|
+
*
|
|
1704
|
+
* @param {string} value
|
|
1705
|
+
* @returns {string}
|
|
1706
|
+
*/
|
|
1707
|
+
function stripTrailingSlashes(value) {
|
|
1708
|
+
let end = value.length;
|
|
1709
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
1710
|
+
end -= 1;
|
|
1711
|
+
}
|
|
1712
|
+
return value.slice(0, end);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Fetch JSON from a public HeyClaude API path. Tests inject a deterministic
|
|
1717
|
+
* fetcher via `options.fetchPublicApi`; production uses `fetch()` with a
|
|
1718
|
+
* bounded {@link DISCOVERY_FETCH_TIMEOUT_MS} timeout, `redirect: "error"`,
|
|
1719
|
+
* and a JSON `accept` header. Throws on non-2xx responses so callers can
|
|
1720
|
+
* convert failures into the "unavailable" graceful-degradation envelope.
|
|
1721
|
+
*
|
|
1722
|
+
* @param {string} apiPath API path beginning with `/api/...`.
|
|
1723
|
+
* @param {{
|
|
1724
|
+
* publicApiBaseUrl?: string,
|
|
1725
|
+
* fetchPublicApi?: (apiPath: string) => Promise<unknown>,
|
|
1726
|
+
* }} [options]
|
|
1727
|
+
* @returns {Promise<unknown>} Parsed JSON body from the upstream response.
|
|
1728
|
+
*/
|
|
1729
|
+
async function fetchPublicApiJson(apiPath, options = {}) {
|
|
1730
|
+
if (typeof options.fetchPublicApi === "function") {
|
|
1731
|
+
return options.fetchPublicApi(apiPath);
|
|
1732
|
+
}
|
|
1733
|
+
const baseUrl = stripTrailingSlashes(publicApiBaseUrl(options));
|
|
1734
|
+
const url = `${baseUrl}${apiPath.startsWith("/") ? "" : "/"}${apiPath}`;
|
|
1735
|
+
const controller = new AbortController();
|
|
1736
|
+
const timeout = setTimeout(
|
|
1737
|
+
() => controller.abort(),
|
|
1738
|
+
DISCOVERY_FETCH_TIMEOUT_MS,
|
|
1739
|
+
);
|
|
1740
|
+
try {
|
|
1741
|
+
const response = await fetch(url, {
|
|
1742
|
+
method: "GET",
|
|
1743
|
+
headers: { accept: jsonMimeType },
|
|
1744
|
+
redirect: "error",
|
|
1745
|
+
signal: controller.signal,
|
|
1746
|
+
});
|
|
1747
|
+
if (!response.ok) {
|
|
1748
|
+
throw new Error(`Public API ${apiPath} returned ${response.status}.`);
|
|
1749
|
+
}
|
|
1750
|
+
return await response.json();
|
|
1751
|
+
} finally {
|
|
1752
|
+
clearTimeout(timeout);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/**
|
|
1757
|
+
* Build the standard "unavailable" error envelope used when a dynamic
|
|
1758
|
+
* resource cannot be loaded. Distinct from `notFound` / `invalid` so MCP
|
|
1759
|
+
* clients can tell apart "endpoint failed" from "no such resource" and
|
|
1760
|
+
* keep the surface read-only.
|
|
1761
|
+
*
|
|
1762
|
+
* @param {string} message Human-readable explanation.
|
|
1763
|
+
* @param {string} [details] Optional underlying error message.
|
|
1764
|
+
* @returns {{ ok: false, error: { code: "unavailable", message: string, details?: string } }}
|
|
1765
|
+
*/
|
|
1766
|
+
function unavailable(message, details) {
|
|
1767
|
+
return {
|
|
1768
|
+
ok: false,
|
|
1769
|
+
error: {
|
|
1770
|
+
code: "unavailable",
|
|
1771
|
+
message,
|
|
1772
|
+
...(details ? { details } : {}),
|
|
1773
|
+
},
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
* Build the `heyclaude://registry/recent` resource payload. Reads the
|
|
1779
|
+
* generated `search-index.json` artifact, sorts entries by `repoUpdatedAt`
|
|
1780
|
+
* (falling back to `updatedAt` / `dateAdded`) descending, and bounds
|
|
1781
|
+
* output to {@link DISCOVERY_RESOURCE_LIMIT} entries. Each entry carries
|
|
1782
|
+
* the standard `toEntrySummary` shape plus `updatedAt` and `updateKind`.
|
|
1783
|
+
*
|
|
1784
|
+
* @param {import("./registry.d.ts").RegistryArtifactLoaders} [options]
|
|
1785
|
+
* @returns {Promise<import("./registry.d.ts").RegistryToolResult>}
|
|
1786
|
+
*/
|
|
1787
|
+
export async function listRegistryRecent(options = {}) {
|
|
1788
|
+
const searchIndex = unwrapEntries(
|
|
1789
|
+
await readJsonArtifact("search-index.json", options),
|
|
1790
|
+
);
|
|
1791
|
+
const entries = searchIndex
|
|
1792
|
+
.slice()
|
|
1793
|
+
.sort((left, right) => {
|
|
1794
|
+
const dateCompare = entryUpdatedAt(right).localeCompare(
|
|
1795
|
+
entryUpdatedAt(left),
|
|
1796
|
+
);
|
|
1797
|
+
if (dateCompare !== 0) return dateCompare;
|
|
1798
|
+
return String(left.title || "").localeCompare(String(right.title || ""));
|
|
1799
|
+
})
|
|
1800
|
+
.slice(0, DISCOVERY_RESOURCE_LIMIT)
|
|
1801
|
+
.map((entry) => ({
|
|
1802
|
+
...toEntrySummary(entry),
|
|
1803
|
+
updatedAt: entryUpdatedAt(entry),
|
|
1804
|
+
updateKind: entry.repoUpdatedAt ? "upstream_update" : "added",
|
|
1805
|
+
}));
|
|
1806
|
+
|
|
1807
|
+
return {
|
|
1808
|
+
ok: true,
|
|
1809
|
+
kind: "registry-recent",
|
|
1810
|
+
schemaVersion: 1,
|
|
1811
|
+
limit: DISCOVERY_RESOURCE_LIMIT,
|
|
1812
|
+
count: entries.length,
|
|
1813
|
+
entries,
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Normalize a raw `/api/registry/trending` entry into the small, stable
|
|
1819
|
+
* shape published by the MCP `registry/trending` resource. Defends against
|
|
1820
|
+
* upstream field churn (missing arrays, non-numeric scores, dropped
|
|
1821
|
+
* `trustSignals`) so MCP clients see a predictable schema.
|
|
1822
|
+
*
|
|
1823
|
+
* @param {Record<string, unknown> & { category: string, slug: string }} entry
|
|
1824
|
+
* @returns {Record<string, unknown>} Normalized trending entry.
|
|
1825
|
+
*/
|
|
1826
|
+
function toTrendingEntry(entry) {
|
|
1827
|
+
return {
|
|
1828
|
+
key: `${entry.category}:${entry.slug}`,
|
|
1829
|
+
category: entry.category,
|
|
1830
|
+
slug: entry.slug,
|
|
1831
|
+
title: entry.title || "",
|
|
1832
|
+
description: entry.description || "",
|
|
1833
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
1834
|
+
platforms: Array.isArray(entry.platforms) ? entry.platforms : [],
|
|
1835
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
1836
|
+
dateAdded: entry.dateAdded || "",
|
|
1837
|
+
score: typeof entry.score === "number" ? entry.score : 0,
|
|
1838
|
+
reasons: Array.isArray(entry.reasons) ? entry.reasons : [],
|
|
1839
|
+
trustSignals: entry.trustSignals || { sourceStatus: "missing" },
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* Build the `heyclaude://registry/trending` resource payload. Reuses the
|
|
1845
|
+
* public `/api/registry/trending` endpoint (no DB access from the MCP
|
|
1846
|
+
* package). Returns an `unavailable` envelope when the upstream fetch
|
|
1847
|
+
* fails so MCP clients degrade gracefully. Output is bounded to
|
|
1848
|
+
* {@link DISCOVERY_RESOURCE_LIMIT} entries and forwards `signalsAvailable`
|
|
1849
|
+
* when present so consumers can tell which scoring signals applied.
|
|
1850
|
+
*
|
|
1851
|
+
* @param {import("./registry.d.ts").RegistryArtifactLoaders & {
|
|
1852
|
+
* publicApiBaseUrl?: string,
|
|
1853
|
+
* fetchPublicApi?: (apiPath: string) => Promise<unknown>,
|
|
1854
|
+
* }} [options]
|
|
1855
|
+
* @returns {Promise<import("./registry.d.ts").RegistryToolResult>}
|
|
1856
|
+
*/
|
|
1857
|
+
export async function listRegistryTrending(options = {}) {
|
|
1858
|
+
let payload;
|
|
1859
|
+
try {
|
|
1860
|
+
payload = await fetchPublicApiJson(
|
|
1861
|
+
`/api/registry/trending?limit=${DISCOVERY_RESOURCE_LIMIT}`,
|
|
1862
|
+
options,
|
|
1863
|
+
);
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
return unavailable(
|
|
1866
|
+
"Trending registry state is currently unavailable.",
|
|
1867
|
+
String(error?.message || error || ""),
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
if (!payload || !Array.isArray(payload.entries)) {
|
|
1872
|
+
return unavailable(
|
|
1873
|
+
"Trending registry state is currently unavailable.",
|
|
1874
|
+
"Upstream payload is missing the expected entries array.",
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
const entries = payload.entries
|
|
1878
|
+
.slice(0, DISCOVERY_RESOURCE_LIMIT)
|
|
1879
|
+
.map(toTrendingEntry);
|
|
1880
|
+
|
|
1881
|
+
return {
|
|
1882
|
+
ok: true,
|
|
1883
|
+
kind: "registry-trending",
|
|
1884
|
+
schemaVersion: payload?.schemaVersion ?? 1,
|
|
1885
|
+
category: payload?.category || "all",
|
|
1886
|
+
platform: payload?.platform || "all",
|
|
1887
|
+
limit: DISCOVERY_RESOURCE_LIMIT,
|
|
1888
|
+
count: entries.length,
|
|
1889
|
+
signalsAvailable:
|
|
1890
|
+
payload?.signalsAvailable && typeof payload.signalsAvailable === "object"
|
|
1891
|
+
? payload.signalsAvailable
|
|
1892
|
+
: null,
|
|
1893
|
+
source: "public-api",
|
|
1894
|
+
entries,
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Normalize a raw `/api/jobs` entry into the small, stable shape published
|
|
1900
|
+
* by the MCP `jobs/active` resource. Defends against upstream field churn
|
|
1901
|
+
* and never exposes private/admin-only fields (we only project the public
|
|
1902
|
+
* subset already returned by `buildPublicJobsIndex`).
|
|
1903
|
+
*
|
|
1904
|
+
* @param {Record<string, unknown>} job
|
|
1905
|
+
* @returns {Record<string, unknown>} Normalized public job entry.
|
|
1906
|
+
*/
|
|
1907
|
+
function toJobEntry(job) {
|
|
1908
|
+
return {
|
|
1909
|
+
id: job.id || job.slug || "",
|
|
1910
|
+
title: job.title || "",
|
|
1911
|
+
company: job.company || "",
|
|
1912
|
+
location: job.location || "",
|
|
1913
|
+
type: job.type || "",
|
|
1914
|
+
isRemote: Boolean(job.isRemote),
|
|
1915
|
+
tier: job.tier || "",
|
|
1916
|
+
applyUrl: job.applyUrl || job.url || "",
|
|
1917
|
+
sourceLabel: job.sourceLabel || "",
|
|
1918
|
+
postedAt: job.postedAt || job.publishedAt || "",
|
|
1919
|
+
labels: Array.isArray(job.labels) ? job.labels : [],
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Build the `heyclaude://jobs/active` resource payload. Reuses the public
|
|
1925
|
+
* `/api/jobs` endpoint (no DB access from the MCP package) and returns an
|
|
1926
|
+
* `unavailable` envelope when the upstream fetch fails. Output is bounded
|
|
1927
|
+
* to {@link DISCOVERY_RESOURCE_LIMIT} entries and forwards `totalAvailable`
|
|
1928
|
+
* when the upstream reports it.
|
|
1929
|
+
*
|
|
1930
|
+
* @param {import("./registry.d.ts").RegistryArtifactLoaders & {
|
|
1931
|
+
* publicApiBaseUrl?: string,
|
|
1932
|
+
* fetchPublicApi?: (apiPath: string) => Promise<unknown>,
|
|
1933
|
+
* }} [options]
|
|
1934
|
+
* @returns {Promise<import("./registry.d.ts").RegistryToolResult>}
|
|
1935
|
+
*/
|
|
1936
|
+
export async function listJobsActive(options = {}) {
|
|
1937
|
+
let payload;
|
|
1938
|
+
try {
|
|
1939
|
+
payload = await fetchPublicApiJson(
|
|
1940
|
+
`/api/jobs?limit=${DISCOVERY_RESOURCE_LIMIT}`,
|
|
1941
|
+
options,
|
|
1942
|
+
);
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
return unavailable(
|
|
1945
|
+
"Active jobs state is currently unavailable.",
|
|
1946
|
+
String(error?.message || error || ""),
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
if (!payload || !Array.isArray(payload.entries)) {
|
|
1951
|
+
return unavailable(
|
|
1952
|
+
"Active jobs state is currently unavailable.",
|
|
1953
|
+
"Upstream payload is missing the expected entries array.",
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
const entries = payload.entries
|
|
1957
|
+
.slice(0, DISCOVERY_RESOURCE_LIMIT)
|
|
1958
|
+
.map(toJobEntry);
|
|
1959
|
+
|
|
1960
|
+
return {
|
|
1961
|
+
ok: true,
|
|
1962
|
+
kind: "jobs-active",
|
|
1963
|
+
schemaVersion: payload?.schemaVersion ?? 1,
|
|
1964
|
+
limit: DISCOVERY_RESOURCE_LIMIT,
|
|
1965
|
+
count: entries.length,
|
|
1966
|
+
totalAvailable:
|
|
1967
|
+
typeof payload?.totalAvailable === "number"
|
|
1968
|
+
? payload.totalAvailable
|
|
1969
|
+
: null,
|
|
1970
|
+
source: "public-api",
|
|
1971
|
+
entries,
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
export const PROMPT_DEFINITIONS = [
|
|
1976
|
+
{
|
|
1977
|
+
name: "find_best_asset",
|
|
1978
|
+
title: "Find the best Claude asset",
|
|
1979
|
+
description:
|
|
1980
|
+
"Guide a client through searching, comparing, and recommending HeyClaude entries for a use case.",
|
|
1981
|
+
arguments: [
|
|
1982
|
+
{
|
|
1983
|
+
name: "use_case",
|
|
1984
|
+
description: "The task, workflow, or problem the user wants to solve.",
|
|
1985
|
+
required: true,
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
name: "category",
|
|
1989
|
+
description: "Optional HeyClaude category to constrain discovery.",
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
name: "platform",
|
|
1993
|
+
description:
|
|
1994
|
+
"Optional client/platform such as Claude, Codex, Cursor, or Windsurf.",
|
|
1995
|
+
},
|
|
1996
|
+
],
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
name: "prepare_submission",
|
|
2000
|
+
title: "Prepare a HeyClaude submission",
|
|
2001
|
+
description:
|
|
2002
|
+
"Guide a user through drafting a maintainer-reviewed HeyClaude submission without opening a PR automatically.",
|
|
2003
|
+
arguments: [
|
|
2004
|
+
{ name: "category", description: "Submission category.", required: true },
|
|
2005
|
+
{ name: "name", description: "Submission name or title." },
|
|
2006
|
+
{
|
|
2007
|
+
name: "source_url",
|
|
2008
|
+
description: "Primary source, docs, package, or repo URL.",
|
|
2009
|
+
},
|
|
2010
|
+
],
|
|
2011
|
+
},
|
|
2012
|
+
{
|
|
2013
|
+
name: "review_submission_before_pr",
|
|
2014
|
+
title: "Review submission before opening PR",
|
|
2015
|
+
description:
|
|
2016
|
+
"Check a draft for schema gaps, duplicate risk, source review, and maintainer checklist items.",
|
|
2017
|
+
arguments: [
|
|
2018
|
+
{
|
|
2019
|
+
name: "draft",
|
|
2020
|
+
description: "A concise description or JSON-shaped draft fields.",
|
|
2021
|
+
required: true,
|
|
2022
|
+
},
|
|
2023
|
+
],
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
name: "install_asset_safely",
|
|
2027
|
+
title: "Install a HeyClaude asset safely",
|
|
2028
|
+
description:
|
|
2029
|
+
"Guide installation/use of one entry while keeping source and secret-handling checks explicit.",
|
|
2030
|
+
arguments: [
|
|
2031
|
+
{ name: "category", description: "Entry category.", required: true },
|
|
2032
|
+
{ name: "slug", description: "Entry slug.", required: true },
|
|
2033
|
+
{ name: "platform", description: "Optional target client/platform." },
|
|
2034
|
+
],
|
|
2035
|
+
},
|
|
2036
|
+
];
|
|
2037
|
+
|
|
2038
|
+
export async function listRegistryResources(args = {}, options = {}) {
|
|
2039
|
+
const manifest = await readJsonArtifact("registry-manifest.json", options);
|
|
2040
|
+
const categories = Object.keys(manifest.categories || {}).sort();
|
|
2041
|
+
return {
|
|
2042
|
+
resources: [
|
|
2043
|
+
{
|
|
2044
|
+
uri: "heyclaude://feeds/directory",
|
|
2045
|
+
name: "HeyClaude directory index",
|
|
2046
|
+
title: "HeyClaude directory index",
|
|
2047
|
+
description: "Generated public directory index artifact.",
|
|
2048
|
+
mimeType: jsonMimeType,
|
|
2049
|
+
},
|
|
2050
|
+
...categories.map((category) => ({
|
|
2051
|
+
uri: `heyclaude://category/${category}`,
|
|
2052
|
+
name: `HeyClaude ${category} category`,
|
|
2053
|
+
title: `HeyClaude ${category}`,
|
|
2054
|
+
description: `Generated public ${category} category summary entries.`,
|
|
2055
|
+
mimeType: jsonMimeType,
|
|
2056
|
+
})),
|
|
2057
|
+
...DISCOVERY_RESOURCES,
|
|
2058
|
+
],
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
export function listRegistryResourceTemplates() {
|
|
2063
|
+
return {
|
|
2064
|
+
resourceTemplates: RESOURCE_TEMPLATES,
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
export async function readRegistryResource(args = {}, options = {}) {
|
|
2069
|
+
const uri = String(args.uri || "");
|
|
2070
|
+
const resourcePayload = (payload) => ({
|
|
2071
|
+
contents: [
|
|
2072
|
+
{
|
|
2073
|
+
uri: uri || "heyclaude://error",
|
|
2074
|
+
mimeType: jsonMimeType,
|
|
2075
|
+
text: JSON.stringify(withPublicPolicy(payload), null, 2),
|
|
2076
|
+
},
|
|
2077
|
+
],
|
|
2078
|
+
});
|
|
2079
|
+
let parsed;
|
|
2080
|
+
try {
|
|
2081
|
+
parsed = new URL(uri);
|
|
2082
|
+
} catch {
|
|
2083
|
+
return resourcePayload(
|
|
2084
|
+
notFound(`Unsupported HeyClaude resource URI: ${uri}`),
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
if (parsed.protocol !== "heyclaude:") {
|
|
2088
|
+
return resourcePayload(
|
|
2089
|
+
notFound(`Unsupported HeyClaude resource URI: ${uri}`),
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
2094
|
+
let payload;
|
|
2095
|
+
if (parsed.hostname === "feeds" && parts[0] === "directory") {
|
|
2096
|
+
payload = await readJsonArtifact("directory-index.json", options);
|
|
2097
|
+
} else if (parsed.hostname === "category" && parts.length === 1) {
|
|
2098
|
+
const category = normalizeText(parts[0]);
|
|
2099
|
+
if (!isSafePathPart(category)) {
|
|
2100
|
+
return resourcePayload(
|
|
2101
|
+
invalid("Category resource path is not slug-safe."),
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
const entries = unwrapEntries(
|
|
2105
|
+
await readJsonArtifact("search-index.json", options),
|
|
2106
|
+
)
|
|
2107
|
+
.filter((entry) => entry.category === category)
|
|
2108
|
+
.map(toEntrySummary);
|
|
2109
|
+
payload = {
|
|
2110
|
+
ok: true,
|
|
2111
|
+
category,
|
|
2112
|
+
total: entries.length,
|
|
2113
|
+
entries,
|
|
2114
|
+
};
|
|
2115
|
+
} else if (parsed.hostname === "entry" && parts.length === 2) {
|
|
2116
|
+
const [category, slug] = parts.map(normalizeText);
|
|
2117
|
+
const detail = await getEntryDetail({ category, slug }, options);
|
|
2118
|
+
payload = detail;
|
|
2119
|
+
} else if (
|
|
2120
|
+
parsed.hostname === "registry" &&
|
|
2121
|
+
parts.length === 1 &&
|
|
2122
|
+
parts[0] === "recent"
|
|
2123
|
+
) {
|
|
2124
|
+
payload = await listRegistryRecent(options);
|
|
2125
|
+
} else if (
|
|
2126
|
+
parsed.hostname === "registry" &&
|
|
2127
|
+
parts.length === 1 &&
|
|
2128
|
+
parts[0] === "trending"
|
|
2129
|
+
) {
|
|
2130
|
+
payload = await listRegistryTrending(options);
|
|
2131
|
+
} else if (
|
|
2132
|
+
parsed.hostname === "jobs" &&
|
|
2133
|
+
parts.length === 1 &&
|
|
2134
|
+
parts[0] === "active"
|
|
2135
|
+
) {
|
|
2136
|
+
payload = await listJobsActive(options);
|
|
2137
|
+
} else {
|
|
2138
|
+
return resourcePayload(
|
|
2139
|
+
notFound(`Unsupported HeyClaude resource URI: ${uri}`),
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
return resourcePayload(payload);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function promptArgument(args, name) {
|
|
2147
|
+
return String(args?.[name] || "").trim();
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
export function listRegistryPrompts() {
|
|
2151
|
+
return {
|
|
2152
|
+
prompts: PROMPT_DEFINITIONS,
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
export function getRegistryPrompt(args = {}) {
|
|
2157
|
+
const name = String(args.name || "");
|
|
2158
|
+
const prompt = PROMPT_DEFINITIONS.find(
|
|
2159
|
+
(candidate) => candidate.name === name,
|
|
2160
|
+
);
|
|
2161
|
+
if (!prompt) {
|
|
2162
|
+
return {
|
|
2163
|
+
description: "Unknown HeyClaude MCP prompt.",
|
|
2164
|
+
messages: [
|
|
2165
|
+
{
|
|
2166
|
+
role: "user",
|
|
2167
|
+
content: {
|
|
2168
|
+
type: "text",
|
|
2169
|
+
text: `Unknown HeyClaude MCP prompt: ${name}`,
|
|
2170
|
+
},
|
|
2171
|
+
},
|
|
2172
|
+
],
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
const values = args.arguments || {};
|
|
2176
|
+
const useCase = promptArgument(values, "use_case");
|
|
2177
|
+
const category = promptArgument(values, "category");
|
|
2178
|
+
const platform = promptArgument(values, "platform");
|
|
2179
|
+
const slug = promptArgument(values, "slug");
|
|
2180
|
+
const sourceUrl = promptArgument(values, "source_url");
|
|
2181
|
+
const draft = promptArgument(values, "draft");
|
|
2182
|
+
|
|
2183
|
+
const promptTextByName = {
|
|
2184
|
+
find_best_asset: `Find the best HeyClaude asset for this use case: ${useCase || "(not provided)"}.
|
|
2185
|
+
|
|
2186
|
+
Use the read-only HeyClaude MCP tools. Start with search_registry or list_category_entries${category ? ` in category ${category}` : ""}${platform ? ` for platform ${platform}` : ""}. Compare credible candidates with compare_entries, inspect details with get_entry_detail, and cite exact category/slug pairs. Do not invent popularity metrics when source stats are absent.`,
|
|
2187
|
+
prepare_submission: `Prepare a HeyClaude submission draft${category ? ` for category ${category}` : ""}${promptArgument(values, "name") ? ` named ${promptArgument(values, "name")}` : ""}${sourceUrl ? ` from ${sourceUrl}` : ""}.
|
|
2188
|
+
|
|
2189
|
+
Use get_submission_schema, get_submission_examples, prepare_submission_draft, review_submission_draft, and search_duplicate_entries. Return missing fields and the canonical PR-first submit URL/body. Do not create GitHub issues or publish content.`,
|
|
2190
|
+
review_submission_before_pr: `Review this HeyClaude submission draft before a PR is opened:
|
|
2191
|
+
|
|
2192
|
+
${draft || "(draft not provided)"}
|
|
2193
|
+
|
|
2194
|
+
Use review_submission_draft and search_duplicate_entries where structured fields are available. Treat schema-valid as not publish-valid, call out source-review needs, and keep the result maintainer-reviewed.`,
|
|
2195
|
+
install_asset_safely: `Help install or use the HeyClaude entry ${category || "(category)"}/${slug || "(slug)"}${platform ? ` for ${platform}` : ""}.
|
|
2196
|
+
|
|
2197
|
+
Use get_install_guidance and get_copyable_asset. Include source links, config/install text exactly as returned, and secret-handling cautions where relevant. Do not write local files or claim the install was completed.`,
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
return {
|
|
2201
|
+
description: prompt.description,
|
|
2202
|
+
messages: [
|
|
2203
|
+
{
|
|
2204
|
+
role: "user",
|
|
2205
|
+
content: {
|
|
2206
|
+
type: "text",
|
|
2207
|
+
text: promptTextByName[name],
|
|
2208
|
+
},
|
|
2209
|
+
},
|
|
2210
|
+
],
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
export async function getCompatibility(args = {}, options = {}) {
|
|
2215
|
+
const category = normalizeText(args.category || "skills");
|
|
2216
|
+
const slug = normalizeText(args.slug);
|
|
2217
|
+
if (!slug) return invalid("slug is required.");
|
|
2218
|
+
|
|
2219
|
+
const entry = await readEntry(category, slug, options);
|
|
2220
|
+
if (!entry) {
|
|
2221
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
return {
|
|
2225
|
+
ok: true,
|
|
2226
|
+
key: `${entry.category}:${entry.slug}`,
|
|
2227
|
+
category: entry.category,
|
|
2228
|
+
slug: entry.slug,
|
|
2229
|
+
platformCompatibility: buildSkillPlatformCompatibility(entry),
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
export async function getInstallGuidance(args = {}, options = {}) {
|
|
2234
|
+
const category = normalizeText(args.category);
|
|
2235
|
+
const slug = normalizeText(args.slug);
|
|
2236
|
+
const platform = normalizePlatform(args.platform);
|
|
2237
|
+
if (!category || !slug) {
|
|
2238
|
+
return invalid("category and slug are required.");
|
|
2239
|
+
}
|
|
334
2240
|
|
|
335
2241
|
const entry = await readEntry(category, slug, options);
|
|
336
2242
|
if (!entry) {
|
|
@@ -345,7 +2251,7 @@ export async function getInstallGuidance(args = {}, options = {}) {
|
|
|
345
2251
|
return {
|
|
346
2252
|
ok: true,
|
|
347
2253
|
key: `${entry.category}:${entry.slug}`,
|
|
348
|
-
canonicalUrl:
|
|
2254
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
349
2255
|
title: entry.title,
|
|
350
2256
|
installCommand: entry.installCommand || entry.commandSyntax || "",
|
|
351
2257
|
configSnippet: entry.configSnippet || "",
|
|
@@ -353,6 +2259,9 @@ export async function getInstallGuidance(args = {}, options = {}) {
|
|
|
353
2259
|
downloadUrl: entry.downloadUrl || "",
|
|
354
2260
|
documentationUrl: entry.documentationUrl || "",
|
|
355
2261
|
repoUrl: entry.repoUrl || "",
|
|
2262
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
2263
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
2264
|
+
trust: entryTrustSummary(entry),
|
|
356
2265
|
platform: platform || "",
|
|
357
2266
|
selectedCompatibility,
|
|
358
2267
|
platformCompatibility: compatibility,
|
|
@@ -450,6 +2359,131 @@ export async function getCategorySubmissionGuidance(args = {}, options = {}) {
|
|
|
450
2359
|
);
|
|
451
2360
|
}
|
|
452
2361
|
|
|
2362
|
+
export async function prepareSubmissionDraft(args = {}, options = {}) {
|
|
2363
|
+
return prepareSubmissionDraftFromSpec(
|
|
2364
|
+
await readSubmissionSpec(options),
|
|
2365
|
+
args,
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
export async function getSubmissionExamples(args = {}, options = {}) {
|
|
2370
|
+
return getSubmissionExamplesFromSpec(await readSubmissionSpec(options), args);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
export async function reviewSubmissionDraft(args = {}, options = {}) {
|
|
2374
|
+
const [spec, searchIndex] = await Promise.all([
|
|
2375
|
+
readSubmissionSpec(options),
|
|
2376
|
+
readJsonArtifact("search-index.json", options),
|
|
2377
|
+
]);
|
|
2378
|
+
return reviewSubmissionDraftFromSpec(spec, args, unwrapEntries(searchIndex));
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
export async function getSubmissionPolicy() {
|
|
2382
|
+
return {
|
|
2383
|
+
ok: true,
|
|
2384
|
+
publicPolicy: MCP_PUBLIC_POLICY,
|
|
2385
|
+
reviewModel: {
|
|
2386
|
+
prFirst: true,
|
|
2387
|
+
maintainerReviewRequired: true,
|
|
2388
|
+
autoMerge: "content_only_private_gate",
|
|
2389
|
+
autoMergeRequires: [
|
|
2390
|
+
"single content file only",
|
|
2391
|
+
"validate-content",
|
|
2392
|
+
"Superagent Security Scan",
|
|
2393
|
+
"private maintainer-agent review",
|
|
2394
|
+
],
|
|
2395
|
+
mutatingAutomationOwner: "private submission gate",
|
|
2396
|
+
},
|
|
2397
|
+
artifactPolicy: {
|
|
2398
|
+
communityHostedArchivesAllowed: false,
|
|
2399
|
+
communityZipHostingAllowed: false,
|
|
2400
|
+
communityMcpbHostingAllowed: false,
|
|
2401
|
+
maintainerBuiltDownloadsOnly: true,
|
|
2402
|
+
firstPartyDownloadsRequireVerification: true,
|
|
2403
|
+
},
|
|
2404
|
+
submissionGuidance: [
|
|
2405
|
+
"Use source-backed or copyable-content submissions for community content.",
|
|
2406
|
+
"Do not request public HeyClaude /downloads hosting for community ZIP/MCPB artifacts.",
|
|
2407
|
+
"Add safety_notes when a submission runs code, writes externally, uses permissions, or starts background workers.",
|
|
2408
|
+
"Add privacy_notes when a submission reads local files, logs, credentials, telemetry, or third-party user data.",
|
|
2409
|
+
"Commercial, affiliate, sponsored, or paid product listings go through maintainer review and disclosure, not the free content queue.",
|
|
2410
|
+
],
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
export async function explainEntryTrust(args = {}, options = {}) {
|
|
2415
|
+
const category = normalizeText(args.category);
|
|
2416
|
+
const slug = normalizeText(args.slug);
|
|
2417
|
+
if (!category || !slug) {
|
|
2418
|
+
return invalid("category and slug are required.");
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
const entry = await readEntry(category, slug, options);
|
|
2422
|
+
if (!entry) {
|
|
2423
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
return {
|
|
2427
|
+
ok: true,
|
|
2428
|
+
key: `${entry.category}:${entry.slug}`,
|
|
2429
|
+
title: entry.title,
|
|
2430
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
2431
|
+
trust: entryTrustSummary(entry),
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
export async function reviewEntrySafety(args = {}, options = {}) {
|
|
2436
|
+
const platform = normalizePlatform(args.platform);
|
|
2437
|
+
const entries = [];
|
|
2438
|
+
for (const target of args.entries || []) {
|
|
2439
|
+
const category = normalizeText(target.category);
|
|
2440
|
+
const slug = normalizeText(target.slug);
|
|
2441
|
+
const entry = await readEntry(category, slug, options);
|
|
2442
|
+
if (!entry) {
|
|
2443
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
2444
|
+
}
|
|
2445
|
+
const compatibility = buildSkillPlatformCompatibility(entry);
|
|
2446
|
+
entries.push({
|
|
2447
|
+
key: `${entry.category}:${entry.slug}`,
|
|
2448
|
+
category: entry.category,
|
|
2449
|
+
slug: entry.slug,
|
|
2450
|
+
title: entry.title,
|
|
2451
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
2452
|
+
selectedCompatibility: platform
|
|
2453
|
+
? compatibility.find((item) => item.platform === platform) || null
|
|
2454
|
+
: null,
|
|
2455
|
+
trust: entryTrustSummary(entry),
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
const entriesWithNotes = entries.filter(
|
|
2460
|
+
(entry) =>
|
|
2461
|
+
entry.trust.disclosures.hasSafetyNotes ||
|
|
2462
|
+
entry.trust.disclosures.hasPrivacyNotes,
|
|
2463
|
+
);
|
|
2464
|
+
|
|
2465
|
+
return {
|
|
2466
|
+
ok: true,
|
|
2467
|
+
platform: platform || "",
|
|
2468
|
+
count: entries.length,
|
|
2469
|
+
entries,
|
|
2470
|
+
summary: {
|
|
2471
|
+
entriesWithSafetyOrPrivacyNotes: entriesWithNotes.length,
|
|
2472
|
+
firstPartyPackages: entries.filter(
|
|
2473
|
+
(entry) => entry.trust.package.downloadTrust === "first-party",
|
|
2474
|
+
).length,
|
|
2475
|
+
sourceBacked: entries.filter(
|
|
2476
|
+
(entry) => entry.trust.source.status === "available",
|
|
2477
|
+
).length,
|
|
2478
|
+
},
|
|
2479
|
+
reviewNotes: [
|
|
2480
|
+
"This is a metadata review, not a malware scan or install verdict.",
|
|
2481
|
+
"Prefer source-backed entries and first-party maintainer-built downloads when installing packages.",
|
|
2482
|
+
"Inspect commands, requested permissions, and external writes before running any copied content.",
|
|
2483
|
+
],
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
|
|
453
2487
|
export async function callRegistryTool(name, args = {}, options = {}) {
|
|
454
2488
|
if (!READ_ONLY_TOOL_NAMES.includes(name)) {
|
|
455
2489
|
return invalid(`Unknown read-only HeyClaude MCP tool: ${name}`);
|
|
@@ -469,28 +2503,87 @@ export async function callRegistryTool(name, args = {}, options = {}) {
|
|
|
469
2503
|
throw error;
|
|
470
2504
|
}
|
|
471
2505
|
|
|
2506
|
+
let result;
|
|
472
2507
|
switch (name) {
|
|
473
2508
|
case "search_registry":
|
|
474
|
-
|
|
2509
|
+
result = await searchRegistry(parsedArgs, options);
|
|
2510
|
+
break;
|
|
2511
|
+
case "plan_workflow_toolbox":
|
|
2512
|
+
result = await planWorkflowToolbox(parsedArgs, options);
|
|
2513
|
+
break;
|
|
2514
|
+
case "server_info":
|
|
2515
|
+
result = await getServerInfo(parsedArgs, options);
|
|
2516
|
+
break;
|
|
2517
|
+
case "list_category_entries":
|
|
2518
|
+
result = await listCategoryEntries(parsedArgs, options);
|
|
2519
|
+
break;
|
|
2520
|
+
case "get_recent_updates":
|
|
2521
|
+
result = await getRecentUpdates(parsedArgs, options);
|
|
2522
|
+
break;
|
|
2523
|
+
case "get_related_entries":
|
|
2524
|
+
result = await getRelatedEntries(parsedArgs, options);
|
|
2525
|
+
break;
|
|
475
2526
|
case "get_entry_detail":
|
|
476
|
-
|
|
2527
|
+
result = await getEntryDetail(parsedArgs, options);
|
|
2528
|
+
break;
|
|
2529
|
+
case "get_copyable_asset":
|
|
2530
|
+
result = await getCopyableAsset(parsedArgs, options);
|
|
2531
|
+
break;
|
|
2532
|
+
case "compare_entries":
|
|
2533
|
+
result = await compareEntries(parsedArgs, options);
|
|
2534
|
+
break;
|
|
2535
|
+
case "get_registry_stats":
|
|
2536
|
+
result = await getRegistryStats(parsedArgs, options);
|
|
2537
|
+
break;
|
|
2538
|
+
case "get_client_setup":
|
|
2539
|
+
result = await getClientSetup(parsedArgs, options);
|
|
2540
|
+
break;
|
|
477
2541
|
case "get_compatibility":
|
|
478
|
-
|
|
2542
|
+
result = await getCompatibility(parsedArgs, options);
|
|
2543
|
+
break;
|
|
479
2544
|
case "get_install_guidance":
|
|
480
|
-
|
|
2545
|
+
result = await getInstallGuidance(parsedArgs, options);
|
|
2546
|
+
break;
|
|
481
2547
|
case "get_platform_adapter":
|
|
482
|
-
|
|
2548
|
+
result = await getPlatformAdapter(parsedArgs, options);
|
|
2549
|
+
break;
|
|
483
2550
|
case "list_distribution_feeds":
|
|
484
|
-
|
|
2551
|
+
result = await listDistributionFeeds(parsedArgs, options);
|
|
2552
|
+
break;
|
|
485
2553
|
case "get_submission_schema":
|
|
486
|
-
|
|
2554
|
+
result = await getSubmissionSchema(parsedArgs, options);
|
|
2555
|
+
break;
|
|
487
2556
|
case "validate_submission_draft":
|
|
488
|
-
|
|
2557
|
+
result = await validateSubmissionDraft(parsedArgs, options);
|
|
2558
|
+
break;
|
|
489
2559
|
case "search_duplicate_entries":
|
|
490
|
-
|
|
2560
|
+
result = await searchDuplicateRegistryEntries(parsedArgs, options);
|
|
2561
|
+
break;
|
|
491
2562
|
case "build_submission_urls":
|
|
492
|
-
|
|
2563
|
+
result = await buildSubmissionUrls(parsedArgs, options);
|
|
2564
|
+
break;
|
|
493
2565
|
case "get_category_submission_guidance":
|
|
494
|
-
|
|
2566
|
+
result = await getCategorySubmissionGuidance(parsedArgs, options);
|
|
2567
|
+
break;
|
|
2568
|
+
case "prepare_submission_draft":
|
|
2569
|
+
result = await prepareSubmissionDraft(parsedArgs, options);
|
|
2570
|
+
break;
|
|
2571
|
+
case "get_submission_examples":
|
|
2572
|
+
result = await getSubmissionExamples(parsedArgs, options);
|
|
2573
|
+
break;
|
|
2574
|
+
case "review_submission_draft":
|
|
2575
|
+
result = await reviewSubmissionDraft(parsedArgs, options);
|
|
2576
|
+
break;
|
|
2577
|
+
case "get_submission_policy":
|
|
2578
|
+
result = await getSubmissionPolicy(parsedArgs, options);
|
|
2579
|
+
break;
|
|
2580
|
+
case "explain_entry_trust":
|
|
2581
|
+
result = await explainEntryTrust(parsedArgs, options);
|
|
2582
|
+
break;
|
|
2583
|
+
case "review_entry_safety":
|
|
2584
|
+
result = await reviewEntrySafety(parsedArgs, options);
|
|
2585
|
+
break;
|
|
495
2586
|
}
|
|
2587
|
+
|
|
2588
|
+
return withPublicPolicy(result);
|
|
496
2589
|
}
|