Skip to main content
Glama
symbol-tools.ts26.3 kB
import * as vscode from 'vscode'; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from 'zod'; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import * as path from 'path'; import * as fs from 'fs'; import { logger } from '../utils/logger'; /** * Convert a symbol kind to a string representation * @param kind The symbol kind enum value * @returns String representation of the symbol kind */ function symbolKindToString(kind: vscode.SymbolKind): string { switch (kind) { case vscode.SymbolKind.File: return 'File'; case vscode.SymbolKind.Module: return 'Module'; case vscode.SymbolKind.Namespace: return 'Namespace'; case vscode.SymbolKind.Package: return 'Package'; case vscode.SymbolKind.Class: return 'Class'; case vscode.SymbolKind.Method: return 'Method'; case vscode.SymbolKind.Property: return 'Property'; case vscode.SymbolKind.Field: return 'Field'; case vscode.SymbolKind.Constructor: return 'Constructor'; case vscode.SymbolKind.Enum: return 'Enum'; case vscode.SymbolKind.Interface: return 'Interface'; case vscode.SymbolKind.Function: return 'Function'; case vscode.SymbolKind.Variable: return 'Variable'; case vscode.SymbolKind.Constant: return 'Constant'; case vscode.SymbolKind.String: return 'String'; case vscode.SymbolKind.Number: return 'Number'; case vscode.SymbolKind.Boolean: return 'Boolean'; case vscode.SymbolKind.Array: return 'Array'; case vscode.SymbolKind.Object: return 'Object'; case vscode.SymbolKind.Key: return 'Key'; case vscode.SymbolKind.Null: return 'Null'; case vscode.SymbolKind.EnumMember: return 'EnumMember'; case vscode.SymbolKind.Struct: return 'Struct'; case vscode.SymbolKind.Event: return 'Event'; case vscode.SymbolKind.Operator: return 'Operator'; case vscode.SymbolKind.TypeParameter: return 'TypeParameter'; default: return 'Unknown'; } } /** * Converts a workspace URI to a path relative to the workspace root * @param uri The URI to convert * @returns Path relative to workspace root */ function uriToWorkspacePath(uri: vscode.Uri): string { if (!vscode.workspace.workspaceFolders) { return uri.fsPath; } const workspaceFolder = vscode.workspace.workspaceFolders[0]; const workspaceRoot = workspaceFolder.uri.fsPath; // Convert to relative path const relativePath = path.relative(workspaceRoot, uri.fsPath); return relativePath; } /** * Get a preview of the code at a specific line * @param uri The URI of the document * @param line The line number (0-based) * @returns The line content as a string or undefined if not available */ async function getPreview(uri: vscode.Uri, line?: number): Promise<string | undefined> { if (line === undefined) { return undefined; } try { // Try to open the document from VS Code's text document manager const documents = vscode.workspace.textDocuments; let document = documents.find(doc => doc.uri.toString() === uri.toString()); // If document is not already open, try to read it from the file system if (!document) { try { const content = await vscode.workspace.fs.readFile(uri); const text = Buffer.from(content).toString('utf8'); const lines = text.split(/\r?\n/); if (line >= 0 && line < lines.length) { return lines[line].trim(); } } catch (error) { logger.warn(`[getPreview] Could not read file: ${error instanceof Error ? error.message : String(error)}`); return undefined; } } else { // Document is open, get the line directly if (line >= 0 && line < document.lineCount) { return document.lineAt(line).text.trim(); } } } catch (error) { logger.warn(`[getPreview] Error getting preview: ${error instanceof Error ? error.message : String(error)}`); } return undefined; } /** * Get the text content of a specific line in a file * @param uri The URI of the document * @param line The line number (0-based) * @returns The text content of the line or undefined if line doesn't exist */ async function getLineText(uri: vscode.Uri, line: number): Promise<string | undefined> { try { // Open the document using VS Code's API const document = await vscode.workspace.openTextDocument(uri); // Check if the line exists if (line >= 0 && line < document.lineCount) { return document.lineAt(line).text; } return undefined; } catch (error) { logger.warn(`[getLineText] Error getting line text: ${error instanceof Error ? error.message : String(error)}`); return undefined; } } /** * Find the first occurrence of a symbol in a line of text * @param lineText The text content of the line * @param symbolName The exact symbol name to search for * @returns The character position (index) where the symbol starts, or -1 if not found */ function findSymbolInLine(lineText: string, symbolName: string): number { return lineText.indexOf(symbolName); } /** * Process hover content to extract string value * @param content The hover content item * @returns String representation of the content */ function processHoverContent(content: any): string { if (typeof content === 'string') { return content; } else if (content && typeof content === 'object' && 'value' in content) { return content.value; } return String(content); } /** * Get hover information for a symbol at a specific position in a document * @param uri The URI of the text document * @param position The position of the symbol * @returns Hover information for the symbol */ export async function getSymbolHoverInfo( uri: vscode.Uri, position: vscode.Position ): Promise<{ hovers: Array<{ contents: string[]; range?: { start: { line: number; character: number }; end: { line: number; character: number }; }; preview?: string; }>; }> { logger.info(`[getSymbolHoverInfo] Getting hover info for ${uri.toString()} at position (${position.line},${position.character})`); try { // Execute the hover provider const commandResult = await vscode.commands.executeCommand<vscode.Hover[]>( 'vscode.executeHoverProvider', uri, position ) || []; logger.info(`[getSymbolHoverInfo] Found ${commandResult.length} hover results`); // Map the hover results to a more friendly format const hovers = await Promise.all(commandResult.map(async hover => { // Process the contents let contents: string[] = []; if (Array.isArray(hover.contents)) { contents = hover.contents.map(processHoverContent); } else if (hover.contents) { contents = [processHoverContent(hover.contents)]; } // Format the range if available const range = hover.range ? { start: { line: hover.range.start.line, character: hover.range.start.character }, end: { line: hover.range.end.line, character: hover.range.end.character } } : undefined; // Get a preview of the code if range is available const preview = await getPreview(uri, hover.range?.start.line); return { contents, range, preview }; })); return { hovers }; } catch (error) { logger.error(`[getSymbolHoverInfo] Error: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Search for symbols across the workspace * @param query The search query * @param maxResults Maximum number of results to return * @returns Array of formatted symbol information objects */ export async function searchWorkspaceSymbols(query: string, maxResults: number = 10): Promise<{ symbols: Array<{ name: string; kind: string; location: string; containerName?: string; range?: { start: { line: number; character: number }; end: { line: number; character: number }; }; }>; total: number; }> { logger.info(`[searchWorkspaceSymbols] Starting with query: "${query}", maxResults: ${maxResults}`); try { // Execute the workspace symbol provider const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>( 'vscode.executeWorkspaceSymbolProvider', query ) || []; logger.info(`[searchWorkspaceSymbols] Found ${symbols.length} symbols`); // Get total count before limiting const totalCount = symbols.length; // Apply limit const limitedSymbols = symbols.slice(0, maxResults); // Format the results const result = { symbols: limitedSymbols.map(symbol => { const formatted = { name: symbol.name, kind: symbolKindToString(symbol.kind), location: `${uriToWorkspacePath(symbol.location.uri)}:${symbol.location.range.start.line + 1}:${symbol.location.range.start.character}`, range: { start: { line: symbol.location.range.start.line + 1, character: symbol.location.range.start.character }, end: { line: symbol.location.range.end.line + 1, character: symbol.location.range.end.character } } }; // Add container name if available if (symbol.containerName) { Object.assign(formatted, { containerName: symbol.containerName }); } return formatted; }), total: totalCount }; return result; } catch (error) { logger.error(`[searchWorkspaceSymbols] Error: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Get all document symbols from a file in hierarchical format * @param uri The URI of the document * @param maxDepth Maximum nesting depth to display (optional) * @returns Formatted symbol information with hierarchy */ export async function getDocumentSymbols( uri: vscode.Uri, maxDepth?: number ): Promise<{ symbols: Array<{ name: string; detail?: string; kind: string; range: { start: { line: number; character: number }; end: { line: number; character: number }; }; selectionRange: { start: { line: number; character: number }; end: { line: number; character: number }; }; depth: number; children?: any[]; }>; total: number; totalByKind: Record<string, number>; }> { logger.info(`[getDocumentSymbols] Getting symbols for ${uri.toString()}, maxDepth: ${maxDepth}`); try { // Execute the document symbol provider const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>( 'vscode.executeDocumentSymbolProvider', uri ) || []; logger.info(`[getDocumentSymbols] Found ${symbols.length} top-level symbols`); const flatSymbols: any[] = []; const kindCounts: Record<string, number> = {}; // Recursive function to process symbols and their children function processSymbols(symbols: vscode.DocumentSymbol[], depth: number = 0) { for (const symbol of symbols) { // Skip if max depth exceeded if (maxDepth !== undefined && depth > maxDepth) { continue; } const kindString = symbolKindToString(symbol.kind); kindCounts[kindString] = (kindCounts[kindString] || 0) + 1; const processedSymbol = { name: symbol.name, detail: symbol.detail || undefined, kind: kindString, range: { start: { line: symbol.range.start.line + 1, character: symbol.range.start.character }, end: { line: symbol.range.end.line + 1, character: symbol.range.end.character } }, selectionRange: { start: { line: symbol.selectionRange.start.line + 1, character: symbol.selectionRange.start.character }, end: { line: symbol.selectionRange.end.line + 1, character: symbol.selectionRange.end.character } }, depth, children: symbol.children && symbol.children.length > 0 ? symbol.children.length : undefined }; flatSymbols.push(processedSymbol); // Recursively process children if (symbol.children && symbol.children.length > 0) { processSymbols(symbol.children, depth + 1); } } } processSymbols(symbols); return { symbols: flatSymbols, total: flatSymbols.length, totalByKind: kindCounts }; } catch (error) { logger.error(`[getDocumentSymbols] Error: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Registers MCP symbol-related tools with the server * @param server MCP server instance */ export function registerSymbolTools(server: McpServer): void { // Add search_symbols_code tool server.tool( 'search_symbols_code', `Searches for symbols (functions, classes, variables) across workspace using fuzzy matching. WHEN TO USE: Finding function/class definitions, exploring project structure, locating specific elements. Search: Supports partial terms (e.g., 'createW' matches 'createWorkspaceFile'). Returns location and container info. Limit results to avoid overwhelming output - increase maxResults only if needed.`, { query: z.string().describe('The search query for symbol names'), maxResults: z.number().optional().default(10).describe('Maximum number of results to return (default: 10)') }, async ({ query, maxResults = 10 }): Promise<CallToolResult> => { logger.info(`[search_symbols_code] Tool called with query="${query}", maxResults=${maxResults}`); try { logger.info('[search_symbols_code] Searching workspace symbols'); const result = await searchWorkspaceSymbols(query, maxResults); let resultText: string; if (result.symbols.length === 0) { resultText = `No symbols found matching query "${query}".`; } else { resultText = `Found ${result.total} symbols matching query "${query}"`; if (result.total > maxResults) { resultText += ` (showing first ${maxResults})`; } resultText += ":\n\n"; for (const symbol of result.symbols) { resultText += `${symbol.name} (${symbol.kind})`; if (symbol.containerName) { resultText += ` in ${symbol.containerName}`; } resultText += `\nLocation: ${symbol.location}\n\n`; } } const callResult: CallToolResult = { content: [ { type: 'text', text: resultText } ] }; logger.info('[search_symbols_code] Successfully completed'); return callResult; } catch (error) { logger.error(`[search_symbols_code] Error in tool: ${error instanceof Error ? error.message : String(error)}`); throw error; } } ); // Add get_symbol_definition_code tool with updated parameters server.tool( 'get_symbol_definition_code', `Gets definition information for a symbol using hover data (type, docs, source). WHEN TO USE: Understanding what a symbol represents, checking function signatures, quick API reference. USE search_symbols_code instead for: finding symbols by name across the project. Requires exact symbol name and line number. If symbol not found on line, returns clear message.`, { path: z.string().describe('The path to the file containing the symbol'), line: z.number().describe('The line number of the symbol (1-based)'), symbol: z.string().describe('The symbol name to look for on the specified line') }, async ({ path, line, symbol }): Promise<CallToolResult> => { logger.info(`[get_symbol_definition_code] Tool called with path="${path}", line=${line}, symbol="${symbol}"`); // Convert 1-based input to 0-based for VS Code API const zeroBasedLine = line - 1; try { if (!vscode.workspace.workspaceFolders) { throw new Error('No workspace folder open'); } const workspaceRoot = vscode.workspace.workspaceFolders[0].uri.fsPath; const fullPath = require('path').resolve(workspaceRoot, path); const uri = vscode.Uri.file(fullPath); // Check if file exists try { await vscode.workspace.fs.stat(uri); } catch (error) { throw new Error(`File not found: ${path}`); } // Get the content of the specified line const lineText = await getLineText(uri, zeroBasedLine); if (!lineText) { throw new Error(`Line ${line} not found in file: ${path}`); } // Find the character position of the symbol in the line const character = findSymbolInLine(lineText, symbol); if (character === -1) { return { content: [ { type: 'text', text: `Symbol "${symbol}" not found on line ${line} in file: ${path}` } ] }; } // Create a position object const position = new vscode.Position(zeroBasedLine, character); // Get hover information const hoverResult = await getSymbolHoverInfo(uri, position); let resultText: string; if (hoverResult.hovers.length === 0) { resultText = `No definition information found for symbol "${symbol}" at ${path}:${line}:${character}.`; } else { resultText = `Definition information for symbol "${symbol}" at ${path}:${line}:${character}:\n\n`; for (const hover of hoverResult.hovers) { // Add preview if available if (hover.preview) { resultText += `Code context: \`${hover.preview}\`\n\n`; } // Add contents for (const content of hover.contents) { resultText += `${content}\n\n`; } // Add range if available if (hover.range) { resultText += `Symbol range: [${hover.range.start.line}:${hover.range.start.character}] to [${hover.range.end.line}:${hover.range.end.character}]\n\n`; } } } const callResult: CallToolResult = { content: [ { type: 'text', text: resultText } ] }; logger.info('[get_symbol_definition_code] Successfully completed'); return callResult; } catch (error) { logger.error(`[get_symbol_definition_code] Error in tool: ${error instanceof Error ? error.message : String(error)}`); throw error; } } ); // Add get_document_symbols_code tool server.tool( 'get_document_symbols_code', `Gets complete symbol outline for a file showing hierarchical structure and line numbers. WHEN TO USE: Understanding file structure, getting overview of all symbols, finding symbol positions. This tool should be be preferred over reading the file using read_file_code when only an overview of the file is needed. USE search_symbols_code instead for: finding specific symbols by name across the project. Shows classes, functions, methods, variables with line ranges. Use maxDepth for large files to avoid deep nesting.`, { path: z.string().describe('The path to the file to analyze (relative to workspace)'), maxDepth: z.number().optional().describe('Maximum nesting depth to display (optional)') }, async ({ path, maxDepth }): Promise<CallToolResult> => { logger.info(`[get_document_symbols_code] Tool called with path="${path}", maxDepth=${maxDepth}`); try { if (!vscode.workspace.workspaceFolders) { throw new Error('No workspace folder open'); } const workspaceRoot = vscode.workspace.workspaceFolders[0].uri.fsPath; const fullPath = require('path').resolve(workspaceRoot, path); const uri = vscode.Uri.file(fullPath); // Check if file exists try { await vscode.workspace.fs.stat(uri); } catch (error) { throw new Error(`File not found: ${path}`); } logger.info('[get_document_symbols_code] Getting document symbols'); const result = await getDocumentSymbols(uri, maxDepth); let resultText: string; if (result.symbols.length === 0) { resultText = `No symbols found in file: ${path}`; } else { resultText = `Document symbols for ${path} (${result.total} total symbols):\n\n`; // Add summary by kind const kindSummary = Object.entries(result.totalByKind) .map(([kind, count]) => `${count} ${kind}${count !== 1 ? 's' : ''}`) .join(', '); resultText += `Summary: ${kindSummary}\n\n`; // Add hierarchical symbol listing for (const symbol of result.symbols) { const indent = ' '.repeat(symbol.depth); resultText += `${indent}${symbol.name} (${symbol.kind})`; if (symbol.detail) { resultText += ` - ${symbol.detail}`; } resultText += `\n${indent} Range: ${symbol.range.start.line}:${symbol.range.start.character}-${symbol.range.end.line}:${symbol.range.end.character}`; if (symbol.children !== undefined) { resultText += ` | Children: ${symbol.children}`; } resultText += '\n\n'; } } const callResult: CallToolResult = { content: [ { type: 'text', text: resultText } ] }; logger.info('[get_document_symbols_code] Successfully completed'); return callResult; } catch (error) { logger.error(`[get_document_symbols_code] Error in tool: ${error instanceof Error ? error.message : String(error)}`); throw error; } } ); }

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/juehang/vscode-mcp-server'

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