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)
+
+[](https://nodejs.org)
+[](https://www.npmjs.com/package/source-map-parser-mcp)
+[](https://npmjs.com/package/source-map-parser-mcp)
+[](https://github.com/MasonChow/source-map-parser-mcp/actions)
+[](https://codecov.io/gh/MasonChow/source-map-parser-mcp)
+
+
+
+
+
+
+本项目实现了一个基于 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)
-
-[](https://nodejs.org)
-[](https://www.npmjs.com/package/source-map-parser-mcp)
-[](https://npmjs.com/package/source-map-parser-mcp)
-[](https://github.com/MasonChow/source-map-parser-mcp/actions)
-[](https://codecov.io/gh/MasonChow/source-map-parser-mcp)
-
-
-
-
-
-
-本项目实现了一个基于 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