Skip to main content
Glama
AutomateLab-tech

automatelab-ai-seo

Official

audit_page

Analyze a webpage for SEO issues, providing actionable fixes, severity ratings, and a 0-100 composite score covering schema, robots, technical, sitemap, and AI-Overview eligibility.

Instructions

Full AI-SEO audit of a single URL: returns categorized findings (info/warning/error) with severity, fix instructions, and a 0-100 composite score plus per-dimension subscores.

Read-only. Fetches the URL once and runs every sub-audit (schema, robots, technical, sitemap, AI-Overview eligibility) against the response. No writes, no third-party APIs, no auth required, no rate limits beyond polite per-host throttling.

Deterministic, rule-based scoring; no LLM calls. Same URL + same input flags returns the same score.

When to use: the default entry point for audit any page. Use this instead of calling check_technical / audit_schema / check_robots / check_sitemap / score_ai_overview_eligibility individually unless you specifically need only one dimension - this tool composes all of them.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesPublic URL to audit. Must be a fully-qualified http(s) URL that returns HTTP 200 (redirects are followed). The tool fetches this URL once and runs every sub-audit (schema, robots, technical, sitemap, AI-Overview eligibility) against the response.
include_raw_htmlNoIf true, return the full raw HTML in the response under `raw_html`. Default false. Set true only when you need to inspect markup that wasn't captured by the structured findings; the payload can be large.
respect_robotsNoIf true (default), the tool checks robots.txt before fetching and skips disallowed paths, returning a robots_blocked finding instead. Set to false ONLY for auditing your own site where you've intentionally blocked crawlers and need the audit to bypass that block.

Implementation Reference

  • Main handler for audit_page. Fetches the URL once, runs sub-audits (schema, technical, robots, freshness, structure, authority, entity_density, sitemap), computes weighted score and grade, deduplicates findings.
    export async function auditPage(input: AuditPageInput): Promise<AuditPageResult> {
      const hostDelays: HostDelayMap = new Map();
      const robotsCache = new Map<string, string>();
    
      // Fetch URL once
      const result = await politeFetch(input.url, {
        respectRobots: input.respect_robots,
        hostDelays,
        robotsCache,
      });
    
      const ct = result.headers["content-type"];
      const ctStr = Array.isArray(ct) ? ct[0] : (ct ?? "");
      if (ctStr && !ctStr.includes("html")) {
        throw new ToolFetchError({
          type: "non_html_response",
          url: input.url,
          content_type: ctStr,
        });
      }
    
      const fetched_at = new Date().toISOString();
      const allFindings: Finding[] = [];
    
      // --- Schema dimension ---
      let schemaScore = 50;
      try {
        const schemaResult = await auditSchema(
          { url: input.url, respect_robots: input.respect_robots },
          hostDelays,
          robotsCache
        );
        schemaScore = schemaResult.ai_citation_readiness_score;
        allFindings.push(...schemaResult.findings);
      } catch {
        // schema audit failed - use default score
      }
    
      // --- Technical dimension ---
      let technicalScore = 50;
      try {
        const techResult = await checkTechnical(
          { url: input.url, respect_robots: input.respect_robots },
          hostDelays,
          robotsCache
        );
        // Derive technical score from findings
        const techFindings = techResult.findings;
        allFindings.push(...techFindings);
        const criticals = techFindings.filter((f) => f.severity === "critical").length;
        const warnings = techFindings.filter((f) => f.severity === "warning").length;
        technicalScore = Math.max(0, 100 - criticals * 20 - warnings * 8);
        // noindex is a killer
        if (techResult.noindex) technicalScore = Math.max(0, technicalScore - 30);
      } catch {
        // technical audit failed
      }
    
      // --- Robots dimension ---
      let robotsScore = 70;
      try {
        const hostname = new URL(input.url).hostname;
        const robotsResult = await checkRobots({ domain: hostname });
        const robotsFindings = robotsResult.findings.filter(
          (f) => f.severity === "critical" || f.severity === "warning"
        );
        allFindings.push(...robotsResult.findings);
        robotsScore = Math.max(
          0,
          100 -
            robotsFindings.filter((f) => f.severity === "critical").length * 15 -
            robotsFindings.filter((f) => f.severity === "warning").length * 7
        );
      } catch {
        // robots check failed
      }
    
      // --- Page content for structure/freshness/authority/entity_density ---
      const head = parseHead(result.body);
      const body = parseBody(result.body, input.url);
      const jsonLdBlocks = parseJsonLd(result.body);
      const foundTypes = getAllSchemaTypes(jsonLdBlocks);
    
      // --- Freshness dimension ---
      let dateModified: string | null = null;
      for (const b of jsonLdBlocks) {
        const dm = b.parsed["dateModified"] ?? b.parsed["datePublished"];
        if (typeof dm === "string") {
          dateModified = dm;
          break;
        }
      }
      const freshnessScoreVal = freshnessScore(dateModified);
      if (freshnessScoreVal < 50) {
        allFindings.push({
          severity: "warning",
          category: "freshness",
          where: "Article.dateModified",
          message: dateModified
            ? "Content appears stale (dateModified > 90 days ago)."
            : "No dateModified found in structured data.",
          fix: "Update content and set dateModified to today in Article JSON-LD.",
          estimated_impact: "medium",
        });
      }
    
      // --- Structure dimension ---
      const hasFaq = foundTypes.includes("FAQPage") || body.h3s.some((h) => h.endsWith("?"));
      const hasHowTo = foundTypes.includes("HowTo");
      const hasOrderedList = body.orderedLists > 0;
      const hasTable = body.tables > 0;
      const goodHeadings = body.h2s.length >= 2;
      let structureScore = 20;
      if (hasFaq) structureScore += 30;
      if (hasHowTo) structureScore += 15;
      if (hasOrderedList) structureScore += 15;
      if (hasTable) structureScore += 10;
      if (goodHeadings) structureScore += 10;
      structureScore = Math.min(100, structureScore);
    
      if (!hasFaq) {
        allFindings.push({
          severity: "critical",
          category: "structure",
          where: "<body>",
          message: "No FAQ structure found (no FAQPage schema or H3 question headings).",
          fix: "Add FAQ H3 headings ending in '?' with answer paragraphs, and a FAQPage JSON-LD block.",
          estimated_impact: "high",
        });
      }
    
      // --- Authority dimension ---
      const hasOrg = foundTypes.includes("Organization");
      const hasPerson = jsonLdBlocks.some((b) => b.types.includes("Person"));
      const hasArticleWithAuthorNode = jsonLdBlocks.some(
        (b) =>
          b.types.some((t) => ["Article", "BlogPosting", "NewsArticle"].includes(t)) &&
          typeof b.parsed["author"] === "object" &&
          b.parsed["author"] !== null
      );
      let authorityScore = 10;
      if (hasOrg) authorityScore += 30;
      if (hasPerson || hasArticleWithAuthorNode) authorityScore += 30;
      const sameAsCount = jsonLdBlocks.reduce((acc, b) => {
        const sa = b.parsed["sameAs"];
        return acc + (Array.isArray(sa) ? sa.length : sa ? 1 : 0);
      }, 0);
      if (sameAsCount > 0) authorityScore += Math.min(30, sameAsCount * 5);
      authorityScore = Math.min(100, authorityScore);
    
      if (authorityScore < 40) {
        allFindings.push({
          severity: "warning",
          category: "authority",
          where: "page-level",
          message: "Low authority signals - missing Organization or author Person schema.",
          fix: "Add Organization JSON-LD and Article.author as a Person node with sameAs links.",
          estimated_impact: "high",
        });
      }
    
      // --- Entity density dimension ---
      const authoritativeDomains = [
        "wikipedia.org","wikidata.org",".gov","linkedin.com","twitter.com",
        "x.com","crunchbase.com","bloomberg.com","reuters.com",
      ];
      const authExternalLinks = body.externalLinks.filter((href) =>
        authoritativeDomains.some((d) => href.includes(d))
      ).length;
      const entityDensityScore = Math.min(100, (sameAsCount + authExternalLinks) * 7);
    
      // --- Sitemap dimension ---
      let sitemapScore = 50;
      try {
        const hostname = new URL(input.url).hostname;
        const sitemapResult = await checkSitemap(
          { domain: hostname, max_urls_to_check: 50 },
          hostDelays,
          robotsCache
        );
        if (sitemapResult.status === "found") {
          sitemapScore = 80;
          if (sitemapResult.urls_with_lastmod / Math.max(1, sitemapResult.total_urls) > 0.8) {
            sitemapScore = 100;
          }
        } else {
          sitemapScore = 20;
        }
      } catch {
        // sitemap check failed
      }
    
      // --- Weighted composite score ---
      const dimensionScores = {
        schema: schemaScore,
        robots: robotsScore,
        technical: technicalScore,
        freshness: freshnessScoreVal,
        structure: structureScore,
        authority: authorityScore,
        entity_density: entityDensityScore,
        sitemap: sitemapScore,
      };
    
      const score = computeWeightedScore(dimensionScores);
      const grade = deriveGrade(score);
    
      // Deduplicate findings (same message from multiple sub-tools)
      const seen = new Set<string>();
      const deduped = allFindings.filter((f) => {
        const key = `${f.severity}:${f.category}:${f.message}`;
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
      });
    
      const output: AuditPageResult = {
        url: input.url,
        fetched_at,
        findings: deduped,
        score,
        grade,
        dimension_scores: dimensionScores,
      };
    
      if (input.include_raw_html) {
        output.raw_html = result.body;
      }
    
      return output;
    }
  • Zod schema for audit_page input: url (required), include_raw_html (optional, default false), respect_robots (optional, default true).
    export const auditPageInputSchema = z.object({
      url: z
        .string()
        .url()
        .describe("Public URL to audit. Must be a fully-qualified http(s) URL that returns HTTP 200 (redirects are followed). The tool fetches this URL once and runs every sub-audit (schema, robots, technical, sitemap, AI-Overview eligibility) against the response."),
      include_raw_html: z
        .boolean()
        .optional()
        .default(false)
        .describe("If true, return the full raw HTML in the response under `raw_html`. Default false. Set true only when you need to inspect markup that wasn't captured by the structured findings; the payload can be large."),
      respect_robots: z
        .boolean()
        .optional()
        .default(true)
        .describe("If true (default), the tool checks robots.txt before fetching and skips disallowed paths, returning a robots_blocked finding instead. Set to false ONLY for auditing your own site where you've intentionally blocked crawlers and need the audit to bypass that block."),
    });
  • Type definition for the result returned by auditPage, extending AuditResult with per-dimension scores and optional raw HTML.
    export interface AuditPageResult extends AuditResult {
      dimension_scores: {
        schema: number;
        robots: number;
        technical: number;
        freshness: number;
        structure: number;
        authority: number;
        entity_density: number;
        sitemap: number;
      };
      raw_html?: string;
    }
  • src/index.ts:56-67 (registration)
    Registration of audit_page tool in the MCP server with description and handler wrapped in error handling.
    // --- Tool 1: audit_page ---
    server.tool(
      "audit_page",
      [
        "Full AI-SEO audit of a single URL: returns categorized findings (info/warning/error) with severity, fix instructions, and a 0-100 composite score plus per-dimension subscores.",
        "Read-only. Fetches the URL once and runs every sub-audit (schema, robots, technical, sitemap, AI-Overview eligibility) against the response. No writes, no third-party APIs, no auth required, no rate limits beyond polite per-host throttling.",
        "Deterministic, rule-based scoring; no LLM calls. Same URL + same input flags returns the same score.",
        "When to use: the default entry point for `audit any page`. Use this instead of calling check_technical / audit_schema / check_robots / check_sitemap / score_ai_overview_eligibility individually unless you specifically need only one dimension - this tool composes all of them.",
      ].join("\n\n"),
      auditPageInputSchema.shape,
      async (input) => wrapHandler(() => auditPage(input))
    );
  • Weighted composite score calculation used by audit_page: schema 25%, technical 20%, structure 20%, robots 10%, freshness 10%, authority 10%, entity_density 3%, sitemap 2%.
    export function computeWeightedScore(dimensions: {
      schema: number;
      technical: number;
      structure: number;
      robots: number;
      freshness: number;
      authority: number;
      entity_density: number;
      sitemap: number;
    }): number {
      const score =
        dimensions.schema * 0.25 +
        dimensions.technical * 0.2 +
        dimensions.structure * 0.2 +
        dimensions.robots * 0.1 +
        dimensions.freshness * 0.1 +
        dimensions.authority * 0.1 +
        dimensions.entity_density * 0.03 +
        dimensions.sitemap * 0.02;
      return Math.round(Math.max(0, Math.min(100, score)));
    }
Behavior5/5

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

With no annotations, the description fully discloses behavioral traits: read-only (fetches URL once, no writes, no auth, polite throttling), deterministic rule-based scoring (no LLM calls), and the effect of the `respect_robots` parameter. This exceeds expectations.

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?

Four well-structured sentences, each adding unique value: first defines purpose, second covers safety/read-only, third deterministic nature, fourth usage guidance. No wasted words.

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

Completeness4/5

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

Covers purpose, behavior, usage, and parameter nuances comprehensively. Describes return structure (findings, scores) but lacks exact format details. Still complete enough given no output schema.

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. Description adds value by explaining when to set `respect_robots` to false (own site with intentional blocks) and warning about `include_raw_html` payload size, going beyond what the schema provides.

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 it performs a full AI-SEO audit of a single URL, returning categorized findings, scores, and fix instructions. It distinguishes from siblings by explicitly listing the sub-audits it composes and positioning it as the default entry point.

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

Usage Guidelines5/5

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

Explicitly states 'When to use: the default entry point for audit any page' and advises using this tool instead of calling individual audit tools unless only one dimension is needed. Provides clear context and 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/AutomateLab-tech/ai-seo'

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