@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.
@@ -0,0 +1,804 @@
1
+ // src/server.ts
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema
6
+ } from "@modelcontextprotocol/sdk/types.js";
7
+ import { analyzers } from "@rankcli/agent-runtime";
8
+
9
+ // src/fetch-guard.ts
10
+ import { lookup } from "dns/promises";
11
+ import { isIP } from "net";
12
+ var USER_AGENT = "RankCLI/1.0 (+https://rankcli.dev)";
13
+ var MAX_REDIRECTS = 5;
14
+ var TIMEOUT_MS = 15e3;
15
+ function ipv4ToInt(ip) {
16
+ return ip.split(".").reduce((acc, part) => (acc << 8) + Number(part), 0) >>> 0;
17
+ }
18
+ function inV4(ip, cidr) {
19
+ const [base, bits] = cidr.split("/");
20
+ const mask = Number(bits) === 0 ? 0 : ~0 << 32 - Number(bits) >>> 0;
21
+ return (ipv4ToInt(ip) & mask) === (ipv4ToInt(base) & mask);
22
+ }
23
+ var BLOCKED_V4 = [
24
+ "0.0.0.0/8",
25
+ "10.0.0.0/8",
26
+ "100.64.0.0/10",
27
+ "127.0.0.0/8",
28
+ "169.254.0.0/16",
29
+ "172.16.0.0/12",
30
+ "192.0.0.0/24",
31
+ "192.168.0.0/16",
32
+ "198.18.0.0/15",
33
+ "224.0.0.0/4",
34
+ "240.0.0.0/4"
35
+ ];
36
+ function isPrivateAddress(ip) {
37
+ const version = isIP(ip);
38
+ if (version === 4) return BLOCKED_V4.some((cidr) => inV4(ip, cidr));
39
+ if (version === 6) {
40
+ const v6 = ip.toLowerCase();
41
+ const mapped = v6.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
42
+ if (mapped) return isPrivateAddress(mapped[1]);
43
+ if (v6 === "::" || v6 === "::1") return true;
44
+ const first = parseInt(v6.split(":")[0] || "0", 16);
45
+ if ((first & 65024) === 64512) return true;
46
+ if ((first & 65472) === 65152) return true;
47
+ if ((first & 65280) === 65280) return true;
48
+ return false;
49
+ }
50
+ return true;
51
+ }
52
+ async function assertPublicUrl(url) {
53
+ const host = url.hostname.replace(/^\[|\]$/g, "");
54
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
55
+ throw new Error(`Refusing to fetch ${url.origin}: private network address`);
56
+ }
57
+ const addresses = isIP(host) ? [{ address: host }] : await lookup(host, { all: true, verbatim: true });
58
+ if (addresses.length === 0 || addresses.some((a) => isPrivateAddress(a.address))) {
59
+ throw new Error(`Refusing to fetch ${url.origin}: private network address`);
60
+ }
61
+ }
62
+ async function guardedFetch(rawUrl, options = {}) {
63
+ let url;
64
+ try {
65
+ url = new URL(rawUrl);
66
+ } catch {
67
+ throw new Error(`Invalid URL: ${rawUrl}`);
68
+ }
69
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
70
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
71
+ throw new Error(`Refusing to fetch ${url.protocol} URL - only http(s) is supported`);
72
+ }
73
+ if (options.blockPrivateNetworks) await assertPublicUrl(url);
74
+ const response = await fetch(url, {
75
+ headers: { "User-Agent": USER_AGENT },
76
+ redirect: options.blockPrivateNetworks ? "manual" : "follow",
77
+ signal: AbortSignal.timeout(TIMEOUT_MS)
78
+ });
79
+ const location = response.headers.get("location");
80
+ if (options.blockPrivateNetworks && response.status >= 300 && response.status < 400 && location) {
81
+ url = new URL(location, url);
82
+ continue;
83
+ }
84
+ return response;
85
+ }
86
+ throw new Error(`Too many redirects fetching ${rawUrl}`);
87
+ }
88
+
89
+ // src/server.ts
90
+ async function fetchHtml(url, options = {}) {
91
+ const res = await guardedFetch(url, options);
92
+ if (!res.ok) {
93
+ throw new Error(`Fetching ${url} returned ${res.status} ${res.statusText}`);
94
+ }
95
+ return res.text();
96
+ }
97
+ async function fetchRobotsTxt(pageUrl, options = {}) {
98
+ let robotsUrl;
99
+ try {
100
+ robotsUrl = new URL("/robots.txt", pageUrl).href;
101
+ } catch {
102
+ return { note: "robots.txt not checked: invalid URL." };
103
+ }
104
+ try {
105
+ const res = await guardedFetch(robotsUrl, options);
106
+ if (res.ok) {
107
+ return { content: await res.text(), note: `robots.txt fetched from ${robotsUrl}.` };
108
+ }
109
+ if (res.status >= 500 || res.status === 429) {
110
+ return {
111
+ unknown: true,
112
+ note: `robots.txt returned HTTP ${res.status}, so crawler access is unknown - Google treats a 5xx robots.txt as "disallow everything" until it answers.`
113
+ };
114
+ }
115
+ return { note: `No robots.txt (HTTP ${res.status}) - every crawler is allowed by default.` };
116
+ } catch (error) {
117
+ return { unknown: true, note: `robots.txt could not be fetched (${error instanceof Error ? error.message : String(error)}), so crawler access is unknown.` };
118
+ }
119
+ }
120
+ async function resolveRobotsTxt(url, provided, options) {
121
+ if (provided !== void 0) return { content: provided, note: "robots.txt as provided." };
122
+ return fetchRobotsTxt(url, options);
123
+ }
124
+ var ANALYSIS_TOOLS = [
125
+ {
126
+ name: "seo_analyze",
127
+ description: `Run comprehensive SEO analysis on a webpage. Returns scores and issues for:
128
+ - GEO (AI Search Optimization) - Is the site visible to ChatGPT, Perplexity, Claude?
129
+ - Core Web Vitals (LCP, CLS, INP) - Performance estimates
130
+ - Structured Data - Schema.org validation
131
+ - Security Headers - HTTPS, HSTS, CSP
132
+ - Mobile SEO - Responsive design, touch targets
133
+ - Images - Alt text, formats, dimensions
134
+ - Internal Linking - Anchor text, orphan detection
135
+
136
+ Use this for a complete SEO audit.`,
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ url: {
141
+ type: "string",
142
+ description: "URL of the page to analyze"
143
+ },
144
+ html: {
145
+ type: "string",
146
+ description: "HTML content of the page (optional if URL is provided)"
147
+ },
148
+ robotsTxt: {
149
+ type: "string",
150
+ description: "robots.txt content (optional - fetched from the site when omitted)"
151
+ }
152
+ },
153
+ required: ["url"]
154
+ }
155
+ },
156
+ {
157
+ name: "seo_geo_check",
158
+ description: `Check if a website is optimized for AI search engines (GEO - Generative Engine Optimization).
159
+
160
+ Analyzes:
161
+ - AI crawler access (GPTBot, ClaudeBot, PerplexityBot, etc.)
162
+ - robots.txt rules for AI crawlers
163
+ - JS rendering requirements (can AI crawlers see content?)
164
+ - Content structure for LLM consumption
165
+ - Citation readiness (trust signals)
166
+ - FAQ/entity extraction capability
167
+
168
+ Critical for visibility in ChatGPT, Perplexity, Claude, and Gemini responses.`,
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ url: {
173
+ type: "string",
174
+ description: "URL of the page to analyze"
175
+ },
176
+ html: {
177
+ type: "string",
178
+ description: "HTML content of the page (optional - fetched from the URL when omitted)"
179
+ },
180
+ robotsTxt: {
181
+ type: "string",
182
+ description: "robots.txt content (optional - fetched from the site when omitted)"
183
+ }
184
+ },
185
+ required: ["url"]
186
+ }
187
+ },
188
+ {
189
+ name: "seo_robots_ai",
190
+ description: `Analyze robots.txt for AI crawler permissions. Shows which AI crawlers (GPTBot, ClaudeBot, PerplexityBot, etc.) are allowed or blocked.`,
191
+ inputSchema: {
192
+ type: "object",
193
+ properties: {
194
+ robotsTxt: {
195
+ type: "string",
196
+ description: "Content of robots.txt file"
197
+ }
198
+ },
199
+ required: ["robotsTxt"]
200
+ }
201
+ },
202
+ {
203
+ name: "seo_generate_robots",
204
+ description: `Generate an AI-friendly robots.txt that allows all major AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.)`,
205
+ inputSchema: {
206
+ type: "object",
207
+ properties: {
208
+ siteUrl: {
209
+ type: "string",
210
+ description: "Base URL of the site (e.g., https://example.com)"
211
+ }
212
+ },
213
+ required: ["siteUrl"]
214
+ }
215
+ },
216
+ {
217
+ name: "seo_core_web_vitals",
218
+ description: `Estimate Core Web Vitals (LCP, CLS, INP, TTFB) from HTML analysis. Identifies issues like:
219
+ - Render-blocking resources
220
+ - Images without dimensions
221
+ - Large JavaScript bundles
222
+ - Missing preload hints`,
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {
226
+ url: {
227
+ type: "string",
228
+ description: "URL of the page"
229
+ },
230
+ html: {
231
+ type: "string",
232
+ description: "HTML content of the page"
233
+ }
234
+ },
235
+ required: ["url", "html"]
236
+ }
237
+ },
238
+ {
239
+ name: "seo_structured_data",
240
+ description: `Validate JSON-LD structured data (Schema.org). Checks for:
241
+ - Required properties per schema type
242
+ - Article, Product, FAQ, HowTo, LocalBusiness schemas
243
+ - Rich result eligibility
244
+ - Common mistakes`,
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {
248
+ url: {
249
+ type: "string",
250
+ description: "URL of the page"
251
+ },
252
+ html: {
253
+ type: "string",
254
+ description: "HTML content with JSON-LD"
255
+ }
256
+ },
257
+ required: ["url", "html"]
258
+ }
259
+ },
260
+ {
261
+ name: "seo_generate_schema",
262
+ description: `Generate JSON-LD structured data template for a page type (article, product, faq, local-business, website).`,
263
+ inputSchema: {
264
+ type: "object",
265
+ properties: {
266
+ pageType: {
267
+ type: "string",
268
+ enum: ["article", "product", "faq", "local-business", "website"],
269
+ description: "Type of page"
270
+ },
271
+ siteName: {
272
+ type: "string",
273
+ description: "Name of the website"
274
+ },
275
+ siteUrl: {
276
+ type: "string",
277
+ description: "Base URL of the website"
278
+ },
279
+ authorName: {
280
+ type: "string",
281
+ description: "Default author name (for articles)"
282
+ },
283
+ organizationName: {
284
+ type: "string",
285
+ description: "Organization name"
286
+ }
287
+ },
288
+ required: ["pageType", "siteName", "siteUrl"]
289
+ }
290
+ },
291
+ {
292
+ name: "seo_security_headers",
293
+ description: `Analyze security headers (HTTPS, HSTS, CSP, X-Frame-Options, etc.). Returns a security grade A+ through F.`,
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ url: {
298
+ type: "string",
299
+ description: "URL of the page"
300
+ },
301
+ headers: {
302
+ type: "object",
303
+ description: "HTTP response headers",
304
+ additionalProperties: { type: "string" }
305
+ }
306
+ },
307
+ required: ["url", "headers"]
308
+ }
309
+ },
310
+ {
311
+ name: "seo_generate_security_headers",
312
+ description: `Generate recommended security headers configuration for a site.`,
313
+ inputSchema: {
314
+ type: "object",
315
+ properties: {
316
+ siteUrl: {
317
+ type: "string",
318
+ description: "Base URL of the site"
319
+ }
320
+ },
321
+ required: ["siteUrl"]
322
+ }
323
+ },
324
+ {
325
+ name: "seo_images",
326
+ description: `Analyze images for SEO and performance. Checks alt text, dimensions, formats (WebP/AVIF), lazy loading, and responsive images.`,
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ url: {
331
+ type: "string",
332
+ description: "URL of the page"
333
+ },
334
+ html: {
335
+ type: "string",
336
+ description: "HTML content of the page"
337
+ }
338
+ },
339
+ required: ["url", "html"]
340
+ }
341
+ },
342
+ {
343
+ name: "seo_internal_links",
344
+ description: `Analyze internal linking structure. Checks anchor text quality, orphan page risk, link distribution, and suggests linking opportunities.`,
345
+ inputSchema: {
346
+ type: "object",
347
+ properties: {
348
+ url: {
349
+ type: "string",
350
+ description: "URL of the page"
351
+ },
352
+ html: {
353
+ type: "string",
354
+ description: "HTML content of the page"
355
+ }
356
+ },
357
+ required: ["url", "html"]
358
+ }
359
+ },
360
+ {
361
+ name: "seo_mobile",
362
+ description: `Analyze mobile SEO. Checks viewport, touch targets, font sizes, content width, PWA readiness.`,
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ url: {
367
+ type: "string",
368
+ description: "URL of the page"
369
+ },
370
+ html: {
371
+ type: "string",
372
+ description: "HTML content of the page"
373
+ }
374
+ },
375
+ required: ["url", "html"]
376
+ }
377
+ },
378
+ {
379
+ name: "seo_ai_crawlers",
380
+ description: `Get information about all known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.) including their user agents and purposes.`,
381
+ inputSchema: {
382
+ type: "object",
383
+ properties: {}
384
+ }
385
+ }
386
+ ];
387
+ var BRIDGE_FOOTER = `
388
+ ---
389
+ *This ran with no signup. For GitHub auto-fix PRs, scheduled monitoring, and a dashboard with history across audits, connect via the RankCLI CLI (\`rankcli login\`) or create a free account at [rankcli.dev](https://rankcli.dev).*`;
390
+ function formatComprehensiveResult(result) {
391
+ const criticalIssues = result.allIssues.filter((i) => i.severity === "critical");
392
+ const warnings = result.allIssues.filter((i) => i.severity === "warning");
393
+ return `# SEO Analysis Report
394
+
395
+ **URL:** ${result.url}
396
+ **Overall Score:** ${result.overallScore}/100
397
+ **Analyzed:** ${result.timestamp}
398
+
399
+ ## Scores
400
+
401
+ | Category | Score |
402
+ |----------|-------|
403
+ | GEO (AI Search) | ${result.grades.geo}/100 |
404
+ | Core Web Vitals | ${result.grades.coreWebVitals}/100 |
405
+ | Security | ${result.grades.security} |
406
+ | Structured Data | ${result.grades.structuredData}/100 |
407
+ | Images | ${result.grades.images}/100 |
408
+ | Internal Links | ${result.grades.internalLinking}/100 |
409
+ | Mobile SEO | ${result.grades.mobile}/100 |
410
+
411
+ ## Critical Issues (${criticalIssues.length})
412
+
413
+ ${criticalIssues.map((i) => `### \u274C ${i.title}
414
+ ${i.description}
415
+
416
+ **Fix:** ${i.howToFix}`).join("\n\n") || "None!"}
417
+
418
+ ## Warnings (${warnings.length})
419
+
420
+ ${warnings.slice(0, 5).map((i) => `- **${i.title}:** ${i.description}`).join("\n") || "None!"}
421
+
422
+ ## Priority Recommendations
423
+
424
+ ${result.prioritizedRecommendations.map((r, i) => `${i + 1}. ${r}`).join("\n")}
425
+ ${BRIDGE_FOOTER}
426
+ `;
427
+ }
428
+ function formatGEOResult(result, robots) {
429
+ const access = robots.unknown ? `| \u2754 Unknown | ${robots.note} |` : `| \u2705 Allowed | ${result.aiCrawlerAccess.allowedCrawlers.join(", ") || "None"} |
430
+ | \u274C Blocked | ${result.aiCrawlerAccess.blockedCrawlers.join(", ") || "None"} |`;
431
+ return `# GEO Analysis (AI Search Optimization)
432
+
433
+ **Score:** ${result.score}/100
434
+
435
+ ## AI Crawler Access
436
+
437
+ | Status | Crawlers |
438
+ |--------|----------|
439
+ ${access}
440
+
441
+ _${robots.note}_
442
+
443
+ **Server-Side Rendered:** ${result.aiCrawlerAccess.serverSideRendered ? "\u2705 Yes" : "\u274C No"}
444
+ **JS Rendering Required:** ${result.aiCrawlerAccess.jsRenderingRequired ? "\u26A0\uFE0F Yes (AI crawlers may not see content)" : "\u2705 No"}
445
+
446
+ ## LLM Friendliness Scores
447
+
448
+ | Signal | Score |
449
+ |--------|-------|
450
+ | Content Clarity | ${result.llmSignals.contentClarity}/100 |
451
+ | Fact Density | ${result.llmSignals.factDensity}/100 |
452
+ | Structure Quality | ${result.llmSignals.structureQuality}/100 |
453
+ | Citation Quality | ${result.llmSignals.citationQuality}/100 |
454
+
455
+ ## Content Structure
456
+
457
+ - Structured Data: ${result.contentStructure.hasStructuredData ? "\u2705" : "\u274C"}
458
+ - FAQ Schema: ${result.contentStructure.hasFAQSchema ? "\u2705" : "\u274C"}
459
+ - Article Schema: ${result.contentStructure.hasArticleSchema ? "\u2705" : "\u274C"}
460
+ - Heading Hierarchy: ${result.contentStructure.headingHierarchy}
461
+
462
+ ## Citation Readiness
463
+
464
+ Trust Signals: ${result.citationReadiness.trustSignals.join(", ") || "None detected"}
465
+
466
+ ## Recommendations
467
+
468
+ ${result.recommendations.map((r) => `- ${r}`).join("\n")}
469
+ ${BRIDGE_FOOTER}
470
+ `;
471
+ }
472
+ function formatCWVResult(result) {
473
+ const emoji = (est) => est === "good" ? "\u{1F7E2}" : est === "needs-improvement" ? "\u{1F7E1}" : "\u{1F534}";
474
+ return `# Core Web Vitals Estimate
475
+
476
+ **Overall Score:** ${result.overallScore}/100
477
+
478
+ | Metric | Estimate | Issues |
479
+ |--------|----------|--------|
480
+ | LCP (Largest Contentful Paint) | ${emoji(result.lcp.estimate)} ${result.lcp.estimate} | ${result.lcp.issues.length} |
481
+ | CLS (Cumulative Layout Shift) | ${emoji(result.cls.estimate)} ${result.cls.estimate} | ${result.cls.issues.length} |
482
+ | INP (Interaction to Next Paint) | ${emoji(result.inp.estimate)} ${result.inp.estimate} | ${result.inp.issues.length} |
483
+ | TTFB (Time to First Byte) | ${emoji(result.ttfb.estimate)} ${result.ttfb.estimate} | ${result.ttfb.issues.length} |
484
+
485
+ ## Issues
486
+
487
+ ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No critical issues!"}
488
+ `;
489
+ }
490
+ function formatStructuredDataResult(result) {
491
+ return `# Structured Data Analysis
492
+
493
+ **Score:** ${result.score}/100
494
+ **Schemas Found:** ${result.schemas.length}
495
+
496
+ ## Schema Types
497
+
498
+ | Type | Valid | Errors |
499
+ |------|-------|--------|
500
+ ${result.schemas.map((s) => `| ${s.type} | ${s.isValid ? "\u2705" : "\u274C"} | ${s.errors.join("; ") || "-"} |`).join("\n") || "| None found | - | - |"}
501
+
502
+ ## Rich Result Eligibility
503
+
504
+ - Organization: ${result.hasOrganization ? "\u2705" : "\u274C"}
505
+ - WebSite: ${result.hasWebSite ? "\u2705" : "\u274C"}
506
+ - Breadcrumb: ${result.hasBreadcrumb ? "\u2705" : "\u274C"}
507
+ - Article: ${result.hasArticle ? "\u2705" : "\u274C"}
508
+ - Product: ${result.hasProduct ? "\u2705" : "\u274C"}
509
+ - FAQ: ${result.hasFAQ ? "\u2705" : "\u274C"}
510
+ - HowTo: ${result.hasHowTo ? "\u2705" : "\u274C"}
511
+
512
+ ## Recommendations
513
+
514
+ ${result.recommendations.map((r) => `- ${r}`).join("\n")}
515
+ `;
516
+ }
517
+ function formatSecurityResult(result) {
518
+ return `# Security Headers Analysis
519
+
520
+ **Grade:** ${result.grade}
521
+ **Score:** ${result.score}/100
522
+
523
+ ## Headers
524
+
525
+ | Header | Status |
526
+ |--------|--------|
527
+ | HTTPS | ${result.https.enabled ? "\u2705" : "\u274C"} |
528
+ | HSTS | ${result.https.hasHSTS ? "\u2705" : "\u274C"} |
529
+ | CSP | ${result.contentSecurity.hasCSP ? "\u2705" : "\u274C"} |
530
+ | X-Frame-Options | ${result.frameOptions.hasXFrameOptions ? "\u2705" : "\u274C"} |
531
+ | X-Content-Type-Options | ${result.contentTypeOptions.hasXContentTypeOptions ? "\u2705" : "\u274C"} |
532
+ | Referrer-Policy | ${result.referrerPolicy.hasReferrerPolicy ? "\u2705" : "\u274C"} |
533
+ | Permissions-Policy | ${result.permissionsPolicy.hasPermissionsPolicy ? "\u2705" : "\u274C"} |
534
+
535
+ ## Issues
536
+
537
+ ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
538
+ `;
539
+ }
540
+ function formatImagesResult(result) {
541
+ return `# Image Analysis
542
+
543
+ **Score:** ${result.score}/100
544
+ **Total Images:** ${result.totalImages}
545
+
546
+ ## Stats
547
+
548
+ | Metric | Count |
549
+ |--------|-------|
550
+ | With Alt Text | ${result.imagesWithAlt} |
551
+ | With Dimensions | ${result.imagesWithDimensions} |
552
+ | Lazy Loading | ${result.imagesWithLazyLoading} |
553
+ | Modern Formats | ${result.modernFormats} |
554
+ | Legacy Formats | ${result.legacyFormats} |
555
+
556
+ ## Issues
557
+
558
+ ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
559
+
560
+ ## Recommendations
561
+
562
+ ${result.recommendations.map((r) => `- ${r}`).join("\n")}
563
+ `;
564
+ }
565
+ function formatInternalLinksResult(result) {
566
+ return `# Internal Linking Analysis
567
+
568
+ **Score:** ${result.score}/100
569
+
570
+ ## Stats
571
+
572
+ | Metric | Count |
573
+ |--------|-------|
574
+ | Total Links | ${result.totalLinks} |
575
+ | Internal Links | ${result.internalLinks} |
576
+ | External Links | ${result.externalLinks} |
577
+ | Navigation Links | ${result.navigationLinks} |
578
+ | Content Links | ${result.contentLinks} |
579
+ | Unique Internal Targets | ${result.uniqueInternalTargets} |
580
+
581
+ ## Anchor Text Quality
582
+
583
+ - Descriptive: ${result.anchorTextAnalysis.descriptive}
584
+ - Generic: ${result.anchorTextAnalysis.generic}
585
+ - Empty: ${result.anchorTextAnalysis.empty}
586
+
587
+ ## Issues
588
+
589
+ ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
590
+
591
+ ## Recommendations
592
+
593
+ ${result.recommendations.map((r) => `- ${r}`).join("\n")}
594
+ `;
595
+ }
596
+ function formatMobileResult(result) {
597
+ return `# Mobile SEO Analysis
598
+
599
+ **Score:** ${result.score}/100
600
+
601
+ ## Viewport
602
+
603
+ - Has Viewport: ${result.viewport.hasViewport ? "\u2705" : "\u274C"}
604
+ - Responsive: ${result.viewport.isResponsive ? "\u2705" : "\u274C"}
605
+ ${result.viewport.viewportContent ? `- Content: \`${result.viewport.viewportContent}\`` : ""}
606
+
607
+ ## Touch Targets
608
+
609
+ - Small Targets: ${result.touchTargets.smallTargets}
610
+ - Proper Targets: ${result.touchTargets.properTargets}
611
+
612
+ ## Mobile Features
613
+
614
+ - Apple Touch Icon: ${result.mobileSpecific.hasAppleTouchIcon ? "\u2705" : "\u274C"}
615
+ - Theme Color: ${result.mobileSpecific.hasThemeColor ? "\u2705" : "\u274C"}
616
+ - Web App Manifest: ${result.mobileSpecific.hasManifest ? "\u2705" : "\u274C"}
617
+ - Responsive Images: ${result.mobileSpecific.hasMobileOptimizedImages ? "\u2705" : "\u274C"}
618
+
619
+ ## Issues
620
+
621
+ ${result.issues.map((i) => `- **${i.title}:** ${i.howToFix}`).join("\n") || "No issues!"}
622
+
623
+ ## Recommendations
624
+
625
+ ${result.recommendations.map((r) => `- ${r}`).join("\n")}
626
+ `;
627
+ }
628
+ function formatAICrawlersInfo() {
629
+ const crawlers = Object.entries(analyzers.AI_CRAWLERS_INFO);
630
+ return `# Known AI Crawlers
631
+
632
+ ${crawlers.map(([name, info]) => `## ${name}
633
+ - **User-Agent:** ${info.userAgent}
634
+ - **Company:** ${info.company}
635
+ - **Purpose:** ${info.purpose}
636
+ `).join("\n")}
637
+
638
+ ## robots.txt Example
639
+
640
+ \`\`\`
641
+ User-agent: GPTBot
642
+ Allow: /
643
+
644
+ User-agent: Claude-Web
645
+ Allow: /
646
+
647
+ User-agent: PerplexityBot
648
+ Allow: /
649
+ \`\`\`
650
+ `;
651
+ }
652
+ async function handleAnalysisTool(name, args, options = {}) {
653
+ switch (name) {
654
+ case "seo_analyze": {
655
+ const { url, html: providedHtml, robotsTxt } = args;
656
+ const [html, robots] = await Promise.all([
657
+ providedHtml ?? fetchHtml(url, options),
658
+ resolveRobotsTxt(url, robotsTxt, options)
659
+ ]);
660
+ const result = await analyzers.analyzeComprehensive(html, url, { robotsTxt: robots.content });
661
+ return { content: [{ type: "text", text: `${formatComprehensiveResult(result)}
662
+ _${robots.note}_
663
+ ` }] };
664
+ }
665
+ case "seo_geo_check": {
666
+ const { url, html: providedHtml, robotsTxt } = args;
667
+ const [html, robots] = await Promise.all([
668
+ providedHtml ?? fetchHtml(url, options),
669
+ resolveRobotsTxt(url, robotsTxt, options)
670
+ ]);
671
+ const result = await analyzers.analyzeGEO(html, url, robots.content);
672
+ return { content: [{ type: "text", text: formatGEOResult(result, robots) }] };
673
+ }
674
+ case "seo_robots_ai": {
675
+ const { robotsTxt } = args;
676
+ const result = analyzers.analyzeRobotsTxtForAI(robotsTxt);
677
+ return {
678
+ content: [
679
+ {
680
+ type: "text",
681
+ text: `## AI Crawler Analysis
682
+
683
+ **Allowed:** ${result.allowed.join(", ") || "None"}
684
+
685
+ **Blocked:** ${result.blocked.join(", ") || "None"}
686
+
687
+ **Recommendations:**
688
+ ${result.recommendations.map((r) => `- ${r}`).join("\n") || "- All good!"}`
689
+ }
690
+ ]
691
+ };
692
+ }
693
+ case "seo_generate_robots": {
694
+ const { siteUrl } = args;
695
+ const robotsTxt = analyzers.generateAIFriendlyRobotsTxt(siteUrl);
696
+ return {
697
+ content: [{ type: "text", text: `## AI-Friendly robots.txt
698
+
699
+ \`\`\`
700
+ ${robotsTxt}\`\`\`` }]
701
+ };
702
+ }
703
+ case "seo_core_web_vitals": {
704
+ const { url, html } = args;
705
+ const result = analyzers.analyzeCoreWebVitals(html, url);
706
+ return { content: [{ type: "text", text: formatCWVResult(result) }] };
707
+ }
708
+ case "seo_structured_data": {
709
+ const { url, html } = args;
710
+ const result = analyzers.analyzeStructuredData(html, url);
711
+ return { content: [{ type: "text", text: formatStructuredDataResult(result) }] };
712
+ }
713
+ case "seo_generate_schema": {
714
+ const { pageType, siteName, siteUrl, authorName, organizationName } = args;
715
+ const schema = analyzers.generateSchemaTemplate(pageType, { siteName, siteUrl, authorName, organizationName });
716
+ return {
717
+ content: [
718
+ {
719
+ type: "text",
720
+ text: `## ${pageType} Schema Template
721
+
722
+ \`\`\`json
723
+ ${schema}
724
+ \`\`\`
725
+
726
+ Replace {{placeholders}} with actual values.`
727
+ }
728
+ ]
729
+ };
730
+ }
731
+ case "seo_security_headers": {
732
+ const { url, headers } = args;
733
+ const result = analyzers.analyzeSecurityHeaders(headers, url);
734
+ return { content: [{ type: "text", text: formatSecurityResult(result) }] };
735
+ }
736
+ case "seo_generate_security_headers": {
737
+ const { siteUrl } = args;
738
+ const headers = analyzers.generateSecurityHeaders(siteUrl);
739
+ return {
740
+ content: [
741
+ {
742
+ type: "text",
743
+ text: `## Recommended Security Headers
744
+
745
+ ${Object.entries(headers).map(([k, v]) => `**${k}:**
746
+ \`${v}\``).join("\n\n")}`
747
+ }
748
+ ]
749
+ };
750
+ }
751
+ case "seo_images": {
752
+ const { url, html } = args;
753
+ const result = analyzers.analyzeImages(html, url);
754
+ return { content: [{ type: "text", text: formatImagesResult(result) }] };
755
+ }
756
+ case "seo_internal_links": {
757
+ const { url, html } = args;
758
+ const result = analyzers.analyzeInternalLinking(html, url);
759
+ return { content: [{ type: "text", text: formatInternalLinksResult(result) }] };
760
+ }
761
+ case "seo_mobile": {
762
+ const { url, html } = args;
763
+ const result = analyzers.analyzeMobileSEO(html, url);
764
+ return { content: [{ type: "text", text: formatMobileResult(result) }] };
765
+ }
766
+ case "seo_ai_crawlers": {
767
+ return { content: [{ type: "text", text: formatAICrawlersInfo() }] };
768
+ }
769
+ default:
770
+ return void 0;
771
+ }
772
+ }
773
+ function createAnalysisServer(options = {}) {
774
+ const server = new Server(
775
+ { name: "rankcli", version: "0.0.1" },
776
+ { capabilities: { tools: {} } }
777
+ );
778
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
779
+ return { tools: ANALYSIS_TOOLS };
780
+ });
781
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
782
+ const { name, arguments: args } = request.params;
783
+ try {
784
+ const result = await handleAnalysisTool(name, args, options);
785
+ if (result) return result;
786
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
787
+ } catch (error) {
788
+ return {
789
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
790
+ isError: true
791
+ };
792
+ }
793
+ });
794
+ return server;
795
+ }
796
+
797
+ export {
798
+ isPrivateAddress,
799
+ fetchHtml,
800
+ fetchRobotsTxt,
801
+ ANALYSIS_TOOLS,
802
+ handleAnalysisTool,
803
+ createAnalysisServer
804
+ };