Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
42 changes: 39 additions & 3 deletions ai-code-mcp-debug-tools.el
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,42 @@
:optional t)))
"Optional MCP eval tool specification.")

(defun ai-code-mcp-debug-tools--register-base-tools ()
"Register the standard MCP debugging tools."
(dolist (tool ai-code-mcp-debug-tools--specs)
(apply #'ai-code-mcp-make-tool tool)))

(defun ai-code-mcp-debug-tools--register-eval-tool ()
"Register the optional `eval_elisp' MCP tool."
(apply #'ai-code-mcp-make-tool ai-code-mcp-debug-tools--eval-spec))

(defun ai-code-mcp-debug-tools--enabled-p ()
"Return non-nil when debug tools are enabled globally."
ai-code-mcp-debug-tools-enabled)

(defun ai-code-mcp-debug-tools--eval-enabled-p ()
"Return non-nil when `eval_elisp' is enabled globally."
ai-code-mcp-debug-tools-enable-eval-elisp)

(defun ai-code-mcp-debug-tools--require-enabled ()
"Signal an error unless debug inspection tools are enabled."
(unless (ai-code-mcp-debug-tools--enabled-p)
(error
"Enable ai-code-mcp-debug-tools-enabled to use the global Emacs debug MCP tools")))

(defun ai-code-mcp-debug-tools--require-eval-enabled ()
"Signal an error unless `eval_elisp' is enabled."
(unless (ai-code-mcp-debug-tools--eval-enabled-p)
(error
"Enable ai-code-mcp-debug-tools-enable-eval-elisp to use the global eval_elisp MCP tool")))

(defun ai-code-mcp-debug-tools-setup ()
"Register optional MCP debugging tools when enabled."
(when ai-code-mcp-debug-tools-enabled
(ai-code-mcp--ensure-error-capture)
(dolist (tool ai-code-mcp-debug-tools--specs)
(apply #'ai-code-mcp-make-tool tool))
(ai-code-mcp-debug-tools--register-base-tools)
(when ai-code-mcp-debug-tools-enable-eval-elisp
(apply #'ai-code-mcp-make-tool ai-code-mcp-debug-tools--eval-spec))))
(ai-code-mcp-debug-tools--register-eval-tool))))

(defun ai-code-mcp--documentation-summary (documentation)
"Return a trimmed summary line for DOCUMENTATION."
Expand Down Expand Up @@ -343,6 +371,7 @@ keeps the backtrace on failures."

(defun ai-code-mcp-get-variable-binding-info (variable-name &optional buffer-name)
"Return JSON binding details for VARIABLE-NAME in BUFFER-NAME."
(ai-code-mcp-debug-tools--require-enabled)
(let ((symbol (ai-code-mcp--find-existing-variable-symbol variable-name)))
(if (not symbol)
(json-encode
Expand Down Expand Up @@ -380,6 +409,7 @@ keeps the backtrace on failures."
"Return the printed representation of VARIABLE-NAME.
Return a friendly error string when VARIABLE-NAME does not name an
existing bound variable."
(ai-code-mcp-debug-tools--require-enabled)
(let ((symbol (ai-code-mcp--find-existing-variable-symbol variable-name)))
(cond
((not symbol)
Expand Down Expand Up @@ -429,6 +459,7 @@ existing bound variable."

(defun ai-code-mcp-get-function-info (function-name)
"Return JSON metadata describing FUNCTION-NAME."
(ai-code-mcp-debug-tools--require-enabled)
(let ((symbol (ai-code-mcp--find-existing-function-symbol function-name)))
(if (not (and symbol (fboundp symbol)))
(json-encode
Expand Down Expand Up @@ -480,6 +511,7 @@ existing bound variable."

(defun ai-code-mcp-get-last-error-backtrace ()
"Return a JSON snapshot of the most recently recorded Emacs error."
(ai-code-mcp-debug-tools--require-enabled)
(json-encode
(if ai-code-mcp--last-error-record
(ai-code-mcp--last-error-json-payload ai-code-mcp--last-error-record)
Expand Down Expand Up @@ -520,6 +552,7 @@ existing bound variable."

(defun ai-code-mcp-get-feature-load-state (feature-name)
"Return JSON load-state details for FEATURE-NAME."
(ai-code-mcp-debug-tools--require-enabled)
(if (not (ai-code-mcp--valid-feature-name-p feature-name))
(json-encode
(ai-code-mcp--invalid-feature-load-state-payload feature-name))
Expand All @@ -539,6 +572,7 @@ existing bound variable."

(defun ai-code-mcp-get-recent-messages (&optional limit)
"Return a JSON payload for recent messages using LIMIT."
(ai-code-mcp-debug-tools--require-enabled)
(let* ((limit (or limit 50))
(messages (ai-code-mcp--message-lines)))
(unless (and (integerp limit) (> limit 0))
Expand All @@ -556,6 +590,8 @@ existing bound variable."
Return a JSON payload. BUFFER-NAME or FILE-PATH select the evaluation
context. CAPTURE-MESSAGES, INCLUDE-BACKTRACE, and TIMEOUT-MS control
diagnostics."
(ai-code-mcp-debug-tools--require-enabled)
(ai-code-mcp-debug-tools--require-eval-enabled)
(let* ((capture-messages (ai-code-mcp-debug-tools--bool-arg
capture-messages
t))
Expand Down
54 changes: 54 additions & 0 deletions ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,59 @@ ARG is the prefix argument."
clipboard-context)))))
(ai-code--insert-prompt final-prompt)))))

(defun ai-code--emacs-runtime-debug-prompt (description eval-available-p
request-eval-elisp)
"Return an Emacs runtime debugging prompt from DESCRIPTION.
EVAL-AVAILABLE-P reports whether `eval_elisp' is globally enabled.
REQUEST-EVAL-ELISP reports whether this debug run may use it."
(format
(concat
"Use the Emacs MCP tools available in this session to debug my Emacs runtime.\n"
"The issue may involve an interactive function or a key binding.\n"
"%s\n\n"
"Inspect the relevant runtime state first: keymaps, command metadata,\n"
"variables, recent messages, load state, and the last backtrace when useful.\n"
"Explain what you find, then recommend the smallest fix or next step.\n\n"
"Runtime issue description:\n"
"%s")
(cond
((and eval-available-p request-eval-elisp)
"eval_elisp is enabled in your Emacs MCP config and is allowed for this debugging run.")
(eval-available-p
"eval_elisp is enabled in your Emacs MCP config, but it was not requested for this debugging run.")
(t
"eval_elisp is disabled in your Emacs MCP config, so rely on non-eval inspection tools unless you first enable ai-code-mcp-debug-tools-enable-eval-elisp."))
description))

;;;###autoload
(defun ai-code-debug-emacs-runtime ()
"Assemble and send an Emacs runtime debugging prompt for the current AI session."
(interactive)
(unless (bound-and-true-p ai-code-mcp-debug-tools-enabled)
(user-error
"Enable ai-code-mcp-debug-tools-enabled before using Emacs runtime debugging"))
(let* ((description
(ai-code-read-string
"Describe the Emacs runtime issue (it can be an interactive function or a key binding): "))
(eval-available-p
(bound-and-true-p ai-code-mcp-debug-tools-enable-eval-elisp))
(request-eval-elisp
(y-or-n-p
"Allow AI to eval Emacs Lisp while debugging this Emacs runtime issue? ")))
(when description
(when (and request-eval-elisp
(not eval-available-p))
(user-error
"Enable ai-code-mcp-debug-tools-enable-eval-elisp before requesting eval_elisp debugging"))
(when-let* ((prompt
(ai-code-read-string
"Confirm and edit Emacs runtime debug prompt: "
(ai-code--emacs-runtime-debug-prompt
description
eval-available-p
request-eval-elisp))))
(ai-code--insert-prompt prompt)))))
Comment on lines +508 to +534

;;;###autoload
(defun ai-code-cli-switch-to-buffer-or-hide ()
"Hide the current buffer when its name both begins and ends with '*'.
Expand Down Expand Up @@ -639,6 +692,7 @@ Shows the current backend label to the right."
("p" "Open prompt history file" ai-code-open-prompt-file)
("m" "Debug python MCP server" ai-code-debug-mcp)
("N" "Toggle notifications" ai-code-notifications-toggle)
("d" "Debug Emacs runtime" ai-code-debug-emacs-runtime)
("h" "Help / Quick Start" ai-code-onboarding-open-quickstart))

(transient-define-prefix ai-code-menu-default ()
Expand Down
24 changes: 24 additions & 0 deletions test/test_ai-code-mcp-debug-tools.el
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@
(alist-get 'tools tools-result))))
(should (member "eval_elisp" tool-names)))))

(ert-deftest ai-code-test-mcp-debug-tools-disabled-globally-blocks-access ()
"Global disable should block direct use of the optional debug tools."
(let ((ai-code-mcp-server-tools nil)
(ai-code-mcp-debug-tools-enabled nil)
(ai-code-mcp-debug-tools-enable-eval-elisp nil))
(should-error
(ai-code-mcp-get-recent-messages))
(should-error
(ai-code-mcp-eval-elisp "(+ 1 2)"))))

(ert-deftest ai-code-test-mcp-debug-tools-errors-name-global-flags ()
"Global gating errors should point to the relevant defcustom names."
(let ((ai-code-mcp-debug-tools-enabled nil)
(ai-code-mcp-debug-tools-enable-eval-elisp nil))
(should (string-match-p
"ai-code-mcp-debug-tools-enabled"
(error-message-string
(should-error (ai-code-mcp-get-recent-messages)))))
(let ((ai-code-mcp-debug-tools-enabled t))
(should (string-match-p
"ai-code-mcp-debug-tools-enable-eval-elisp"
(error-message-string
(should-error (ai-code-mcp-eval-elisp "(+ 1 2)"))))))))

(ert-deftest ai-code-test-mcp-tools-list-warns-eval-elisp-is-unrestricted ()
"Eval tool metadata should warn about unrestricted side effects."
(let ((ai-code-mcp-server-tools nil)
Expand Down
97 changes: 97 additions & 0 deletions test/test_ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -565,13 +565,110 @@
(should (eq ai-code-backends-infra-terminal-backend 'ghostel))
(should sync-called))))

(ert-deftest ai-code-test-debug-emacs-runtime-uses-global-eval-flag-in-prompt ()
"Debug Emacs runtime should describe the global eval flag state."
(let (description-prompt
confirm-read-args
sent-prompt)
(let ((ai-code-mcp-debug-tools-enabled t)
(ai-code-mcp-debug-tools-enable-eval-elisp t))
(cl-letf (((symbol-function 'y-or-n-p)
(lambda (prompt)
(should (string-match-p "eval Emacs Lisp" prompt))
t))
((symbol-function 'ai-code-read-string)
(lambda (prompt &optional initial-input _candidate-list)
(cond
((string-match-p "Describe the Emacs runtime issue" prompt)
(setq description-prompt prompt)
"C-c x runs the wrong interactive command")
((string-match-p "Confirm and edit Emacs runtime debug prompt" prompt)
(setq confirm-read-args (list prompt initial-input))
initial-input)
(t
(ert-fail (format "Unexpected prompt: %s" prompt))))))
((symbol-function 'ai-code--insert-prompt)
(lambda (prompt)
(setq sent-prompt prompt))))
(ai-code-debug-emacs-runtime)))
(should (string-match-p "interactive function or a key binding"
description-prompt))
(should (equal (car confirm-read-args)
"Confirm and edit Emacs runtime debug prompt: "))
(should (string-match-p "Use the Emacs MCP tools available in this session"
(cadr confirm-read-args)))
(should (string-match-p "eval_elisp is enabled in your Emacs MCP config"
(cadr confirm-read-args)))
(should (string-match-p "C-c x runs the wrong interactive command"
sent-prompt))))

(ert-deftest ai-code-test-debug-emacs-runtime-errors-when-global-eval-flag-is-off ()
"Debug Emacs runtime should tell the user to enable the global eval flag."
(let ((ai-code-mcp-debug-tools-enabled t)
(ai-code-mcp-debug-tools-enable-eval-elisp nil)
description-prompt)
(cl-letf (((symbol-function 'y-or-n-p)
(lambda (_prompt) t))
((symbol-function 'ai-code-read-string)
(lambda (prompt &optional _initial-input _candidate-list)
(setq description-prompt prompt)
"M-x foo fails"))
((symbol-function 'ai-code--insert-prompt)
(lambda (&rest _args)
(ert-fail "Should not send a prompt when eval_elisp is disabled globally."))))
(should-error
(ai-code-debug-emacs-runtime)
:type 'user-error))
(should (string-match-p "Describe the Emacs runtime issue"
description-prompt))))

(ert-deftest ai-code-test-debug-emacs-runtime-distinguishes-config-from-run-consent ()
"Debug Emacs runtime should separate global eval availability from per-run consent."
(let (confirm-read-args)
(let ((ai-code-mcp-debug-tools-enabled t)
(ai-code-mcp-debug-tools-enable-eval-elisp t))
(cl-letf (((symbol-function 'y-or-n-p)
(lambda (_prompt) nil))
((symbol-function 'ai-code-read-string)
(lambda (prompt &optional initial-input _candidate-list)
(if (string-match-p "Confirm and edit Emacs runtime debug prompt" prompt)
(progn
(setq confirm-read-args (list prompt initial-input))
initial-input)
"C-c x runs the wrong interactive command")))
((symbol-function 'ai-code--insert-prompt)
(lambda (&rest _args) nil)))
(ai-code-debug-emacs-runtime)))
(should (string-match-p
"eval_elisp is enabled in your Emacs MCP config"
(cadr confirm-read-args)))
(should (string-match-p
"not requested for this debugging run"
(cadr confirm-read-args)))))

(ert-deftest ai-code-test-debug-emacs-runtime-removes-stale-done-comment ()
"The source should not keep the stale DONE note for the runtime debug menu item."
(with-temp-buffer
(insert-file-contents (expand-file-name "ai-code.el" default-directory))
(should-not
(search-forward ";; DONE: add a menu item: Debug your emacs runtime." nil t))))

(ert-deftest ai-code-test-menu-ai-cli-session-includes-select-terminal-entry ()
"Test that the AI CLI session menu exposes terminal backend selection."
(let ((suffix (transient-get-suffix 'ai-code--menu-ai-cli-session "l")))
(should suffix)
(should (eq (plist-get (cdr suffix) :command)
'ai-code-select-terminal))))

(ert-deftest ai-code-test-menu-other-tools-includes-debug-emacs-runtime-entry ()
"Test that the Other Tools menu exposes Emacs runtime debugging."
(let ((suffix (transient-get-suffix 'ai-code--menu-other-tools "d")))
(should suffix)
(should (eq (plist-get (cdr suffix) :command)
'ai-code-debug-emacs-runtime))
(should (equal (plist-get (cdr suffix) :description)
"Debug Emacs runtime"))))

(ert-deftest ai-code-test-menu-prefix-command-default-layout ()
"Test that the default menu layout uses the original transient."
(let ((ai-code-menu-layout 'default))
Expand Down
Loading