All files / src/tools scans.ts

100% Statements 18/18
80% Branches 8/10
100% Functions 4/4
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98            2x 7x   2x 16x                             4x 4x   3x 1x           2x                           1x         16x                             5x 5x   4x 2x 2x                         2x         1x          
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as z from "zod/v4";
import type { IsAgentReadyClient } from "../client.js";
import { formatScanResult } from "../format.js";
import { toTextResult, toErrorResult } from "../tool-result.js";
 
const isScanCompleted = (result: { status: string }): boolean =>
  result.status === "completed" || result.status === "failed";
 
export const registerScanTools = (server: McpServer, client: IsAgentReadyClient): void => {
  server.registerTool(
    "scan_website",
    {
      title: "Scan Website for AI Agent Readiness",
      description:
        "Trigger a new scan of a website for AI agent readiness. The scan runs asynchronously and typically takes 15-30 seconds. If a recent scan exists (within 1 hour), returns cached results. Use get_scan_results to poll for completed results.",
      annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
      inputSchema: z.object({
        url: z
          .string()
          .min(1)
          .describe("The full URL to scan (must be http:// or https://)"),
      }),
    },
    async ({ url }) => {
      try {
        const result = await client.createScan(url);
 
        if (isScanCompleted(result)) {
          return toTextResult(
            `Recent scan found (cached):\n\n${formatScanResult(result as Parameters<typeof formatScanResult>[0])}`,
            { type: "cached_result", ...result },
          );
        }
 
        return toTextResult(
          [
            `Scan enqueued for ${result.domain}`,
            `Status: ${result.status}`,
            `ID: ${result.id}`,
            "message" in result ? `Message: ${result.message}` : "",
            "",
            `Poll with get_scan_results for domain "${result.domain}" to check when results are ready.`,
          ]
            .filter(Boolean)
            .join("\n"),
          { type: "enqueued", id: result.id, domain: result.domain, status: result.status },
        );
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "get_scan_results",
    {
      title: "Get Scan Results",
      description:
        "Get the latest scan results for a domain, including overall score, letter grade, per-category breakdowns, and individual checkpoint results with actionable recommendations. Returns a pending status if the scan is still in progress.",
      annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
      inputSchema: z.object({
        domain: z
          .string()
          .min(1)
          .describe("The domain to get scan results for (e.g., example.com)"),
      }),
    },
    async ({ domain }) => {
      try {
        const result = await client.getScanResults(domain);
 
        if (!isScanCompleted(result)) {
          const message = "message" in result ? String(result.message) : "";
          return toTextResult(
            [
              `Scan for ${result.domain} is ${result.status}.`,
              message,
              "",
              "Poll again in a few seconds to check for completion.",
            ]
              .filter(Boolean)
              .join("\n"),
            { type: "pending", id: result.id, domain: result.domain, status: result.status },
          );
        }
 
        return toTextResult(
          formatScanResult(result as Parameters<typeof formatScanResult>[0]),
          { type: "completed", ...result },
        );
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
};