remote-seo-checker.py•35.1 kB
#!/usr/bin/env python3
"""
Remote MCP Server - Professional SEO Checker
This is a remote MCP server for comprehensive SEO analysis that can be deployed
to Hostinger or other hosting platforms. This implementation provides professional-grade
SEO analysis capabilities for websites.
DEPLOYMENT WORKFLOW:
1. Deploy this server to Hostinger using your hosting account
2. Get the deployed URL from Hostinger (e.g., https://srv563806.hstgr.cloud)
3. Use that URL + /mcp in your MCP client configuration:
- Claude Desktop/Code
- Cursor
- Windsurf
- Other MCP-compatible applications
Example MCP client configuration:
{
"mcpServers": {
"seo-checker": {
"url": "https://srv563806.hstgr.cloud/mcp",
"description": "Professional SEO analysis and optimization recommendations"
}
}
}
This remote MCP server provides comprehensive SEO analysis including:
- Title tags, meta descriptions, headers structure analysis
- Image optimization and alt tags checking
- Page speed and technical SEO evaluation
- Content analysis and keyword density
- Social media tags (Open Graph, Twitter Cards)
- Schema markup detection
- Mobile-friendliness indicators
- Performance scoring and actionable recommendations
"""
import asyncio
import json
import logging
import re
import time
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urljoin, urlparse
import aiohttp
from bs4 import BeautifulSoup
from fastmcp import FastMCP
# Configure logging for the MCP server
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("seo-checker")
# Create the FastMCP server instance
# This is the main MCP server object that will handle client connections
mcp = FastMCP(
name="Professional SEO Checker",
instructions="When asked about SEO analysis, page optimization, meta tags, or search engine optimization, use the appropriate SEO checking tools."
)
class SEOChecker:
"""Professional SEO analysis tool with comprehensive website evaluation
This class encapsulates the core SEO analysis logic that will be
exposed through MCP tools. It performs detailed analysis of various
SEO factors and provides actionable recommendations.
"""
def __init__(self):
# Configure reasonable timeouts to prevent hanging requests
self.default_timeout = 20
self.user_agent = "Mozilla/5.0 (SEO Checker MCP Server; +https://srv563806.hstgr.cloud/seo-bot)"
async def analyze_page_seo(self, url: str) -> Dict[str, Any]:
"""Comprehensive SEO analysis of a webpage
This is the main async method that coordinates all SEO analysis.
MCP tools must be async to avoid blocking the server.
"""
# Ensure URL has protocol for proper parsing
if not url.startswith(('http://', 'https://')):
url = f'https://{url}'
parsed_url = urlparse(url)
# Initialize result structure with all possible fields
results = {
"url": url,
"domain": parsed_url.netloc,
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "unknown",
"seo_score": 0,
"page_info": {},
"title_analysis": {},
"meta_analysis": {},
"header_analysis": {},
"content_analysis": {},
"image_analysis": {},
"technical_seo": {},
"social_media": {},
"recommendations": [],
"critical_issues": [],
"warnings": [],
"errors": []
}
try:
# Fetch page content and measure performance
page_data = await self._fetch_page_with_timing(url)
if page_data["status"]["code"] != 200:
results["errors"].append(f"Cannot access page: {page_data['status']['code']} {page_data['status']['text']}")
results["status"] = "error"
return results
# Parse HTML content using BeautifulSoup for analysis
soup = BeautifulSoup(page_data["content"], 'html.parser')
# Perform comprehensive SEO analyses
results["page_info"] = self._analyze_page_info(page_data, soup)
results["title_analysis"] = self._analyze_title(soup)
results["meta_analysis"] = self._analyze_meta_tags(soup)
results["header_analysis"] = self._analyze_headers(soup)
results["content_analysis"] = self._analyze_content(soup, page_data["content"])
results["image_analysis"] = self._analyze_images(soup, url)
results["technical_seo"] = self._analyze_technical_seo(soup, page_data)
results["social_media"] = self._analyze_social_media_tags(soup)
# Generate recommendations and calculate overall score
results["recommendations"], results["critical_issues"], results["warnings"] = self._generate_recommendations(results)
results["seo_score"] = self._calculate_seo_score(results)
results["status"] = "success"
except Exception as e:
results["status"] = "error"
results["errors"].append(str(e))
logger.error(f"SEO analysis failed for {url} - {e}")
return results
async def _fetch_page_with_timing(self, url: str) -> Dict[str, Any]:
"""Fetch page with performance timing for technical SEO analysis
Uses aiohttp for async HTTP requests and measures timing metrics
that are important for SEO performance evaluation.
"""
timeout = aiohttp.ClientTimeout(total=self.default_timeout)
headers = {'User-Agent': self.user_agent}
start_time = time.time()
async with aiohttp.ClientSession(timeout=timeout, headers=headers) as session:
try:
async with session.get(url, allow_redirects=True) as response:
ttfb_time = time.time()
content = await response.text()
end_time = time.time()
return {
"content": content,
"status": {
"code": response.status,
"text": response.reason,
"final_url": str(response.url)
},
"timing": {
"ttfb_ms": round((ttfb_time - start_time) * 1000, 2),
"total_ms": round((end_time - start_time) * 1000, 2),
"size_bytes": len(content.encode('utf-8'))
},
"headers": dict(response.headers)
}
except Exception as e:
return {
"content": "",
"status": {"code": 0, "text": str(e), "final_url": url},
"timing": {"ttfb_ms": 0, "total_ms": 0, "size_bytes": 0},
"headers": {}
}
def _analyze_page_info(self, page_data: Dict, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze basic page information for SEO fundamentals"""
return {
"status_code": page_data["status"]["code"],
"final_url": page_data["status"]["final_url"],
"load_time_ms": page_data["timing"]["total_ms"],
"page_size_kb": round(page_data["timing"]["size_bytes"] / 1024, 2),
"has_doctype": str(soup).startswith('<!DOCTYPE'),
"language": soup.find('html', {'lang': True}).get('lang') if soup.find('html', {'lang': True}) else None,
"charset": self._extract_charset(soup)
}
def _extract_charset(self, soup: BeautifulSoup) -> Optional[str]:
"""Extract character encoding information"""
charset_tag = soup.find('meta', {'charset': True})
if charset_tag:
return charset_tag.get('charset')
content_type = soup.find('meta', {'http-equiv': 'Content-Type'})
if content_type and content_type.get('content'):
content = content_type.get('content')
match = re.search(r'charset=([^;]+)', content)
if match:
return match.group(1)
return None
def _analyze_title(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze title tag for SEO optimization"""
title_tag = soup.find('title')
if not title_tag:
return {
"exists": False,
"content": "",
"length": 0,
"issues": ["Missing title tag"],
"score": 0
}
title_text = title_tag.get_text().strip()
title_length = len(title_text)
issues = []
score = 100
# Title length analysis based on SEO best practices
if title_length == 0:
issues.append("Title tag is empty")
score -= 50
elif title_length < 30:
issues.append("Title is too short (< 30 characters)")
score -= 20
elif title_length > 60:
issues.append("Title may be truncated in search results (> 60 characters)")
score -= 10
# Additional SEO checks
if title_text.count('|') > 2:
issues.append("Too many separators in title")
score -= 5
if title_text.upper() == title_text and len(title_text) > 10:
issues.append("Title is in ALL CAPS")
score -= 10
return {
"exists": True,
"content": title_text,
"length": title_length,
"issues": issues,
"score": max(0, score)
}
def _analyze_meta_tags(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze all important meta tags for SEO"""
meta_analysis = {
"description": self._analyze_meta_description(soup),
"keywords": self._analyze_meta_keywords(soup),
"robots": self._analyze_robots_meta(soup),
"viewport": self._analyze_viewport_meta(soup),
"canonical": self._analyze_canonical(soup)
}
return meta_analysis
def _analyze_meta_description(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze meta description for search snippet optimization"""
desc_tag = soup.find('meta', {'name': 'description'}) or soup.find('meta', {'property': 'description'})
if not desc_tag or not desc_tag.get('content'):
return {
"exists": False,
"content": "",
"length": 0,
"issues": ["Missing meta description"],
"score": 0
}
desc_text = desc_tag.get('content').strip()
desc_length = len(desc_text)
issues = []
score = 100
# Meta description length optimization
if desc_length == 0:
issues.append("Meta description is empty")
score -= 50
elif desc_length < 120:
issues.append("Meta description is too short (< 120 characters)")
score -= 15
elif desc_length > 160:
issues.append("Meta description may be truncated (> 160 characters)")
score -= 10
return {
"exists": True,
"content": desc_text,
"length": desc_length,
"issues": issues,
"score": max(0, score)
}
def _analyze_meta_keywords(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze meta keywords (obsolete but worth noting)"""
keywords_tag = soup.find('meta', {'name': 'keywords'})
if not keywords_tag:
return {"exists": False, "note": "Meta keywords not used (good - they're obsolete)"}
return {
"exists": True,
"content": keywords_tag.get('content', '').strip(),
"note": "Meta keywords are obsolete and ignored by search engines"
}
def _analyze_robots_meta(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze robots meta tag for crawling directives"""
robots_tag = soup.find('meta', {'name': 'robots'})
if not robots_tag:
return {"exists": False, "directives": [], "issues": []}
content = robots_tag.get('content', '').lower()
directives = [d.strip() for d in content.split(',')]
issues = []
# Check for SEO-impacting directives
if 'noindex' in directives:
issues.append("Page is set to NOINDEX - won't appear in search results")
if 'nofollow' in directives:
issues.append("Page is set to NOFOLLOW - links won't be followed")
return {
"exists": True,
"directives": directives,
"issues": issues
}
def _analyze_viewport_meta(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze viewport meta tag for mobile SEO"""
viewport_tag = soup.find('meta', {'name': 'viewport'})
if not viewport_tag:
return {
"exists": False,
"issues": ["Missing viewport meta tag - may not be mobile-friendly"]
}
content = viewport_tag.get('content', '')
has_width = 'width=' in content
has_initial_scale = 'initial-scale=' in content
issues = []
if not has_width:
issues.append("Viewport should specify width")
if not has_initial_scale:
issues.append("Viewport should specify initial-scale")
return {
"exists": True,
"content": content,
"issues": issues
}
def _analyze_canonical(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze canonical link tag for duplicate content prevention"""
canonical_tag = soup.find('link', {'rel': 'canonical'})
if not canonical_tag:
return {
"exists": False,
"issues": ["Missing canonical URL - may cause duplicate content issues"]
}
canonical_url = canonical_tag.get('href', '')
issues = []
if not canonical_url:
issues.append("Canonical tag exists but has no href")
elif not canonical_url.startswith(('http://', 'https://')):
issues.append("Canonical URL should be absolute")
return {
"exists": True,
"url": canonical_url,
"issues": issues
}
def _analyze_headers(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze header tag structure (H1-H6) for content hierarchy"""
headers = {}
structure_issues = []
# Extract all header tags
for i in range(1, 7):
header_tags = soup.find_all(f'h{i}')
headers[f'h{i}'] = {
"count": len(header_tags),
"content": [tag.get_text().strip()[:100] for tag in header_tags[:5]] # First 5, truncated
}
# H1 analysis (critical for SEO)
h1_count = headers['h1']['count']
if h1_count == 0:
structure_issues.append("Missing H1 tag")
elif h1_count > 1:
structure_issues.append(f"Multiple H1 tags found ({h1_count}) - should have only one")
# Overall structure analysis
has_headers = any(headers[f'h{i}']['count'] > 0 for i in range(1, 7))
if not has_headers:
structure_issues.append("No header tags found")
return {
"structure": headers,
"issues": structure_issues,
"score": 100 - (len(structure_issues) * 15)
}
def _analyze_content(self, soup: BeautifulSoup, html_content: str) -> Dict[str, Any]:
"""Analyze page content for SEO quality"""
# Extract text content for analysis
text_content = soup.get_text()
words = text_content.split()
word_count = len(words)
issues = []
score = 100
# Content length analysis
if word_count < 300:
issues.append("Content is thin (< 300 words)")
score -= 25
elif word_count < 150:
issues.append("Very thin content (< 150 words)")
score -= 40
# Text to HTML ratio analysis
html_size = len(html_content)
text_size = len(text_content)
text_ratio = (text_size / html_size) * 100 if html_size > 0 else 0
if text_ratio < 15:
issues.append("Low text-to-HTML ratio (< 15%)")
score -= 15
return {
"word_count": word_count,
"character_count": len(text_content),
"text_to_html_ratio": round(text_ratio, 1),
"issues": issues,
"score": max(0, score)
}
def _analyze_images(self, soup: BeautifulSoup, base_url: str) -> Dict[str, Any]:
"""Analyze images for SEO optimization"""
img_tags = soup.find_all('img')
total_images = len(img_tags)
if total_images == 0:
return {
"total_images": 0,
"images_with_alt": 0,
"images_without_alt": 0,
"issues": ["No images found"],
"score": 100
}
images_with_alt = 0
images_without_alt = 0
issues = []
# Check each image for alt text
for img in img_tags:
alt_text = img.get('alt')
if alt_text is not None and alt_text.strip():
images_with_alt += 1
else:
images_without_alt += 1
# Calculate alt text percentage
alt_percentage = (images_with_alt / total_images) * 100
score = alt_percentage
if images_without_alt > 0:
issues.append(f"{images_without_alt} images missing alt text")
return {
"total_images": total_images,
"images_with_alt": images_with_alt,
"images_without_alt": images_without_alt,
"alt_percentage": round(alt_percentage, 1),
"issues": issues,
"score": round(score)
}
def _analyze_technical_seo(self, soup: BeautifulSoup, page_data: Dict) -> Dict[str, Any]:
"""Analyze technical SEO factors"""
issues = []
# Page speed analysis (important ranking factor)
load_time = page_data["timing"]["total_ms"]
if load_time > 3000:
issues.append("Slow page load time (> 3 seconds)")
elif load_time > 2000:
issues.append("Page load time could be improved (> 2 seconds)")
# HTTPS check (ranking factor)
is_https = page_data["status"]["final_url"].startswith('https')
if not is_https:
issues.append("Page is not served over HTTPS")
# Check for structured data (schema markup)
schema_scripts = soup.find_all('script', {'type': 'application/ld+json'})
has_schema = len(schema_scripts) > 0
return {
"https": is_https,
"load_time_ms": load_time,
"page_size_kb": round(page_data["timing"]["size_bytes"] / 1024, 2),
"has_schema_markup": has_schema,
"schema_types": len(schema_scripts),
"issues": issues
}
def _analyze_social_media_tags(self, soup: BeautifulSoup) -> Dict[str, Any]:
"""Analyze Open Graph and Twitter Card tags for social SEO"""
# Open Graph tags analysis
og_tags = {}
og_metas = soup.find_all('meta', property=lambda x: x and x.startswith('og:'))
for tag in og_metas:
prop = tag.get('property', '').replace('og:', '')
og_tags[prop] = tag.get('content', '')
# Twitter Card tags analysis
twitter_tags = {}
twitter_metas = soup.find_all('meta', {'name': lambda x: x and x.startswith('twitter:')})
for tag in twitter_metas:
name = tag.get('name', '').replace('twitter:', '')
twitter_tags[name] = tag.get('content', '')
# Score calculation for social media optimization
og_score = 0
twitter_score = 0
essential_og = ['title', 'description', 'image', 'url']
og_score = sum(25 for tag in essential_og if tag in og_tags and og_tags[tag])
essential_twitter = ['card', 'title', 'description']
twitter_score = sum(33 for tag in essential_twitter if tag in twitter_tags and twitter_tags[tag])
return {
"open_graph": {
"tags": og_tags,
"score": og_score,
"has_essential": og_score == 100
},
"twitter_cards": {
"tags": twitter_tags,
"score": min(twitter_score, 100),
"has_essential": twitter_score >= 100
}
}
def _generate_recommendations(self, results: Dict) -> Tuple[List[str], List[str], List[str]]:
"""Generate SEO recommendations based on comprehensive analysis"""
recommendations = []
critical_issues = []
warnings = []
# Title tag recommendations
if not results["title_analysis"]["exists"]:
critical_issues.append("Add a title tag to the page")
elif results["title_analysis"]["length"] < 30:
recommendations.append("Expand title tag (aim for 30-60 characters)")
elif results["title_analysis"]["length"] > 60:
warnings.append("Consider shortening title tag to avoid truncation")
# Meta description recommendations
if not results["meta_analysis"]["description"]["exists"]:
critical_issues.append("Add a meta description tag")
elif results["meta_analysis"]["description"]["length"] < 120:
recommendations.append("Expand meta description (aim for 120-160 characters)")
# Header structure recommendations
if results["header_analysis"]["structure"]["h1"]["count"] == 0:
critical_issues.append("Add an H1 tag to the page")
elif results["header_analysis"]["structure"]["h1"]["count"] > 1:
warnings.append("Use only one H1 tag per page")
# Content recommendations
if results["content_analysis"]["word_count"] < 300:
recommendations.append("Increase content length (aim for 300+ words)")
# Image optimization recommendations
if results["image_analysis"]["images_without_alt"] > 0:
recommendations.append(f"Add alt text to {results['image_analysis']['images_without_alt']} images")
# Technical SEO recommendations
tech_issues = results["technical_seo"]["issues"]
for issue in tech_issues:
if "HTTPS" in issue:
critical_issues.append("Implement SSL certificate (HTTPS)")
elif "slow" in issue.lower():
recommendations.append("Improve page load speed")
# Social media recommendations
if not results["social_media"]["open_graph"]["has_essential"]:
recommendations.append("Add Open Graph tags for better social media sharing")
return recommendations, critical_issues, warnings
def _calculate_seo_score(self, results: Dict) -> int:
"""Calculate overall SEO score based on weighted factors"""
scores = []
# Title score (20% weight) - critical for rankings
scores.append(results["title_analysis"].get("score", 0) * 0.20)
# Meta description score (15% weight) - important for CTR
scores.append(results["meta_analysis"]["description"].get("score", 0) * 0.15)
# Headers score (15% weight) - content structure
scores.append(results["header_analysis"].get("score", 0) * 0.15)
# Content score (20% weight) - content quality
scores.append(results["content_analysis"].get("score", 0) * 0.20)
# Images score (10% weight) - accessibility and SEO
scores.append(results["image_analysis"].get("score", 0) * 0.10)
# Technical SEO score (20% weight) - technical factors
tech_score = 100
if results["technical_seo"]["issues"]:
tech_score -= len(results["technical_seo"]["issues"]) * 20
scores.append(max(0, tech_score) * 0.20)
return round(sum(scores))
# Initialize SEO checker instance
# This will be used by all MCP tools
seo_checker = SEOChecker()
# MCP TOOLS
# Tools are functions that MCP clients can call to perform actions
# They must be decorated with @mcp.tool() and should be async
@mcp.tool()
async def analyze_seo(url: str) -> str:
"""Comprehensive SEO analysis of a webpage
This MCP tool provides detailed SEO analysis with scoring and recommendations.
Returns a formatted string response for easy reading by MCP clients.
Args:
url: The webpage URL to analyze (e.g., 'example.com' or 'https://example.com')
Returns:
Detailed SEO analysis and recommendations
"""
result = await seo_checker.analyze_page_seo(url)
if result["status"] == "error":
return f"❌ SEO analysis failed for {url}\n\nErrors:\n" + "\n".join(result["errors"])
# SEO Score emoji and grade determination
score = result["seo_score"]
if score >= 90:
score_emoji = "🏆"
grade = "EXCELLENT"
elif score >= 80:
score_emoji = "🟢"
grade = "GOOD"
elif score >= 70:
score_emoji = "🟡"
grade = "FAIR"
elif score >= 60:
score_emoji = "🟠"
grade = "NEEDS WORK"
else:
score_emoji = "🔴"
grade = "POOR"
title = result["title_analysis"]
meta_desc = result["meta_analysis"]["description"]
headers = result["header_analysis"]
content = result["content_analysis"]
images = result["image_analysis"]
response = f"""{score_emoji} SEO Analysis for {result["domain"]}
🎯 OVERALL SEO SCORE: {score}/100 ({grade})
📄 TITLE TAG
• Content: "{title.get('content', 'MISSING')[:80]}{'...' if len(title.get('content', '')) > 80 else ''}"
• Length: {title.get('length', 0)} characters
• Status: {'✅ Good' if title.get('score', 0) >= 80 else '⚠️ Needs improvement'}
📝 META DESCRIPTION
• Content: "{meta_desc.get('content', 'MISSING')[:100]}{'...' if len(meta_desc.get('content', '')) > 100 else ''}"
• Length: {meta_desc.get('length', 0)} characters
• Status: {'✅ Good' if meta_desc.get('score', 0) >= 80 else '⚠️ Needs improvement'}
🏗️ HEADER STRUCTURE
• H1 Tags: {headers['structure']['h1']['count']} {'✅' if headers['structure']['h1']['count'] == 1 else '⚠️'}
• H2 Tags: {headers['structure']['h2']['count']}
• H3 Tags: {headers['structure']['h3']['count']}
📊 CONTENT ANALYSIS
• Word Count: {content['word_count']} words
• Text-to-HTML Ratio: {content['text_to_html_ratio']}%
• Status: {'✅ Good' if content['word_count'] >= 300 else '⚠️ Thin content'}
🖼️ IMAGE OPTIMIZATION
• Total Images: {images['total_images']}
• With Alt Text: {images['images_with_alt']} ({images.get('alt_percentage', 0)}%)
• Missing Alt Text: {images['images_without_alt']}
⚡ TECHNICAL SEO
• HTTPS: {'✅ Yes' if result['technical_seo']['https'] else '❌ No'}
• Load Time: {result['technical_seo']['load_time_ms']}ms
• Page Size: {result['technical_seo']['page_size_kb']} KB
• Schema Markup: {'✅ Yes' if result['technical_seo']['has_schema_markup'] else '❌ No'}
"""
# Critical issues section
if result["critical_issues"]:
response += f"\n🚨 CRITICAL ISSUES ({len(result['critical_issues'])})\n"
for issue in result["critical_issues"]:
response += f"• {issue}\n"
# Recommendations section
if result["recommendations"]:
response += f"\n💡 RECOMMENDATIONS ({len(result['recommendations'])})\n"
for rec in result["recommendations"][:5]: # Top 5 recommendations
response += f"• {rec}\n"
if len(result["recommendations"]) > 5:
response += f"• ... and {len(result['recommendations']) - 5} more recommendations\n"
return response
@mcp.tool()
async def seo_quick_check(url: str) -> str:
"""Quick SEO health check
This MCP tool provides a rapid SEO status overview.
Useful for quick assessments and monitoring.
Args:
url: The webpage URL to check
Returns:
Brief SEO status summary
"""
result = await seo_checker.analyze_page_seo(url)
if result["status"] == "error":
return f"❌ Cannot analyze {url}: {'; '.join(result['errors'])}"
score = result["seo_score"]
domain = result["domain"]
# Quick status indicators for key SEO factors
title_ok = result["title_analysis"].get("score", 0) >= 80
meta_ok = result["meta_analysis"]["description"].get("score", 0) >= 80
h1_ok = result["header_analysis"]["structure"]["h1"]["count"] == 1
content_ok = result["content_analysis"]["word_count"] >= 300
images_ok = result["image_analysis"]["images_without_alt"] == 0
https_ok = result["technical_seo"]["https"]
# Score emoji and status
if score >= 80:
score_emoji = "🟢"
status = "GOOD"
elif score >= 60:
score_emoji = "🟡"
status = "NEEDS WORK"
else:
score_emoji = "🔴"
status = "POOR"
return f"""{score_emoji} {domain} - SEO Health: {status} ({score}/100)
Quick Checks:
{'✅' if title_ok else '❌'} Title tag | {'✅' if meta_ok else '❌'} Meta description | {'✅' if h1_ok else '❌'} H1 structure
{'✅' if content_ok else '❌'} Content length | {'✅' if images_ok else '❌'} Image alt tags | {'✅' if https_ok else '❌'} HTTPS
{len(result['critical_issues'])} critical issues, {len(result['recommendations'])} recommendations"""
@mcp.tool()
async def seo_meta_tags_check(url: str) -> str:
"""Focused analysis of meta tags and social media optimization
This MCP tool provides detailed analysis of meta tags and social sharing.
Useful for optimizing search snippets and social media appearance.
Args:
url: The webpage URL to analyze
Returns:
Detailed meta tags and social media analysis
"""
result = await seo_checker.analyze_page_seo(url)
if result["status"] == "error":
return f"❌ Meta tags analysis failed for {url}\n\nErrors:\n" + "\n".join(result["errors"])
title = result["title_analysis"]
meta = result["meta_analysis"]
social = result["social_media"]
response = f"""🏷️ Meta Tags & Social Media Analysis for {result["domain"]}
📄 TITLE TAG
• Content: "{title.get('content', 'MISSING')}"
• Length: {title.get('length', 0)} characters (optimal: 30-60)
• Issues: {', '.join(title.get('issues', [])) or 'None'}
📝 META DESCRIPTION
• Content: "{meta['description'].get('content', 'MISSING')}"
• Length: {meta['description'].get('length', 0)} characters (optimal: 120-160)
• Issues: {', '.join(meta['description'].get('issues', [])) or 'None'}
🔍 META ROBOTS
• Status: {'Present' if meta['robots']['exists'] else 'Not set (default: index,follow)'}
"""
if meta['robots']['exists']:
response += f"• Directives: {', '.join(meta['robots']['directives'])}\n"
if meta['robots']['issues']:
response += f"• Issues: {', '.join(meta['robots']['issues'])}\n"
response += f"""
🔗 CANONICAL URL
• Status: {'Present' if meta['canonical']['exists'] else 'Missing'}
"""
if meta['canonical']['exists']:
response += f"• URL: {meta['canonical'].get('url', 'Empty')}\n"
if meta['canonical']['issues']:
response += f"• Issues: {', '.join(meta['canonical']['issues'])}\n"
# Social Media Tags Analysis
og = social['open_graph']
twitter = social['twitter_cards']
response += f"""
📱 SOCIAL MEDIA OPTIMIZATION
• Open Graph Score: {og['score']}/100
• Twitter Cards Score: {twitter['score']}/100
🌍 OPEN GRAPH TAGS ({len(og['tags'])} found)
"""
essential_og = ['title', 'description', 'image', 'url']
for tag in essential_og:
status = "✅" if tag in og['tags'] and og['tags'][tag] else "❌"
content = og['tags'].get(tag, 'Missing')[:60]
response += f"• og:{tag}: {status} {content}\n"
response += f"\n🐦 TWITTER CARD TAGS ({len(twitter['tags'])} found)\n"
essential_twitter = ['card', 'title', 'description']
for tag in essential_twitter:
status = "✅" if tag in twitter['tags'] and twitter['tags'][tag] else "❌"
content = twitter['tags'].get(tag, 'Missing')[:60]
response += f"• twitter:{tag}: {status} {content}\n"
return response
# MCP RESOURCES
# Resources are data that can be accessed by MCP clients using URIs
# They provide a way to expose structured data through the MCP protocol
@mcp.resource("seo://analyze/{url}")
async def seo_analysis_resource(url: str) -> str:
"""Get SEO analysis data as a resource
This MCP resource allows clients to access SEO data using a URI like:
seo://analyze/example.com
Resources return raw data (JSON) rather than formatted strings.
"""
# URL decode if needed for proper processing
url = url.replace('%2F', '/').replace('%3A', ':')
result = await seo_checker.analyze_page_seo(url)
return json.dumps(result, indent=2, default=str)
# MCP SERVER STARTUP
# This section configures and starts the MCP server for remote deployment
if __name__ == "__main__":
import os
# Get port from environment variable (used by hosting platforms like Hostinger)
port = int(os.environ.get("PORT", 8080))
# Start the MCP server with HTTP transport for remote access
# - transport="streamable-http": Uses HTTP for communication with MCP clients
# - host="0.0.0.0": Accepts connections from any IP (needed for remote deployment)
# - port: The port to listen on (from environment or default 8080)
# - log_level="debug": Enables detailed logging for development and troubleshooting
mcp.run(transport="streamable-http", host="0.0.0.0", port=port, log_level="debug")