Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 47 additions & 25 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ The server uses a pluggable transport architecture:

### Tool and Security Framework
- `mcp-server-tools.el` - Tool registry and execution framework
- `mcp-server-emacs-tools.el` - Emacs-specific tool implementations
- `mcp-server-emacs-tools.el` - Tool loader (loads tools from `tools/` directory)
- `mcp-server-security.el` - Permission management and sandboxing
- `tools/` - Individual tool implementations (self-registering modules)

## Essential Commands

Expand Down Expand Up @@ -87,10 +88,16 @@ The server supports multiple socket naming strategies via `mcp-server-socket-nam

## MCP Tool Registry

The server exposes the following tool:
The server exposes the following tools:

### Elisp Execution
- `eval-elisp` - Execute arbitrary Elisp expressions safely
- `get-diagnostics` - Get flycheck/flymake diagnostics from project buffers

Tools can be selectively enabled via `mcp-server-emacs-tools-enabled`:
```elisp
(setq mcp-server-emacs-tools-enabled 'all) ; All tools (default)
(setq mcp-server-emacs-tools-enabled '(get-diagnostics)) ; Only diagnostics
```

## Security Model

Expand Down Expand Up @@ -150,17 +157,34 @@ if client.connect() and client.initialize():
## Development Workflow

### Adding New Tools

Create a new file in `tools/` directory:

```elisp
;;; 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)))

(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"]))
:function #'mcp-server-emacs-tools--my-tool-handler))

(provide 'mcp-server-emacs-tools-my-tool)
```

Then add to `mcp-server-emacs-tools.el`:
```elisp
(mcp-server-tools-register
"tool-name"
"Display Title"
"Description of functionality"
'((type . "object")
(properties . ((param . ((type . "string")))))
(required . ["param"]))
(lambda (args)
(let ((param (alist-get 'param args)))
(format "Result: %s" param))))
(require 'mcp-server-emacs-tools-my-tool)
```

### Testing Changes
Expand All @@ -185,22 +209,20 @@ mcp-server/
├── 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 # Emacs-specific tool implementations
├── 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
│ │ ├── test-config.el # Main test configuration
│ │ ├── minimal-test-config.el # Minimal test configuration
│ │ └── test-json-false.el # JSON serialization tests
│ ├── 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
│ │ ├── start-test-server.sh # Test server startup
│ │ ├── test-hello-world.sh # Hello world test
│ │ ├── test-hello-world.py # Python test script
│ │ └── debug-test.py # Debug test utilities
│ │ └── test-runner.sh # Comprehensive test suite
│ └── integration/ # Integration test scripts
│ ├── test-unix-socket-fixed.sh # Fixed Unix socket tests
│ ├── test-unix-socket.sh # Original socket tests
├── mcp-wrapper.py # Python wrapper for MCP clients
├── mcp-wrapper.py # Python wrapper for MCP clients
└── mcp-wrapper.sh # Shell wrapper for MCP clients
```

Expand Down
96 changes: 82 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Alternatively, use package managers:
;; Using straight.el
(use-package mcp-server
:straight (:type git :host github :repo "rhblind/emacs-mcp-server"
:files ("*.el" "mcp-wrapper.py" "mcp-wrapper.sh"))
:files ("*.el" "tools/*.el" "mcp-wrapper.py" "mcp-wrapper.sh"))
:config
(add-hook 'emacs-startup-hook #'mcp-server-start-unix))

Expand All @@ -41,7 +41,7 @@ Alternatively, use package managers:
;; Using Doom Emacs package! macro
(package! mcp-server
:recipe (:type git :host github :repo "rhblind/emacs-mcp-server"
:files ("*.el" "mcp-wrapper.py" "mcp-wrapper.sh")))
:files ("*.el" "tools/*.el" "mcp-wrapper.py" "mcp-wrapper.sh")))
```

**Start the server:** Run `M-x mcp-server-start-unix` in Emacs. The server creates a Unix socket at `~/.config/emacs/.local/cache/emacs-mcp-server.sock`.
Expand Down Expand Up @@ -94,8 +94,41 @@ Once connected, LLMs can perform powerful operations in your Emacs environment:

## Available Tools

**Current Tool:**
- `eval-elisp` - Execute arbitrary elisp expressions safely and return the result
- `get-diagnostics` - Get flycheck/flymake diagnostics from project buffers

### get-diagnostics

Retrieves errors and warnings from flycheck or flymake (auto-detected per buffer) across all project buffers. Results are grouped by file and sorted by severity.

**Parameters:**
| Parameter | Type | Required | Description |
|-------------|--------|----------|-----------------------------------------------|
| `file_path` | string | No | Get diagnostics for a specific file only |
| `severity` | string | No | Filter by `"error"`, `"warning"`, or `"info"` |

**Example response:**
```json
{
"summary": {"total": 5, "errors": 2, "warnings": 3, "info": 0},
"files": {
"/path/to/file.el": {
"error_count": 2,
"warning_count": 1,
"info_count": 0,
"diagnostics": [
{
"line": 42,
"column": 10,
"message": "Unused variable 'foo'",
"severity": "warning",
"source": "emacs-lisp"
}
]
}
}
}
```

## Configuration

Expand All @@ -108,6 +141,19 @@ Once connected, LLMs can perform powerful operations in your Emacs environment:
(setq mcp-server-socket-name "custom") ; Custom: emacs-mcp-server-custom.sock
```

**Tool selection:** Choose which tools to enable:

```elisp
;; Enable all tools (default)
(setq mcp-server-emacs-tools-enabled 'all)

;; Enable only specific tools (disable eval-elisp for security)
(setq mcp-server-emacs-tools-enabled '(get-diagnostics))

;; Enable only eval-elisp
(setq mcp-server-emacs-tools-enabled '(eval-elisp))
```

**Other configuration options:**

```elisp
Expand All @@ -123,7 +169,10 @@ Once connected, LLMs can perform powerful operations in your Emacs environment:

## Security

The MCP server implements comprehensive security measures to protect your Emacs environment and sensitive data from unauthorized access by LLMs.
> [!WARNING]
> The MCP server implements certain security measures to protect your Emacs environment and sensitive data from unauthorized access by LLMs.
> However, the `eval-elisp` tool enables arbitrary evaluation of Elisp code in your Emacs process and therefore by definition enables remote code execution for the LLM.
> **Use this tool with extreme caution!**

### Security Features

Expand Down Expand Up @@ -368,17 +417,36 @@ print(response)
**Quick test:** Run `./test/scripts/test-runner.sh` or test a specific socket with `./test/integration/test-unix-socket-fixed.sh ~/.emacs.d/.local/cache/emacs-mcp-server.sock`

**Adding custom tools:**

Create a new file in the `tools/` directory:

```elisp
;;; 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)))

(mcp-server-register-tool
(make-mcp-server-tool
:name "my-tool"
:title "My Custom Tool"
:description "Description of what this tool does"
:input-schema '((type . "object")
(properties . ((param . ((type . "string")
(description . "A parameter")))))
(required . ["param"]))
:function #'mcp-server-emacs-tools--my-tool-handler))

(provide 'mcp-server-emacs-tools-my-tool)
```

Then add to `mcp-server-emacs-tools.el`:
```elisp
(mcp-server-tools-register
"my-tool"
"My Custom Tool"
"Description of what this tool does"
'((type . "object")
(properties . ((param . ((type . "string")))))
(required . ["param"]))
(lambda (args)
(let ((param (alist-get 'param args)))
(format "Result: %s" param))))
(require 'mcp-server-emacs-tools-my-tool)
```

## Troubleshooting
Expand Down
77 changes: 56 additions & 21 deletions mcp-server-emacs-tools.el
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,68 @@

;;; Commentary:

;; This module provides Emacs-specific MCP tools.
;; This module loads and registers Emacs-specific MCP tools from the tools/ directory.
;; Users can customize which tools are enabled via `mcp-server-emacs-tools-enabled'.

;;; Code:

(require 'mcp-server-tools)
(require 'mcp-server-security)
(require 'cl-lib)

(defgroup mcp-server-emacs-tools nil
"Emacs-specific MCP tools configuration."
:group 'mcp-server
:prefix "mcp-server-emacs-tools-")

(defcustom mcp-server-emacs-tools-enabled 'all
"Which MCP tools to enable.
Can be `all' to enable all available tools, or a list of tool
names (symbols) to enable selectively.

Available tools:
- `eval-elisp' - Execute arbitrary Elisp expressions
- `get-diagnostics' - Get flycheck/flymake diagnostics

Example: \\='(get-diagnostics) to enable only diagnostics."
:type '(choice (const :tag "All tools" all)
(repeat :tag "Selected tools" symbol))
:group 'mcp-server-emacs-tools)

(defconst mcp-server-emacs-tools--available
'((eval-elisp . (mcp-server-emacs-tools-eval-elisp
mcp-server-emacs-tools--eval-elisp-register))
(get-diagnostics . (mcp-server-emacs-tools-diagnostics
mcp-server-emacs-tools--diagnostics-register)))
"Alist mapping tool names to (feature register-function) pairs.")

;; Add tools directory to load path
(let* ((this-file (or load-file-name buffer-file-name))
(tools-dir (and this-file
(expand-file-name "tools" (file-name-directory this-file)))))
(when tools-dir
(add-to-list 'load-path tools-dir)))

(defun mcp-server-emacs-tools--tool-enabled-p (tool-name)
"Return non-nil if TOOL-NAME is enabled."
(or (eq mcp-server-emacs-tools-enabled 'all)
(memq tool-name mcp-server-emacs-tools-enabled)))

(defun mcp-server-emacs-tools-register ()
"Register all Emacs-specific MCP tools."

;; eval-elisp - Execute arbitrary elisp expressions
(mcp-server-tools-register
"eval-elisp"
"Execute Elisp Expression"
"Execute arbitrary Elisp code and return the result."
'((type . "object")
(properties . ((expression . ((type . "string")
(description . "The Elisp expression to evaluate")))))
(required . ("expression")))
(lambda (args)
(let ((expression (alist-get 'expression args)))
(condition-case err
(let ((form (read-from-string expression)))
(format "%S" (mcp-server-security-safe-eval (car form))))
(error (format "Error: %s" (error-message-string err))))))))
"Register enabled Emacs MCP tools.
Only tools listed in `mcp-server-emacs-tools-enabled' are loaded.
This function can be called multiple times to re-register tools
after `mcp-server-tools-cleanup'."
(dolist (tool-spec mcp-server-emacs-tools--available)
(let* ((tool-name (car tool-spec))
(feature (cadr tool-spec))
(register-fn (caddr tool-spec)))
(when (mcp-server-emacs-tools--tool-enabled-p tool-name)
(require feature)
(when (fboundp register-fn)
(funcall register-fn))))))

;; Register tools on initial load
(mcp-server-emacs-tools-register)

(provide 'mcp-server-emacs-tools)

;;; mcp-server-emacs-tools.el ends here
;;; mcp-server-emacs-tools.el ends here
Loading