Skip to main content
Glama

seo_score

Analyze markdown content for SEO quality by providing a keyword. Get a 0-100 score, grade, and detailed breakdown. Basic analysis is free; full analysis with prioritized suggestions and keyword gap detection uses 1 credit.

Instructions

Analyze markdown content for SEO quality and return a 0-100 score with grade. Basic scoring (heading structure, word count, keyword density, readability) requires no credentials and is FREE. Full analysis (issue list, prioritized suggestions, related-keyword gaps) costs 1 credit. Returns: { score, grade, breakdown, issues?, suggestions? }. Common errors: missing 'content' (VALIDATION_ERROR), insufficient credits for full analysis (PAYMENT_REQUIRED).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
contentYesThe markdown content to analyze
keywordYesThe target keyword or phrase

Implementation Reference

  • Handler function for the seo_score tool. Validates input (content, keyword), calls scoreContent() for full analysis. If no credits, returns a reduced result (score, readability, density, word count) with a note to purchase credits.
    export async function handleSeoScore(input: z.infer<typeof seoScoreSchema>) {
      const contentErr = validateRequired(input.content, "content");
      if (contentErr) return makeError("VALIDATION_ERROR", contentErr);
    
      const keywordErr = validateRequired(input.keyword, "keyword");
      if (keywordErr) return makeError("VALIDATION_ERROR", keywordErr);
    
      const result = scoreContent(input.content, input.keyword);
    
      if (!hasCredits()) {
        return makeSuccess({
          score: result.score,
          readability: result.readability,
          keyword_density: result.keyword_density,
          word_count: result.word_count,
          note: "Purchase credits at pipepost.dev to unlock full SEO analysis with issues and suggestions",
        });
      }
    
      return makeSuccess(result);
    }
  • Zod schema for seo_score tool input: requires 'content' (markdown string) and 'keyword' (target keyword).
    export const seoScoreSchema = z.object({
      content: z.string().describe("The markdown content to analyze"),
      keyword: z.string().describe("The target keyword or phrase"),
    });
  • src/index.ts:106-110 (registration)
    Registers the 'seo_score' tool on the MCP server with description, schema shape, and handler callback. Parses input, calls handleSeoScore, and formats the response.
    server.tool("seo_score", "Analyze markdown content for SEO quality and return a 0-100 score with grade. Basic scoring (heading structure, word count, keyword density, readability) requires no credentials and is FREE. Full analysis (issue list, prioritized suggestions, related-keyword gaps) costs 1 credit. Returns: { score, grade, breakdown, issues?, suggestions? }. Common errors: missing 'content' (VALIDATION_ERROR), insufficient credits for full analysis (PAYMENT_REQUIRED).", seoScoreSchema.shape, async (input) => {
      const parsed = seoScoreSchema.parse(input);
      const result = await handleSeoScore(parsed);
      return { content: [{ type: "text", text: formatToolResponse("seo_score", result, formatSeoScore) }] };
    });
  • Core scoring logic: computes composite SEO score (0-100) from word count, Flesch-Kincaid readability, keyword density, heading structure, and keyword placement. Includes issue/suggestion generation.
    export function scoreContent(content: string, keyword: string): SeoScore {
      const words = countWords(content);
      const headings = countHeadings(content);
      const fk = fleschKincaid(content);
      const density = keywordDensity(content, keyword);
      const issues: string[] = [];
      const suggestions: string[] = [];
    
      if (words === 0) {
        return {
          score: 0,
          readability: { flesch_kincaid: 0, grade_level: "N/A" },
          keyword_density: 0,
          word_count: 0,
          heading_structure: headings,
          issues: ["Content is empty"],
          suggestions: ["Add content to analyze"],
        };
      }
    
      // Word count checks
      if (words < 300) {
        issues.push(`Content is under 300 words (got ${words})`);
        suggestions.push("Aim for at least 800-1,500 words for SEO");
      } else if (words < 800) {
        suggestions.push("Consider expanding to 1,000+ words for better ranking potential");
      }
    
      // Heading checks
      if (headings.h1 === 0) {
        issues.push("Missing H1 heading");
      } else if (headings.h1 > 1) {
        issues.push(`Multiple H1 headings found (${headings.h1}) — use only one`);
      }
    
      if (headings.h2 === 0 && words > 300) {
        issues.push("No H2 subheadings — break content into sections");
      }
    
      // Keyword checks
      if (density === 0) {
        issues.push(`Target keyword "${keyword}" not found in content`);
      } else if (density > 3) {
        issues.push(`Keyword density too high (${density}%) — may be flagged as keyword stuffing`);
        suggestions.push("Aim for 1-2% keyword density");
      } else if (density < 0.5) {
        suggestions.push(`Low keyword density (${density}%) — consider adding "${keyword}" in key sections`);
      }
    
      // Check keyword in headings
      const kwLower = keyword.toLowerCase();
      const headingLines = content.split("\n").filter((l) => l.trim().startsWith("#"));
      const kwInHeading = headingLines.some((l) => l.toLowerCase().includes(kwLower));
      if (!kwInHeading && headingLines.length > 0) {
        issues.push(`Target keyword "${keyword}" not found in any heading`);
        suggestions.push("Include the target keyword in at least one heading");
      }
    
      // Calculate composite score
      let score = 50; // base
    
      // Word count contribution (0-20)
      if (words >= 1500) score += 20;
      else if (words >= 800) score += 15;
      else if (words >= 300) score += 8;
      else score -= 10;
    
      // Readability contribution (0-15)
      if (fk >= 50 && fk <= 80) score += 15; // sweet spot
      else if (fk >= 30) score += 8;
    
      // Keyword density contribution (0-15)
      if (density >= 0.5 && density <= 2.5) score += 15;
      else if (density > 0 && density < 3.5) score += 8;
    
      // Structure contribution (0-10)
      if (headings.h1 === 1) score += 5;
      if (headings.h2 >= 2) score += 5;
    
      // Penalty per issue
      score -= issues.length * 5;
    
      score = Math.max(0, Math.min(100, score));
    
      return {
        score,
        readability: { flesch_kincaid: fk, grade_level: gradeLevel(fk) },
        keyword_density: density,
        word_count: words,
        heading_structure: headings,
        issues,
        suggestions,
      };
    }
  • Formats the seo_score result into a human-readable string with breakdown table, issues list, suggestions, and optional note.
    export function formatSeoScore(data: unknown): string {
      const d = data as {
        score: number;
        readability: { flesch_kincaid: number; grade_level: string };
        keyword_density: number;
        word_count: number;
        heading_structure?: { h1: number; h2: number; h3: number };
        issues?: string[];
        suggestions?: string[];
        note?: string;
      };
    
      const readTime = Math.max(1, Math.ceil(d.word_count / 200));
      const lines = [
        `# SEO Score: ${d.score}/100`,
        "",
        statBar(d.score, 100),
        "",
        section(
          "Breakdown",
          table(
            ["Category", "Details"],
            [
              ["Readability", `${d.readability.grade_level} (Flesch-Kincaid ${d.readability.flesch_kincaid})`],
              ["Keywords", `Density: ${d.keyword_density}%`],
              ...(d.heading_structure
                ? [["Structure" as string | number, `H1: ${d.heading_structure.h1} \u00b7 H2: ${d.heading_structure.h2} \u00b7 H3: ${d.heading_structure.h3}` as string | number]]
                : []),
              ["Word Count", `${d.word_count.toLocaleString()} words (${readTime} min read)`],
            ]
          )
        ),
      ];
    
      if (d.issues && d.issues.length > 0) {
        lines.push(
          "",
          section("Issues", d.issues.map((i) => `\u2718 ${i}`).join("\n"))
        );
      }
    
      if (d.suggestions && d.suggestions.length > 0) {
        lines.push(
          "",
          section("Suggestions", d.suggestions.map((s) => `\u2192 ${s}`).join("\n"))
        );
      }
    
      if (d.note) {
        lines.push("", note(d.note));
      }
    
      return lines.join("\n");
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully discloses behavioral traits: basic scoring is free, full analysis costs 1 credit, output includes optional fields (issues, suggestions), and common errors (VALIDATION_ERROR, PAYMENT_REQUIRED). This exceeds disclosure needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise with two main sentences plus return and error notes. It front-loads the core purpose. While not using bullet points, it remains clear and efficient without redundant phrases.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema, the description covers all essential aspects: tool purpose, parameters, free vs. paid tiers, return structure (with optional fields), and error conditions. It is sufficiently complete for an agent to use correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds context by linking the keyword parameter to scoring criteria (keyword density) and mentioning the 'content' parameter in error handling. This adds meaningful value beyond the schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool analyzes markdown content for SEO quality, returning a 0-100 score with grade. It distinguishes basic (free) and full (paid) analysis, and differentiates from siblings like seo_meta (metadata) and seo_schema (schema markup) through the focus on content quality scoring.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context on when to use the tool (for SEO analysis of markdown) and explains the two tiers (free vs. credit-costing). It does not explicitly compare to sibling tools or state when not to use, but the error conditions (e.g., missing content, insufficient credits) guide proper usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/MendleM/pipepost'

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