Skills rendering in collections

This commit is contained in:
Aaron Powell
2025-12-18 11:05:39 +11:00
parent 03cb605a4f
commit 2b19cc3a3d

View File

@@ -1,30 +1,28 @@
#!/usr/bin/env node #!/usr/bin/env node
import fs from "fs"; import fs from "fs";
import path from "path"; import path, { dirname } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname } from "path";
import { import {
parseCollectionYaml, AGENTS_DIR,
extractMcpServers, AKA_INSTALL_URLS,
COLLECTIONS_DIR,
DOCS_DIR,
INSTRUCTIONS_DIR,
PROMPTS_DIR,
repoBaseUrl,
ROOT_FOLDER,
SKILLS_DIR,
TEMPLATES,
vscodeInsidersInstallImage,
vscodeInstallImage,
} from "./constants.mjs";
import {
extractMcpServerConfigs, extractMcpServerConfigs,
parseCollectionYaml,
parseFrontmatter, parseFrontmatter,
parseSkillMetadata, parseSkillMetadata,
} from "./yaml-parser.mjs"; } from "./yaml-parser.mjs";
import {
TEMPLATES,
AKA_INSTALL_URLS,
repoBaseUrl,
vscodeInstallImage,
vscodeInsidersInstallImage,
ROOT_FOLDER,
PROMPTS_DIR,
AGENTS_DIR,
SKILLS_DIR,
COLLECTIONS_DIR,
INSTRUCTIONS_DIR,
DOCS_DIR,
} from "./constants.mjs";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -55,14 +53,16 @@ async function loadMcpRegistryNames() {
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET; if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
try { try {
console.log('Fetching MCP registry from API...'); console.log("Fetching MCP registry from API...");
const allServers = []; const allServers = [];
let cursor = null; let cursor = null;
const apiUrl = 'https://api.mcp.github.com/v0.1/servers/'; const apiUrl = "https://api.mcp.github.com/v0.1/servers/";
// Fetch all pages using cursor-based pagination // Fetch all pages using cursor-based pagination
do { do {
const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl; const url = cursor
? `${apiUrl}?cursor=${encodeURIComponent(cursor)}`
: apiUrl;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
@@ -78,8 +78,9 @@ async function loadMcpRegistryNames() {
if (serverName) { if (serverName) {
// Try to get displayName from GitHub metadata, fall back to server name // Try to get displayName from GitHub metadata, fall back to server name
const displayName = const displayName =
entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName || entry?.server?._meta?.[
serverName; "io.modelcontextprotocol.registry/publisher-provided"
]?.github?.displayName || serverName;
allServers.push({ allServers.push({
name: serverName, name: serverName,
@@ -431,19 +432,23 @@ function generateMcpServerLinks(servers, registryNames) {
// Match against both displayName and full name (case-insensitive) // Match against both displayName and full name (case-insensitive)
const serverNameLower = serverName.toLowerCase(); const serverNameLower = serverName.toLowerCase();
const registryEntry = registryNames.find( const registryEntry = registryNames.find((entry) => {
(entry) => {
// Exact match on displayName or fullName // Exact match on displayName or fullName
if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) { if (
entry.displayName === serverNameLower ||
entry.fullName === serverNameLower
) {
return true; return true;
} }
// Check if the serverName matches a part of the full name after a slash // Check if the serverName matches a part of the full name after a slash
// e.g., "apify" matches "com.apify/apify-mcp-server" // e.g., "apify" matches "com.apify/apify-mcp-server"
const nameParts = entry.fullName.split('/'); const nameParts = entry.fullName.split("/");
if (nameParts.length > 1 && nameParts[1]) { if (nameParts.length > 1 && nameParts[1]) {
// Check if it matches the second part (after the slash) // Check if it matches the second part (after the slash)
const secondPart = nameParts[1].replace('-mcp-server', '').replace('-mcp', ''); const secondPart = nameParts[1]
.replace("-mcp-server", "")
.replace("-mcp", "");
if (secondPart === serverNameLower) { if (secondPart === serverNameLower) {
return true; return true;
} }
@@ -451,8 +456,7 @@ function generateMcpServerLinks(servers, registryNames) {
// Check if serverName matches the displayName ignoring case // Check if serverName matches the displayName ignoring case
return entry.displayName === serverNameLower; return entry.displayName === serverNameLower;
} });
);
const serverLabel = registryEntry const serverLabel = registryEntry
? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})` ? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})`
: serverName; : serverName;
@@ -489,9 +493,7 @@ function generateSkillsSection(skillsDir) {
} }
// Get all skill folders (directories) // Get all skill folders (directories)
const skillFolders = fs const skillFolders = fs.readdirSync(skillsDir).filter((file) => {
.readdirSync(skillsDir)
.filter((file) => {
const filePath = path.join(skillsDir, file); const filePath = path.join(skillsDir, file);
return fs.statSync(filePath).isDirectory(); return fs.statSync(filePath).isDirectory();
}); });
@@ -768,7 +770,11 @@ function generateFeaturedCollectionsSection(collectionsDir) {
* @param {string} collectionId - Collection ID * @param {string} collectionId - Collection ID
* @param {{ name: string, displayName: string }[]} registryNames - Pre-loaded MCP registry names * @param {{ name: string, displayName: string }[]} registryNames - Pre-loaded MCP registry names
*/ */
function generateCollectionReadme(collection, collectionId, registryNames = []) { function generateCollectionReadme(
collection,
collectionId,
registryNames = []
) {
if (!collection || !collection.items) { if (!collection || !collection.items) {
return `# ${collectionId}\n\nCollection not found or invalid.`; return `# ${collectionId}\n\nCollection not found or invalid.`;
} }
@@ -820,20 +826,23 @@ function generateCollectionReadme(collection, collectionId, registryNames = [])
? "Instruction" ? "Instruction"
: item.kind === "agent" : item.kind === "agent"
? "Agent" ? "Agent"
: item.kind === "skill"
? "Skill"
: "Prompt"; : "Prompt";
const link = `../${item.path}`; const link = `../${item.path}`;
// Create install badges for each item // Create install badges for each item (skills don't use chat install badges)
const badges = makeBadges( const badgeType =
item.path,
item.kind === "instruction" item.kind === "instruction"
? "instructions" ? "instructions"
: item.kind === "chat-mode" : item.kind === "chat-mode"
? "mode" ? "mode"
: item.kind === "agent" : item.kind === "agent"
? "agent" ? "agent"
: "prompt" : item.kind === "skill"
); ? null
: "prompt";
const badges = badgeType ? makeBadges(item.path, badgeType) : "";
const usageDescription = item.usage const usageDescription = item.usage
? `${description} [see usage](#${title ? `${description} [see usage](#${title
@@ -891,15 +900,21 @@ function buildCollectionRow({
kind, kind,
registryNames = [], registryNames = [],
}) { }) {
const titleCell = badges
? `[${title}](${link})<br />${badges}`
: `[${title}](${link})`;
if (hasAgents) { if (hasAgents) {
// Only agents currently have MCP servers; future migration may extend to chat modes. // Only agents currently have MCP servers; future migration may extend to chat modes.
const mcpServers = const mcpServers =
kind === "agent" ? extractMcpServerConfigs(filePath) : []; kind === "agent" ? extractMcpServerConfigs(filePath) : [];
const mcpServerCell = const mcpServerCell =
mcpServers.length > 0 ? generateMcpServerLinks(mcpServers, registryNames) : ""; mcpServers.length > 0
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`; ? generateMcpServerLinks(mcpServers, registryNames)
: "";
return `| ${titleCell} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`;
} }
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} |\n`; return `| ${titleCell} | ${typeDisplay} | ${usageDescription} |\n`;
} }
// Utility: write file only if content changed // Utility: write file only if content changed
@@ -921,7 +936,13 @@ function writeFileIfChanged(filePath, content) {
} }
// Build per-category README content using existing generators, upgrading headings to H1 // Build per-category README content using existing generators, upgrading headings to H1
function buildCategoryReadme(sectionBuilder, dirPath, headerLine, usageLine, registryNames = []) { function buildCategoryReadme(
sectionBuilder,
dirPath,
headerLine,
usageLine,
registryNames = []
) {
const section = sectionBuilder(dirPath, registryNames); const section = sectionBuilder(dirPath, registryNames);
if (section && section.trim()) { if (section && section.trim()) {
// Upgrade the first markdown heading level from ## to # for standalone README files // Upgrade the first markdown heading level from ## to # for standalone README files
@@ -1077,7 +1098,9 @@ async function main() {
writeFileIfChanged(mainReadmePath, readmeContent); writeFileIfChanged(mainReadmePath, readmeContent);
console.log("Main README.md updated with featured collections"); console.log("Main README.md updated with featured collections");
} else { } else {
console.warn("README.md not found, skipping featured collections update"); console.warn(
"README.md not found, skipping featured collections update"
);
} }
} else { } else {
console.log("No featured collections found to add to README.md"); console.log("No featured collections found to add to README.md");
@@ -1095,4 +1118,3 @@ main().catch((error) => {
console.error(error.stack); console.error(error.stack);
process.exit(1); process.exit(1);
}); });