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; }

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