Skip to content

Commit da21de8

Browse files
authored
Merge pull request #8 from rhblind/7-tool-calling
7 tool calling
2 parents bda5401 + f9ad85f commit da21de8

File tree

6 files changed

+162
-86
lines changed

6 files changed

+162
-86
lines changed

CLAUDE.md

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ This is an Emacs MCP (Model Context Protocol) Server implementation written in p
102102
(required . ("expression")))
103103
```
104104

105+
### JSON Boolean Values
106+
107+
**NEVER use `:json-false` in this codebase. It is banned.**
108+
109+
Always use `t` for JSON `true` and `:false` for JSON `false`:
110+
```elisp
111+
;; Correct
112+
:annotations '((readOnlyHint . t)
113+
(destructiveHint . :false))
114+
115+
;; BANNED - will break the MCP protocol!
116+
:annotations '((readOnlyHint . t)
117+
(destructiveHint . :json-false)) ; DO NOT USE
118+
```
119+
120+
Why: `: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.
121+
105122
### Autoload Cookies
106123

107124
**Use `;;;###autoload` only for user-facing interactive commands:**
@@ -164,6 +181,12 @@ Respects `mcp-server-tools-filter' - disabled tools cannot be called."
164181

165182
This project requires **Emacs 27.1+** for native JSON support (`json-serialize`, `json-parse-string`).
166183

184+
### Version Bumping
185+
186+
When releasing a new version, update **both** locations in `mcp-server.el`:
187+
1. Header comment: `;; Version: X.Y.Z`
188+
2. Runtime constant: `(defconst mcp-server-version "X.Y.Z" ...)`
189+
167190
## Key Architecture Components
168191

169192
### Modular Transport System
@@ -252,11 +275,49 @@ Tools can be selectively enabled via `mcp-server-emacs-tools-enabled`:
252275

253276
## Security Model
254277

255-
### Permission System
256-
- Dangerous operations require user confirmation
257-
- Permission decisions are cached per session
278+
### Tool Annotations (MCP Specification)
279+
280+
Tools expose behavior hints via annotations that MCP clients use to determine
281+
whether to prompt users for permission:
282+
283+
| Annotation | Description |
284+
|------------|-------------|
285+
| `readOnlyHint` | `true` if tool doesn't modify anything |
286+
| `destructiveHint` | `true` if tool may cause destructive changes |
287+
| `idempotentHint` | `true` if repeated calls have no additional effect |
288+
| `openWorldHint` | `true` if tool interacts with external entities |
289+
290+
Current tool annotations:
291+
- `eval-elisp`: destructive, non-idempotent, open-world (can do anything)
292+
- `get-diagnostics`: read-only, idempotent, closed-world (safe)
293+
294+
### Permission Handling
295+
296+
The security model has two layers:
297+
298+
1. **MCP client prompting** - Clients like Claude Code use tool annotations
299+
(`destructiveHint`, etc.) to decide whether to prompt users for tool access.
300+
301+
2. **Emacs blocklist** - Dangerous functions (e.g., `delete-file`, `shell-command`)
302+
are always blocked, regardless of whether the tool was allowed by the client.
303+
304+
By default (`mcp-server-security-prompt-for-permissions` = `nil`):
305+
- Dangerous operations are **blocked silently** (no minibuffer prompt)
306+
- Safe operations are allowed
307+
- The blocklist is always enforced
308+
309+
To enable Emacs-side prompting (approve dangerous operations case-by-case):
310+
```elisp
311+
(setq mcp-server-security-prompt-for-permissions t)
312+
```
313+
314+
This prompts in the minibuffer instead of blocking, letting users approve
315+
dangerous operations individually.
316+
317+
### Permission Caching
318+
- Permission decisions are cached per session
258319
- Comprehensive audit trail of all actions
259-
- Configurable prompting behavior
320+
- View audit log: `M-x mcp-server-security-show-audit-log`
260321

261322
### Input Validation
262323
- JSON Schema validation for all tool inputs
@@ -266,7 +327,7 @@ Tools can be selectively enabled via `mcp-server-emacs-tools-enabled`:
266327
### Execution Sandboxing
267328
- 30-second default timeout for operations
268329
- Memory usage monitoring
269-
- Restricted access to dangerous functions
330+
- Restricted access to dangerous functions (when Emacs prompting enabled)
270331

271332
## Client Integration Examples
272333

@@ -416,4 +477,4 @@ The modular transport design allows adding new transport mechanisms:
416477
- Implement the `mcp-server-transport` interface
417478
- Register with `mcp-server-transport-register`
418479
- Support for stdio, TCP, WebSocket, etc.
419-
- Always update relevant files with significant changes. For example, when changing tests make sure to update the tests/README.md file.
480+
- Always update relevant files with significant changes. For example, when changing tests make sure to update the tests/README.md file.

mcp-server-security.el

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,13 @@ Use this to whitelist specific functions you trust the LLM to use freely."
8888
:type 'integer
8989
:group 'mcp-server)
9090

91-
(defcustom mcp-server-security-prompt-for-permissions t
92-
"Whether to prompt user for dangerous operations."
91+
(defcustom mcp-server-security-prompt-for-permissions nil
92+
"Whether to prompt user in Emacs for dangerous operations.
93+
When nil (the default), dangerous operations are blocked without prompting.
94+
The MCP client uses tool annotations to determine whether to prompt for
95+
tool-level permission, but the blocklist is always enforced.
96+
Set to t to prompt in the Emacs minibuffer instead of blocking, allowing
97+
users to approve dangerous operations on a case-by-case basis."
9398
:type 'boolean
9499
:group 'mcp-server)
95100

@@ -148,7 +153,7 @@ Returns t if permitted, nil otherwise."
148153
('no
149154
(mcp-server-security--log-audit operation data nil)
150155
nil)))
151-
;; If not prompting, deny dangerous operations by default
156+
;; When not prompting, still block dangerous operations
152157
(let ((granted (not (mcp-server-security--is-dangerous-operation operation))))
153158
(puthash cache-key granted mcp-server-security--permission-cache)
154159
(mcp-server-security--log-audit operation data granted)
@@ -213,20 +218,20 @@ Returns the input if safe, signals an error otherwise."
213218
;; Check for shell command injection
214219
(when (string-match-p "[;&|`$]" input)
215220
(error "Input contains potentially dangerous shell characters"))
216-
221+
217222
;; Check for path traversal
218223
(when (string-match-p "\\.\\./\\|~/" input)
219224
(error "Input contains potentially dangerous path patterns"))
220-
225+
221226
;; Check for excessive length
222227
(when (> (length input) 10000)
223228
(error "Input exceeds maximum length")))
224-
229+
225230
;; Check for suspicious elisp code patterns in strings
226231
(when (and (stringp input)
227232
(string-match-p "(\\s-*\\(?:eval\\|load\\|shell-command\\)" input))
228233
(error "Input contains potentially dangerous elisp patterns"))
229-
234+
230235
input)
231236

232237
(defun mcp-server-security-sanitize-string (str)
@@ -245,7 +250,7 @@ Returns the input if safe, signals an error otherwise."
245250
"Safely evaluate FORM with security restrictions."
246251
;; Check if form contains dangerous functions
247252
(mcp-server-security--check-form-safety form)
248-
253+
249254
;; Execute with timeout and memory limits
250255
(mcp-server-security--execute-with-limits
251256
(lambda () (eval form))))
@@ -258,8 +263,9 @@ Returns the input if safe, signals an error otherwise."
258263
(when (and (member form mcp-server-security-dangerous-functions)
259264
(not (member form mcp-server-security-allowed-dangerous-functions)))
260265
(unless (mcp-server-security-check-permission form)
261-
(error "Permission denied for function: %s" form))))
262-
266+
(error "Security: `%s' is blocked. Add it to `mcp-server-security-allowed-dangerous-functions' \
267+
to allow, or set `mcp-server-security-prompt-for-permissions' to t to prompt" form))))
268+
263269
;; Check lists (function calls)
264270
((listp form)
265271
(when form
@@ -270,26 +276,27 @@ Returns the input if safe, signals an error otherwise."
270276
(when (and (member func mcp-server-security-dangerous-functions)
271277
(not (member func mcp-server-security-allowed-dangerous-functions)))
272278
(unless (mcp-server-security-check-permission func args)
273-
(error "Permission denied for function: %s" func)))
274-
279+
(error "Security: `%s' is blocked. Add it to `mcp-server-security-allowed-dangerous-functions' \
280+
to allow, or set `mcp-server-security-prompt-for-permissions' to t to prompt" func)))
281+
275282
;; Special checks for file access functions
276283
(when (memq func '(find-file find-file-noselect view-file insert-file-contents))
277284
(let ((file-path (car args)))
278285
(when (and file-path (stringp file-path))
279286
(when (mcp-server-security--is-sensitive-file file-path)
280-
(unless (mcp-server-security-check-permission
287+
(unless (mcp-server-security-check-permission
281288
(format "access-sensitive-file:%s" func) file-path)
282289
(error "Permission denied for sensitive file access: %s" file-path))))))
283-
290+
284291
;; Special checks for buffer access functions
285292
(when (memq func '(switch-to-buffer set-buffer with-current-buffer))
286-
(let ((buffer-name (if (eq func 'with-current-buffer)
293+
(let ((buffer-name (if (eq func 'with-current-buffer)
287294
(car args)
288295
(car args))))
289296
(when (and buffer-name (stringp buffer-name))
290297
(when (mcp-server-security--is-sensitive-buffer buffer-name)
291298
(error "Access denied to sensitive buffer: %s" buffer-name))))))
292-
299+
293300
;; Recursively check arguments
294301
(dolist (arg args)
295302
(mcp-server-security--check-form-safety arg)))))))
@@ -298,18 +305,18 @@ Returns the input if safe, signals an error otherwise."
298305
"Execute FUNC with time and memory limits."
299306
(let ((start-time (current-time))
300307
(start-gc-cons-threshold gc-cons-threshold))
301-
308+
302309
;; Set conservative GC threshold for memory monitoring
303310
(setq gc-cons-threshold 1000000)
304-
311+
305312
(unwind-protect
306313
(with-timeout (mcp-server-security-max-execution-time
307314
(error "Execution timeout exceeded"))
308315
(funcall func))
309-
316+
310317
;; Restore GC threshold
311318
(setq gc-cons-threshold start-gc-cons-threshold)
312-
319+
313320
;; Log execution time
314321
(let ((elapsed (float-time (time-subtract (current-time) start-time))))
315322
(when (> elapsed 5.0)
@@ -324,7 +331,7 @@ Returns the input if safe, signals an error otherwise."
324331
(data . ,data)
325332
(granted . ,granted))))
326333
(push entry mcp-server-security--audit-log)
327-
334+
328335
;; Keep only last 1000 entries
329336
(when (> (length mcp-server-security--audit-log) 1000)
330337
(setq mcp-server-security--audit-log

mcp-server-transport.el

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383
(defun mcp-server-transport-send (transport-name client-id message)
8484
"Send MESSAGE to CLIENT-ID using transport NAME."
85-
(mcp-server-transport--debug "transport-send START - transport=%s, client=%s"
85+
(mcp-server-transport--debug "transport-send START - transport=%s, client=%s"
8686
transport-name client-id)
8787
(let ((transport (mcp-server-transport-get transport-name)))
8888
(mcp-server-transport--debug "transport object = %S" transport)
@@ -150,9 +150,9 @@ This bypasses the normal JSON serialization."
150150
:null)
151151
;; If it's an alist (list of key-value pairs where each pair is (key . value))
152152
;; Check: list of cons cells where each cons has a non-list car (the key)
153-
((and (listp obj)
154-
(not (null obj))
155-
(seq-every-p (lambda (item)
153+
((and (listp obj)
154+
(not (null obj))
155+
(seq-every-p (lambda (item)
156156
(and (consp item)
157157
(not (listp (car item))))) obj))
158158
(let ((ht (make-hash-table :test 'equal)))
@@ -162,7 +162,7 @@ This bypasses the normal JSON serialization."
162162
(car pair)))
163163
(value (mcp-server-transport--alist-to-json (cdr pair))))
164164
(when (string= key "isError")
165-
(mcp-server-transport--debug "Processing isError - raw value: %S, converted value: %S"
165+
(mcp-server-transport--debug "Processing isError - raw value: %S, converted value: %S"
166166
(cdr pair) value))
167167
(puthash key value ht)))
168168
ht))
@@ -172,16 +172,16 @@ This bypasses the normal JSON serialization."
172172
;; If it's a symbol, convert to string (except for special values)
173173
((symbolp obj)
174174
(cond
175-
((eq obj t)
175+
((eq obj t)
176176
(mcp-server-transport--debug "Converting symbol t to JSON true")
177177
t)
178-
((eq obj :null)
178+
((eq obj :null)
179179
(mcp-server-transport--debug "Converting symbol :null to JSON null")
180180
:null)
181-
((eq obj :false)
181+
((eq obj :false)
182182
(mcp-server-transport--debug "Converting symbol :false to JSON false")
183183
:false)
184-
(t
184+
(t
185185
(mcp-server-transport--debug "Unexpected symbol %S (name: %s, type: %s) being converted to string"
186186
obj (symbol-name obj) (type-of obj))
187187
(symbol-name obj))))
@@ -199,10 +199,10 @@ This bypasses the normal JSON serialization."
199199
"Validate that MESSAGE is a proper JSON-RPC 2.0 message."
200200
(unless (alist-get 'jsonrpc message)
201201
(error "Missing jsonrpc field"))
202-
202+
203203
(unless (string= (alist-get 'jsonrpc message) "2.0")
204204
(error "Invalid jsonrpc version: %s" (alist-get 'jsonrpc message)))
205-
205+
206206
message)
207207

208208
;;; Client Management Utilities
@@ -226,22 +226,22 @@ This bypasses the normal JSON serialization."
226226
Returns updated buffer with remaining partial data."
227227
(let ((combined (concat buffer new-data))
228228
(remaining ""))
229-
229+
230230
;; Process complete lines
231231
(while (string-match "\n" combined)
232232
(let* ((line-end (match-end 0))
233233
(line (substring combined 0 (1- line-end))))
234-
234+
235235
;; Remove processed line from combined buffer
236236
(setq combined (substring combined line-end))
237-
237+
238238
;; Process the line if it's not empty
239239
(when (> (length (string-trim line)) 0)
240240
(condition-case err
241241
(funcall line-processor line)
242242
(error
243243
(mcp-server-transport--error "Error processing line: %s" (error-message-string err)))))))
244-
244+
245245
;; Return remaining partial data
246246
combined))
247247

0 commit comments

Comments
 (0)