Skip to content

Commit 48d0a97

Browse files
authored
Feat: Add more emacs mcps: buffer and project query tools and xref support (#277)
* Add buffer and project query tools and xref support * update author * add README * add screenshot * addressing feedbacks * add HISTORY
1 parent ba8c113 commit 48d0a97

5 files changed

Lines changed: 425 additions & 13 deletions

File tree

HISTORY.org

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Release history
33

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

68
** 1.67
79
- Chore: Size side windows by body width and add test

README.org

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ AI Code includes an Emacs MCP server with these built-in tools:
196196
- =imenu_list_symbols=
197197
- =xref_find_references=
198198
- =treesit_info=
199+
- .. and others
200+
201+
screenshot inside Codex cli:
202+
203+
[[file:./emacs_mcp_tools.png]]
199204

200205
***** Use It with an AI CLI
201206

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

451457
** License
452458

ai-code-mcp-server.el

Lines changed: 184 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; ai-code-mcp-server.el --- MCP tools core for AI Code Interface -*- lexical-binding: t; -*-
22

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

66
;;; Commentary:
@@ -14,6 +14,8 @@
1414
(require 'cl-lib)
1515
(require 'imenu)
1616
(require 'project)
17+
(require 'seq)
18+
(require 'subr-x)
1719
(require 'xref)
1820

1921
(require 'ai-code-input)
@@ -52,6 +54,28 @@ Each item is a plist with at least `:function', `:name', and `:description'."
5254
:name "project_info"
5355
:description "Get information about the current project context."
5456
:args nil)
57+
(:function ai-code-mcp-buffer-query
58+
:name "buffer_query"
59+
:description "Read contents from an Emacs buffer by line range."
60+
:args ((:name "buffer_name"
61+
:type string
62+
:description "Name of the buffer to read.")
63+
(:name "start_line"
64+
:type integer
65+
:description "1-based first line to read."
66+
:optional t)
67+
(:name "num_lines"
68+
:type integer
69+
:description "Number of lines to read from start_line."
70+
:optional t)))
71+
(:function ai-code-mcp-get-project-files
72+
:name "get_project_files"
73+
:description "List regular files in the current project."
74+
:args nil)
75+
(:function ai-code-mcp-get-project-buffers
76+
:name "get_project_buffers"
77+
:description "List open buffers that belong to the current project."
78+
:args nil)
5579
(:function ai-code-mcp-imenu-list-symbols
5680
:name "imenu_list_symbols"
5781
:description "List useful symbols in a file via imenu."
@@ -67,6 +91,18 @@ Each item is a plist with at least `:function', `:name', and `:description'."
6791
(:name "file_path"
6892
:type string
6993
:description "Path to the file that provides backend context.")))
94+
(:function ai-code-mcp-xref-find-definitions-at-point
95+
:name "xref_find_definitions_at_point"
96+
:description "Find definitions of the identifier at a file location."
97+
:args ((:name "file_path"
98+
:type string
99+
:description "Path to the file that provides backend context.")
100+
(:name "line"
101+
:type integer
102+
:description "1-based line number.")
103+
(:name "column"
104+
:type integer
105+
:description "0-based column number.")))
70106
(:function ai-code-mcp-treesit-info
71107
:name "treesit_info"
72108
:description "Return tree-sitter node information for a file location."
@@ -183,6 +219,91 @@ Required keys are `:function', `:name', and `:description'."
183219
"No active buffer")
184220
file-count)))
185221

222+
(defun ai-code-mcp--validate-buffer-query-range (start-line num-lines)
223+
"Validate optional buffer query range arguments START-LINE and NUM-LINES."
224+
(when (or (and start-line (not num-lines))
225+
(and num-lines (not start-line)))
226+
(error "Arguments start_line and num_lines must both be provided or both be omitted"))
227+
(when (and start-line
228+
(or (< start-line 1)
229+
(< num-lines 1)))
230+
(error "Arguments start_line and num_lines must be positive integers")))
231+
232+
(defun ai-code-mcp--drop-trailing-newline (text)
233+
"Return TEXT without a single trailing newline."
234+
(if (string-suffix-p "\n" text)
235+
(substring text 0 -1)
236+
text))
237+
238+
(defun ai-code-mcp-buffer-query (buffer-name &optional start-line num-lines)
239+
"Return contents from BUFFER-NAME.
240+
When START-LINE and NUM-LINES are non-nil, return only that line range."
241+
(let ((buffer (get-buffer buffer-name)))
242+
(if (not buffer)
243+
(format "Error: Buffer not found: %s" buffer-name)
244+
(ai-code-mcp--validate-buffer-query-range start-line num-lines)
245+
(with-current-buffer buffer
246+
(save-excursion
247+
(if (not start-line)
248+
(buffer-substring-no-properties (point-min) (point-max))
249+
(goto-char (point-min))
250+
(forward-line (1- start-line))
251+
(let ((start-pos (point)))
252+
(forward-line num-lines)
253+
(ai-code-mcp--drop-trailing-newline
254+
(buffer-substring-no-properties start-pos (point))))))))))
255+
256+
(defun ai-code-mcp--project-files (project-dir)
257+
"Return absolute regular files inside PROJECT-DIR."
258+
(let* ((default-directory (file-name-as-directory project-dir))
259+
(project (project-current nil project-dir))
260+
(project-root default-directory))
261+
(or (ignore-errors
262+
(when (and project (fboundp 'project-files))
263+
(seq-filter
264+
#'file-regular-p
265+
(mapcar (lambda (file)
266+
(if (file-name-absolute-p file)
267+
file
268+
(expand-file-name file project-root)))
269+
(project-files project)))))
270+
(cl-labels
271+
((collect-files (dir)
272+
(apply
273+
#'append
274+
(mapcar
275+
(lambda (entry)
276+
(cond
277+
((member entry '("." "..")) nil)
278+
((string-prefix-p "." entry) nil)
279+
(t
280+
(let ((path (expand-file-name entry dir)))
281+
(cond
282+
((file-directory-p path)
283+
(collect-files path))
284+
((file-regular-p path)
285+
(list path))
286+
(t nil))))))
287+
(directory-files dir nil nil t)))))
288+
(collect-files project-root)))))
289+
290+
(defun ai-code-mcp-get-project-files ()
291+
"Return regular files in the current project as relative paths."
292+
(let ((project-dir (ai-code-mcp--project-directory)))
293+
(if (not (and project-dir (file-directory-p project-dir)))
294+
nil
295+
(mapcar #'ai-code-mcp--display-path
296+
(ai-code-mcp--project-files project-dir)))))
297+
298+
(defun ai-code-mcp-get-project-buffers ()
299+
"Return open buffers that belong to the current project."
300+
(let ((project-dir (ai-code-mcp--project-directory)))
301+
(delq nil
302+
(mapcar
303+
(lambda (buffer)
304+
(ai-code-mcp--project-buffer-entry buffer project-dir))
305+
(buffer-list)))))
306+
186307
(defun ai-code-mcp-imenu-list-symbols (file-path)
187308
"Return formatted imenu entries for FILE-PATH."
188309
(let* ((resolved-file (ai-code-mcp--require-file-path file-path))
@@ -205,8 +326,29 @@ Required keys are `:function', `:name', and `:description'."
205326
(format "No references found for '%s'" identifier)
206327
(mapcar #'ai-code-mcp--format-xref-item items))))))))
207328

329+
(defun ai-code-mcp-xref-find-definitions-at-point (file-path line column)
330+
"Return formatted xref definitions for the identifier at FILE-PATH:LINE:COLUMN."
331+
(let ((buffer (ai-code-mcp--file-buffer
332+
(ai-code-mcp--require-file-path file-path))))
333+
(with-current-buffer buffer
334+
(save-excursion
335+
(goto-char (point-min))
336+
(forward-line (1- line))
337+
(move-to-column column)
338+
(let ((backend (xref-find-backend)))
339+
(if (not backend)
340+
(format "No xref backend available for %s" file-path)
341+
(let ((identifier (xref-backend-identifier-at-point backend)))
342+
(if (not identifier)
343+
(format "No identifier at %s:%d:%d" file-path line column)
344+
(let ((items (xref-backend-definitions backend identifier)))
345+
(if (not items)
346+
(format "No definitions found for '%s'" identifier)
347+
(mapcar #'ai-code-mcp--format-xref-item items)))))))))))
348+
208349
(defun ai-code-mcp-treesit-info (file-path &optional line column whole-file)
209-
"Return tree-sitter information for FILE-PATH at LINE and COLUMN."
350+
"Return tree-sitter information for FILE-PATH at LINE and COLUMN.
351+
When WHOLE-FILE is non-nil, inspect the root node instead."
210352
(cond
211353
((not (and (fboundp 'treesit-available-p)
212354
(treesit-available-p)))
@@ -332,9 +474,35 @@ Required keys are `:function', `:name', and `:description'."
332474
"Return RESULT converted to a tool response string."
333475
(cond
334476
((stringp result) result)
335-
((listp result) (mapconcat #'identity result "\n"))
477+
((listp result)
478+
(mapconcat (lambda (item)
479+
(if (stringp item)
480+
item
481+
(format "%S" item)))
482+
result
483+
"\n"))
336484
(t (format "%s" result))))
337485

486+
(defun ai-code-mcp--project-buffer-entry (buffer project-dir)
487+
"Return buffer metadata for BUFFER when it belongs to PROJECT-DIR."
488+
(when (ai-code-mcp--buffer-in-project-p buffer project-dir)
489+
(with-current-buffer buffer
490+
`((name . ,(buffer-name buffer))
491+
(mode . ,major-mode)
492+
(file . ,(buffer-file-name))
493+
(modified . ,(buffer-modified-p buffer))))))
494+
495+
(defun ai-code-mcp--buffer-in-project-p (buffer project-dir)
496+
"Return non-nil when BUFFER belongs to PROJECT-DIR."
497+
(and (file-directory-p project-dir)
498+
(with-current-buffer buffer
499+
(let ((file (buffer-file-name))
500+
(buffer-dir default-directory))
501+
(or (and file
502+
(file-in-directory-p file project-dir))
503+
(and buffer-dir
504+
(file-in-directory-p buffer-dir project-dir)))))))
505+
338506
(defun ai-code-mcp--project-directory ()
339507
"Return the best available project directory."
340508
(or (when-let ((context (ai-code-mcp-get-session-context)))
@@ -352,17 +520,21 @@ Required keys are `:function', `:name', and `:description'."
352520

353521
(defun ai-code-mcp--display-path (file-path)
354522
"Return FILE-PATH relative to the active project when possible."
355-
(let ((project-dir (ai-code-mcp--project-directory)))
356-
(if (and project-dir
357-
(string-prefix-p (expand-file-name project-dir)
358-
(expand-file-name file-path)))
359-
(file-relative-name file-path project-dir)
360-
(file-name-nondirectory file-path))))
523+
(let* ((expanded-path (and file-path (expand-file-name file-path)))
524+
(project-dir (ai-code-mcp--project-directory))
525+
(project-root (and project-dir
526+
(file-name-as-directory
527+
(expand-file-name project-dir)))))
528+
(if (and expanded-path
529+
project-root
530+
(file-in-directory-p expanded-path project-root))
531+
(file-relative-name expanded-path project-root)
532+
expanded-path)))
361533

362534
(defun ai-code-mcp--require-file-path (file-path)
363535
"Return FILE-PATH as an absolute path or signal an error."
364536
(unless file-path
365-
(error "file_path is required"))
537+
(error "Argument file_path is required"))
366538
(expand-file-name file-path))
367539

368540
(defun ai-code-mcp--file-buffer (file-path)
@@ -392,7 +564,8 @@ Required keys are `:function', `:name', and `:description'."
392564
(defun ai-code-mcp--format-xref-item (item)
393565
"Return a human-readable line for xref ITEM."
394566
(let* ((location (xref-item-location item))
395-
(group (xref-location-group location))
567+
(group (ai-code-mcp--display-path
568+
(xref-location-group location)))
396569
(marker (xref-location-marker location))
397570
(line (with-current-buffer (marker-buffer marker)
398571
(save-excursion

emacs_mcp_tools.png

43.8 KB
Loading

0 commit comments

Comments
 (0)