Skip to main content
Glama
Ixe1

Code Scanner Server

by Ixe1

scan_code

Scan directories for code files (JS, TS, C#, PHP, CSS, etc.) and extract structural elements like functions and classes with line numbers. Customize output formats (XML, Markdown, JSON) and filter results by file patterns, element types, and modifiers for precise analysis.

Instructions

Scans a directory for code files (JS, TS, C#, PHP, CSS, respecting .gitignore) and lists definitions (functions, classes, etc.) with line numbers. Supports XML, Markdown, and JSON output.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
detailLevelNoLevel of detail to include in the output.standard
directoryYesThe absolute path to the directory to scan. Relative paths are not supported.
excludeModifiersNoModifiers to exclude.
excludeNamePatternNoRegex pattern to exclude element names.
excludePathsNoFile path patterns to exclude.
excludeTypesNoElement types to exclude.
filePatternsNoGlob patterns for file extensions to include.
includeModifiersNoModifiers to include (e.g., public, private).
includePathsNoAdditional file path patterns to include.
includeTypesNoElement types to include (e.g., class, method).
namePatternNoRegex pattern to match element names.
outputFormatNoOutput format for the results.markdown

Implementation Reference

  • MCP CallToolRequestSchema handler specifically for the 'scan_code' tool. Validates input parameters, constructs filter options, calls performScan, and returns formatted results as text content.
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
    	if (request.params.name !== "scan_code") {
    		throw new McpError(
    			ErrorCode.MethodNotFound,
    			`Unknown tool: ${request.params.name}`
    		);
    	}
    
    	const args = request.params.arguments;
    
    	// Validate directory argument
    	if (typeof args?.directory !== "string") {
    		throw new McpError(
    			ErrorCode.InvalidParams,
    			"Missing or invalid 'directory' argument (must be a string)."
    		);
    	}
    	// Ensure directory is an absolute path
    	if (!path.isAbsolute(args.directory)) {
    		throw new McpError(
    			ErrorCode.InvalidParams,
    			"Invalid 'directory' argument: Path must be absolute."
    		);
    	}
    
    	const filePatterns =
    		args?.filePatterns && Array.isArray(args.filePatterns)
    			? args.filePatterns
    			: defaultFilePatterns; // Use consistent defaults
    	const outputFormat = // Default to markdown unless 'xml' or 'json' is specified
    		args?.outputFormat === "xml" ? "xml" :
    		args?.outputFormat === "json" ? "json" : "markdown";
    	const detailLevel =
    		args?.detailLevel === "minimal" || args?.detailLevel === "detailed" ?
    		args.detailLevel : "standard"; // Default to standard unless specified
    
    	// Build filter options from MCP arguments
    	const filterOptions: FilterOptions = {};
    
    	// Process include-types - split comma-separated values if needed
    	if (args?.includeTypes && Array.isArray(args.includeTypes)) {
    		filterOptions.includeTypes = args.includeTypes.flatMap(t =>
    			typeof t === 'string' && t.includes(',') ? t.split(',') : t
    		);
    	}
    
    	// Process exclude-types - split comma-separated values if needed
    	if (args?.excludeTypes && Array.isArray(args.excludeTypes)) {
    		filterOptions.excludeTypes = args.excludeTypes.flatMap(t =>
    			typeof t === 'string' && t.includes(',') ? t.split(',') : t
    		);
    	}
    
    	// Process include-modifiers - split comma-separated values if needed
    	if (args?.includeModifiers && Array.isArray(args.includeModifiers)) {
    		filterOptions.includeModifiers = args.includeModifiers.flatMap(m =>
    			typeof m === 'string' && m.includes(',') ? m.split(',') : m
    		);
    	}
    
    	// Process exclude-modifiers - split comma-separated values if needed
    	if (args?.excludeModifiers && Array.isArray(args.excludeModifiers)) {
    		filterOptions.excludeModifiers = args.excludeModifiers.flatMap(m =>
    			typeof m === 'string' && m.includes(',') ? m.split(',') : m
    		);
    	}
    
    	if (args?.namePattern && typeof args.namePattern === 'string') {
    		filterOptions.namePattern = args.namePattern;
    	}
    
    	if (args?.excludeNamePattern && typeof args.excludeNamePattern === 'string') {
    		filterOptions.excludeNamePattern = args.excludeNamePattern;
    	}
    
    	// Process include-paths - split comma-separated values if needed
    	if (args?.includePaths && Array.isArray(args.includePaths)) {
    		filterOptions.includePaths = args.includePaths.flatMap(p =>
    			typeof p === 'string' && p.includes(',') ? p.split(',') : p
    		);
    	}
    
    	// Process exclude-paths - split comma-separated values if needed
    	if (args?.excludePaths && Array.isArray(args.excludePaths)) {
    		filterOptions.excludePaths = args.excludePaths.flatMap(p =>
    			typeof p === 'string' && p.includes(',') ? p.split(',') : p
    		);
    	}
    
    	try {
            // Call the refactored function
    		const outputText = await performScan(
    			args.directory,
    			filePatterns,
    			outputFormat,
    			detailLevel as 'minimal' | 'standard' | 'detailed',
    			filterOptions
    		);
    
    		return {
    			content: [
    				{
    					type: "text",
    					text: outputText,
    				},
    			],
    		};
    	} catch (error: any) {
    		console.error(`Error during scan_code execution: ${error}`);
    		// If it's an error from performScan, wrap it in McpError
            if (error instanceof Error && !(error instanceof McpError)) {
                 throw new McpError(
                    ErrorCode.InternalError,
                    `Failed to scan directory: ${error.message}`
                );
            }
            // Re-throw McpErrors or other unexpected errors
    		throw error;
    	}
    });
  • Input schema defining the parameters for the 'scan_code' tool, including required 'directory' and optional filters, formats, etc.
    inputSchema: {
    	type: "object",
    	properties: {
    		directory: {
    			type: "string",
    			description:
    				"The absolute path to the directory to scan. Relative paths are not supported.",
    		},
    		filePatterns: {
    			type: "array",
    			items: { type: "string" },
    			description:
    				"Glob patterns for file extensions to include.",
    			default: defaultFilePatterns, // Use consistent defaults
    		},
    		outputFormat: {
    			type: "string",
    			enum: ["xml", "markdown", "json"],
    			description: "Output format for the results.",
    			default: "markdown",
    		},
    		detailLevel: {
    			type: "string",
    			enum: ["minimal", "standard", "detailed"],
    			description: "Level of detail to include in the output.",
    			default: "standard",
    		},
    		includeTypes: {
    			type: "array",
    			items: { type: "string" },
    			description: "Element types to include (e.g., class, method).",
    		},
    		excludeTypes: {
    			type: "array",
    			items: { type: "string" },
    			description: "Element types to exclude.",
    		},
    		includeModifiers: {
    			type: "array",
    			items: { type: "string" },
    			description: "Modifiers to include (e.g., public, private).",
    		},
    		excludeModifiers: {
    			type: "array",
    			items: { type: "string" },
    			description: "Modifiers to exclude.",
    		},
    		namePattern: {
    			type: "string",
    			description: "Regex pattern to match element names.",
    		},
    		excludeNamePattern: {
    			type: "string",
    			description: "Regex pattern to exclude element names.",
    		},
    		includePaths: {
    			type: "array",
    			items: { type: "string" },
    			description: "Additional file path patterns to include.",
    		},
    		excludePaths: {
    			type: "array",
    			items: { type: "string" },
    			description: "File path patterns to exclude.",
    		},
    	},
    	required: ["directory"],
  • src/index.ts:1400-1478 (registration)
    Tool registration in the ListToolsRequestSchema handler, defining the 'scan_code' tool with name, description, and input schema.
    server.setRequestHandler(ListToolsRequestSchema, async () => {
    	return {
    		tools: [
    			{
    				name: "scan_code",
    				description:
    					"Scans a directory for code files (JS, TS, C#, PHP, CSS, respecting .gitignore) and lists definitions (functions, classes, etc.) with line numbers. Supports XML, Markdown, and JSON output.",
    				inputSchema: {
    					type: "object",
    					properties: {
    						directory: {
    							type: "string",
    							description:
    								"The absolute path to the directory to scan. Relative paths are not supported.",
    						},
    						filePatterns: {
    							type: "array",
    							items: { type: "string" },
    							description:
    								"Glob patterns for file extensions to include.",
    							default: defaultFilePatterns, // Use consistent defaults
    						},
    						outputFormat: {
    							type: "string",
    							enum: ["xml", "markdown", "json"],
    							description: "Output format for the results.",
    							default: "markdown",
    						},
    						detailLevel: {
    							type: "string",
    							enum: ["minimal", "standard", "detailed"],
    							description: "Level of detail to include in the output.",
    							default: "standard",
    						},
    						includeTypes: {
    							type: "array",
    							items: { type: "string" },
    							description: "Element types to include (e.g., class, method).",
    						},
    						excludeTypes: {
    							type: "array",
    							items: { type: "string" },
    							description: "Element types to exclude.",
    						},
    						includeModifiers: {
    							type: "array",
    							items: { type: "string" },
    							description: "Modifiers to include (e.g., public, private).",
    						},
    						excludeModifiers: {
    							type: "array",
    							items: { type: "string" },
    							description: "Modifiers to exclude.",
    						},
    						namePattern: {
    							type: "string",
    							description: "Regex pattern to match element names.",
    						},
    						excludeNamePattern: {
    							type: "string",
    							description: "Regex pattern to exclude element names.",
    						},
    						includePaths: {
    							type: "array",
    							items: { type: "string" },
    							description: "Additional file path patterns to include.",
    						},
    						excludePaths: {
    							type: "array",
    							items: { type: "string" },
    							description: "File path patterns to exclude.",
    						},
    					},
    					required: ["directory"],
    				},
    			},
    		],
    	};
    });
  • Main helper function performScan that implements the scanning logic: file discovery, Tree-sitter parsing, filtering, and output formatting.
    async function performScan(
    	directory: string,
    	filePatterns: string[],
    	outputFormat: 'xml' | 'markdown' | 'json',
    	detailLevel: 'minimal' | 'standard' | 'detailed' = 'standard',
    	filterOptions: FilterOptions = {}
    ): Promise<string> {
    	const startTime = Date.now();
    	console.error(`Starting scan in directory: ${directory}`);
    	console.error(`File patterns: ${filePatterns.join(', ')}`);
    	console.error(`Output format: ${outputFormat}, Detail level: ${detailLevel}`);
    	console.error(`Filter options: ${JSON.stringify(filterOptions)}`);
    
    
    	// Resolve the target directory relative to the current working directory
    	const targetDir = path.resolve(process.cwd(), directory);
    	console.error(`Resolved target directory: ${targetDir}`);
    
    	try {
    		// Check if the directory exists using async fs
    		await fs.access(targetDir);
    	} catch (error) {
    		throw new Error(`Directory not found: ${targetDir}`);
    	}
    
    	// Find .gitignore
    	const gitignorePath = await findGitignore(targetDir);
    	const ignoreFilter = await getIgnoreFilter(gitignorePath);
    	console.error(`Using .gitignore: ${gitignorePath || 'None found'}`);
    
    
    	// --- File Discovery ---
    	console.error("Starting file discovery...");
    	let files: Set<string>; // Declare files set here
    
    	// Check if includePaths is provided and should be restrictive
    	if (filterOptions.includePaths && filterOptions.includePaths.length > 0) {
    		console.error("`includePaths` provided, operating in restrictive mode.");
    		files = new Set<string>(); // Initialize as empty set
    
    		// Process only the paths specified in includePaths
    		for (const includePath of filterOptions.includePaths) {
    			// Handle potential glob patterns within includePaths if necessary,
    			// or treat them as specific files/directories.
    			// Current logic reuses the file/directory handling from below.
    			const absoluteIncludePath = path.resolve(targetDir, includePath);
    			try {
    				const stats = await fs.stat(absoluteIncludePath);
    				if (stats.isFile()) {
    					files.add(path.normalize(absoluteIncludePath));
    					console.error(`Added specific file from includePaths: ${absoluteIncludePath}`);
    				} else if (stats.isDirectory()) {
    					console.error(`Included path is a directory, scanning recursively: ${absoluteIncludePath}`);
    					// Use fg to find files within the directory, respecting basic ignores
    					const dirFiles = await fg(path.join(absoluteIncludePath, '**/*').replace(/\\/g, '/'), {
    						dot: true,
    						onlyFiles: true,
    						absolute: true,
    						ignore: ['**/node_modules/**', '**/.git/**'], // Consistent ignores
    					});
    					dirFiles.forEach(f => files.add(path.normalize(f)));
    					console.error(`Added ${dirFiles.length} files from included directory: ${absoluteIncludePath}`);
    				}
    			} catch (statError: any) {
    				// Handle cases where includePath might be a glob pattern needing fg
    				if (includePath.includes('*') || includePath.includes('?')) {
    					console.error(`Included path looks like a glob, attempting glob match: ${includePath}`);
    					try {
    						const globMatches = await fg(path.join(targetDir, includePath).replace(/\\/g, '/'), {
    							dot: true, onlyFiles: true, absolute: true, cwd: targetDir,
    							ignore: ['**/node_modules/**', '**/.git/**'],
    						});
    						globMatches.forEach(match => files.add(path.normalize(match)));
    						console.error(`Added ${globMatches.length} files from includePaths glob: ${includePath}`);
    					} catch (globError) {
    						console.error(`Error matching includePaths glob ${includePath}:`, globError);
    					}
    				} else if (statError.code === 'ENOENT') {
    					console.warn(`Warning: Included path not found: ${absoluteIncludePath}`);
    				} else {
    					console.error(`Error processing included path ${absoluteIncludePath}:`, statError);
    				}
    			}
    		}
    		console.error(`Total files after processing restrictive includePaths: ${files.size}`);
    
    	} else {
    		// --- Standard File Discovery using filePatterns (glob) ---
    		console.error("No restrictive `includePaths`, using standard `filePatterns` globbing.");
    		const globMatchedFiles = new Set<string>(); // Store absolute paths from glob matching
    
    		// Combine default and provided patterns if necessary
    		const combinedPatterns = [...new Set(filePatterns)];
    		console.error(`Combined glob patterns for search: ${combinedPatterns.join(', ')}`);
    
    		// Execute glob patterns relative to the target directory
    		const globPatterns = combinedPatterns.map(p => path.join(targetDir, p).replace(/\\/g, '/'));
    		console.error(`Absolute glob patterns for fast-glob: ${globPatterns.join(', ')}`);
    
    		try {
    			const globMatches = await fg(globPatterns, {
    				dot: true, onlyFiles: true, absolute: true, cwd: targetDir,
    				ignore: ['**/node_modules/**', '**/.git/**'],
    			});
    			console.error(`fast-glob matched ${globMatches.length} files initially.`);
    			globMatches.forEach(match => globMatchedFiles.add(path.normalize(match)));
    		} catch (globError) {
    			console.error("Error during fast-glob execution:", globError);
    			throw new Error("File globbing failed.");
    		}
    
    		files = new Set<string>(globMatchedFiles); // Start with glob results
    
    		// --- Include specific non-glob paths (additive) ---
    		// This part only runs if includePaths was NOT restrictive
    		const nonGlobIncludes = filterOptions.includePaths?.filter(p => !p.includes('*') && !p.includes('?')) || [];
    		if (nonGlobIncludes.length > 0) {
    			console.error("Adding specific non-glob paths from `includePaths`...");
    			for (const includePath of nonGlobIncludes) {
    				const absoluteIncludePath = path.resolve(targetDir, includePath);
    				try {
    					const stats = await fs.stat(absoluteIncludePath);
    					if (stats.isFile()) {
    						files.add(path.normalize(absoluteIncludePath));
    						console.error(`Added specific file from includePaths: ${absoluteIncludePath}`);
    					} else if (stats.isDirectory()) {
    						console.error(`Included path is a directory, scanning recursively: ${absoluteIncludePath}`);
    						const dirFiles = await fg(path.join(absoluteIncludePath, '**/*').replace(/\\/g, '/'), {
    							dot: true, onlyFiles: true, absolute: true,
    							ignore: ['**/node_modules/**', '**/.git/**'],
    						});
    						dirFiles.forEach(f => files.add(path.normalize(f)));
    						console.error(`Added ${dirFiles.length} files from included directory: ${absoluteIncludePath}`);
    					}
    				} catch (statError: any) {
    					if (statError.code === 'ENOENT') {
    						console.warn(`Warning: Included path not found: ${absoluteIncludePath}`);
    					} else {
    						console.error(`Error stating included path ${absoluteIncludePath}:`, statError);
    					}
    				}
    			}
    		}
    	}
    	// --- End of Conditional File Discovery ---
    
    	// --- Include specific non-glob paths if provided --- // This section title is now misleading, the logic is handled above. Remove/adjust comment.
    	// const files = new Set<string>(globMatchedFiles); // This line is now inside the else block
    	// The logic previously here (lines 883-911) has been integrated into the
    	// conditional blocks above (restrictive 'if' and standard 'else')
    	// to handle includePaths correctly in both scenarios.
    
    
    	// --- Filtering based on .gitignore and excludePaths ---
    	let filesToFilter = Array.from(files);
    	console.error(`Total files before gitignore/exclude filtering: ${filesToFilter.length}`);
    
    
    	// 1. Apply .gitignore filter
        filesToFilter = filesToFilter.filter(absPath => {
            const relativePath = path.relative(targetDir, absPath).replace(/\\/g, '/'); // Relative path for ignore check
            return ignoreFilter(relativePath);
        });
    	console.error(`Files after gitignore filtering: ${filesToFilter.length}`);
    
    
    	// 2. Apply excludePaths filter (using minimatch)
        const excludePatterns = filterOptions.excludePaths || [];
        if (excludePatterns.length > 0) {
            console.error(`Applying excludePaths patterns: ${excludePatterns.join(', ')}`);
            const filteredFiles = filesToFilter.filter((absPath) => {
                const relativePath = path.relative(targetDir, absPath).replace(/\\/g, '/');
                // Check if the relative path matches any exclude pattern
                const isExcluded = excludePatterns.some(pattern => minimatch(relativePath, pattern, { dot: true }));
                if (isExcluded) {
                    // console.error(`Excluding file due to pattern '${excludePatterns.find(p => minimatch(relativePath, p))}': ${relativePath}`);
                }
                return !isExcluded; // Keep if not excluded
            });
    		const excludedCount = filesToFilter.length - filteredFiles.length;
    		if (excludedCount > 0) {
    			console.error(`Excluded ${excludedCount} files based on excludePaths patterns.`);
    		}
            filesToFilter = filteredFiles;
        }
    	console.error(`Files after excludePaths filtering: ${filesToFilter.length}`);
    
    
    	// --- Parsing and Definition Extraction ---
    	const results: { [filePath: string]: Definition[] } = {};
    	console.error(`Parsing ${filesToFilter.length} files...`);
    
    
    	for (const absoluteFilePath of filesToFilter) {
    		// Use relative path for keys in the results object for cleaner output
    		const relativePath = path.relative(targetDir, absoluteFilePath).replace(/\\/g, '/'); // Use forward slashes
    		try {
    			const content = await fs.readFile(absoluteFilePath, "utf-8");
    			const definitions = parseCodeWithTreeSitter(content, absoluteFilePath); // Pass absolute path here
    			if (definitions.length > 0) { // Only add files with definitions or errors
    				results[absoluteFilePath] = definitions; // Store with absolute path initially
    				// console.error(`Parsed ${relativePath}: Found ${definitions.length} potential definitions.`);
    			} else {
    				// console.error(`Parsed ${relativePath}: No definitions found.`);
    			}
    		} catch (error: any) {
    			console.error(`Error reading or parsing file ${relativePath}:`, error.message);
    			results[absoluteFilePath] = [{ type: "error", name: `Failed to read/parse: ${error.message}`, startLine: 0, endLine: 0 }];
    		}
    	}
    	console.error("Finished parsing files.");
    
    
    	// --- Filtering Definitions ---
    	console.error("Applying definition filters...");
    	const filteredResults: { [filePath: string]: Definition[] } = {};
    	for (const absoluteFilePath in results) {
    		const definitions = results[absoluteFilePath];
    		const filteredDefs = applyFiltersToDefinitions(definitions, filterOptions);
    
    		// Only include the file in the final results if it has non-error definitions after filtering
    		const hasRealDefinitions = filteredDefs.some(def => def.type !== 'error');
    		if (hasRealDefinitions) {
    			const relativePath = path.relative(targetDir, absoluteFilePath).replace(/\\/g, '/');
    			filteredResults[relativePath] = filteredDefs; // Use relative path for final output keys
    			// console.error(`Filtered ${relativePath}: Kept ${filteredDefs.length} definitions.`);
    		} else {
    			// console.error(`Filtered ${relativePath}: No definitions kept.`);
    		}
    	}
    	console.error("Finished applying definition filters.");
    
    
    	// --- Formatting Output ---
    	console.error(`Formatting results as ${outputFormat}...`);
    	let outputText: string;
    	switch (outputFormat) {
    		case "xml":
    			outputText = formatResultsXML(filteredResults, detailLevel);
    			break;
    		case "json":
    			outputText = formatResultsJSON(filteredResults, detailLevel);
    			break;
    		case "markdown":
    		default:
    			// Pass the original directory path for relative path calculation in Markdown
    			outputText = formatResultsMarkdown(filteredResults, detailLevel, directory);
    			break;
    	}
    	console.error("Finished formatting results.");
    	const endTime = Date.now();
    	console.error(`Scan completed in ${endTime - startTime}ms.`);
    
    
    	return outputText;
    }
  • Key helper for parsing individual code files using Tree-sitter queries to extract code definitions (functions, classes, methods, etc.) with metrics and hierarchy.
    function parseCodeWithTreeSitter(
    	code: string,
    	filePath: string
    ): Definition[] {
    	const definitions: Definition[] = [];
    	const fileExt = path.extname(filePath).toLowerCase();
    	const language = languageMap[fileExt];
    
    	if (!language) {
    		return [{ type: "error", name: "Unsupported file type", startLine: 0, endLine: 0 }];
    	}
    
    	parser.setLanguage(language);
    	let tree: Parser.Tree;
    	try {
    		tree = parser.parse(code);
    	} catch (error: any) {
    		// Use absolute paths for reliable comparison
    		const absoluteFilePath = path.resolve(filePath);
    		const absoluteTargetFilePath = path.resolve("src/index.ts"); // Resolve relative to CWD
    		const errorLogPath = path.join(process.cwd(), "parser-error.log"); // Log in CWD
    
    		if (absoluteFilePath === absoluteTargetFilePath) {
    			const errorMessage = `ERROR: Tree-sitter failed to parse ${filePath} at ${new Date().toISOString()}.\nFull error:\n${JSON.stringify(error, null, 2)}\n---\n`;
    			// Log the error asynchronously, but don't block parsing
    			fs.appendFile(errorLogPath, errorMessage)
    				.then(() => console.error(`ERROR: Tree-sitter failed to parse ${filePath}. See ${errorLogPath} for details.`))
    				.catch(logErr => console.error(`FATAL: Failed to write to ${errorLogPath}: ${logErr.message}`));
    			
    			// Return a specific error definition to indicate parsing failure for this critical file
    			return [{ type: "error", name: `Failed to parse self (${filePath})`, startLine: 0, endLine: 0 }];
    		} else {
    			// For other files, just log the error and return an empty array
    			console.error(`Error parsing ${filePath}:`, error);
    			return [{ type: "error", name: `Failed to parse ${filePath}`, startLine: 0, endLine: 0 }];
    		}
    	}
    
    	const langQueries = queries[fileExt];
    	if (!langQueries) {
    		return [{ type: "error", name: "No queries defined for file type", startLine: 0, endLine: 0 }];
    	}
    
    	// Generate unique IDs
    	let idCounter = 0;
    	const generateId = () => `def-${idCounter++}`;
    
    	for (const defType in langQueries) {
    		const queryStr = langQueries[defType];
    		if (!queryStr) continue; // Skip empty queries (like CSS or JS enums)
    
    		try {
    			const query = new Parser.Query(language, queryStr);
    			const matches = query.matches(tree.rootNode);
    
    			for (const match of matches) {
    				const nameNode = match.captures.find((c: Parser.QueryCapture) => c.name === "name")?.node;
    				const modifierNode = match.captures.find((c: Parser.QueryCapture) => c.name === "modifier")?.node;
    				const definitionNode = match.captures.find((c: Parser.QueryCapture) => c.name === defType)?.node; // Use defType capture
    				if (!nameNode || !definitionNode) continue;
    
    				// Capture optional fields
    				const dataTypeNode = match.captures.find((c: Parser.QueryCapture) => c.name === "dataType")?.node;
    				const valueNode = match.captures.find((c: Parser.QueryCapture) => c.name === "value")?.node;
    				const returnTypeNode = match.captures.find((c: Parser.QueryCapture) => c.name === "return_type")?.node;
    				const paramsNode = match.captures.find((c: Parser.QueryCapture) => c.name === "params")?.node; // Capture params block
    
    				// Determine parent type based on language and definition type
    				let parentType: string | undefined;
    				const localScopeTypes = ['variable', 'local_variable']; // Types typically defined within functions/methods
    
    				// Parent type is determined later by the tree traversal approach
    				// Local scope types don't need special handling since we use node traversal
    
    				const definition: Definition = {
    					id: generateId(),
    					type: defType,
    					name: nameNode.text,
    					startLine: definitionNode.startPosition.row + 1,
    					endLine: definitionNode.endPosition.row + 1,
    					loc: definitionNode.endPosition.row - definitionNode.startPosition.row + 1, // Calculate LoC
    					modifier: modifierNode?.text,
    					dataType: dataTypeNode?.text,
    					value: valueNode?.text,
    					returnType: returnTypeNode?.text,
    					children: [], // Initialize children array
    					// Initialize metrics - parameterCount and complexity calculated later
    					parameterCount: 0,
    					complexity: 1,
    				};
    
    				// Extract signature for methods/functions if possible
    				if (['method', 'function'].includes(defType)) {
    					// Calculate complexity for functions/methods
    					definition.complexity = calculateComplexity(definitionNode); // Calculate Complexity
    
    					// Attempt to get the full signature text
    					const startPos = definitionNode.startPosition;
    					const endPos = definitionNode.endPosition;
    					const sourceLines = code.split('\n');
    
    					// Heuristic: Try to capture the signature line(s)
    					// This might need refinement based on language specifics
    					let signature = '';
    					if (startPos.row === endPos.row) {
    						signature = sourceLines[startPos.row].substring(startPos.column, endPos.column);
    					} else {
    						// Multi-line signature (less common for just the signature part)
    						// Try to capture up to the opening brace '{' or equivalent
    						const openingBraceIndex = definitionNode.text.indexOf('{');
    						signature = openingBraceIndex > -1 ? definitionNode.text.substring(0, openingBraceIndex).trim() : definitionNode.text.split('\n')[0].trim();
    					}
    					// Clean up signature (optional)
    					signature = signature.replace(/\s+/g, ' ').trim();
    					// definition.signature = signature; // Add if needed later
    
    					// Extract parameters if paramsNode exists
    					if (paramsNode) {
    						const parameters: Parameter[] = [];
    						const paramCaptures = query.captures(paramsNode); // Query within the params node
    
    						// Find parameter names and types (adjust query capture names as needed)
    						let paramMatch: { name?: string, type?: string } = {};
    						for (const capture of paramCaptures) {
    							if (capture.name === 'param_name') {
    								paramMatch.name = capture.node.text;
    							} else if (capture.name === 'param_type') {
    								paramMatch.type = capture.node.text;
    							}
    
    							// When we have a name, add the parameter and reset
    							if (paramMatch.name) {
    								parameters.push({ name: paramMatch.name, type: paramMatch.type });
    								paramMatch = {}; // Reset for the next parameter
    							}
    						}
    						// Fallback: If captures don't work well, parse the text directly (less robust)
    						if (parameters.length === 0 && paramsNode.text.length > 2) { // Avoid empty "()"
    							const paramList = paramsNode.text.slice(1, -1).split(','); // Remove () and split
    							parameters.push(...paramList.map((p: string) => {
    								const trimmed = p.trim();
    								// Basic type inference (example for TS/PHP like syntax)
    								const typeMatch = trimmed.match(/^(\S+)\s+(\$\S+|\S+)/); // e.g., "string $name" or "int count"
    								if (typeMatch) {
    									return { name: typeMatch[2], type: typeMatch[1] };
    								}
    								return { name: trimmed }; // Just name if no type found
    							}).filter((p: { name: string; type?: string; }) => p.name)); // Filter out empty params
    						}
    
    
    						definition.parameters = parameters;
    						definition.parameterCount = parameters.length; // Calculate Parameter Count
    
    						// Find calls within this function/method body
    						const callQueryStr = langQueries['call'];
    						if (callQueryStr) {
    							try {
    								const callQuery = new Parser.Query(language, callQueryStr);
    								const callMatches = callQuery.matches(definitionNode); // Search within the definition node
    								const calledNames = new Set<string>(); // Use Set to avoid duplicates
    
    								for (const callMatch of callMatches) {
    									const callNameNode = callMatch.captures.find((c: Parser.QueryCapture) => c.name === "call_name")?.node;
    									if (callNameNode) {
    										calledNames.add(callNameNode.text);
    									}
    								}
    								if (calledNames.size > 0) {
    									definition.calls = Array.from(calledNames);
    								}
    							} catch (callQueryError: any) {
    								console.warn(`Warning: Failed to execute call query for ${defType} ${definition.name} in ${filePath}:`, callQueryError.message);
    							}
    						}
    					}
    
    					// Extract return type if returnTypeNode exists
    					if (returnTypeNode) {
    						definition.returnType = returnTypeNode.text;
    					} else {
    						// Fallback: Try to capture from signature text (language-specific)
    						// Example for PHP/TS style: function getName(): string { ... }
    						const returnTypeMatch = signature.match(/:\s*(\w+)\s*\{?$/);
    						if (returnTypeMatch) {
    							definition.returnType = returnTypeMatch[1];
    						}
    					}
    				}
    
    
    				definitions.push(definition);
    			}
    		} catch (queryError: any) {
    			console.error(`Error executing query for ${defType} in ${filePath}:`, queryError);
    			// Continue to next definition type
    		}
    	}
    
    	// --- Parent-Child Relationship Logic ---
    	// Create a map for quick lookup by ID
    	const definitionMap = new Map(definitions.map(def => [def.id, def]));
    
    	definitions.forEach(def => {
    		const node = tree.rootNode.descendantForPosition({ row: def.startLine - 1, column: 0 }); // Get node at start line
    		if (node) {
    			// No redundant parent-child relationship logic (removed)
    
    
    			// More robust parent finding: traverse up the syntax tree
    			let potentialParentNode = node.parent;
    			let parent: Definition | undefined = undefined;
    			while (potentialParentNode) {
    				parent = definitions.find(p =>
    					p.id !== def.id &&
    					p.startLine === potentialParentNode!.startPosition.row + 1 &&
    					p.endLine === potentialParentNode!.endPosition.row + 1 &&
    					['class', 'namespace', 'interface', 'enum', 'method', 'function'].includes(p.type) // Plausible parent types
    				);
    				if (parent) break; // Found a direct parent definition
    				potentialParentNode = potentialParentNode.parent;
    			}
    
    
    			if (parent) {
    				def.parentId = parent.id;
    				if (!parent.children) {
    					parent.children = [];
    				}
    				if (def.id) { // Ensure def.id is defined before pushing
    					parent.children.push(def.id);
    				}
    			}
    		}
    	});
    
    
    	return definitions;
    }
Install Server

Other Tools

Related Tools

Latest Blog Posts

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/Ixe1/code-scanner-server'

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