All files / src/tools fetch.ts

92.85% Statements 26/28
70% Branches 7/10
100% Functions 4/4
92.3% Lines 24/26

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            2x 5x 4x       2x 10x                                 3x 3x   2x       2x 1x   1x     2x   1x         10x                                 1x 1x   1x       1x 1x 1x 2x       1x 1x     1x              
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as z from "zod/v4";
import type { SrcmapClient } from "../srcmap-client.js";
import type { FetchResult, ExtractResult } from "../types.js";
import { toTextResult, toErrorResult } from "../tool-result.js";
 
const formatSize = (bytes: number): string => {
  if (bytes < 1024) return `${bytes} B`;
  Eif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
 
export const registerFetchTools = (server: McpServer, client: SrcmapClient): void => {
  server.registerTool(
    "sourcemap_fetch",
    {
      title: "Fetch Bundle & Source Map",
      description:
        "Download a JavaScript or CSS bundle and its source map from a URL. " +
        "Automatically resolves the sourceMappingURL reference (inline data URIs, external URLs, " +
        "and conventional .map suffix fallback). Saves both files to the output directory. " +
        "Use this as the first step when debugging a production website.",
      annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
 
      inputSchema: z.object({
        url: z.string().describe("URL of the JavaScript or CSS file to fetch"),
        outputDir: z.string().default("/tmp/srcmap").describe("Directory to save the downloaded files (default: /tmp/srcmap)"),
      }),
    },
    async ({ url, outputDir }) => {
      try {
        const result = (await client.fetch(url, outputDir)) as unknown as FetchResult;
 
        const parts = [
          `Fetched bundle: ${result.bundle.file} (${formatSize(result.bundle.size)})`,
        ];
 
        if (result.sourceMap) {
          parts.push(`Source map: ${result.sourceMap.file} (${formatSize(result.sourceMap.size)})`);
        } else {
          parts.push("No source map found for this bundle.");
        }
 
        return toTextResult(parts.join("\n"), result as unknown as Record<string, unknown>);
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "sourcemap_extract_sources",
    {
      title: "Extract Original Sources",
      description:
        "Extract all embedded original source files from a source map to disk. " +
        "Writes each sourcesContent entry as a file, preserving the directory structure. " +
        "Handles webpack://, file://, and relative path prefixes. " +
        "Use this after fetching a source map to get the full original source tree.",
      annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
 
      inputSchema: z.object({
        file: z.string().describe("Path to a source map file (.map)"),
        outputDir: z.string().default("/tmp/srcmap-sources").describe("Directory to extract source files to (default: /tmp/srcmap-sources)"),
      }),
    },
    async ({ file, outputDir }) => {
      try {
        const result = (await client.sourcesExtract(file, outputDir)) as unknown as ExtractResult;
 
        const parts = [
          `Extracted ${result.extracted.length}/${result.total} sources to ${outputDir}`,
        ];
 
        Eif (result.extracted.length > 0) {
          parts.push("");
          for (const entry of result.extracted) {
            parts.push(`  ${entry.source} [${formatSize(entry.size)}]`);
          }
        }
 
        Eif (result.skipped.length > 0) {
          parts.push("", `Skipped ${result.skipped.length} sources without content`);
        }
 
        return toTextResult(parts.join("\n"), result as unknown as Record<string, unknown>);
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
};