From 2b19cc3a3d7b5d3ed4bbb473c0b509ec4d1a1f39 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 18 Dec 2025 11:05:39 +1100 Subject: [PATCH] Skills rendering in collections --- eng/update-readme.mjs | 298 +++++++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 138 deletions(-) diff --git a/eng/update-readme.mjs b/eng/update-readme.mjs index faaae7d6..dcb4e611 100644 --- a/eng/update-readme.mjs +++ b/eng/update-readme.mjs @@ -1,30 +1,28 @@ #!/usr/bin/env node import fs from "fs"; -import path from "path"; +import path, { dirname } from "path"; import { fileURLToPath } from "url"; -import { dirname } from "path"; import { - parseCollectionYaml, - extractMcpServers, + AGENTS_DIR, + AKA_INSTALL_URLS, + COLLECTIONS_DIR, + DOCS_DIR, + INSTRUCTIONS_DIR, + PROMPTS_DIR, + repoBaseUrl, + ROOT_FOLDER, + SKILLS_DIR, + TEMPLATES, + vscodeInsidersInstallImage, + vscodeInstallImage, +} from "./constants.mjs"; +import { extractMcpServerConfigs, + parseCollectionYaml, parseFrontmatter, parseSkillMetadata, } 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 __dirname = dirname(__filename); @@ -55,14 +53,16 @@ async function loadMcpRegistryNames() { if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET; try { - console.log('Fetching MCP registry from API...'); + console.log("Fetching MCP registry from API..."); const allServers = []; 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 do { - const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl; + const url = cursor + ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` + : apiUrl; const response = await fetch(url); if (!response.ok) { @@ -78,8 +78,9 @@ async function loadMcpRegistryNames() { if (serverName) { // Try to get displayName from GitHub metadata, fall back to server name const displayName = - entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName || - serverName; + entry?.server?._meta?.[ + "io.modelcontextprotocol.registry/publisher-provided" + ]?.github?.displayName || serverName; allServers.push({ name: serverName, @@ -431,28 +432,31 @@ function generateMcpServerLinks(servers, registryNames) { // Match against both displayName and full name (case-insensitive) const serverNameLower = serverName.toLowerCase(); - const registryEntry = registryNames.find( - (entry) => { - // Exact match on displayName or fullName - if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) { + const registryEntry = registryNames.find((entry) => { + // Exact match on displayName or fullName + if ( + entry.displayName === serverNameLower || + entry.fullName === serverNameLower + ) { + return true; + } + + // Check if the serverName matches a part of the full name after a slash + // e.g., "apify" matches "com.apify/apify-mcp-server" + const nameParts = entry.fullName.split("/"); + if (nameParts.length > 1 && nameParts[1]) { + // Check if it matches the second part (after the slash) + const secondPart = nameParts[1] + .replace("-mcp-server", "") + .replace("-mcp", ""); + if (secondPart === serverNameLower) { return true; } - - // Check if the serverName matches a part of the full name after a slash - // e.g., "apify" matches "com.apify/apify-mcp-server" - const nameParts = entry.fullName.split('/'); - if (nameParts.length > 1 && nameParts[1]) { - // Check if it matches the second part (after the slash) - const secondPart = nameParts[1].replace('-mcp-server', '').replace('-mcp', ''); - if (secondPart === serverNameLower) { - return true; - } - } - - // Check if serverName matches the displayName ignoring case - return entry.displayName === serverNameLower; } - ); + + // Check if serverName matches the displayName ignoring case + return entry.displayName === serverNameLower; + }); const serverLabel = registryEntry ? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})` : serverName; @@ -489,12 +493,10 @@ function generateSkillsSection(skillsDir) { } // Get all skill folders (directories) - const skillFolders = fs - .readdirSync(skillsDir) - .filter((file) => { - const filePath = path.join(skillsDir, file); - return fs.statSync(filePath).isDirectory(); - }); + const skillFolders = fs.readdirSync(skillsDir).filter((file) => { + const filePath = path.join(skillsDir, file); + return fs.statSync(filePath).isDirectory(); + }); // Parse each skill folder const skillEntries = skillFolders @@ -768,7 +770,11 @@ function generateFeaturedCollectionsSection(collectionsDir) { * @param {string} collectionId - Collection ID * @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) { return `# ${collectionId}\n\nCollection not found or invalid.`; } @@ -820,20 +826,23 @@ function generateCollectionReadme(collection, collectionId, registryNames = []) ? "Instruction" : item.kind === "agent" ? "Agent" + : item.kind === "skill" + ? "Skill" : "Prompt"; const link = `../${item.path}`; - // Create install badges for each item - const badges = makeBadges( - item.path, + // Create install badges for each item (skills don't use chat install badges) + const badgeType = item.kind === "instruction" ? "instructions" : item.kind === "chat-mode" ? "mode" : item.kind === "agent" ? "agent" - : "prompt" - ); + : item.kind === "skill" + ? null + : "prompt"; + const badges = badgeType ? makeBadges(item.path, badgeType) : ""; const usageDescription = item.usage ? `${description} [see usage](#${title @@ -891,15 +900,21 @@ function buildCollectionRow({ kind, registryNames = [], }) { + const titleCell = badges + ? `[${title}](${link})
${badges}` + : `[${title}](${link})`; + if (hasAgents) { // Only agents currently have MCP servers; future migration may extend to chat modes. const mcpServers = kind === "agent" ? extractMcpServerConfigs(filePath) : []; const mcpServerCell = - mcpServers.length > 0 ? generateMcpServerLinks(mcpServers, registryNames) : ""; - return `| [${title}](${link})
${badges} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`; + mcpServers.length > 0 + ? generateMcpServerLinks(mcpServers, registryNames) + : ""; + return `| ${titleCell} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`; } - return `| [${title}](${link})
${badges} | ${typeDisplay} | ${usageDescription} |\n`; + return `| ${titleCell} | ${typeDisplay} | ${usageDescription} |\n`; } // 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 -function buildCategoryReadme(sectionBuilder, dirPath, headerLine, usageLine, registryNames = []) { +function buildCategoryReadme( + sectionBuilder, + dirPath, + headerLine, + usageLine, + registryNames = [] +) { const section = sectionBuilder(dirPath, registryNames); if (section && section.trim()) { // Upgrade the first markdown heading level from ## to # for standalone README files @@ -984,104 +1005,106 @@ async function main() { registryNames ); - // Generate collections README - const collectionsReadme = buildCategoryReadme( - generateCollectionsSection, - COLLECTIONS_DIR, - collectionsHeader, - TEMPLATES.collectionsUsage, - registryNames - ); + // Generate collections README + const collectionsReadme = buildCategoryReadme( + generateCollectionsSection, + COLLECTIONS_DIR, + collectionsHeader, + TEMPLATES.collectionsUsage, + registryNames + ); - // Ensure docs directory exists for category outputs - if (!fs.existsSync(DOCS_DIR)) { - fs.mkdirSync(DOCS_DIR, { recursive: true }); - } + // Ensure docs directory exists for category outputs + if (!fs.existsSync(DOCS_DIR)) { + fs.mkdirSync(DOCS_DIR, { recursive: true }); + } - // Write category outputs into docs folder - writeFileIfChanged( - path.join(DOCS_DIR, "README.instructions.md"), - instructionsReadme - ); - writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme); - writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme); - writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme); - writeFileIfChanged( - path.join(DOCS_DIR, "README.collections.md"), - collectionsReadme - ); + // Write category outputs into docs folder + writeFileIfChanged( + path.join(DOCS_DIR, "README.instructions.md"), + instructionsReadme + ); + writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme); + writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme); + writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme); + writeFileIfChanged( + path.join(DOCS_DIR, "README.collections.md"), + collectionsReadme + ); - // Generate individual collection README files - if (fs.existsSync(COLLECTIONS_DIR)) { - console.log("Generating individual collection README files..."); + // Generate individual collection README files + if (fs.existsSync(COLLECTIONS_DIR)) { + console.log("Generating individual collection README files..."); - const collectionFiles = fs - .readdirSync(COLLECTIONS_DIR) - .filter((file) => file.endsWith(".collection.yml")); + const collectionFiles = fs + .readdirSync(COLLECTIONS_DIR) + .filter((file) => file.endsWith(".collection.yml")); - for (const file of collectionFiles) { - const filePath = path.join(COLLECTIONS_DIR, file); - const collection = parseCollectionYaml(filePath); + for (const file of collectionFiles) { + const filePath = path.join(COLLECTIONS_DIR, file); + const collection = parseCollectionYaml(filePath); - if (collection) { - const collectionId = - collection.id || path.basename(file, ".collection.yml"); - const readmeContent = generateCollectionReadme( - collection, - collectionId, - registryNames - ); - const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`); - writeFileIfChanged(readmeFile, readmeContent); + if (collection) { + const collectionId = + collection.id || path.basename(file, ".collection.yml"); + const readmeContent = generateCollectionReadme( + collection, + collectionId, + registryNames + ); + const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`); + writeFileIfChanged(readmeFile, readmeContent); + } } } - } - // Generate featured collections section and update main README.md - console.log("Updating main README.md with featured collections..."); - const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR); + // Generate featured collections section and update main README.md + console.log("Updating main README.md with featured collections..."); + const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR); - if (featuredSection) { - const mainReadmePath = path.join(ROOT_FOLDER, "README.md"); + if (featuredSection) { + const mainReadmePath = path.join(ROOT_FOLDER, "README.md"); - if (fs.existsSync(mainReadmePath)) { - let readmeContent = fs.readFileSync(mainReadmePath, "utf8"); + if (fs.existsSync(mainReadmePath)) { + let readmeContent = fs.readFileSync(mainReadmePath, "utf8"); - // Define markers to identify where to insert the featured collections - const startMarker = "## 🌟 Featured Collections"; - const endMarker = "## MCP Server"; + // Define markers to identify where to insert the featured collections + const startMarker = "## 🌟 Featured Collections"; + const endMarker = "## MCP Server"; - // Check if the section already exists - const startIndex = readmeContent.indexOf(startMarker); + // Check if the section already exists + const startIndex = readmeContent.indexOf(startMarker); - if (startIndex !== -1) { - // Section exists, replace it - const endIndex = readmeContent.indexOf(endMarker, startIndex); - if (endIndex !== -1) { - // Replace the existing section - const beforeSection = readmeContent.substring(0, startIndex); - const afterSection = readmeContent.substring(endIndex); - readmeContent = - beforeSection + featuredSection + "\n\n" + afterSection; + if (startIndex !== -1) { + // Section exists, replace it + const endIndex = readmeContent.indexOf(endMarker, startIndex); + if (endIndex !== -1) { + // Replace the existing section + const beforeSection = readmeContent.substring(0, startIndex); + const afterSection = readmeContent.substring(endIndex); + readmeContent = + beforeSection + featuredSection + "\n\n" + afterSection; + } + } else { + // Section doesn't exist, insert it before "## MCP Server" + const mcpIndex = readmeContent.indexOf(endMarker); + if (mcpIndex !== -1) { + const beforeMcp = readmeContent.substring(0, mcpIndex); + const afterMcp = readmeContent.substring(mcpIndex); + readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp; + } } + + writeFileIfChanged(mainReadmePath, readmeContent); + console.log("Main README.md updated with featured collections"); } else { - // Section doesn't exist, insert it before "## MCP Server" - const mcpIndex = readmeContent.indexOf(endMarker); - if (mcpIndex !== -1) { - const beforeMcp = readmeContent.substring(0, mcpIndex); - const afterMcp = readmeContent.substring(mcpIndex); - readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp; - } + console.warn( + "README.md not found, skipping featured collections update" + ); } - - writeFileIfChanged(mainReadmePath, readmeContent); - console.log("Main README.md updated with featured collections"); } else { - console.warn("README.md not found, skipping featured collections update"); + console.log("No featured collections found to add to README.md"); } - } else { - console.log("No featured collections found to add to README.md"); - } } catch (error) { console.error(`Error generating category README files: ${error.message}`); console.error(error.stack); @@ -1095,4 +1118,3 @@ main().catch((error) => { console.error(error.stack); process.exit(1); }); -