Skip to main content
Glama

Edit-MCP

edit-instance-manager.ts10.7 kB
import { spawn, ChildProcess } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import { v4 as uuidv4 } from 'uuid'; import { EventEmitter } from 'events'; export interface EditCommand { type: 'open' | 'close' | 'save' | 'edit' | 'find' | 'replace' | 'goto'; params: any; } export interface EditResult { success: boolean; message?: string; data?: any; } export interface EditInstanceState { sessionId: string; openFiles: string[]; activeFile?: string; running: boolean; } export interface MultiFileEditOperation { files: string[]; operation: EditCommand; } export interface ComplexEditOperation { type: string; params: any; } export class EditInstance extends EventEmitter { private process: ChildProcess; private sessionId: string; private openFiles: Set<string> = new Set(); private activeFile?: string; private running: boolean = false; private outputBuffer: string = ''; private errorBuffer: string = ''; private commandQueue: Array<{ command: string; resolve: (result: string) => void; reject: (error: Error) => void; }> = []; private commandInProgress: boolean = false; constructor(process: ChildProcess, sessionId: string) { super(); this.process = process; this.sessionId = sessionId; this.setupProcessHandlers(); } private setupProcessHandlers(): void { this.process.stdout?.on('data', (data: Buffer) => { const output = data.toString(); this.outputBuffer += output; this.emit('stdout', output); this.checkCommandCompletion(); }); this.process.stderr?.on('data', (data: Buffer) => { const error = data.toString(); this.errorBuffer += error; this.emit('stderr', error); }); this.process.on('exit', (code: number | null) => { this.running = false; this.emit('exit', code); // Reject any pending commands while (this.commandQueue.length > 0) { const command = this.commandQueue.shift(); if (command) { command.reject(new Error(`Edit process exited with code ${code}`)); } } }); this.running = true; } private checkCommandCompletion(): void { if (this.commandQueue.length === 0 || !this.commandInProgress) { return; } // Check if the command has completed // This is a simplified approach; in a real implementation, we would need // to look for specific markers in the output to determine completion if (this.outputBuffer.includes('Command completed')) { const command = this.commandQueue.shift(); if (command) { this.commandInProgress = false; command.resolve(this.outputBuffer); this.outputBuffer = ''; this.errorBuffer = ''; this.processNextCommand(); } } } private processNextCommand(): void { if (this.commandQueue.length === 0 || this.commandInProgress) { return; } const command = this.commandQueue[0]; this.commandInProgress = true; this.process.stdin?.write(command.command + '\n'); } public async openFile(filePath: string): Promise<void> { await this.executeCommand(`open ${filePath}`); this.openFiles.add(filePath); this.activeFile = filePath; } public async closeFile(filePath: string): Promise<void> { await this.executeCommand(`close ${filePath}`); this.openFiles.delete(filePath); if (this.activeFile === filePath) { this.activeFile = undefined; } } public async executeCommand(command: string): Promise<string> { return new Promise<string>((resolve, reject) => { if (!this.running) { reject(new Error('Edit process is not running')); return; } this.commandQueue.push({ command, resolve, reject }); if (!this.commandInProgress) { this.processNextCommand(); } }); } public async getState(): Promise<EditInstanceState> { return { sessionId: this.sessionId, openFiles: Array.from(this.openFiles), activeFile: this.activeFile, running: this.running }; } public async terminate(): Promise<void> { if (!this.running) { return; } // Try to gracefully exit try { await this.executeCommand('exit'); } catch (error) { // Ignore errors } // Force kill if still running if (this.running) { this.process.kill(); } } } export class EditInstanceManager { private instances: Map<string, EditInstance> = new Map(); private editExecutablePath: string; private maxInstances: number; private instanceTimeout: number; constructor(editExecutablePath: string, maxInstances: number = 5, instanceTimeout: number = 300000) { this.editExecutablePath = editExecutablePath; this.maxInstances = maxInstances; this.instanceTimeout = instanceTimeout; } public async createInstance(sessionId: string = uuidv4()): Promise<EditInstance> { if (this.instances.size >= this.maxInstances) { throw new Error(`Maximum number of Edit instances (${this.maxInstances}) reached`); } const process = spawn(this.editExecutablePath, [], { stdio: ['pipe', 'pipe', 'pipe'], shell: false }); const instance = new EditInstance(process, sessionId); this.instances.set(sessionId, instance); // Set up timeout to destroy the instance if it's not used setTimeout(() => { if (this.instances.has(sessionId)) { console.log(`Edit instance ${sessionId} timed out, destroying...`); this.destroyInstance(sessionId).catch(console.error); } }, this.instanceTimeout); return instance; } public async destroyInstance(sessionId: string): Promise<void> { const instance = this.instances.get(sessionId); if (!instance) { throw new Error(`Edit instance ${sessionId} not found`); } await instance.terminate(); this.instances.delete(sessionId); } public async executeEditCommand(sessionId: string, command: EditCommand): Promise<EditResult> { const instance = this.instances.get(sessionId); if (!instance) { throw new Error(`Edit instance ${sessionId} not found`); } try { let result: string; switch (command.type) { case 'open': await instance.openFile(command.params.path); return { success: true }; case 'close': await instance.closeFile(command.params.path); return { success: true }; case 'save': result = await instance.executeCommand(`save ${command.params.path}`); return { success: true, message: result }; case 'edit': // This is a simplified approach; in a real implementation, we would need // to handle different types of edits (insert, delete, replace, etc.) result = await instance.executeCommand(`edit ${JSON.stringify(command.params)}`); return { success: true, message: result }; case 'find': result = await instance.executeCommand(`find ${command.params.pattern}`); return { success: true, message: result }; case 'replace': result = await instance.executeCommand(`replace ${command.params.pattern} ${command.params.replacement}`); return { success: true, message: result }; case 'goto': result = await instance.executeCommand(`goto ${command.params.line} ${command.params.column}`); return { success: true, message: result }; default: throw new Error(`Unknown command type: ${command.type}`); } } catch (error: any) { return { success: false, message: error.message }; } } public async createEditSession(files: string[]): Promise<string> { const sessionId = uuidv4(); const instance = await this.createInstance(sessionId); // Open all files for (const file of files) { await instance.openFile(file); } return sessionId; } public async closeEditSession(sessionId: string): Promise<void> { await this.destroyInstance(sessionId); } public async performComplexEdit(sessionId: string, operation: ComplexEditOperation): Promise<EditResult> { const instance = this.instances.get(sessionId); if (!instance) { throw new Error(`Edit instance ${sessionId} not found`); } try { // This is a simplified approach; in a real implementation, we would need // to handle different types of complex edits const result = await instance.executeCommand(`complex-edit ${JSON.stringify(operation)}`); return { success: true, message: result }; } catch (error: any) { return { success: false, message: error.message }; } } public async coordinateMultiFileEdit(operation: MultiFileEditOperation): Promise<EditResult[]> { const sessionId = await this.createEditSession(operation.files); const results: EditResult[] = []; try { for (const file of operation.files) { // Clone the operation for each file const fileOperation: EditCommand = { ...operation.operation, params: { ...operation.operation.params, path: file } }; const result = await this.executeEditCommand(sessionId, fileOperation); results.push(result); } } finally { // Always close the session await this.closeEditSession(sessionId).catch(console.error); } return results; } public async getInstanceState(sessionId: string): Promise<EditInstanceState> { const instance = this.instances.get(sessionId); if (!instance) { throw new Error(`Edit instance ${sessionId} not found`); } return await instance.getState(); } public async getAllInstanceStates(): Promise<Map<string, EditInstanceState>> { const states = new Map<string, EditInstanceState>(); for (const [sessionId, instance] of this.instances.entries()) { states.set(sessionId, await instance.getState()); } return states; } public dispose(): void { // Destroy all instances for (const [sessionId, instance] of this.instances.entries()) { instance.terminate().catch(console.error); } this.instances.clear(); } }

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/mixelpixx/microsoft-edit-mcp'

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