Skip to main content
Glama
sharozdawa

content-optimizer-mcp

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
NameRequiredDescriptionDefault
contentYesThe content to score (Markdown or plain text)
keywordYesThe target keyword to optimize for
target_word_countNoTarget word count (default: 1500)

Implementation Reference

  • 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 };
    }
  • 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),
            },
          ],
        };
      }
    );

Latest Blog Posts

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/sharozdawa/content-optimizer'

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