tidewave 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54751fab877a22eeffd2bd5179bebb1d566b92edf284fb34ffb2499a7ab04d15
4
- data.tar.gz: d00206e13b21d3e52e9331cf740cdca113fe574095d6a45444861c3f5c5ce140
3
+ metadata.gz: 6d4f421f00436367f21d189e0fe49ea8a1ce4f611457b720f62027b4b8f080dc
4
+ data.tar.gz: 6bb95b976f34b5520b54fb8a0789d1adf18c591cef285bcd5500f591b2c4d1a3
5
5
  SHA512:
6
- metadata.gz: faff1feb577d5f467f58dbed4b3be691bdf160e991f30406d9812ec275575d448452460ffb0219603294a4021a7f7075bbcded1c5eef3c7bf68693e30a9ca107
7
- data.tar.gz: 987ac660e700738721fbd0e919f3d191b8a1a13af23c55a11b958f956aee638ec18326647a63b50099c5ada75885bf6ddcaba0f86577602c9437e074c5bc8857
6
+ metadata.gz: 851460abeef4e3d29d0d475a83f1e962bb0a6279708e25cc25ed2b8620782e48f837f8db279fac1da822af3c1d5472208be2340ca691ef5a226ab7bd7f4292f9
7
+ data.tar.gz: 48f5e731497bf822329c2de6865361c0d31bfe5fa64afed0c213d75d8de4db08ed4f44aa7ef76e889803330c70ba202712f724d569650d2707dccd3debb74d11
data/README.md CHANGED
@@ -29,6 +29,24 @@ Tidewave will now run on the same port as your regular Rails application.
29
29
  In particular, the MCP is located by default at http://localhost:3000/tidewave/mcp.
30
30
  [You must configure your editor and AI assistants accordingly](https://hexdocs.pm/tidewave/mcp.html).
31
31
 
32
+ ## Configuration
33
+
34
+ You may configure the `tidewave` using the following syntax:
35
+
36
+ ```ruby
37
+ config.tidewave.allowed_ips = [IPAddr.new("192.168.97.1")]
38
+ ```
39
+
40
+ The following options are available:
41
+
42
+ * `:allowed_origins`
43
+
44
+ * `:localhost_only`
45
+
46
+ * `:allowed_ips`
47
+
48
+ You can read more about this options in [FastMCP](https://github.com/yjacquin/fast_mcp) README.
49
+
32
50
  ## Considerations
33
51
 
34
52
  ### Production Environment
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tidewave
4
+ class Configuration
5
+ attr_accessor :logger, :allowed_origins, :localhost_only, :allowed_ips
6
+
7
+ def initialize
8
+ @logger = Logger.new(STDOUT)
9
+ @allowed_origins = nil
10
+ @localhost_only = true
11
+ @allowed_ips = nil
12
+ end
13
+ end
14
+ end
@@ -4,38 +4,40 @@ module Tidewave
4
4
  module FileTracker
5
5
  extend self
6
6
 
7
- def project_files
8
- `git --git-dir #{git_root}/.git ls-files --cached --others --exclude-standard`.split("\n")
7
+ def project_files(glob_pattern: nil)
8
+ args = %w[--git-dir] + [ "#{git_root}/.git", "ls-files", "--cached", "--others" ]
9
+ args += glob_pattern ? [ glob_pattern ] : [ "--exclude-standard" ]
10
+ `git #{args.join(" ")}`.split("\n")
9
11
  end
10
12
 
11
- def read_file(path)
12
- validate_path_access!(path)
13
-
14
- # Retrieve the full path
13
+ def read_file(path, line_offset: 0, count: nil)
15
14
  full_path = file_full_path(path)
15
+ # Explicitly read the mtime first to avoid race conditions
16
+ mtime = File.mtime(full_path).to_i
17
+ content = File.read(full_path)
16
18
 
17
- # Record the file as read
18
- record_read(path)
19
+ if line_offset > 0 || count
20
+ lines = content.lines
21
+ start_idx = [ line_offset, 0 ].max
22
+ count = (count || lines.length)
23
+ selected_lines = lines[start_idx, count]
24
+ content = selected_lines ? selected_lines.join : ""
25
+ end
19
26
 
20
- # Read and return the file contents
21
- File.read(full_path)
27
+ [ mtime, content ]
22
28
  end
23
29
 
24
30
  def write_file(path, content)
25
- validate_path_access!(path, validate_existence: false)
26
- # Retrieve the full path
31
+ validate_ruby_syntax!(content) if ruby_file?(path)
27
32
  full_path = file_full_path(path)
28
33
 
29
- dirname = File.dirname(full_path)
30
-
31
34
  # Create the directory if it doesn't exist
35
+ dirname = File.dirname(full_path)
32
36
  FileUtils.mkdir_p(dirname)
33
37
 
34
- # Write the file contents
38
+ # Write and return the file contents
35
39
  File.write(full_path, content)
36
-
37
- # Read and return the file contents
38
- read_file(path)
40
+ content
39
41
  end
40
42
 
41
43
  def file_full_path(path)
@@ -47,7 +49,7 @@ module Tidewave
47
49
  end
48
50
 
49
51
  def validate_path_access!(path, validate_existence: true)
50
- raise ArgumentError, "File path must not start with '..'" if path.start_with?("..")
52
+ raise ArgumentError, "File path must not contain '..'" if path.include?("..")
51
53
 
52
54
  # Ensure the path is within the project
53
55
  full_path = file_full_path(path)
@@ -56,68 +58,44 @@ module Tidewave
56
58
  raise ArgumentError, "File path must be within the project directory" unless full_path.start_with?(git_root)
57
59
 
58
60
  # Verify the file exists
59
- raise ArgumentError, "File not found: #{path}" unless File.exist?(full_path) && validate_existence
61
+ raise ArgumentError, "File not found: #{path}" if validate_existence && !File.exist?(full_path)
60
62
 
61
63
  true
62
64
  end
63
65
 
64
- def validate_path_is_editable!(path)
66
+ def validate_path_is_editable!(path, atime)
65
67
  validate_path_access!(path)
66
- validate_path_has_been_read_since_last_write!(path)
68
+ validate_path_has_been_read_since_last_write!(path, atime)
67
69
 
68
70
  true
69
71
  end
70
72
 
71
- def validate_path_is_writable!(path)
73
+ def validate_path_is_writable!(path, atime)
72
74
  validate_path_access!(path, validate_existence: false)
73
- validate_path_has_been_read_since_last_write!(path)
75
+ validate_path_has_been_read_since_last_write!(path, atime)
74
76
 
75
77
  true
76
78
  end
77
79
 
78
- def validate_path_has_been_read_since_last_write!(path)
79
- raise ArgumentError, "File has been modified since last read, please read the file again" unless file_was_read_since_last_write?(path)
80
+ private
80
81
 
81
- true
82
+ def ruby_file?(path)
83
+ [ ".rb", ".rake", ".gemspec" ].include?(File.extname(path)) ||
84
+ [ "Gemfile" ].include?(File.basename(path))
82
85
  end
83
86
 
84
- # Record when a file was read
85
- def record_read(path)
86
- file_records[path] = Time.now
87
+ def validate_ruby_syntax!(content)
88
+ RubyVM::AbstractSyntaxTree.parse(content)
89
+ rescue SyntaxError => e
90
+ raise "Invalid Ruby syntax: #{e.message}"
87
91
  end
88
92
 
93
+ def validate_path_has_been_read_since_last_write!(path, atime)
94
+ if atime && File.mtime(file_full_path(path)).to_i > atime
95
+ raise ArgumentError, "File has been modified since last read, please read the file again"
96
+ end
89
97
 
90
- def file_was_read_since_last_write?(path)
91
- file_was_read?(path) && last_read_at(path) >= last_modified_at(path)
92
- end
93
-
94
- # Check if a file has been read
95
- def file_was_read?(path)
96
- file_records.key?(path)
97
- end
98
-
99
- # Check if a file exists
100
- def file_exists?(path)
101
- File.exist?(file_full_path(path))
102
- end
103
-
104
- # Get the timestamp when a file was last read
105
- def last_read_at(path)
106
- file_records[path]
107
- end
108
-
109
- def last_modified_at(path)
110
- File.mtime(file_full_path(path))
111
- end
112
-
113
- # Reset all tracked files (useful for testing)
114
- def reset
115
- @file_records = {}
116
- end
117
-
118
- # Hash mapping file paths to their read records
119
- def file_records
120
- @file_records ||= {}
98
+ true
121
99
  end
122
100
  end
123
101
  end
@@ -3,14 +3,22 @@
3
3
  require "fast_mcp"
4
4
  require "logger"
5
5
  require "fileutils"
6
- require "tidewave/tool_resolver"
6
+ require "tidewave/configuration"
7
+ require "active_support/core_ext/class"
8
+
9
+ gem_tools_path = File.expand_path("tools/**/*.rb", __dir__)
10
+ Dir[gem_tools_path].each { |f| require f }
7
11
 
8
12
  module Tidewave
9
13
  class Railtie < Rails::Railtie
14
+ config.tidewave = Tidewave::Configuration.new
15
+
10
16
  initializer "tidewave.setup_mcp" do |app|
11
17
  # Prevent MCP server from being mounted if Rails is not running in development mode
12
18
  raise "For security reasons, Tidewave is only supported in development mode" unless Rails.env.development?
13
19
 
20
+ config = app.config.tidewave
21
+
14
22
  # Set up MCP server with the host application
15
23
  FastMcp.mount_in_rails(
16
24
  app,
@@ -19,12 +27,21 @@ module Tidewave
19
27
  path_prefix: Tidewave::PATH_PREFIX,
20
28
  messages_route: Tidewave::MESSAGES_ROUTE,
21
29
  sse_route: Tidewave::SSE_ROUTE,
22
- logger: Logger.new(STDOUT)
30
+ logger: config.logger,
31
+ allowed_origins: config.allowed_origins,
32
+ localhost_only: config.localhost_only,
33
+ allowed_ips: config.allowed_ips
23
34
  ) do |server|
24
35
  app.config.before_initialize do
25
- # Register a custom middleware to register tools depending on `include_fs_tools` query parameter
26
- server.register_tools(*Tidewave::ToolResolver::ALL_TOOLS)
27
- app.middleware.use Tidewave::ToolResolver, server
36
+ server.filter_tools do |request, tools|
37
+ if request.params["include_fs_tools"] != "true"
38
+ tools.reject { |tool| tool.tags.include?(:file_system_tool) }
39
+ else
40
+ tools
41
+ end
42
+ end
43
+
44
+ server.register_tools(*Tidewave::Tools::Base.descendants)
28
45
  end
29
46
  end
30
47
  end
@@ -3,13 +3,6 @@
3
3
  module Tidewave
4
4
  module Tools
5
5
  class Base < FastMcp::Tool
6
- def self.file_system_tool
7
- @file_system_tool = true
8
- end
9
-
10
- def self.file_system_tool?
11
- @file_system_tool
12
- end
13
6
  end
14
7
  end
15
8
  end
@@ -2,10 +2,8 @@
2
2
 
3
3
  require "tidewave/file_tracker"
4
4
 
5
- # TODO: This tool is not yet implemented, it does not track the latest file read.
6
- # Also, it does not raise an error if the file contains the substring multiple times.
7
5
  class Tidewave::Tools::EditProjectFile < Tidewave::Tools::Base
8
- file_system_tool
6
+ tags :file_system_tool
9
7
 
10
8
  tool_name "edit_project_file"
11
9
  description <<~DESCRIPTION
@@ -28,11 +26,12 @@ class Tidewave::Tools::EditProjectFile < Tidewave::Tools::Base
28
26
  required(:path).filled(:string).description("The path to the file to edit. It is relative to the project root.")
29
27
  required(:old_string).filled(:string).description("The string to search for")
30
28
  required(:new_string).filled(:string).description("The string to replace the old_string with")
29
+ optional(:atime).filled(:integer).hidden.description("The Unix timestamp this file was last accessed. Not to be used.")
31
30
  end
32
31
 
33
- def call(path:, old_string:, new_string:)
32
+ def call(path:, old_string:, new_string:, atime: nil)
34
33
  # Check if the file exists within the project root and has been read
35
- Tidewave::FileTracker.validate_path_is_editable!(path)
34
+ Tidewave::FileTracker.validate_path_is_editable!(path, atime)
36
35
 
37
36
  old_content = Tidewave::FileTracker.read_file(path)
38
37
 
@@ -42,7 +41,7 @@ class Tidewave::Tools::EditProjectFile < Tidewave::Tools::Base
42
41
  raise ArgumentError, "old_string is not unique" if scan_result.size > 1
43
42
 
44
43
  new_content = old_content.sub(old_string, new_string)
45
-
46
44
  Tidewave::FileTracker.write_file(path, new_content)
45
+ "OK"
47
46
  end
48
47
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ class Tidewave::Tools::GetPackageLocation < Tidewave::Tools::Base
6
+ tool_name "get_package_location"
7
+
8
+ description <<~DESCRIPTION
9
+ Returns the location of dependency packages.
10
+ You can use this tool to get the location of any project dependency. Optionally,
11
+ a specific dependency name can be provided to only return the location of that dependency.
12
+ Use the result in combination with shell tools like grep to look for specific
13
+ code inside dependencies.
14
+ DESCRIPTION
15
+
16
+ arguments do
17
+ optional(:package).maybe(:string).description(
18
+ "The name of the package to get the location of. If not provided, the location of all packages will be returned."
19
+ )
20
+ end
21
+
22
+ def call(package: nil)
23
+ raise "get_package_location only works with projects using Bundler" unless defined?(Bundler)
24
+ specs = Bundler.load.specs
25
+
26
+ if package
27
+ spec = specs.find { |s| s.name == package }
28
+ if spec
29
+ spec.full_gem_path
30
+ else
31
+ raise "Package #{package} not found. Check your Gemfile for available packages."
32
+ end
33
+ else
34
+ # For all packages, return a formatted string with package names and locations
35
+ specs.map do |spec|
36
+ relative_path = Pathname.new(spec.full_gem_path).relative_path_from(Pathname.new(Dir.pwd))
37
+ "#{spec.name}: #{relative_path}"
38
+ end.join("\n")
39
+ end
40
+ end
41
+ end
@@ -1,58 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string/inflections"
4
- require "active_support/core_ext/object/blank"
5
-
6
3
  class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
7
4
  tool_name "get_source_location"
8
5
 
9
6
  description <<~DESCRIPTION
10
- Returns the source location for the given module (or function).
7
+ Returns the source location for the given reference.
8
+
9
+ The reference may be a constant, most commonly classes and modules
10
+ such as `String`, an instance method, such as `String#gsub`, or class
11
+ method, such as `File.executable?`
11
12
 
12
- This works for modules in the current project, as well as dependencies.
13
+ This works for methods in the current project, as well as dependencies.
13
14
 
14
- This tool only works if you know the specific module (and optionally function) that is being targeted.
15
+ This tool only works if you know the specific constant/method being targeted.
15
16
  If that is the case, prefer this tool over grepping the file system.
16
17
  DESCRIPTION
17
18
 
18
19
  arguments do
19
- required(:module_name).filled(:string).description("The module to get source location for. When this is the single argument passed, the entire module source is returned.")
20
- optional(:function_name).filled(:string).description("The function to get source location for. When used, a module must also be passed.")
20
+ required(:reference).filled(:string).description("The constant/method to lookup, such String, String#gsub or File.executable?")
21
21
  end
22
22
 
23
+ def call(reference:)
24
+ file_path, line_number = get_source_location(reference)
23
25
 
24
- def call(module_name:, function_name: nil)
25
- file_path, line_number = get_source_location(module_name, function_name)
26
-
26
+ if file_path
27
27
  {
28
28
  file_path: file_path,
29
29
  line_number: line_number
30
30
  }.to_json
31
+ else
32
+ raise NameError, "could not find source location for #{reference}"
33
+ end
31
34
  end
32
35
 
33
36
  private
34
37
 
35
- def get_source_location(module_name, function_name)
36
- begin
37
- module_ref = module_name.constantize
38
- rescue NameError
39
- raise NameError, "Module #{module_name} not found"
40
- end
38
+ def get_source_location(reference)
39
+ constant_path, selector, method_name = reference.rpartition(/\.|#/)
41
40
 
42
- return get_method_definition(module_ref, function_name) if function_name.present?
41
+ # There are no selectors, so the method_name is a constant path
42
+ return Object.const_source_location(method_name) if selector.empty?
43
43
 
44
- module_ref.const_source_location(module_name)
45
- end
44
+ begin
45
+ mod = Object.const_get(constant_path)
46
+ rescue NameError => e
47
+ raise e
48
+ rescue
49
+ raise "wrong or invalid reference #{reference}"
50
+ end
46
51
 
47
- def get_method_definition(module_ref, function_name)
48
- module_ref.method(function_name).source_location
49
- rescue NameError
50
- get_instance_method_definition(module_ref, function_name)
51
- end
52
+ raise "reference #{constant_path} does not point a class/module" unless mod.is_a?(Module)
52
53
 
53
- def get_instance_method_definition(module_ref, function_name)
54
- module_ref.instance_method(function_name).source_location
55
- rescue NameError
56
- raise NameError, "Method #{function_name} not found in module #{module_ref.name}"
54
+ if selector == "#"
55
+ mod.instance_method(method_name).source_location
56
+ else
57
+ mod.method(method_name).source_location
58
+ end
57
59
  end
58
60
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Tidewave::Tools::GrepProjectFiles < Tidewave::Tools::Base
4
- file_system_tool
4
+ tags :file_system_tool
5
5
 
6
6
  def self.ripgrep_executable
7
7
  @ripgrep_executable ||= `which rg`.strip
@@ -52,16 +52,18 @@ class Tidewave::Tools::GrepProjectFiles < Tidewave::Tools::Base
52
52
  end
53
53
 
54
54
  def execute_grep(pattern, glob, case_sensitive, max_results)
55
- files = Tidewave::Tools::GlobProjectFiles.new.call(pattern: glob)
55
+ glob = "**/*" if glob.blank?
56
+ files = Dir.glob(glob, base: Tidewave::FileTracker.git_root)
56
57
  results = []
57
58
  files.each do |file|
58
- next unless File.file?(file)
59
+ full_path = File.join(Tidewave::FileTracker.git_root, file)
60
+ next unless File.file?(full_path)
59
61
 
60
62
  begin
61
63
  file_matches = 0
62
64
  line_number = 0
63
65
 
64
- File.foreach(file) do |line|
66
+ File.foreach(full_path) do |line|
65
67
  line_number += 1
66
68
 
67
69
  # Check if line matches pattern with proper case sensitivity
@@ -3,12 +3,24 @@
3
3
  require "tidewave/file_tracker"
4
4
 
5
5
  class Tidewave::Tools::ListProjectFiles < Tidewave::Tools::Base
6
- file_system_tool
6
+ tags :file_system_tool
7
7
 
8
8
  tool_name "list_project_files"
9
- description "Returns a list of all files in the project that are not ignored by .gitignore."
9
+ description <<~DESC
10
+ Returns a list of files in the project.
10
11
 
11
- def call
12
- Tidewave::FileTracker.project_files
12
+ By default, when no arguments are passed, it returns all files in the project that
13
+ are not ignored by .gitignore.
14
+
15
+ Optionally, a glob_pattern can be passed to filter this list. When a pattern is passed,
16
+ the gitignore check will be skipped.
17
+ DESC
18
+
19
+ arguments do
20
+ optional(:glob_pattern).maybe(:string).description("Optional: a glob pattern to filter the listed files. If a pattern is passed, the .gitignore check will be skipped.")
21
+ end
22
+
23
+ def call(glob_pattern: nil)
24
+ Tidewave::FileTracker.project_files(glob_pattern: glob_pattern)
13
25
  end
14
26
  end
@@ -28,7 +28,14 @@ class Tidewave::Tools::PackageSearch < Tidewave::Tools::Base
28
28
  response = Net::HTTP.get_response(uri)
29
29
 
30
30
  if response.is_a?(Net::HTTPSuccess)
31
- JSON.parse(response.body)
31
+ JSON.parse(response.body).map do |package|
32
+ {
33
+ name: package["name"],
34
+ version: package["version"],
35
+ downloads: package["downloads"],
36
+ documentation_uri: package["documentation_uri"]
37
+ }
38
+ end
32
39
  else
33
40
  raise "RubyGems API request failed with status code: #{response.code}"
34
41
  end
@@ -3,18 +3,23 @@
3
3
  require "tidewave/file_tracker"
4
4
 
5
5
  class Tidewave::Tools::ReadProjectFile < Tidewave::Tools::Base
6
- file_system_tool
6
+ tags :file_system_tool
7
7
 
8
8
  tool_name "read_project_file"
9
9
  description <<~DESCRIPTION
10
- Returns the contents of the given file. Matches the `resources/read` MCP method.
10
+ Returns the contents of the given file.
11
+ Supports an optional line_offset and count. To read the full file, only the path needs to be passed.
11
12
  DESCRIPTION
12
13
 
13
14
  arguments do
14
15
  required(:path).filled(:string).description("The path to the file to read. It is relative to the project root.")
16
+ optional(:line_offset).filled(:integer).description("Optional: the starting line offset from which to read. Defaults to 0.")
17
+ optional(:count).filled(:integer).description("Optional: the number of lines to read. Defaults to all.")
15
18
  end
16
19
 
17
- def call(path:)
18
- Tidewave::FileTracker.read_file(path)
20
+ def call(path:, **keywords)
21
+ Tidewave::FileTracker.validate_path_access!(path)
22
+ _meta[:mtime], contents = Tidewave::FileTracker.read_file(path, **keywords)
23
+ contents
19
24
  end
20
25
  end
@@ -3,7 +3,7 @@
3
3
  require "open3"
4
4
 
5
5
  class Tidewave::Tools::ShellEval < Tidewave::Tools::Base
6
- file_system_tool
6
+ tags :file_system_tool
7
7
  class CommandFailedError < StandardError; end
8
8
 
9
9
  tool_name "shell_eval"
@@ -3,7 +3,7 @@
3
3
  require "tidewave/file_tracker"
4
4
 
5
5
  class Tidewave::Tools::WriteProjectFile < Tidewave::Tools::Base
6
- file_system_tool
6
+ tags :file_system_tool
7
7
 
8
8
  tool_name "write_project_file"
9
9
  description <<~DESCRIPTION
@@ -15,11 +15,14 @@ class Tidewave::Tools::WriteProjectFile < Tidewave::Tools::Base
15
15
  arguments do
16
16
  required(:path).filled(:string).description("The path to the file to write. It is relative to the project root.")
17
17
  required(:content).filled(:string).description("The content to write to the file")
18
+ optional(:atime).filled(:integer).hidden.description("The Unix timestamp this file was last accessed. Not to be used.")
18
19
  end
19
20
 
20
- def call(path:, content:)
21
- Tidewave::FileTracker.validate_path_is_writable!(path)
22
-
21
+ def call(path:, content:, atime: nil)
22
+ Tidewave::FileTracker.validate_path_is_writable!(path, atime)
23
23
  Tidewave::FileTracker.write_file(path, content)
24
+ _meta[:mtime] = Time.now.to_i
25
+
26
+ "OK"
24
27
  end
25
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tidewave
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tidewave
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yorick Jacquin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-01 00:00:00.000000000 Z
11
+ date: 2025-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 7.2.0
19
+ version: 7.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 7.2.0
26
+ version: 7.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: fast-mcp
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 1.3.0
33
+ version: 1.5.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 1.3.0
40
+ version: 1.5.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rack
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -63,16 +63,16 @@ files:
63
63
  - README.md
64
64
  - config/database.yml
65
65
  - lib/tidewave.rb
66
+ - lib/tidewave/configuration.rb
66
67
  - lib/tidewave/file_tracker.rb
67
68
  - lib/tidewave/railtie.rb
68
- - lib/tidewave/tool_resolver.rb
69
69
  - lib/tidewave/tools/base.rb
70
70
  - lib/tidewave/tools/edit_project_file.rb
71
71
  - lib/tidewave/tools/execute_sql_query.rb
72
72
  - lib/tidewave/tools/get_logs.rb
73
73
  - lib/tidewave/tools/get_models.rb
74
+ - lib/tidewave/tools/get_package_location.rb
74
75
  - lib/tidewave/tools/get_source_location.rb
75
- - lib/tidewave/tools/glob_project_files.rb
76
76
  - lib/tidewave/tools/grep_project_files.rb
77
77
  - lib/tidewave/tools/list_project_files.rb
78
78
  - lib/tidewave/tools/package_search.rb
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rack"
4
-
5
- gem_tools_path = File.expand_path("../../lib/tidewave/tools/**/*.rb", __dir__)
6
- Dir[gem_tools_path].each { |f| require f }
7
-
8
- module Tidewave
9
- class ToolResolver
10
- ALL_TOOLS = Tidewave::Tools::Base.descendants
11
- NON_FILE_SYSTEM_TOOLS = ALL_TOOLS.reject(&:file_system_tool?)
12
- MESSAGES_PATH = "/tidewave/messages".freeze
13
- TOOLS_LIST_METHOD = "tools/list".freeze
14
- INCLUDE_FS_TOOLS_PARAM = "include_fs_tools".freeze
15
-
16
- def initialize(app, server)
17
- @app = app
18
- @server = server
19
- end
20
-
21
- def call(env)
22
- request = Rack::Request.new(env)
23
- request_path = request.path
24
- request_body = extract_request_body(request)
25
- request_params = request.params
26
-
27
- # Override tools list response if requested
28
- return override_tools_list_response(env) if overriding_tools_list_request?(request_path, request_params, request_body)
29
-
30
- # Forward the request to the underlying app (RackTransport)
31
- @app.call(env)
32
- end
33
-
34
- private
35
-
36
- def extract_request_body(request)
37
- JSON.parse(request.body.read)
38
- rescue JSON::ParserError => e
39
- {}
40
- ensure
41
- request.body.rewind
42
- end
43
-
44
- # When we want to exclude file system tools, we need to handle the request differently to prevent from listing them
45
- def overriding_tools_list_request?(request_path, request_params, request_body)
46
- request_path == MESSAGES_PATH && request_body["method"] == TOOLS_LIST_METHOD && request_params[INCLUDE_FS_TOOLS_PARAM] != "true"
47
- end
48
-
49
- RESPONSE_HEADERS = { "Content-Type" => "application/json" }
50
-
51
- def override_tools_list_response(env)
52
- register_non_file_system_tools
53
- @app.call(env).tap { register_all_tools }
54
- end
55
-
56
- def register_non_file_system_tools
57
- reset_server_tools
58
- @server.register_tools(*NON_FILE_SYSTEM_TOOLS)
59
- end
60
-
61
- def register_all_tools
62
- reset_server_tools
63
- @server.register_tools(*ALL_TOOLS)
64
- end
65
-
66
- def reset_server_tools
67
- @server.instance_variable_set(:@tools, {})
68
- end
69
- end
70
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tidewave/file_tracker"
4
-
5
- class Tidewave::Tools::GlobProjectFiles < Tidewave::Tools::Base
6
- file_system_tool
7
-
8
- tool_name "glob_project_files"
9
- description "Searches for files matching the given glob pattern."
10
-
11
- arguments do
12
- required(:pattern).filled(:string).description('The glob pattern to match files against, e.g., \"**/*.ex\"')
13
- end
14
-
15
- def call(pattern:)
16
- Dir.glob(pattern, base: Tidewave::FileTracker.git_root)
17
- end
18
- end