diff --git a/README.md b/README.md index 103a4f3..70f9ff7 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,8 @@ npx -y source-map-parser-mcp@latest 1. **Stack Parsing**: Parse the corresponding source code location based on provided line number, column number, and Source Map file. 2. **Batch Processing**: Support parsing multiple stack traces simultaneously and return batch results. 3. **Context Extraction**: Extract context code for a specified number of lines to help developers better understand the environment where errors occur. +4. **Context Lookup**: Look up original source code context for specific compiled code positions. +5. **Source Unpacking**: Extract all source files and their content from source maps. ## MCP Service Tool Description @@ -170,6 +172,66 @@ Parse stack information by providing stack traces and Source Map addresses. } ``` +### `lookup_context` + +Look up original source code context for a specific line and column position in compiled/minified code. + +#### Request Example + +- line: The line number in the compiled code (1-based), required. +- column: The column number in the compiled code, required. +- sourceMapUrl: The URL of the source map file, required. +- contextLines: Number of context lines to include (default: 5), optional. + +```json +{ + "line": 42, + "column": 15, + "sourceMapUrl": "https://example.com/app.js.map", + "contextLines": 5 +} +``` + +#### Response Example + +```json +{ + "content": [ + { + "type": "text", + "text": "{\"filePath\":\"src/utils.js\",\"targetLine\":25,\"contextLines\":[{\"lineNumber\":23,\"content\":\"function calculateSum(a, b) {\"},{\"lineNumber\":24,\"content\":\" if (a < 0 || b < 0) {\"},{\"lineNumber\":25,\"content\":\" throw new Error('Negative numbers not allowed');\"},{\"lineNumber\":26,\"content\":\" }\"},{\"lineNumber\":27,\"content\":\" return a + b;\"}]}" + } + ] +} +``` + +### `unpack_sources` + +Extract all source files and their content from a source map. + +#### Request Example + +- sourceMapUrl: The URL of the source map file to unpack, required. + +```json +{ + "sourceMapUrl": "https://example.com/bundle.js.map" +} +``` + +#### Response Example + +```json +{ + "content": [ + { + "type": "text", + "text": "{\"sources\":{\"src/index.js\":\"import { utils } from './utils.js';\\nconsole.log('Hello World!');\",\"src/utils.js\":\"export const utils = { add: (a, b) => a + b };\"},\"sourceRoot\":\"/\",\"file\":\"bundle.js\",\"totalSources\":2}" + } + ] +} +``` + ### Parsing Result Description - `success`: Indicates whether the parsing was successful. diff --git a/README.zh-CN.md b/README.zh-CN.md index 0129438..ee83002 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,6 +1,42 @@ +# Source Map 解析器 + +🌐 **语言**: [English](README.md) | [简体中文](README.zh-CN.md) + +[![Node Version](https://img.shields.io/node/v/source-map-parser-mcp)](https://nodejs.org) +[![npm](https://img.shields.io/npm/v/source-map-parser-mcp.svg)](https://www.npmjs.com/package/source-map-parser-mcp) +[![Downloads](https://img.shields.io/npm/dm/source-map-parser-mcp)](https://npmjs.com/package/source-map-parser-mcp) +[![Build Status](https://github.com/MasonChow/source-map-parser-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/MasonChow/source-map-parser-mcp/actions) +[![codecov](https://codecov.io/gh/MasonChow/source-map-parser-mcp/graph/badge.svg)](https://codecov.io/gh/MasonChow/source-map-parser-mcp) +![](https://badge.mcpx.dev?type=server&features=tools 'MCP server with tools') + + + + + +本项目实现了一个基于 WebAssembly 的 Source Map 解析器,能够将 JavaScript 错误堆栈信息映射回源代码,并提取相关的上下文信息,开发者可以方便地将 JavaScript 错误堆栈信息映射回源代码,快速定位和修复问题。希望本项目的文档能帮助开发者更好地理解和使用该工具 + +## MCP 串接 + +> 注意: 需要 Node.js 20+ 版本支持 + +方式一:NPX 直接运行 + +```bash +npx -y source-map-parser-mcp@latest +``` + +方式二:下载构建产物 + +从 [GitHub Release](https://github.com/MasonChow/source-map-parser-mcp/releases) 页面下载对应版本的构建产物,然后运行: + +```bash +node dist/main.es.js +``` + ### 作为 npm 包在自定义 MCP 服务中使用 你可以在自己的 MCP 进程中嵌入本项目提供的工具,并按需定制行为。 + 安装: ```bash @@ -41,41 +77,6 @@ const parser = new Parser({ contextOffsetLine: 1 }); // await parser.batchParseStack([{ line, column, sourceMapUrl }]); ``` -# Source Map 解析器 - -🌐 **语言**: [English](README.md) | [简体中文](README.zh-CN.md) - -[![Node Version](https://img.shields.io/node/v/source-map-parser-mcp)](https://nodejs.org) -[![npm](https://img.shields.io/npm/v/source-map-parser-mcp.svg)](https://www.npmjs.com/package/source-map-parser-mcp) -[![Downloads](https://img.shields.io/npm/dm/source-map-parser-mcp)](https://npmjs.com/package/source-map-parser-mcp) -[![Build Status](https://github.com/MasonChow/source-map-parser-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/MasonChow/source-map-parser-mcp/actions) -[![codecov](https://codecov.io/gh/MasonChow/source-map-parser-mcp/graph/badge.svg)](https://codecov.io/gh/MasonChow/source-map-parser-mcp) -![](https://badge.mcpx.dev?type=server&features=tools 'MCP server with tools') - - - - - -本项目实现了一个基于 WebAssembly 的 Source Map 解析器,能够将 JavaScript 错误堆栈信息映射回源代码,并提取相关的上下文信息,开发者可以方便地将 JavaScript 错误堆栈信息映射回源代码,快速定位和修复问题。希望本项目的文档能帮助开发者更好地理解和使用该工具 - -## MCP 串接 - -> 注意: 需要 Node.js 20+ 版本支持 - -方式一:NPX 直接运行 - -```bash -npx -y source-map-parser-mcp@latest -``` - -方式二:下载构建产物 - -从 [GitHub Release](https://github.com/MasonChow/source-map-parser-mcp/releases) 页面下载对应版本的构建产物,然后运行: - -```bash -node dist/main.es.js -``` - ### 构建与类型声明 本项目同时提供 ESM 与 CJS 构建,并打包为单一的 TypeScript 声明文件: @@ -124,6 +125,8 @@ npx -y source-map-parser-mcp@latest 1. **堆栈解析**:根据提供的行号、列号和 Source Map 文件,解析出对应的源代码位置。 2. **批量解析**:**支持同时解析多个堆栈信息**,返回批量结果。 3. **上下文提取**:可以提取指定行数的上下文代码,帮助开发者更好地理解错误发生的环境。 +4. **上下文查找**:查找编译代码中特定位置对应的原始源代码上下文。 +5. **源文件解包**:从 source map 中提取所有源文件及其内容。 ## MCP 服务工具说明 @@ -166,6 +169,66 @@ npx -y source-map-parser-mcp@latest } ``` +### `lookup_context` + +查找编译/压缩代码中特定行列位置对应的原始源代码上下文。 + +#### 请求示例 + +- line: 编译代码中的行号(从1开始),必填。 +- column: 编译代码中的列号,必填。 +- sourceMapUrl: Source Map 文件的 URL,必填。 +- contextLines: 包含的上下文行数(默认:5),可选。 + +```json +{ + "line": 42, + "column": 15, + "sourceMapUrl": "https://example.com/app.js.map", + "contextLines": 5 +} +``` + +#### 响应示例 + +```json +{ + "content": [ + { + "type": "text", + "text": "{\"filePath\":\"src/utils.js\",\"targetLine\":25,\"contextLines\":[{\"lineNumber\":23,\"content\":\"function calculateSum(a, b) {\"},{\"lineNumber\":24,\"content\":\" if (a < 0 || b < 0) {\"},{\"lineNumber\":25,\"content\":\" throw new Error('Negative numbers not allowed');\"},{\"lineNumber\":26,\"content\":\" }\"},{\"lineNumber\":27,\"content\":\" return a + b;\"}]}" + } + ] +} +``` + +### `unpack_sources` + +从 source map 中提取所有源文件及其内容。 + +#### 请求示例 + +- sourceMapUrl: 要解包的 Source Map 文件 URL,必填。 + +```json +{ + "sourceMapUrl": "https://example.com/bundle.js.map" +} +``` + +#### 响应示例 + +```json +{ + "content": [ + { + "type": "text", + "text": "{\"sources\":{\"src/index.js\":\"import { utils } from './utils.js';\\nconsole.log('Hello World!');\",\"src/utils.js\":\"export const utils = { add: (a, b) => a + b };\"},\"sourceRoot\":\"/\",\"file\":\"bundle.js\",\"totalSources\":2}" + } + ] +} +``` + ### 4. 解析结果说明 - `success`:表示解析是否成功。 diff --git a/package.json b/package.json index 71fa88b..4ed75ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "source-map-parser-mcp", - "version": "1.5.0", + "version": "1.6.0", "type": "module", "description": "Parse JavaScript error stack traces back to original source code using source maps", "mcpName": "io.github.MasonChow/source-map-parser-mcp", diff --git a/src/index.ts b/src/index.ts index 29d903e..0141cf7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export { registerTools } from './tools.js'; -export type { ToolsRegistryOptions } from './tools.js'; +export type { ToolsRegistryOptions, ToolName } from './tools.js'; export { default as Parser } from './parser.js'; \ No newline at end of file diff --git a/src/parser.ts b/src/parser.ts index 8fc69a0..21f645b 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -381,5 +381,73 @@ class Parser { }); } } + + /** + * Looks up source code context for a specific line and column position + * @param line - 1-based line number in the compiled code + * @param column - Column number in the compiled code + * @param sourceMapUrl - URL of the source map file + * @param contextLines - Number of context lines to include (default: 5) + * @returns Context snippet or null if not found + */ + public async lookupContext(line: number, column: number, sourceMapUrl: string, contextLines: number = 5) { + validateUrl(sourceMapUrl); + await this.init(); + + try { + const sourceMapContent = await this.fetchSourceMapContent(sourceMapUrl); + // Use the high-level lookup_context function directly + const result = sourceMapParser.lookup_context(sourceMapContent, line, column, contextLines); + + return result; + } catch (error) { + throw new Error("lookup context error: " + (error instanceof Error ? error.message : error), { + cause: error, + }); + } + } + + /** + * Unpacks all source files and their content from a source map + * @param sourceMapUrl - URL of the source map file + * @returns Object containing all source files with their content + */ + public async unpackSources(sourceMapUrl: string) { + validateUrl(sourceMapUrl); + + try { + const sourceMapContent = await this.fetchSourceMapContent(sourceMapUrl); + const sourceMap = JSON.parse(sourceMapContent); + + if (!sourceMap.sources || !Array.isArray(sourceMap.sources)) { + throw new Error("Invalid source map: missing or invalid sources array"); + } + + const result: Record = {}; + + // Extract sources from sourcesContent if available + if (sourceMap.sourcesContent && Array.isArray(sourceMap.sourcesContent)) { + sourceMap.sources.forEach((source: string, index: number) => { + result[source] = sourceMap.sourcesContent[index] || null; + }); + } else { + // If no sourcesContent, list sources with null content + sourceMap.sources.forEach((source: string) => { + result[source] = null; + }); + } + + return { + sources: result, + sourceRoot: sourceMap.sourceRoot || null, + file: sourceMap.file || null, + totalSources: sourceMap.sources.length + }; + } catch (error) { + throw new Error("unpack sources error: " + (error instanceof Error ? error.message : error), { + cause: error, + }); + } + } } export default Parser; \ No newline at end of file diff --git a/src/tools.ts b/src/tools.ts index d40410a..c014c8e 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -2,30 +2,179 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type Parser from './parser.js'; +/** + * Union type representing all available tool names in the source map parser MCP server. + * + * @public + */ +export type ToolName = "parse_stack" | "lookup_context" | "unpack_sources"; + +/** + * Configuration options for filtering which tools should be registered. + * + * @public + */ +export interface ToolFilterOptions { + /** List of tools that are allowed to be registered. If provided, only these tools will be registered. */ + allowList?: ToolName[]; + /** List of tools that should not be registered. These tools will be excluded from registration. */ + blockList?: ToolName[]; +} + +/** + * Configuration options for the tools registry. + * + * @public + */ export interface ToolsRegistryOptions { + /** Number of context lines to include around the target location in source code. Defaults to 1. */ contextOffsetLine?: number; + /** Filter options to control which tools are registered. */ + toolFilter?: ToolFilterOptions; } +/** + * Result type for batch parsing operations. + * Each element represents either a successful parse with token data or a failed parse with error information. + * + * @internal + */ type BatchParseResult = Array<{ + /** Indicates the parsing operation failed */ success: false; + /** Error that occurred during parsing */ error: Error; } | { + /** Indicates the parsing operation succeeded */ success: true; + /** Parsed token containing source map information */ token: { + /** Line number in the original source */ line: number; + /** Column number in the original source */ column: number; + /** Array of source code lines with context */ sourceCode: Array<{ + /** Line number in the source file */ line: number; + /** Whether this line is part of the error stack trace */ isStackLine: boolean; + /** Raw source code content */ raw: string; }>; + /** Source file path */ src: string; }; }>; +/** + * Determines whether a tool should be registered based on the provided filter options. + * + * @param toolName - The name of the tool to check + * @param filter - Filter options containing allowList and/or blockList + * @returns `true` if the tool should be registered, `false` otherwise + * + * @remarks + * - If no filter is provided, all tools are registered + * - If allowList is provided and non-empty, only tools in the allowList are registered + * - If blockList is provided and non-empty, tools in the blockList are excluded + * - allowList takes precedence over blockList + * + * @internal + */ +function shouldRegisterTool(toolName: ToolName, filter?: ToolFilterOptions): boolean { + if (!filter) { + return true; + } + + if (filter.allowList && filter.allowList.length > 0) { + return filter.allowList.includes(toolName); + } + + if (filter.blockList && filter.blockList.length > 0) { + return !filter.blockList.includes(toolName); + } + + return true; +} + +/** + * Interface defining the structure for tool configuration. + * + * @internal + */ +interface ToolDefinition { + /** The unique name identifier for the tool */ + name: ToolName; + /** Markdown-formatted description of the tool's functionality and usage */ + description: string; + /** Zod schema object defining the tool's parameter validation */ + schema: Record; + /** + * Function that handles the tool's execution logic + * + * @param params - Parameters passed to the tool (validated against schema) + * @param getParser - Function to get or create a Parser instance + * @returns Promise resolving to the tool's response + */ + handler: (params: any, getParser: () => Promise) => Promise; +} + +/** + * Registers all available source map parsing tools with the MCP server. + * + * This function uses a declarative approach to register tools, automatically handling + * filtering and registration logic for all defined tools. + * + * @param server - The MCP server instance to register tools with + * @param options - Configuration options for tool registration + * + * @remarks + * The function creates a lazy-loaded parser instance that is shared across all tools + * for efficiency. Tools are registered based on the provided filter options. + * + * Available tools: + * - `parse_stack`: Maps error stack traces to original source locations + * - `lookup_context`: Retrieves source code context for specific positions + * - `unpack_sources`: Extracts all source files from a source map + * + * @example + * ```typescript + * import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + * import { registerTools } from './tools.js'; + * + * const server = new McpServer({ name: 'source-map-parser', version: '1.0.0' }); + * + * // Register all tools + * registerTools(server); + * + * // Register only specific tools + * registerTools(server, { + * toolFilter: { + * allowList: ['parse_stack', 'lookup_context'] + * } + * }); + * + * // Exclude specific tools + * registerTools(server, { + * toolFilter: { + * blockList: ['unpack_sources'] + * } + * }); + * ``` + * + * @public + */ export function registerTools(server: McpServer, options: ToolsRegistryOptions = {}) { let parserInstance: Parser | null = null; + /** + * Lazy-loaded parser instance getter. + * Creates and configures a parser instance on first access, then reuses it. + * + * @returns Promise resolving to the configured Parser instance + * @internal + */ const getParser = async (): Promise => { if (!parserInstance) { const { default: ParserClass } = await import('./parser.js'); @@ -36,7 +185,16 @@ export function registerTools(server: McpServer, options: ToolsRegistryOptions = return parserInstance; }; - server.tool("parse_stack", ` + /** + * Centralized tool definitions using declarative configuration. + * Each tool is defined with its name, description, schema, and handler function. + * + * @internal + */ + const toolDefinitions: ToolDefinition[] = [ + { + name: "parse_stack", + description: ` # Parse Error Stack Trace This tool allows you to parse error stack traces by providing the following: @@ -57,47 +215,141 @@ export function registerTools(server: McpServer, options: ToolsRegistryOptions = ## Returns: - A JSON object containing the parsed stack trace information, including the mapped source code location and context lines. - If parsing fails, an error message will be returned for the corresponding stack trace. -`, { - stacks: z.array( - z.object({ - line: z.number({ - description: "The line number in the stack trace.", - }), - column: z.number({ - description: "The column number in the stack trace.", - }), - sourceMapUrl: z.string({ - description: "The URL of the source map file corresponding to the stack trace.", - }), - }) - ) -}, async ({ stacks }) => { - const parser = await getParser(); - const parserRes: BatchParseResult = await parser.batchParseStack(stacks); - - if (parserRes.length === 0) { - return { - isError: true, - content: [{ type: "text", text: "No data could be parsed from the provided stack traces." }], - } - } +`, + schema: { + stacks: z.array( + z.object({ + line: z.number({ + description: "The line number in the stack trace.", + }), + column: z.number({ + description: "The column number in the stack trace.", + }), + sourceMapUrl: z.string({ + description: "The URL of the source map file corresponding to the stack trace.", + }), + }) + ) + }, + handler: async ({ stacks }, getParser) => { + const parser = await getParser(); + const parserRes: BatchParseResult = await parser.batchParseStack(stacks); - return { - content: [{ - type: "text", text: JSON.stringify(parserRes.map((e) => { - if (e.success) { - return e; - } else { - // Sanitize error messages to avoid exposing internal details - const sanitizedMessage = e.error.message.replace(/[^\w\s.:\-]/g, ''); + if (parserRes.length === 0) { return { - success: false, - msg: sanitizedMessage, + isError: true, + content: [{ type: "text", text: "No data could be parsed from the provided stack traces." }], } } - })) - }], - } -}); + return { + content: [{ + type: "text", text: JSON.stringify(parserRes.map((e) => { + if (e.success) { + return e; + } else { + // Sanitize error messages to avoid exposing internal details + const sanitizedMessage = e.error.message.replace(/[^\w\s.:\-]/g, ''); + return { + success: false, + msg: sanitizedMessage, + } + } + })) + }], + } + } + }, + { + name: "lookup_context", + description: ` + # Lookup Source Code Context + + This tool looks up original source code context for a specific line and column position in compiled/minified code. + + ## Parameters: + - **line**: The line number in the compiled code (1-based) + - **column**: The column number in the compiled code + - **sourceMapUrl**: The URL of the source map file + - **contextLines** (optional): Number of context lines to include before and after the target line (default: 5) + + ## Returns: + - A JSON object containing the source code context snippet with file path, target line info, and surrounding context lines + - Returns null if the position cannot be mapped +`, + schema: { + line: z.number({ + description: "The line number in the compiled code (1-based)", + }), + column: z.number({ + description: "The column number in the compiled code", + }), + sourceMapUrl: z.string({ + description: "The URL of the source map file", + }), + contextLines: z.number({ + description: "Number of context lines to include (default: 5)", + }).optional().default(5), + }, + handler: async ({ line, column, sourceMapUrl, contextLines }, getParser) => { + const parser = await getParser(); + const result = await parser.lookupContext(line, column, sourceMapUrl, contextLines); + + return { + content: [{ + type: "text", + text: JSON.stringify(result, null, 2) + }], + } + } + }, + { + name: "unpack_sources", + description: ` + # Unpack Source Map Sources + + This tool extracts all source files and their content from a source map. + + ## Parameters: + - **sourceMapUrl**: The URL of the source map file to unpack + + ## Returns: + - A JSON object containing: + - **sources**: Object with source file paths as keys and their content as values + - **sourceRoot**: The source root path from the source map + - **file**: The original file name + - **totalSources**: Total number of source files found +`, + schema: { + sourceMapUrl: z.string({ + description: "The URL of the source map file to unpack", + }), + }, + handler: async ({ sourceMapUrl }, getParser) => { + const parser = await getParser(); + const result = await parser.unpackSources(sourceMapUrl); + + return { + content: [{ + type: "text", + text: JSON.stringify(result, null, 2) + }], + } + } + } + ]; + + /** + * Automatic tool registration loop. + * Iterates through all tool definitions and registers those that pass the filter. + * + * @internal + */ + toolDefinitions.forEach(tool => { + if (shouldRegisterTool(tool.name, options.toolFilter)) { + server.tool(tool.name, tool.description, tool.schema, async (params) => { + return tool.handler(params, getParser); + }); + } + }); } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2ed87fd..e47367c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,6 @@ "dist", ], "include": [ - "src", - "types", + "src" ] } \ No newline at end of file diff --git a/types/source_map_parser_node.d.ts b/types/source_map_parser_node.d.ts deleted file mode 100644 index 29401b4..0000000 --- a/types/source_map_parser_node.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -declare module 'source_map_parser_node' { - export function init(): Promise; - export function generate_token_by_single_stack( - line: number, - column: number, - sourceMap: string, - contextOffset?: number - ): string; - - // Add other functions from the package as needed - export function lookup_token( - sourceMap: string, - line: number, - column: number - ): string; - - export function lookup_token_with_context( - sourceMap: string, - line: number, - column: number, - contextLines: number - ): string; - - export function map_error_stack( - sourceMap: string, - errorStack: string, - contextLines?: number - ): string; -} \ No newline at end of file