From 481690a3b65baa9170d3df456f8f64842cf14f7b Mon Sep 17 00:00:00 2001 From: tninja Date: Wed, 29 Apr 2026 20:36:11 -0700 Subject: [PATCH 1/3] Add session-scoped MCP debug tools and command --- ai-code-mcp-debug-tools.el | 74 ++++++++++++++++++++++++++-- ai-code.el | 62 +++++++++++++++++++++++ test/test_ai-code-mcp-debug-tools.el | 28 +++++++++++ test/test_ai-code.el | 53 ++++++++++++++++++++ 4 files changed, 214 insertions(+), 3 deletions(-) diff --git a/ai-code-mcp-debug-tools.el b/ai-code-mcp-debug-tools.el index 271c31b..41320d0 100644 --- a/ai-code-mcp-debug-tools.el +++ b/ai-code-mcp-debug-tools.el @@ -30,6 +30,12 @@ :type 'boolean :group 'ai-code-mcp-debug-tools) +(defvar ai-code-mcp-debug-tools--session-overrides (make-hash-table :test 'equal) + "Hash table of session-local debug tool overrides keyed by MCP session id.") + +(defvar ai-code-mcp--current-session-id nil + "Dynamically bound MCP session id for the current tool invocation.") + (defvar ai-code-mcp--last-error-record nil "Most recent Emacs error snapshot recorded for MCP diagnostics tools.") @@ -107,14 +113,68 @@ :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--session-override (&optional session-id) + "Return session-local override for SESSION-ID or the active MCP session." + (gethash (or session-id ai-code-mcp--current-session-id) + ai-code-mcp-debug-tools--session-overrides)) + +(defun ai-code-mcp-debug-tools--enabled-p () + "Return non-nil when debug tools are enabled for the active session." + (or ai-code-mcp-debug-tools-enabled + (alist-get 'enabled + (ai-code-mcp-debug-tools--session-override)))) + +(defun ai-code-mcp-debug-tools--eval-enabled-p () + "Return non-nil when `eval_elisp' is enabled for the active session." + (or ai-code-mcp-debug-tools-enable-eval-elisp + (alist-get 'enable_eval_elisp + (ai-code-mcp-debug-tools--session-override)))) + +(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 "Emacs debug MCP tools are disabled for this session"))) + +(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 "The eval_elisp tool is disabled for this session"))) + (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)))) + +;;;###autoload +(defun ai-code-mcp-debug-tools-enable-for-session + (session-id &optional enable-eval-elisp) + "Enable MCP debug tools for SESSION-ID. +When ENABLE-EVAL-ELISP is non-nil, also expose `eval_elisp' for that +session." + (unless (and (stringp session-id) + (not (string-empty-p session-id))) + (user-error "SESSION-ID is required to enable Emacs debug MCP tools")) + (puthash session-id + `((enabled . t) + (enable_eval_elisp . ,(and enable-eval-elisp t))) + ai-code-mcp-debug-tools--session-overrides) + (ai-code-mcp--ensure-error-capture) + (ai-code-mcp-debug-tools--register-base-tools) + (when enable-eval-elisp + (ai-code-mcp-debug-tools--register-eval-tool)) + session-id) (defun ai-code-mcp--documentation-summary (documentation) "Return a trimmed summary line for DOCUMENTATION." @@ -343,6 +403,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 @@ -380,6 +441,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) @@ -429,6 +491,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 @@ -480,6 +543,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) @@ -520,6 +584,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)) @@ -539,6 +604,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)) @@ -556,6 +622,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)) diff --git a/ai-code.el b/ai-code.el index 669487b..94827e2 100644 --- a/ai-code.el +++ b/ai-code.el @@ -138,12 +138,18 @@ (defvar ai-code-mcp-agent-enabled-backends) (declare-function ai-code-install-backend-skills "ai-code-backends") (declare-function ai-code-backends-infra--session-buffer-p "ai-code-backends-infra" (buffer)) +(declare-function ai-code-mcp-debug-tools-enable-for-session + "ai-code-mcp-debug-tools" + (session-id &optional enable-eval-elisp)) (declare-function ai-code--process-word-for-filepath "ai-code-prompt-mode" (word git-root-truename)) (declare-function ai-code-call-gptel-sync "ai-code-prompt-mode" (question)) ;; Default aliases are set when a backend is applied via `ai-code-select-backend`. +(defvar ai-code-mcp-agent--session-id nil + "Buffer-local MCP session id attached by `ai-code-mcp-agent`.") + ;;;###autoload (defcustom ai-code-use-gptel-headline nil "Whether to use GPTel to generate headlines for prompt sections. @@ -480,6 +486,60 @@ ARG is the prefix argument." clipboard-context))))) (ai-code--insert-prompt final-prompt))))) +(defun ai-code--active-mcp-session-id () + "Return the active AI session MCP id, or nil when unavailable." + (or (and (boundp 'ai-code-mcp-agent--session-id) + ai-code-mcp-agent--session-id) + (when-let ((session-buffer + (save-window-excursion + (ignore-errors (ai-code-cli-switch-to-buffer))))) + (when (buffer-live-p session-buffer) + (buffer-local-value 'ai-code-mcp-agent--session-id + session-buffer))))) + +(defun ai-code--emacs-runtime-debug-prompt (description enable-eval-elisp) + "Return an Emacs runtime debugging prompt from DESCRIPTION. +ENABLE-EVAL-ELISP describes whether `eval_elisp' is available." + (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") + (if enable-eval-elisp + "eval_elisp is enabled for this debugging session." + "eval_elisp is disabled for this debugging session; rely on non-eval inspection tools unless you explain why more access is needed.") + description)) + +;;;###autoload +(defun ai-code-debug-emacs-runtime () + "Assemble and send an Emacs runtime debugging prompt for the current AI session." + (interactive) + (let* ((description + (ai-code-read-string + "Describe the Emacs runtime issue (eg: key binding / interactive function): ")) + (enable-eval-elisp + (y-or-n-p + "Allow AI to eval Emacs Lisp while debugging this Emacs runtime issue? ")) + (session-id (ai-code--active-mcp-session-id))) + (when description + (when-let* ((prompt + (ai-code-read-string + "Confirm and edit Emacs runtime debug prompt: " + (ai-code--emacs-runtime-debug-prompt + description + enable-eval-elisp)))) + (when (and session-id + (fboundp 'ai-code-mcp-debug-tools-enable-for-session)) + (ai-code-mcp-debug-tools-enable-for-session + session-id + enable-eval-elisp)) + (ai-code--insert-prompt prompt))))) + ;;;###autoload (defun ai-code-cli-switch-to-buffer-or-hide () "Hide the current buffer when its name both begins and ends with '*'. @@ -639,6 +699,8 @@ 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) + ;; 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. + ("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 () diff --git a/test/test_ai-code-mcp-debug-tools.el b/test/test_ai-code-mcp-debug-tools.el index c6617af..ced3aed 100644 --- a/test/test_ai-code-mcp-debug-tools.el +++ b/test/test_ai-code-mcp-debug-tools.el @@ -121,6 +121,34 @@ (alist-get 'tools tools-result)))) (should (member "eval_elisp" tool-names))))) +(ert-deftest ai-code-test-mcp-debug-tools-session-enable-overrides-global-disable () + "A session override should allow debug tools without changing the global default." + (let ((ai-code-mcp-server-tools nil) + (ai-code-mcp-debug-tools-enabled nil) + (ai-code-mcp-debug-tools-enable-eval-elisp nil) + (ai-code-mcp-debug-tools--session-overrides (make-hash-table :test 'equal))) + (ai-code-mcp-debug-tools-enable-for-session "session-1" t) + (let* ((ai-code-mcp--current-session-id "session-1") + (payload + (ai-code-test-mcp-debug-tools--read-json-payload + (ai-code-mcp-dispatch + "tools/call" + '((name . "eval_elisp") + (arguments . ((code . "(+ 1 2)")))))))) + (should (equal t (alist-get 'ok payload))) + (should (equal "3" (alist-get 'value_repr payload)))) + (let ((ai-code-mcp--current-session-id "session-2")) + (should-error + (ai-code-mcp-dispatch + "tools/call" + '((name . "get_recent_messages") + (arguments . ())))) + (should-error + (ai-code-mcp-dispatch + "tools/call" + '((name . "eval_elisp") + (arguments . ((code . "(+ 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) diff --git a/test/test_ai-code.el b/test/test_ai-code.el index 99c7e8a..e0b16c8 100644 --- a/test/test_ai-code.el +++ b/test/test_ai-code.el @@ -565,6 +565,50 @@ (should (eq ai-code-backends-infra-terminal-backend 'ghostel)) (should sync-called)))) +(ert-deftest ai-code-test-debug-emacs-runtime-enables-session-tools-and-sends-confirmed-prompt () + "Debug Emacs runtime should enable MCP debug tools for the active session." + (let (description-prompt + confirm-read-args + enabled-session-id + enabled-eval-elisp + sent-prompt) + (cl-letf (((symbol-function 'y-or-n-p) + (lambda (prompt) + (should (string-match-p "eval Emacs Lisp" prompt)) + t)) + ((symbol-function 'ai-code--active-mcp-session-id) + (lambda () "session-123")) + ((symbol-function 'ai-code-mcp-debug-tools-enable-for-session) + (lambda (session-id &optional enable-eval-elisp) + (setq enabled-session-id session-id + enabled-eval-elisp enable-eval-elisp))) + ((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 enabled-session-id "session-123")) + (should enabled-eval-elisp) + (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" (cadr confirm-read-args))) + (should (string-match-p "C-c x runs the wrong interactive command" + sent-prompt)))) + (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"))) @@ -572,6 +616,15 @@ (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)) From f2f00e996a572ca6cd3d727a6086d009efb568ae Mon Sep 17 00:00:00 2001 From: tninja Date: Thu, 30 Apr 2026 19:37:44 -0700 Subject: [PATCH 2/3] Simplify Emacs runtime debug flag flow --- ai-code-mcp-debug-tools.el | 42 +++------------------------ ai-code.el | 38 +++++++----------------- test/test_ai-code-mcp-debug-tools.el | 32 +++++---------------- test/test_ai-code.el | 43 ++++++++++++++++++---------- 4 files changed, 50 insertions(+), 105 deletions(-) diff --git a/ai-code-mcp-debug-tools.el b/ai-code-mcp-debug-tools.el index 41320d0..93ed965 100644 --- a/ai-code-mcp-debug-tools.el +++ b/ai-code-mcp-debug-tools.el @@ -30,12 +30,6 @@ :type 'boolean :group 'ai-code-mcp-debug-tools) -(defvar ai-code-mcp-debug-tools--session-overrides (make-hash-table :test 'equal) - "Hash table of session-local debug tool overrides keyed by MCP session id.") - -(defvar ai-code-mcp--current-session-id nil - "Dynamically bound MCP session id for the current tool invocation.") - (defvar ai-code-mcp--last-error-record nil "Most recent Emacs error snapshot recorded for MCP diagnostics tools.") @@ -122,22 +116,13 @@ "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--session-override (&optional session-id) - "Return session-local override for SESSION-ID or the active MCP session." - (gethash (or session-id ai-code-mcp--current-session-id) - ai-code-mcp-debug-tools--session-overrides)) - (defun ai-code-mcp-debug-tools--enabled-p () - "Return non-nil when debug tools are enabled for the active session." - (or ai-code-mcp-debug-tools-enabled - (alist-get 'enabled - (ai-code-mcp-debug-tools--session-override)))) + "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 for the active session." - (or ai-code-mcp-debug-tools-enable-eval-elisp - (alist-get 'enable_eval_elisp - (ai-code-mcp-debug-tools--session-override)))) + "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." @@ -157,25 +142,6 @@ (when ai-code-mcp-debug-tools-enable-eval-elisp (ai-code-mcp-debug-tools--register-eval-tool)))) -;;;###autoload -(defun ai-code-mcp-debug-tools-enable-for-session - (session-id &optional enable-eval-elisp) - "Enable MCP debug tools for SESSION-ID. -When ENABLE-EVAL-ELISP is non-nil, also expose `eval_elisp' for that -session." - (unless (and (stringp session-id) - (not (string-empty-p session-id))) - (user-error "SESSION-ID is required to enable Emacs debug MCP tools")) - (puthash session-id - `((enabled . t) - (enable_eval_elisp . ,(and enable-eval-elisp t))) - ai-code-mcp-debug-tools--session-overrides) - (ai-code-mcp--ensure-error-capture) - (ai-code-mcp-debug-tools--register-base-tools) - (when enable-eval-elisp - (ai-code-mcp-debug-tools--register-eval-tool)) - session-id) - (defun ai-code-mcp--documentation-summary (documentation) "Return a trimmed summary line for DOCUMENTATION." (if (stringp documentation) diff --git a/ai-code.el b/ai-code.el index 94827e2..4b785f1 100644 --- a/ai-code.el +++ b/ai-code.el @@ -138,18 +138,12 @@ (defvar ai-code-mcp-agent-enabled-backends) (declare-function ai-code-install-backend-skills "ai-code-backends") (declare-function ai-code-backends-infra--session-buffer-p "ai-code-backends-infra" (buffer)) -(declare-function ai-code-mcp-debug-tools-enable-for-session - "ai-code-mcp-debug-tools" - (session-id &optional enable-eval-elisp)) (declare-function ai-code--process-word-for-filepath "ai-code-prompt-mode" (word git-root-truename)) (declare-function ai-code-call-gptel-sync "ai-code-prompt-mode" (question)) ;; Default aliases are set when a backend is applied via `ai-code-select-backend`. -(defvar ai-code-mcp-agent--session-id nil - "Buffer-local MCP session id attached by `ai-code-mcp-agent`.") - ;;;###autoload (defcustom ai-code-use-gptel-headline nil "Whether to use GPTel to generate headlines for prompt sections. @@ -486,17 +480,6 @@ ARG is the prefix argument." clipboard-context))))) (ai-code--insert-prompt final-prompt))))) -(defun ai-code--active-mcp-session-id () - "Return the active AI session MCP id, or nil when unavailable." - (or (and (boundp 'ai-code-mcp-agent--session-id) - ai-code-mcp-agent--session-id) - (when-let ((session-buffer - (save-window-excursion - (ignore-errors (ai-code-cli-switch-to-buffer))))) - (when (buffer-live-p session-buffer) - (buffer-local-value 'ai-code-mcp-agent--session-id - session-buffer))))) - (defun ai-code--emacs-runtime-debug-prompt (description enable-eval-elisp) "Return an Emacs runtime debugging prompt from DESCRIPTION. ENABLE-EVAL-ELISP describes whether `eval_elisp' is available." @@ -511,33 +494,34 @@ ENABLE-EVAL-ELISP describes whether `eval_elisp' is available." "Runtime issue description:\n" "%s") (if enable-eval-elisp - "eval_elisp is enabled for this debugging session." - "eval_elisp is disabled for this debugging session; rely on non-eval inspection tools unless you explain why more access is needed.") + "eval_elisp is enabled in your Emacs MCP config." + "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 (eg: key binding / interactive function): ")) + "Describe the Emacs runtime issue (it can be an interactive function or a key binding): ")) (enable-eval-elisp (y-or-n-p - "Allow AI to eval Emacs Lisp while debugging this Emacs runtime issue? ")) - (session-id (ai-code--active-mcp-session-id))) + "Allow AI to eval Emacs Lisp while debugging this Emacs runtime issue? "))) (when description + (when (and enable-eval-elisp + (not (bound-and-true-p ai-code-mcp-debug-tools-enable-eval-elisp))) + (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 enable-eval-elisp)))) - (when (and session-id - (fboundp 'ai-code-mcp-debug-tools-enable-for-session)) - (ai-code-mcp-debug-tools-enable-for-session - session-id - enable-eval-elisp)) (ai-code--insert-prompt prompt))))) ;;;###autoload diff --git a/test/test_ai-code-mcp-debug-tools.el b/test/test_ai-code-mcp-debug-tools.el index ced3aed..5d2a7ca 100644 --- a/test/test_ai-code-mcp-debug-tools.el +++ b/test/test_ai-code-mcp-debug-tools.el @@ -121,33 +121,15 @@ (alist-get 'tools tools-result)))) (should (member "eval_elisp" tool-names))))) -(ert-deftest ai-code-test-mcp-debug-tools-session-enable-overrides-global-disable () - "A session override should allow debug tools without changing the global default." +(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) - (ai-code-mcp-debug-tools--session-overrides (make-hash-table :test 'equal))) - (ai-code-mcp-debug-tools-enable-for-session "session-1" t) - (let* ((ai-code-mcp--current-session-id "session-1") - (payload - (ai-code-test-mcp-debug-tools--read-json-payload - (ai-code-mcp-dispatch - "tools/call" - '((name . "eval_elisp") - (arguments . ((code . "(+ 1 2)")))))))) - (should (equal t (alist-get 'ok payload))) - (should (equal "3" (alist-get 'value_repr payload)))) - (let ((ai-code-mcp--current-session-id "session-2")) - (should-error - (ai-code-mcp-dispatch - "tools/call" - '((name . "get_recent_messages") - (arguments . ())))) - (should-error - (ai-code-mcp-dispatch - "tools/call" - '((name . "eval_elisp") - (arguments . ((code . "(+ 1 2)"))))))))) + (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-tools-list-warns-eval-elisp-is-unrestricted () "Eval tool metadata should warn about unrestricted side effects." diff --git a/test/test_ai-code.el b/test/test_ai-code.el index e0b16c8..cf3beea 100644 --- a/test/test_ai-code.el +++ b/test/test_ai-code.el @@ -565,23 +565,17 @@ (should (eq ai-code-backends-infra-terminal-backend 'ghostel)) (should sync-called)))) -(ert-deftest ai-code-test-debug-emacs-runtime-enables-session-tools-and-sends-confirmed-prompt () - "Debug Emacs runtime should enable MCP debug tools for the active session." +(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 - enabled-session-id - enabled-eval-elisp sent-prompt) - (cl-letf (((symbol-function 'y-or-n-p) + (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--active-mcp-session-id) - (lambda () "session-123")) - ((symbol-function 'ai-code-mcp-debug-tools-enable-for-session) - (lambda (session-id &optional enable-eval-elisp) - (setq enabled-session-id session-id - enabled-eval-elisp enable-eval-elisp))) ((symbol-function 'ai-code-read-string) (lambda (prompt &optional initial-input _candidate-list) (cond @@ -596,19 +590,38 @@ ((symbol-function 'ai-code--insert-prompt) (lambda (prompt) (setq sent-prompt prompt)))) - (ai-code-debug-emacs-runtime)) + (ai-code-debug-emacs-runtime))) (should (string-match-p "interactive function or a key binding" description-prompt)) - (should (equal enabled-session-id "session-123")) - (should enabled-eval-elisp) (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" (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-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"))) From ddffe2b29555e02f1c9c55c80c041c71c8a5a714 Mon Sep 17 00:00:00 2001 From: tninja Date: Thu, 30 Apr 2026 20:51:15 -0700 Subject: [PATCH 3/3] Address runtime debug review feedback --- ai-code-mcp-debug-tools.el | 6 ++++-- ai-code.el | 28 ++++++++++++++++--------- test/test_ai-code-mcp-debug-tools.el | 14 +++++++++++++ test/test_ai-code.el | 31 ++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 12 deletions(-) diff --git a/ai-code-mcp-debug-tools.el b/ai-code-mcp-debug-tools.el index 93ed965..22b9a60 100644 --- a/ai-code-mcp-debug-tools.el +++ b/ai-code-mcp-debug-tools.el @@ -127,12 +127,14 @@ (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 "Emacs debug MCP tools are disabled for this session"))) + (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 "The eval_elisp tool is disabled for this session"))) + (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." diff --git a/ai-code.el b/ai-code.el index 4b785f1..3c02a9d 100644 --- a/ai-code.el +++ b/ai-code.el @@ -480,9 +480,11 @@ ARG is the prefix argument." clipboard-context))))) (ai-code--insert-prompt final-prompt))))) -(defun ai-code--emacs-runtime-debug-prompt (description enable-eval-elisp) +(defun ai-code--emacs-runtime-debug-prompt (description eval-available-p + request-eval-elisp) "Return an Emacs runtime debugging prompt from DESCRIPTION. -ENABLE-EVAL-ELISP describes whether `eval_elisp' is available." +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" @@ -493,9 +495,13 @@ ENABLE-EVAL-ELISP describes whether `eval_elisp' is available." "Explain what you find, then recommend the smallest fix or next step.\n\n" "Runtime issue description:\n" "%s") - (if enable-eval-elisp - "eval_elisp is enabled in your Emacs MCP config." - "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.") + (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 @@ -508,12 +514,14 @@ ENABLE-EVAL-ELISP describes whether `eval_elisp' is available." (let* ((description (ai-code-read-string "Describe the Emacs runtime issue (it can be an interactive function or a key binding): ")) - (enable-eval-elisp + (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 enable-eval-elisp - (not (bound-and-true-p ai-code-mcp-debug-tools-enable-eval-elisp))) + (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 @@ -521,7 +529,8 @@ ENABLE-EVAL-ELISP describes whether `eval_elisp' is available." "Confirm and edit Emacs runtime debug prompt: " (ai-code--emacs-runtime-debug-prompt description - enable-eval-elisp)))) + eval-available-p + request-eval-elisp)))) (ai-code--insert-prompt prompt))))) ;;;###autoload @@ -683,7 +692,6 @@ 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) - ;; 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. ("d" "Debug Emacs runtime" ai-code-debug-emacs-runtime) ("h" "Help / Quick Start" ai-code-onboarding-open-quickstart)) diff --git a/test/test_ai-code-mcp-debug-tools.el b/test/test_ai-code-mcp-debug-tools.el index 5d2a7ca..a2aeab6 100644 --- a/test/test_ai-code-mcp-debug-tools.el +++ b/test/test_ai-code-mcp-debug-tools.el @@ -131,6 +131,20 @@ (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) diff --git a/test/test_ai-code.el b/test/test_ai-code.el index cf3beea..41dca8c 100644 --- a/test/test_ai-code.el +++ b/test/test_ai-code.el @@ -622,6 +622,37 @@ (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")))