Skip to main content
Glama

Edit-MCP

mcp-server.ts13.6 kB
import { spawn, ChildProcess } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import chalk from 'chalk'; import { JSONRPC_VERSION, LATEST_PROTOCOL_VERSION, JSONRPCRequest, JSONRPCResponse, JSONRPCError, JSONRPCNotification, RequestId, ServerCapabilities, Implementation, InitializeRequest, InitializeResult, Tool, Resource, TextResourceContents, BlobResourceContents, CallToolResult, TextContent, INVALID_PARAMS, METHOD_NOT_FOUND, INTERNAL_ERROR } from './mcp-types'; import { createSuccessResponse, createErrorResponse, createNotification, createRequest, parseJsonRpc, isRequest, isNotification, getPayload } from './jsonrpc-wrapper'; export interface MCPServerOptions { name: string; version: string; editExecutablePath?: string; maxEditInstances?: number; instanceTimeout?: number; capabilities?: Partial<ServerCapabilities>; instructions?: string; } export class MCPServer { private name: string; private version: string; private editExecutablePath: string; private maxEditInstances: number; private instanceTimeout: number; private capabilities: ServerCapabilities; private instructions?: string; private initialized = false; private requestHandlers: Map<string, (params: any) => Promise<any>> = new Map(); private notificationHandlers: Map<string, (params: any) => void> = new Map(); private tools: Map<string, Tool> = new Map(); private resources: Map<string, Resource> = new Map(); private editInstances: Map<string, ChildProcess> = new Map(); private nextRequestId = 1; constructor(options: MCPServerOptions) { this.name = options.name; this.version = options.version; this.editExecutablePath = options.editExecutablePath || this.findEditExecutable(); this.maxEditInstances = options.maxEditInstances || 5; this.instanceTimeout = options.instanceTimeout || 300000; // 5 minutes this.instructions = options.instructions; // Set up default capabilities this.capabilities = { resources: { subscribe: true, listChanged: true }, tools: { listChanged: true }, logging: {}, ...options.capabilities }; // Register core request handlers this.registerRequestHandler('initialize', this.handleInitialize.bind(this)); this.registerRequestHandler('ping', this.handlePing.bind(this)); this.registerRequestHandler('resources/list', this.handleResourcesList.bind(this)); this.registerRequestHandler('resources/read', this.handleResourcesRead.bind(this)); this.registerRequestHandler('tools/list', this.handleToolsList.bind(this)); this.registerRequestHandler('tools/call', this.handleToolsCall.bind(this)); // Register core notification handlers this.registerNotificationHandler('notifications/initialized', this.handleInitialized.bind(this)); } /** * Attempts to find the Edit executable in common locations */ private findEditExecutable(): string { // Check if the executable is in the PATH const isWindows = process.platform === 'win32'; const executableName = isWindows ? 'edit.exe' : 'edit'; // Common locations to check const commonLocations = [ // Windows locations 'C:\\Program Files\\Microsoft\\Edit\\edit.exe', 'C:\\Program Files (x86)\\Microsoft\\Edit\\edit.exe', // Unix-like locations '/usr/bin/edit', '/usr/local/bin/edit', '/opt/microsoft/edit/edit' ]; // First check if it's in the PATH try { const which = require('child_process').execSync( isWindows ? 'where edit' : 'which edit', { encoding: 'utf8' } ).trim(); if (which && fs.existsSync(which)) { return which; } } catch (error) { // Not in PATH, continue checking common locations } // Check common locations for (const location of commonLocations) { if (fs.existsSync(location)) { return location; } } // Default to just the executable name and hope it's in the PATH return executableName; } /** * Registers a request handler for a specific method */ public registerRequestHandler(method: string, handler: (params: any) => Promise<any>): void { this.requestHandlers.set(method, handler); } /** * Registers a notification handler for a specific method */ public registerNotificationHandler(method: string, handler: (params: any) => void): void { this.notificationHandlers.set(method, handler); } /** * Registers a tool that can be called by clients */ public registerTool(tool: Tool): void { this.tools.set(tool.name, tool); } /** * Registers a resource that can be accessed by clients */ public registerResource(resource: Resource): void { this.resources.set(resource.uri, resource); } /** * Handles an incoming JSON-RPC message */ public async handleMessage(message: string): Promise<string | null> { try { const parsed = parseJsonRpc(message); if (Array.isArray(parsed)) { // Handle batch requests const responses = await Promise.all( parsed.map(item => this.processMessage(item)) ); // Filter out null responses (notifications) const validResponses = responses.filter(r => r !== null); if (validResponses.length === 0) { return null; } return JSON.stringify(validResponses); } else { // Handle single request const response = await this.processMessage(parsed); if (response === null) { return null; } return JSON.stringify(response); } } catch (error) { console.error('Error parsing JSON-RPC message:', error); // Return a parse error const errorResponse = createErrorResponse(null, 'Parse error', -32700); return JSON.stringify(errorResponse); } } /** * Processes a single parsed JSON-RPC message */ private async processMessage(parsed: any): Promise<any | null> { if (isRequest(parsed)) { return await this.handleRequest(getPayload(parsed)); } else if (isNotification(parsed)) { await this.handleNotification(getPayload(parsed)); return null; } else { console.warn('Received an unexpected message type:', parsed.type); return null; } } /** * Handles a JSON-RPC request */ private async handleRequest(request: JSONRPCRequest): Promise<any> { console.log(chalk.blue(`Received request: ${request.method}`)); // Special case for initialize request if (request.method === 'initialize' && this.initialized) { return createErrorResponse( request.id, 'Server is already initialized', INVALID_PARAMS ); } // For all other requests, ensure the server is initialized if (request.method !== 'initialize' && !this.initialized) { return createErrorResponse( request.id, 'Server is not initialized', INVALID_PARAMS ); } const handler = this.requestHandlers.get(request.method); if (!handler) { return createErrorResponse( request.id, `Method not found: ${request.method}`, METHOD_NOT_FOUND ); } try { const result = await handler(request.params || {}); return createSuccessResponse(request.id, result); } catch (error: any) { console.error(`Error handling request ${request.method}:`, error); return createErrorResponse( request.id, error.message || 'Internal error', error.code || INTERNAL_ERROR, error.data ); } } /** * Handles a JSON-RPC notification */ private async handleNotification(notification: JSONRPCNotification): Promise<void> { console.log(chalk.green(`Received notification: ${notification.method}`)); const handler = this.notificationHandlers.get(notification.method); if (!handler) { console.warn(`No handler registered for notification: ${notification.method}`); return; } try { await handler(notification.params || {}); } catch (error) { console.error(`Error handling notification ${notification.method}:`, error); } } /** * Creates a JSON-RPC notification */ public createNotification(method: string, params?: any): string { const notification = createNotification(method, params); return JSON.stringify(notification); } /** * Creates a JSON-RPC request */ public createRequest(method: string, params?: any): { id: RequestId, message: string } { const id = this.nextRequestId++; const request = createRequest(id, method, params); return { id, message: JSON.stringify(request) }; } /** * Handles the initialize request */ private async handleInitialize(params: InitializeRequest['params']): Promise<InitializeResult> { console.log(chalk.yellow('Initializing server...')); console.log(`Client protocol version: ${params.protocolVersion}`); console.log(`Client info: ${params.clientInfo.name} ${params.clientInfo.version}`); return { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: this.capabilities, serverInfo: { name: this.name, version: this.version }, instructions: this.instructions }; } /** * Handles the initialized notification */ private handleInitialized(params: any): void { console.log(chalk.yellow('Server initialized')); this.initialized = true; } /** * Handles the ping request */ private async handlePing(): Promise<{}> { return {}; } /** * Handles the resources/list request */ private async handleResourcesList(params: any): Promise<{ resources: Resource[] }> { // In a real implementation, we would handle pagination here return { resources: Array.from(this.resources.values()) }; } /** * Handles the resources/read request */ private async handleResourcesRead(params: { uri: string }): Promise<{ contents: (TextResourceContents | BlobResourceContents)[] }> { const { uri } = params; if (!uri) { throw new Error('URI is required'); } const resource = this.resources.get(uri); if (!resource) { throw new Error(`Resource not found: ${uri}`); } // In a real implementation, we would read the resource content here // For now, we'll just return a placeholder return { contents: [ { uri, mimeType: resource.mimeType || 'text/plain', text: `Content of ${uri}` } as TextResourceContents ] }; } /** * Handles the tools/list request */ private async handleToolsList(params: any): Promise<{ tools: Tool[] }> { // In a real implementation, we would handle pagination here return { tools: Array.from(this.tools.values()) }; } /** * Handles the tools/call request */ private async handleToolsCall(params: { name: string, arguments?: any }): Promise<CallToolResult> { const { name, arguments: args } = params; if (!name) { throw new Error('Tool name is required'); } const tool = this.tools.get(name); if (!tool) { throw new Error(`Tool not found: ${name}`); } // In a real implementation, we would execute the tool here // For now, we'll just return a placeholder return { content: [ { type: 'text', text: `Executed tool ${name} with arguments: ${JSON.stringify(args)}` } as TextContent ] }; } /** * Spawns a new Edit instance */ public spawnEditInstance(sessionId: string, files: string[] = []): ChildProcess { if (this.editInstances.size >= this.maxEditInstances) { throw new Error(`Maximum number of Edit instances (${this.maxEditInstances}) reached`); } const args = [...files]; console.log(`Spawning Edit instance with args: ${args.join(' ')}`); const process = spawn(this.editExecutablePath, args, { stdio: ['pipe', 'pipe', 'pipe'], shell: false }); this.editInstances.set(sessionId, process); // Set up timeout to kill the process if it's not used setTimeout(() => { if (this.editInstances.has(sessionId)) { console.log(`Edit instance ${sessionId} timed out, killing...`); this.killEditInstance(sessionId); } }, this.instanceTimeout); // Handle process exit process.on('exit', (code) => { console.log(`Edit instance ${sessionId} exited with code ${code}`); this.editInstances.delete(sessionId); }); return process; } /** * Kills an Edit instance */ public killEditInstance(sessionId: string): void { const process = this.editInstances.get(sessionId); if (process) { process.kill(); this.editInstances.delete(sessionId); } } }

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