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
2 changes: 2 additions & 0 deletions HISTORY.org
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Release history

** Main branch change
- Feat: Add more emacs mcps: buffer and project query tools and xref support
- Referencing https://github.com/acmorrow/claude-code-ide-extras, port 4 mcps from there.

** 1.67
- Chore: Size side windows by body width and add test
Expand Down
6 changes: 6 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ AI Code includes an Emacs MCP server with these built-in tools:
- =imenu_list_symbols=
- =xref_find_references=
- =treesit_info=
- .. and others

screenshot inside Codex cli:

[[file:./emacs_mcp_tools.png]]

***** Use It with an AI CLI

Expand Down Expand Up @@ -447,6 +452,7 @@ The following books introduce how to use AI to assist programming and potentiall
- Gemini CLI (`[[https://github.com/linchen2chris/gemini-cli.el][gemini-cli.el]]`)
- [[https://github.com/xenodium/agent-shell][agent-shell]] ([[https://github.com/xenodium/acp.el][acp.el]])
- [[https://eca.dev/][ECA (Editor Code Assistant)]]
- [[https://github.com/acmorrow/claude-code-ide-extras][claude-code-ide-extras.el]]

** License

Expand Down
195 changes: 184 additions & 11 deletions ai-code-mcp-server.el
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
;;; ai-code-mcp-server.el --- MCP tools core for AI Code Interface -*- lexical-binding: t; -*-

;; Author: Yoav Orot, Kang Tu, AI Agent
;; Author: Yoav Orot, Kang Tu, Andrew Morrow, AI Agent
;; SPDX-License-Identifier: Apache-2.0

;;; Commentary:
Expand All @@ -14,6 +14,8 @@
(require 'cl-lib)
(require 'imenu)
(require 'project)
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

ai-code-mcp-server.el uses several seq-* functions (e.g., seq-remove, seq-filter) but does not (require 'seq). Many other modules in this repo explicitly require seq when using these functions; adding the require here avoids load-order dependent runtime failures and byte-compile warnings when this file is loaded standalone.

Suggested change
(require 'project)
(require 'project)
(require 'seq)

Copilot uses AI. Check for mistakes.
(require 'seq)
(require 'subr-x)
(require 'xref)

(require 'ai-code-input)
Expand Down Expand Up @@ -52,6 +54,28 @@ Each item is a plist with at least `:function', `:name', and `:description'."
:name "project_info"
:description "Get information about the current project context."
:args nil)
(:function ai-code-mcp-buffer-query
:name "buffer_query"
:description "Read contents from an Emacs buffer by line range."
:args ((:name "buffer_name"
:type string
:description "Name of the buffer to read.")
(:name "start_line"
:type integer
:description "1-based first line to read."
:optional t)
(:name "num_lines"
:type integer
:description "Number of lines to read from start_line."
:optional t)))
(:function ai-code-mcp-get-project-files
:name "get_project_files"
:description "List regular files in the current project."
:args nil)
(:function ai-code-mcp-get-project-buffers
:name "get_project_buffers"
:description "List open buffers that belong to the current project."
:args nil)
(:function ai-code-mcp-imenu-list-symbols
:name "imenu_list_symbols"
:description "List useful symbols in a file via imenu."
Expand All @@ -67,6 +91,18 @@ Each item is a plist with at least `:function', `:name', and `:description'."
(:name "file_path"
:type string
:description "Path to the file that provides backend context.")))
(:function ai-code-mcp-xref-find-definitions-at-point
:name "xref_find_definitions_at_point"
:description "Find definitions of the identifier at a file location."
:args ((:name "file_path"
:type string
:description "Path to the file that provides backend context.")
(:name "line"
:type integer
:description "1-based line number.")
(:name "column"
:type integer
:description "0-based column number.")))
(:function ai-code-mcp-treesit-info
:name "treesit_info"
:description "Return tree-sitter node information for a file location."
Expand Down Expand Up @@ -183,6 +219,91 @@ Required keys are `:function', `:name', and `:description'."
"No active buffer")
file-count)))

(defun ai-code-mcp--validate-buffer-query-range (start-line num-lines)
"Validate optional buffer query range arguments START-LINE and NUM-LINES."
(when (or (and start-line (not num-lines))
(and num-lines (not start-line)))
(error "Arguments start_line and num_lines must both be provided or both be omitted"))
(when (and start-line
(or (< start-line 1)
(< num-lines 1)))
(error "Arguments start_line and num_lines must be positive integers")))

(defun ai-code-mcp--drop-trailing-newline (text)
"Return TEXT without a single trailing newline."
(if (string-suffix-p "\n" text)
(substring text 0 -1)
text))

(defun ai-code-mcp-buffer-query (buffer-name &optional start-line num-lines)
"Return contents from BUFFER-NAME.
When START-LINE and NUM-LINES are non-nil, return only that line range."
(let ((buffer (get-buffer buffer-name)))
(if (not buffer)
(format "Error: Buffer not found: %s" buffer-name)
(ai-code-mcp--validate-buffer-query-range start-line num-lines)
(with-current-buffer buffer
(save-excursion
(if (not start-line)
(buffer-substring-no-properties (point-min) (point-max))
(goto-char (point-min))
(forward-line (1- start-line))
(let ((start-pos (point)))
(forward-line num-lines)
(ai-code-mcp--drop-trailing-newline
(buffer-substring-no-properties start-pos (point))))))))))

(defun ai-code-mcp--project-files (project-dir)
"Return absolute regular files inside PROJECT-DIR."
(let* ((default-directory (file-name-as-directory project-dir))
(project (project-current nil project-dir))
(project-root default-directory))
(or (ignore-errors
(when (and project (fboundp 'project-files))
(seq-filter
#'file-regular-p
(mapcar (lambda (file)
(if (file-name-absolute-p file)
file
(expand-file-name file project-root)))
(project-files project)))))
(cl-labels
((collect-files (dir)
(apply
#'append
(mapcar
(lambda (entry)
(cond
((member entry '("." "..")) nil)
((string-prefix-p "." entry) nil)
(t
(let ((path (expand-file-name entry dir)))
(cond
((file-directory-p path)
(collect-files path))
((file-regular-p path)
(list path))
(t nil))))))
(directory-files dir nil nil t)))))
(collect-files project-root)))))

(defun ai-code-mcp-get-project-files ()
"Return regular files in the current project as relative paths."
(let ((project-dir (ai-code-mcp--project-directory)))
(if (not (and project-dir (file-directory-p project-dir)))
nil
(mapcar #'ai-code-mcp--display-path
(ai-code-mcp--project-files project-dir)))))

(defun ai-code-mcp-get-project-buffers ()
"Return open buffers that belong to the current project."
(let ((project-dir (ai-code-mcp--project-directory)))
(delq nil
(mapcar
(lambda (buffer)
(ai-code-mcp--project-buffer-entry buffer project-dir))
(buffer-list)))))

(defun ai-code-mcp-imenu-list-symbols (file-path)
"Return formatted imenu entries for FILE-PATH."
(let* ((resolved-file (ai-code-mcp--require-file-path file-path))
Expand All @@ -205,8 +326,29 @@ Required keys are `:function', `:name', and `:description'."
(format "No references found for '%s'" identifier)
(mapcar #'ai-code-mcp--format-xref-item items))))))))

(defun ai-code-mcp-xref-find-definitions-at-point (file-path line column)
"Return formatted xref definitions for the identifier at FILE-PATH:LINE:COLUMN."
(let ((buffer (ai-code-mcp--file-buffer
(ai-code-mcp--require-file-path file-path))))
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(move-to-column column)
(let ((backend (xref-find-backend)))
(if (not backend)
(format "No xref backend available for %s" file-path)
(let ((identifier (xref-backend-identifier-at-point backend)))
(if (not identifier)
(format "No identifier at %s:%d:%d" file-path line column)
(let ((items (xref-backend-definitions backend identifier)))
(if (not items)
(format "No definitions found for '%s'" identifier)
(mapcar #'ai-code-mcp--format-xref-item items)))))))))))

(defun ai-code-mcp-treesit-info (file-path &optional line column whole-file)
"Return tree-sitter information for FILE-PATH at LINE and COLUMN."
"Return tree-sitter information for FILE-PATH at LINE and COLUMN.
When WHOLE-FILE is non-nil, inspect the root node instead."
(cond
((not (and (fboundp 'treesit-available-p)
(treesit-available-p)))
Expand Down Expand Up @@ -332,9 +474,35 @@ Required keys are `:function', `:name', and `:description'."
"Return RESULT converted to a tool response string."
(cond
((stringp result) result)
((listp result) (mapconcat #'identity result "\n"))
((listp result)
(mapconcat (lambda (item)
(if (stringp item)
item
(format "%S" item)))
result
"\n"))
(t (format "%s" result))))

(defun ai-code-mcp--project-buffer-entry (buffer project-dir)
"Return buffer metadata for BUFFER when it belongs to PROJECT-DIR."
(when (ai-code-mcp--buffer-in-project-p buffer project-dir)
(with-current-buffer buffer
`((name . ,(buffer-name buffer))
(mode . ,major-mode)
(file . ,(buffer-file-name))
(modified . ,(buffer-modified-p buffer))))))

(defun ai-code-mcp--buffer-in-project-p (buffer project-dir)
"Return non-nil when BUFFER belongs to PROJECT-DIR."
(and (file-directory-p project-dir)
(with-current-buffer buffer
(let ((file (buffer-file-name))
(buffer-dir default-directory))
(or (and file
(file-in-directory-p file project-dir))
(and buffer-dir
(file-in-directory-p buffer-dir project-dir)))))))

(defun ai-code-mcp--project-directory ()
"Return the best available project directory."
(or (when-let ((context (ai-code-mcp-get-session-context)))
Expand All @@ -352,17 +520,21 @@ Required keys are `:function', `:name', and `:description'."

(defun ai-code-mcp--display-path (file-path)
"Return FILE-PATH relative to the active project when possible."
(let ((project-dir (ai-code-mcp--project-directory)))
(if (and project-dir
(string-prefix-p (expand-file-name project-dir)
(expand-file-name file-path)))
(file-relative-name file-path project-dir)
(file-name-nondirectory file-path))))
(let* ((expanded-path (and file-path (expand-file-name file-path)))
(project-dir (ai-code-mcp--project-directory))
(project-root (and project-dir
(file-name-as-directory
(expand-file-name project-dir)))))
(if (and expanded-path
project-root
(file-in-directory-p expanded-path project-root))
(file-relative-name expanded-path project-root)
expanded-path)))

(defun ai-code-mcp--require-file-path (file-path)
"Return FILE-PATH as an absolute path or signal an error."
(unless file-path
(error "file_path is required"))
(error "Argument file_path is required"))
(expand-file-name file-path))

(defun ai-code-mcp--file-buffer (file-path)
Expand Down Expand Up @@ -392,7 +564,8 @@ Required keys are `:function', `:name', and `:description'."
(defun ai-code-mcp--format-xref-item (item)
"Return a human-readable line for xref ITEM."
(let* ((location (xref-item-location item))
(group (xref-location-group location))
(group (ai-code-mcp--display-path
(xref-location-group location)))
Comment on lines +567 to +568
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 Preserve full paths for external xref hits

Wrapping xref-location-group with ai-code-mcp--display-path truncates any location outside the active project to file-name-nondirectory, so references/definitions from different external files with the same basename become indistinguishable (for example, two different index.js files). Before this change, the formatter kept the full group path, so this is a regression in result accuracy whenever the backend returns matches outside the project root.

Useful? React with 👍 / 👎.

(marker (xref-location-marker location))
Comment on lines 564 to 569
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

ai-code-mcp--format-xref-item now runs xref-location-group through ai-code-mcp--display-path, but ai-code-mcp--display-path uses a simple string-prefix-p check against project-dir. That can misclassify paths like /tmp/proj2/... as being under /tmp/proj and produce misleading relative paths with ... Consider switching the containment check to file-in-directory-p (or ensure project-dir is normalized with a trailing slash) before calling file-relative-name.

Copilot uses AI. Check for mistakes.
(line (with-current-buffer (marker-buffer marker)
(save-excursion
Expand Down
Binary file added emacs_mcp_tools.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading