@heyclaude/mcp 0.1.2 → 0.2.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 +13 -0
- package/README.md +49 -4
- package/package.json +1 -1
- package/scripts/validate-endpoint.mjs +172 -6
- package/src/registry.d.ts +91 -0
- package/src/registry.js +1036 -11
- package/src/remote-proxy.d.ts +20 -0
- package/src/remote-proxy.js +155 -42
- package/src/schemas.d.ts +12 -0
- package/src/schemas.js +119 -0
- package/src/server.js +40 -1
- package/src/submissions.d.ts +13 -0
- package/src/submissions.js +226 -0
package/src/registry.js
CHANGED
|
@@ -7,15 +7,21 @@ import {
|
|
|
7
7
|
platformFeedSlug,
|
|
8
8
|
SITE_URL,
|
|
9
9
|
} from "./platforms.js";
|
|
10
|
+
import { DEFAULT_REMOTE_MCP_URL } from "./endpoint-url.js";
|
|
11
|
+
import { packageName, packageVersion } from "./package-metadata.js";
|
|
10
12
|
import {
|
|
11
13
|
formatZodError,
|
|
12
14
|
jsonSchemaForTool,
|
|
15
|
+
jsonSchemaForToolOutput,
|
|
13
16
|
parseToolArguments,
|
|
14
17
|
} from "./schemas.js";
|
|
15
18
|
import {
|
|
16
19
|
buildSubmissionUrlsFromSpec,
|
|
20
|
+
getSubmissionExamplesFromSpec,
|
|
17
21
|
getCategorySubmissionGuidanceFromSpec,
|
|
22
|
+
prepareSubmissionDraftFromSpec,
|
|
18
23
|
getSubmissionSchemaFromSpec,
|
|
24
|
+
reviewSubmissionDraftFromSpec,
|
|
19
25
|
searchDuplicateEntries,
|
|
20
26
|
validateSubmissionDraftFromSpec,
|
|
21
27
|
} from "./submissions.js";
|
|
@@ -26,6 +32,17 @@ const repoRoot = path.resolve(
|
|
|
26
32
|
);
|
|
27
33
|
const defaultDataDir = path.join(repoRoot, "apps", "web", "public", "data");
|
|
28
34
|
const safePathPartPattern = /^[a-z0-9-]+$/;
|
|
35
|
+
const jsonMimeType = "application/json";
|
|
36
|
+
|
|
37
|
+
export const MCP_PUBLIC_POLICY = {
|
|
38
|
+
apiKeyRequired: false,
|
|
39
|
+
readOnly: true,
|
|
40
|
+
createsIssues: false,
|
|
41
|
+
createsPullRequests: false,
|
|
42
|
+
publishesContent: false,
|
|
43
|
+
writesLocalFiles: false,
|
|
44
|
+
note: "HeyClaude MCP tools only read public registry artifacts or prepare maintainer-reviewed submission drafts.",
|
|
45
|
+
};
|
|
29
46
|
|
|
30
47
|
const platformAliases = new Map([
|
|
31
48
|
["claude", "Claude"],
|
|
@@ -43,7 +60,15 @@ const platformAliases = new Map([
|
|
|
43
60
|
|
|
44
61
|
export const READ_ONLY_TOOL_NAMES = [
|
|
45
62
|
"search_registry",
|
|
63
|
+
"server_info",
|
|
64
|
+
"list_category_entries",
|
|
65
|
+
"get_recent_updates",
|
|
66
|
+
"get_related_entries",
|
|
46
67
|
"get_entry_detail",
|
|
68
|
+
"get_copyable_asset",
|
|
69
|
+
"compare_entries",
|
|
70
|
+
"get_registry_stats",
|
|
71
|
+
"get_client_setup",
|
|
47
72
|
"get_compatibility",
|
|
48
73
|
"get_install_guidance",
|
|
49
74
|
"get_platform_adapter",
|
|
@@ -53,6 +78,9 @@ export const READ_ONLY_TOOL_NAMES = [
|
|
|
53
78
|
"search_duplicate_entries",
|
|
54
79
|
"build_submission_urls",
|
|
55
80
|
"get_category_submission_guidance",
|
|
81
|
+
"prepare_submission_draft",
|
|
82
|
+
"get_submission_examples",
|
|
83
|
+
"review_submission_draft",
|
|
56
84
|
];
|
|
57
85
|
|
|
58
86
|
export const TOOL_DEFINITIONS = [
|
|
@@ -61,6 +89,37 @@ export const TOOL_DEFINITIONS = [
|
|
|
61
89
|
description:
|
|
62
90
|
"Search read-only HeyClaude registry entries by query, category, and skill platform compatibility.",
|
|
63
91
|
inputSchema: jsonSchemaForTool("search_registry"),
|
|
92
|
+
outputSchema: jsonSchemaForToolOutput("search_registry"),
|
|
93
|
+
annotations: {
|
|
94
|
+
readOnlyHint: true,
|
|
95
|
+
destructiveHint: false,
|
|
96
|
+
idempotentHint: true,
|
|
97
|
+
openWorldHint: false,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "server_info",
|
|
102
|
+
description:
|
|
103
|
+
"Fetch read-only HeyClaude MCP package, registry, tool, and public rate-limit metadata.",
|
|
104
|
+
inputSchema: jsonSchemaForTool("server_info"),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "list_category_entries",
|
|
108
|
+
description:
|
|
109
|
+
"List read-only HeyClaude entries with bounded pagination and optional category, platform, tag, and query filters.",
|
|
110
|
+
inputSchema: jsonSchemaForTool("list_category_entries"),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "get_recent_updates",
|
|
114
|
+
description:
|
|
115
|
+
"List recently added or upstream-updated HeyClaude entries from generated registry metadata.",
|
|
116
|
+
inputSchema: jsonSchemaForTool("get_recent_updates"),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "get_related_entries",
|
|
120
|
+
description:
|
|
121
|
+
"Fetch read-only related HeyClaude entries based on category, tags, platforms, keywords, and source metadata.",
|
|
122
|
+
inputSchema: jsonSchemaForTool("get_related_entries"),
|
|
64
123
|
},
|
|
65
124
|
{
|
|
66
125
|
name: "get_entry_detail",
|
|
@@ -68,6 +127,30 @@ export const TOOL_DEFINITIONS = [
|
|
|
68
127
|
"Fetch a read-only HeyClaude registry entry detail payload by category and slug.",
|
|
69
128
|
inputSchema: jsonSchemaForTool("get_entry_detail"),
|
|
70
129
|
},
|
|
130
|
+
{
|
|
131
|
+
name: "get_copyable_asset",
|
|
132
|
+
description:
|
|
133
|
+
"Fetch the category-aware copy/install asset for a HeyClaude entry without writing local files.",
|
|
134
|
+
inputSchema: jsonSchemaForTool("get_copyable_asset"),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "compare_entries",
|
|
138
|
+
description:
|
|
139
|
+
"Compare 2-5 read-only HeyClaude entries by fit, category, platforms, source metadata, and install complexity.",
|
|
140
|
+
inputSchema: jsonSchemaForTool("compare_entries"),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "get_registry_stats",
|
|
144
|
+
description:
|
|
145
|
+
"Fetch aggregate read-only registry stats, freshness, category counts, and real source-signal coverage.",
|
|
146
|
+
inputSchema: jsonSchemaForTool("get_registry_stats"),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "get_client_setup",
|
|
150
|
+
description:
|
|
151
|
+
"Fetch read-only MCP client setup snippets for Codex, Claude Desktop, Cursor, Windsurf, or remote HTTP clients.",
|
|
152
|
+
inputSchema: jsonSchemaForTool("get_client_setup"),
|
|
153
|
+
},
|
|
71
154
|
{
|
|
72
155
|
name: "get_compatibility",
|
|
73
156
|
description:
|
|
@@ -122,8 +205,36 @@ export const TOOL_DEFINITIONS = [
|
|
|
122
205
|
"Fetch category-specific HeyClaude contribution guidance, required fields, and review expectations.",
|
|
123
206
|
inputSchema: jsonSchemaForTool("get_category_submission_guidance"),
|
|
124
207
|
},
|
|
208
|
+
{
|
|
209
|
+
name: "prepare_submission_draft",
|
|
210
|
+
description:
|
|
211
|
+
"Build a read-only maintainer-reviewed HeyClaude submission draft with canonical issue text and URLs.",
|
|
212
|
+
inputSchema: jsonSchemaForTool("prepare_submission_draft"),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "get_submission_examples",
|
|
216
|
+
description:
|
|
217
|
+
"Fetch read-only category examples and templates for faster, more accurate HeyClaude submissions.",
|
|
218
|
+
inputSchema: jsonSchemaForTool("get_submission_examples"),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "review_submission_draft",
|
|
222
|
+
description:
|
|
223
|
+
"Review a HeyClaude submission draft locally for schema errors, duplicate risk, and maintainer checklist items without writing to GitHub.",
|
|
224
|
+
inputSchema: jsonSchemaForTool("review_submission_draft"),
|
|
225
|
+
},
|
|
125
226
|
];
|
|
126
227
|
|
|
228
|
+
for (const tool of TOOL_DEFINITIONS) {
|
|
229
|
+
tool.outputSchema ||= jsonSchemaForToolOutput(tool.name);
|
|
230
|
+
tool.annotations ||= {
|
|
231
|
+
readOnlyHint: true,
|
|
232
|
+
destructiveHint: false,
|
|
233
|
+
idempotentHint: true,
|
|
234
|
+
openWorldHint: false,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
127
238
|
function dataDirFromOptions(options = {}) {
|
|
128
239
|
return options.dataDir || process.env.HEYCLAUDE_DATA_DIR || defaultDataDir;
|
|
129
240
|
}
|
|
@@ -180,6 +291,12 @@ function normalizeLimit(value, fallback = 10) {
|
|
|
180
291
|
return Math.max(1, Math.min(25, Math.trunc(numeric)));
|
|
181
292
|
}
|
|
182
293
|
|
|
294
|
+
function normalizeOffset(value) {
|
|
295
|
+
const numeric = Number(value);
|
|
296
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
297
|
+
return Math.max(0, Math.min(5000, Math.trunc(numeric)));
|
|
298
|
+
}
|
|
299
|
+
|
|
183
300
|
function normalizePlatform(value) {
|
|
184
301
|
const normalized = normalizeText(value).replace(/[^a-z0-9]+/g, "-");
|
|
185
302
|
if (!normalized) return "";
|
|
@@ -211,6 +328,13 @@ function entryMatchesPlatform(entry, platform) {
|
|
|
211
328
|
return (entry.platforms || []).some((candidate) => candidate === platform);
|
|
212
329
|
}
|
|
213
330
|
|
|
331
|
+
function entryMatchesTag(entry, tag) {
|
|
332
|
+
if (!tag) return true;
|
|
333
|
+
return (entry.tags || []).some(
|
|
334
|
+
(candidate) => normalizeText(candidate) === tag,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
214
338
|
function toSearchResult(entry) {
|
|
215
339
|
return {
|
|
216
340
|
key: `${entry.category}:${entry.slug}`,
|
|
@@ -232,6 +356,170 @@ function toSearchResult(entry) {
|
|
|
232
356
|
};
|
|
233
357
|
}
|
|
234
358
|
|
|
359
|
+
function toEntrySummary(entry) {
|
|
360
|
+
return {
|
|
361
|
+
...toSearchResult(entry),
|
|
362
|
+
dateAdded: entry.dateAdded || "",
|
|
363
|
+
repoUpdatedAt: entry.repoUpdatedAt || null,
|
|
364
|
+
verificationStatus: entry.verificationStatus || "",
|
|
365
|
+
installable: Boolean(entry.installable),
|
|
366
|
+
supportLevels: entry.supportLevels || [],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function entryUpdatedAt(entry) {
|
|
371
|
+
return String(
|
|
372
|
+
entry.repoUpdatedAt || entry.updatedAt || entry.dateAdded || "",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function sourceHost(value) {
|
|
377
|
+
const text = String(value || "").trim();
|
|
378
|
+
if (!text) return "";
|
|
379
|
+
try {
|
|
380
|
+
return new URL(text).hostname.toLowerCase().replace(/^www\./, "");
|
|
381
|
+
} catch {
|
|
382
|
+
return "";
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function entrySourceHosts(entry) {
|
|
387
|
+
return [
|
|
388
|
+
entry.documentationUrl,
|
|
389
|
+
entry.repoUrl,
|
|
390
|
+
entry.url,
|
|
391
|
+
entry.canonicalUrl,
|
|
392
|
+
entry.llmsUrl,
|
|
393
|
+
entry.apiUrl,
|
|
394
|
+
]
|
|
395
|
+
.map(sourceHost)
|
|
396
|
+
.filter(Boolean);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function intersection(left = [], right = [], normalize = normalizeText) {
|
|
400
|
+
const rightValues = new Set((right || []).map(normalize).filter(Boolean));
|
|
401
|
+
return (left || [])
|
|
402
|
+
.map(normalize)
|
|
403
|
+
.filter((value, index, values) => value && values.indexOf(value) === index)
|
|
404
|
+
.filter((value) => rightValues.has(value));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function unique(values = []) {
|
|
408
|
+
return values.filter(
|
|
409
|
+
(value, index, list) => value && list.indexOf(value) === index,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function normalizeDateFloor(value) {
|
|
414
|
+
const text = String(value || "").trim();
|
|
415
|
+
if (!text) return "";
|
|
416
|
+
const timestamp = Date.parse(text);
|
|
417
|
+
if (!Number.isFinite(timestamp)) return "";
|
|
418
|
+
return new Date(timestamp).toISOString().slice(0, 10);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function withPublicPolicy(result) {
|
|
422
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
if (result.policy) return result;
|
|
426
|
+
return { ...result, policy: MCP_PUBLIC_POLICY };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function sourceSummary(entry) {
|
|
430
|
+
return {
|
|
431
|
+
repoUrl: entry.repoUrl || entry.githubUrl || "",
|
|
432
|
+
documentationUrl: entry.documentationUrl || "",
|
|
433
|
+
downloadUrl: entry.downloadUrl || "",
|
|
434
|
+
sourceHosts: unique(entrySourceHosts(entry)),
|
|
435
|
+
githubStars:
|
|
436
|
+
typeof entry.githubStars === "number" ? entry.githubStars : null,
|
|
437
|
+
githubForks:
|
|
438
|
+
typeof entry.githubForks === "number" ? entry.githubForks : null,
|
|
439
|
+
repoUpdatedAt: entry.repoUpdatedAt || null,
|
|
440
|
+
downloadTrust: entry.downloadTrust || null,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function contentAsset(type, label, content, format = "markdown") {
|
|
445
|
+
const text =
|
|
446
|
+
content && typeof content === "object"
|
|
447
|
+
? JSON.stringify(content, null, 2)
|
|
448
|
+
: String(content || "").trim();
|
|
449
|
+
if (!text) return null;
|
|
450
|
+
return {
|
|
451
|
+
type,
|
|
452
|
+
label,
|
|
453
|
+
format,
|
|
454
|
+
content: text,
|
|
455
|
+
length: text.length,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function categoryPrimaryAsset(entry) {
|
|
460
|
+
const assets = [
|
|
461
|
+
contentAsset(
|
|
462
|
+
"full_content",
|
|
463
|
+
"Full usable entry content",
|
|
464
|
+
entry.fullCopyableContent || entry.copySnippet || entry.body,
|
|
465
|
+
),
|
|
466
|
+
contentAsset(
|
|
467
|
+
"install_command",
|
|
468
|
+
"Install command",
|
|
469
|
+
entry.installCommand,
|
|
470
|
+
"shell",
|
|
471
|
+
),
|
|
472
|
+
contentAsset(
|
|
473
|
+
"config_snippet",
|
|
474
|
+
"Configuration snippet",
|
|
475
|
+
entry.configSnippet,
|
|
476
|
+
"text",
|
|
477
|
+
),
|
|
478
|
+
contentAsset("script", "Script body", entry.scriptBody, "text"),
|
|
479
|
+
contentAsset(
|
|
480
|
+
"command_syntax",
|
|
481
|
+
"Command syntax",
|
|
482
|
+
entry.commandSyntax,
|
|
483
|
+
"text",
|
|
484
|
+
),
|
|
485
|
+
contentAsset("usage", "Usage snippet", entry.usageSnippet, "markdown"),
|
|
486
|
+
contentAsset("items", "Collection items", entry.items, "json"),
|
|
487
|
+
].filter(Boolean);
|
|
488
|
+
|
|
489
|
+
const preferredByCategory = {
|
|
490
|
+
agents: ["full_content", "usage"],
|
|
491
|
+
rules: ["full_content", "script", "usage"],
|
|
492
|
+
hooks: ["config_snippet", "script", "install_command", "usage"],
|
|
493
|
+
mcp: ["config_snippet", "install_command", "usage"],
|
|
494
|
+
skills: ["install_command", "full_content", "usage"],
|
|
495
|
+
statuslines: ["config_snippet", "script", "full_content", "usage"],
|
|
496
|
+
commands: ["command_syntax", "install_command", "full_content", "usage"],
|
|
497
|
+
collections: ["items", "full_content", "usage"],
|
|
498
|
+
guides: ["full_content", "usage"],
|
|
499
|
+
};
|
|
500
|
+
const preferred = preferredByCategory[entry.category] || ["full_content"];
|
|
501
|
+
return (
|
|
502
|
+
preferred
|
|
503
|
+
.map((type) => assets.find((asset) => asset.type === type))
|
|
504
|
+
.find(Boolean) ||
|
|
505
|
+
assets[0] ||
|
|
506
|
+
null
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function entryInstallComplexity(entry) {
|
|
511
|
+
const pieces = [
|
|
512
|
+
entry.installCommand,
|
|
513
|
+
entry.configSnippet,
|
|
514
|
+
entry.downloadUrl,
|
|
515
|
+
entry.prerequisites,
|
|
516
|
+
].filter((value) => String(value || "").trim());
|
|
517
|
+
if (pieces.length >= 3) return "higher";
|
|
518
|
+
if (pieces.length === 2) return "medium";
|
|
519
|
+
if (pieces.length === 1) return "low";
|
|
520
|
+
return "unknown";
|
|
521
|
+
}
|
|
522
|
+
|
|
235
523
|
async function readEntry(category, slug, options = {}) {
|
|
236
524
|
if (!isSafePathPart(category) || !isSafePathPart(slug)) {
|
|
237
525
|
return null;
|
|
@@ -285,6 +573,190 @@ export async function searchRegistry(args = {}, options = {}) {
|
|
|
285
573
|
};
|
|
286
574
|
}
|
|
287
575
|
|
|
576
|
+
export async function getServerInfo(args = {}, options = {}) {
|
|
577
|
+
const manifest = await readJsonArtifact("registry-manifest.json", options);
|
|
578
|
+
return {
|
|
579
|
+
ok: true,
|
|
580
|
+
package: {
|
|
581
|
+
name: packageName,
|
|
582
|
+
version: packageVersion,
|
|
583
|
+
},
|
|
584
|
+
endpoint: {
|
|
585
|
+
url: DEFAULT_REMOTE_MCP_URL,
|
|
586
|
+
auth: "none",
|
|
587
|
+
transport: "streamable-http",
|
|
588
|
+
stdioBridge: "npx -y @heyclaude/mcp",
|
|
589
|
+
requestBodyLimitBytes: 64 * 1024,
|
|
590
|
+
rateLimit: {
|
|
591
|
+
scope: "mcp-streamable",
|
|
592
|
+
limit: 60,
|
|
593
|
+
windowSeconds: 60,
|
|
594
|
+
binding: "API_MCP_RATE_LIMIT",
|
|
595
|
+
note: "Cloudflare enforces the durable production limit when the binding is available; local/dev falls back to an in-process limiter.",
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
registry: {
|
|
599
|
+
schemaVersion: manifest.schemaVersion,
|
|
600
|
+
generatedAt: manifest.generatedAt,
|
|
601
|
+
totalEntries: manifest.totalEntries,
|
|
602
|
+
categories: manifest.categories || {},
|
|
603
|
+
},
|
|
604
|
+
tools: READ_ONLY_TOOL_NAMES,
|
|
605
|
+
policy: MCP_PUBLIC_POLICY,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export async function listCategoryEntries(args = {}, options = {}) {
|
|
610
|
+
const category = normalizeText(args.category);
|
|
611
|
+
const platform = normalizePlatform(args.platform);
|
|
612
|
+
const tag = normalizeText(args.tag);
|
|
613
|
+
const query = normalizeText(args.query);
|
|
614
|
+
const offset = normalizeOffset(args.offset);
|
|
615
|
+
const limit = normalizeLimit(args.limit, 20);
|
|
616
|
+
const searchIndex = unwrapEntries(
|
|
617
|
+
await readJsonArtifact("search-index.json", options),
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const entries = searchIndex
|
|
621
|
+
.filter((entry) => !category || entry.category === category)
|
|
622
|
+
.filter((entry) => entryMatchesPlatform(entry, platform))
|
|
623
|
+
.filter((entry) => entryMatchesTag(entry, tag))
|
|
624
|
+
.filter((entry) => entryMatchesQuery(entry, query));
|
|
625
|
+
const page = entries.slice(offset, offset + limit).map(toEntrySummary);
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
ok: true,
|
|
629
|
+
category: category || "",
|
|
630
|
+
platform: platform || "",
|
|
631
|
+
tag: tag || "",
|
|
632
|
+
query: args.query || "",
|
|
633
|
+
total: entries.length,
|
|
634
|
+
count: page.length,
|
|
635
|
+
offset,
|
|
636
|
+
limit,
|
|
637
|
+
nextOffset: offset + limit < entries.length ? offset + limit : null,
|
|
638
|
+
entries: page,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export async function getRecentUpdates(args = {}, options = {}) {
|
|
643
|
+
const category = normalizeText(args.category);
|
|
644
|
+
const since = args.since ? normalizeDateFloor(args.since) : "";
|
|
645
|
+
if (args.since && !since) {
|
|
646
|
+
return invalid("since must be a parseable date such as 2026-05-01.");
|
|
647
|
+
}
|
|
648
|
+
const limit = normalizeLimit(args.limit, 10);
|
|
649
|
+
const searchIndex = unwrapEntries(
|
|
650
|
+
await readJsonArtifact("search-index.json", options),
|
|
651
|
+
);
|
|
652
|
+
const entries = searchIndex
|
|
653
|
+
.filter((entry) => !category || entry.category === category)
|
|
654
|
+
.filter((entry) => !since || entryUpdatedAt(entry) >= since)
|
|
655
|
+
.slice()
|
|
656
|
+
.sort((left, right) => {
|
|
657
|
+
const dateCompare = entryUpdatedAt(right).localeCompare(
|
|
658
|
+
entryUpdatedAt(left),
|
|
659
|
+
);
|
|
660
|
+
if (dateCompare !== 0) return dateCompare;
|
|
661
|
+
return String(left.title || "").localeCompare(String(right.title || ""));
|
|
662
|
+
})
|
|
663
|
+
.slice(0, limit)
|
|
664
|
+
.map((entry) => ({
|
|
665
|
+
...toEntrySummary(entry),
|
|
666
|
+
updatedAt: entryUpdatedAt(entry),
|
|
667
|
+
updateKind: entry.repoUpdatedAt ? "upstream_update" : "added",
|
|
668
|
+
}));
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
ok: true,
|
|
672
|
+
category: category || "",
|
|
673
|
+
since,
|
|
674
|
+
count: entries.length,
|
|
675
|
+
entries,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function scoreRelatedEntry(target, candidate) {
|
|
680
|
+
if (
|
|
681
|
+
target.category === candidate.category &&
|
|
682
|
+
target.slug === candidate.slug
|
|
683
|
+
) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const sharedTags = intersection(target.tags, candidate.tags);
|
|
688
|
+
const sharedKeywords = intersection(target.keywords, candidate.keywords);
|
|
689
|
+
const sharedPlatforms = intersection(
|
|
690
|
+
target.platforms,
|
|
691
|
+
candidate.platforms,
|
|
692
|
+
(value) => String(value || ""),
|
|
693
|
+
);
|
|
694
|
+
const sharedHosts = intersection(
|
|
695
|
+
entrySourceHosts(target),
|
|
696
|
+
entrySourceHosts(candidate),
|
|
697
|
+
(value) => String(value || ""),
|
|
698
|
+
);
|
|
699
|
+
const score =
|
|
700
|
+
(target.category === candidate.category ? 4 : 0) +
|
|
701
|
+
sharedTags.length * 3 +
|
|
702
|
+
Math.min(sharedKeywords.length, 6) +
|
|
703
|
+
sharedPlatforms.length +
|
|
704
|
+
sharedHosts.length * 2;
|
|
705
|
+
|
|
706
|
+
if (score <= 0) return null;
|
|
707
|
+
return {
|
|
708
|
+
score,
|
|
709
|
+
reasons: [
|
|
710
|
+
...(target.category === candidate.category ? ["same_category"] : []),
|
|
711
|
+
...sharedTags.map((tag) => `tag:${tag}`),
|
|
712
|
+
...sharedPlatforms.map((platform) => `platform:${platform}`),
|
|
713
|
+
...sharedHosts.map((host) => `source:${host}`),
|
|
714
|
+
],
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export async function getRelatedEntries(args = {}, options = {}) {
|
|
719
|
+
const category = normalizeText(args.category);
|
|
720
|
+
const slug = normalizeText(args.slug);
|
|
721
|
+
const limit = normalizeLimit(args.limit, 8);
|
|
722
|
+
const searchIndex = unwrapEntries(
|
|
723
|
+
await readJsonArtifact("search-index.json", options),
|
|
724
|
+
);
|
|
725
|
+
const target = searchIndex.find(
|
|
726
|
+
(entry) => entry.category === category && entry.slug === slug,
|
|
727
|
+
);
|
|
728
|
+
if (!target) {
|
|
729
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const entries = searchIndex
|
|
733
|
+
.map((entry) => {
|
|
734
|
+
const related = scoreRelatedEntry(target, entry);
|
|
735
|
+
return related ? { entry, related } : null;
|
|
736
|
+
})
|
|
737
|
+
.filter(Boolean)
|
|
738
|
+
.sort((left, right) => {
|
|
739
|
+
const scoreCompare = right.related.score - left.related.score;
|
|
740
|
+
if (scoreCompare !== 0) return scoreCompare;
|
|
741
|
+
return entryUpdatedAt(right.entry).localeCompare(
|
|
742
|
+
entryUpdatedAt(left.entry),
|
|
743
|
+
);
|
|
744
|
+
})
|
|
745
|
+
.slice(0, limit)
|
|
746
|
+
.map(({ entry, related }) => ({
|
|
747
|
+
...toEntrySummary(entry),
|
|
748
|
+
relatedScore: related.score,
|
|
749
|
+
relatedReasons: related.reasons,
|
|
750
|
+
}));
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
ok: true,
|
|
754
|
+
key: `${target.category}:${target.slug}`,
|
|
755
|
+
count: entries.length,
|
|
756
|
+
entries,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
288
760
|
export async function getEntryDetail(args = {}, options = {}) {
|
|
289
761
|
const category = normalizeText(args.category);
|
|
290
762
|
const slug = normalizeText(args.slug);
|
|
@@ -305,6 +777,493 @@ export async function getEntryDetail(args = {}, options = {}) {
|
|
|
305
777
|
};
|
|
306
778
|
}
|
|
307
779
|
|
|
780
|
+
export async function getCopyableAsset(args = {}, options = {}) {
|
|
781
|
+
const category = normalizeText(args.category);
|
|
782
|
+
const slug = normalizeText(args.slug);
|
|
783
|
+
const platform = normalizePlatform(args.platform);
|
|
784
|
+
if (!category || !slug) {
|
|
785
|
+
return invalid("category and slug are required.");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const entry = await readEntry(category, slug, options);
|
|
789
|
+
if (!entry) {
|
|
790
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const primary = categoryPrimaryAsset(entry);
|
|
794
|
+
const assets = [
|
|
795
|
+
contentAsset(
|
|
796
|
+
"full_content",
|
|
797
|
+
"Full usable entry content",
|
|
798
|
+
entry.fullCopyableContent || entry.copySnippet || entry.body,
|
|
799
|
+
),
|
|
800
|
+
contentAsset(
|
|
801
|
+
"install_command",
|
|
802
|
+
"Install command",
|
|
803
|
+
entry.installCommand,
|
|
804
|
+
"shell",
|
|
805
|
+
),
|
|
806
|
+
contentAsset(
|
|
807
|
+
"config_snippet",
|
|
808
|
+
"Configuration snippet",
|
|
809
|
+
entry.configSnippet,
|
|
810
|
+
"text",
|
|
811
|
+
),
|
|
812
|
+
contentAsset("script", "Script body", entry.scriptBody, "text"),
|
|
813
|
+
contentAsset(
|
|
814
|
+
"command_syntax",
|
|
815
|
+
"Command syntax",
|
|
816
|
+
entry.commandSyntax,
|
|
817
|
+
"text",
|
|
818
|
+
),
|
|
819
|
+
contentAsset("usage", "Usage snippet", entry.usageSnippet, "markdown"),
|
|
820
|
+
contentAsset("items", "Collection items", entry.items, "json"),
|
|
821
|
+
].filter(Boolean);
|
|
822
|
+
const compatibility = buildSkillPlatformCompatibility(entry);
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
ok: true,
|
|
826
|
+
key: `${entry.category}:${entry.slug}`,
|
|
827
|
+
category: entry.category,
|
|
828
|
+
slug: entry.slug,
|
|
829
|
+
title: entry.title,
|
|
830
|
+
canonicalUrl: `${SITE_URL}/${entry.category}/${entry.slug}`,
|
|
831
|
+
platform: platform || "",
|
|
832
|
+
primaryAsset: primary,
|
|
833
|
+
assets,
|
|
834
|
+
installCommand: entry.installCommand || "",
|
|
835
|
+
configSnippet: entry.configSnippet || "",
|
|
836
|
+
usageSnippet: entry.usageSnippet || "",
|
|
837
|
+
downloadUrl: entry.downloadUrl || "",
|
|
838
|
+
platformCompatibility: compatibility,
|
|
839
|
+
source: sourceSummary(entry),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export async function compareEntries(args = {}, options = {}) {
|
|
844
|
+
const platform = normalizePlatform(args.platform);
|
|
845
|
+
const entries = [];
|
|
846
|
+
for (const target of args.entries || []) {
|
|
847
|
+
const category = normalizeText(target.category);
|
|
848
|
+
const slug = normalizeText(target.slug);
|
|
849
|
+
const entry = await readEntry(category, slug, options);
|
|
850
|
+
if (!entry) {
|
|
851
|
+
return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
|
|
852
|
+
}
|
|
853
|
+
entries.push(entry);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const compared = entries.map((entry) => {
|
|
857
|
+
const compatibility = buildSkillPlatformCompatibility(entry);
|
|
858
|
+
const selectedCompatibility = platform
|
|
859
|
+
? compatibility.find((item) => item.platform === platform) || null
|
|
860
|
+
: null;
|
|
861
|
+
return {
|
|
862
|
+
key: `${entry.category}:${entry.slug}`,
|
|
863
|
+
category: entry.category,
|
|
864
|
+
slug: entry.slug,
|
|
865
|
+
title: entry.title,
|
|
866
|
+
description: entry.description,
|
|
867
|
+
canonicalUrl: `${SITE_URL}/${entry.category}/${entry.slug}`,
|
|
868
|
+
tags: entry.tags || [],
|
|
869
|
+
platforms: entry.platforms || [],
|
|
870
|
+
selectedCompatibility,
|
|
871
|
+
installComplexity: entryInstallComplexity(entry),
|
|
872
|
+
copyableAssetTypes: [
|
|
873
|
+
categoryPrimaryAsset(entry)?.type,
|
|
874
|
+
entry.configSnippet ? "config_snippet" : "",
|
|
875
|
+
entry.installCommand ? "install_command" : "",
|
|
876
|
+
entry.scriptBody ? "script" : "",
|
|
877
|
+
].filter(Boolean),
|
|
878
|
+
source: sourceSummary(entry),
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
ok: true,
|
|
884
|
+
platform: platform || "",
|
|
885
|
+
count: compared.length,
|
|
886
|
+
sharedTags: intersection(
|
|
887
|
+
compared[0]?.tags || [],
|
|
888
|
+
compared.slice(1).flatMap((entry) => entry.tags || []),
|
|
889
|
+
),
|
|
890
|
+
entries: compared,
|
|
891
|
+
comparisonNotes: [
|
|
892
|
+
"Prefer exact category fit before source popularity.",
|
|
893
|
+
"Treat GitHub stars/forks as source signals only when present; absence is not a negative ranking.",
|
|
894
|
+
"Install complexity is derived from available install/config/download/prerequisite metadata.",
|
|
895
|
+
],
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export async function getRegistryStats(args = {}, options = {}) {
|
|
900
|
+
const [manifest, searchIndexPayload] = await Promise.all([
|
|
901
|
+
readJsonArtifact("registry-manifest.json", options),
|
|
902
|
+
readJsonArtifact("search-index.json", options),
|
|
903
|
+
]);
|
|
904
|
+
const entries = unwrapEntries(searchIndexPayload);
|
|
905
|
+
const platformCounts = new Map();
|
|
906
|
+
const tagCounts = new Map();
|
|
907
|
+
for (const entry of entries) {
|
|
908
|
+
for (const platform of entry.platforms || []) {
|
|
909
|
+
platformCounts.set(platform, (platformCounts.get(platform) || 0) + 1);
|
|
910
|
+
}
|
|
911
|
+
for (const tag of entry.tags || []) {
|
|
912
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
ok: true,
|
|
918
|
+
package: {
|
|
919
|
+
name: packageName,
|
|
920
|
+
version: packageVersion,
|
|
921
|
+
},
|
|
922
|
+
registry: {
|
|
923
|
+
schemaVersion: manifest.schemaVersion,
|
|
924
|
+
generatedAt: manifest.generatedAt,
|
|
925
|
+
totalEntries: manifest.totalEntries,
|
|
926
|
+
categories: manifest.categories || {},
|
|
927
|
+
},
|
|
928
|
+
freshness: {
|
|
929
|
+
entriesWithRepoUpdatedAt: entries.filter((entry) => entry.repoUpdatedAt)
|
|
930
|
+
.length,
|
|
931
|
+
entriesAddedLast30Days: entries.filter((entry) => {
|
|
932
|
+
const added = Date.parse(entry.dateAdded || "");
|
|
933
|
+
return (
|
|
934
|
+
Number.isFinite(added) &&
|
|
935
|
+
Date.now() - added <= 30 * 24 * 60 * 60 * 1000
|
|
936
|
+
);
|
|
937
|
+
}).length,
|
|
938
|
+
},
|
|
939
|
+
sourceSignals: {
|
|
940
|
+
entriesWithGithubStats: entries.filter(
|
|
941
|
+
(entry) => typeof entry.githubStars === "number",
|
|
942
|
+
).length,
|
|
943
|
+
installableEntries: entries.filter((entry) => entry.installable).length,
|
|
944
|
+
},
|
|
945
|
+
platforms: Object.fromEntries(
|
|
946
|
+
[...platformCounts.entries()].sort((left, right) =>
|
|
947
|
+
left[0].localeCompare(right[0]),
|
|
948
|
+
),
|
|
949
|
+
),
|
|
950
|
+
topTags: [...tagCounts.entries()]
|
|
951
|
+
.sort(
|
|
952
|
+
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
|
|
953
|
+
)
|
|
954
|
+
.slice(0, 20)
|
|
955
|
+
.map(([tag, count]) => ({ tag, count })),
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export async function getClientSetup(args = {}) {
|
|
960
|
+
const endpointUrl = args.endpointUrl || DEFAULT_REMOTE_MCP_URL;
|
|
961
|
+
const snippets = {
|
|
962
|
+
codex: {
|
|
963
|
+
label: "Codex stdio bridge",
|
|
964
|
+
config: {
|
|
965
|
+
mcpServers: {
|
|
966
|
+
heyclaude: {
|
|
967
|
+
command: "npx",
|
|
968
|
+
args: ["-y", "@heyclaude/mcp"],
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
"claude-desktop": {
|
|
974
|
+
label: "Claude Desktop stdio bridge",
|
|
975
|
+
config: {
|
|
976
|
+
mcpServers: {
|
|
977
|
+
heyclaude: {
|
|
978
|
+
command: "npx",
|
|
979
|
+
args: ["-y", "@heyclaude/mcp"],
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
cursor: {
|
|
985
|
+
label: "Cursor remote MCP",
|
|
986
|
+
config: {
|
|
987
|
+
mcpServers: {
|
|
988
|
+
heyclaude: {
|
|
989
|
+
url: endpointUrl,
|
|
990
|
+
},
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
windsurf: {
|
|
995
|
+
label: "Windsurf remote MCP",
|
|
996
|
+
config: {
|
|
997
|
+
mcpServers: {
|
|
998
|
+
heyclaude: {
|
|
999
|
+
serverUrl: endpointUrl,
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
"remote-http": {
|
|
1005
|
+
label: "Streamable HTTP endpoint",
|
|
1006
|
+
endpointUrl,
|
|
1007
|
+
headers: {
|
|
1008
|
+
accept: "application/json, text/event-stream",
|
|
1009
|
+
"content-type": "application/json",
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
const client = args.client || "";
|
|
1014
|
+
return {
|
|
1015
|
+
ok: true,
|
|
1016
|
+
endpointUrl,
|
|
1017
|
+
apiKeyRequired: false,
|
|
1018
|
+
selectedClient: client,
|
|
1019
|
+
snippets: client ? { [client]: snippets[client] } : snippets,
|
|
1020
|
+
notes: [
|
|
1021
|
+
"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.",
|
|
1023
|
+
"Use --url only when testing a custom preview or deployment.",
|
|
1024
|
+
],
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
export const RESOURCE_TEMPLATES = [
|
|
1029
|
+
{
|
|
1030
|
+
uriTemplate: "heyclaude://entry/{category}/{slug}",
|
|
1031
|
+
name: "HeyClaude entry detail",
|
|
1032
|
+
title: "HeyClaude entry detail",
|
|
1033
|
+
description:
|
|
1034
|
+
"Read a single generated HeyClaude entry detail artifact as JSON.",
|
|
1035
|
+
mimeType: jsonMimeType,
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
uriTemplate: "heyclaude://category/{category}",
|
|
1039
|
+
name: "HeyClaude category entries",
|
|
1040
|
+
title: "HeyClaude category entries",
|
|
1041
|
+
description:
|
|
1042
|
+
"Read generated summary entries for one HeyClaude category as JSON.",
|
|
1043
|
+
mimeType: jsonMimeType,
|
|
1044
|
+
},
|
|
1045
|
+
];
|
|
1046
|
+
|
|
1047
|
+
export const PROMPT_DEFINITIONS = [
|
|
1048
|
+
{
|
|
1049
|
+
name: "find_best_asset",
|
|
1050
|
+
title: "Find the best Claude asset",
|
|
1051
|
+
description:
|
|
1052
|
+
"Guide a client through searching, comparing, and recommending HeyClaude entries for a use case.",
|
|
1053
|
+
arguments: [
|
|
1054
|
+
{
|
|
1055
|
+
name: "use_case",
|
|
1056
|
+
description: "The task, workflow, or problem the user wants to solve.",
|
|
1057
|
+
required: true,
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: "category",
|
|
1061
|
+
description: "Optional HeyClaude category to constrain discovery.",
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
name: "platform",
|
|
1065
|
+
description:
|
|
1066
|
+
"Optional client/platform such as Claude, Codex, Cursor, or Windsurf.",
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
name: "prepare_submission",
|
|
1072
|
+
title: "Prepare a HeyClaude submission",
|
|
1073
|
+
description:
|
|
1074
|
+
"Guide a user through drafting a maintainer-reviewed HeyClaude submission without opening an issue automatically.",
|
|
1075
|
+
arguments: [
|
|
1076
|
+
{ name: "category", description: "Submission category.", required: true },
|
|
1077
|
+
{ name: "name", description: "Submission name or title." },
|
|
1078
|
+
{
|
|
1079
|
+
name: "source_url",
|
|
1080
|
+
description: "Primary source, docs, package, or repo URL.",
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
name: "review_submission_before_issue",
|
|
1086
|
+
title: "Review submission before opening issue",
|
|
1087
|
+
description:
|
|
1088
|
+
"Check a draft for schema gaps, duplicate risk, source review, and maintainer checklist items.",
|
|
1089
|
+
arguments: [
|
|
1090
|
+
{
|
|
1091
|
+
name: "draft",
|
|
1092
|
+
description: "A concise description or JSON-shaped draft fields.",
|
|
1093
|
+
required: true,
|
|
1094
|
+
},
|
|
1095
|
+
],
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
name: "install_asset_safely",
|
|
1099
|
+
title: "Install a HeyClaude asset safely",
|
|
1100
|
+
description:
|
|
1101
|
+
"Guide installation/use of one entry while keeping source and secret-handling checks explicit.",
|
|
1102
|
+
arguments: [
|
|
1103
|
+
{ name: "category", description: "Entry category.", required: true },
|
|
1104
|
+
{ name: "slug", description: "Entry slug.", required: true },
|
|
1105
|
+
{ name: "platform", description: "Optional target client/platform." },
|
|
1106
|
+
],
|
|
1107
|
+
},
|
|
1108
|
+
];
|
|
1109
|
+
|
|
1110
|
+
export async function listRegistryResources(args = {}, options = {}) {
|
|
1111
|
+
const manifest = await readJsonArtifact("registry-manifest.json", options);
|
|
1112
|
+
const categories = Object.keys(manifest.categories || {}).sort();
|
|
1113
|
+
return {
|
|
1114
|
+
resources: [
|
|
1115
|
+
{
|
|
1116
|
+
uri: "heyclaude://feeds/directory",
|
|
1117
|
+
name: "HeyClaude directory index",
|
|
1118
|
+
title: "HeyClaude directory index",
|
|
1119
|
+
description: "Generated public directory index artifact.",
|
|
1120
|
+
mimeType: jsonMimeType,
|
|
1121
|
+
},
|
|
1122
|
+
...categories.map((category) => ({
|
|
1123
|
+
uri: `heyclaude://category/${category}`,
|
|
1124
|
+
name: `HeyClaude ${category} category`,
|
|
1125
|
+
title: `HeyClaude ${category}`,
|
|
1126
|
+
description: `Generated public ${category} category summary entries.`,
|
|
1127
|
+
mimeType: jsonMimeType,
|
|
1128
|
+
})),
|
|
1129
|
+
],
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
export function listRegistryResourceTemplates() {
|
|
1134
|
+
return {
|
|
1135
|
+
resourceTemplates: RESOURCE_TEMPLATES,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export async function readRegistryResource(args = {}, options = {}) {
|
|
1140
|
+
const uri = String(args.uri || "");
|
|
1141
|
+
const resourcePayload = (payload) => ({
|
|
1142
|
+
contents: [
|
|
1143
|
+
{
|
|
1144
|
+
uri: uri || "heyclaude://error",
|
|
1145
|
+
mimeType: jsonMimeType,
|
|
1146
|
+
text: JSON.stringify(withPublicPolicy(payload), null, 2),
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
});
|
|
1150
|
+
let parsed;
|
|
1151
|
+
try {
|
|
1152
|
+
parsed = new URL(uri);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return resourcePayload(
|
|
1155
|
+
notFound(`Unsupported HeyClaude resource URI: ${uri}`),
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (parsed.protocol !== "heyclaude:") {
|
|
1159
|
+
return resourcePayload(
|
|
1160
|
+
notFound(`Unsupported HeyClaude resource URI: ${uri}`),
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
1165
|
+
let payload;
|
|
1166
|
+
if (parsed.hostname === "feeds" && parts[0] === "directory") {
|
|
1167
|
+
payload = await readJsonArtifact("directory-index.json", options);
|
|
1168
|
+
} else if (parsed.hostname === "category" && parts.length === 1) {
|
|
1169
|
+
const category = normalizeText(parts[0]);
|
|
1170
|
+
if (!isSafePathPart(category)) {
|
|
1171
|
+
return resourcePayload(
|
|
1172
|
+
invalid("Category resource path is not slug-safe."),
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
const entries = unwrapEntries(
|
|
1176
|
+
await readJsonArtifact("search-index.json", options),
|
|
1177
|
+
)
|
|
1178
|
+
.filter((entry) => entry.category === category)
|
|
1179
|
+
.map(toEntrySummary);
|
|
1180
|
+
payload = {
|
|
1181
|
+
ok: true,
|
|
1182
|
+
category,
|
|
1183
|
+
total: entries.length,
|
|
1184
|
+
entries,
|
|
1185
|
+
};
|
|
1186
|
+
} else if (parsed.hostname === "entry" && parts.length === 2) {
|
|
1187
|
+
const [category, slug] = parts.map(normalizeText);
|
|
1188
|
+
const detail = await getEntryDetail({ category, slug }, options);
|
|
1189
|
+
payload = detail;
|
|
1190
|
+
} else {
|
|
1191
|
+
return resourcePayload(
|
|
1192
|
+
notFound(`Unsupported HeyClaude resource URI: ${uri}`),
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return resourcePayload(payload);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function promptArgument(args, name) {
|
|
1200
|
+
return String(args?.[name] || "").trim();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export function listRegistryPrompts() {
|
|
1204
|
+
return {
|
|
1205
|
+
prompts: PROMPT_DEFINITIONS,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export function getRegistryPrompt(args = {}) {
|
|
1210
|
+
const name = String(args.name || "");
|
|
1211
|
+
const prompt = PROMPT_DEFINITIONS.find(
|
|
1212
|
+
(candidate) => candidate.name === name,
|
|
1213
|
+
);
|
|
1214
|
+
if (!prompt) {
|
|
1215
|
+
return {
|
|
1216
|
+
description: "Unknown HeyClaude MCP prompt.",
|
|
1217
|
+
messages: [
|
|
1218
|
+
{
|
|
1219
|
+
role: "user",
|
|
1220
|
+
content: {
|
|
1221
|
+
type: "text",
|
|
1222
|
+
text: `Unknown HeyClaude MCP prompt: ${name}`,
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
],
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
const values = args.arguments || {};
|
|
1229
|
+
const useCase = promptArgument(values, "use_case");
|
|
1230
|
+
const category = promptArgument(values, "category");
|
|
1231
|
+
const platform = promptArgument(values, "platform");
|
|
1232
|
+
const slug = promptArgument(values, "slug");
|
|
1233
|
+
const sourceUrl = promptArgument(values, "source_url");
|
|
1234
|
+
const draft = promptArgument(values, "draft");
|
|
1235
|
+
|
|
1236
|
+
const promptTextByName = {
|
|
1237
|
+
find_best_asset: `Find the best HeyClaude asset for this use case: ${useCase || "(not provided)"}.
|
|
1238
|
+
|
|
1239
|
+
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
|
+
prepare_submission: `Prepare a HeyClaude submission draft${category ? ` for category ${category}` : ""}${promptArgument(values, "name") ? ` named ${promptArgument(values, "name")}` : ""}${sourceUrl ? ` from ${sourceUrl}` : ""}.
|
|
1241
|
+
|
|
1242
|
+
Use get_submission_schema, get_submission_examples, prepare_submission_draft, review_submission_draft, and search_duplicate_entries. Return missing fields and the canonical issue draft URL/body. Do not create a GitHub issue or publish content.`,
|
|
1243
|
+
review_submission_before_issue: `Review this HeyClaude submission draft before an issue is opened:
|
|
1244
|
+
|
|
1245
|
+
${draft || "(draft not provided)"}
|
|
1246
|
+
|
|
1247
|
+
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.`,
|
|
1248
|
+
install_asset_safely: `Help install or use the HeyClaude entry ${category || "(category)"}/${slug || "(slug)"}${platform ? ` for ${platform}` : ""}.
|
|
1249
|
+
|
|
1250
|
+
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.`,
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
return {
|
|
1254
|
+
description: prompt.description,
|
|
1255
|
+
messages: [
|
|
1256
|
+
{
|
|
1257
|
+
role: "user",
|
|
1258
|
+
content: {
|
|
1259
|
+
type: "text",
|
|
1260
|
+
text: promptTextByName[name],
|
|
1261
|
+
},
|
|
1262
|
+
},
|
|
1263
|
+
],
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
308
1267
|
export async function getCompatibility(args = {}, options = {}) {
|
|
309
1268
|
const category = normalizeText(args.category || "skills");
|
|
310
1269
|
const slug = normalizeText(args.slug);
|
|
@@ -450,6 +1409,25 @@ export async function getCategorySubmissionGuidance(args = {}, options = {}) {
|
|
|
450
1409
|
);
|
|
451
1410
|
}
|
|
452
1411
|
|
|
1412
|
+
export async function prepareSubmissionDraft(args = {}, options = {}) {
|
|
1413
|
+
return prepareSubmissionDraftFromSpec(
|
|
1414
|
+
await readSubmissionSpec(options),
|
|
1415
|
+
args,
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
export async function getSubmissionExamples(args = {}, options = {}) {
|
|
1420
|
+
return getSubmissionExamplesFromSpec(await readSubmissionSpec(options), args);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
export async function reviewSubmissionDraft(args = {}, options = {}) {
|
|
1424
|
+
const [spec, searchIndex] = await Promise.all([
|
|
1425
|
+
readSubmissionSpec(options),
|
|
1426
|
+
readJsonArtifact("search-index.json", options),
|
|
1427
|
+
]);
|
|
1428
|
+
return reviewSubmissionDraftFromSpec(spec, args, unwrapEntries(searchIndex));
|
|
1429
|
+
}
|
|
1430
|
+
|
|
453
1431
|
export async function callRegistryTool(name, args = {}, options = {}) {
|
|
454
1432
|
if (!READ_ONLY_TOOL_NAMES.includes(name)) {
|
|
455
1433
|
return invalid(`Unknown read-only HeyClaude MCP tool: ${name}`);
|
|
@@ -469,28 +1447,75 @@ export async function callRegistryTool(name, args = {}, options = {}) {
|
|
|
469
1447
|
throw error;
|
|
470
1448
|
}
|
|
471
1449
|
|
|
1450
|
+
let result;
|
|
472
1451
|
switch (name) {
|
|
473
1452
|
case "search_registry":
|
|
474
|
-
|
|
1453
|
+
result = await searchRegistry(parsedArgs, options);
|
|
1454
|
+
break;
|
|
1455
|
+
case "server_info":
|
|
1456
|
+
result = await getServerInfo(parsedArgs, options);
|
|
1457
|
+
break;
|
|
1458
|
+
case "list_category_entries":
|
|
1459
|
+
result = await listCategoryEntries(parsedArgs, options);
|
|
1460
|
+
break;
|
|
1461
|
+
case "get_recent_updates":
|
|
1462
|
+
result = await getRecentUpdates(parsedArgs, options);
|
|
1463
|
+
break;
|
|
1464
|
+
case "get_related_entries":
|
|
1465
|
+
result = await getRelatedEntries(parsedArgs, options);
|
|
1466
|
+
break;
|
|
475
1467
|
case "get_entry_detail":
|
|
476
|
-
|
|
1468
|
+
result = await getEntryDetail(parsedArgs, options);
|
|
1469
|
+
break;
|
|
1470
|
+
case "get_copyable_asset":
|
|
1471
|
+
result = await getCopyableAsset(parsedArgs, options);
|
|
1472
|
+
break;
|
|
1473
|
+
case "compare_entries":
|
|
1474
|
+
result = await compareEntries(parsedArgs, options);
|
|
1475
|
+
break;
|
|
1476
|
+
case "get_registry_stats":
|
|
1477
|
+
result = await getRegistryStats(parsedArgs, options);
|
|
1478
|
+
break;
|
|
1479
|
+
case "get_client_setup":
|
|
1480
|
+
result = await getClientSetup(parsedArgs, options);
|
|
1481
|
+
break;
|
|
477
1482
|
case "get_compatibility":
|
|
478
|
-
|
|
1483
|
+
result = await getCompatibility(parsedArgs, options);
|
|
1484
|
+
break;
|
|
479
1485
|
case "get_install_guidance":
|
|
480
|
-
|
|
1486
|
+
result = await getInstallGuidance(parsedArgs, options);
|
|
1487
|
+
break;
|
|
481
1488
|
case "get_platform_adapter":
|
|
482
|
-
|
|
1489
|
+
result = await getPlatformAdapter(parsedArgs, options);
|
|
1490
|
+
break;
|
|
483
1491
|
case "list_distribution_feeds":
|
|
484
|
-
|
|
1492
|
+
result = await listDistributionFeeds(parsedArgs, options);
|
|
1493
|
+
break;
|
|
485
1494
|
case "get_submission_schema":
|
|
486
|
-
|
|
1495
|
+
result = await getSubmissionSchema(parsedArgs, options);
|
|
1496
|
+
break;
|
|
487
1497
|
case "validate_submission_draft":
|
|
488
|
-
|
|
1498
|
+
result = await validateSubmissionDraft(parsedArgs, options);
|
|
1499
|
+
break;
|
|
489
1500
|
case "search_duplicate_entries":
|
|
490
|
-
|
|
1501
|
+
result = await searchDuplicateRegistryEntries(parsedArgs, options);
|
|
1502
|
+
break;
|
|
491
1503
|
case "build_submission_urls":
|
|
492
|
-
|
|
1504
|
+
result = await buildSubmissionUrls(parsedArgs, options);
|
|
1505
|
+
break;
|
|
493
1506
|
case "get_category_submission_guidance":
|
|
494
|
-
|
|
1507
|
+
result = await getCategorySubmissionGuidance(parsedArgs, options);
|
|
1508
|
+
break;
|
|
1509
|
+
case "prepare_submission_draft":
|
|
1510
|
+
result = await prepareSubmissionDraft(parsedArgs, options);
|
|
1511
|
+
break;
|
|
1512
|
+
case "get_submission_examples":
|
|
1513
|
+
result = await getSubmissionExamples(parsedArgs, options);
|
|
1514
|
+
break;
|
|
1515
|
+
case "review_submission_draft":
|
|
1516
|
+
result = await reviewSubmissionDraft(parsedArgs, options);
|
|
1517
|
+
break;
|
|
495
1518
|
}
|
|
1519
|
+
|
|
1520
|
+
return withPublicPolicy(result);
|
|
496
1521
|
}
|