@heyclaude/mcp 0.1.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.
@@ -0,0 +1,97 @@
1
+ export type RegistryToolResult = {
2
+ ok: boolean;
3
+ [key: string]: unknown;
4
+ };
5
+
6
+ export type RegistryArtifactLoaders = {
7
+ dataDir?: string;
8
+ readJsonArtifact?: <T = unknown>(relativePath: string) => Promise<T>;
9
+ readTextArtifact?: (relativePath: string) => Promise<string>;
10
+ };
11
+
12
+ export const READ_ONLY_TOOL_NAMES: string[];
13
+ export const TOOL_DEFINITIONS: Array<{
14
+ name: string;
15
+ description: string;
16
+ inputSchema: Record<string, unknown>;
17
+ }>;
18
+
19
+ export function searchRegistry(
20
+ args?: Record<string, unknown>,
21
+ options?: RegistryArtifactLoaders,
22
+ ): Promise<RegistryToolResult>;
23
+
24
+ export function getEntryDetail(
25
+ args?: Record<string, unknown>,
26
+ options?: RegistryArtifactLoaders,
27
+ ): Promise<RegistryToolResult>;
28
+
29
+ export function getCompatibility(
30
+ args?: Record<string, unknown>,
31
+ options?: RegistryArtifactLoaders,
32
+ ): Promise<RegistryToolResult>;
33
+
34
+ export function getInstallGuidance(
35
+ args?: Record<string, unknown>,
36
+ options?: RegistryArtifactLoaders,
37
+ ): Promise<RegistryToolResult>;
38
+
39
+ export function getPlatformAdapter(
40
+ args?: Record<string, unknown>,
41
+ options?: RegistryArtifactLoaders,
42
+ ): Promise<RegistryToolResult>;
43
+
44
+ export function listDistributionFeeds(
45
+ args?: Record<string, unknown>,
46
+ options?: RegistryArtifactLoaders,
47
+ ): Promise<RegistryToolResult>;
48
+
49
+ export function getSubmissionSchema(
50
+ args?: Record<string, unknown>,
51
+ options?: RegistryArtifactLoaders,
52
+ ): Promise<RegistryToolResult>;
53
+
54
+ export function validateSubmissionDraft(
55
+ args?: Record<string, unknown>,
56
+ options?: RegistryArtifactLoaders,
57
+ ): Promise<RegistryToolResult>;
58
+
59
+ export function searchDuplicateRegistryEntries(
60
+ args?: Record<string, unknown>,
61
+ options?: RegistryArtifactLoaders,
62
+ ): Promise<RegistryToolResult>;
63
+
64
+ export function buildSubmissionUrls(
65
+ args?: Record<string, unknown>,
66
+ options?: RegistryArtifactLoaders,
67
+ ): Promise<RegistryToolResult>;
68
+
69
+ export function getCategorySubmissionGuidance(
70
+ args?: Record<string, unknown>,
71
+ options?: RegistryArtifactLoaders,
72
+ ): Promise<RegistryToolResult>;
73
+
74
+ export function callRegistryTool(
75
+ name: string,
76
+ args?: Record<string, unknown>,
77
+ options?: RegistryArtifactLoaders,
78
+ ): Promise<RegistryToolResult>;
79
+
80
+ export {
81
+ SearchRegistryInputSchema,
82
+ EntryDetailInputSchema,
83
+ CompatibilityInputSchema,
84
+ InstallGuidanceInputSchema,
85
+ PlatformAdapterInputSchema,
86
+ ListDistributionFeedsInputSchema,
87
+ SubmissionFieldsSchema,
88
+ GetSubmissionSchemaInputSchema,
89
+ ValidateSubmissionDraftInputSchema,
90
+ SearchDuplicateEntriesInputSchema,
91
+ BuildSubmissionUrlsInputSchema,
92
+ CategorySubmissionGuidanceInputSchema,
93
+ TOOL_INPUT_SCHEMAS,
94
+ jsonSchemaForTool,
95
+ parseToolArguments,
96
+ formatZodError,
97
+ } from "./schemas.js";
@@ -0,0 +1,496 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import {
6
+ buildSkillPlatformCompatibility,
7
+ platformFeedSlug,
8
+ SITE_URL,
9
+ } from "./platforms.js";
10
+ import {
11
+ formatZodError,
12
+ jsonSchemaForTool,
13
+ parseToolArguments,
14
+ } from "./schemas.js";
15
+ import {
16
+ buildSubmissionUrlsFromSpec,
17
+ getCategorySubmissionGuidanceFromSpec,
18
+ getSubmissionSchemaFromSpec,
19
+ searchDuplicateEntries,
20
+ validateSubmissionDraftFromSpec,
21
+ } from "./submissions.js";
22
+
23
+ const repoRoot = path.resolve(
24
+ path.dirname(fileURLToPath(import.meta.url)),
25
+ "../../..",
26
+ );
27
+ const defaultDataDir = path.join(repoRoot, "apps", "web", "public", "data");
28
+ const safePathPartPattern = /^[a-z0-9-]+$/;
29
+
30
+ const platformAliases = new Map([
31
+ ["claude", "Claude"],
32
+ ["codex", "Codex"],
33
+ ["openai", "Codex"],
34
+ ["windsurf", "Windsurf"],
35
+ ["gemini", "Gemini"],
36
+ ["cursor", "Cursor"],
37
+ ["cursor-rules", "Cursor"],
38
+ ["generic-agents", "Generic AGENTS"],
39
+ ["agents", "Generic AGENTS"],
40
+ ["agents-context", "Generic AGENTS"],
41
+ ["agents-md", "Generic AGENTS"],
42
+ ]);
43
+
44
+ export const READ_ONLY_TOOL_NAMES = [
45
+ "search_registry",
46
+ "get_entry_detail",
47
+ "get_compatibility",
48
+ "get_install_guidance",
49
+ "get_platform_adapter",
50
+ "list_distribution_feeds",
51
+ "get_submission_schema",
52
+ "validate_submission_draft",
53
+ "search_duplicate_entries",
54
+ "build_submission_urls",
55
+ "get_category_submission_guidance",
56
+ ];
57
+
58
+ export const TOOL_DEFINITIONS = [
59
+ {
60
+ name: "search_registry",
61
+ description:
62
+ "Search read-only HeyClaude registry entries by query, category, and skill platform compatibility.",
63
+ inputSchema: jsonSchemaForTool("search_registry"),
64
+ },
65
+ {
66
+ name: "get_entry_detail",
67
+ description:
68
+ "Fetch a read-only HeyClaude registry entry detail payload by category and slug.",
69
+ inputSchema: jsonSchemaForTool("get_entry_detail"),
70
+ },
71
+ {
72
+ name: "get_compatibility",
73
+ description:
74
+ "Fetch platform compatibility metadata for a HeyClaude skill entry.",
75
+ inputSchema: jsonSchemaForTool("get_compatibility"),
76
+ },
77
+ {
78
+ name: "get_install_guidance",
79
+ description:
80
+ "Fetch read-only install, config, usage, and package guidance for a HeyClaude entry.",
81
+ inputSchema: jsonSchemaForTool("get_install_guidance"),
82
+ },
83
+ {
84
+ name: "get_platform_adapter",
85
+ description:
86
+ "Fetch generated read-only platform adapter content, currently Cursor rule adapters for skill packages.",
87
+ inputSchema: jsonSchemaForTool("get_platform_adapter"),
88
+ },
89
+ {
90
+ name: "list_distribution_feeds",
91
+ description:
92
+ "List read-only HeyClaude registry feeds, category feeds, platform feeds, and artifact locations.",
93
+ inputSchema: jsonSchemaForTool("list_distribution_feeds"),
94
+ },
95
+ {
96
+ name: "get_submission_schema",
97
+ description:
98
+ "Fetch read-only HeyClaude submission schemas and GitHub issue template fields by category.",
99
+ inputSchema: jsonSchemaForTool("get_submission_schema"),
100
+ },
101
+ {
102
+ name: "validate_submission_draft",
103
+ description:
104
+ "Validate a HeyClaude content submission draft locally without creating GitHub issues or publishing content.",
105
+ inputSchema: jsonSchemaForTool("validate_submission_draft"),
106
+ },
107
+ {
108
+ name: "search_duplicate_entries",
109
+ description:
110
+ "Search generated registry artifacts for likely duplicate entries before a user opens a submission issue.",
111
+ inputSchema: jsonSchemaForTool("search_duplicate_entries"),
112
+ },
113
+ {
114
+ name: "build_submission_urls",
115
+ description:
116
+ "Build prefilled HeyClaude submit and GitHub issue URLs for a validated submission draft without making write calls.",
117
+ inputSchema: jsonSchemaForTool("build_submission_urls"),
118
+ },
119
+ {
120
+ name: "get_category_submission_guidance",
121
+ description:
122
+ "Fetch category-specific HeyClaude contribution guidance, required fields, and review expectations.",
123
+ inputSchema: jsonSchemaForTool("get_category_submission_guidance"),
124
+ },
125
+ ];
126
+
127
+ function dataDirFromOptions(options = {}) {
128
+ return options.dataDir || process.env.HEYCLAUDE_DATA_DIR || defaultDataDir;
129
+ }
130
+
131
+ function isSafePathPart(value) {
132
+ return safePathPartPattern.test(String(value || ""));
133
+ }
134
+
135
+ function safeRelativePath(relativePath) {
136
+ const parts = String(relativePath || "").split("/");
137
+ if (
138
+ !parts.length ||
139
+ parts.some((part) => !part || part === "." || part === "..")
140
+ ) {
141
+ throw new Error(`Unsafe registry artifact path: ${relativePath}`);
142
+ }
143
+ return parts.join(path.sep);
144
+ }
145
+
146
+ async function readTextArtifact(relativePath, options = {}) {
147
+ if (typeof options.readTextArtifact === "function") {
148
+ return options.readTextArtifact(relativePath);
149
+ }
150
+
151
+ const dataDir = dataDirFromOptions(options);
152
+ const filePath = path.join(dataDir, safeRelativePath(relativePath));
153
+ return readFile(filePath, "utf8");
154
+ }
155
+
156
+ async function readJsonArtifact(relativePath, options = {}) {
157
+ if (typeof options.readJsonArtifact === "function") {
158
+ return options.readJsonArtifact(relativePath);
159
+ }
160
+
161
+ return JSON.parse(await readTextArtifact(relativePath, options));
162
+ }
163
+
164
+ function unwrapEntries(payload) {
165
+ if (!payload || !Array.isArray(payload.entries)) {
166
+ throw new Error("Expected registry artifact envelope with entries array.");
167
+ }
168
+ return payload.entries;
169
+ }
170
+
171
+ function normalizeText(value) {
172
+ return String(value || "")
173
+ .trim()
174
+ .toLowerCase();
175
+ }
176
+
177
+ function normalizeLimit(value, fallback = 10) {
178
+ const numeric = Number(value);
179
+ if (!Number.isFinite(numeric)) return fallback;
180
+ return Math.max(1, Math.min(25, Math.trunc(numeric)));
181
+ }
182
+
183
+ function normalizePlatform(value) {
184
+ const normalized = normalizeText(value).replace(/[^a-z0-9]+/g, "-");
185
+ if (!normalized) return "";
186
+ return platformAliases.get(normalized) || String(value || "").trim();
187
+ }
188
+
189
+ function entryMatchesQuery(entry, query) {
190
+ if (!query) return true;
191
+ const haystack = [
192
+ entry.title,
193
+ entry.description,
194
+ entry.cardDescription,
195
+ entry.category,
196
+ entry.slug,
197
+ entry.author,
198
+ entry.submittedBy,
199
+ entry.brandName,
200
+ entry.brandDomain,
201
+ ...(entry.tags || []),
202
+ ...(entry.keywords || []),
203
+ ]
204
+ .map(normalizeText)
205
+ .join(" ");
206
+ return haystack.includes(query);
207
+ }
208
+
209
+ function entryMatchesPlatform(entry, platform) {
210
+ if (!platform) return true;
211
+ return (entry.platforms || []).some((candidate) => candidate === platform);
212
+ }
213
+
214
+ function toSearchResult(entry) {
215
+ return {
216
+ key: `${entry.category}:${entry.slug}`,
217
+ category: entry.category,
218
+ slug: entry.slug,
219
+ title: entry.title,
220
+ description: entry.description,
221
+ tags: entry.tags || [],
222
+ platforms: entry.platforms || [],
223
+ brandName: entry.brandName || "",
224
+ brandDomain: entry.brandDomain || "",
225
+ submittedBy: entry.submittedBy || "",
226
+ claimStatus: entry.claimStatus || "",
227
+ url: entry.url || `${SITE_URL}/${entry.category}/${entry.slug}`,
228
+ canonicalUrl:
229
+ entry.canonicalUrl ||
230
+ entry.url ||
231
+ `${SITE_URL}/${entry.category}/${entry.slug}`,
232
+ };
233
+ }
234
+
235
+ async function readEntry(category, slug, options = {}) {
236
+ if (!isSafePathPart(category) || !isSafePathPart(slug)) {
237
+ return null;
238
+ }
239
+ try {
240
+ const payload = await readJsonArtifact(
241
+ `entries/${category}/${slug}.json`,
242
+ options,
243
+ );
244
+ return payload?.entry || null;
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ function notFound(message) {
251
+ return { ok: false, error: { code: "not_found", message } };
252
+ }
253
+
254
+ function invalid(message) {
255
+ return { ok: false, error: { code: "invalid_request", message } };
256
+ }
257
+
258
+ function invalidWithDetails(message, details) {
259
+ return { ok: false, error: { code: "invalid_request", message, details } };
260
+ }
261
+
262
+ export async function searchRegistry(args = {}, options = {}) {
263
+ const query = normalizeText(args.query);
264
+ const category = normalizeText(args.category);
265
+ const platform = normalizePlatform(args.platform);
266
+ const limit = normalizeLimit(args.limit);
267
+ const searchIndex = unwrapEntries(
268
+ await readJsonArtifact("search-index.json", options),
269
+ );
270
+
271
+ const entries = searchIndex
272
+ .filter((entry) => !category || entry.category === category)
273
+ .filter((entry) => entryMatchesPlatform(entry, platform))
274
+ .filter((entry) => entryMatchesQuery(entry, query))
275
+ .slice(0, limit)
276
+ .map(toSearchResult);
277
+
278
+ return {
279
+ ok: true,
280
+ count: entries.length,
281
+ query: args.query || "",
282
+ category: category || "",
283
+ platform: platform || "",
284
+ entries,
285
+ };
286
+ }
287
+
288
+ export async function getEntryDetail(args = {}, options = {}) {
289
+ const category = normalizeText(args.category);
290
+ const slug = normalizeText(args.slug);
291
+ if (!category || !slug) {
292
+ return invalid("category and slug are required.");
293
+ }
294
+
295
+ const entry = await readEntry(category, slug, options);
296
+ if (!entry) {
297
+ return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
298
+ }
299
+
300
+ return {
301
+ ok: true,
302
+ key: `${entry.category}:${entry.slug}`,
303
+ canonicalUrl: `${SITE_URL}/${entry.category}/${entry.slug}`,
304
+ entry,
305
+ };
306
+ }
307
+
308
+ export async function getCompatibility(args = {}, options = {}) {
309
+ const category = normalizeText(args.category || "skills");
310
+ const slug = normalizeText(args.slug);
311
+ if (!slug) return invalid("slug is required.");
312
+
313
+ const entry = await readEntry(category, slug, options);
314
+ if (!entry) {
315
+ return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
316
+ }
317
+
318
+ return {
319
+ ok: true,
320
+ key: `${entry.category}:${entry.slug}`,
321
+ category: entry.category,
322
+ slug: entry.slug,
323
+ platformCompatibility: buildSkillPlatformCompatibility(entry),
324
+ };
325
+ }
326
+
327
+ export async function getInstallGuidance(args = {}, options = {}) {
328
+ const category = normalizeText(args.category);
329
+ const slug = normalizeText(args.slug);
330
+ const platform = normalizePlatform(args.platform);
331
+ if (!category || !slug) {
332
+ return invalid("category and slug are required.");
333
+ }
334
+
335
+ const entry = await readEntry(category, slug, options);
336
+ if (!entry) {
337
+ return notFound(`No HeyClaude entry found for ${category}/${slug}.`);
338
+ }
339
+
340
+ const compatibility = buildSkillPlatformCompatibility(entry);
341
+ const selectedCompatibility = platform
342
+ ? compatibility.find((item) => item.platform === platform) || null
343
+ : null;
344
+
345
+ return {
346
+ ok: true,
347
+ key: `${entry.category}:${entry.slug}`,
348
+ canonicalUrl: `${SITE_URL}/${entry.category}/${entry.slug}`,
349
+ title: entry.title,
350
+ installCommand: entry.installCommand || entry.commandSyntax || "",
351
+ configSnippet: entry.configSnippet || "",
352
+ usageSnippet: entry.usageSnippet || "",
353
+ downloadUrl: entry.downloadUrl || "",
354
+ documentationUrl: entry.documentationUrl || "",
355
+ repoUrl: entry.repoUrl || "",
356
+ platform: platform || "",
357
+ selectedCompatibility,
358
+ platformCompatibility: compatibility,
359
+ };
360
+ }
361
+
362
+ export async function getPlatformAdapter(args = {}, options = {}) {
363
+ const slug = normalizeText(args.slug);
364
+ const platform = normalizePlatform(args.platform || "cursor");
365
+ if (!slug) return invalid("slug is required.");
366
+
367
+ if (platform !== "Cursor") {
368
+ return {
369
+ ok: true,
370
+ platform,
371
+ slug,
372
+ adapterAvailable: false,
373
+ message:
374
+ "Native Agent Skill platforms use the SKILL.md package directly; generated adapters are currently provided for Cursor rules.",
375
+ };
376
+ }
377
+
378
+ const entry = await readEntry("skills", slug, options);
379
+ if (!entry) {
380
+ return notFound(`No HeyClaude skill found for ${slug}.`);
381
+ }
382
+
383
+ try {
384
+ const adapter = await readTextArtifact(
385
+ `skill-adapters/cursor/${slug}.mdc`,
386
+ options,
387
+ );
388
+ return {
389
+ ok: true,
390
+ platform: "Cursor",
391
+ slug,
392
+ adapterAvailable: true,
393
+ adapterPath: `/data/skill-adapters/cursor/${slug}.mdc`,
394
+ content: adapter,
395
+ };
396
+ } catch {
397
+ return notFound(`No Cursor adapter generated for ${slug}.`);
398
+ }
399
+ }
400
+
401
+ export async function listDistributionFeeds(args = {}, options = {}) {
402
+ const [manifest, feedIndex] = await Promise.all([
403
+ readJsonArtifact("registry-manifest.json", options),
404
+ readJsonArtifact("feeds/index.json", options),
405
+ ]);
406
+
407
+ return {
408
+ ok: true,
409
+ schemaVersion: manifest.schemaVersion,
410
+ generatedAt: manifest.generatedAt,
411
+ artifacts: manifest.artifacts,
412
+ categories: feedIndex.categories || [],
413
+ platforms: (feedIndex.platforms || []).map((platform) => ({
414
+ ...platform,
415
+ feedSlug: platformFeedSlug(platform.platform),
416
+ })),
417
+ };
418
+ }
419
+
420
+ async function readSubmissionSpec(options = {}) {
421
+ return readJsonArtifact("submission-spec.json", options);
422
+ }
423
+
424
+ export async function getSubmissionSchema(args = {}, options = {}) {
425
+ return getSubmissionSchemaFromSpec(await readSubmissionSpec(options), args);
426
+ }
427
+
428
+ export async function validateSubmissionDraft(args = {}, options = {}) {
429
+ return validateSubmissionDraftFromSpec(
430
+ await readSubmissionSpec(options),
431
+ args,
432
+ );
433
+ }
434
+
435
+ export async function searchDuplicateRegistryEntries(args = {}, options = {}) {
436
+ const searchIndex = unwrapEntries(
437
+ await readJsonArtifact("search-index.json", options),
438
+ );
439
+ return searchDuplicateEntries(searchIndex, args);
440
+ }
441
+
442
+ export async function buildSubmissionUrls(args = {}, options = {}) {
443
+ return buildSubmissionUrlsFromSpec(await readSubmissionSpec(options), args);
444
+ }
445
+
446
+ export async function getCategorySubmissionGuidance(args = {}, options = {}) {
447
+ return getCategorySubmissionGuidanceFromSpec(
448
+ await readSubmissionSpec(options),
449
+ args,
450
+ );
451
+ }
452
+
453
+ export async function callRegistryTool(name, args = {}, options = {}) {
454
+ if (!READ_ONLY_TOOL_NAMES.includes(name)) {
455
+ return invalid(`Unknown read-only HeyClaude MCP tool: ${name}`);
456
+ }
457
+
458
+ let parsedArgs;
459
+ try {
460
+ parsedArgs = parseToolArguments(name, args);
461
+ } catch (error) {
462
+ const details = formatZodError(error);
463
+ if (details) {
464
+ return invalidWithDetails(
465
+ "Invalid HeyClaude MCP tool arguments.",
466
+ details,
467
+ );
468
+ }
469
+ throw error;
470
+ }
471
+
472
+ switch (name) {
473
+ case "search_registry":
474
+ return searchRegistry(parsedArgs, options);
475
+ case "get_entry_detail":
476
+ return getEntryDetail(parsedArgs, options);
477
+ case "get_compatibility":
478
+ return getCompatibility(parsedArgs, options);
479
+ case "get_install_guidance":
480
+ return getInstallGuidance(parsedArgs, options);
481
+ case "get_platform_adapter":
482
+ return getPlatformAdapter(parsedArgs, options);
483
+ case "list_distribution_feeds":
484
+ return listDistributionFeeds(parsedArgs, options);
485
+ case "get_submission_schema":
486
+ return getSubmissionSchema(parsedArgs, options);
487
+ case "validate_submission_draft":
488
+ return validateSubmissionDraft(parsedArgs, options);
489
+ case "search_duplicate_entries":
490
+ return searchDuplicateRegistryEntries(parsedArgs, options);
491
+ case "build_submission_urls":
492
+ return buildSubmissionUrls(parsedArgs, options);
493
+ case "get_category_submission_guidance":
494
+ return getCategorySubmissionGuidance(parsedArgs, options);
495
+ }
496
+ }
@@ -0,0 +1,19 @@
1
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+
3
+ export type RemoteProxyOptions = {
4
+ url?: string | URL;
5
+ timeoutMs?: number | string;
6
+ };
7
+
8
+ export function createRemoteMcpProxyServer(
9
+ options?: RemoteProxyOptions,
10
+ ): Promise<{
11
+ server: Server;
12
+ client: unknown;
13
+ endpointUrl: URL;
14
+ timeoutMs: number;
15
+ }>;
16
+
17
+ export function runRemoteStdioProxy(
18
+ options?: RemoteProxyOptions,
19
+ ): Promise<void>;