@rankcli/mcp-server 0.0.2 → 0.0.4

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.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/dist/index.js +156 -1
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rankcli/mcp-server
2
2
 
3
- Free, local SEO + GEO (AI-search-citation) analysis as an MCP tool. No signup, no API key, nothing sent to RankCLI's servers — it runs entirely inside your MCP host (Claude Code, Claude Desktop, Cursor, etc.) against HTML you already have or that your host fetches for you.
3
+ Free, local SEO + GEO (AI-search-citation) analysis as an MCP tool. No signup, no API key, nothing sent to RankCLI's servers — it runs entirely inside your MCP host (Claude Code, Claude Desktop, Cursor, etc.) against HTML you already have or that your host fetches for you. The one exception is `rankcli_connect`, an explicit opt-in tool that links this server to a RankCLI account.
4
4
 
5
5
  ```bash
6
6
  npx @rankcli/mcp-server
@@ -36,6 +36,12 @@ npx @rankcli/mcp-server
36
36
  | `seo_internal_links` | Anchor text quality, orphan-page risk |
37
37
  | `seo_mobile` | Viewport, touch targets, PWA readiness |
38
38
  | `seo_ai_crawlers` | Reference list of known AI crawler user agents |
39
+ | `rankcli_connect` | Link this server to a RankCLI account for GitHub auto-fix PRs, scheduled monitoring, and a dashboard with audit history. Opens a browser tab to approve; the only tool here that contacts rankcli.dev |
40
+ | `rankcli_disconnect` | Unlink this server from your RankCLI account (local only — doesn't touch the account itself) |
41
+
42
+ ## Connecting your account
43
+
44
+ Ask Claude to "connect my RankCLI account," or call `rankcli_connect` directly. It opens rankcli.dev in your browser to approve (or asks for permission first, on MCP hosts that support that inline) - once approved, this server and the `rankcli` CLI on the same machine both use the connection, with nothing further to configure. It works the same on the free plan: connect a BYOK AI provider key at [rankcli.dev/account?tab=ai-provider](https://rankcli.dev/account?tab=ai-provider) and auto-fix PRs run on your own key at no cost, including several providers with genuinely free tiers.
39
45
 
40
46
  ## Example
41
47
 
package/dist/index.js CHANGED
@@ -7,7 +7,10 @@ import {
7
7
  CallToolRequestSchema,
8
8
  ListToolsRequestSchema
9
9
  } from "@modelcontextprotocol/sdk/types.js";
10
+ import Conf from "conf";
10
11
  import { analyzers } from "@rankcli/agent-runtime";
12
+ var SUPABASE_URL = process.env.RANKCLI_SUPABASE_URL || "https://bspljbxwbjiqueeyzyat.supabase.co";
13
+ var localConfig = new Conf({ projectName: "rankcli" });
11
14
  var TOOLS = [
12
15
  {
13
16
  name: "seo_analyze",
@@ -269,6 +272,22 @@ Critical for visibility in ChatGPT, Perplexity, Claude, and Gemini responses.`,
269
272
  type: "object",
270
273
  properties: {}
271
274
  }
275
+ },
276
+ {
277
+ name: "rankcli_connect",
278
+ 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.`,
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {}
282
+ }
283
+ },
284
+ {
285
+ name: "rankcli_disconnect",
286
+ description: `Unlink this AI assistant from its RankCLI account. Only affects this local connection - does not delete the RankCLI account or revoke other API keys.`,
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {}
290
+ }
272
291
  }
273
292
  ];
274
293
  var server = new Server(
@@ -442,6 +461,27 @@ ${Object.entries(headers).map(([k, v]) => `**${k}:**
442
461
  ]
443
462
  };
444
463
  }
464
+ case "rankcli_connect": {
465
+ const result = await connectToRankCLI();
466
+ return {
467
+ content: [{ type: "text", text: result.text }],
468
+ ...result.isError ? { isError: true } : {}
469
+ };
470
+ }
471
+ case "rankcli_disconnect": {
472
+ const wasConnected = !!localConfig.get("apiKey");
473
+ localConfig.delete("apiKey");
474
+ localConfig.delete("apiKeyName");
475
+ localConfig.delete("email");
476
+ return {
477
+ content: [
478
+ {
479
+ type: "text",
480
+ text: wasConnected ? "Disconnected. This assistant no longer has access to your RankCLI dashboard." : "Not connected - nothing to do."
481
+ }
482
+ ]
483
+ };
484
+ }
445
485
  default:
446
486
  return {
447
487
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
@@ -462,7 +502,122 @@ ${Object.entries(headers).map(([k, v]) => `**${k}:**
462
502
  });
463
503
  var BRIDGE_FOOTER = `
464
504
  ---
465
- *This ran locally with no signup. For GitHub auto-fix PRs, scheduled monitoring, and a dashboard with history across audits, create a free account at [rankcli.dev](https://rankcli.dev).*`;
505
+ *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).*`;
506
+ async function pollConnectStatus(connectId, timeoutMs) {
507
+ const deadline = Date.now() + timeoutMs;
508
+ while (Date.now() < deadline) {
509
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
510
+ try {
511
+ const res = await fetch(`${SUPABASE_URL}/functions/v1/mcp-connect-status`, {
512
+ method: "POST",
513
+ headers: { "Content-Type": "application/json" },
514
+ body: JSON.stringify({ connectId })
515
+ });
516
+ const data = await res.json();
517
+ if (data.status === "claimed") {
518
+ return { email: data.email, apiKey: data.apiKey, apiKeyName: data.apiKeyName };
519
+ }
520
+ if (data.status === "expired" || data.status === "not_found") {
521
+ return null;
522
+ }
523
+ } catch {
524
+ }
525
+ }
526
+ return null;
527
+ }
528
+ function backgroundAwaitConnect(connectId) {
529
+ pollConnectStatus(connectId, 5 * 6e4).then(async (claimed) => {
530
+ if (claimed) {
531
+ localConfig.set("apiKey", claimed.apiKey);
532
+ localConfig.set("apiKeyName", claimed.apiKeyName);
533
+ localConfig.set("email", claimed.email);
534
+ if (localConfig.get("pendingConnectId") === connectId) clearPendingConnect();
535
+ }
536
+ await server.createElicitationCompletionNotifier(connectId)().catch(() => {
537
+ });
538
+ }).catch((err) => {
539
+ console.error("rankcli_connect: background poll failed:", err);
540
+ });
541
+ }
542
+ function clearPendingConnect() {
543
+ localConfig.delete("pendingConnectId");
544
+ localConfig.delete("pendingConnectUrl");
545
+ localConfig.delete("pendingConnectExpiresAt");
546
+ }
547
+ async function connectToRankCLI() {
548
+ const existingKey = localConfig.get("apiKey");
549
+ if (existingKey) {
550
+ const email = localConfig.get("email") || "your account";
551
+ return { text: `Already connected as ${email}. Run rankcli_disconnect first to switch accounts.` };
552
+ }
553
+ const pendingId = localConfig.get("pendingConnectId");
554
+ const pendingUrl = localConfig.get("pendingConnectUrl");
555
+ const pendingExpiresAt = localConfig.get("pendingConnectExpiresAt");
556
+ if (pendingId && pendingUrl && pendingExpiresAt) {
557
+ if (Date.now() >= pendingExpiresAt) {
558
+ clearPendingConnect();
559
+ } else {
560
+ const claimed = await pollConnectStatus(pendingId, 3e3);
561
+ if (claimed) {
562
+ localConfig.set("apiKey", claimed.apiKey);
563
+ localConfig.set("apiKeyName", claimed.apiKeyName);
564
+ localConfig.set("email", claimed.email);
565
+ clearPendingConnect();
566
+ return { text: `Connected as ${claimed.email}! Your dashboard, GitHub auto-fix PRs, and audit history are now linked to this AI assistant.` };
567
+ }
568
+ const minutesLeft = Math.max(1, Math.round((pendingExpiresAt - Date.now()) / 6e4));
569
+ return {
570
+ text: `Still waiting for approval. Open this link if you haven't yet, then run rankcli_connect again:
571
+
572
+ ${pendingUrl}
573
+
574
+ Expires in ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"}.`
575
+ };
576
+ }
577
+ }
578
+ let startData;
579
+ try {
580
+ const startRes = await fetch(`${SUPABASE_URL}/functions/v1/mcp-connect-start`, { method: "POST" });
581
+ if (!startRes.ok) throw new Error(`status ${startRes.status}`);
582
+ startData = await startRes.json();
583
+ } catch (err) {
584
+ return {
585
+ text: `Could not reach rankcli.dev to start the connection (${err instanceof Error ? err.message : "network error"}). Try again in a moment.`,
586
+ isError: true
587
+ };
588
+ }
589
+ const { connectId, url, expiresInSeconds } = startData;
590
+ const expiresMinutes = Math.round(expiresInSeconds / 60);
591
+ localConfig.set("pendingConnectId", connectId);
592
+ localConfig.set("pendingConnectUrl", url);
593
+ localConfig.set("pendingConnectExpiresAt", Date.now() + expiresInSeconds * 1e3);
594
+ const supportsUrlElicitation = !!server.getClientCapabilities()?.elicitation?.url;
595
+ if (supportsUrlElicitation) {
596
+ try {
597
+ const result = await server.elicitInput({
598
+ mode: "url",
599
+ message: "Connect your RankCLI account to enable GitHub auto-fix PRs, scheduled monitoring, and a dashboard with audit history.",
600
+ url,
601
+ elicitationId: connectId
602
+ });
603
+ if (result.action === "accept") {
604
+ backgroundAwaitConnect(connectId);
605
+ return { text: "Opening rankcli.dev to connect your account - I'll let you know as soon as it's done." };
606
+ }
607
+ clearPendingConnect();
608
+ return { text: "Connection cancelled." };
609
+ } catch (err) {
610
+ console.error("rankcli_connect: elicitation failed, falling back to link:", err);
611
+ }
612
+ }
613
+ return {
614
+ text: `Open this link to connect your RankCLI account:
615
+
616
+ ${url}
617
+
618
+ Then run rankcli_connect again to finish (link expires in ${expiresMinutes} minutes).`
619
+ };
620
+ }
466
621
  function formatComprehensiveResult(result) {
467
622
  const criticalIssues = result.allIssues.filter((i) => i.severity === "critical");
468
623
  const warnings = result.allIssues.filter((i) => i.severity === "warning");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankcli/mcp-server",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "MCP (Model Context Protocol) server for RankCLI SEO analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,8 +26,9 @@
26
26
  "author": "Integrallis",
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.0.0",
30
- "@rankcli/agent-runtime": "^0.0.17"
29
+ "@modelcontextprotocol/sdk": "^1.30.0",
30
+ "@rankcli/agent-runtime": "^0.0.17",
31
+ "conf": "^12.0.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "tsup": "^8.0.0",