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,28 +432,31 @@ 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 (
if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) { 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; 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 const serverLabel = registryEntry
? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})` ? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})`
: serverName; : serverName;
@@ -489,12 +493,10 @@ 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) const filePath = path.join(skillsDir, file);
.filter((file) => { return fs.statSync(filePath).isDirectory();
const filePath = path.join(skillsDir, file); });
return fs.statSync(filePath).isDirectory();
});
// Parse each skill folder // Parse each skill folder
const skillEntries = skillFolders const skillEntries = skillFolders
@@ -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
@@ -984,104 +1005,106 @@ async function main() {
registryNames registryNames
); );
// Generate collections README // Generate collections README
const collectionsReadme = buildCategoryReadme( const collectionsReadme = buildCategoryReadme(
generateCollectionsSection, generateCollectionsSection,
COLLECTIONS_DIR, COLLECTIONS_DIR,
collectionsHeader, collectionsHeader,
TEMPLATES.collectionsUsage, TEMPLATES.collectionsUsage,
registryNames registryNames
); );
// Ensure docs directory exists for category outputs // Ensure docs directory exists for category outputs
if (!fs.existsSync(DOCS_DIR)) { if (!fs.existsSync(DOCS_DIR)) {
fs.mkdirSync(DOCS_DIR, { recursive: true }); fs.mkdirSync(DOCS_DIR, { recursive: true });
} }
// Write category outputs into docs folder // Write category outputs into docs folder
writeFileIfChanged( writeFileIfChanged(
path.join(DOCS_DIR, "README.instructions.md"), path.join(DOCS_DIR, "README.instructions.md"),
instructionsReadme instructionsReadme
); );
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme); writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
writeFileIfChanged( writeFileIfChanged(
path.join(DOCS_DIR, "README.collections.md"), path.join(DOCS_DIR, "README.collections.md"),
collectionsReadme collectionsReadme
); );
// Generate individual collection README files // Generate individual collection README files
if (fs.existsSync(COLLECTIONS_DIR)) { if (fs.existsSync(COLLECTIONS_DIR)) {
console.log("Generating individual collection README files..."); console.log("Generating individual collection README files...");
const collectionFiles = fs const collectionFiles = fs
.readdirSync(COLLECTIONS_DIR) .readdirSync(COLLECTIONS_DIR)
.filter((file) => file.endsWith(".collection.yml")); .filter((file) => file.endsWith(".collection.yml"));
for (const file of collectionFiles) { for (const file of collectionFiles) {
const filePath = path.join(COLLECTIONS_DIR, file); const filePath = path.join(COLLECTIONS_DIR, file);
const collection = parseCollectionYaml(filePath); const collection = parseCollectionYaml(filePath);
if (collection) { if (collection) {
const collectionId = const collectionId =
collection.id || path.basename(file, ".collection.yml"); collection.id || path.basename(file, ".collection.yml");
const readmeContent = generateCollectionReadme( const readmeContent = generateCollectionReadme(
collection, collection,
collectionId, collectionId,
registryNames registryNames
); );
const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`); const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`);
writeFileIfChanged(readmeFile, readmeContent); writeFileIfChanged(readmeFile, readmeContent);
}
} }
} }
}
// Generate featured collections section and update main README.md // Generate featured collections section and update main README.md
console.log("Updating main README.md with featured collections..."); console.log("Updating main README.md with featured collections...");
const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR); const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR);
if (featuredSection) { if (featuredSection) {
const mainReadmePath = path.join(ROOT_FOLDER, "README.md"); const mainReadmePath = path.join(ROOT_FOLDER, "README.md");
if (fs.existsSync(mainReadmePath)) { if (fs.existsSync(mainReadmePath)) {
let readmeContent = fs.readFileSync(mainReadmePath, "utf8"); let readmeContent = fs.readFileSync(mainReadmePath, "utf8");
// Define markers to identify where to insert the featured collections // Define markers to identify where to insert the featured collections
const startMarker = "## 🌟 Featured Collections"; const startMarker = "## 🌟 Featured Collections";
const endMarker = "## MCP Server"; const endMarker = "## MCP Server";
// Check if the section already exists // Check if the section already exists
const startIndex = readmeContent.indexOf(startMarker); const startIndex = readmeContent.indexOf(startMarker);
if (startIndex !== -1) { if (startIndex !== -1) {
// Section exists, replace it // Section exists, replace it
const endIndex = readmeContent.indexOf(endMarker, startIndex); const endIndex = readmeContent.indexOf(endMarker, startIndex);
if (endIndex !== -1) { if (endIndex !== -1) {
// Replace the existing section // Replace the existing section
const beforeSection = readmeContent.substring(0, startIndex); const beforeSection = readmeContent.substring(0, startIndex);
const afterSection = readmeContent.substring(endIndex); const afterSection = readmeContent.substring(endIndex);
readmeContent = readmeContent =
beforeSection + featuredSection + "\n\n" + afterSection; 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 { } else {
// Section doesn't exist, insert it before "## MCP Server" console.warn(
const mcpIndex = readmeContent.indexOf(endMarker); "README.md not found, skipping featured collections update"
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 { } 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) { } catch (error) {
console.error(`Error generating category README files: ${error.message}`); console.error(`Error generating category README files: ${error.message}`);
console.error(error.stack); console.error(error.stack);
@@ -1095,4 +1118,3 @@ main().catch((error) => {
console.error(error.stack); console.error(error.stack);
process.exit(1); process.exit(1);
}); });