Skip to content

Commit 09975c3

Browse files
committed
addressing feedbacks
1 parent e3ebf63 commit 09975c3

2 files changed

Lines changed: 183 additions & 16 deletions

File tree

ai-code-mcp-server.el

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
(require 'cl-lib)
1515
(require 'imenu)
1616
(require 'project)
17+
(require 'seq)
1718
(require 'subr-x)
1819
(require 'xref)
1920

@@ -218,15 +219,29 @@ Required keys are `:function', `:name', and `:description'."
218219
"No active buffer")
219220
file-count)))
220221

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+
221238
(defun ai-code-mcp-buffer-query (buffer-name &optional start-line num-lines)
222239
"Return contents from BUFFER-NAME.
223240
When START-LINE and NUM-LINES are non-nil, return only that line range."
224241
(let ((buffer (get-buffer buffer-name)))
225242
(if (not buffer)
226243
(format "Error: Buffer not found: %s" buffer-name)
227-
(when (or (and start-line (not num-lines))
228-
(and num-lines (not start-line)))
229-
(error "Arguments start_line and num_lines must both be provided or both be omitted"))
244+
(ai-code-mcp--validate-buffer-query-range start-line num-lines)
230245
(with-current-buffer buffer
231246
(save-excursion
232247
(if (not start-line)
@@ -235,18 +250,50 @@ When START-LINE and NUM-LINES are non-nil, return only that line range."
235250
(forward-line (1- start-line))
236251
(let ((start-pos (point)))
237252
(forward-line num-lines)
238-
(string-trim-right
253+
(ai-code-mcp--drop-trailing-newline
239254
(buffer-substring-no-properties start-pos (point))))))))))
240255

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+
241290
(defun ai-code-mcp-get-project-files ()
242291
"Return regular files in the current project as relative paths."
243292
(let ((project-dir (ai-code-mcp--project-directory)))
244293
(if (not (and project-dir (file-directory-p project-dir)))
245294
nil
246295
(mapcar #'ai-code-mcp--display-path
247-
(seq-filter
248-
#'file-regular-p
249-
(directory-files-recursively project-dir ".*" t))))))
296+
(ai-code-mcp--project-files project-dir)))))
250297

251298
(defun ai-code-mcp-get-project-buffers ()
252299
"Return open buffers that belong to the current project."
@@ -473,12 +520,16 @@ When WHOLE-FILE is non-nil, inspect the root node instead."
473520

474521
(defun ai-code-mcp--display-path (file-path)
475522
"Return FILE-PATH relative to the active project when possible."
476-
(let ((project-dir (ai-code-mcp--project-directory)))
477-
(if (and project-dir
478-
(string-prefix-p (expand-file-name project-dir)
479-
(expand-file-name file-path)))
480-
(file-relative-name file-path project-dir)
481-
(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)))
482533

483534
(defun ai-code-mcp--require-file-path (file-path)
484535
"Return FILE-PATH as an absolute path or signal an error."

test/test_ai-code-mcp-server.el

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@
228228
(should (member "sample.el:4: beta" result))))
229229
(delete-directory project-dir t))))
230230

231+
(ert-deftest ai-code-test-mcp-server-source-requires-seq-explicitly ()
232+
"The MCP server source should declare its seq dependency explicitly."
233+
(with-temp-buffer
234+
(insert-file-contents "ai-code-mcp-server.el")
235+
(goto-char (point-min))
236+
(should (search-forward "(require 'seq)" nil t))))
237+
231238
(ert-deftest ai-code-test-mcp-buffer-query-returns-selected-buffer-lines ()
232239
"Buffer query should return the requested line range from a live buffer."
233240
(let ((buffer (generate-new-buffer " *ai-code-mcp-buffer-query*")))
@@ -242,6 +249,33 @@
242249
(when (buffer-live-p buffer)
243250
(kill-buffer buffer)))))
244251

252+
(ert-deftest ai-code-test-mcp-buffer-query-preserves-trailing-whitespace ()
253+
"Buffer query should preserve trailing whitespace in the selected text."
254+
(let ((buffer (generate-new-buffer " *ai-code-mcp-buffer-query-whitespace*")))
255+
(unwind-protect
256+
(with-current-buffer buffer
257+
(insert "alpha\nbeta \n")
258+
(should (equal "beta "
259+
(ai-code-mcp-buffer-query
260+
(buffer-name buffer)
261+
2
262+
1))))
263+
(when (buffer-live-p buffer)
264+
(kill-buffer buffer)))))
265+
266+
(ert-deftest ai-code-test-mcp-buffer-query-requires-positive-line-range ()
267+
"Buffer query should reject non-positive line range arguments."
268+
(let ((buffer (generate-new-buffer " *ai-code-mcp-buffer-query-range*")))
269+
(unwind-protect
270+
(with-current-buffer buffer
271+
(insert "alpha\nbeta\n")
272+
(should-error
273+
(ai-code-mcp-buffer-query (buffer-name buffer) 0 1))
274+
(should-error
275+
(ai-code-mcp-buffer-query (buffer-name buffer) 1 0)))
276+
(when (buffer-live-p buffer)
277+
(kill-buffer buffer)))))
278+
245279
(ert-deftest ai-code-test-mcp-get-project-files-returns-relative-project-paths ()
246280
"Project files should list regular files relative to the session project root."
247281
(let* ((project-dir (make-temp-file "ai-code-mcp-project-files-" t))
@@ -264,6 +298,35 @@
264298
(kill-buffer buffer))
265299
(delete-directory project-dir t))))
266300

301+
(ert-deftest ai-code-test-mcp-get-project-files-skips-hidden-directories ()
302+
"Project files should skip hidden directories such as .git."
303+
(let* ((project-dir (make-temp-file "ai-code-mcp-project-files-hidden-" t))
304+
(file-a (expand-file-name "alpha.el" project-dir))
305+
(file-b (expand-file-name "nested/beta.el" project-dir))
306+
(hidden-file (expand-file-name ".git/HEAD" project-dir))
307+
(buffer (generate-new-buffer " *ai-code-mcp-project-files-hidden*"))
308+
(ai-code-mcp--sessions (make-hash-table :test 'equal))
309+
(ai-code-mcp--current-session-id "session-project-files-hidden"))
310+
(unwind-protect
311+
(progn
312+
(make-directory (file-name-directory file-b) t)
313+
(make-directory (file-name-directory hidden-file) t)
314+
(with-temp-file file-a
315+
(insert "(message \"alpha\")\n"))
316+
(with-temp-file file-b
317+
(insert "(message \"beta\")\n"))
318+
(with-temp-file hidden-file
319+
(insert "ref: refs/heads/main\n"))
320+
(ai-code-mcp-register-session
321+
"session-project-files-hidden"
322+
project-dir
323+
buffer)
324+
(should (equal '("alpha.el" "nested/beta.el")
325+
(sort (ai-code-mcp-get-project-files) #'string<))))
326+
(when (buffer-live-p buffer)
327+
(kill-buffer buffer))
328+
(delete-directory project-dir t))))
329+
267330
(ert-deftest ai-code-test-mcp-get-project-buffers-lists-open-buffers-in-project ()
268331
"Project buffers should include file-visiting buffers under the active project."
269332
(let* ((project-dir (make-temp-file "ai-code-mcp-project-buffers-" t))
@@ -309,7 +372,7 @@
309372
"Definitions-at-point should resolve via the xref backend at a file location."
310373
(let* ((project-dir (make-temp-file "ai-code-mcp-xref-defs-" t))
311374
(file-path (expand-file-name "defs.el" project-dir))
312-
(buffer (generate-new-buffer " *ai-code-mcp-xref-defs*")))
375+
visited-buffer)
313376
(unwind-protect
314377
(progn
315378
(with-temp-file file-path
@@ -330,10 +393,63 @@
330393
(ai-code-mcp-xref-find-definitions-at-point
331394
file-path
332395
2
333-
3)))))
396+
3))))
397+
(setq visited-buffer (find-buffer-visiting file-path)))
398+
(when (buffer-live-p visited-buffer)
399+
(kill-buffer visited-buffer))
400+
(delete-directory project-dir t))))
401+
402+
(ert-deftest ai-code-test-mcp-display-path-keeps-external-sibling-absolute ()
403+
"Display path should keep sibling paths outside the project absolute."
404+
(let* ((project-dir (make-temp-file "ai-code-mcp-display-path-" t))
405+
(sibling-dir (concat project-dir "-sibling"))
406+
(external-file (expand-file-name "other.el" sibling-dir))
407+
(buffer (generate-new-buffer " *ai-code-mcp-display-path*"))
408+
(ai-code-mcp--sessions (make-hash-table :test 'equal))
409+
(ai-code-mcp--current-session-id "session-display-path"))
410+
(unwind-protect
411+
(progn
412+
(make-directory sibling-dir t)
413+
(with-temp-file external-file
414+
(insert "(message \"other\")\n"))
415+
(ai-code-mcp-register-session "session-display-path" project-dir buffer)
416+
(should (equal (expand-file-name external-file)
417+
(ai-code-mcp--display-path external-file))))
334418
(when (buffer-live-p buffer)
335419
(kill-buffer buffer))
336-
(delete-directory project-dir t))))
420+
(let ((visited-buffer (find-buffer-visiting external-file)))
421+
(when (buffer-live-p visited-buffer)
422+
(kill-buffer visited-buffer)))
423+
(delete-directory project-dir t)
424+
(delete-directory sibling-dir t))))
425+
426+
(ert-deftest ai-code-test-mcp-format-xref-item-preserves-external-absolute-path ()
427+
"Xref items outside the project should keep their absolute file path."
428+
(let* ((project-dir (make-temp-file "ai-code-mcp-xref-project-" t))
429+
(external-dir (make-temp-file "ai-code-mcp-xref-external-" t))
430+
(external-file (expand-file-name "index.el" external-dir))
431+
(buffer (generate-new-buffer " *ai-code-mcp-xref-format*"))
432+
(ai-code-mcp--sessions (make-hash-table :test 'equal))
433+
(ai-code-mcp--current-session-id "session-xref-format"))
434+
(unwind-protect
435+
(progn
436+
(with-temp-file external-file
437+
(insert "(message \"external\")\n"))
438+
(ai-code-mcp-register-session "session-xref-format" project-dir buffer)
439+
(should (equal
440+
(format "%s:1: external summary"
441+
(expand-file-name external-file))
442+
(ai-code-mcp--format-xref-item
443+
(xref-make
444+
"external summary"
445+
(xref-make-file-location external-file 1 0))))))
446+
(when (buffer-live-p buffer)
447+
(kill-buffer buffer))
448+
(let ((visited-buffer (find-buffer-visiting external-file)))
449+
(when (buffer-live-p visited-buffer)
450+
(kill-buffer visited-buffer)))
451+
(delete-directory project-dir t)
452+
(delete-directory external-dir t))))
337453

338454
(provide 'test_ai-code-mcp-server)
339455

0 commit comments

Comments
 (0)