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:
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
0 commit comments