UNPKG

@ariadnejs/mcp

Version:

Model Context Protocol server for Ariadne - Expose code intelligence capabilities to AI agents

660 lines 28.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getSymbolContextSchema = void 0; exports.getSymbolContext = getSymbolContext; const zod_1 = require("zod"); // Request schema for the MCP tool exports.getSymbolContextSchema = zod_1.z.object({ symbol: zod_1.z.string().describe("Name of the symbol to look up (function, class, variable, etc.)"), searchScope: zod_1.z.enum(["file", "project", "dependencies"]).optional().default("project").describe("Scope to search within"), includeTests: zod_1.z.boolean().optional().default(false).describe("Whether to include test file references") }); /** * Implementation of get_symbol_context MCP tool * Finds a symbol by name and returns comprehensive context */ async function getSymbolContext(project, request) { const { symbol, searchScope, includeTests } = request; // Find all definitions matching the symbol name const definitions = findSymbolDefinitions(project, symbol, searchScope); if (definitions.length === 0) { return { error: "symbol_not_found", message: `No symbol named '${symbol}' found in ${searchScope}`, suggestions: findSimilarSymbols(project, symbol, searchScope) }; } // If multiple definitions found, use heuristics to pick the best one // For now, we'll use the first one and potentially add disambiguation later const primaryDef = definitions[0]; // Extract comprehensive context const symbolInfo = extractSymbolInfo(primaryDef); const definitionInfo = extractDefinitionInfo(primaryDef, project); const usageInfo = findSymbolUsages(project, primaryDef, includeTests); const relationships = analyzeRelationships(project, primaryDef); const metrics = calculateMetrics(primaryDef, usageInfo); return { symbol: symbolInfo, definition: definitionInfo, usage: usageInfo, relationships, metrics }; } // Helper functions function findSymbolDefinitions(project, symbolName, _searchScope) { const definitions = []; // Get all file graphs from the project const fileGraphs = project.get_all_scope_graphs(); for (const [filePath, graph] of fileGraphs) { // Include all files - we'll filter test functions later if needed // Get all definition nodes from the graph const defs = graph.getNodes('definition'); for (const def of defs) { // Type guard to ensure we have a definition node with required properties if ('name' in def && def.name === symbolName) { definitions.push({ ...def, file_path: filePath, graph }); } } } return definitions; } function findSimilarSymbols(project, symbolName, _searchScope) { const allSymbols = new Set(); const fileGraphs = project.get_all_scope_graphs(); for (const [_filePath, graph] of fileGraphs) { // Include all files - test filtering happens at the function level const defs = graph.getNodes('definition'); for (const def of defs) { // Type guard to ensure we have a definition node with name property if ('name' in def && typeof def.name === 'string') { allSymbols.add(def.name); } } } // Improved similarity matching const lowerSymbol = symbolName.toLowerCase(); const suggestions = Array.from(allSymbols) .filter(s => { const lowerS = s.toLowerCase(); // Check if either contains the other if (lowerS.includes(lowerSymbol) || lowerSymbol.includes(lowerS)) { return true; } // Check if they share a common prefix (at least 3 chars) const minLen = Math.min(lowerS.length, lowerSymbol.length, 3); return lowerS.substring(0, minLen) === lowerSymbol.substring(0, minLen); }) .sort((a, b) => { // Sort by similarity - prefer exact prefix matches const aLower = a.toLowerCase(); const bLower = b.toLowerCase(); if (aLower.startsWith(lowerSymbol) && !bLower.startsWith(lowerSymbol)) return -1; if (!aLower.startsWith(lowerSymbol) && bLower.startsWith(lowerSymbol)) return 1; return a.length - b.length; // Prefer shorter names }) .slice(0, 5); return suggestions; } function extractSymbolInfo(def) { const symbolKindMap = { "function": "function", "method": "method", "class": "class", "struct": "struct", "interface": "interface", "type": "type", "enum": "enum", "variable": "variable", "property": "property" }; return { name: def.name, kind: symbolKindMap[def.symbol_kind] || "unknown", // TODO: Extract signature from the implementation // TODO: Determine visibility from modifiers }; } function extractDefinitionInfo(def, project) { let implementation = "// Source code not available"; let startLine = def.range.start.row; try { // Use the public API to get source code implementation = project.get_source_code(def, def.file_path); // Check if this definition is exported and prepend export keyword if needed // Since export detection via graph nodes isn't working reliably, // let's check if the source code around the definition contains 'export' if (!implementation.startsWith('export ')) { // Try to get the full line including export keyword try { const fullLineDef = { kind: 'variable', name: '_dummy', symbol_kind: 'variable', symbol_id: '_dummy', id: -1, file_path: def.file_path, range: { start: { row: def.range.start.row, column: 0 }, end: { row: def.range.end.row, column: 999 } } }; const fullLine = project.get_source_code(fullLineDef, def.file_path); // Check if the line starts with 'export' if (fullLine.trimStart().startsWith('export ')) { implementation = 'export ' + implementation; } } catch (e) { // Fallback to checking graph nodes if (def.graph) { const exports = def.graph.getNodes('export'); const isExported = exports.length > 0; // Simplified check if (isExported) { implementation = 'export ' + implementation; } } } } } catch (error) { // If source code extraction fails, use fallback } // Extract documentation and decorators using Ariadne's built-in API let documentation; let annotations; // Use the docstring from the def if available if (def.docstring) { documentation = def.docstring; } // If no docstring, try to extract JSDoc from the source if (!documentation && implementation) { // Look for JSDoc comment in the implementation or just before it const jsdocMatch = implementation.match(/\/\*\*([\s\S]*?)\*\//); if (jsdocMatch) { documentation = jsdocMatch[0]; } else { // Try to get the line before the definition to check for JSDoc try { if (def.range.start.row > 0) { const prevLineDef = { kind: 'variable', name: '_dummy', symbol_kind: 'variable', symbol_id: '_dummy', id: -1, file_path: def.file_path, range: { start: { row: Math.max(0, def.range.start.row - 10), column: 0 }, end: { row: def.range.start.row - 1, column: 999 } } }; const prevLines = project.get_source_code(prevLineDef, def.file_path); const jsdocInPrev = prevLines.match(/\/\*\*([\s\S]*?)\*\//); if (jsdocInPrev) { documentation = jsdocInPrev[0]; } } } catch (e) { // Ignore errors in documentation extraction } } } // Extract annotations/decorators try { if (def.range.start.row > 0) { const prevLineDef = { kind: 'variable', name: '_dummy', symbol_kind: 'variable', symbol_id: '_dummy', id: -1, file_path: def.file_path, range: { start: { row: Math.max(0, def.range.start.row - 10), column: 0 }, end: { row: def.range.start.row, column: 999 } } }; const prevLines = project.get_source_code(prevLineDef, def.file_path); // Look for decorators/annotations like @deprecated, @override, etc. const decoratorMatches = prevLines.match(/@\w+/g); if (decoratorMatches && decoratorMatches.length > 0) { annotations = decoratorMatches; } } } catch (e) { // Ignore errors in annotation extraction } return { file: def.file_path, line: startLine + 1, // Convert to 1-indexed implementation, documentation, annotations }; } function findSymbolUsages(project, def, includeTests) { const directReferences = []; const imports = []; const tests = []; const fileGraphs = project.get_all_scope_graphs(); // First, find local references in the same file const localRefs = def.graph.getRefsForDef(def.id); for (const ref of localRefs) { const context = extractReferenceContext(def.file_path, ref, project); // Check if this is a test file and we should track it as a test const isInTestFunction = isReferenceInTestFunction(def.file_path, ref, fileGraphs); if (isInTestFunction && includeTests) { const testName = extractTestName(def.file_path, ref, project); tests.push({ file: def.file_path, testName: testName || "test function", line: ref.range.start.row + 1 }); } else if (!isInTestFunction) { directReferences.push({ file: def.file_path, line: ref.range.start.row + 1, context }); } } // Check if this is an exported definition by looking for is_exported property // The core library sets this property during parsing const isExported = def.is_exported === true; if (isExported) { // Search for imports and references in other files for (const [filePath, graph] of fileGraphs) { if (filePath === def.file_path) continue; // Find imports using getNodes method const importNodes = graph.getNodes('import'); for (const imp of importNodes) { // Check if this import matches our definition // Import nodes have 'name' (local name) and optionally 'source_name' (imported name) const matchesName = 'name' in imp && (imp.name === def.name || ('source_name' in imp && imp.source_name === def.name)); if (matchesName) { // Verify the import is actually from the file containing the definition // For now, we'll include all matching imports (TODO: improve module resolution) imports.push({ file: filePath, line: 'range' in imp ? imp.range.start.row + 1 : 0, context: `import { ${imp.name} } from '${imp.source_module || '...'}'` }); // Find references to this import using the helper function const importRefs = findReferencesToImport(graph, imp); // Also find direct references by name in this file const allRefs = graph.getNodes('reference'); const nameMatchingRefs = allRefs.filter(ref => ref.name === imp.name); for (const ref of nameMatchingRefs) { const context = extractReferenceContext(filePath, ref, project); // Check if the reference is inside a test function const isInTestFunction = isReferenceInTestFunction(filePath, ref, fileGraphs); if (isInTestFunction) { if (includeTests) { // Try to extract test name const testName = extractTestName(filePath, ref, project); tests.push({ file: filePath, testName: testName || "test function", line: ref.range.start.row + 1 }); } // If includeTests is false, we skip test references entirely } else { directReferences.push({ file: filePath, line: ref.range.start.row + 1, context }); } } } } } } return { directReferences, imports, tests, totalCount: directReferences.length + tests.length }; } function findReferencesToImport(graph, imp) { const refs = []; const allRefs = graph.getNodes('reference'); for (const ref of allRefs) { const connectedImports = graph.getImportsForRef(ref.id); if (connectedImports.some((i) => i.id === imp.id)) { refs.push(ref); } } return refs; } function extractReferenceContext(filePath, ref, project) { try { // Create a dummy def with just the range we need (one line) const dummyDef = { ...ref, file_path: filePath, range: { start: { row: ref.range.start.row, column: 0 }, end: { row: ref.range.start.row, column: 999 } } }; const line = project.get_source_code(dummyDef, filePath); // Trim whitespace and limit length return line.trim().substring(0, 100); } catch (error) { return ""; } } function extractTestName(filePath, ref, project) { try { // Search backwards for test/it/describe for (let i = ref.range.start.row; i >= 0; i--) { const dummyDef = { kind: 'variable', name: '_dummy', symbol_kind: 'variable', symbol_id: '_dummy', id: -1, file_path: filePath, range: { start: { row: i, column: 0 }, end: { row: i, column: 999 } } }; const line = project.get_source_code(dummyDef, filePath); const testMatch = line.match(/(?:test|it|describe)\s*\(\s*['"`]([^'"`]+)['"`]/); if (testMatch) { return testMatch[1]; } // Don't search too far if (ref.range.start.row - i > 20) break; } } catch (error) { // Failed to extract test name } return null; } function isReferenceInTestFunction(filePath, _ref, _fileGraphs) { // Note: We're not using graph or ref parameters currently due to limitations // in how Ariadne detects test functions (primarily for named functions, not // anonymous functions or code blocks within test suites) // For now, use a simpler heuristic: check if the file is a test file // This is because: // 1. Arrow functions inside test blocks may not be captured as function definitions // 2. The reference might be directly in a test block, not in a named function // 3. Ariadne's test detection is primarily for named functions // TODO: Improve this by traversing the AST to find enclosing test blocks const isTestFile = filePath.includes('test') || filePath.includes('spec'); return isTestFile; } function analyzeRelationships(project, def) { const relationships = { calls: [], calledBy: [], dependencies: [], dependents: [] }; // Analyze function call relationships if (def.symbol_kind === 'function') { try { // Get the call graph for the entire project const callGraph = project.get_call_graph(); // Find the node for this function const functionNode = callGraph.nodes.get(def.symbol_id); if (functionNode) { // Extract calls and called_by relationships relationships.calls = functionNode.calls.map(call => call.symbol); relationships.calledBy = functionNode.called_by; // already strings } } catch (error) { // Call graph generation might fail for some codebases console.warn(`Failed to generate call graph: ${error}`); } } // Analyze class inheritance relationships if (def.symbol_kind === 'class' || def.symbol_kind === 'struct' || def.symbol_kind === 'interface') { try { const classRelationships = project.get_class_relationships(def); if (classRelationships) { // Set parent class if (classRelationships.parent_class) { relationships.extends = classRelationships.parent_class; } // Set implemented interfaces if (classRelationships.implemented_interfaces && classRelationships.implemented_interfaces.length > 0) { relationships.implements = classRelationships.implemented_interfaces; } } else { // Fallback: try to extract inheritance from source code const fallbackRelationships = extractInheritanceFromSource(project, def); if (fallbackRelationships.extends) { relationships.extends = fallbackRelationships.extends; } if (fallbackRelationships.implements && fallbackRelationships.implements.length > 0) { relationships.implements = fallbackRelationships.implements; } } } catch (error) { // Inheritance analysis might fail for some classes console.warn(`Failed to analyze inheritance for ${def.name}: ${error}`); // Try fallback method const fallbackRelationships = extractInheritanceFromSource(project, def); if (fallbackRelationships.extends) { relationships.extends = fallbackRelationships.extends; } if (fallbackRelationships.implements && fallbackRelationships.implements.length > 0) { relationships.implements = fallbackRelationships.implements; } } // Find subclasses/implementations using fallback if needed try { if (def.symbol_kind === 'class' || def.symbol_kind === 'struct') { const subclasses = project.find_subclasses(def); if (subclasses.length > 0) { relationships.dependents = subclasses.map(sub => sub.name); } else { // Fallback: search for classes that extend this one relationships.dependents = findDependentClassesFromSource(project, def); } } else if (def.symbol_kind === 'interface') { const implementations = project.find_implementations(def); if (implementations.length > 0) { relationships.dependents = implementations.map(impl => impl.name); } else { // Fallback: search for classes that implement this interface relationships.dependents = findDependentClassesFromSource(project, def); } } } catch (error) { console.warn(`Failed to find dependents for ${def.name}: ${error}`); // Use fallback method relationships.dependents = findDependentClassesFromSource(project, def); } } // TODO: Still missing: // - General symbol dependencies (imports, variable usage) // - Cross-file type dependencies return relationships; } // Fallback inheritance extraction functions function extractInheritanceFromSource(project, def) { try { // Get the source code around the class definition using enclosing_range const defWithEnclosingRange = { ...def, range: def.enclosing_range || def.range }; const implementation = project.get_source_code(defWithEnclosingRange, def.file_path); const result = {}; // Extract extends relationship const extendsMatch = implementation.match(/class\s+\w+\s+extends\s+(\w+)/); if (extendsMatch) { result.extends = extendsMatch[1]; } // Extract implements relationships const implementsMatch = implementation.match(/(?:class|interface)\s+\w+(?:\s+extends\s+\w+)?\s+implements\s+([\w\s,]+)/); if (implementsMatch) { result.implements = implementsMatch[1] .split(',') .map(name => name.trim()) .filter(name => name.length > 0); } // Handle interface extension if (def.symbol_kind === 'interface') { const interfaceExtendsMatch = implementation.match(/interface\s+\w+\s+extends\s+([\w\s,]+)/); if (interfaceExtendsMatch) { const extendedInterfaces = interfaceExtendsMatch[1] .split(',') .map(name => name.trim()) .filter(name => name.length > 0); if (extendedInterfaces.length === 1) { result.extends = extendedInterfaces[0]; } else if (extendedInterfaces.length > 1) { // For multiple interface inheritance, use the first one as extends result.extends = extendedInterfaces[0]; // Could also set implements to the rest, but this is less common } } } // Handle Rust trait implementations by looking at references if (def.symbol_kind === 'struct' && def.file_path.endsWith('.rs')) { // Look for references to this struct in impl blocks try { const fileGraphs = project.get_all_scope_graphs(); const graph = fileGraphs.get(def.file_path); if (graph) { const refs = graph.getNodes('reference'); const structRefs = refs.filter(ref => ref.name === def.name); const implementedTraits = []; for (const ref of structRefs) { try { const refDef = { ...ref, range: { start: { row: ref.range.start.row, column: 0 }, end: { row: ref.range.start.row, column: 999 } } }; const line = project.get_source_code(refDef, def.file_path); // Look for impl patterns: "impl TraitName for StructName" const implMatch = line.match(/impl\s+(\w+)\s+for\s+\w+/); if (implMatch) { const traitName = implMatch[1]; if (!implementedTraits.includes(traitName)) { implementedTraits.push(traitName); } } } catch (error) { // Skip this reference } } if (implementedTraits.length > 0) { result.implements = implementedTraits; } } } catch (error) { // Fallback failed, ignore } } return result; } catch (error) { return {}; } } function findDependentClassesFromSource(project, def) { const dependents = []; try { // Search all files for classes that extend or implement this definition const fileGraphs = project.get_all_scope_graphs(); for (const [filePath, graph] of fileGraphs) { const definitions = graph.getNodes('definition'); const classDefinitions = definitions.filter(d => d.symbol_kind === 'class' || d.symbol_kind === 'struct' || d.symbol_kind === 'interface'); for (const classDef of classDefinitions) { if (classDef.name === def.name) continue; // Skip self try { const defWithEnclosingRange = { ...classDef, range: classDef.enclosing_range || classDef.range }; const implementation = project.get_source_code(defWithEnclosingRange, filePath); // Check for extends relationship const extendsPattern = new RegExp(`(?:class|interface)\\s+\\w+\\s+extends\\s+${def.name}\\b`); if (extendsPattern.test(implementation)) { dependents.push(classDef.name); continue; } // Check for implements relationship const implementsPattern = new RegExp(`class\\s+\\w+(?:\\s+extends\\s+\\w+)?\\s+implements\\s+[\\w\\s,]*\\b${def.name}\\b`); if (implementsPattern.test(implementation)) { dependents.push(classDef.name); continue; } // Handle Rust trait implementations if (filePath.endsWith('.rs')) { const rustImplPattern = new RegExp(`impl\\s+${def.name}\\s+for\\s+(\\w+)`); const rustMatch = implementation.match(rustImplPattern); if (rustMatch) { dependents.push(rustMatch[1]); } } } catch (error) { // Skip this definition if we can't get its source } } } } catch (error) { // Return empty array on error } return dependents; } function calculateMetrics(def, _usage) { // Use metadata.line_count if available (most accurate), otherwise fall back to range calculation let linesOfCode; if (def.metadata?.line_count) { linesOfCode = def.metadata.line_count; } else { // Fall back to range calculation (only covers the definition line) const range = def.enclosing_range || def.range; linesOfCode = range.end.row - range.start.row + 1; } // TODO: Calculate cyclomatic complexity // TODO: Calculate test coverage percentage return { linesOfCode }; } //# sourceMappingURL=get_symbol_context.js.map