score_content
Analyze content against a keyword across 7 SEO categories to get an overall score, detailed breakdowns, and actionable optimization recommendations.
Instructions
Score content against a keyword across 7 SEO categories. Returns an overall score (0-100), per-category breakdowns, and actionable recommendations.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| content | Yes | The content to score (Markdown or plain text) | |
| keyword | Yes | The target keyword to optimize for | |
| target_word_count | No | Target word count (default: 1500) |
Implementation Reference
- mcp-server/src/index.ts:108-276 (handler)The scoreContent function performs the core logic for evaluating the content against the target keyword.
function scoreContent( content: string, keyword: string, targetWordCount?: number ): { overallScore: number; categories: Record<string, { score: number; maxScore: number; details: string }>; recommendations: string[]; } { const words = countWords(content); const target = targetWordCount || 1500; const headings = extractHeadings(content); const { score: readabilityScore, gradeLevel } = fleschKincaidScore(content); const topics = getKeywordTopics(keyword); const contentLower = content.toLowerCase(); const keywordLower = keyword.toLowerCase(); const categories: Record<string, { score: number; maxScore: number; details: string }> = {}; const recommendations: string[] = []; // 1. Word Count (max 15) const wordRatio = Math.min(words / target, 1.5); let wordScore: number; if (wordRatio < 0.5) wordScore = Math.round(wordRatio * 10); else if (wordRatio < 0.8) wordScore = Math.round(5 + (wordRatio - 0.5) * 20); else if (wordRatio <= 1.2) wordScore = Math.round(11 + (Math.min(wordRatio, 1.0) - 0.8) * 20); else wordScore = Math.max(10, 15 - Math.round((wordRatio - 1.2) * 10)); wordScore = Math.min(15, Math.max(0, wordScore)); categories["Word Count"] = { score: wordScore, maxScore: 15, details: `${words} words (target: ${target}). Ratio: ${(wordRatio * 100).toFixed(0)}%.`, }; if (words < target * 0.8) recommendations.push(`Increase content length from ${words} to at least ${target} words.`); if (words > target * 1.5) recommendations.push(`Content is ${words} words, significantly over target of ${target}. Consider tightening.`); // 2. Keyword Usage (max 20) const keywordCount = (contentLower.match(new RegExp(keywordLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")) || []).length; const density = words > 0 ? (keywordCount / words) * 100 : 0; const lines = content.split("\n"); const firstParagraph = lines.slice(0, 5).join(" ").toLowerCase(); const keywordInFirstPara = firstParagraph.includes(keywordLower); const keywordInHeadings = headings.some((h) => h.text.toLowerCase().includes(keywordLower)); let keywordScore = 0; if (density >= 0.5 && density <= 2.5) keywordScore += 8; else if (density > 0 && density < 0.5) keywordScore += 3; else if (density > 2.5 && density <= 4) keywordScore += 4; else if (density > 4) keywordScore += 1; if (keywordInFirstPara) keywordScore += 5; if (keywordInHeadings) keywordScore += 5; if (keywordCount >= 3) keywordScore += 2; keywordScore = Math.min(20, keywordScore); categories["Keyword Usage"] = { score: keywordScore, maxScore: 20, details: `Keyword "${keyword}" appears ${keywordCount} times (density: ${density.toFixed(2)}%). In first paragraph: ${keywordInFirstPara ? "yes" : "no"}. In headings: ${keywordInHeadings ? "yes" : "no"}.`, }; if (!keywordInFirstPara) recommendations.push(`Include the keyword "${keyword}" in the first paragraph.`); if (!keywordInHeadings) recommendations.push(`Add the keyword "${keyword}" to at least one heading.`); if (density < 0.5) recommendations.push(`Keyword density is low (${density.toFixed(2)}%). Aim for 0.5-2.5%.`); if (density > 3) recommendations.push(`Keyword density is high (${density.toFixed(2)}%). Reduce to avoid over-optimization.`); // 3. Heading Structure (max 15) const h1s = headings.filter((h) => h.level === 1); const h2s = headings.filter((h) => h.level === 2); const h3s = headings.filter((h) => h.level === 3); let headingScore = 0; if (h1s.length === 1) headingScore += 4; else if (h1s.length > 1) headingScore += 1; if (h2s.length >= 3) headingScore += 5; else if (h2s.length >= 1) headingScore += 3; if (h3s.length >= 2) headingScore += 3; else if (h3s.length >= 1) headingScore += 1; if (headings.length >= 5) headingScore += 3; else if (headings.length >= 3) headingScore += 2; headingScore = Math.min(15, headingScore); categories["Heading Structure"] = { score: headingScore, maxScore: 15, details: `H1: ${h1s.length}, H2: ${h2s.length}, H3: ${h3s.length}. Total headings: ${headings.length}.`, }; if (h1s.length === 0) recommendations.push("Add an H1 heading containing the primary keyword."); if (h1s.length > 1) recommendations.push("Use only one H1 heading per page."); if (h2s.length < 3) recommendations.push("Add more H2 subheadings to improve content structure (aim for 3+)."); if (h3s.length === 0 && words > 800) recommendations.push("Add H3 subheadings for detailed subsections."); // 4. Readability (max 15) let readScore = 0; if (readabilityScore >= 60 && readabilityScore <= 80) readScore = 15; else if (readabilityScore >= 50 && readabilityScore < 60) readScore = 12; else if (readabilityScore >= 80 && readabilityScore <= 90) readScore = 12; else if (readabilityScore >= 40 && readabilityScore < 50) readScore = 8; else if (readabilityScore > 90) readScore = 8; else if (readabilityScore >= 30) readScore = 5; else readScore = 2; categories["Readability"] = { score: readScore, maxScore: 15, details: `Flesch-Kincaid score: ${readabilityScore} (grade level: ${gradeLevel}). ${readabilityScore >= 60 ? "Good" : readabilityScore >= 40 ? "Moderate" : "Difficult"} readability.`, }; if (readabilityScore < 50) recommendations.push("Improve readability: use shorter sentences and simpler words."); if (gradeLevel > 12) recommendations.push(`Grade level ${gradeLevel} is too high. Aim for grade 8-10 for web content.`); // 5. Entity Coverage (max 15) const coveredTopics = topics.filter((t) => contentLower.includes(t.toLowerCase())); const coverage = topics.length > 0 ? coveredTopics.length / topics.length : 0; let entityScore = Math.round(coverage * 15); entityScore = Math.min(15, entityScore); categories["Entity Coverage"] = { score: entityScore, maxScore: 15, details: `${coveredTopics.length}/${topics.length} expected topics covered (${(coverage * 100).toFixed(0)}%).`, }; const missingTopics = topics.filter((t) => !contentLower.includes(t.toLowerCase())); if (missingTopics.length > 0) { recommendations.push(`Cover these missing topics: ${missingTopics.slice(0, 5).join(", ")}.`); } // 6. Content Depth (max 10) const paragraphs = content.split(/\n\n+/).filter((p) => p.trim().length > 0); const avgParaLength = paragraphs.length > 0 ? words / paragraphs.length : words; const hasList = /[-*]\s+\w|^\d+\.\s+\w/m.test(content); const hasNumbers = /\d+%|\d+\s*(million|billion|thousand|percent)/i.test(content); let depthScore = 0; if (paragraphs.length >= 5) depthScore += 3; else if (paragraphs.length >= 3) depthScore += 2; if (avgParaLength >= 30 && avgParaLength <= 150) depthScore += 3; else if (avgParaLength > 0) depthScore += 1; if (hasList) depthScore += 2; if (hasNumbers) depthScore += 2; depthScore = Math.min(10, depthScore); categories["Content Depth"] = { score: depthScore, maxScore: 10, details: `${paragraphs.length} paragraphs, avg ${Math.round(avgParaLength)} words/paragraph. Lists: ${hasList ? "yes" : "no"}. Data/stats: ${hasNumbers ? "yes" : "no"}.`, }; if (!hasList) recommendations.push("Add bullet points or numbered lists to improve scannability."); if (!hasNumbers) recommendations.push("Include statistics, data points, or numbers to add credibility."); if (paragraphs.length < 5) recommendations.push("Break content into more paragraphs for better readability."); // 7. Internal Structure (max 10) const hasIntro = words >= 50 && countWords(lines.slice(0, 5).join(" ")) >= 20; const hasConclusion = contentLower.includes("conclusion") || contentLower.includes("summary") || contentLower.includes("final thoughts") || contentLower.includes("wrapping up") || contentLower.includes("key takeaways"); const hasMeta = contentLower.includes("meta") || content.includes("description=") || content.includes("title="); let structureScore = 0; if (hasIntro) structureScore += 3; if (hasConclusion) structureScore += 3; if (headings.length > 0 && h2s.length >= 2) structureScore += 2; if (words > 300) structureScore += 2; structureScore = Math.min(10, structureScore); categories["Internal Structure"] = { score: structureScore, maxScore: 10, details: `Introduction: ${hasIntro ? "yes" : "no"}. Conclusion: ${hasConclusion ? "yes" : "no"}. Logical flow: ${headings.length >= 3 ? "good" : "needs improvement"}.`, }; if (!hasConclusion) recommendations.push("Add a conclusion or key takeaways section."); if (!hasIntro) recommendations.push("Strengthen the introduction — it should hook the reader and mention the keyword."); // Overall score const overallScore = Object.values(categories).reduce((sum, c) => sum + c.score, 0); return { overallScore, categories, recommendations }; } - mcp-server/src/index.ts:330-349 (registration)The "score_content" tool is registered with the McpServer, accepting content, keyword, and optional target_word_count as input parameters.
server.tool( "score_content", "Score content against a keyword across 7 SEO categories. Returns an overall score (0-100), per-category breakdowns, and actionable recommendations.", { content: z.string().describe("The content to score (Markdown or plain text)"), keyword: z.string().describe("The target keyword to optimize for"), target_word_count: z.number().optional().describe("Target word count (default: 1500)"), }, async ({ content, keyword, target_word_count }) => { const result = scoreContent(content, keyword, target_word_count); return { content: [ { type: "text" as const, text: JSON.stringify(result, null, 2), }, ], }; } );