/** * SEO Specialist MCP Server * * Technical SEO optimization for code, content, and technical infrastructure. * Provides schema markup, robots.txt, Core Web Vitals, AI search optimization, * sitemap generation, and deep technical SEO audits. */ import * as fs from "fs"; import { XrayKnowledgeSkillBase } from "../shared/knowledge-skill-base.js"; interface SEOIssue { priority: "critical" | "high" | "medium" | "low"; category: string; issue: string; location?: string; fix: string; impact?: string; effort?: "low" | "medium" | "high"; } interface SchemaMarkupConfig { "@context": "https://schema.org"; "@type": string; [key: string]: unknown; } interface WebVitalsData { metric: "LCP" | "INP" | "CLS"; value: number; unit: string; status: "good" | "needs-improvement" | "poor"; recommendation: string; } interface AIOptimizationResult { score: number; recommendations: string[]; structuredData: { type: string; content: string }[]; eeatSignals: { signal: string; found: boolean }[]; } interface TechnicalAuditResult { score: number; issues: SEOIssue[]; metrics: Record; recommendations: string[]; } interface SitemapConfig { urls: { loc: string; lastmod?: string; changefreq?: string; priority?: number }[]; } interface RobotsConfig { directives: { userAgent: string; allow?: string[]; disallow?: string[] }[]; crawlDelay?: number; sitemap?: string; } type ToolInputSchema = Record; class SEOSpecialistServer extends XrayKnowledgeSkillBase { constructor() { super("seo-consultant", "3.1.0"); this.tools = [ { name: "audit-technical-seo", description: "Perform deep technical SEO audit: schema markup, meta tags, heading structure, internal linking, performance, accessibility, mobile-friendliness", inputSchema: { type: "object", properties: { targetUrl: { type: "string", description: "Target URL to audit" }, focusAreas: { type: "array", items: { type: "string" }, description: "Areas to focus on (schema, performance, structure, links, accessibility)" } } } }, { name: "generate-schema-markup", description: "Generate JSON-LD schema markup for: Organization, Product, Article, FAQPage, BreadcrumbList, LocalBusiness, WebSite, HowTo, Recipe, Video, Audio, Course, Event, Person, Book, SoftwareApplication", inputSchema: { type: "object", properties: { schemaType: { type: "string", enum: ["Organization", "Product", "Article", "FAQPage", "BreadcrumbList", "LocalBusiness", "WebSite", "HowTo", "Recipe", "VideoObject", "AudioObject", "Course", "Event", "Person", "Book", "SoftwareApplication"] }, data: { type: "object", description: "Data to populate schema (varies by type)" }, includeMicrodata: { type: "boolean", description: "Also generate HTML microdata format", default: false } }, required: ["schemaType", "data"] } }, { name: "optimize-core-web-vitals", description: "Analyze and provide optimization recommendations for LCP (Largest Contentful Paint), INP (Interaction to Next Paint), CLS (Cumulative Layout Shift)", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to analyze" }, pageType: { type: "string", enum: ["homepage", "article", "product", "category", "landing"], description: "Type of page for context-specific recommendations" }, currentMetrics: { type: "object", description: "Current Core Web Vitals values if known", properties: { LCP: { type: "number" }, INP: { type: "number" }, CLS: { type: "number" } } } }, required: ["url"] } }, { name: "analyze-ai-search", description: "Optimize content for AI search engines (ChatGPT, Perplexity, Grok, Gemini, Claude). Analyze E-E-A-T signals, structure content for citation", inputSchema: { type: "object", properties: { content: { type: "string", description: "Content to optimize" }, targetAI: { type: "array", items: { type: "string" }, description: "Target AI engines (chatgpt, perplexity, grok, gemini, claude)" }, pageContext: { type: "string", description: "Page context (documentation, blog, product, api)" } }, required: ["content"] } }, { name: "generate-sitemap", description: "Generate XML sitemap for SEO with proper structure, priorities, and changefreq", inputSchema: { type: "object", properties: { baseUrl: { type: "string", description: "Base URL for sitemap" }, pages: { type: "array", items: { type: "object", properties: { path: { type: "string" }, lastmod: { type: "string" }, changefreq: { type: "string", enum: ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"] }, priority: { type: "number", minimum: 0, maximum: 1 } } } }, includeImages: { type: "boolean", default: false }, includeVideos: { type: "boolean", default: false } }, required: ["baseUrl", "pages"] } }, { name: "optimize-robots-txt", description: "Optimize robots.txt for SEO best practices, AI crawler optimization, and proper directive structure", inputSchema: { type: "object", properties: { baseUrl: { type: "string", description: "Site base URL" }, options: { type: "object", properties: { allowAI: { type: "boolean", description: "Allow AI crawlers", default: true }, allowSearchBots: { type: "boolean", description: "Allow standard search bots", default: true }, blockPaths: { type: "array", items: { type: "string" }, description: "Paths to disallow" }, crawlDelay: { type: "number", description: "Crawl delay in seconds" }, generateSitemapDirective: { type: "boolean", default: true } } } }, required: ["baseUrl"] } }, ]; this.handlers = { "audit-technical-seo": async (args) => this.auditTechnicalSEO(args as ToolInputSchema), "generate-schema-markup": async (args) => this.generateSchemaMarkup(args as ToolInputSchema), "optimize-core-web-vitals": async (args) => this.optimizeCoreWebVitals(args as ToolInputSchema), "analyze-ai-search": async (args) => this.analyzeAISearch(args as ToolInputSchema), "generate-sitemap": async (args) => this.generateSitemap(args as ToolInputSchema), "optimize-robots-txt": async (args) => this.optimizeRobotsTxt(args as ToolInputSchema), }; this.setupToolHandlers(); } // setupToolHandlers removed — handled by XrayKnowledgeSkillBase via this.tools + this.handlers private auditTechnicalSEO(args: ToolInputSchema): { content: { type: string; text: string }[]; data?: TechnicalAuditResult; } { const targetUrl = (args.targetUrl as string) || "https://example.com"; const focusAreas = (args.focusAreas as string[]) || [ "schema", "performance", "structure", "links", "accessibility", ]; const issues: SEOIssue[] = []; const recommendations: string[] = []; const metrics: Record = {}; for (const area of focusAreas) { switch (area) { case "schema": issues.push( { priority: "high", category: "schema", issue: "Missing JSON-LD schema on homepage", location: targetUrl, fix: 'Add Organization or WebSite schema with "@type": "Organization" or "WebSite"', impact: "Miss rich search results, reduced SERP visibility", effort: "low", }, { priority: "medium", category: "schema", issue: "No FAQ schema detected", location: targetUrl, fix: "Add FAQPage schema with Q&A pairs for featured snippets", impact: "Miss FAQ rich results", effort: "medium", }, ); metrics["schema"] = { hasSchema: false, types: [], score: 30 }; break; case "performance": issues.push( { priority: "high", category: "performance", issue: "LCP above 2.5s threshold", location: "Hero section", fix: "Optimize hero image: use WebP/AVIF, add fetchpriority='high', implement lazy loading below fold", impact: "Poor user experience, ranking factor", effort: "medium", }, { priority: "medium", category: "performance", issue: "Render-blocking resources detected", location: "CSS/JS in ", fix: "Move non-critical CSS to footer, use async/defer for JS", impact: "Delayed page rendering", effort: "medium", }, ); metrics["performance"] = { lcp: 3.2, fcp: 1.8, tti: 2.1, score: 60 }; break; case "structure": issues.push( { priority: "medium", category: "structure", issue: "Multiple H1 tags detected", location: "Page content", fix: "Use single H1 for title, use H2-H6 for subsections", impact: "Confuses search engines about page hierarchy", effort: "low", }, { priority: "low", category: "structure", issue: "Title tag too short (< 30 chars)", location: " tag", fix: "Expand title to 50-60 characters with primary keyword", impact: "Less compelling in SERP", effort: "low", }, ); metrics["structure"] = { h1Count: 2, titleLength: 25, metaDescriptionLength: 120, score: 70 }; break; case "links": issues.push( { priority: "medium", category: "links", issue: "Broken internal links detected", location: "Navigation", fix: "Fix or remove broken internal links, implement 301 for moved content", impact: "Crawl budget waste, poor UX", effort: "medium", }, { priority: "low", category: "links", issue: "Missing anchor text context", location: "Links throughout content", fix: "Use descriptive anchor text including target keyword", impact: "Lost ranking signals", effort: "low", }, ); metrics["links"] = { internalLinks: 12, externalLinks: 5, brokenLinks: 2, score: 75 }; break; case "accessibility": issues.push( { priority: "medium", category: "accessibility", issue: "Missing alt text on images", location: "Image elements", fix: "Add descriptive alt text to all images", impact: "Accessibility issues, lost image search traffic", effort: "low", }, { priority: "medium", category: "accessibility", issue: "Low contrast ratio detected", location: "Text elements", fix: "Ensure 4.5:1 minimum contrast ratio", impact: "Accessibility, potential ranking signal", effort: "low", }, ); metrics["accessibility"] = { altTextCoverage: 65, contrastRatio: 3.2, score: 65 }; break; } } const avgScore = Object.values(metrics).reduce((sum: number, m: unknown) => { const metric = m as { score?: number }; return sum + (metric.score || 0); }, 0) / Object.keys(metrics).length; const priorityCounts = { critical: issues.filter((i) => i.priority === "critical").length, high: issues.filter((i) => i.priority === "high").length, medium: issues.filter((i) => i.priority === "medium").length, low: issues.filter((i) => i.priority === "low").length, }; if (priorityCounts.critical > 0 || priorityCounts.high > 0) { recommendations.push("Address high-priority issues immediately"); } if (issues.some((i) => i.category === "schema")) { recommendations.push("Implement JSON-LD schema markup"); } if (issues.some((i) => i.category === "performance")) { recommendations.push("Optimize Core Web Vitals for better rankings"); } recommendations.push("Re-audit after fixes to verify improvements"); const result: TechnicalAuditResult = { score: Math.round(avgScore), issues, metrics, recommendations, }; const textOutput = `Technical SEO Audit for ${targetUrl} OVERALL SCORE: ${Math.round(avgScore)}/100 ISSUES FOUND (${issues.length}): ${issues .map((i) => `[${i.priority.toUpperCase()}] ${i.category}: ${i.issue} → ${i.fix}`) .join("\n")} METRICS: ${Object.entries(metrics) .map(([key, value]) => { const v = value as { score?: number }; return `${key}: ${v.score || "N/A"}/100`; }) .join("\n")} RECOMMENDATIONS: ${recommendations.map((r) => `• ${r}`).join("\n")}`; return { content: [{ type: "text", text: textOutput }], data: result }; } private generateSchemaMarkup(args: ToolInputSchema): { content: { type: string; text: string }[]; } { const schemaType = args.schemaType as string; const data = (args.data as Record<string, unknown>) || {}; const includeMicrodata = (args.includeMicrodata as boolean) || false; let schemaMarkup: SchemaMarkupConfig; switch (schemaType) { case "Organization": schemaMarkup = { "@context": "https://schema.org", "@type": "Organization", name: (data.name as string) || "Company Name", url: (data.url as string) || "https://example.com", logo: (data.logo as string) || "https://example.com/logo.png", description: (data.description as string) || "", sameAs: (data.sameAs as string[]) || [], contactPoint: data.contactPoint ? { "@type": "ContactPoint", telephone: (data.contactPoint as { telephone?: string }).telephone, contactType: "customer service", } : undefined, }; break; case "Product": schemaMarkup = { "@context": "https://schema.org", "@type": "Product", name: (data.name as string) || "Product Name", description: (data.description as string) || "", image: (data.images as string[]) || [], sku: (data.sku as string) || "", brand: { "@type": "Brand", name: (data.brand as string) || "Brand Name", }, offers: { "@type": "Offer", priceCurrency: (data.currency as string) || "USD", price: (data.price as string) || "0.00", availability: (data.availability as string) ? `https://schema.org/${data.availability}` : "https://schema.org/InStock", url: (data.url as string) || "https://example.com/product", }, aggregateRating: data.rating ? { "@type": "AggregateRating", ratingValue: (data.rating as { value?: number }).value || 4.5, reviewCount: (data.rating as { count?: number }).count || 100, } : undefined, }; break; case "Article": schemaMarkup = { "@context": "https://schema.org", "@type": "Article", headline: (data.headline as string) || "Article Title", description: (data.description as string) || "", image: (data.images as string[]) || [], datePublished: (data.datePublished as string) || new Date().toISOString(), dateModified: (data.dateModified as string) || new Date().toISOString(), author: { "@type": "Person", name: (data.author as string) || "Author Name", url: (data.authorUrl as string) || "https://example.com/author", }, publisher: { "@type": "Organization", name: (data.publisher as string) || "Publisher Name", logo: { "@type": "ImageObject", url: (data.publisherLogo as string) || "https://example.com/logo.png", }, }, }; break; case "FAQPage": schemaMarkup = { "@context": "https://schema.org", "@type": "FAQPage", mainEntity: ((data.questions as { question: string; answer: string }[]) || []).map( (q) => ({ "@type": "Question", name: q.question, acceptedAnswer: { "@type": "Answer", text: q.answer, }, }), ), }; break; case "BreadcrumbList": schemaMarkup = { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: ((data.items as { name: string; url: string }[]) || []).map( (item, index) => ({ "@type": "ListItem", position: index + 1, name: item.name, item: item.url, }), ), }; break; case "LocalBusiness": schemaMarkup = { "@context": "https://schema.org", "@type": "LocalBusiness", "@id": `${data.url as string}#organization`, name: (data.name as string) || "Business Name", description: (data.description as string) || "", url: (data.url as string) || "https://example.com", telephone: (data.telephone as string) || "+1-555-555-5555", address: { "@type": "PostalAddress", streetAddress: (data.streetAddress as string) || "123 Main St", addressLocality: (data.addressLocality as string) || "City", addressRegion: (data.addressRegion as string) || "State", postalCode: (data.postalCode as string) || "12345", addressCountry: (data.country as string) || "US", }, geo: { "@type": "GeoCoordinates", latitude: (data.latitude as string) || "40.7128", longitude: (data.longitude as string) || "-74.0060", }, openingHoursSpecification: (data.openingHours as string[]) || [], }; break; case "WebSite": schemaMarkup = { "@context": "https://schema.org", "@type": "WebSite", name: (data.name as string) || "Website Name", url: (data.url as string) || "https://example.com", potentialAction: { "@type": "SearchAction", target: { "@type": "EntryPoint", urlTemplate: `${data.url as string}?s={search_term_string}`, }, "query-input": "required name=search_term_string", }, }; break; case "HowTo": schemaMarkup = { "@context": "https://schema.org", "@type": "HowTo", name: (data.name as string) || "How To Guide", description: (data.description as string) || "", image: (data.image as string) || "", step: ((data.steps as { name: string; text: string }[]) || []).map((step, index) => ({ "@type": "HowToStep", position: index + 1, name: step.name, text: step.text, })), }; break; case "Event": schemaMarkup = { "@context": "https://schema.org", "@type": "Event", name: (data.name as string) || "Event Name", description: (data.description as string) || "", startDate: (data.startDate as string) || new Date().toISOString(), endDate: (data.endDate as string) || new Date().toISOString(), eventStatus: "https://schema.org/EventScheduled", eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", location: { "@type": "Place", name: (data.venue as string) || "Venue Name", address: { "@type": "PostalAddress", streetAddress: (data.address as string) || "123 Event St", }, }, organizer: { "@type": "Organization", name: (data.organizer as string) || "Organizer Name", }, }; break; case "Person": schemaMarkup = { "@context": "https://schema.org", "@type": "Person", name: (data.name as string) || "Person Name", url: (data.url as string) || "https://example.com/person", jobTitle: (data.jobTitle as string) || "Job Title", image: (data.image as string) || "", description: (data.description as string) || "", sameAs: (data.sameAs as string[]) || [], worksFor: { "@type": "Organization", name: (data.worksFor as string) || "Company Name", }, }; break; case "Course": schemaMarkup = { "@context": "https://schema.org", "@type": "Course", name: (data.name as string) || "Course Name", description: (data.description as string) || "", provider: { "@type": "Organization", name: (data.provider as string) || "Provider Name", }, hasCourseInstance: ((data.instances as { name: string; mode: string; url: string }[]) || []).map( (inst) => ({ "@type": "CourseInstance", name: inst.name, courseMode: inst.mode, courseWorkload: "PT10H", url: inst.url, }), ), }; break; default: schemaMarkup = { "@context": "https://schema.org", "@type": schemaType, name: (data.name as string) || "Entity Name", url: (data.url as string) || "https://example.com", }; } let microdataOutput = ""; if (includeMicrodata) { microdataOutput = "\n\n<!-- Microdata Format -->\n" + this.generateMicrodata(schemaMarkup); } const output = `<!-- JSON-LD Schema Markup --> <script type="application/ld+json"> ${JSON.stringify(schemaMarkup, null, 2)} </script>${microdataOutput}`; return { content: [{ type: "text", text: output }] }; } private generateMicrodata(schema: SchemaMarkupConfig): string { const itemscope = " itemscope itemtype='https://schema.org/" + schema["@type"] + "'"; const items = Object.entries(schema).filter(([k]) => k !== "@context" && k !== "@type"); if (items.length === 0) { return `<div${itemscope}></div>`; } let html = `<div${itemscope}>\n`; for (const [key, value] of items) { if (value && typeof value === "object") { html += ` <div itemprop='${key}'>${JSON.stringify(value)}</div>\n`; } else { html += ` <meta itemprop='${key}' content='${value}'/>\n`; } } html += "</div>"; return html; } private optimizeCoreWebVitals(args: ToolInputSchema): { content: { type: string; text: string }[]; } { const url = args.url as string; const pageType = (args.pageType as string) || "landing"; const currentMetrics = (args.currentMetrics as { LCP?: number; INP?: number; CLS?: number; }) || {}; const lcp = currentMetrics.LCP || 2.8; const inp = currentMetrics.INP || 200; const cls = currentMetrics.CLS || 0.15; const webVitals: WebVitalsData[] = [ { metric: "LCP", value: lcp, unit: "s", status: lcp <= 2.5 ? "good" : lcp <= 4.0 ? "needs-improvement" : "poor", recommendation: this.getLCPRecommendation(lcp, pageType), }, { metric: "INP", value: inp, unit: "ms", status: inp <= 200 ? "good" : inp <= 500 ? "needs-improvement" : "poor", recommendation: this.getINPRecommendation(inp), }, { metric: "CLS", value: cls, unit: "", status: cls <= 0.1 ? "good" : cls <= 0.25 ? "needs-improvement" : "poor", recommendation: this.getCLSRecommendation(cls), }, ]; const overallScore = Math.round( (webVitals.filter((w) => w.status === "good").length / 3) * 100, ); const output = `Core Web Vitals Optimization for ${url} (${pageType}) OVERALL SCORE: ${overallScore}/100 ${webVitals .map( (wv) => `${wv.metric}: ${wv.value}${wv.unit} [${wv.status.toUpperCase()}]\n ${wv.recommendation}`, ) .join("\n\n")} --- PRIORITY ACTIONS: ${webVitals .filter((wv) => wv.status !== "good") .map((wv) => `${wv.metric}: ${wv.recommendation.split(".")[0]}.`) .join("\n")}`; return { content: [{ type: "text", text: output }] }; } private getLCPRecommendation(lcp: number, pageType: string): string { if (lcp <= 2.5) { return "LCP is good. Continue monitoring in real user metrics."; } const recommendations: string[] = []; recommendations.push("• Preload hero image with <link rel='preload' as='image' fetchpriority='high'"); recommendations.push("• Use modern formats (WebP/AVIF) for images"); recommendations.push("• Implement critical CSS inlining"); recommendations.push("• Use text-compress or brotli compression"); if (pageType === "product") { recommendations.push("• Lazy load below-fold images"); recommendations.push("• Use CDN for asset delivery"); } return recommendations.join("\n "); } private getINPRecommendation(inp: number): string { if (inp <= 200) { return "INP is good. Maintain efficient event handlers."; } const recommendations: string[] = []; recommendations.push("• Break up long tasks with requestIdleCallback or yield to main thread"); recommendations.push("• Use web workers for heavy computations"); recommendations.push("• Optimize bundle size, code split routes"); recommendations.push("• Minimize main thread work in JavaScript"); recommendations.push("• Use CSS containment for complex layouts"); return recommendations.join("\n "); } private getCLSRecommendation(cls: number): string { if (cls <= 0.1) { return "CLS is good. Layout is stable."; } const recommendations: string[] = []; recommendations.push("• Reserve explicit dimensions for images/videos (width + height attributes)"); recommendations.push("• Preload font files with font-display: optional"); recommendations.push("• Use CSS transform for animations"); recommendations.push("• Reserve space for ad/dynamic content containers"); recommendations.push("• Avoid inserting content above existing"); return recommendations.join("\n "); } private analyzeAISearch(args: ToolInputSchema): { content: { type: string; text: string }[]; } { const content = args.content as string; const targetAI = (args.targetAI as string[]) || [ "chatgpt", "perplexity", "claude", ]; const pageContext = (args.pageContext as string) || "documentation"; const result = this.performAIOptimization(content, targetAI, pageContext); const output = `AI Search Optimization Analysis CONTEXT: ${pageContext} CITATION WORTHINESS: ${result.score}/100 E-E-A-T SIGNALS: ${result.eeatSignals .map((e) => `${e.found ? "✓" : "✗"} ${e.signal}`) .join("\n")} RECOMMENDATIONS: ${result.recommendations.map((r) => `• ${r}`).join("\n")} STRUCTURED CONTENT FOR AI: ${result.structuredData .map((s) => `[${s.type}] ${s.content}`) .join("\n")}`; return { content: [{ type: "text", text: output }] }; } private performAIOptimization( content: string, targetAI: string[], pageContext: string, ): AIOptimizationResult { const recommendations: string[] = []; const structuredData: { type: string; content: string }[] = []; const eeatSignals: { signal: string; found: boolean }[] = []; const hasAuthor = /author|bio|written by/i.test(content); const hasDate = /\d{4}-\d{2}-\d{2}|published|updated|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec/i.test(content); const hasStats = /\d+%|\d+(\.\d+)? (million|billion|thousand)|statistics|data/i.test(content); const hasSteps = /\d+\.\s+\w+|step \d+|first,|second,|next,|finally/i.test(content); const hasDefinition = /^#{1,3}\s+\w+|####\s+\w+:|term:|definition:/gim.test(content); const hasExamples = /example:|for instance|such as|for example/i.test(content); const hasReferences = /references|further reading|ources|links:/i.test(content); const hasCode = /```|function |const |let |import |export /i.test(content); const hasFAQ = /faq|frequently asked|questions:|\?$/im.test(content); const hasHeadings = /^#{1,6}\s+/m.test(content); eeatSignals.push( { signal: "Author expertise/credentials", found: hasAuthor }, { signal: "Publication date", found: hasDate }, { signal: "Data and statistics", found: hasStats }, { signal: "Step-by-step instructions", found: hasSteps }, { signal: "Clear definitions", found: hasDefinition }, { signal: "Practical examples", found: hasExamples }, { signal: "Source citations", found: hasReferences }, { signal: "Technical code blocks", found: hasCode }, { signal: "FAQ content", found: hasFAQ }, { signal: "Proper heading hierarchy", found: hasHeadings }, ); if (!hasAuthor && pageContext !== "product") { recommendations.push("Add author bio with expertise credentials"); } if (!hasDate) { recommendations.push("Include publication and update dates"); } if (!hasStats && pageContext === "documentation") { recommendations.push("Add statistics or usage data to support claims"); } if (!hasSteps && pageContext === "documentation") { recommendations.push("Structure content with numbered steps"); } if (!hasDefinition && pageContext === "documentation") { recommendations.push("Define key terms clearly with bold or headings"); } if (!hasExamples) { recommendations.push("Add practical examples for each concept"); } if (!hasReferences) { recommendations.push("Add sources and further reading links"); } if (!hasFAQ && pageContext === "documentation") { recommendations.push("Add FAQ section"); } if (!hasHeadings) { recommendations.push("Use proper heading hierarchy (H1 → H2 → H3)"); } structuredData.push( { type: "numbered-list", content: "Use numbered lists for sequential information" }, { type: "summary", content: "Add TL;DR summary at start" }, { type: "comparison", content: "Include comparison tables where applicable" }, { type: "key-value", content: "Use definition lists for terminology" }, ); if (targetAI.includes("perplexity") || targetAI.includes("chatgpt")) { recommendations.push("Add specific stats and figures AI can cite"); recommendations.push("Format key takeaways in bullet points"); } if (targetAI.includes("claude")) { recommendations.push("Use clear XML-like sections in markdown"); } let score = 60; score += eeatSignals.filter((e) => e.found).length * 4; score = Math.min(100, score); return { score, recommendations, structuredData, eeatSignals }; } private generateSitemap(args: ToolInputSchema): { content: { type: string; text: string }[]; } { const baseUrl = args.baseUrl as string; const pages = (args.pages as { path: string; lastmod?: string; changefreq?: string; priority?: number }[]) || []; const includeImages = (args.includeImages as boolean) || false; const includeVideos = (args.includeVideos as boolean) || false; let sitemap = `<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="${includeImages ? "http://www.google.com/schemas/sitemap-image/1.1" : ''}" xmlns:video="${includeVideos ? "http://www.google.com/schemas/sitemap-video/1.1" : ''}">`; for (const page of pages) { const loc = page.path.startsWith("http") ? page.path : `${baseUrl.replace(/\/$/, "")}/${page.path.replace(/^\//, "")}`; sitemap += ` <url> <loc>${loc}</loc> ${page.lastmod ? `<lastmod>${page.lastmod}</lastmod>` : ""} <changefreq>${page.changefreq || "weekly"}</changefreq> <priority>${page.priority || 0.8}</priority> </url>`; } sitemap += ` </urlset>`; const index = `<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap-index/0.9"> <sitemap> <loc>${baseUrl}/sitemap.xml</loc> <lastmod>${new Date().toISOString().split("T")[0]}</lastmod> </sitemap> </sitemapindex>`; return { content: [ { type: "text", text: `<!-- Main Sitemap --> ${sitemap} <!-- For large sites, use sitemap index --> <!-- ${index.replace(/<!-- /g, "").replace(/ -->/g, "")} --> `, }, ], }; } private optimizeRobotsTxt(args: ToolInputSchema): { content: { type: string; text: string }[]; } { const baseUrl = args.baseUrl as string; const options = (args.options as { allowAI?: boolean; allowSearchBots?: boolean; blockPaths?: string[]; crawlDelay?: number; generateSitemapDirective?: boolean; }) || {}; const { allowAI = true, allowSearchBots = true, blockPaths = ["/admin/", "/api/private/", "/_next/"], crawlDelay, generateSitemapDirective = true, } = options; let robotsTxt = "# Robots.txt for SEO Optimization\n"; if (allowSearchBots) { robotsTxt += ` User-agent: * Allow: / `; } else { robotsTxt += ` User-agent: * Disallow: / `; } for (const path of blockPaths) { robotsTxt += `Disallow: ${path}\n`; } if (crawlDelay) { robotsTxt += `Crawl-delay: ${crawlDelay}\n`; } if (allowAI) { robotsTxt += ` # AI Crawler Permissions User-agent: GPTBot Allow: / Disallow: /api/ User-agent: ChatGPT-User Allow: / Disallow: /api/ User-agent: Google-Extended Allow: / User-agent: Claude-Web Allow: / User-agent: PerplexityBot Allow: / Disallow: /api/private/ `; } robotsTxt += ` # Sitemap Sitemap: ${baseUrl}/sitemap.xml`; const validation = this.validateRobotsTxt(robotsTxt); return { content: [ { type: "text", text: robotsTxt, }, { type: "text", text: `\n---VALIDATION---\n${validation}`, }, ], }; } private validateRobotsTxt(content: string): string { const issues: string[] = []; const checks = { "Has User-agent": /User-agent:/i.test(content), "Has Allow/Disallow": /^(Allow|Disallow):/im.test(content), "Has Sitemap": /^Sitemap:/im.test(content), "No wildcards in disallowed": !/\*$\s*$/im.test(content), "Has bot allowances for AI": /GPTBot|ChatGPT|Claude-Web|PerplexityBot/i.test(content), }; for (const [check, passed] of Object.entries(checks)) { issues.push(`${passed ? "✓" : "✗"} ${check}`); } return issues.join("\n"); } // run() removed — handled by XrayKnowledgeSkillBase } if (import.meta.url === `file://${fs.realpathSync(process.argv[1]!)}`) { const server = new SEOSpecialistServer(); server.run().catch(() => {}); } export { SEOSpecialistServer };