UNPKG

claude-code-emacs-mcp-server

Version:

MCP server for Claude Code Emacs integration

419 lines 17.5 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const emacs_bridge_js_1 = require("./emacs-bridge.js"); const notification_schema_js_1 = require("./schemas/notification-schema.js"); const diagnostic_schema_js_1 = require("./schemas/diagnostic-schema.js"); const definition_schema_js_1 = require("./schemas/definition-schema.js"); const reference_schema_js_1 = require("./schemas/reference-schema.js"); const describe_schema_js_1 = require("./schemas/describe-schema.js"); const buffer_schema_js_1 = require("./schemas/buffer-schema.js"); const selection_schema_js_1 = require("./schemas/selection-schema.js"); const index_js_1 = require("./tools/index.js"); const index_js_2 = require("./resources/index.js"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const child_process_1 = require("child_process"); const util_1 = require("util"); const execAsync = (0, util_1.promisify)(child_process_1.exec); // Normalize project root by removing trailing slash function normalizeProjectRoot(root) { return root.replace(/\/$/, ''); } // Create log file in project root const projectRoot = normalizeProjectRoot(process.cwd()); const logFile = path.join(projectRoot, '.claude-code-emacs-mcp.log'); const logStream = fs.createWriteStream(logFile, { flags: 'a' }); function log(message) { const timestamp = new Date().toISOString(); logStream.write(`[${timestamp}] ${message}\n`); } log(`Starting MCP server for project: ${projectRoot}...`); log(`Log file: ${logFile}`); // Create a new bridge instance for each MCP server // This ensures isolation between different Claude Code sessions const bridge = new emacs_bridge_js_1.EmacsBridge(log); const server = new mcp_js_1.McpServer({ name: 'claude-code-emacs-mcp', version: '0.1.0', }, { capabilities: { tools: {}, resources: { subscribe: false, listChanged: true // We'll notify when buffer list changes }, }, }); // Set up notification handler to forward Emacs events to Claude Code bridge.setNotificationHandler((method, params) => { log(`Forwarding Emacs notification to Claude Code: ${method} with params: ${JSON.stringify(params)}`); // If buffer list changed, send resource list changed notification if (method === 'emacs/bufferListUpdated') { // Note: We can't dynamically update resources in the current MCP SDK version // but we can notify that resources have changed server.server.notification({ method: 'notifications/resources/list_changed' }); log('Sent resource list changed notification due to buffer list update'); } server.server.notification({ method: method, params: params }); }); // Register tools with McpServer function registerTools() { // getOpenBuffers tool server.registerTool('getOpenBuffers', { description: 'Get list of open buffers in current project', inputSchema: buffer_schema_js_1.getOpenBuffersInputSchema.shape, outputSchema: buffer_schema_js_1.getOpenBuffersOutputSchema.shape }, async (args, _extra) => { const result = await (0, index_js_1.handleGetOpenBuffers)(bridge, args); return { content: result.content, structuredContent: { buffers: result.buffers }, isError: result.isError }; }); // getCurrentSelection tool server.registerTool('getCurrentSelection', { description: 'Get current text selection in Emacs', inputSchema: selection_schema_js_1.getCurrentSelectionInputSchema.shape, outputSchema: selection_schema_js_1.getCurrentSelectionOutputSchema.shape }, async (args, _extra) => { const result = await (0, index_js_1.handleGetCurrentSelection)(bridge, args); return { content: result.content, structuredContent: { selection: result.selection, file: result.file, start: result.start, end: result.end }, isError: result.isError }; }); // getDiagnostics tool server.registerTool('getDiagnostics', { description: 'Get project-wide LSP diagnostics using specified buffer for LSP context', inputSchema: diagnostic_schema_js_1.getDiagnosticsInputSchema.shape, outputSchema: diagnostic_schema_js_1.getDiagnosticsOutputSchema.shape }, async (args, _extra) => { const result = await (0, index_js_1.handleGetDiagnostics)(bridge, args); return { content: result.content, structuredContent: { diagnostics: result.diagnostics }, isError: result.isError }; }); // getDefinition tool server.registerTool('getDefinition', { description: 'Find definition of symbol using LSP', inputSchema: definition_schema_js_1.getDefinitionInputSchema.shape, outputSchema: definition_schema_js_1.getDefinitionOutputSchema.shape }, async (args, _extra) => { const result = await (0, index_js_1.handleGetDefinition)(bridge, args); return { content: result.content, structuredContent: { definitions: result.definitions }, isError: result.isError }; }); // findReferences tool server.registerTool('findReferences', { description: 'Find all references to a symbol using LSP', inputSchema: reference_schema_js_1.findReferencesInputSchema.shape, outputSchema: reference_schema_js_1.findReferencesOutputSchema.shape }, async (args, _extra) => { const result = await (0, index_js_1.handleFindReferences)(bridge, args); return { content: result.content, structuredContent: { references: result.references }, isError: result.isError }; }); // describeSymbol tool server.registerTool('describeSymbol', { description: 'Get full documentation and information about a symbol using LSP hover', inputSchema: describe_schema_js_1.describeSymbolInputSchema.shape, outputSchema: describe_schema_js_1.describeSymbolOutputSchema.shape }, async (args, _extra) => { const result = await (0, index_js_1.handleDescribeSymbol)(bridge, args); return { content: result.content, structuredContent: { documentation: result.documentation }, isError: result.isError }; }); // sendNotification tool server.registerTool('sendNotification', { description: 'Send a desktop notification to alert the user when tasks complete or need attention', inputSchema: notification_schema_js_1.sendNotificationInputSchema.shape, outputSchema: notification_schema_js_1.sendNotificationOutputSchema.shape }, async (args, _extra) => { const result = await (0, index_js_1.handleSendNotification)(bridge, args); return { content: result.content, structuredContent: { status: result.status, message: result.message, }, isError: result.isError }; }); // Register diff tools individually for better type inference // openDiffFile tool server.registerTool('openDiffFile', { description: index_js_1.diffTools.openDiffFile.description, inputSchema: index_js_1.diffTools.openDiffFile.inputSchema.shape, outputSchema: index_js_1.diffTools.openDiffFile.outputSchema.shape }, async (args, _extra) => { const result = await index_js_1.diffTools.openDiffFile.handler(bridge, args); return { content: result.content, structuredContent: result.structuredContent, isError: result.isError }; }); // openRevisionDiff tool server.registerTool('openRevisionDiff', { description: index_js_1.diffTools.openRevisionDiff.description, inputSchema: index_js_1.diffTools.openRevisionDiff.inputSchema.shape, outputSchema: index_js_1.diffTools.openRevisionDiff.outputSchema.shape }, async (args, _extra) => { const result = await index_js_1.diffTools.openRevisionDiff.handler(bridge, args); return { content: result.content, structuredContent: result.structuredContent, isError: result.isError }; }); // openCurrentChanges tool server.registerTool('openCurrentChanges', { description: index_js_1.diffTools.openCurrentChanges.description, inputSchema: index_js_1.diffTools.openCurrentChanges.inputSchema.shape, outputSchema: index_js_1.diffTools.openCurrentChanges.outputSchema.shape }, async (args, _extra) => { const result = await index_js_1.diffTools.openCurrentChanges.handler(bridge, args); return { content: result.content, structuredContent: result.structuredContent, isError: result.isError }; }); // openDiffContent tool server.registerTool('openDiffContent', { description: index_js_1.diffTools.openDiffContent.description, inputSchema: index_js_1.diffTools.openDiffContent.inputSchema.shape, outputSchema: index_js_1.diffTools.openDiffContent.outputSchema.shape }, async (args, _extra) => { const result = await index_js_1.diffTools.openDiffContent.handler(bridge, args); return { content: result.content, structuredContent: result.structuredContent, isError: result.isError }; }); } // Register resources with dynamic listing function registerResources() { // Register buffer resources using ResourceTemplate with list callback const bufferTemplate = new mcp_js_1.ResourceTemplate('emacs://buffer/{+path}', { list: async () => { try { const resources = await index_js_2.bufferResourceHandler.list(bridge); log(`ResourceTemplate list callback: found ${resources.length} buffer resources`); return { resources }; } catch (error) { log(`Error in ResourceTemplate list callback: ${error}`); return { resources: [] }; } } }); server.registerResource('emacs-buffers', bufferTemplate, { title: 'Emacs Buffers', description: 'Open buffers in Emacs', mimeType: 'text/plain' }, async (uri, variables) => { log(`Reading buffer resource: ${uri}, path: ${variables.path}`); // Reconstruct the full path with leading slash const fullPath = `/${variables.path}`; const fullUri = `emacs://buffer${fullPath}`; const result = await index_js_2.bufferResourceHandler.read(bridge, fullUri); return { contents: [{ uri: uri.toString(), mimeType: result.mimeType || 'text/plain', text: result.text }] }; }); // Project info resource (static) server.registerResource('project-info', 'emacs://project/info', { title: 'Project Information', description: 'Current project information', mimeType: 'application/json' }, async (uri) => { log(`Reading project resource: ${uri}`); const result = await index_js_2.projectResourceHandler.read(bridge, uri.toString()); return { contents: [{ uri: uri.toString(), mimeType: result.mimeType || 'application/json', text: result.text }] }; }); log('Resources registered successfully'); } // Notify Emacs about the port async function notifyEmacsPort(port) { const projectRoot = normalizeProjectRoot(process.cwd()); const elisp = `(claude-code-emacs-mcp-register-port "${projectRoot}" ${port})`; // Try emacsclient first try { await execAsync(`emacsclient --eval '${elisp}'`); log(`Notified Emacs about port ${port} for project ${projectRoot}`); } catch (error) { log(`Failed to notify Emacs via emacsclient: ${error}`); // Continue even if notification fails - Emacs might not be running in server mode } // Also write port info to a file as fallback try { const portFile = path.join(os.tmpdir(), `claude-code-emacs-mcp-${projectRoot.replace(/[^a-zA-Z0-9]/g, '_')}.port`); await fs.promises.writeFile(portFile, JSON.stringify({ port, projectRoot }), 'utf8'); log(`Wrote port info to ${portFile}`); } catch (error) { log(`Failed to write port file: ${error}`); } } // Start server async function main() { // Use project root as session ID const sessionId = normalizeProjectRoot(process.cwd()); // Start Emacs bridge with port 0 for automatic assignment const port = await bridge.start(0, sessionId); // Notify Emacs about the assigned port await notifyEmacsPort(port); // Register tools and resources registerTools(); registerResources(); const ping = async () => { try { await server.server.ping(); log(`Ping successful for session ${sessionId}`); setTimeout(ping, 30000); } catch (error) { log(`Ping failed for session ${sessionId}, Emacs bridge on port ${port}. Exitting...`); await cleanup(); process.exit(1); } }; server.server.oninitialized = () => { log(`MCP server initialized for session ${sessionId}, Emacs bridge on port ${port}`); log(`Starting ping monitoring for session ${sessionId}`); ping(); const cap = server.server.getClientCapabilities(); log(`Client capabilities: ${JSON.stringify(cap)}`); }; // For MCP, use stdio transport const transport = new stdio_js_1.StdioServerTransport(); await server.connect(transport); log(`MCP server running for session ${sessionId}, Emacs bridge on port ${port}`); } // Cleanup on exit process.on('SIGINT', async () => { log('Received SIGINT, shutting down...'); await cleanup(); process.exit(0); }); process.on('SIGTERM', async () => { log('Received SIGTERM, shutting down...'); await cleanup(); process.exit(0); }); // Handle uncaught exceptions and rejections process.on('uncaughtException', (error) => { log(`Uncaught exception: ${error.message}`); log(`Stack: ${error.stack}`); log(`Project root: ${normalizeProjectRoot(process.cwd())}`); cleanup().then(() => process.exit(1)); }); process.on('unhandledRejection', (reason, promise) => { log(`Unhandled rejection at: ${promise}, reason: ${reason}`); log(`Project root: ${normalizeProjectRoot(process.cwd())}`); cleanup().then(() => process.exit(1)); }); async function cleanup() { const projectRoot = normalizeProjectRoot(process.cwd()); try { const elisp = `(claude-code-emacs-mcp-unregister-port "${projectRoot}")`; await execAsync(`emacsclient --eval '${elisp}'`); log(`Unregistered port for project ${projectRoot}`); } catch (error) { log(`Failed to unregister port: ${error}`); } // Clean up port file try { const portFile = path.join(os.tmpdir(), `claude-code-emacs-mcp-${projectRoot.replace(/[^a-zA-Z0-9]/g, '_')}.port`); await fs.promises.unlink(portFile); log(`Removed port file ${portFile}`); } catch (error) { log(`Failed to remove port file: ${error}`); } await bridge.stop(); } main().catch((error) => { log(`Server error: ${error.message}`); log(`Stack: ${error.stack}`); log(`Project root: ${normalizeProjectRoot(process.cwd())}`); log(`Process info: PID=${process.pid}, Node=${process.version}`); cleanup().then(() => process.exit(1)); }); //# sourceMappingURL=index.js.map