Skip to main content
Glama

WebSee MCP Server

by 1AQuantum
source-intelligence-tools.js32.2 kB
/** * Source Intelligence Tools for WebSee MCP Server * * Provides advanced source mapping, debugging, and analysis capabilities * for frontend applications through the Model Context Protocol. * * @module source-intelligence-tools */ import { z } from "zod"; import { SourceMapResolver } from "../source-map-resolver.js"; import { BuildArtifactManager } from "../build-artifact-manager.js"; // ============================================================================ // Zod Schemas for Tool Input Validation // ============================================================================ /** * Schema for source_map_resolve tool * Resolves a minified location to its original source */ export const SourceMapResolveSchema = z.object({ url: z.string().url().describe("The URL of the minified JavaScript file"), line: z.number().int().positive().describe("Line number in minified file (1-indexed)"), column: z.number().int().min(0).describe("Column number in minified file (0-indexed)"), }); /** * Schema for source_map_get_content tool * Retrieves original source file content */ export const SourceMapGetContentSchema = z.object({ file: z.string().describe("Original source file path from source map"), startLine: z.number().int().positive().optional().describe("Start line number (1-indexed, optional)"), endLine: z.number().int().positive().optional().describe("End line number (1-indexed, optional)"), }); /** * Schema for source_trace_stack tool * Enhances a complete stack trace with source maps */ export const SourceTraceStackSchema = z.object({ stackTrace: z.string().describe("Full stack trace string to resolve"), }); /** * Schema for source_find_definition tool * Finds function/class definition in source code */ export const SourceFindDefinitionSchema = z.object({ functionName: z.string().describe("Name of the function or class to find"), file: z.string().optional().describe("Specific source file to search in (optional)"), }); /** * Schema for source_get_symbols tool * Lists exports, imports, and types from a source file */ export const SourceGetSymbolsSchema = z.object({ file: z.string().describe("Source file path to analyze"), }); /** * Schema for source_map_bundle tool * Maps a bundle file to all its source files */ export const SourceMapBundleSchema = z.object({ bundlePath: z.string().describe("Path or URL to the bundle file"), }); /** * Schema for source_coverage_map tool * Maps code coverage data to source files */ export const SourceCoverageMapSchema = z.object({ coverageData: z.record(z.any()).describe("V8 coverage data object"), }); // ============================================================================ // Tool Implementation Class // ============================================================================ /** * SourceIntelligenceTools provides MCP-compatible source intelligence tools * for debugging and analyzing frontend applications. */ export class SourceIntelligenceTools { sourceMapResolver; buildManager; initialized = false; constructor(cacheSize = 50, projectRoot) { this.sourceMapResolver = new SourceMapResolver(cacheSize); this.buildManager = new BuildArtifactManager(projectRoot); } /** * Initialize the tools with a Playwright page */ async initialize(page) { if (this.initialized) { return; } await this.sourceMapResolver.initialize(page); // Try to load build artifacts (non-blocking) try { await this.buildManager.loadBuildArtifacts(); } catch (error) { console.warn("Build artifacts not loaded:", error.message); } this.initialized = true; } // ========================================================================== // Tool 1: source_map_resolve // ========================================================================== /** * Resolve a minified location to its original source * * @param params - URL, line, and column of minified code * @returns Original source location with file, line, column, name, and content */ async sourceMapResolve(params) { this.ensureInitialized(); const location = await this.sourceMapResolver.resolveLocation(params.url, params.line, params.column); if (!location) { return null; } return { file: location.file, line: location.line, column: location.column, name: location.name, content: location.content, }; } // ========================================================================== // Tool 2: source_map_get_content // ========================================================================== /** * Get original source file content with optional line range * * @param params - File path and optional line range * @returns File content with metadata */ async sourceMapGetContent(params) { this.ensureInitialized(); // Get the source file content from the source map resolver const sourceContent = await this.getSourceFileContent(params.file); if (!sourceContent) { return null; } const lines = sourceContent.split("\n"); const totalLines = lines.length; let content = sourceContent; let range; if (params.startLine !== undefined || params.endLine !== undefined) { const start = Math.max(0, (params.startLine || 1) - 1); const end = Math.min(totalLines, params.endLine || totalLines); content = lines.slice(start, end).join("\n"); range = { start: start + 1, end }; } // Detect language from file extension const language = this.detectLanguage(params.file); return { file: params.file, content, language, totalLines, range, }; } // ========================================================================== // Tool 3: source_trace_stack // ========================================================================== /** * Enhance a full stack trace with source map resolution * * @param params - Stack trace string * @returns Original and resolved stack traces */ async sourceTraceStack(params) { this.ensureInitialized(); const originalLines = params.stackTrace.split("\n"); const resolvedLines = []; const frames = []; for (const line of originalLines) { // Parse stack frame const frame = this.parseStackFrame(line); if (!frame) { resolvedLines.push(line); continue; } // Resolve with source map const resolved = await this.sourceMapResolver.resolveLocation(frame.fileName, frame.lineNumber, frame.columnNumber); if (resolved) { const resolvedLine = this.formatStackFrame({ functionName: frame.functionName || resolved.name, fileName: resolved.file, lineNumber: resolved.line, columnNumber: resolved.column, source: resolved.content, }); resolvedLines.push(resolvedLine); frames.push({ ...frame, fileName: resolved.file, lineNumber: resolved.line, columnNumber: resolved.column, source: resolved.content, original: frame, resolved: true, }); } else { resolvedLines.push(line); frames.push({ ...frame, original: frame, resolved: false, }); } } return { original: originalLines, resolved: resolvedLines, frames, }; } // ========================================================================== // Tool 4: source_find_definition // ========================================================================== /** * Find function or class definition in source code * * @param params - Function/class name and optional file filter * @returns Definition location and code snippet */ async sourceFindDefinition(params) { this.ensureInitialized(); // Search through all loaded source files const result = await this.searchForDefinition(params.functionName, params.file); return result; } // ========================================================================== // Tool 5: source_get_symbols // ========================================================================== /** * List exports, imports, and types from a source file * * @param params - Source file path * @returns Symbols found in the file */ async sourceGetSymbols(params) { this.ensureInitialized(); const content = await this.getSourceFileContent(params.file); if (!content) { return null; } const exports = this.extractExports(content); const imports = this.extractImports(content); const types = this.extractTypes(content); return { file: params.file, exports, imports, types, }; } // ========================================================================== // Tool 6: source_map_bundle // ========================================================================== /** * Map a bundle file to all its source files * * @param params - Bundle file path or URL * @returns Bundle info with all source files */ async sourceMapBundle(params) { this.ensureInitialized(); const bundleInfo = await this.analyzeBundleSourceMap(params.bundlePath); return bundleInfo; } // ========================================================================== // Tool 7: source_coverage_map // ========================================================================== /** * Map code coverage data to original source files * * @param params - V8 coverage data * @returns Coverage mapped to source files */ async sourceCoverageMap(params) { this.ensureInitialized(); const coverage = await this.mapCoverageToSources(params.coverageData); return coverage; } // ========================================================================== // Helper Methods // ========================================================================== ensureInitialized() { if (!this.initialized) { throw new Error("SourceIntelligenceTools not initialized. Call initialize() first."); } } /** * Get source file content from cached source maps */ async getSourceFileContent(filePath) { return this.sourceMapResolver.getSourceContent(filePath); } /** * Detect programming language from file extension */ detectLanguage(filePath) { const ext = filePath.split(".").pop()?.toLowerCase(); const languageMap = { ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", vue: "vue", svelte: "svelte", py: "python", rb: "ruby", go: "go", rs: "rust", java: "java", cpp: "cpp", c: "c", cs: "csharp", }; return languageMap[ext || ""] || "unknown"; } /** * Parse a stack frame line into structured data */ parseStackFrame(line) { // Match common stack trace formats: // at functionName (file:line:column) // at file:line:column // functionName@file:line:column (Firefox) const chromeMatch = line.match(/at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?/); if (chromeMatch) { const [, functionName, fileName, lineStr, colStr] = chromeMatch; return { functionName: functionName?.trim(), fileName: fileName.trim(), lineNumber: parseInt(lineStr), columnNumber: parseInt(colStr), }; } const firefoxMatch = line.match(/(.+?)@(.+?):(\d+):(\d+)/); if (firefoxMatch) { const [, functionName, fileName, lineStr, colStr] = firefoxMatch; return { functionName: functionName.trim(), fileName: fileName.trim(), lineNumber: parseInt(lineStr), columnNumber: parseInt(colStr), }; } return null; } /** * Format a stack frame into a readable string */ formatStackFrame(frame) { const func = frame.functionName || "<anonymous>"; const location = `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`; let result = ` at ${func} (${location})`; if (frame.source) { const sourceLine = frame.source.split("\n")[0]?.trim(); if (sourceLine) { result += `\n > ${sourceLine}`; } } return result; } /** * Search for function/class definition across source files */ async searchForDefinition(name, fileFilter) { // Get all source files from loaded source maps const allFiles = this.sourceMapResolver.getAllSourceFiles(); // Filter files if requested const filesToSearch = fileFilter ? allFiles.filter(f => f.includes(fileFilter)) : allFiles; // Search patterns for definitions const patterns = [ new RegExp(`^\\s*export\\s+function\\s+${name}\\s*\\(`), new RegExp(`^\\s*function\\s+${name}\\s*\\(`), new RegExp(`^\\s*export\\s+const\\s+${name}\\s*=`), new RegExp(`^\\s*const\\s+${name}\\s*=`), new RegExp(`^\\s*export\\s+class\\s+${name}\\s+`), new RegExp(`^\\s*class\\s+${name}\\s+`), ]; for (const file of filesToSearch) { const content = this.sourceMapResolver.getSourceContent(file); if (!content) continue; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const pattern of patterns) { if (pattern.test(line)) { // Extract code context (this line + next 5 lines) const codeLines = lines.slice(i, Math.min(i + 6, lines.length)); const exports = this.extractExports(content); return { file, line: i + 1, column: 0, code: codeLines.join('\n'), exports: exports.map(e => e.name), }; } } } } return null; } /** * Extract export statements from source code */ extractExports(content) { const exports = []; const lines = content.split("\n"); lines.forEach((line, index) => { // Match various export patterns const patterns = [ /export\s+(function|class|const|let|var)\s+(\w+)/, /export\s+default\s+(function|class)?\s*(\w+)?/, /export\s*{\s*([^}]+)\s*}/, ]; for (const pattern of patterns) { const match = line.match(pattern); if (match) { if (match[3]) { // Named exports const names = match[3].split(",").map(n => n.trim().split(/\s+as\s+/)[0]); names.forEach(name => { exports.push({ name, type: "named", line: index + 1 }); }); } else if (match[2]) { exports.push({ name: match[2], type: match[1] === "default" ? "default" : match[1] || "unknown", line: index + 1 }); } } } }); return exports; } /** * Extract import statements from source code */ extractImports(content) { const imports = []; const lines = content.split("\n"); lines.forEach((line, index) => { const match = line.match(/import\s+(?:{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/); if (match) { const from = match[3]; if (match[1]) { // Named imports const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/)[0]); names.forEach(name => { imports.push({ name, from, line: index + 1 }); }); } else if (match[2]) { // Default import imports.push({ name: match[2], from, line: index + 1 }); } } }); return imports; } /** * Extract type definitions from source code */ extractTypes(content) { const types = []; const lines = content.split("\n"); lines.forEach((line, index) => { // Match TypeScript type definitions const patterns = [ { regex: /type\s+(\w+)\s*=/, kind: "type" }, { regex: /interface\s+(\w+)/, kind: "interface" }, { regex: /enum\s+(\w+)/, kind: "enum" }, ]; for (const { regex, kind } of patterns) { const match = line.match(regex); if (match) { types.push({ name: match[1], kind, line: index + 1 }); } } }); return types; } /** * Analyze bundle source map and extract all source files */ async analyzeBundleSourceMap(bundlePath) { // Get the source map consumer for this bundle const consumer = await this.sourceMapResolver.getSourceMapConsumer(bundlePath); if (!consumer) { return null; } // Get all source files - need to access via any cast as sources is not exposed in types const sources = consumer.sources; // Extract sample mappings (first 20 mappings) const mappings = []; // Sample some mappings let count = 0; consumer.eachMapping((mapping) => { if (count < 20 && mapping.source) { mappings.push({ source: mapping.source, generatedLine: mapping.generatedLine, generatedColumn: mapping.generatedColumn, originalLine: mapping.originalLine, originalColumn: mapping.originalColumn, }); count++; } }); return { bundle: bundlePath, sources: sources, mappings, size: sources.length, }; } /** * Map V8 coverage data to original source files */ async mapCoverageToSources(coverageData) { // V8 coverage data format: // { url: string, ranges: Array<{ start, end, count }> } const fileCoverage = new Map(); // Process coverage data if (Array.isArray(coverageData)) { for (const entry of coverageData) { const url = entry.url; const ranges = entry.functions?.[0]?.ranges || entry.ranges || []; for (const range of ranges) { // Convert byte offsets to line numbers (simplified) // In real implementation, you'd use the actual source text const startLine = Math.floor(range.startOffset / 80) + 1; // Rough approximation const endLine = Math.floor(range.endOffset / 80) + 1; const isCovered = range.count > 0; // Resolve to original source for (let line = startLine; line <= endLine; line++) { const resolved = await this.sourceMapResolver.resolveLocation(url, line, 0); if (resolved) { if (!fileCoverage.has(resolved.file)) { fileCoverage.set(resolved.file, { covered: new Set(), total: new Set() }); } const coverage = fileCoverage.get(resolved.file); coverage.total.add(resolved.line); if (isCovered) { coverage.covered.add(resolved.line); } } } } } } // Convert to output format const covered = []; const uncovered = []; let totalCovered = 0; let totalLines = 0; for (const [file, coverage] of Array.from(fileCoverage.entries())) { const coveredLines = Array.from(coverage.covered).sort((a, b) => a - b); const totalFileLines = coverage.total.size; const percentage = totalFileLines > 0 ? (coveredLines.length / totalFileLines) * 100 : 0; totalCovered += coveredLines.length; totalLines += totalFileLines; if (percentage > 0) { covered.push({ file, lines: coveredLines, percentage }); } const uncoveredLines = Array.from(coverage.total).filter((l) => !coverage.covered.has(l)).sort((a, b) => a - b); if (uncoveredLines.length > 0) { uncovered.push({ file, lines: uncoveredLines, percentage: 100 - percentage, }); } } const overallPercentage = totalLines > 0 ? (totalCovered / totalLines) * 100 : 0; return { covered, uncovered, percentage: overallPercentage, }; } /** * Cleanup and destroy resources */ async destroy() { if (this.sourceMapResolver) { await this.sourceMapResolver.destroy(); } if (this.buildManager) { this.buildManager.clear(); } this.initialized = false; } } // ============================================================================ // MCP Tool Definitions // ============================================================================ /** * MCP tool definitions for registration with the server */ export const SOURCE_INTELLIGENCE_TOOL_DEFINITIONS = [ { name: "source_map_resolve", description: "Resolve a minified JavaScript location to its original source file, line, and column using source maps", inputSchema: { type: "object", properties: { url: { type: "string", description: "The URL of the minified JavaScript file" }, line: { type: "number", description: "Line number in minified file (1-indexed)" }, column: { type: "number", description: "Column number in minified file (0-indexed)" }, }, required: ["url", "line", "column"], }, }, { name: "source_map_get_content", description: "Get the content of an original source file from source maps, optionally filtered by line range", inputSchema: { type: "object", properties: { file: { type: "string", description: "Original source file path from source map" }, startLine: { type: "number", description: "Start line number (1-indexed, optional)" }, endLine: { type: "number", description: "End line number (1-indexed, optional)" }, }, required: ["file"], }, }, { name: "source_trace_stack", description: "Enhance a complete error stack trace by resolving all frames to their original source locations using source maps", inputSchema: { type: "object", properties: { stackTrace: { type: "string", description: "Full stack trace string to resolve" }, }, required: ["stackTrace"], }, }, { name: "source_find_definition", description: "Find the definition of a function or class in the original source code", inputSchema: { type: "object", properties: { functionName: { type: "string", description: "Name of the function or class to find" }, file: { type: "string", description: "Specific source file to search in (optional)" }, }, required: ["functionName"], }, }, { name: "source_get_symbols", description: "List all exports, imports, and type definitions from an original source file", inputSchema: { type: "object", properties: { file: { type: "string", description: "Source file path to analyze" }, }, required: ["file"], }, }, { name: "source_map_bundle", description: "Map a JavaScript bundle file to all of its original source files and show the relationship", inputSchema: { type: "object", properties: { bundlePath: { type: "string", description: "Path or URL to the bundle file" }, }, required: ["bundlePath"], }, }, { name: "source_coverage_map", description: "Map V8 code coverage data from minified bundles back to original source files for accurate coverage reporting", inputSchema: { type: "object", properties: { coverageData: { type: "object", description: "V8 coverage data object" }, }, required: ["coverageData"], }, }, ]; // ============================================================================ // Exported Tool Handler Functions // ============================================================================ // Global instance to maintain state across tool calls let toolsInstance = null; /** * Get or create the SourceIntelligenceTools instance */ async function getToolsInstance(page) { if (!toolsInstance) { toolsInstance = new SourceIntelligenceTools(); await toolsInstance.initialize(page); } return toolsInstance; } /** * Handler for source_map_resolve tool */ export async function sourceMapResolve(page, params) { const tools = await getToolsInstance(page); const result = await tools.sourceMapResolve(params); if (!result) { return { success: false, message: `No source map found for ${params.url} at ${params.line}:${params.column}`, minified: { url: params.url, line: params.line, column: params.column, }, }; } return { success: true, minified: { url: params.url, line: params.line, column: params.column, }, original: result, }; } /** * Handler for source_map_get_content tool */ export async function sourceMapGetContent(page, params) { const tools = await getToolsInstance(page); const result = await tools.sourceMapGetContent(params); if (!result) { return { success: false, message: `Source file not found: ${params.file}`, file: params.file, }; } return { success: true, ...result, }; } /** * Handler for source_trace_stack tool */ export async function sourceTraceStack(page, params) { const tools = await getToolsInstance(page); const result = await tools.sourceTraceStack(params); return { success: true, ...result, summary: { totalFrames: result.frames.length, resolvedFrames: result.frames.filter((f) => f.resolved).length, unresolvedFrames: result.frames.filter((f) => !f.resolved).length, }, }; } /** * Handler for source_find_definition tool */ export async function sourceFindDefinition(page, params) { const tools = await getToolsInstance(page); const result = await tools.sourceFindDefinition(params); if (!result) { return { success: false, message: `Definition not found for: ${params.functionName}${params.file ? ` in ${params.file}` : ''}`, functionName: params.functionName, }; } return { success: true, functionName: params.functionName, definition: result, }; } /** * Handler for source_get_symbols tool */ export async function sourceGetSymbols(page, params) { const tools = await getToolsInstance(page); const result = await tools.sourceGetSymbols(params); if (!result) { return { success: false, message: `Source file not found: ${params.file}`, file: params.file, }; } return { success: true, ...result, summary: { totalExports: result.exports.length, totalImports: result.imports.length, totalTypes: result.types.length, }, }; } /** * Handler for source_map_bundle tool */ export async function sourceMapBundle(page, params) { const tools = await getToolsInstance(page); const result = await tools.sourceMapBundle(params); if (!result) { return { success: false, message: `No source map found for bundle: ${params.bundlePath}`, bundle: params.bundlePath, }; } return { success: true, ...result, summary: { totalSources: result.sources.length, sampleMappings: result.mappings.length, }, }; } /** * Handler for source_coverage_map tool */ export async function sourceCoverageMap(page, params) { const tools = await getToolsInstance(page); const result = await tools.sourceCoverageMap(params); return { success: true, ...result, summary: { totalFiles: result.covered.length + result.uncovered.length, coveredFiles: result.covered.length, uncoveredFiles: result.uncovered.length, overallPercentage: result.percentage.toFixed(2) + '%', }, }; } // ============================================================================ // Exports // ============================================================================ export default SOURCE_INTELLIGENCE_TOOL_DEFINITIONS; //# sourceMappingURL=source-intelligence-tools.js.map

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1AQuantum/websee-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server