@heyclaude/mcp 0.2.0 → 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 +12 -0
- package/README.md +29 -14
- package/package.json +1 -1
- package/scripts/validate-endpoint.mjs +68 -10
- package/src/package-metadata.js +4 -7
- package/src/registry.d.ts +49 -1
- package/src/registry.js +1104 -36
- package/src/schemas.d.ts +3 -0
- package/src/schemas.js +62 -1
- package/src/submissions.d.ts +1 -2
- package/src/submissions.js +255 -84
package/src/registry.js
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
platformFeedSlug,
|
|
8
8
|
SITE_URL,
|
|
9
9
|
} from "./platforms.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_REMOTE_MCP_URL,
|
|
12
|
+
normalizeEndpointUrl,
|
|
13
|
+
} from "./endpoint-url.js";
|
|
11
14
|
import { packageName, packageVersion } from "./package-metadata.js";
|
|
12
15
|
import {
|
|
13
16
|
formatZodError,
|
|
@@ -26,13 +29,18 @@ import {
|
|
|
26
29
|
validateSubmissionDraftFromSpec,
|
|
27
30
|
} from "./submissions.js";
|
|
28
31
|
|
|
29
|
-
const repoRoot = path.resolve(
|
|
30
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
31
|
-
"../../..",
|
|
32
|
-
);
|
|
33
|
-
const defaultDataDir = path.join(repoRoot, "apps", "web", "public", "data");
|
|
34
32
|
const safePathPartPattern = /^[a-z0-9-]+$/;
|
|
35
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
|
+
}
|
|
36
44
|
|
|
37
45
|
export const MCP_PUBLIC_POLICY = {
|
|
38
46
|
apiKeyRequired: false,
|
|
@@ -60,6 +68,7 @@ const platformAliases = new Map([
|
|
|
60
68
|
|
|
61
69
|
export const READ_ONLY_TOOL_NAMES = [
|
|
62
70
|
"search_registry",
|
|
71
|
+
"plan_workflow_toolbox",
|
|
63
72
|
"server_info",
|
|
64
73
|
"list_category_entries",
|
|
65
74
|
"get_recent_updates",
|
|
@@ -81,6 +90,9 @@ export const READ_ONLY_TOOL_NAMES = [
|
|
|
81
90
|
"prepare_submission_draft",
|
|
82
91
|
"get_submission_examples",
|
|
83
92
|
"review_submission_draft",
|
|
93
|
+
"get_submission_policy",
|
|
94
|
+
"explain_entry_trust",
|
|
95
|
+
"review_entry_safety",
|
|
84
96
|
];
|
|
85
97
|
|
|
86
98
|
export const TOOL_DEFINITIONS = [
|
|
@@ -97,6 +109,19 @@ export const TOOL_DEFINITIONS = [
|
|
|
97
109
|
openWorldHint: false,
|
|
98
110
|
},
|
|
99
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
|
+
},
|
|
100
125
|
{
|
|
101
126
|
name: "server_info",
|
|
102
127
|
description:
|
|
@@ -178,25 +203,25 @@ export const TOOL_DEFINITIONS = [
|
|
|
178
203
|
{
|
|
179
204
|
name: "get_submission_schema",
|
|
180
205
|
description:
|
|
181
|
-
"Fetch read-only HeyClaude submission schemas
|
|
206
|
+
"Fetch read-only HeyClaude submission schemas for PR-first intake by category.",
|
|
182
207
|
inputSchema: jsonSchemaForTool("get_submission_schema"),
|
|
183
208
|
},
|
|
184
209
|
{
|
|
185
210
|
name: "validate_submission_draft",
|
|
186
211
|
description:
|
|
187
|
-
"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.",
|
|
188
213
|
inputSchema: jsonSchemaForTool("validate_submission_draft"),
|
|
189
214
|
},
|
|
190
215
|
{
|
|
191
216
|
name: "search_duplicate_entries",
|
|
192
217
|
description:
|
|
193
|
-
"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.",
|
|
194
219
|
inputSchema: jsonSchemaForTool("search_duplicate_entries"),
|
|
195
220
|
},
|
|
196
221
|
{
|
|
197
222
|
name: "build_submission_urls",
|
|
198
223
|
description:
|
|
199
|
-
"Build prefilled HeyClaude submit and
|
|
224
|
+
"Build prefilled HeyClaude submit and review URLs for a validated PR-first submission draft without making write calls.",
|
|
200
225
|
inputSchema: jsonSchemaForTool("build_submission_urls"),
|
|
201
226
|
},
|
|
202
227
|
{
|
|
@@ -208,7 +233,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
208
233
|
{
|
|
209
234
|
name: "prepare_submission_draft",
|
|
210
235
|
description:
|
|
211
|
-
"Build a read-only maintainer-reviewed HeyClaude submission draft with canonical
|
|
236
|
+
"Build a read-only maintainer-reviewed HeyClaude submission draft with canonical PR text and URLs.",
|
|
212
237
|
inputSchema: jsonSchemaForTool("prepare_submission_draft"),
|
|
213
238
|
},
|
|
214
239
|
{
|
|
@@ -223,6 +248,24 @@ export const TOOL_DEFINITIONS = [
|
|
|
223
248
|
"Review a HeyClaude submission draft locally for schema errors, duplicate risk, and maintainer checklist items without writing to GitHub.",
|
|
224
249
|
inputSchema: jsonSchemaForTool("review_submission_draft"),
|
|
225
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
|
+
},
|
|
226
269
|
];
|
|
227
270
|
|
|
228
271
|
for (const tool of TOOL_DEFINITIONS) {
|
|
@@ -236,7 +279,24 @@ for (const tool of TOOL_DEFINITIONS) {
|
|
|
236
279
|
}
|
|
237
280
|
|
|
238
281
|
function dataDirFromOptions(options = {}) {
|
|
239
|
-
|
|
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");
|
|
240
300
|
}
|
|
241
301
|
|
|
242
302
|
function isSafePathPart(value) {
|
|
@@ -315,6 +375,8 @@ function entryMatchesQuery(entry, query) {
|
|
|
315
375
|
entry.submittedBy,
|
|
316
376
|
entry.brandName,
|
|
317
377
|
entry.brandDomain,
|
|
378
|
+
...notes(entry.safetyNotes),
|
|
379
|
+
...notes(entry.privacyNotes),
|
|
318
380
|
...(entry.tags || []),
|
|
319
381
|
...(entry.keywords || []),
|
|
320
382
|
]
|
|
@@ -323,6 +385,122 @@ function entryMatchesQuery(entry, query) {
|
|
|
323
385
|
return haystack.includes(query);
|
|
324
386
|
}
|
|
325
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
|
+
|
|
326
504
|
function entryMatchesPlatform(entry, platform) {
|
|
327
505
|
if (!platform) return true;
|
|
328
506
|
return (entry.platforms || []).some((candidate) => candidate === platform);
|
|
@@ -335,7 +513,84 @@ function entryMatchesTag(entry, tag) {
|
|
|
335
513
|
);
|
|
336
514
|
}
|
|
337
515
|
|
|
338
|
-
function
|
|
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) {
|
|
339
594
|
return {
|
|
340
595
|
key: `${entry.category}:${entry.slug}`,
|
|
341
596
|
category: entry.category,
|
|
@@ -348,11 +603,14 @@ function toSearchResult(entry) {
|
|
|
348
603
|
brandDomain: entry.brandDomain || "",
|
|
349
604
|
submittedBy: entry.submittedBy || "",
|
|
350
605
|
claimStatus: entry.claimStatus || "",
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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),
|
|
356
614
|
};
|
|
357
615
|
}
|
|
358
616
|
|
|
@@ -363,6 +621,8 @@ function toEntrySummary(entry) {
|
|
|
363
621
|
repoUpdatedAt: entry.repoUpdatedAt || null,
|
|
364
622
|
verificationStatus: entry.verificationStatus || "",
|
|
365
623
|
installable: Boolean(entry.installable),
|
|
624
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
625
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
366
626
|
supportLevels: entry.supportLevels || [],
|
|
367
627
|
};
|
|
368
628
|
}
|
|
@@ -410,6 +670,12 @@ function unique(values = []) {
|
|
|
410
670
|
);
|
|
411
671
|
}
|
|
412
672
|
|
|
673
|
+
function notes(values) {
|
|
674
|
+
return Array.isArray(values)
|
|
675
|
+
? values.map((value) => String(value || "").trim()).filter(Boolean)
|
|
676
|
+
: [];
|
|
677
|
+
}
|
|
678
|
+
|
|
413
679
|
function normalizeDateFloor(value) {
|
|
414
680
|
const text = String(value || "").trim();
|
|
415
681
|
if (!text) return "";
|
|
@@ -441,6 +707,87 @@ function sourceSummary(entry) {
|
|
|
441
707
|
};
|
|
442
708
|
}
|
|
443
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
|
+
|
|
444
791
|
function contentAsset(type, label, content, format = "markdown") {
|
|
445
792
|
const text =
|
|
446
793
|
content && typeof content === "object"
|
|
@@ -552,16 +899,19 @@ export async function searchRegistry(args = {}, options = {}) {
|
|
|
552
899
|
const category = normalizeText(args.category);
|
|
553
900
|
const platform = normalizePlatform(args.platform);
|
|
554
901
|
const limit = normalizeLimit(args.limit);
|
|
902
|
+
const trustFilters = parsedTrustArgs(args);
|
|
555
903
|
const searchIndex = unwrapEntries(
|
|
556
904
|
await readJsonArtifact("search-index.json", options),
|
|
557
905
|
);
|
|
558
906
|
|
|
559
|
-
const
|
|
907
|
+
const matched = searchIndex
|
|
560
908
|
.filter((entry) => !category || entry.category === category)
|
|
561
909
|
.filter((entry) => entryMatchesPlatform(entry, platform))
|
|
562
910
|
.filter((entry) => entryMatchesQuery(entry, query))
|
|
911
|
+
.filter((entry) => entryMatchesTrustFilters(entry, trustFilters));
|
|
912
|
+
const entries = rankSearchEntries(matched, query)
|
|
563
913
|
.slice(0, limit)
|
|
564
|
-
.map(toSearchResult);
|
|
914
|
+
.map((item) => toSearchResult(item.entry, item));
|
|
565
915
|
|
|
566
916
|
return {
|
|
567
917
|
ok: true,
|
|
@@ -569,10 +919,205 @@ export async function searchRegistry(args = {}, options = {}) {
|
|
|
569
919
|
query: args.query || "",
|
|
570
920
|
category: category || "",
|
|
571
921
|
platform: platform || "",
|
|
922
|
+
filters: trustFilters,
|
|
572
923
|
entries,
|
|
573
924
|
};
|
|
574
925
|
}
|
|
575
926
|
|
|
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;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
for (const item of ranked) {
|
|
941
|
+
if (selected.includes(item)) continue;
|
|
942
|
+
selected.push(item);
|
|
943
|
+
if (selected.length >= limit) return selected;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return selected;
|
|
947
|
+
}
|
|
948
|
+
|
|
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
|
+
}
|
|
987
|
+
|
|
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);
|
|
1034
|
+
}
|
|
1035
|
+
return [...counts]
|
|
1036
|
+
.map(([category, count]) => ({ category, count }))
|
|
1037
|
+
.sort((left, right) => left.category.localeCompare(right.category));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function toolboxTrustSummary(entries) {
|
|
1041
|
+
return {
|
|
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,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
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);
|
|
1071
|
+
const category = normalizeText(args.category);
|
|
1072
|
+
const platform = normalizePlatform(args.platform);
|
|
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
|
+
|
|
576
1121
|
export async function getServerInfo(args = {}, options = {}) {
|
|
577
1122
|
const manifest = await readJsonArtifact("registry-manifest.json", options);
|
|
578
1123
|
return {
|
|
@@ -729,6 +1274,39 @@ export async function getRelatedEntries(args = {}, options = {}) {
|
|
|
729
1274
|
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
730
1275
|
}
|
|
731
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
|
+
|
|
732
1310
|
const entries = searchIndex
|
|
733
1311
|
.map((entry) => {
|
|
734
1312
|
const related = scoreRelatedEntry(target, entry);
|
|
@@ -772,8 +1350,13 @@ export async function getEntryDetail(args = {}, options = {}) {
|
|
|
772
1350
|
return {
|
|
773
1351
|
ok: true,
|
|
774
1352
|
key: `${entry.category}:${entry.slug}`,
|
|
775
|
-
canonicalUrl:
|
|
776
|
-
entry
|
|
1353
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
1354
|
+
entry: {
|
|
1355
|
+
...entry,
|
|
1356
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
1357
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
1358
|
+
},
|
|
1359
|
+
trust: entryTrustSummary(entry),
|
|
777
1360
|
};
|
|
778
1361
|
}
|
|
779
1362
|
|
|
@@ -827,7 +1410,7 @@ export async function getCopyableAsset(args = {}, options = {}) {
|
|
|
827
1410
|
category: entry.category,
|
|
828
1411
|
slug: entry.slug,
|
|
829
1412
|
title: entry.title,
|
|
830
|
-
canonicalUrl:
|
|
1413
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
831
1414
|
platform: platform || "",
|
|
832
1415
|
primaryAsset: primary,
|
|
833
1416
|
assets,
|
|
@@ -835,8 +1418,11 @@ export async function getCopyableAsset(args = {}, options = {}) {
|
|
|
835
1418
|
configSnippet: entry.configSnippet || "",
|
|
836
1419
|
usageSnippet: entry.usageSnippet || "",
|
|
837
1420
|
downloadUrl: entry.downloadUrl || "",
|
|
1421
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
1422
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
838
1423
|
platformCompatibility: compatibility,
|
|
839
1424
|
source: sourceSummary(entry),
|
|
1425
|
+
trust: entryTrustSummary(entry),
|
|
840
1426
|
};
|
|
841
1427
|
}
|
|
842
1428
|
|
|
@@ -864,7 +1450,7 @@ export async function compareEntries(args = {}, options = {}) {
|
|
|
864
1450
|
slug: entry.slug,
|
|
865
1451
|
title: entry.title,
|
|
866
1452
|
description: entry.description,
|
|
867
|
-
canonicalUrl:
|
|
1453
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
868
1454
|
tags: entry.tags || [],
|
|
869
1455
|
platforms: entry.platforms || [],
|
|
870
1456
|
selectedCompatibility,
|
|
@@ -876,22 +1462,29 @@ export async function compareEntries(args = {}, options = {}) {
|
|
|
876
1462
|
entry.scriptBody ? "script" : "",
|
|
877
1463
|
].filter(Boolean),
|
|
878
1464
|
source: sourceSummary(entry),
|
|
1465
|
+
trust: entryTrustSummary(entry),
|
|
879
1466
|
};
|
|
880
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
|
+
: [];
|
|
881
1476
|
|
|
882
1477
|
return {
|
|
883
1478
|
ok: true,
|
|
884
1479
|
platform: platform || "",
|
|
885
1480
|
count: compared.length,
|
|
886
|
-
sharedTags
|
|
887
|
-
compared[0]?.tags || [],
|
|
888
|
-
compared.slice(1).flatMap((entry) => entry.tags || []),
|
|
889
|
-
),
|
|
1481
|
+
sharedTags,
|
|
890
1482
|
entries: compared,
|
|
891
1483
|
comparisonNotes: [
|
|
892
1484
|
"Prefer exact category fit before source popularity.",
|
|
893
1485
|
"Treat GitHub stars/forks as source signals only when present; absence is not a negative ranking.",
|
|
894
1486
|
"Install complexity is derived from available install/config/download/prerequisite metadata.",
|
|
1487
|
+
"Safety/privacy notes are disclosure metadata, not a malware verdict.",
|
|
895
1488
|
],
|
|
896
1489
|
};
|
|
897
1490
|
}
|
|
@@ -957,7 +1550,18 @@ export async function getRegistryStats(args = {}, options = {}) {
|
|
|
957
1550
|
}
|
|
958
1551
|
|
|
959
1552
|
export async function getClientSetup(args = {}) {
|
|
960
|
-
|
|
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
|
+
}
|
|
961
1565
|
const snippets = {
|
|
962
1566
|
codex: {
|
|
963
1567
|
label: "Codex stdio bridge",
|
|
@@ -1019,7 +1623,7 @@ export async function getClientSetup(args = {}) {
|
|
|
1019
1623
|
snippets: client ? { [client]: snippets[client] } : snippets,
|
|
1020
1624
|
notes: [
|
|
1021
1625
|
"The public endpoint is read-only and does not need an API key.",
|
|
1022
|
-
"Submission tools prepare maintainer-reviewed drafts; they do not open GitHub issues.",
|
|
1626
|
+
"Submission tools prepare maintainer-reviewed PR-first drafts; they do not open GitHub issues.",
|
|
1023
1627
|
"Use --url only when testing a custom preview or deployment.",
|
|
1024
1628
|
],
|
|
1025
1629
|
};
|
|
@@ -1044,6 +1648,330 @@ export const RESOURCE_TEMPLATES = [
|
|
|
1044
1648
|
},
|
|
1045
1649
|
];
|
|
1046
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
|
+
|
|
1047
1975
|
export const PROMPT_DEFINITIONS = [
|
|
1048
1976
|
{
|
|
1049
1977
|
name: "find_best_asset",
|
|
@@ -1071,7 +1999,7 @@ export const PROMPT_DEFINITIONS = [
|
|
|
1071
1999
|
name: "prepare_submission",
|
|
1072
2000
|
title: "Prepare a HeyClaude submission",
|
|
1073
2001
|
description:
|
|
1074
|
-
"Guide a user through drafting a maintainer-reviewed HeyClaude submission without opening
|
|
2002
|
+
"Guide a user through drafting a maintainer-reviewed HeyClaude submission without opening a PR automatically.",
|
|
1075
2003
|
arguments: [
|
|
1076
2004
|
{ name: "category", description: "Submission category.", required: true },
|
|
1077
2005
|
{ name: "name", description: "Submission name or title." },
|
|
@@ -1082,8 +2010,8 @@ export const PROMPT_DEFINITIONS = [
|
|
|
1082
2010
|
],
|
|
1083
2011
|
},
|
|
1084
2012
|
{
|
|
1085
|
-
name: "
|
|
1086
|
-
title: "Review submission before opening
|
|
2013
|
+
name: "review_submission_before_pr",
|
|
2014
|
+
title: "Review submission before opening PR",
|
|
1087
2015
|
description:
|
|
1088
2016
|
"Check a draft for schema gaps, duplicate risk, source review, and maintainer checklist items.",
|
|
1089
2017
|
arguments: [
|
|
@@ -1126,6 +2054,7 @@ export async function listRegistryResources(args = {}, options = {}) {
|
|
|
1126
2054
|
description: `Generated public ${category} category summary entries.`,
|
|
1127
2055
|
mimeType: jsonMimeType,
|
|
1128
2056
|
})),
|
|
2057
|
+
...DISCOVERY_RESOURCES,
|
|
1129
2058
|
],
|
|
1130
2059
|
};
|
|
1131
2060
|
}
|
|
@@ -1187,6 +2116,24 @@ export async function readRegistryResource(args = {}, options = {}) {
|
|
|
1187
2116
|
const [category, slug] = parts.map(normalizeText);
|
|
1188
2117
|
const detail = await getEntryDetail({ category, slug }, options);
|
|
1189
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);
|
|
1190
2137
|
} else {
|
|
1191
2138
|
return resourcePayload(
|
|
1192
2139
|
notFound(`Unsupported HeyClaude resource URI: ${uri}`),
|
|
@@ -1239,8 +2186,8 @@ export function getRegistryPrompt(args = {}) {
|
|
|
1239
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.`,
|
|
1240
2187
|
prepare_submission: `Prepare a HeyClaude submission draft${category ? ` for category ${category}` : ""}${promptArgument(values, "name") ? ` named ${promptArgument(values, "name")}` : ""}${sourceUrl ? ` from ${sourceUrl}` : ""}.
|
|
1241
2188
|
|
|
1242
|
-
Use get_submission_schema, get_submission_examples, prepare_submission_draft, review_submission_draft, and search_duplicate_entries. Return missing fields and the canonical
|
|
1243
|
-
|
|
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:
|
|
1244
2191
|
|
|
1245
2192
|
${draft || "(draft not provided)"}
|
|
1246
2193
|
|
|
@@ -1304,7 +2251,7 @@ export async function getInstallGuidance(args = {}, options = {}) {
|
|
|
1304
2251
|
return {
|
|
1305
2252
|
ok: true,
|
|
1306
2253
|
key: `${entry.category}:${entry.slug}`,
|
|
1307
|
-
canonicalUrl:
|
|
2254
|
+
canonicalUrl: entryCanonicalUrl(entry),
|
|
1308
2255
|
title: entry.title,
|
|
1309
2256
|
installCommand: entry.installCommand || entry.commandSyntax || "",
|
|
1310
2257
|
configSnippet: entry.configSnippet || "",
|
|
@@ -1312,6 +2259,9 @@ export async function getInstallGuidance(args = {}, options = {}) {
|
|
|
1312
2259
|
downloadUrl: entry.downloadUrl || "",
|
|
1313
2260
|
documentationUrl: entry.documentationUrl || "",
|
|
1314
2261
|
repoUrl: entry.repoUrl || "",
|
|
2262
|
+
safetyNotes: notes(entry.safetyNotes),
|
|
2263
|
+
privacyNotes: notes(entry.privacyNotes),
|
|
2264
|
+
trust: entryTrustSummary(entry),
|
|
1315
2265
|
platform: platform || "",
|
|
1316
2266
|
selectedCompatibility,
|
|
1317
2267
|
platformCompatibility: compatibility,
|
|
@@ -1428,6 +2378,112 @@ export async function reviewSubmissionDraft(args = {}, options = {}) {
|
|
|
1428
2378
|
return reviewSubmissionDraftFromSpec(spec, args, unwrapEntries(searchIndex));
|
|
1429
2379
|
}
|
|
1430
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
|
+
|
|
1431
2487
|
export async function callRegistryTool(name, args = {}, options = {}) {
|
|
1432
2488
|
if (!READ_ONLY_TOOL_NAMES.includes(name)) {
|
|
1433
2489
|
return invalid(`Unknown read-only HeyClaude MCP tool: ${name}`);
|
|
@@ -1452,6 +2508,9 @@ export async function callRegistryTool(name, args = {}, options = {}) {
|
|
|
1452
2508
|
case "search_registry":
|
|
1453
2509
|
result = await searchRegistry(parsedArgs, options);
|
|
1454
2510
|
break;
|
|
2511
|
+
case "plan_workflow_toolbox":
|
|
2512
|
+
result = await planWorkflowToolbox(parsedArgs, options);
|
|
2513
|
+
break;
|
|
1455
2514
|
case "server_info":
|
|
1456
2515
|
result = await getServerInfo(parsedArgs, options);
|
|
1457
2516
|
break;
|
|
@@ -1515,6 +2574,15 @@ export async function callRegistryTool(name, args = {}, options = {}) {
|
|
|
1515
2574
|
case "review_submission_draft":
|
|
1516
2575
|
result = await reviewSubmissionDraft(parsedArgs, options);
|
|
1517
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;
|
|
1518
2586
|
}
|
|
1519
2587
|
|
|
1520
2588
|
return withPublicPolicy(result);
|