@rankcli/mcp-server 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ANALYSIS_TOOLS,
4
+ handleAnalysisTool
5
+ } from "./chunk-FJ2K7KRV.js";
2
6
 
3
7
  // src/index.ts
4
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -8,280 +12,9 @@ import {
8
12
  ListToolsRequestSchema
9
13
  } from "@modelcontextprotocol/sdk/types.js";
10
14
  import Conf from "conf";
11
- import { analyzers } from "@rankcli/agent-runtime";
12
15
  var SUPABASE_URL = process.env.RANKCLI_SUPABASE_URL || "https://bspljbxwbjiqueeyzyat.supabase.co";
13
16
  var localConfig = new Conf({ projectName: "rankcli" });
14
- async function fetchHtml(url) {
15
- const res = await fetch(url, {
16
- headers: { "User-Agent": "RankCLI/1.0 (+https://rankcli.dev)" }
17
- });
18
- if (!res.ok) {
19
- throw new Error(`Fetching ${url} returned ${res.status} ${res.statusText}`);
20
- }
21
- return res.text();
22
- }
23
- var TOOLS = [
24
- {
25
- name: "seo_analyze",
26
- description: `Run comprehensive SEO analysis on a webpage. Returns scores and issues for:
27
- - GEO (AI Search Optimization) - Is the site visible to ChatGPT, Perplexity, Claude?
28
- - Core Web Vitals (LCP, CLS, INP) - Performance estimates
29
- - Structured Data - Schema.org validation
30
- - Security Headers - HTTPS, HSTS, CSP
31
- - Mobile SEO - Responsive design, touch targets
32
- - Images - Alt text, formats, dimensions
33
- - Internal Linking - Anchor text, orphan detection
34
-
35
- Use this for a complete SEO audit.`,
36
- inputSchema: {
37
- type: "object",
38
- properties: {
39
- url: {
40
- type: "string",
41
- description: "URL of the page to analyze"
42
- },
43
- html: {
44
- type: "string",
45
- description: "HTML content of the page (optional if URL is provided)"
46
- },
47
- robotsTxt: {
48
- type: "string",
49
- description: "robots.txt content for AI crawler analysis (optional)"
50
- }
51
- },
52
- required: ["url"]
53
- }
54
- },
55
- {
56
- name: "seo_geo_check",
57
- description: `Check if a website is optimized for AI search engines (GEO - Generative Engine Optimization).
58
-
59
- Analyzes:
60
- - AI crawler access (GPTBot, ClaudeBot, PerplexityBot, etc.)
61
- - robots.txt rules for AI crawlers
62
- - JS rendering requirements (can AI crawlers see content?)
63
- - Content structure for LLM consumption
64
- - Citation readiness (trust signals)
65
- - FAQ/entity extraction capability
66
-
67
- Critical for visibility in ChatGPT, Perplexity, Claude, and Gemini responses.`,
68
- inputSchema: {
69
- type: "object",
70
- properties: {
71
- url: {
72
- type: "string",
73
- description: "URL of the page to analyze"
74
- },
75
- html: {
76
- type: "string",
77
- description: "HTML content of the page"
78
- },
79
- robotsTxt: {
80
- type: "string",
81
- description: "robots.txt content"
82
- }
83
- },
84
- required: ["url"]
85
- }
86
- },
87
- {
88
- name: "seo_robots_ai",
89
- description: `Analyze robots.txt for AI crawler permissions. Shows which AI crawlers (GPTBot, ClaudeBot, PerplexityBot, etc.) are allowed or blocked.`,
90
- inputSchema: {
91
- type: "object",
92
- properties: {
93
- robotsTxt: {
94
- type: "string",
95
- description: "Content of robots.txt file"
96
- }
97
- },
98
- required: ["robotsTxt"]
99
- }
100
- },
101
- {
102
- name: "seo_generate_robots",
103
- description: `Generate an AI-friendly robots.txt that allows all major AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.)`,
104
- inputSchema: {
105
- type: "object",
106
- properties: {
107
- siteUrl: {
108
- type: "string",
109
- description: "Base URL of the site (e.g., https://example.com)"
110
- }
111
- },
112
- required: ["siteUrl"]
113
- }
114
- },
115
- {
116
- name: "seo_core_web_vitals",
117
- description: `Estimate Core Web Vitals (LCP, CLS, INP, TTFB) from HTML analysis. Identifies issues like:
118
- - Render-blocking resources
119
- - Images without dimensions
120
- - Large JavaScript bundles
121
- - Missing preload hints`,
122
- inputSchema: {
123
- type: "object",
124
- properties: {
125
- url: {
126
- type: "string",
127
- description: "URL of the page"
128
- },
129
- html: {
130
- type: "string",
131
- description: "HTML content of the page"
132
- }
133
- },
134
- required: ["url", "html"]
135
- }
136
- },
137
- {
138
- name: "seo_structured_data",
139
- description: `Validate JSON-LD structured data (Schema.org). Checks for:
140
- - Required properties per schema type
141
- - Article, Product, FAQ, HowTo, LocalBusiness schemas
142
- - Rich result eligibility
143
- - Common mistakes`,
144
- inputSchema: {
145
- type: "object",
146
- properties: {
147
- url: {
148
- type: "string",
149
- description: "URL of the page"
150
- },
151
- html: {
152
- type: "string",
153
- description: "HTML content with JSON-LD"
154
- }
155
- },
156
- required: ["url", "html"]
157
- }
158
- },
159
- {
160
- name: "seo_generate_schema",
161
- description: `Generate JSON-LD structured data template for a page type (article, product, faq, local-business, website).`,
162
- inputSchema: {
163
- type: "object",
164
- properties: {
165
- pageType: {
166
- type: "string",
167
- enum: ["article", "product", "faq", "local-business", "website"],
168
- description: "Type of page"
169
- },
170
- siteName: {
171
- type: "string",
172
- description: "Name of the website"
173
- },
174
- siteUrl: {
175
- type: "string",
176
- description: "Base URL of the website"
177
- },
178
- authorName: {
179
- type: "string",
180
- description: "Default author name (for articles)"
181
- },
182
- organizationName: {
183
- type: "string",
184
- description: "Organization name"
185
- }
186
- },
187
- required: ["pageType", "siteName", "siteUrl"]
188
- }
189
- },
190
- {
191
- name: "seo_security_headers",
192
- description: `Analyze security headers (HTTPS, HSTS, CSP, X-Frame-Options, etc.). Returns a security grade A+ through F.`,
193
- inputSchema: {
194
- type: "object",
195
- properties: {
196
- url: {
197
- type: "string",
198
- description: "URL of the page"
199
- },
200
- headers: {
201
- type: "object",
202
- description: "HTTP response headers",
203
- additionalProperties: { type: "string" }
204
- }
205
- },
206
- required: ["url", "headers"]
207
- }
208
- },
209
- {
210
- name: "seo_generate_security_headers",
211
- description: `Generate recommended security headers configuration for a site.`,
212
- inputSchema: {
213
- type: "object",
214
- properties: {
215
- siteUrl: {
216
- type: "string",
217
- description: "Base URL of the site"
218
- }
219
- },
220
- required: ["siteUrl"]
221
- }
222
- },
223
- {
224
- name: "seo_images",
225
- description: `Analyze images for SEO and performance. Checks alt text, dimensions, formats (WebP/AVIF), lazy loading, and responsive images.`,
226
- inputSchema: {
227
- type: "object",
228
- properties: {
229
- url: {
230
- type: "string",
231
- description: "URL of the page"
232
- },
233
- html: {
234
- type: "string",
235
- description: "HTML content of the page"
236
- }
237
- },
238
- required: ["url", "html"]
239
- }
240
- },
241
- {
242
- name: "seo_internal_links",
243
- description: `Analyze internal linking structure. Checks anchor text quality, orphan page risk, link distribution, and suggests linking opportunities.`,
244
- inputSchema: {
245
- type: "object",
246
- properties: {
247
- url: {
248
- type: "string",
249
- description: "URL of the page"
250
- },
251
- html: {
252
- type: "string",
253
- description: "HTML content of the page"
254
- }
255
- },
256
- required: ["url", "html"]
257
- }
258
- },
259
- {
260
- name: "seo_mobile",
261
- description: `Analyze mobile SEO. Checks viewport, touch targets, font sizes, content width, PWA readiness.`,
262
- inputSchema: {
263
- type: "object",
264
- properties: {
265
- url: {
266
- type: "string",
267
- description: "URL of the page"
268
- },
269
- html: {
270
- type: "string",
271
- description: "HTML content of the page"
272
- }
273
- },
274
- required: ["url", "html"]
275
- }
276
- },
277
- {
278
- name: "seo_ai_crawlers",
279
- description: `Get information about all known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.) including their user agents and purposes.`,
280
- inputSchema: {
281
- type: "object",
282
- properties: {}
283
- }
284
- },
17
+ var ACCOUNT_TOOLS = [
285
18
  {
286
19
  name: "rankcli_connect",
287
20
  description: `Link this AI assistant to a RankCLI account (rankcli.dev). This is the only tool in this server that contacts rankcli.dev, and only when you call it - every other tool runs fully locally. Connecting unlocks GitHub auto-fix PRs, scheduled monitoring, and a dashboard with history across audits; it does not change how the other tools work. Opens a browser tab for the user to approve (or reuses an existing session if already logged in). Safe to call anytime - if already connected, it just reports who's connected.`,
@@ -299,6 +32,7 @@ Critical for visibility in ChatGPT, Perplexity, Claude, and Gemini responses.`,
299
32
  }
300
33
  }
301
34
  ];
35
+ var TOOLS = [...ANALYSIS_TOOLS, ...ACCOUNT_TOOLS];
302
36
  var server = new Server(
303
37
  {
304
38
  name: "rankcli",
@@ -316,147 +50,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
316
50
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
317
51
  const { name, arguments: args } = request.params;
318
52
  try {
53
+ const analysisResult = await handleAnalysisTool(name, args);
54
+ if (analysisResult) return analysisResult;
319
55
  switch (name) {
320
- case "seo_analyze": {
321
- const { url, html: providedHtml, robotsTxt } = args;
322
- const html = providedHtml ?? await fetchHtml(url);
323
- const result = await analyzers.analyzeComprehensive(html, url, { robotsTxt });
324
- return {
325
- content: [
326
- {
327
- type: "text",
328
- text: formatComprehensiveResult(result)
329
- }
330
- ]
331
- };
332
- }
333
- case "seo_geo_check": {
334
- const { url, html: providedHtml, robotsTxt } = args;
335
- const html = providedHtml ?? await fetchHtml(url);
336
- const result = await analyzers.analyzeGEO(html, url, robotsTxt);
337
- return {
338
- content: [{ type: "text", text: formatGEOResult(result) }]
339
- };
340
- }
341
- case "seo_robots_ai": {
342
- const { robotsTxt } = args;
343
- const result = analyzers.analyzeRobotsTxtForAI(robotsTxt);
344
- return {
345
- content: [
346
- {
347
- type: "text",
348
- text: `## AI Crawler Analysis
349
-
350
- **Allowed:** ${result.allowed.join(", ") || "None"}
351
-
352
- **Blocked:** ${result.blocked.join(", ") || "None"}
353
-
354
- **Recommendations:**
355
- ${result.recommendations.map((r) => `- ${r}`).join("\n") || "- All good!"}`
356
- }
357
- ]
358
- };
359
- }
360
- case "seo_generate_robots": {
361
- const { siteUrl } = args;
362
- const robotsTxt = analyzers.generateAIFriendlyRobotsTxt(siteUrl);
363
- return {
364
- content: [
365
- {
366
- type: "text",
367
- text: `## AI-Friendly robots.txt
368
-
369
- \`\`\`
370
- ${robotsTxt}\`\`\``
371
- }
372
- ]
373
- };
374
- }
375
- case "seo_core_web_vitals": {
376
- const { url, html } = args;
377
- const result = analyzers.analyzeCoreWebVitals(html, url);
378
- return {
379
- content: [{ type: "text", text: formatCWVResult(result) }]
380
- };
381
- }
382
- case "seo_structured_data": {
383
- const { url, html } = args;
384
- const result = analyzers.analyzeStructuredData(html, url);
385
- return {
386
- content: [{ type: "text", text: formatStructuredDataResult(result) }]
387
- };
388
- }
389
- case "seo_generate_schema": {
390
- const { pageType, siteName, siteUrl, authorName, organizationName } = args;
391
- const schema = analyzers.generateSchemaTemplate(pageType, { siteName, siteUrl, authorName, organizationName });
392
- return {
393
- content: [
394
- {
395
- type: "text",
396
- text: `## ${pageType} Schema Template
397
-
398
- \`\`\`json
399
- ${schema}
400
- \`\`\`
401
-
402
- Replace {{placeholders}} with actual values.`
403
- }
404
- ]
405
- };
406
- }
407
- case "seo_security_headers": {
408
- const { url, headers } = args;
409
- const result = analyzers.analyzeSecurityHeaders(headers, url);
410
- return {
411
- content: [{ type: "text", text: formatSecurityResult(result) }]
412
- };
413
- }
414
- case "seo_generate_security_headers": {
415
- const { siteUrl } = args;
416
- const headers = analyzers.generateSecurityHeaders(siteUrl);
417
- return {
418
- content: [
419
- {
420
- type: "text",
421
- text: `## Recommended Security Headers
422
-
423
- ${Object.entries(headers).map(([k, v]) => `**${k}:**
424
- \`${v}\``).join("\n\n")}`
425
- }
426
- ]
427
- };
428
- }
429
- case "seo_images": {
430
- const { url, html } = args;
431
- const result = analyzers.analyzeImages(html, url);
432
- return {
433
- content: [{ type: "text", text: formatImagesResult(result) }]
434
- };
435
- }
436
- case "seo_internal_links": {
437
- const { url, html } = args;
438
- const result = analyzers.analyzeInternalLinking(html, url);
439
- return {
440
- content: [{ type: "text", text: formatInternalLinksResult(result) }]
441
- };
442
- }
443
- case "seo_mobile": {
444
- const { url, html } = args;
445
- const result = analyzers.analyzeMobileSEO(html, url);
446
- return {
447
- content: [{ type: "text", text: formatMobileResult(result) }]
448
- };
449
- }
450
- case "seo_ai_crawlers": {
451
- return {
452
- content: [
453
- {
454
- type: "text",
455
- text: formatAICrawlersInfo()
456
- }
457
- ]
458
- };
459
- }
460
56
  case "rankcli_connect": {
461
57
  const result = await connectToRankCLI();
462
58
  return {
@@ -496,28 +92,25 @@ ${Object.entries(headers).map(([k, v]) => `**${k}:**
496
92
  };
497
93
  }
498
94
  });
499
- var BRIDGE_FOOTER = `
500
- ---
501
- *This ran locally with no signup. For GitHub auto-fix PRs, scheduled monitoring, and a dashboard with history across audits, call the rankcli_connect tool or create a free account at [rankcli.dev](https://rankcli.dev).*`;
502
95
  async function pollConnectStatus(connectId, timeoutMs) {
503
96
  const deadline = Date.now() + timeoutMs;
504
97
  while (Date.now() < deadline) {
505
- await new Promise((resolve) => setTimeout(resolve, 2e3));
506
98
  try {
507
99
  const res = await fetch(`${SUPABASE_URL}/functions/v1/mcp-connect-status`, {
508
100
  method: "POST",
509
101
  headers: { "Content-Type": "application/json" },
510
102
  body: JSON.stringify({ connectId })
511
103
  });
512
- const data = await res.json();
513
- if (data.status === "claimed") {
514
- return { email: data.email, apiKey: data.apiKey, apiKeyName: data.apiKeyName };
104
+ const status = await res.json();
105
+ if (status.status === "claimed" && status.apiKey) {
106
+ return { apiKey: status.apiKey, apiKeyName: status.apiKeyName, email: status.email };
515
107
  }
516
- if (data.status === "expired" || data.status === "not_found") {
108
+ if (status.status === "expired" || status.status === "not_found") {
517
109
  return null;
518
110
  }
519
111
  } catch {
520
112
  }
113
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
521
114
  }
522
115
  return null;
523
116
  }
@@ -525,6 +118,12 @@ function clearPendingConnect() {
525
118
  localConfig.delete("pendingConnectId");
526
119
  localConfig.delete("pendingConnectUrl");
527
120
  localConfig.delete("pendingConnectExpiresAt");
121
+ localConfig.delete("pendingConnectCode");
122
+ }
123
+ function codeLine(userCode) {
124
+ return userCode ? `
125
+
126
+ Approve only if the page shows this code: ${userCode}` : "";
528
127
  }
529
128
  async function connectToRankCLI() {
530
129
  const existingKey = localConfig.get("apiKey");
@@ -551,7 +150,7 @@ async function connectToRankCLI() {
551
150
  return {
552
151
  text: `Still waiting for approval. Open this link if you haven't yet, then run rankcli_connect again:
553
152
 
554
- ${pendingUrl}
153
+ ${pendingUrl}${codeLine(localConfig.get("pendingConnectCode"))}
555
154
 
556
155
  Expires in ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"}.`
557
156
  };
@@ -568,17 +167,18 @@ Expires in ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"}.`
568
167
  isError: true
569
168
  };
570
169
  }
571
- const { connectId, url, expiresInSeconds } = startData;
170
+ const { connectId, userCode, url, expiresInSeconds } = startData;
572
171
  const expiresMinutes = Math.round(expiresInSeconds / 60);
573
172
  localConfig.set("pendingConnectId", connectId);
574
173
  localConfig.set("pendingConnectUrl", url);
575
174
  localConfig.set("pendingConnectExpiresAt", Date.now() + expiresInSeconds * 1e3);
175
+ if (userCode) localConfig.set("pendingConnectCode", userCode);
576
176
  const supportsUrlElicitation = !!server.getClientCapabilities()?.elicitation?.url;
577
177
  if (supportsUrlElicitation) {
578
178
  try {
579
179
  const result = await server.elicitInput({
580
180
  mode: "url",
581
- message: "Connect your RankCLI account to enable GitHub auto-fix PRs, scheduled monitoring, and a dashboard with audit history.",
181
+ message: `Connect your RankCLI account to enable GitHub auto-fix PRs, scheduled monitoring, and a dashboard with audit history.${codeLine(userCode)}`,
582
182
  url,
583
183
  elicitationId: connectId
584
184
  });
@@ -602,270 +202,11 @@ Expires in ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"}.`
602
202
  return {
603
203
  text: `Open this link to connect your RankCLI account:
604
204
 
605
- ${url}
205
+ ${url}${codeLine(userCode)}
606
206
 
607
207
  Then run rankcli_connect again to finish (link expires in ${expiresMinutes} minutes).`
608
208
  };
609
209
  }
610
- function formatComprehensiveResult(result) {
611
- const criticalIssues = result.allIssues.filter((i) => i.severity === "critical");
612
- const warnings = result.allIssues.filter((i) => i.severity === "warning");
613
- return `# SEO Analysis Report
614
-
615
- **URL:** ${result.url}
616
- **Overall Score:** ${result.overallScore}/100
617
- **Analyzed:** ${result.timestamp}
618
-
619
- ## Scores
620
-
621
- | Category | Score |
622
- |----------|-------|
623
- | GEO (AI Search) | ${result.grades.geo}/100 |
624
- | Core Web Vitals | ${result.grades.coreWebVitals}/100 |
625
- | Security | ${result.grades.security} |
626
- | Structured Data | ${result.grades.structuredData}/100 |
627
- | Images | ${result.grades.images}/100 |
628
- | Internal Links | ${result.grades.internalLinking}/100 |
629
- | Mobile SEO | ${result.grades.mobile}/100 |
630
-
631
- ## Critical Issues (${criticalIssues.length})
632
-
633
- ${criticalIssues.map((i) => `### \u274C ${i.title}
634
- ${i.description}
635
-
636
- **Fix:** ${i.howToFix}`).join("\n\n") || "None!"}
637
-
638
- ## Warnings (${warnings.length})
639
-
640
- ${warnings.slice(0, 5).map((i) => `- **${i.title}:** ${i.description}`).join("\n") || "None!"}
641
-
642
- ## Priority Recommendations
643
-
644
- ${result.prioritizedRecommendations.map((r, i) => `${i + 1}. ${r}`).join("\n")}
645
- ${BRIDGE_FOOTER}
646
- `;
647
- }
648
- function formatGEOResult(result) {
649
- return `# GEO Analysis (AI Search Optimization)
650
-
651
- **Score:** ${result.score}/100
652
-
653
- ## AI Crawler Access
654
-
655
- | Status | Crawlers |
656
- |--------|----------|
657
- | \u2705 Allowed | ${result.aiCrawlerAccess.allowedCrawlers.join(", ") || "None"} |
658
- | \u274C Blocked | ${result.aiCrawlerAccess.blockedCrawlers.join(", ") || "None"} |
659
-
660
- **Server-Side Rendered:** ${result.aiCrawlerAccess.serverSideRendered ? "\u2705 Yes" : "\u274C No"}
661
- **JS Rendering Required:** ${result.aiCrawlerAccess.jsRenderingRequired ? "\u26A0\uFE0F Yes (AI crawlers may not see content)" : "\u2705 No"}
662
-
663
- ## LLM Friendliness Scores
664
-
665
- | Signal | Score |
666
- |--------|-------|
667
- | Content Clarity | ${result.llmSignals.contentClarity}/100 |
668
- | Fact Density | ${result.llmSignals.factDensity}/100 |
669
- | Structure Quality | ${result.llmSignals.structureQuality}/100 |
670
- | Citation Quality | ${result.llmSignals.citationQuality}/100 |
671
-
672
- ## Content Structure
673
-
674
- - Structured Data: ${result.contentStructure.hasStructuredData ? "\u2705" : "\u274C"}
675
- - FAQ Schema: ${result.contentStructure.hasFAQSchema ? "\u2705" : "\u274C"}
676
- - Article Schema: ${result.contentStructure.hasArticleSchema ? "\u2705" : "\u274C"}
677
- - Heading Hierarchy: ${result.contentStructure.headingHierarchy}
678
-
679
- ## Citation Readiness
680
-
681
- Trust Signals: ${result.citationReadiness.trustSignals.join(", ") || "None detected"}
682
-
683
- ## Recommendations
684
-
685
- ${result.recommendations.map((r) => `- ${r}`).join("\n")}
686
- ${BRIDGE_FOOTER}
687
- `;
688
- }
689
- function formatCWVResult(result) {
690
- const emoji = (est) => est === "good" ? "\u{1F7E2}" : est === "needs-improvement" ? "\u{1F7E1}" : "\u{1F534}";
691
- return `# Core Web Vitals Estimate
692
-
693
- **Overall Score:** ${result.overallScore}/100
694
-
695
- | Metric | Estimate | Issues |
696
- |--------|----------|--------|
697
- | LCP (Largest Contentful Paint) | ${emoji(result.lcp.estimate)} ${result.lcp.estimate} | ${result.lcp.issues.length} |
698
- | CLS (Cumulative Layout Shift) | ${emoji(result.cls.estimate)} ${result.cls.estimate} | ${result.cls.issues.length} |
699
- | INP (Interaction to Next Paint) | ${emoji(result.inp.estimate)} ${result.inp.estimate} | ${result.inp.issues.length} |
700
- | TTFB (Time to First Byte) | ${emoji(result.ttfb.estimate)} ${result.ttfb.estimate} | ${result.ttfb.issues.length} |
701
-
702
- ## Issues
703
-
704
- ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No critical issues!"}
705
- `;
706
- }
707
- function formatStructuredDataResult(result) {
708
- return `# Structured Data Analysis
709
-
710
- **Score:** ${result.score}/100
711
- **Schemas Found:** ${result.schemas.length}
712
-
713
- ## Schema Types
714
-
715
- | Type | Valid | Errors |
716
- |------|-------|--------|
717
- ${result.schemas.map((s) => `| ${s.type} | ${s.isValid ? "\u2705" : "\u274C"} | ${s.errors.join("; ") || "-"} |`).join("\n") || "| None found | - | - |"}
718
-
719
- ## Rich Result Eligibility
720
-
721
- - Organization: ${result.hasOrganization ? "\u2705" : "\u274C"}
722
- - WebSite: ${result.hasWebSite ? "\u2705" : "\u274C"}
723
- - Breadcrumb: ${result.hasBreadcrumb ? "\u2705" : "\u274C"}
724
- - Article: ${result.hasArticle ? "\u2705" : "\u274C"}
725
- - Product: ${result.hasProduct ? "\u2705" : "\u274C"}
726
- - FAQ: ${result.hasFAQ ? "\u2705" : "\u274C"}
727
- - HowTo: ${result.hasHowTo ? "\u2705" : "\u274C"}
728
-
729
- ## Recommendations
730
-
731
- ${result.recommendations.map((r) => `- ${r}`).join("\n")}
732
- `;
733
- }
734
- function formatSecurityResult(result) {
735
- return `# Security Headers Analysis
736
-
737
- **Grade:** ${result.grade}
738
- **Score:** ${result.score}/100
739
-
740
- ## Headers
741
-
742
- | Header | Status |
743
- |--------|--------|
744
- | HTTPS | ${result.https.enabled ? "\u2705" : "\u274C"} |
745
- | HSTS | ${result.https.hasHSTS ? "\u2705" : "\u274C"} |
746
- | CSP | ${result.contentSecurity.hasCSP ? "\u2705" : "\u274C"} |
747
- | X-Frame-Options | ${result.frameOptions.hasXFrameOptions ? "\u2705" : "\u274C"} |
748
- | X-Content-Type-Options | ${result.contentTypeOptions.hasXContentTypeOptions ? "\u2705" : "\u274C"} |
749
- | Referrer-Policy | ${result.referrerPolicy.hasReferrerPolicy ? "\u2705" : "\u274C"} |
750
- | Permissions-Policy | ${result.permissionsPolicy.hasPermissionsPolicy ? "\u2705" : "\u274C"} |
751
-
752
- ## Issues
753
-
754
- ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
755
- `;
756
- }
757
- function formatImagesResult(result) {
758
- return `# Image Analysis
759
-
760
- **Score:** ${result.score}/100
761
- **Total Images:** ${result.totalImages}
762
-
763
- ## Stats
764
-
765
- | Metric | Count |
766
- |--------|-------|
767
- | With Alt Text | ${result.imagesWithAlt} |
768
- | With Dimensions | ${result.imagesWithDimensions} |
769
- | Lazy Loading | ${result.imagesWithLazyLoading} |
770
- | Modern Formats | ${result.modernFormats} |
771
- | Legacy Formats | ${result.legacyFormats} |
772
-
773
- ## Issues
774
-
775
- ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
776
-
777
- ## Recommendations
778
-
779
- ${result.recommendations.map((r) => `- ${r}`).join("\n")}
780
- `;
781
- }
782
- function formatInternalLinksResult(result) {
783
- return `# Internal Linking Analysis
784
-
785
- **Score:** ${result.score}/100
786
-
787
- ## Stats
788
-
789
- | Metric | Count |
790
- |--------|-------|
791
- | Total Links | ${result.totalLinks} |
792
- | Internal Links | ${result.internalLinks} |
793
- | External Links | ${result.externalLinks} |
794
- | Navigation Links | ${result.navigationLinks} |
795
- | Content Links | ${result.contentLinks} |
796
- | Unique Internal Targets | ${result.uniqueInternalTargets} |
797
-
798
- ## Anchor Text Quality
799
-
800
- - Descriptive: ${result.anchorTextAnalysis.descriptive}
801
- - Generic: ${result.anchorTextAnalysis.generic}
802
- - Empty: ${result.anchorTextAnalysis.empty}
803
-
804
- ## Issues
805
-
806
- ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
807
-
808
- ## Recommendations
809
-
810
- ${result.recommendations.map((r) => `- ${r}`).join("\n")}
811
- `;
812
- }
813
- function formatMobileResult(result) {
814
- return `# Mobile SEO Analysis
815
-
816
- **Score:** ${result.score}/100
817
-
818
- ## Viewport
819
-
820
- - Has Viewport: ${result.viewport.hasViewport ? "\u2705" : "\u274C"}
821
- - Responsive: ${result.viewport.isResponsive ? "\u2705" : "\u274C"}
822
- ${result.viewport.viewportContent ? `- Content: \`${result.viewport.viewportContent}\`` : ""}
823
-
824
- ## Touch Targets
825
-
826
- - Small Targets: ${result.touchTargets.smallTargets}
827
- - Proper Targets: ${result.touchTargets.properTargets}
828
-
829
- ## Mobile Features
830
-
831
- - Apple Touch Icon: ${result.mobileSpecific.hasAppleTouchIcon ? "\u2705" : "\u274C"}
832
- - Theme Color: ${result.mobileSpecific.hasThemeColor ? "\u2705" : "\u274C"}
833
- - Web App Manifest: ${result.mobileSpecific.hasManifest ? "\u2705" : "\u274C"}
834
- - Responsive Images: ${result.mobileSpecific.hasMobileOptimizedImages ? "\u2705" : "\u274C"}
835
-
836
- ## Issues
837
-
838
- ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
839
-
840
- ## Recommendations
841
-
842
- ${result.recommendations.map((r) => `- ${r}`).join("\n")}
843
- `;
844
- }
845
- function formatAICrawlersInfo() {
846
- const crawlers = Object.entries(analyzers.AI_CRAWLERS_INFO);
847
- return `# Known AI Crawlers
848
-
849
- ${crawlers.map(([name, info]) => `## ${name}
850
- - **User-Agent:** ${info.userAgent}
851
- - **Company:** ${info.company}
852
- - **Purpose:** ${info.purpose}
853
- `).join("\n")}
854
-
855
- ## robots.txt Example
856
-
857
- \`\`\`
858
- User-agent: GPTBot
859
- Allow: /
860
-
861
- User-agent: Claude-Web
862
- Allow: /
863
-
864
- User-agent: PerplexityBot
865
- Allow: /
866
- \`\`\`
867
- `;
868
- }
869
210
  async function main() {
870
211
  const transport = new StdioServerTransport();
871
212
  await server.connect(transport);