Skip to content

Commit 481690a

Browse files
committed
Add session-scoped MCP debug tools and command
1 parent 51b2d65 commit 481690a

4 files changed

Lines changed: 214 additions & 3 deletions

File tree

ai-code-mcp-debug-tools.el

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
:type 'boolean
3131
:group 'ai-code-mcp-debug-tools)
3232

33+
(defvar ai-code-mcp-debug-tools--session-overrides (make-hash-table :test 'equal)
34+
"Hash table of session-local debug tool overrides keyed by MCP session id.")
35+
36+
(defvar ai-code-mcp--current-session-id nil
37+
"Dynamically bound MCP session id for the current tool invocation.")
38+
3339
(defvar ai-code-mcp--last-error-record nil
3440
"Most recent Emacs error snapshot recorded for MCP diagnostics tools.")
3541

@@ -107,14 +113,68 @@
107113
:optional t)))
108114
"Optional MCP eval tool specification.")
109115

116+
(defun ai-code-mcp-debug-tools--register-base-tools ()
117+
"Register the standard MCP debugging tools."
118+
(dolist (tool ai-code-mcp-debug-tools--specs)
119+
(apply #'ai-code-mcp-make-tool tool)))
120+
121+
(defun ai-code-mcp-debug-tools--register-eval-tool ()
122+
"Register the optional `eval_elisp' MCP tool."
123+
(apply #'ai-code-mcp-make-tool ai-code-mcp-debug-tools--eval-spec))
124+
125+
(defun ai-code-mcp-debug-tools--session-override (&optional session-id)
126+
"Return session-local override for SESSION-ID or the active MCP session."
127+
(gethash (or session-id ai-code-mcp--current-session-id)
128+
ai-code-mcp-debug-tools--session-overrides))
129+
130+
(defun ai-code-mcp-debug-tools--enabled-p ()
131+
"Return non-nil when debug tools are enabled for the active session."
132+
(or ai-code-mcp-debug-tools-enabled
133+
(alist-get 'enabled
134+
(ai-code-mcp-debug-tools--session-override))))
135+
136+
(defun ai-code-mcp-debug-tools--eval-enabled-p ()
137+
"Return non-nil when `eval_elisp' is enabled for the active session."
138+
(or ai-code-mcp-debug-tools-enable-eval-elisp
139+
(alist-get 'enable_eval_elisp
140+
(ai-code-mcp-debug-tools--session-override))))
141+
142+
(defun ai-code-mcp-debug-tools--require-enabled ()
143+
"Signal an error unless debug inspection tools are enabled."
144+
(unless (ai-code-mcp-debug-tools--enabled-p)
145+
(error "Emacs debug MCP tools are disabled for this session")))
146+
147+
(defun ai-code-mcp-debug-tools--require-eval-enabled ()
148+
"Signal an error unless `eval_elisp' is enabled."
149+
(unless (ai-code-mcp-debug-tools--eval-enabled-p)
150+
(error "The eval_elisp tool is disabled for this session")))
151+
110152
(defun ai-code-mcp-debug-tools-setup ()
111153
"Register optional MCP debugging tools when enabled."
112154
(when ai-code-mcp-debug-tools-enabled
113155
(ai-code-mcp--ensure-error-capture)
114-
(dolist (tool ai-code-mcp-debug-tools--specs)
115-
(apply #'ai-code-mcp-make-tool tool))
156+
(ai-code-mcp-debug-tools--register-base-tools)
116157
(when ai-code-mcp-debug-tools-enable-eval-elisp
117-
(apply #'ai-code-mcp-make-tool ai-code-mcp-debug-tools--eval-spec))))
158+
(ai-code-mcp-debug-tools--register-eval-tool))))
159+
160+
;;;###autoload
161+
(defun ai-code-mcp-debug-tools-enable-for-session
162+
(session-id &optional enable-eval-elisp)
163+
"Enable MCP debug tools for SESSION-ID.
164+
When ENABLE-EVAL-ELISP is non-nil, also expose `eval_elisp' for that
165+
session."
166+
(unless (and (stringp session-id)
167+
(not (string-empty-p session-id)))
168+
(user-error "SESSION-ID is required to enable Emacs debug MCP tools"))
169+
(puthash session-id
170+
`((enabled . t)
171+
(enable_eval_elisp . ,(and enable-eval-elisp t)))
172+
ai-code-mcp-debug-tools--session-overrides)
173+
(ai-code-mcp--ensure-error-capture)
174+
(ai-code-mcp-debug-tools--register-base-tools)
175+
(when enable-eval-elisp
176+
(ai-code-mcp-debug-tools--register-eval-tool))
177+
session-id)
118178

119179
(defun ai-code-mcp--documentation-summary (documentation)
120180
"Return a trimmed summary line for DOCUMENTATION."
@@ -343,6 +403,7 @@ keeps the backtrace on failures."
343403

344404
(defun ai-code-mcp-get-variable-binding-info (variable-name &optional buffer-name)
345405
"Return JSON binding details for VARIABLE-NAME in BUFFER-NAME."
406+
(ai-code-mcp-debug-tools--require-enabled)
346407
(let ((symbol (ai-code-mcp--find-existing-variable-symbol variable-name)))
347408
(if (not symbol)
348409
(json-encode
@@ -380,6 +441,7 @@ keeps the backtrace on failures."
380441
"Return the printed representation of VARIABLE-NAME.
381442
Return a friendly error string when VARIABLE-NAME does not name an
382443
existing bound variable."
444+
(ai-code-mcp-debug-tools--require-enabled)
383445
(let ((symbol (ai-code-mcp--find-existing-variable-symbol variable-name)))
384446
(cond
385447
((not symbol)
@@ -429,6 +491,7 @@ existing bound variable."
429491

430492
(defun ai-code-mcp-get-function-info (function-name)
431493
"Return JSON metadata describing FUNCTION-NAME."
494+
(ai-code-mcp-debug-tools--require-enabled)
432495
(let ((symbol (ai-code-mcp--find-existing-function-symbol function-name)))
433496
(if (not (and symbol (fboundp symbol)))
434497
(json-encode
@@ -480,6 +543,7 @@ existing bound variable."
480543

481544
(defun ai-code-mcp-get-last-error-backtrace ()
482545
"Return a JSON snapshot of the most recently recorded Emacs error."
546+
(ai-code-mcp-debug-tools--require-enabled)
483547
(json-encode
484548
(if ai-code-mcp--last-error-record
485549
(ai-code-mcp--last-error-json-payload ai-code-mcp--last-error-record)
@@ -520,6 +584,7 @@ existing bound variable."
520584

521585
(defun ai-code-mcp-get-feature-load-state (feature-name)
522586
"Return JSON load-state details for FEATURE-NAME."
587+
(ai-code-mcp-debug-tools--require-enabled)
523588
(if (not (ai-code-mcp--valid-feature-name-p feature-name))
524589
(json-encode
525590
(ai-code-mcp--invalid-feature-load-state-payload feature-name))
@@ -539,6 +604,7 @@ existing bound variable."
539604

540605
(defun ai-code-mcp-get-recent-messages (&optional limit)
541606
"Return a JSON payload for recent messages using LIMIT."
607+
(ai-code-mcp-debug-tools--require-enabled)
542608
(let* ((limit (or limit 50))
543609
(messages (ai-code-mcp--message-lines)))
544610
(unless (and (integerp limit) (> limit 0))
@@ -556,6 +622,8 @@ existing bound variable."
556622
Return a JSON payload. BUFFER-NAME or FILE-PATH select the evaluation
557623
context. CAPTURE-MESSAGES, INCLUDE-BACKTRACE, and TIMEOUT-MS control
558624
diagnostics."
625+
(ai-code-mcp-debug-tools--require-enabled)
626+
(ai-code-mcp-debug-tools--require-eval-enabled)
559627
(let* ((capture-messages (ai-code-mcp-debug-tools--bool-arg
560628
capture-messages
561629
t))

ai-code.el

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,18 @@
138138
(defvar ai-code-mcp-agent-enabled-backends)
139139
(declare-function ai-code-install-backend-skills "ai-code-backends")
140140
(declare-function ai-code-backends-infra--session-buffer-p "ai-code-backends-infra" (buffer))
141+
(declare-function ai-code-mcp-debug-tools-enable-for-session
142+
"ai-code-mcp-debug-tools"
143+
(session-id &optional enable-eval-elisp))
141144

142145
(declare-function ai-code--process-word-for-filepath "ai-code-prompt-mode" (word git-root-truename))
143146
(declare-function ai-code-call-gptel-sync "ai-code-prompt-mode" (question))
144147

145148
;; Default aliases are set when a backend is applied via `ai-code-select-backend`.
146149

150+
(defvar ai-code-mcp-agent--session-id nil
151+
"Buffer-local MCP session id attached by `ai-code-mcp-agent`.")
152+
147153
;;;###autoload
148154
(defcustom ai-code-use-gptel-headline nil
149155
"Whether to use GPTel to generate headlines for prompt sections.
@@ -480,6 +486,60 @@ ARG is the prefix argument."
480486
clipboard-context)))))
481487
(ai-code--insert-prompt final-prompt)))))
482488

489+
(defun ai-code--active-mcp-session-id ()
490+
"Return the active AI session MCP id, or nil when unavailable."
491+
(or (and (boundp 'ai-code-mcp-agent--session-id)
492+
ai-code-mcp-agent--session-id)
493+
(when-let ((session-buffer
494+
(save-window-excursion
495+
(ignore-errors (ai-code-cli-switch-to-buffer)))))
496+
(when (buffer-live-p session-buffer)
497+
(buffer-local-value 'ai-code-mcp-agent--session-id
498+
session-buffer)))))
499+
500+
(defun ai-code--emacs-runtime-debug-prompt (description enable-eval-elisp)
501+
"Return an Emacs runtime debugging prompt from DESCRIPTION.
502+
ENABLE-EVAL-ELISP describes whether `eval_elisp' is available."
503+
(format
504+
(concat
505+
"Use the Emacs MCP tools available in this session to debug my Emacs runtime.\n"
506+
"The issue may involve an interactive function or a key binding.\n"
507+
"%s\n\n"
508+
"Inspect the relevant runtime state first: keymaps, command metadata,\n"
509+
"variables, recent messages, load state, and the last backtrace when useful.\n"
510+
"Explain what you find, then recommend the smallest fix or next step.\n\n"
511+
"Runtime issue description:\n"
512+
"%s")
513+
(if enable-eval-elisp
514+
"eval_elisp is enabled for this debugging session."
515+
"eval_elisp is disabled for this debugging session; rely on non-eval inspection tools unless you explain why more access is needed.")
516+
description))
517+
518+
;;;###autoload
519+
(defun ai-code-debug-emacs-runtime ()
520+
"Assemble and send an Emacs runtime debugging prompt for the current AI session."
521+
(interactive)
522+
(let* ((description
523+
(ai-code-read-string
524+
"Describe the Emacs runtime issue (eg: key binding / interactive function): "))
525+
(enable-eval-elisp
526+
(y-or-n-p
527+
"Allow AI to eval Emacs Lisp while debugging this Emacs runtime issue? "))
528+
(session-id (ai-code--active-mcp-session-id)))
529+
(when description
530+
(when-let* ((prompt
531+
(ai-code-read-string
532+
"Confirm and edit Emacs runtime debug prompt: "
533+
(ai-code--emacs-runtime-debug-prompt
534+
description
535+
enable-eval-elisp))))
536+
(when (and session-id
537+
(fboundp 'ai-code-mcp-debug-tools-enable-for-session))
538+
(ai-code-mcp-debug-tools-enable-for-session
539+
session-id
540+
enable-eval-elisp))
541+
(ai-code--insert-prompt prompt)))))
542+
483543
;;;###autoload
484544
(defun ai-code-cli-switch-to-buffer-or-hide ()
485545
"Hide the current buffer when its name both begins and ends with '*'.
@@ -639,6 +699,8 @@ Shows the current backend label to the right."
639699
("p" "Open prompt history file" ai-code-open-prompt-file)
640700
("m" "Debug python MCP server" ai-code-debug-mcp)
641701
("N" "Toggle notifications" ai-code-notifications-toggle)
702+
;; DONE: add a menu item: Debug your emacs runtime. It will temporarily enable ai-code-mcp-debug-tools-enabled, and ask user if they want to enable ai-code-mcp-debug-tools-enable-eval-elisp (eval elisp with AI?) to further help debugging. User can describe what happens (We prompt them that it can debug an interactive function or a key-binding). The final prompt will assemble with user description and then tell AI to user emacs mcp tools to debug. After user confirm the prompt, send to AI.
703+
("d" "Debug Emacs runtime" ai-code-debug-emacs-runtime)
642704
("h" "Help / Quick Start" ai-code-onboarding-open-quickstart))
643705

644706
(transient-define-prefix ai-code-menu-default ()

test/test_ai-code-mcp-debug-tools.el

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,34 @@
121121
(alist-get 'tools tools-result))))
122122
(should (member "eval_elisp" tool-names)))))
123123

124+
(ert-deftest ai-code-test-mcp-debug-tools-session-enable-overrides-global-disable ()
125+
"A session override should allow debug tools without changing the global default."
126+
(let ((ai-code-mcp-server-tools nil)
127+
(ai-code-mcp-debug-tools-enabled nil)
128+
(ai-code-mcp-debug-tools-enable-eval-elisp nil)
129+
(ai-code-mcp-debug-tools--session-overrides (make-hash-table :test 'equal)))
130+
(ai-code-mcp-debug-tools-enable-for-session "session-1" t)
131+
(let* ((ai-code-mcp--current-session-id "session-1")
132+
(payload
133+
(ai-code-test-mcp-debug-tools--read-json-payload
134+
(ai-code-mcp-dispatch
135+
"tools/call"
136+
'((name . "eval_elisp")
137+
(arguments . ((code . "(+ 1 2)"))))))))
138+
(should (equal t (alist-get 'ok payload)))
139+
(should (equal "3" (alist-get 'value_repr payload))))
140+
(let ((ai-code-mcp--current-session-id "session-2"))
141+
(should-error
142+
(ai-code-mcp-dispatch
143+
"tools/call"
144+
'((name . "get_recent_messages")
145+
(arguments . ()))))
146+
(should-error
147+
(ai-code-mcp-dispatch
148+
"tools/call"
149+
'((name . "eval_elisp")
150+
(arguments . ((code . "(+ 1 2)")))))))))
151+
124152
(ert-deftest ai-code-test-mcp-tools-list-warns-eval-elisp-is-unrestricted ()
125153
"Eval tool metadata should warn about unrestricted side effects."
126154
(let ((ai-code-mcp-server-tools nil)

test/test_ai-code.el

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,13 +565,66 @@
565565
(should (eq ai-code-backends-infra-terminal-backend 'ghostel))
566566
(should sync-called))))
567567

568+
(ert-deftest ai-code-test-debug-emacs-runtime-enables-session-tools-and-sends-confirmed-prompt ()
569+
"Debug Emacs runtime should enable MCP debug tools for the active session."
570+
(let (description-prompt
571+
confirm-read-args
572+
enabled-session-id
573+
enabled-eval-elisp
574+
sent-prompt)
575+
(cl-letf (((symbol-function 'y-or-n-p)
576+
(lambda (prompt)
577+
(should (string-match-p "eval Emacs Lisp" prompt))
578+
t))
579+
((symbol-function 'ai-code--active-mcp-session-id)
580+
(lambda () "session-123"))
581+
((symbol-function 'ai-code-mcp-debug-tools-enable-for-session)
582+
(lambda (session-id &optional enable-eval-elisp)
583+
(setq enabled-session-id session-id
584+
enabled-eval-elisp enable-eval-elisp)))
585+
((symbol-function 'ai-code-read-string)
586+
(lambda (prompt &optional initial-input _candidate-list)
587+
(cond
588+
((string-match-p "Describe the Emacs runtime issue" prompt)
589+
(setq description-prompt prompt)
590+
"C-c x runs the wrong interactive command")
591+
((string-match-p "Confirm and edit Emacs runtime debug prompt" prompt)
592+
(setq confirm-read-args (list prompt initial-input))
593+
initial-input)
594+
(t
595+
(ert-fail (format "Unexpected prompt: %s" prompt))))))
596+
((symbol-function 'ai-code--insert-prompt)
597+
(lambda (prompt)
598+
(setq sent-prompt prompt))))
599+
(ai-code-debug-emacs-runtime))
600+
(should (string-match-p "interactive function or a key binding"
601+
description-prompt))
602+
(should (equal enabled-session-id "session-123"))
603+
(should enabled-eval-elisp)
604+
(should (equal (car confirm-read-args)
605+
"Confirm and edit Emacs runtime debug prompt: "))
606+
(should (string-match-p "Use the Emacs MCP tools available in this session"
607+
(cadr confirm-read-args)))
608+
(should (string-match-p "eval_elisp is enabled" (cadr confirm-read-args)))
609+
(should (string-match-p "C-c x runs the wrong interactive command"
610+
sent-prompt))))
611+
568612
(ert-deftest ai-code-test-menu-ai-cli-session-includes-select-terminal-entry ()
569613
"Test that the AI CLI session menu exposes terminal backend selection."
570614
(let ((suffix (transient-get-suffix 'ai-code--menu-ai-cli-session "l")))
571615
(should suffix)
572616
(should (eq (plist-get (cdr suffix) :command)
573617
'ai-code-select-terminal))))
574618

619+
(ert-deftest ai-code-test-menu-other-tools-includes-debug-emacs-runtime-entry ()
620+
"Test that the Other Tools menu exposes Emacs runtime debugging."
621+
(let ((suffix (transient-get-suffix 'ai-code--menu-other-tools "d")))
622+
(should suffix)
623+
(should (eq (plist-get (cdr suffix) :command)
624+
'ai-code-debug-emacs-runtime))
625+
(should (equal (plist-get (cdr suffix) :description)
626+
"Debug Emacs runtime"))))
627+
575628
(ert-deftest ai-code-test-menu-prefix-command-default-layout ()
576629
"Test that the default menu layout uses the original transient."
577630
(let ((ai-code-menu-layout 'default))

0 commit comments

Comments
 (0)