This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is an Emacs MCP (Model Context Protocol) Server implementation written in pure Elisp. It enables direct integration between Large Language Models and Emacs internals by exposing Emacs functionality through standardized MCP tools.
- ALWAYS ensure that parentheses are perfectly balanced!
- Be as concise as possible.
- Be extremely careful with the protocol and transport layers code. Always test code if making changes to it.
- Follow idiomatic Elisp conventions.
Private symbols use double-dash prefix:
;; Public API
(defun mcp-server-start () ...)
(defvar mcp-server-running nil)
;; Private/internal (not for external use)
(defun mcp-server--handle-message () ...)
(defvar mcp-server--client-counter 0)Package prefixes are mandatory:
- All symbols must start with
mcp-server-(or module-specific likemcp-server-tools-) - This prevents namespace pollution
Use defcustom for user-configurable settings:
(defcustom mcp-server-debug nil
"Whether to enable debug logging."
:type 'boolean
:group 'mcp-server)Use defvar only for internal state:
(defvar mcp-server-running nil
"Whether the MCP server is currently running.")All defcustom must include:
:typespecification:group 'mcp-server(or appropriate subgroup)- Descriptive docstring
Always use string= for string equality:
;; Correct
(when (string= method "initialize") ...)
;; Avoid for strings (works but less explicit)
(when (equal method "initialize") ...)Standard pattern uses condition-case:
(condition-case err
(do-something-risky)
(error
(handle-error (error-message-string err))))Use throw/catch only for non-local exit (not errors):
;; Used in message handler for early exit after successful send
(catch 'mcp-handled
(when success
(throw 'mcp-handled 'success))
(fallback-action))Signal errors with error:
(unless tool
(error "Tool not found: %s" name))Use vectors for required fields:
;; Correct - vector
:input-schema '((type . "object")
(required . ["expression"]))
;; Wrong - list (won't serialize correctly)
:input-schema '((type . "object")
(required . ("expression")))NEVER use :json-false in this codebase. It is banned.
Always use t for JSON true and :false for JSON false:
;; Correct
:annotations '((readOnlyHint . t)
(destructiveHint . :false))
;; BANNED - will break the MCP protocol!
:annotations '((readOnlyHint . t)
(destructiveHint . :json-false)) ; DO NOT USEWhy: :json-false is what Emacs returns when parsing JSON input, but json-serialize only accepts :false for output. This codebase produces JSON for the MCP protocol. Using :json-false will cause serialization errors and break client communication.
Use ;;;###autoload only for user-facing interactive commands:
;;;###autoload
(defun mcp-server-start () ...) ; User command - autoload
(defun mcp-server-main () ...) ; Internal entry point - no autoloadNever call private (--) functions from other modules:
;; Wrong - reaching into transport internals
(mcp-server-transport-unix--get-client client-id)
;; Correct - use public API
(mcp-server-transport-send-raw transport-name client-id json-str)Use sit-for instead of sleep-for for waiting:
;; Correct - allows event processing
(while running
(sit-for 0.1))
;; Avoid - blocks event processing
(while running
(sleep-for 0.1))Every public function needs a docstring:
(defun mcp-server-tools-call (name arguments)
"Call tool NAME with ARGUMENTS.
Returns a list of content items in MCP format.
Respects `mcp-server-tools-filter' - disabled tools cannot be called."
...)Use cl-defstruct for data structures:
(cl-defstruct mcp-server-tool
"Structure representing an MCP tool."
name
title
description
input-schema
function)This project requires Emacs 27.1+ for native JSON support (json-serialize, json-parse-string).
When releasing a new version, update both locations in mcp-server.el:
- Header comment:
;; Version: X.Y.Z - Runtime constant:
(defconst mcp-server-version "X.Y.Z" ...)
The server uses a pluggable transport architecture:
mcp-server-transport.el- Base transport interfacemcp-server-transport-unix.el- Unix domain socket implementationmcp-server-transport-tcp.el- TCP transport (planned)- Multiple transport backends can coexist
mcp-server.el- Main entry point and server orchestration- Full MCP draft specification compliance
mcp-server-tools.el- Tool registry and execution frameworkmcp-server-emacs-tools.el- Tool loader (loads tools fromtools/directory)mcp-server-security.el- Permission management and sandboxingtools/- Individual tool implementations (self-registering modules)
./test/scripts/test-runner.sh- Comprehensive test suite for validation./test/scripts/test-runner.sh -v- Run tests with verbose output./test/scripts/test-runner.sh -k- Keep server running for manual testing./test/scripts/test-runner.sh -s- Test against existing server instance
;; Start server with Unix socket (primary transport)
M-x mcp-server-start-unix
;; Start with custom socket name
M-x mcp-server-start-unix-named
;; Configure socket naming strategy
M-x mcp-server-set-socket-name
;; Show server status and connections
M-x mcp-server-status
;; Get current socket path
M-x mcp-server-get-socket-path
;; Stop the server
M-x mcp-server-stop;; Load test configuration
(require 'test-config)
;; Start server with test configuration
M-x mcp-test-start-server
;; Validate refactoring worked correctly
M-x mcp-test-validate-refactoring
;; Toggle debug logging
M-x mcp-server-toggle-debugThe server supports multiple socket naming strategies via mcp-server-socket-name:
- Default naming (
nil) - Createsemacs-mcp-server.sock(recommended for most users) - User-based (
'user) - Createsemacs-mcp-server-{username}.sock(multi-user systems) - Session-based (
'session) - Createsemacs-mcp-server-{username}-{pid}.sock(multiple instances) - Custom function - Dynamic naming via lambda function
- Custom string - Fixed naming like
"my-instance"createsemacs-mcp-server-my-instance.sock
The server exposes the following tools:
eval-elisp- Execute arbitrary Elisp expressions safelyget-diagnostics- Get flycheck/flymake diagnostics from project buffers
Tools can be selectively enabled via mcp-server-emacs-tools-enabled:
(setq mcp-server-emacs-tools-enabled 'all) ; All tools (default)
(setq mcp-server-emacs-tools-enabled '(get-diagnostics)) ; Only diagnosticsTools expose behavior hints via annotations that MCP clients use to determine whether to prompt users for permission:
| Annotation | Description |
|---|---|
readOnlyHint |
true if tool doesn't modify anything |
destructiveHint |
true if tool may cause destructive changes |
idempotentHint |
true if repeated calls have no additional effect |
openWorldHint |
true if tool interacts with external entities |
Current tool annotations:
eval-elisp: destructive, non-idempotent, open-world (can do anything)get-diagnostics: read-only, idempotent, closed-world (safe)
The security model has two layers:
-
MCP client prompting - Clients like Claude Code use tool annotations (
destructiveHint, etc.) to decide whether to prompt users for tool access. -
Emacs blocklist - Dangerous functions (e.g.,
delete-file,shell-command) are always blocked, regardless of whether the tool was allowed by the client.
By default (mcp-server-security-prompt-for-permissions = nil):
- Dangerous operations are blocked silently (no minibuffer prompt)
- Safe operations are allowed
- The blocklist is always enforced
To enable Emacs-side prompting (approve dangerous operations case-by-case):
(setq mcp-server-security-prompt-for-permissions t)This prompts in the minibuffer instead of blocking, letting users approve dangerous operations individually.
- Permission decisions are cached per session
- Comprehensive audit trail of all actions
- View audit log:
M-x mcp-server-security-show-audit-log
- JSON Schema validation for all tool inputs
- Protection against code injection attacks
- Sanitization of string inputs and paths
- 30-second default timeout for operations
- Memory usage monitoring
- Restricted access to dangerous functions (when Emacs prompting enabled)
{
"mcpServers": {
"emacs": {
"command": "/path/to/mcp-wrapper.sh",
"args": ["~/.emacs.d/.local/cache/emacs-mcp-server.sock"],
"transport": "stdio"
}
}
}from examples.unix_socket_client import EmacsMCPClient
client = EmacsMCPClient()
if client.connect() and client.initialize():
result = client.call_tool("eval-elisp", {"expression": "(+ 1 2 3)"})
client.disconnect()# Test full functionality
./test/integration/test-unix-socket-fixed.sh
# Test with custom socket
./test/integration/test-unix-socket-fixed.sh -s /tmp/custom.sock
# Interactive testing
./test/integration/test-unix-socket-fixed.sh -iTools use a self-registration pattern: each tool file registers itself at load time via mcp-server-register-tool. This eliminates manual registry maintenance.
Step 1: Create a new file in tools/ directory:
;;; tools/mcp-server-emacs-tools-my-tool.el
(require 'mcp-server-tools)
(defun mcp-server-emacs-tools--my-tool-handler (args)
"Handle my-tool invocation with ARGS."
(let ((param (alist-get 'param args)))
(format "Result: %s" param)))
;; Self-registration: this runs when the file is loaded
(mcp-server-register-tool
(make-mcp-server-tool
:name "my-tool"
:title "My Tool"
:description "Description of functionality"
:input-schema '((type . "object")
(properties . ((param . ((type . "string")))))
(required . ["param"])) ; Note: use vector, not list
:function #'mcp-server-emacs-tools--my-tool-handler))
(provide 'mcp-server-emacs-tools-my-tool)Step 2: Register in mcp-server-emacs-tools.el:
;; Add to mcp-server-emacs-tools--available alist:
(defconst mcp-server-emacs-tools--available
'((eval-elisp . mcp-server-emacs-tools-eval-elisp)
(get-diagnostics . mcp-server-emacs-tools-diagnostics)
(my-tool . mcp-server-emacs-tools-my-tool)) ; Add your tool here
"Alist mapping tool names (symbols) to their feature names.")Tools are filtered at runtime via mcp-server-tools-filter. The mcp-server-emacs-tools module sets this to check mcp-server-emacs-tools-enabled:
- Disabled tools are invisible - They don't appear in
tools/listresponses - Disabled tools are blocked - Calling them via
tools/callreturns an error - Changes take effect immediately - No server restart needed
;; Enable only specific tools
(setq mcp-server-emacs-tools-enabled '(get-diagnostics))
;; Re-enable all tools
(setq mcp-server-emacs-tools-enabled 'all)- Run
./test/scripts/test-runner.shto validate core functionality - Test with actual MCP clients using wrapper scripts
- Verify security controls work as expected
- Check multi-client concurrent connections
- Enable debug logging:
M-x mcp-server-toggle-debug - Check server status:
M-x mcp-server-status - List connected clients:
M-x mcp-server-list-clients - View security audit log:
M-x mcp-server-security-show-audit-log
mcp-server/
├── mcp-server.el # Main entry point and orchestration
├── mcp-server-transport.el # Transport interface definition
├── mcp-server-transport-unix.el # Unix domain socket implementation
├── mcp-server-transport-tcp.el # TCP transport (planned)
├── mcp-server-tools.el # Tool registry and execution
├── mcp-server-security.el # Security and sandboxing
├── mcp-server-emacs-tools.el # Tool loader (loads from tools/)
├── tools/ # Individual tool implementations
│ ├── mcp-server-emacs-tools-eval-elisp.el # eval-elisp tool
│ └── mcp-server-emacs-tools-diagnostics.el # get-diagnostics tool
├── test/ # Test suite directory
│ ├── config/ # Test configuration files
│ ├── fixtures/ # Test helpers and utilities
│ ├── unit/ # Unit tests
│ │ ├── test-mcp-emacs-tools.el # Tool-specific tests
│ │ └── ...
│ ├── scripts/ # Test runner scripts
│ │ └── test-runner.sh # Comprehensive test suite
│ └── integration/ # Integration test scripts
├── mcp-wrapper.py # Python wrapper for MCP clients
└── mcp-wrapper.sh # Shell wrapper for MCP clients
The server supports concurrent connections from multiple MCP clients:
- Each client gets a unique connection ID
- Client state is tracked independently
- Shared Emacs state requires careful coordination
- Connection cleanup on client disconnect
The modular transport design allows adding new transport mechanisms:
- Implement the
mcp-server-transportinterface - Register with
mcp-server-transport-register - Support for stdio, TCP, WebSocket, etc.
- Always update relevant files with significant changes. For example, when changing tests make sure to update the tests/README.md file.