@gork-labs/secondbrain-mcp
Version:
Second Brain MCP Server - Agent team orchestration with dynamic tool discovery
293 lines (292 loc) • 10.9 kB
JavaScript
import * as fs from 'fs/promises';
import * as path from 'path';
import { logger } from '../utils/logger.js';
/**
* VS Code Tool Proxy - Provides sub-agents with REAL file system tool access
*
* Instead of trying to proxy back to VS Code, this implements the essential
* VS Code tools directly using Node.js APIs. This gives sub-agents actual
* tool capabilities and solves the "hallucination" problem.
*/
export class VSCodeToolProxy {
static instance;
// VS Code tools we can implement directly with Node.js
VSCODE_TOOLS = [
'read_file',
'list_dir',
'grep_search',
'file_search',
'create_file',
'replace_string_in_file'
];
constructor() { }
static getInstance() {
if (!VSCodeToolProxy.instance) {
VSCodeToolProxy.instance = new VSCodeToolProxy();
}
return VSCodeToolProxy.instance;
}
/**
* Check if a tool is a VS Code tool we can implement
*/
isVSCodeTool(toolName) {
return this.VSCODE_TOOLS.includes(toolName);
}
/**
* Execute a VS Code tool using Node.js APIs - REAL IMPLEMENTATION!
*/
async executeVSCodeTool(toolName, args) {
if (!this.isVSCodeTool(toolName)) {
return {
content: `ERROR: ${toolName} is not a supported tool`,
isError: true
};
}
try {
logger.info('Executing VS Code tool', { toolName, args });
switch (toolName) {
case 'read_file':
return await this.readFile(args);
case 'list_dir':
return await this.listDir(args);
case 'grep_search':
return await this.grepSearch(args);
case 'file_search':
return await this.fileSearch(args);
case 'create_file':
return await this.createFile(args);
case 'replace_string_in_file':
return await this.replaceStringInFile(args);
default:
return {
content: `ERROR: Tool ${toolName} not yet implemented`,
isError: true
};
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('VS Code tool execution failed', { toolName, args, error: errorMessage });
return {
content: `ERROR: ${toolName} failed: ${errorMessage}`,
isError: true
};
}
}
/**
* Read file content with line range support
*/
async readFile(args) {
const { filePath, startLine, endLine } = args;
if (!filePath) {
return { content: 'ERROR: filePath is required', isError: true };
}
// Security: Resolve and validate path
const resolvedPath = path.resolve(filePath);
try {
const content = await fs.readFile(resolvedPath, 'utf-8');
// Handle line range if specified
if (startLine !== undefined || endLine !== undefined) {
const lines = content.split('\n');
const start = Math.max(0, (startLine || 1) - 1); // Convert to 0-based
const end = endLine ? Math.min(lines.length, endLine) : lines.length;
const selectedLines = lines.slice(start, end);
const result = selectedLines.join('\n');
return {
content: `File: \`${filePath}\`. Lines ${start + 1} to ${end} (${lines.length} lines total): \n\`\`\`\n${result}\n\`\`\``,
isError: false
};
}
return {
content: `File: \`${filePath}\`. Full content:\n\`\`\`\n${content}\n\`\`\``,
isError: false
};
}
catch (error) {
return {
content: `ERROR: Could not read file ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
isError: true
};
}
}
/**
* List directory contents
*/
async listDir(args) {
const { path: dirPath } = args;
if (!dirPath) {
return { content: 'ERROR: path is required', isError: true };
}
try {
const resolvedPath = path.resolve(dirPath);
const items = await fs.readdir(resolvedPath, { withFileTypes: true });
const result = items.map(item => {
const name = item.isDirectory() ? `${item.name}/` : item.name;
return name;
}).join('\n');
return {
content: `Directory contents of ${dirPath}:\n${result}`,
isError: false
};
}
catch (error) {
return {
content: `ERROR: Could not list directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`,
isError: true
};
}
}
/**
* Search for text in files using grep-like functionality
*/
async grepSearch(args) {
const { query, includePattern, isRegexp } = args;
if (!query) {
return { content: 'ERROR: query is required', isError: true };
}
try {
// For now, implement a simple file search in current directory
const searchDir = includePattern ? path.dirname(includePattern) : process.cwd();
const files = await this.findMatchingFiles(searchDir, includePattern || '**/*');
const matches = [];
const searchRegex = isRegexp ? new RegExp(query, 'i') : new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
for (const file of files.slice(0, 10)) { // Limit to 10 files for performance
try {
const content = await fs.readFile(file, 'utf-8');
const lines = content.split('\n');
lines.forEach((line, index) => {
if (searchRegex.test(line)) {
matches.push(`${file}:${index + 1}:${line.trim()}`);
}
});
}
catch (e) {
// Skip files that can't be read
}
}
return {
content: matches.length > 0 ? matches.join('\n') : 'No matches found',
isError: false
};
}
catch (error) {
return {
content: `ERROR: Search failed: ${error instanceof Error ? error.message : String(error)}`,
isError: true
};
}
}
/**
* Search for files by name pattern
*/
async fileSearch(args) {
const { query } = args;
if (!query) {
return { content: 'ERROR: query is required', isError: true };
}
try {
const files = await this.findMatchingFiles(process.cwd(), query);
return {
content: files.length > 0 ? files.join('\n') : 'No files found matching pattern',
isError: false
};
}
catch (error) {
return {
content: `ERROR: File search failed: ${error instanceof Error ? error.message : String(error)}`,
isError: true
};
}
}
/**
* Create a new file with content
*/
async createFile(args) {
const { filePath, content } = args;
if (!filePath) {
return { content: 'ERROR: filePath is required', isError: true };
}
try {
const resolvedPath = path.resolve(filePath);
// Create directory if it doesn't exist
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
await fs.writeFile(resolvedPath, content || '', 'utf-8');
return {
content: `File created successfully: ${filePath}`,
isError: false
};
}
catch (error) {
return {
content: `ERROR: Could not create file ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
isError: true
};
}
}
/**
* Replace string in file
*/
async replaceStringInFile(args) {
const { filePath, oldString, newString } = args;
if (!filePath || !oldString) {
return { content: 'ERROR: filePath and oldString are required', isError: true };
}
try {
const resolvedPath = path.resolve(filePath);
const content = await fs.readFile(resolvedPath, 'utf-8');
const newContent = content.replace(oldString, newString || '');
await fs.writeFile(resolvedPath, newContent, 'utf-8');
return {
content: `String replaced successfully in ${filePath}`,
isError: false
};
}
catch (error) {
return {
content: `ERROR: Could not replace string in ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
isError: true
};
}
}
/**
* Helper: Find files matching a pattern
*/
async findMatchingFiles(dir, pattern) {
const files = [];
try {
const items = await fs.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory() && !item.name.startsWith('.')) {
// Recursively search subdirectories (limit depth)
if (fullPath.split(path.sep).length - process.cwd().split(path.sep).length < 5) {
const subFiles = await this.findMatchingFiles(fullPath, pattern);
files.push(...subFiles);
}
}
else if (item.isFile()) {
// Simple pattern matching
if (pattern.includes('*')) {
const regex = new RegExp(pattern.replace(/\*/g, '.*'), 'i');
if (regex.test(item.name)) {
files.push(fullPath);
}
}
else if (item.name.includes(pattern)) {
files.push(fullPath);
}
}
}
}
catch (e) {
// Skip directories we can't read
}
return files;
}
/**
* Get list of available VS Code tools
*/
getAvailableVSCodeTools() {
return [...this.VSCODE_TOOLS];
}
}