Skip to main content
Glama

seo_audit

Check 18 SEO signals including meta tags, headings, OG/Twitter cards, structured data, and alt text. Get a 0-100 score with specific recommendations for each failing check.

Instructions

Run a comprehensive SEO audit. Checks 18 SEO signals including meta tags, heading hierarchy, Open Graph tags, Twitter cards, structured data (JSON-LD), canonical URLs, image alt text, and more. Returns a 0-100 score and specific recommendations for each failing check.

Use this when the user wants to check their page's SEO health, improve search engine visibility, or ensure proper social sharing metadata.

This tool is FREE — runs entirely within Claude Code.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesURL of the page to audit (e.g., http://localhost:3000)

Implementation Reference

  • The main handler function 'runSeoAudit' that performs the comprehensive SEO audit. Opens a Puppeteer page, evaluates DOM for SEO signals (meta tags, headings, OG, structured data, etc.), builds checks, and returns a SeoAuditResult with score.
    export async function runSeoAudit(url: string): Promise<SeoAuditResult> {
      const page = await createPage(1440, 900);
    
      try {
        await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 });
    
        const pageData = await page.evaluate(() => {
          const getMeta = (name: string): string | null => {
            const el =
              document.querySelector(`meta[name="${name}"]`) ??
              document.querySelector(`meta[property="${name}"]`);
            return el?.getAttribute("content") ?? null;
          };
    
          const getLink = (rel: string): string | null => {
            const el = document.querySelector(`link[rel="${rel}"]`);
            return el?.getAttribute("href") ?? null;
          };
    
          // Collect headings
          const headings: { tag: string; text: string }[] = [];
          document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((el) => {
            headings.push({
              tag: el.tagName.toLowerCase(),
              text: (el.textContent ?? "").trim().slice(0, 100),
            });
          });
    
          // Collect images without alt
          const images = document.querySelectorAll("img");
          let imagesTotal = 0;
          let imagesMissingAlt = 0;
          images.forEach((img) => {
            imagesTotal++;
            if (!img.getAttribute("alt") && !img.getAttribute("aria-label")) {
              imagesMissingAlt++;
            }
          });
    
          // Collect links
          const links = document.querySelectorAll("a[href]");
          let linksTotal = 0;
          let linksNoText = 0;
          links.forEach((link) => {
            linksTotal++;
            const text = (link.textContent ?? "").trim();
            const ariaLabel = link.getAttribute("aria-label");
            const title = link.getAttribute("title");
            if (!text && !ariaLabel && !title) {
              linksNoText++;
            }
          });
    
          // Check for structured data
          const jsonLdScripts = document.querySelectorAll(
            'script[type="application/ld+json"]'
          );
          const structuredDataBlocks: string[] = [];
          jsonLdScripts.forEach((script) => {
            const content = (script.textContent ?? "").trim();
            if (content) {
              structuredDataBlocks.push(content.slice(0, 200));
            }
          });
    
          return {
            title: document.title,
            metaDescription: getMeta("description"),
            metaViewport:
              document
                .querySelector('meta[name="viewport"]')
                ?.getAttribute("content") ?? null,
            metaRobots: getMeta("robots"),
            canonical: getLink("canonical"),
            lang: document.documentElement.getAttribute("lang"),
            charset:
              document.querySelector("meta[charset]")?.getAttribute("charset") ??
              null,
    
            // Open Graph
            ogTitle: getMeta("og:title"),
            ogDescription: getMeta("og:description"),
            ogImage: getMeta("og:image"),
            ogType: getMeta("og:type"),
            ogUrl: getMeta("og:url"),
    
            // Twitter Card
            twitterCard: getMeta("twitter:card"),
            twitterTitle: getMeta("twitter:title"),
            twitterDescription: getMeta("twitter:description"),
    
            // Content
            headings,
            h1Count: headings.filter((h) => h.tag === "h1").length,
            imagesTotal,
            imagesMissingAlt,
            linksTotal,
            linksNoText,
            structuredDataBlocks,
            hasStructuredData: jsonLdScripts.length > 0,
          };
        });
    
        const checks = buildSeoChecks(pageData, url);
        const passed = checks.filter((c) => c.passed).length;
        const failed = checks.filter((c) => !c.passed).length;
        const score = computeSeoScore(checks);
        const summary = buildSeoSummary(passed, failed, score);
    
        return {
          url,
          timestamp: new Date().toISOString(),
          checks,
          passed,
          failed,
          score,
          summary,
        };
      } finally {
        await closePage(page);
      }
    }
  • src/server.ts:857-889 (registration)
    Registration of the 'seo_audit' MCP tool in server.ts using server.tool(). Defines the tool name, description, input schema (url), and handler that calls runSeoAudit() and formatSeoReport().
      server.tool(
        "seo_audit",
        `Run a comprehensive SEO audit. Checks 18 SEO signals including meta tags, heading hierarchy, Open Graph tags, Twitter cards, structured data (JSON-LD), canonical URLs, image alt text, and more. Returns a 0-100 score and specific recommendations for each failing check.
    
    Use this when the user wants to check their page's SEO health, improve search engine visibility, or ensure proper social sharing metadata.
    
    This tool is FREE — runs entirely within Claude Code.`,
        {
          url: z.string().url().describe("URL of the page to audit (e.g., http://localhost:3000)"),
        },
        async ({ url }) => {
          try {
            const result = await runSeoAudit(url);
            const report = formatSeoReport(result);
    
            return {
              content: [
                { type: "text" as const, text: report },
                {
                  type: "text" as const,
                  text: `\n\n<raw_data>\n${JSON.stringify(result, null, 2)}\n</raw_data>`,
                },
              ],
            };
          } catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            return {
              content: [{ type: "text" as const, text: `SEO audit failed: ${message}` }],
              isError: true,
            };
          }
        }
      );
  • Type definitions: SeoCheckResult interface (id, title, passed, value, recommendation, impact) and SeoAuditResult interface (url, timestamp, checks, passed, failed, score, summary).
    export interface SeoCheckResult {
      readonly id: string;
      readonly title: string;
      readonly passed: boolean;
      readonly value: string | null;
      readonly recommendation: string;
      readonly impact: "critical" | "high" | "medium" | "low";
    }
    
    export interface SeoAuditResult {
      readonly url: string;
      readonly timestamp: string;
      readonly checks: readonly SeoCheckResult[];
      readonly passed: number;
      readonly failed: number;
      readonly score: number;
      readonly summary: string;
    }
  • The 'formatSeoReport' function that formats the SEO audit result as a readable markdown report, grouping issues by impact (critical, high, medium, low) and listing passing checks.
    export function formatSeoReport(result: SeoAuditResult): string {
      const sections: string[] = [
        `## SEO Audit Results`,
        ``,
        `**URL:** ${result.url}`,
        `**Score:** ${result.score}/100`,
        `**Passed:** ${result.passed} | **Failed:** ${result.failed}`,
        ``,
      ];
    
      const failed = result.checks.filter((c) => !c.passed);
      const passed = result.checks.filter((c) => c.passed);
    
      if (failed.length > 0) {
        sections.push(`### Issues Found`);
        sections.push(``);
    
        // Group by impact
        const byImpact = groupByImpact(failed);
        for (const [impact, checks] of byImpact) {
          sections.push(`#### ${impact.toUpperCase()}`);
          for (const check of checks) {
            sections.push(`- **${check.title}**: ${check.recommendation}`);
            if (check.value) {
              sections.push(`  Current: ${check.value}`);
            }
          }
          sections.push(``);
        }
      }
    
      if (passed.length > 0) {
        sections.push(`### Passing Checks`);
        sections.push(``);
        for (const check of passed) {
          const detail = check.value ? ` (${check.value})` : "";
          sections.push(`- ${check.title}${detail}`);
        }
        sections.push(``);
      }
    
      return sections.join("\n");
    }
  • Helper function 'groupByImpact' that sorts SEO check results by severity order (critical > high > medium > low) for the report.
    function groupByImpact(
      checks: readonly SeoCheckResult[]
    ): readonly [string, readonly SeoCheckResult[]][] {
      const order = ["critical", "high", "medium", "low"] as const;
      const grouped = new Map<string, SeoCheckResult[]>();
    
      for (const check of checks) {
        const existing = grouped.get(check.impact) ?? [];
        grouped.set(check.impact, [...existing, check]);
      }
    
      return order
        .filter((impact) => grouped.has(impact))
        .map((impact) => [impact, grouped.get(impact) ?? []] as const);
    }
Behavior4/5

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

With no annotations, the description adds context: it checks 18 signals, returns score/recommendations, runs within Claude Code, and is FREE. It implies read-only behavior without stating destructive effects. No contradictions.

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

Conciseness5/5

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

Three well-structured sentences covering purpose, signals, usage guidance, and free status. No wasted words; front-loaded.

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 the simple input (single URL), no output schema, and no annotations, the description sufficiently covers tool behavior, what it checks, and what it returns. Also notes it's free, which is helpful context.

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

Parameters3/5

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

Schema coverage is 100% for the single parameter 'url'. The description does not add parameter-level details beyond the schema, but the schema itself is clear. Baseline 3 is appropriate.

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 runs a comprehensive SEO audit, lists 18 specific signals checked, and specifies the output (0-100 score and recommendations). This distinguishes it from sibling audit tools (e.g., accessibility_audit, performance_audit).

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?

Provides explicit guidance on when to use: checking SEO health, improving search visibility, or ensuring social sharing metadata. However, it does not explicitly exclude other scenarios or mention alternatives.

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/prembobby39-gif/uimax-mcp'

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