claude-code-emacs-mcp-server
Version:
MCP server for Claude Code Emacs integration
419 lines • 17.5 kB
JavaScript
#!/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