Skip to main content
Glama
mgsrevolver

SEO Inspector & Schema Validator MCP

analyzeSEO

Analyze HTML content for SEO issues and validate structured data schemas to identify optimization opportunities and ensure compliance with search engine requirements.

Instructions

ALWAYS USE THIS TOOL FOR SEO ANALYSIS. DO NOT ATTEMPT TO ANALYZE SEO WITHOUT USING THIS TOOL.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
htmlYesHTML content to analyze

Implementation Reference

  • Tool definition object including name 'analyzeSEO', description, and inputSchema supporting HTML content or directory path.
    const SEO_ANALYZER_TOOL = {
      name: 'analyzeSEO',
      description:
        'Analyzes HTML files for SEO issues and provides recommendations.',
      inputSchema: {
        type: 'object',
        properties: {
          html: {
            type: 'string',
            description: 'HTML content to analyze (optional)',
          },
          directoryPath: {
            type: 'string',
            description: 'Path to directory to analyze (optional)',
          },
        },
      },
    };
  • Registration of the analyzeSEO tool in the ListToolsRequestSchema handler.
    server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [SEO_ANALYZER_TOOL],
    }));
  • MCP server request handler for CallToolRequestSchema. Checks if tool name is 'analyzeSEO' and executes the SEO analysis logic for HTML or directory input, returning formatted results.
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
      if (request.params.name === 'analyzeSEO') {
        try {
          // Handle HTML content analysis
          if (request.params.arguments.html) {
            const html = request.params.arguments.html;
            const analysis = analyzeHtml(html, 'Provided HTML');
    
            return {
              content: [
                {
                  type: 'text',
                  text: formatAnalysisResult(analysis),
                },
              ],
            };
          }
          // Handle directory analysis
          else if (request.params.arguments.directoryPath) {
            const directoryPath = request.params.arguments.directoryPath;
            console.error(`Analyzing directory: ${directoryPath}`);
    
            try {
              // Check if directory exists
              try {
                await fs.access(directoryPath);
              } catch (error) {
                return {
                  content: [
                    {
                      type: 'text',
                      text: `Directory "${directoryPath}" does not exist or is not accessible. Please provide a valid directory path.`,
                    },
                  ],
                };
              }
    
              // Find HTML files
              const htmlFiles = await findHtmlFiles(directoryPath);
    
              if (htmlFiles.length === 0) {
                // Look for index.html in common locations
                const commonLocations = [
                  path.join(directoryPath, 'public', 'index.html'),
                  path.join(directoryPath, 'build', 'index.html'),
                  path.join(directoryPath, 'dist', 'index.html'),
                  path.join(directoryPath, 'index.html'),
                ];
    
                for (const location of commonLocations) {
                  try {
                    await fs.access(location);
                    htmlFiles.push(location);
                    console.error(`Found index.html at ${location}`);
                  } catch (error) {
                    // File doesn't exist, continue checking
                  }
                }
    
                if (htmlFiles.length === 0) {
                  return {
                    content: [
                      {
                        type: 'text',
                        text: `No HTML files found in ${directoryPath} or common subdirectories (public, build, dist).
                        
    If this is a React project, please specify the path to the public or build directory, or provide the path to a specific HTML file.`,
                      },
                    ],
                  };
                }
              }
    
              // Analyze each HTML file
              const results = [];
    
              for (const file of htmlFiles) {
                try {
                  const content = await fs.readFile(file, 'utf8');
                  const relativePath = path.relative(directoryPath, file);
                  const analysis = analyzeHtml(content, relativePath);
                  results.push(analysis);
                } catch (error) {
                  console.error(`Error analyzing ${file}:`, error);
                }
              }
    
              // Format and return results
              return {
                content: [
                  {
                    type: 'text',
                    text: formatDirectoryAnalysisResults(results, directoryPath),
                  },
                ],
              };
            } catch (error) {
              console.error('Error analyzing directory:', error);
              return {
                content: [
                  {
                    type: 'text',
                    text: `Error analyzing directory: ${error.message}`,
                  },
                ],
                isError: true,
              };
            }
          } else {
            return {
              content: [
                {
                  type: 'text',
                  text: 'Please provide either HTML content or a directory path to analyze.',
                },
              ],
            };
          }
        } catch (error) {
          console.error('Error in analyzeSEO tool:', error);
          return {
            content: [
              {
                type: 'text',
                text: `Error analyzing SEO: ${error.message}`,
              },
            ],
            isError: true,
          };
        }
      }
    });
  • Core handler function that performs detailed SEO analysis on HTML content using Cheerio: checks title, meta description, headings, image alts, structured data, canonical, viewport, social meta tags, and detects React apps.
    function analyzeHtml(html, pageIdentifier) {
      const $ = cheerio.load(html);
      const issues = [];
      const recommendations = [];
    
      // Basic SEO checks
      const title = $('title').text();
      const metaDescription = $('meta[name="description"]').attr('content');
      const h1Count = $('h1').length;
      const h2Count = $('h2').length;
      const h3Count = $('h3').length;
    
      // Check for React-specific elements
      const hasReactRoot =
        $('#root').length > 0 ||
        $('#app').length > 0 ||
        $('[data-reactroot]').length > 0;
    
      // Check title
      if (!title) {
        issues.push({ severity: 'high', message: 'Missing page title' });
        recommendations.push('Add a descriptive page title');
      } else if (title.length > 60) {
        issues.push({
          severity: 'medium',
          message: `Title length (${title.length} chars) exceeds recommended maximum of 60 characters`,
        });
        recommendations.push('Shorten title to under 60 characters');
      }
    
      // Check meta description
      if (!metaDescription) {
        issues.push({ severity: 'high', message: 'Missing meta description' });
        recommendations.push('Add a descriptive meta description');
      } else if (metaDescription.length < 50 || metaDescription.length > 160) {
        issues.push({
          severity: 'medium',
          message: `Meta description length (${metaDescription.length} chars) outside recommended range (50-160)`,
        });
        recommendations.push(
          'Adjust meta description to be between 50-160 characters'
        );
      }
    
      // Check headings
      if (h1Count === 0) {
        issues.push({ severity: 'high', message: 'No H1 heading found' });
        recommendations.push('Add an H1 heading to your page');
      } else if (h1Count > 1) {
        issues.push({
          severity: 'medium',
          message: `Multiple H1 headings found (${h1Count})`,
        });
        recommendations.push('Use only one H1 heading per page');
      }
    
      // Check images
      $('img').each((i, img) => {
        const alt = $(img).attr('alt');
        if (!alt && !$(img).attr('role')) {
          issues.push({
            severity: 'medium',
            message: `Image missing alt text: ${
              $(img).attr('src') || 'unknown image'
            }`,
          });
        }
      });
    
      const imagesWithoutAlt = $('img:not([alt])').length;
      if (imagesWithoutAlt > 0) {
        recommendations.push('Add alt text to all images');
      }
    
      // Check for schema markup
      const schemas = [];
      $('script[type="application/ld+json"]').each((i, script) => {
        try {
          const schema = JSON.parse($(script).html());
          schemas.push(schema);
        } catch (e) {
          issues.push({ severity: 'high', message: 'Invalid JSON-LD schema' });
        }
      });
    
      if (schemas.length === 0) {
        issues.push({
          severity: 'medium',
          message: 'No structured data (schema.org) found',
        });
        recommendations.push('Add structured data using JSON-LD');
      }
    
      // Check for canonical URL
      if ($('link[rel="canonical"]').length === 0) {
        issues.push({ severity: 'medium', message: 'Missing canonical URL tag' });
        recommendations.push('Add a canonical URL tag');
      }
    
      // Check for viewport meta tag
      if ($('meta[name="viewport"]').length === 0) {
        issues.push({ severity: 'medium', message: 'Missing viewport meta tag' });
        recommendations.push('Add a viewport meta tag for better mobile rendering');
      }
    
      // Check for social media tags
      const hasOgTags = $('meta[property^="og:"]').length > 0;
      const hasTwitterTags = $('meta[name^="twitter:"]').length > 0;
    
      if (!hasOgTags) {
        issues.push({ severity: 'low', message: 'Missing Open Graph meta tags' });
        recommendations.push(
          'Add Open Graph meta tags for better social media sharing'
        );
      }
    
      if (!hasTwitterTags) {
        issues.push({ severity: 'low', message: 'Missing Twitter Card meta tags' });
        recommendations.push(
          'Add Twitter Card meta tags for better Twitter sharing'
        );
      }
    
      // React-specific recommendations
      if (hasReactRoot) {
        issues.push({
          severity: 'info',
          message:
            'This appears to be a React application with client-side rendering',
        });
        recommendations.push(
          'Consider using server-side rendering (Next.js) or static site generation (Gatsby) for better SEO'
        );
        recommendations.push(
          'Note: This analysis is limited to the static HTML. The rendered content may differ.'
        );
      }
    
      return {
        pageIdentifier,
        title,
        metaDescription,
        headingStructure: {
          h1: h1Count,
          h2: h2Count,
          h3: h3Count,
        },
        schemaCount: schemas.length,
        issues,
        recommendations,
        isReactApp: hasReactRoot,
      };
    }
  • Helper function to recursively find all HTML files (.html, .htm) in a directory, skipping node_modules and .git.
    async function findHtmlFiles(directory) {
      const htmlFiles = [];
    
      async function traverse(dir) {
        try {
          const entries = await fs.readdir(dir, { withFileTypes: true });
    
          for (const entry of entries) {
            if (entry.name === 'node_modules' || entry.name === '.git') {
              continue;
            }
    
            const fullPath = path.join(dir, entry.name);
    
            if (entry.isDirectory()) {
              await traverse(fullPath);
            } else if (
              entry.name.endsWith('.html') ||
              entry.name.endsWith('.htm')
            ) {
              htmlFiles.push(fullPath);
            }
          }
        } catch (error) {
          console.error(`Error traversing ${dir}:`, error);
        }
      }
    
      await traverse(directory);
      return htmlFiles;
    }
Behavior1/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. However, it adds no information about what the tool does behaviorally—such as whether it performs read-only analysis, requires internet access, has rate limits, returns structured data, or handles errors. The description is purely instructional and lacks any transparency into the tool's operations or traits.

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

Conciseness2/5

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

The description is two sentences that are repetitive and under-specified—'ALWAYS USE THIS TOOL FOR SEO ANALYSIS. DO NOT ATTEMPT TO ANALYZE SEO WITHOUT USING THIS TOOL.' This is not conciseness but rather redundancy without adding value. It fails to front-load useful information and wastes space on commands rather than explanation.

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

Completeness1/5

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

Given the tool's complexity (SEO analysis typically involves multiple metrics) and the lack of annotations and output schema, the description is completely inadequate. It doesn't explain what SEO analysis entails, what results to expect, or any behavioral context. For a tool with no structured support and a potentially rich function, this description fails to provide necessary completeness.

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?

The input schema has 100% description coverage, with the single parameter 'html' clearly documented as 'HTML content to analyze'. The description adds no additional meaning about the parameter beyond what the schema provides, such as format expectations or constraints. Since schema coverage is high, the baseline score of 3 is appropriate, as the description doesn't compensate but also doesn't detract.

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

Purpose2/5

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

The description states 'ALWAYS USE THIS TOOL FOR SEO ANALYSIS' which tautologically restates the tool name 'analyzeSEO' without specifying what SEO analysis entails. It doesn't describe what specific SEO metrics or aspects are analyzed, nor does it differentiate from any siblings (though none exist). The description is more of a command than a functional explanation.

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

Usage Guidelines1/5

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

The description provides no guidance on when to use this tool versus alternatives—it only gives an absolute command ('ALWAYS USE THIS TOOL FOR SEO ANALYSIS') without context about what constitutes SEO analysis or prerequisites. There's no mention of input requirements, expected scenarios, or comparisons to other methods, making it misleadingly prescriptive without practical guidance.

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/mgsrevolver/seo-inspector-mcp'

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