Skip to main content
Glama

documcp

by tosin2013
recommend-ssg.ts28.8 kB
import { z } from "zod"; import { MCPToolResponse, formatMCPResponse } from "../types/api.js"; import { getKnowledgeGraph, getProjectContext, getMemoryManager, } from "../memory/kg-integration.js"; import { getUserPreferenceManager } from "../memory/user-preferences.js"; // SSG scoring matrix based on ADR-003 export interface SSGRecommendation { recommended: "jekyll" | "hugo" | "docusaurus" | "mkdocs" | "eleventy"; confidence: number; reasoning: string[]; alternatives: Array<{ name: string; score: number; pros: string[]; cons: string[]; }>; historicalData?: { similarProjectCount: number; successRates: Record<string, { rate: number; sampleSize: number }>; topPerformer?: { ssg: string; successRate: number; deploymentCount: number; }; }; } const inputSchema = z.object({ analysisId: z.string(), userId: z.string().optional().default("default"), preferences: z .object({ priority: z.enum(["simplicity", "features", "performance"]).optional(), ecosystem: z .enum(["javascript", "python", "ruby", "go", "any"]) .optional(), }) .optional(), }); /** * Phase 2.1: Retrieve historical deployment data from knowledge graph */ async function getHistoricalDeploymentData( projectPath?: string, technologies?: string[], ): Promise<{ similarProjectCount: number; successRates: Record<string, { rate: number; sampleSize: number }>; topPerformer?: { ssg: string; successRate: number; deploymentCount: number; }; globalTopPerformer?: { ssg: string; successRate: number; deploymentCount: number; }; }> { try { const kg = await getKnowledgeGraph(); // Get ALL projects for finding global top performers const allProjects = await kg.findNodes({ type: "project" }); // Find similar projects (either by path or by shared technologies) let similarProjects = allProjects; if (projectPath) { // Get context for current project const context = await getProjectContext(projectPath); // If project exists in KG, use its similar projects if (context.similarProjects.length > 0) { similarProjects = context.similarProjects; } else if (technologies && technologies.length > 0) { // Project doesn't exist yet, but we have technologies - find similar by tech const techSet = new Set(technologies.map((t) => t.toLowerCase())); const projectsWithTech = [] as typeof similarProjects; for (const project of allProjects) { const projectTechs = project.properties.technologies || []; const hasShared = projectTechs.some((t: string) => techSet.has(t.toLowerCase()), ); if (hasShared) { projectsWithTech.push(project); } } similarProjects = projectsWithTech; } else { // No project found and no technologies provided similarProjects = []; } } else if (technologies && technologies.length > 0) { // Filter by shared technologies const techSet = new Set(technologies.map((t) => t.toLowerCase())); const projectsWithTech = [] as typeof similarProjects; for (const project of allProjects) { const projectTechs = project.properties.technologies || []; const hasShared = projectTechs.some((t: string) => techSet.has(t.toLowerCase()), ); if (hasShared) { projectsWithTech.push(project); } } similarProjects = projectsWithTech; } else { // No criteria provided similarProjects = []; } // Aggregate deployment data by SSG for similar projects const ssgStats: Record< string, { successes: number; failures: number; total: number } > = {}; // Also track global stats across ALL projects for finding top performers const globalSSGStats: Record< string, { successes: number; failures: number; total: number } > = {}; // Helper function to aggregate stats for a set of projects const aggregateStats = async (projects: typeof allProjects) => { const stats: Record< string, { successes: number; failures: number; total: number } > = {}; for (const project of projects) { const allEdges = await kg.findEdges({ source: project.id }); const deployments = allEdges.filter( (e) => e.type.startsWith("project_deployed_with") || e.properties.baseType === "project_deployed_with", ); for (const deployment of deployments) { const allNodes = await kg.getAllNodes(); const configNode = allNodes.find((n) => n.id === deployment.target); if (configNode && configNode.type === "configuration") { const ssg = configNode.properties.ssg; if (!stats[ssg]) { stats[ssg] = { successes: 0, failures: 0, total: 0 }; } stats[ssg].total++; if (deployment.properties.success) { stats[ssg].successes++; } else { stats[ssg].failures++; } } } } return stats; }; // Aggregate for similar projects Object.assign(ssgStats, await aggregateStats(similarProjects)); // Aggregate for ALL projects (for global top performer) Object.assign(globalSSGStats, await aggregateStats(allProjects)); // Calculate success rates for similar projects const successRates: Record<string, { rate: number; sampleSize: number }> = {}; let topPerformer: | { ssg: string; successRate: number; deploymentCount: number } | undefined; let maxRate = 0; for (const [ssg, stats] of Object.entries(ssgStats)) { if (stats.total > 0) { const rate = stats.successes / stats.total; successRates[ssg] = { rate, sampleSize: stats.total, }; // Track top performer in similar projects (require at least 2 deployments) if (stats.total >= 2 && rate > maxRate) { maxRate = rate; topPerformer = { ssg, successRate: rate, deploymentCount: stats.total, }; } } } // Calculate global top performer from ALL projects let globalTopPerformer: | { ssg: string; successRate: number; deploymentCount: number } | undefined; let globalMaxRate = 0; for (const [ssg, stats] of Object.entries(globalSSGStats)) { if (stats.total >= 2) { const rate = stats.successes / stats.total; if (rate > globalMaxRate) { globalMaxRate = rate; globalTopPerformer = { ssg, successRate: rate, deploymentCount: stats.total, }; } } } return { similarProjectCount: similarProjects.length, successRates, topPerformer, globalTopPerformer, }; } catch (error) { console.warn("Failed to retrieve historical deployment data:", error); return { similarProjectCount: 0, successRates: {}, }; } } /** * Recommends the optimal static site generator (SSG) for a project based on analysis and historical data. * * This function provides intelligent SSG recommendations by analyzing project characteristics, * considering user preferences, and leveraging historical deployment data from the knowledge graph. * It uses a multi-criteria decision analysis approach to score different SSGs and provide * confidence-weighted recommendations with detailed reasoning. * * @param args - The input arguments for SSG recommendation * @param args.analysisId - Unique identifier from a previous repository analysis * @param args.userId - User identifier for personalized recommendations (defaults to "default") * @param args.preferences - Optional user preferences for recommendation weighting * @param args.preferences.priority - Priority focus: "simplicity", "features", or "performance" * @param args.preferences.ecosystem - Preferred technology ecosystem: "javascript", "python", "ruby", "go", or "any" * * @returns Promise resolving to SSG recommendation results * @returns content - Array containing the recommendation results in MCP tool response format * * @throws {Error} When the analysis ID is invalid or not found * @throws {Error} When historical data cannot be retrieved * @throws {Error} When recommendation scoring fails * * @example * ```typescript * // Basic recommendation * const recommendation = await recommendSSG({ * analysisId: "analysis_abc123_def456", * userId: "user123" * }); * * // With preferences * const personalized = await recommendSSG({ * analysisId: "analysis_abc123_def456", * userId: "user123", * preferences: { * priority: "performance", * ecosystem: "javascript" * } * }); * ``` * * @since 1.0.0 * @version 1.2.0 - Added historical data integration and user preferences */ export async function recommendSSG( args: unknown, context?: any, ): Promise<{ content: any[] }> { const startTime = Date.now(); const { analysisId, userId, preferences } = inputSchema.parse(args); const prioritizeSimplicity = preferences?.priority === "simplicity"; const ecosystemPreference = preferences?.ecosystem; // Report initial progress if (context?.meta?.progressToken) { await context.meta.reportProgress?.({ progress: 0, total: 100, }); } await context?.info?.("🔍 Starting SSG recommendation engine..."); // Phase 2.2: Get user preference manager await context?.info?.(`👤 Loading preferences for user: ${userId}...`); const userPreferenceManager = await getUserPreferenceManager(userId); if (context?.meta?.progressToken) { await context.meta.reportProgress?.({ progress: 15, total: 100, }); } try { // Try to retrieve analysis from memory await context?.info?.(`📊 Retrieving analysis: ${analysisId}...`); let analysisData = null; try { const manager = await getMemoryManager(); const analysis = await manager.recall(analysisId); if (analysis && analysis.data) { // Handle the wrapped content structure if (analysis.data.content && Array.isArray(analysis.data.content)) { // Extract the JSON from the first text content const firstContent = analysis.data.content[0]; if ( firstContent && firstContent.type === "text" && firstContent.text ) { try { analysisData = JSON.parse(firstContent.text); } catch (parseError) { // If parse fails, try the direct data analysisData = analysis.data; } } } else { // Direct data structure analysisData = analysis.data; } } } catch (error) { // If memory retrieval fails, continue with fallback logic console.warn( `Could not retrieve analysis ${analysisId} from memory:`, error, ); } if (context?.meta?.progressToken) { await context.meta.reportProgress?.({ progress: 30, total: 100, }); } // Phase 2.1: Retrieve historical deployment data await context?.info?.("📈 Analyzing historical deployment data..."); let historicalData: | { similarProjectCount: number; successRates: Record<string, { rate: number; sampleSize: number }>; topPerformer?: { ssg: string; successRate: number; deploymentCount: number; }; globalTopPerformer?: { ssg: string; successRate: number; deploymentCount: number; }; } | undefined; if (analysisData) { const projectPath = analysisData.path; const technologies = analysisData.dependencies?.languages || []; historicalData = await getHistoricalDeploymentData( projectPath, technologies, ); if (historicalData && historicalData.similarProjectCount > 0) { await context?.info?.( `✨ Found ${historicalData.similarProjectCount} similar project(s) with deployment history`, ); } } if (context?.meta?.progressToken) { await context.meta.reportProgress?.({ progress: 50, total: 100, }); } await context?.info?.("🤔 Calculating SSG recommendations..."); // Determine recommendation based on analysis data if available let finalRecommendation: | "jekyll" | "hugo" | "docusaurus" | "mkdocs" | "eleventy"; let reasoning: string[] = []; let confidence = 0.85; if (analysisData) { // Use actual analysis data to make informed recommendation const ecosystem = analysisData.dependencies?.ecosystem || "unknown"; const hasReact = analysisData.dependencies?.packages?.some( (p: string) => p.includes("react") || p.includes("next"), ); const complexity = analysisData.documentation?.estimatedComplexity || "moderate"; const teamSize = analysisData.recommendations?.teamSize || "small"; // Logic based on real analysis if (ecosystem === "python") { finalRecommendation = "mkdocs"; reasoning = [ "Python ecosystem detected - MkDocs integrates naturally", "Simple configuration with YAML", "Material theme provides excellent UI out of the box", "Strong Python community support", ]; } else if (ecosystem === "ruby") { finalRecommendation = "jekyll"; reasoning = [ "Ruby ecosystem detected - Jekyll is the native choice", "GitHub Pages native support", "Simple static site generation", "Extensive theme ecosystem", ]; } else if (hasReact || ecosystem === "javascript") { if (complexity === "complex" || teamSize === "large") { finalRecommendation = "docusaurus"; reasoning = [ "JavaScript/TypeScript ecosystem with React detected", "Complex project structure benefits from Docusaurus features", "Built-in versioning and internationalization", "MDX support for interactive documentation", ]; } else if (prioritizeSimplicity) { finalRecommendation = "eleventy"; reasoning = [ "JavaScript ecosystem with simplicity priority", "Minimal configuration required", "Fast build times", "Flexible templating options", ]; } else { finalRecommendation = "docusaurus"; reasoning = [ "JavaScript/TypeScript ecosystem detected", "Modern React-based framework", "Active community and regular updates", "Great developer experience", ]; } } else if (ecosystem === "go") { finalRecommendation = "hugo"; reasoning = [ "Go ecosystem detected - Hugo is written in Go", "Extremely fast build times", "No runtime dependencies", "Excellent for large documentation sites", ]; } else { // Default logic when ecosystem is unknown if (prioritizeSimplicity) { finalRecommendation = "jekyll"; reasoning = [ "Simple setup and configuration", "GitHub Pages native support", "Extensive documentation and community", "Mature and stable platform", ]; } else { finalRecommendation = "docusaurus"; reasoning = [ "Modern documentation framework", "Rich feature set out of the box", "Great for technical documentation", "Active development and support", ]; } } // Apply preference overrides if (ecosystemPreference && ecosystemPreference !== "any") { if (ecosystemPreference === "python") { finalRecommendation = "mkdocs"; reasoning.unshift("Python ecosystem explicitly requested"); } else if (ecosystemPreference === "ruby") { finalRecommendation = "jekyll"; reasoning.unshift("Ruby ecosystem explicitly requested"); } else if (ecosystemPreference === "go") { finalRecommendation = "hugo"; reasoning.unshift("Go ecosystem explicitly requested"); } else if (ecosystemPreference === "javascript") { if ( finalRecommendation !== "docusaurus" && finalRecommendation !== "eleventy" ) { finalRecommendation = prioritizeSimplicity ? "eleventy" : "docusaurus"; reasoning.unshift("JavaScript ecosystem explicitly requested"); } } } // Adjust confidence based on data quality if (analysisData.structure?.totalFiles > 100) { confidence = Math.min(0.95, confidence + 0.05); } if ( analysisData.documentation?.hasReadme && analysisData.documentation?.hasDocs ) { confidence = Math.min(0.95, confidence + 0.05); } // Phase 2.1: Adjust recommendation and confidence based on historical data if (historicalData && historicalData.similarProjectCount >= 0) { const recommendedSuccessRate = historicalData.successRates[finalRecommendation]; if (recommendedSuccessRate) { // Boost confidence if historically successful if (recommendedSuccessRate.rate >= 1.0) { // Perfect success rate - maximum boost confidence = Math.min(0.98, confidence + 0.2); reasoning.unshift( `✅ 100% success rate in ${recommendedSuccessRate.sampleSize} similar project(s)`, ); } else if ( recommendedSuccessRate.rate > 0.8 && recommendedSuccessRate.sampleSize >= 2 ) { // High success rate - good boost confidence = Math.min(0.98, confidence + 0.15); reasoning.unshift( `✅ ${(recommendedSuccessRate.rate * 100).toFixed( 0, )}% success rate in ${ recommendedSuccessRate.sampleSize } similar project(s)`, ); } else if ( recommendedSuccessRate.rate < 0.5 && recommendedSuccessRate.sampleSize >= 2 ) { // Reduce confidence if historically problematic confidence = Math.max(0.5, confidence - 0.15); reasoning.unshift( `⚠️ Only ${(recommendedSuccessRate.rate * 100).toFixed( 0, )}% success rate in ${ recommendedSuccessRate.sampleSize } similar project(s)`, ); } } else { // No deployment history for recommended SSG // Check if similar projects had poor outcomes with OTHER SSGs // This indicates general deployment challenges const allSuccessRates = Object.values(historicalData.successRates); if (allSuccessRates.length > 0) { const avgSuccessRate = allSuccessRates.reduce((sum, data) => sum + data.rate, 0) / allSuccessRates.length; const totalSamples = allSuccessRates.reduce( (sum, data) => sum + data.sampleSize, 0, ); // If similar projects had poor deployment success overall, reduce confidence if (avgSuccessRate < 0.5 && totalSamples >= 2) { confidence = Math.max(0.6, confidence - 0.2); // Find the SSG with worst performance to mention const worstSSG = Object.entries( historicalData.successRates, ).reduce( (worst, [ssg, data]) => data.rate < worst.rate ? { ssg, rate: data.rate } : worst, { ssg: "", rate: 1.0 }, ); reasoning.unshift( `⚠️ Similar projects had deployment challenges (${ worstSSG.ssg }: ${(worstSSG.rate * 100).toFixed(0)}% success rate)`, ); } } } // Consider switching to top performer if significantly better // Prefer similar project top performer, fall back to global top performer const performerToConsider = historicalData.topPerformer || historicalData.globalTopPerformer; if ( performerToConsider && performerToConsider.ssg !== finalRecommendation ) { const topPerformer = performerToConsider; const currentRate = recommendedSuccessRate?.rate || 0.5; const isFromSimilarProjects = !!historicalData.topPerformer; // Only switch if from similar projects (same ecosystem/technologies) // For cross-ecosystem recommendations, just mention as alternative const shouldSwitch = isFromSimilarProjects && topPerformer.successRate > currentRate + 0.2 && topPerformer.deploymentCount >= 2; const shouldMention = !shouldSwitch && topPerformer.successRate >= 0.8 && topPerformer.deploymentCount >= 2; if (shouldSwitch) { reasoning.unshift( `📊 Switching to ${topPerformer.ssg} based on ${( topPerformer.successRate * 100 ).toFixed(0)}% success rate across ${ topPerformer.deploymentCount } deployments`, ); finalRecommendation = topPerformer.ssg as | "jekyll" | "hugo" | "docusaurus" | "mkdocs" | "eleventy"; confidence = Math.min(0.95, topPerformer.successRate + 0.1); } else if (shouldMention) { // Mention as alternative if it has good success rate const projectScope = isFromSimilarProjects ? "similar projects" : "all projects"; reasoning.push( `💡 Alternative: ${topPerformer.ssg} has ${( topPerformer.successRate * 100 ).toFixed(0)}% success rate in ${projectScope}`, ); } } // Add general historical context if (historicalData.similarProjectCount >= 2) { const totalDeployments = Object.values( historicalData.successRates, ).reduce((sum, data) => sum + data.sampleSize, 0); reasoning.push( `📚 Based on ${totalDeployments} deployment(s) across ${historicalData.similarProjectCount} similar project(s)`, ); } } } else { // Fallback logic when no analysis data is available const baseRecommendation = prioritizeSimplicity ? "jekyll" : "docusaurus"; finalRecommendation = ecosystemPreference === "python" ? "mkdocs" : baseRecommendation; reasoning = [ "Recommendation based on preferences without full analysis", "Consider running analyze_repository for more accurate recommendation", ]; confidence = 0.65; // Lower confidence without analysis data } // Phase 2.2: Apply user preferences to recommendation // For preference checking, include all SSGs except the current recommendation // This ensures user preferences can override even if their preferred SSG isn't in top alternatives const allSSGs: Array< "jekyll" | "hugo" | "docusaurus" | "mkdocs" | "eleventy" > = ["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"]; const alternativeNames = allSSGs.filter( (ssg) => ssg !== finalRecommendation, ); const preferenceAdjustment = userPreferenceManager.applyPreferencesToRecommendation( finalRecommendation, alternativeNames, ); if (preferenceAdjustment.adjustmentReason) { // User preferences led to a different recommendation finalRecommendation = preferenceAdjustment.recommended as | "jekyll" | "hugo" | "docusaurus" | "mkdocs" | "eleventy"; reasoning.unshift(`🎯 ${preferenceAdjustment.adjustmentReason}`); confidence = Math.min(0.95, confidence + 0.05); } const recommendation: SSGRecommendation = { recommended: finalRecommendation, confidence, reasoning, alternatives: getAlternatives(finalRecommendation, prioritizeSimplicity), historicalData, }; if (context?.meta?.progressToken) { await context.meta.reportProgress?.({ progress: 100, total: 100, }); } const executionTime = Date.now() - startTime; await context?.info?.( `✅ Recommendation complete! Suggesting ${recommendation.recommended.toUpperCase()} with ${( recommendation.confidence * 100 ).toFixed(0)}% confidence (${Math.round(executionTime / 1000)}s)`, ); const response: MCPToolResponse<SSGRecommendation> = { success: true, data: recommendation, metadata: { toolVersion: "1.0.0", executionTime, timestamp: new Date().toISOString(), analysisId, }, recommendations: [ { type: "info", title: "SSG Recommendation", description: `${recommendation.recommended} recommended with ${( recommendation.confidence * 100 ).toFixed(0)}% confidence`, }, ], nextSteps: [ { action: "Generate Configuration", toolRequired: "generate_config", description: `Create ${recommendation.recommended} configuration files`, priority: "high", }, ], }; return formatMCPResponse(response); } catch (error) { const errorResponse: MCPToolResponse = { success: false, error: { code: "RECOMMENDATION_FAILED", message: `Failed to generate SSG recommendation: ${error}`, resolution: "Ensure analysis ID is valid and preferences are correctly formatted", }, metadata: { toolVersion: "1.0.0", executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), analysisId, }, }; return formatMCPResponse(errorResponse); } } function getAlternatives( recommended: string, prioritizeSimplicity: boolean, ): SSGRecommendation["alternatives"] { const allSSGs = [ { name: "Jekyll", score: prioritizeSimplicity ? 0.85 : 0.7, pros: [ "Simple setup", "GitHub Pages native", "Extensive themes", "Ruby ecosystem", ], cons: [ "Ruby dependency", "Slower builds for large sites", "Limited dynamic features", ], }, { name: "Hugo", score: prioritizeSimplicity ? 0.65 : 0.75, pros: [ "Extremely fast builds", "No dependencies", "Go templating", "Great for large sites", ], cons: [ "Steeper learning curve", "Go templating may be unfamiliar", "Less flexible themes", ], }, { name: "Docusaurus", score: prioritizeSimplicity ? 0.7 : 0.9, pros: [ "React-based", "Rich features", "MDX support", "Built-in versioning", ], cons: [ "More complex setup", "Node.js dependency", "Heavier than static generators", ], }, { name: "MkDocs", score: prioritizeSimplicity ? 0.8 : 0.75, pros: [ "Simple setup", "Python-based", "Great themes", "Easy configuration", ], cons: [ "Python dependency", "Less flexible than React-based", "Limited customization", ], }, { name: "Eleventy", score: prioritizeSimplicity ? 0.75 : 0.7, pros: [ "Minimal config", "Fast builds", "Flexible templates", "JavaScript ecosystem", ], cons: [ "Less opinionated", "Fewer built-in features", "Requires more setup for complex sites", ], }, ]; // Filter out the recommended SSG and sort by score return allSSGs .filter((ssg) => ssg.name.toLowerCase() !== recommended.toLowerCase()) .sort((a, b) => b.score - a.score) .slice(0, 2); // Return top 2 alternatives }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/documcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server