Skip to main content
Glama
file-tools.ts11.8 kB
import * as vscode from 'vscode'; import * as path from 'path'; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from 'zod'; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; // Type for file listing results export type FileListingResult = Array<{path: string, type: 'file' | 'directory'}>; // Type for the file listing callback function export type FileListingCallback = (path: string, recursive: boolean) => Promise<FileListingResult>; // Default maximum character count const DEFAULT_MAX_CHARACTERS = 100000; /** * Lists files and directories in the VS Code workspace * @param workspacePath The path within the workspace to list files from * @param recursive Whether to list files recursively * @returns Array of file and directory entries */ export async function listWorkspaceFiles(workspacePath: string, recursive: boolean = false): Promise<FileListingResult> { console.log(`[listWorkspaceFiles] Starting with path: ${workspacePath}, recursive: ${recursive}`); if (!vscode.workspace.workspaceFolders) { throw new Error('No workspace folder is open'); } const workspaceFolder = vscode.workspace.workspaceFolders[0]; const workspaceUri = workspaceFolder.uri; // Create URI for the target directory const targetUri = vscode.Uri.joinPath(workspaceUri, workspacePath); console.log(`[listWorkspaceFiles] Target URI: ${targetUri.fsPath}`); async function processDirectory(dirUri: vscode.Uri, currentPath: string = ''): Promise<FileListingResult> { const entries = await vscode.workspace.fs.readDirectory(dirUri); const result: FileListingResult = []; for (const [name, type] of entries) { const entryPath = currentPath ? path.join(currentPath, name) : name; const itemType: 'file' | 'directory' = (type & vscode.FileType.Directory) ? 'directory' : 'file'; result.push({ path: entryPath, type: itemType }); if (recursive && itemType === 'directory') { const subDirUri = vscode.Uri.joinPath(dirUri, name); const subEntries = await processDirectory(subDirUri, entryPath); result.push(...subEntries); } } return result; } try { const result = await processDirectory(targetUri); console.log(`[listWorkspaceFiles] Found ${result.length} entries`); return result; } catch (error) { console.error('[listWorkspaceFiles] Error:', error); throw error; } } /** * Reads a file from the VS Code workspace with character limit check * @param workspacePath The path within the workspace to the file * @param encoding Encoding to convert the file content to a string. Use 'base64' for base64-encoded string * @param maxCharacters Maximum character count (default: 100,000) * @param startLine The start line number (0-based, inclusive). Use -1 to read from the beginning. * @param endLine The end line number (0-based, inclusive). Use -1 to read to the end. * @returns File content as string (either text-encoded or base64) */ export async function readWorkspaceFile( workspacePath: string, encoding: string = 'utf-8', maxCharacters: number = DEFAULT_MAX_CHARACTERS, startLine: number = -1, endLine: number = -1 ): Promise<string> { console.log(`[readWorkspaceFile] Starting with path: ${workspacePath}, encoding: ${encoding}, maxCharacters: ${maxCharacters}, startLine: ${startLine}, endLine: ${endLine}`); if (!vscode.workspace.workspaceFolders) { throw new Error('No workspace folder is open'); } const workspaceFolder = vscode.workspace.workspaceFolders[0]; const workspaceUri = workspaceFolder.uri; // Create URI for the target file const fileUri = vscode.Uri.joinPath(workspaceUri, workspacePath); console.log(`[readWorkspaceFile] File URI: ${fileUri.fsPath}`); try { // Read the file content as Uint8Array const fileContent = await vscode.workspace.fs.readFile(fileUri); console.log(`[readWorkspaceFile] File read successfully, size: ${fileContent.byteLength} bytes`); if (encoding === 'base64') { // Special case for base64 encoding if (fileContent.byteLength > maxCharacters) { throw new Error(`File content exceeds the maximum character limit (approx. ${fileContent.byteLength} bytes vs ${maxCharacters} allowed)`); } // For base64, we cannot extract lines meaningfully, so we ignore startLine and endLine if (startLine >= 0 || endLine >= 0) { console.warn(`[readWorkspaceFile] Line numbers specified for base64 encoding, ignoring`); } return Buffer.from(fileContent).toString('base64'); } else { // Regular text encoding (utf-8, latin1, etc.) const textDecoder = new TextDecoder(encoding); const textContent = textDecoder.decode(fileContent); // Check if the character count exceeds the limit if (textContent.length > maxCharacters) { throw new Error(`File content exceeds the maximum character limit (${textContent.length} vs ${maxCharacters} allowed)`); } // If line numbers are specified and valid, extract just those lines if (startLine >= 0 || endLine >= 0) { // Split the content into lines const lines = textContent.split('\n'); // Set effective start and end lines const effectiveStartLine = startLine >= 0 ? startLine : 0; const effectiveEndLine = endLine >= 0 ? Math.min(endLine, lines.length - 1) : lines.length - 1; // Validate line numbers if (effectiveStartLine >= lines.length) { throw new Error(`Start line ${effectiveStartLine + 1} is out of range (1-${lines.length})`); } // Make sure endLine is not less than startLine if (effectiveEndLine < effectiveStartLine) { throw new Error(`End line ${effectiveEndLine + 1} is less than start line ${effectiveStartLine + 1}`); } // Extract the requested lines and join them back together const partialContent = lines.slice(effectiveStartLine, effectiveEndLine + 1).join('\n'); console.log(`[readWorkspaceFile] Returning lines ${effectiveStartLine + 1}-${effectiveEndLine + 1}, length: ${partialContent.length} characters`); return partialContent; } return textContent; } } catch (error) { console.error('[readWorkspaceFile] Error:', error); throw error; } } /** * Registers MCP file-related tools with the server * @param server MCP server instance * @param fileListingCallback Callback function for file listing operations */ export function registerFileTools( server: McpServer, fileListingCallback: FileListingCallback ): void { // Add list_files tool server.tool( 'list_files_code', `Explores directory structure in VS Code workspace. WHEN TO USE: Understanding project structure, finding files before read/modify operations. CRITICAL: NEVER set recursive=true on root directory (.) - output too large. Use recursive only on specific subdirectories. Returns files and directories at specified path. Start with path='.' to explore root, then dive into specific subdirectories with recursive=true.`, { path: z.string().describe('The path to list files from'), recursive: z.boolean().optional().default(false).describe('Whether to list files recursively') }, async ({ path, recursive = false }): Promise<CallToolResult> => { console.log(`[list_files] Tool called with path=${path}, recursive=${recursive}`); if (!fileListingCallback) { console.error('[list_files] File listing callback not set'); throw new Error('File listing callback not set'); } try { console.log('[list_files] Calling file listing callback'); const files = await fileListingCallback(path, recursive); console.log(`[list_files] Callback returned ${files.length} items`); const result: CallToolResult = { content: [ { type: 'text', text: JSON.stringify(files, null, 2) } ] }; console.log('[list_files] Successfully completed'); return result; } catch (error) { console.error('[list_files] Error in tool:', error); throw error; } } ); // Update read_file tool with line number parameters server.tool( 'read_file_code', `Retrieves file contents with size limits and partial reading support. WHEN TO USE: Reading code, config files, analyzing implementations. Files >100k chars will fail. Encoding: Text encodings (utf-8, latin1, etc.) for text files, 'base64' for base64-encoded string. Line numbers: Use startLine/endLine (1-based) for large files to read specific sections only. If file too large: Use startLine/endLine to read relevant sections only.`, { path: z.string().describe('The path to the file to read'), encoding: z.string().optional().default('utf-8').describe('Encoding to convert the file content to a string. Use "base64" for base64-encoded string'), maxCharacters: z.number().optional().default(DEFAULT_MAX_CHARACTERS).describe('Maximum character count (default: 100,000)'), startLine: z.number().optional().default(-1).describe('The start line number (1-based, inclusive). Default: read from beginning, denoted by -1'), endLine: z.number().optional().default(-1).describe('The end line number (1-based, inclusive). Default: read to end, denoted by -1') }, async ({ path, encoding = 'utf-8', maxCharacters = DEFAULT_MAX_CHARACTERS, startLine = -1, endLine = -1 }): Promise<CallToolResult> => { console.log(`[read_file] Tool called with path=${path}, encoding=${encoding}, maxCharacters=${maxCharacters}, startLine=${startLine}, endLine=${endLine}`); // Convert 1-based input to 0-based for VS Code API const zeroBasedStartLine = startLine > 0 ? startLine - 1 : startLine; const zeroBasedEndLine = endLine > 0 ? endLine - 1 : endLine; try { console.log('[read_file] Reading file'); const content = await readWorkspaceFile(path, encoding, maxCharacters, zeroBasedStartLine, zeroBasedEndLine); const result: CallToolResult = { content: [ { type: 'text', text: content } ] }; console.log(`[read_file] File read successfully, length: ${content.length} characters`); return result; } catch (error) { console.error('[read_file] Error in tool:', 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