Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
74 changes: 71 additions & 3 deletions ai-code-mcp-debug-tools.el
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand Down Expand Up @@ -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))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep session-only debug tools out of global MCP tool list

ai-code-mcp-debug-tools-enable-for-session is documented as session-scoped, but this code path unconditionally registers debug tools into the global ai-code-mcp-server-tools registry. Since ai-code-mcp--tools-list returns that global list for every session, enabling debug tools for one session makes other sessions advertise those tools even when they are disabled there, which leads agents to call tools that will fail at runtime and breaks the session-scoping contract.

Useful? React with 👍 / 👎.

session-id)

(defun ai-code-mcp--documentation-summary (documentation)
"Return a trimmed summary line for DOCUMENTATION."
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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))
Expand Down
62 changes: 62 additions & 0 deletions ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail fast when no MCP session is attached for runtime debug

This command still builds and sends a prompt asserting that Emacs MCP tools are available even when no session ID exists (for example, non-MCP backends or no active session), because tool enablement is conditional later and silently skipped. In that state the AI receives inaccurate capabilities and will attempt tool calls that cannot work, so the flow should error early or generate a non-MCP prompt variant.

Useful? React with 👍 / 👎.

(ai-code-mcp-debug-tools-enable-for-session
session-id
enable-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 +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 ()
Expand Down
28 changes: 28 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,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)
Expand Down
53 changes: 53 additions & 0 deletions test/test_ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -565,13 +565,66 @@
(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")))
(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