Repository: tidewave-ai/tidewave_phoenix (elixir)

Issue:
Consider merging list_project_files into glob_project_files
The glob is an optional argument.

Pull request:
merge glob_project_files into list_project_files
Closes #33 

Patch:
diff --git a/lib/tidewave/mcp/git_ls.ex b/lib/tidewave/mcp/git_ls.ex
index 87e931a..1f97fca 100644
--- a/lib/tidewave/mcp/git_ls.ex
+++ b/lib/tidewave/mcp/git_ls.ex
@@ -3,8 +3,8 @@ defmodule Tidewave.MCP.GitLS do
 
   alias Tidewave.MCP
 
-  def list_files do
-    execute_git(&list_files/1)
+  def list_files(glob_pattern \\ nil) do
+    execute_git(fn git_dir -> list_files(git_dir, glob_pattern) end)
   end
 
   def detect_line_endings do
@@ -32,9 +32,10 @@ defmodule Tidewave.MCP.GitLS do
     end
   end
 
-  defp list_files(git_dir) do
+  defp list_files(git_dir, glob_pattern) do
     args = if git_dir, do: ["--git-dir", git_dir], else: []
-    args = args ++ ["ls-files", "--cached", "--others", "--exclude-standard"]
+    args = args ++ ["ls-files", "--cached", "--others"]
+    args = if glob_pattern, do: args ++ [glob_pattern], else: args ++ ["--exclude-standard"]
 
     with {result, 0} <- System.cmd("git", args, cd: MCP.root()) do
       {:ok, String.split(result, "\n", trim: true)}
diff --git a/lib/tidewave/mcp/tools/fs.ex b/lib/tidewave/mcp/tools/fs.ex
index 8020104..e9054e5 100644
--- a/lib/tidewave/mcp/tools/fs.ex
+++ b/lib/tidewave/mcp/tools/fs.ex
@@ -12,11 +12,23 @@ defmodule Tidewave.MCP.Tools.FS do
       %{
         name: "list_project_files",
         description: """
-        Returns a list of all files in the project that are not ignored by .gitignore.
+        Returns a list of files in the project.
+
+        By default, when no arguments are passed, it returns all files in the project that
+        are not ignored by .gitignore.
+
+        Optionally, a glob_pattern can be passed to filter this list. When a pattern is passed,
+        the gitignore check will be skipped.
         """,
         inputSchema: %{
           type: "object",
-          properties: %{},
+          properties: %{
+            glob_pattern: %{
+              type: "string",
+              description:
+                "Optional: a glob pattern to filter the listed files. If a pattern is passed, the .gitignore check will be skipped."
+            }
+          },
           required: []
         },
         callback: &list_project_files/1,
@@ -114,22 +126,6 @@ defmodule Tidewave.MCP.Tools.FS do
         callback: &edit_project_file/2,
         listable: &listable/1
       },
-      %{
-        name: "glob_project_files",
-        description: "Searches for files matching the given glob pattern.",
-        inputSchema: %{
-          type: "object",
-          required: ["pattern"],
-          properties: %{
-            pattern: %{
-              type: "string",
-              description: "The glob pattern to match files against, e.g., \"**/*.ex\""
-            }
-          }
-        },
-        callback: &glob_project_files/1,
-        listable: &listable/1
-      },
       %{
         name: "grep_project_files",
         description:
@@ -167,9 +163,25 @@ defmodule Tidewave.MCP.Tools.FS do
     not is_nil(connect_params["include_fs_tools"])
   end
 
-  def list_project_files(_args) do
-    with {:ok, files} <- GitLS.list_files() do
-      {:ok, Enum.join(files, "\n")}
+  def list_project_files(args) do
+    case args do
+      %{"glob_pattern" => glob_pattern} ->
+        git_ls_files(glob_pattern)
+
+      _ ->
+        git_ls_files(nil)
+    end
+  end
+
+  defp git_ls_files(glob_pattern) do
+    with {:ok, files} <- GitLS.list_files(glob_pattern) do
+      case files do
+        [] ->
+          {:ok, "No files found."}
+
+        files ->
+          {:ok, Enum.join(files, "\n")}
+      end
     end
   end
 
@@ -373,34 +385,6 @@ defmodule Tidewave.MCP.Tools.FS do
   defp take_all_or(list, nil), do: list
   defp take_all_or(list, count), do: Enum.take(list, count)
 
-  @doc """
-  Searches for files matching a glob pattern.
-
-  ## Arguments
-  * `pattern` - The glob pattern to match files against
-  """
-  def glob_project_files(args) do
-    case args do
-      %{"pattern" => pattern} ->
-        matches =
-          Path.wildcard(pattern)
-          |> Enum.reject(&File.dir?/1)
-          |> Enum.map(fn path ->
-            rel_path = Path.relative_to(path, File.cwd!())
-
-            %{
-              "path" => rel_path,
-              "name" => Path.basename(path)
-            }
-          end)
-
-        {:ok, Jason.encode!(matches)}
-
-      _ ->
-        {:error, :invalid_arguments}
-    end
-  end
-
   @doc """
   Searches for text patterns in files.
   Uses ripgrep if available, falling back to grep.
diff --git a/lib/tidewave/mcp/tools/source.ex b/lib/tidewave/mcp/tools/source.ex
index f8aef87..3d1d652 100644
--- a/lib/tidewave/mcp/tools/source.ex
+++ b/lib/tidewave/mcp/tools/source.ex
@@ -140,13 +140,13 @@ defmodule Tidewave.MCP.Tools.Source do
 
     case result do
       {_source_file, _module_pair, {fun_file, fun_line}} ->
-        {:ok, "#{fun_file}:#{fun_line}"}
+        {:ok, "#{Path.relative_to(fun_file, MCP.root())}:#{fun_line}"}
 
       {_source_file, {module_file, module_line}, nil} ->
-        {:ok, "#{module_file}:#{module_line}"}
+        {:ok, "#{Path.relative_to(module_file, MCP.root())}:#{module_line}"}
 
       {source_file, nil, nil} ->
-        {:ok, source_file}
+        {:ok, Path.relative_to(source_file, MCP.root())}
 
       {:error, error} ->
         {:error, "Failed to get source location: #{inspect(error)}"}


Test patch:
diff --git a/test/mcp/tools/fs_sync_test.exs b/test/mcp/tools/fs_sync_test.exs
index 956fed0..8c2c187 100644
--- a/test/mcp/tools/fs_sync_test.exs
+++ b/test/mcp/tools/fs_sync_test.exs
@@ -90,7 +90,7 @@ defmodule Tidewave.MCP.Tools.FSSyncTest do
     end
   end
 
-  describe "glob/1" do
+  describe "list_project_files/1 with glob pattern" do
     @describetag :tmp_dir
 
     setup %{tmp_dir: tmp_dir} do
@@ -109,18 +109,27 @@ defmodule Tidewave.MCP.Tools.FSSyncTest do
     end
 
     test "finds files matching pattern" do
-      assert {:ok, text} = FS.glob_project_files(%{"pattern" => "*.txt"})
+      assert {:ok, text} = FS.list_project_files(%{"glob_pattern" => "*.txt"})
 
       assert text =~ "file1.txt"
       refute text =~ "file2.md"
     end
 
     test "finds files with wildcard pattern" do
-      assert {:ok, text} = FS.glob_project_files(%{"pattern" => "**/*.ex"})
+      assert {:ok, text} = FS.list_project_files(%{"glob_pattern" => "*.ex"})
 
       assert text =~ "file3.ex"
       assert text =~ "file4.ex"
       refute text =~ "file1.txt"
     end
+
+    test "ignores .gitignore when passing a glob_pattern" do
+      File.write!(".gitignore", "*.txt")
+
+      assert {:ok, text} =
+               FS.list_project_files(%{"glob_pattern" => "*.txt"})
+
+      assert text =~ "file1.txt"
+    end
   end
 end
diff --git a/test/mcp/tools/fs_test.exs b/test/mcp/tools/fs_test.exs
index f6c8234..fc24cad 100644
--- a/test/mcp/tools/fs_test.exs
+++ b/test/mcp/tools/fs_test.exs
@@ -11,7 +11,6 @@ defmodule Tidewave.MCP.Tools.FSTest do
       assert Enum.any?(tools, &(&1.name == "list_project_files"))
       assert Enum.any?(tools, &(&1.name == "read_project_file"))
       assert Enum.any?(tools, &(&1.name == "write_project_file"))
-      assert Enum.any?(tools, &(&1.name == "glob_project_files"))
 
       # grep tool might be available depending on the system
       has_grep = System.find_executable("rg") != nil || System.find_executable("grep") != nil