All files / src/tools inspection.ts

85.36% Statements 35/41
61.53% Branches 16/26
100% Functions 8/8
87.17% Lines 34/39

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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165            2x 3x 3x       2x 12x                             2x 2x   1x                             2x   1x         12x                           2x 2x 2x   2x 1x           1x             12x                             1x 1x   1x       2x     2x 2x       1x             12x                                   1x 1x   1x       1x 1x 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 { SourceMapInfo, SourcesList, MappingsResult, SourceEntry } from "../types.js";
import { toTextResult, toErrorResult } from "../tool-result.js";
 
const formatSize = (bytes: number): string => {
  Iif (bytes < 1024) return `${bytes} B`;
  Eif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
 
export const registerInspectionTools = (server: McpServer, client: SrcmapClient): void => {
  server.registerTool(
    "sourcemap_info",
    {
      title: "Source Map Info",
      description:
        "Show metadata and statistics for a source map file. " +
        "Returns file name, source count, names, mappings count, lines, content size, and debug ID. " +
        "Use this to understand what a source map contains before doing lookups.",
      annotations: { readOnlyHint: true, openWorldHint: false },
 
      inputSchema: z.object({
        file: z.string().describe("Path to a source map file (.map)"),
      }),
    },
    async ({ file }) => {
      try {
        const info = (await client.info(file)) as unknown as SourceMapInfo;
 
        const lines = [
          "Source map info:",
          info.file ? `  File: ${info.file}` : null,
          `  Sources: ${info.sources}`,
          `  Names: ${info.names}`,
          `  Mappings: ${info.mappings}`,
          info.rangeMappings > 0 ? `  Range mappings: ${info.rangeMappings}` : null,
          `  Lines: ${info.lines}`,
          info.sourcesWithContent > 0
            ? `  Content: ${info.sourcesWithContent}/${info.sources} sources (${formatSize(info.totalContentSize)})`
            : null,
          `  File size: ${formatSize(info.fileSize)}`,
          info.debugId ? `  Debug ID: ${info.debugId}` : null,
        ].filter(Boolean);
 
        return toTextResult(lines.join("\n"), { info } as Record<string, unknown>);
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "sourcemap_validate",
    {
      title: "Validate Source Map",
      description:
        "Validate that a file is a valid source map. " +
        "Reports whether the map is valid and its basic structure (sources, names, mappings count).",
      annotations: { readOnlyHint: true, openWorldHint: false },
 
      inputSchema: z.object({
        file: z.string().describe("Path to a source map file (.map)"),
      }),
    },
    async ({ file }) => {
      try {
        const result = (await client.validate(file)) as Record<string, unknown>;
        const valid = result.valid as boolean;
 
        if (valid) {
          return toTextResult(
            `Valid source map v3: ${result.sources} sources, ${result.names} names, ${result.mappings} mappings across ${result.lines} lines`,
            result,
          );
        }
 
        return toTextResult(`Invalid source map: ${result.error}`, result);
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "sourcemap_sources",
    {
      title: "List Sources",
      description:
        "List all original source files embedded in a source map. " +
        "Shows each source path, whether it has embedded content, content size, and if it's on the ignore list. " +
        "Use this to understand what files a bundle was built from.",
      annotations: { readOnlyHint: true, openWorldHint: false },
 
      inputSchema: z.object({
        file: z.string().describe("Path to a source map file (.map)"),
      }),
    },
    async ({ file }) => {
      try {
        const result = (await client.sources(file)) as unknown as SourcesList;
 
        const lines = [
          `Sources (${result.total}, ${result.withContent} with content):`,
          "",
          ...result.sources.map((s: SourceEntry) => {
            const size = s.hasContent && s.contentSize !== null
              ? ` [${formatSize(s.contentSize)}]`
              : " [no content]";
            const ignored = s.ignored ? " (ignored)" : "";
            return `  ${s.index}: ${s.source}${size}${ignored}`;
          }),
        ];
 
        return toTextResult(lines.join("\n"), result as unknown as Record<string, unknown>);
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "sourcemap_mappings",
    {
      title: "List Mappings",
      description:
        "List mappings in a source map with pagination. " +
        "Shows generated and original positions for each mapping. " +
        "Optionally filter by source file. Use --limit and --offset for pagination.",
      annotations: { readOnlyHint: true, openWorldHint: false },
 
      inputSchema: z.object({
        file: z.string().describe("Path to a source map file (.map)"),
        source: z.string().optional().describe("Filter by source filename"),
        limit: z.number().min(1).max(1000).default(50).describe("Maximum number of mappings to return (default: 50)"),
        offset: z.number().min(0).default(0).describe("Skip first N mappings (default: 0)"),
      }),
    },
    async ({ file, source, limit, offset }) => {
      try {
        const result = (await client.mappings(file, { source, limit, offset })) as unknown as MappingsResult;
 
        const lines = [
          `Mappings (${result.total} total, showing ${result.offset}-${result.offset + result.mappings.length}):`,
          "",
          ...result.mappings.map((m) => {
            const src = m.source ?? "-";
            const name = m.name ? ` name=${m.name}` : "";
            return `  ${m.generatedLine}:${m.generatedColumn} → ${src}:${m.originalLine}:${m.originalColumn}${name}`;
          }),
        ];
 
        Iif (result.hasMore) {
          lines.push("", `  ... ${result.total - result.offset - result.mappings.length} more (use offset=${result.offset + result.mappings.length})`);
        }
 
        return toTextResult(lines.join("\n"), result as unknown as Record<string, unknown>);
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
};