shared_tools 0.4.1 → 0.4.4

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: 77d4fd4712e3a0eff81653da54bafa994746c6334780e95862379a943c3b5274
4
- data.tar.gz: fd504802d298371efe604e341e9a339fef42acecf2ff47148d70007efe901c44
3
+ metadata.gz: 679fe640e745e716a1ec2c9cb77fd9af47b67b09ddd5caba23ef9b6fbbf50d24
4
+ data.tar.gz: afa308531e5afc205b244771db0ed2e8965e5532d5f642d81c3284819db6f7b6
5
5
  SHA512:
6
- metadata.gz: f14ebee5d9ceb6fadfafa19af5bfc18c0ece33ba4d2d6894f2c4192e5ffe9abf41e8f3f4f8a109f2ab646b0ac076b9b3f530839af1dd4f979a7ba3c88b19cab0
7
- data.tar.gz: '0428dda46694b2f0eebb6112884a45f1d33e3a43dd23938598c1b4e3ca9163446b4033c8f1273c6f782047dc4d3c62d6d4b3ffdaff7173cacbfbe4f212a194dd'
6
+ metadata.gz: 87264d8b31d6b016e56ac361fa528a73071163f4dfaa4d562e877e20b7d011aa85a0de033212aa326fe9e8983940893d046237eab043c5ee6f61306dc126c21b
7
+ data.tar.gz: eee3b6364a669539ff7d1ffb290e1255416c0a20f32f68873a4d69061dcf41e5675a23b2ecb3fcacaf7714f6ad0b69d726cf3577642bceef611895574870d44c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### [0.4.4] - 2026-06-06
6
+
7
+ #### Tools
8
+ - Added `SearchCodebaseTool` (`tools/search_codebase_tool.rb`) — read-only codebase search using ripgrep (`rg`) with grep fallback; supports `path`, `extension`, and `max_results` parameters; returns matches with file path and line number; no authorization prompt required
9
+ - Enhanced `ShellEvalTool`: added optional `workdir` parameter to set the working directory for command execution; validates the directory exists before running
10
+ - Enhanced `ShellEvalTool`: output is now truncated at 5000 bytes (stdout) and 2000 bytes (stderr) to prevent oversized tool responses; truncated output is marked with `[truncated]`
11
+ - Moved `require "open3"` to the top of `ShellEvalTool` instead of inline inside `execute`
12
+
13
+ #### Tests
14
+ - Added `test/shared_tools/tools/search_codebase_tool_test.rb` — 16 tests covering search, extension filtering, `max_results` capping, truncation flag, error cases, and tool-key reporting
15
+ - Added 4 tests to `test/shared_tools/tools/eval/shell_eval_tool_test.rb` covering `workdir` execution, missing-directory error, stdout truncation, and non-truncation of short output
16
+
17
+ ### [0.4.2] - 2026-03-27
18
+
19
+ #### MCP Clients
20
+ - Added Playwright MCP client (`mcp/playwright_client.rb`) — browser automation via the official `@playwright/mcp` package (npx auto-download, no pre-installation required)
21
+ - Added `mcp/tool_schema_patch.rb` — strips `$schema` from MCP tool input schemas before API serialization; fixes `BadRequestError` from Claude's API when MCP servers annotate schemas with JSON Schema Draft 2020-12 meta-fields
22
+ - Added demo script: `examples/mcp/playwright_demo.rb`
23
+
3
24
  ## Released
4
25
 
5
26
  ### [0.4.1] - 2026-03-26
data/README.md CHANGED
@@ -437,6 +437,7 @@ SharedTools bundles [Model Context Protocol](https://modelcontextprotocol.io) cl
437
437
  | `require 'shared_tools/mcp/sequential_thinking_client'` | Chain-of-thought reasoning |
438
438
  | `require 'shared_tools/mcp/chart_client'` | Chart and visualisation generation |
439
439
  | `require 'shared_tools/mcp/brave_search_client'` | Web and news search (`BRAVE_API_KEY`) |
440
+ | `require 'shared_tools/mcp/playwright_client'` | Browser automation: navigate, click, fill, screenshot, extract |
440
441
 
441
442
  ```ruby
442
443
  # Load all available clients at once (skips any whose env vars are missing)
@@ -523,6 +524,7 @@ bundle exec ruby -I examples examples/doc_tool_demo.rb
523
524
  | `mcp/sequential_thinking_demo.rb` | Chain-of-thought reasoning |
524
525
  | `mcp/chart_demo.rb` | Chart generation |
525
526
  | `mcp/brave_search_demo.rb` | Brave web search |
527
+ | `mcp/playwright_demo.rb` | Browser automation via Playwright |
526
528
  | `notification_tool_demo.rb` | Desktop notifications, alerts, TTS |
527
529
  | `system_info_tool_demo.rb` | System info |
528
530
  | `weather_tool_demo.rb` | Weather + local forecast |
@@ -0,0 +1,34 @@
1
+ # shared_tools/mcp/playwright_client.rb
2
+ #
3
+ # Playwright MCP Server Client — npx auto-download (no pre-installation required)
4
+ #
5
+ # Provides browser automation via Playwright: navigate pages, click elements,
6
+ # fill forms, take screenshots, extract text/HTML, and interact with web apps.
7
+ #
8
+ # Uses the official @playwright/mcp package from the Playwright team.
9
+ #
10
+ # Prerequisites:
11
+ # - Node.js and npx (https://nodejs.org)
12
+ # The @playwright/mcp package is downloaded automatically on first use via `npx -y`.
13
+ #
14
+ # Configuration:
15
+ # No environment variables required.
16
+ #
17
+ # Usage:
18
+ # require 'shared_tools/mcp/playwright_client'
19
+ # client = RubyLLM::MCP.clients["playwright"]
20
+ # chat = RubyLLM.chat.with_tools(*client.tools)
21
+ #
22
+ # Compatible with ruby_llm-mcp >= 0.7.0
23
+
24
+ require "ruby_llm/mcp"
25
+
26
+ RubyLLM::MCP.add_client(
27
+ name: "playwright",
28
+ transport_type: :stdio,
29
+ config: {
30
+ command: "npx",
31
+ args: ["-y", "@playwright/mcp"],
32
+ env: {},
33
+ },
34
+ )
@@ -1,12 +1,22 @@
1
1
  # calculator_tool.rb - Safe mathematical calculator
2
2
  require 'ruby_llm/tool'
3
- require 'dentaku'
3
+
4
+ DENTAKU_AVAILABLE = begin
5
+ require 'dentaku'
6
+ true
7
+ rescue LoadError, Gem::LoadError
8
+ false
9
+ end
4
10
 
5
11
  module SharedTools
6
12
  module Tools
7
13
  class CalculatorTool < RubyLLM::Tool
8
14
  def self.name = 'calculator'
9
15
 
16
+ def available?
17
+ DENTAKU_AVAILABLE
18
+ end
19
+
10
20
  description <<~'DESCRIPTION'
11
21
  Perform advanced mathematical calculations with comprehensive error handling and validation.
12
22
  This tool supports basic arithmetic operations, parentheses, and common mathematical functions.
@@ -1,12 +1,22 @@
1
1
  # database_query_tool.rb - Safe database query execution
2
2
  require 'ruby_llm/tool'
3
- require 'sequel'
3
+
4
+ SEQUEL_AVAILABLE = begin
5
+ require 'sequel'
6
+ true
7
+ rescue LoadError, Gem::LoadError
8
+ false
9
+ end
4
10
 
5
11
  module SharedTools
6
12
  module Tools
7
13
  class DatabaseQueryTool < RubyLLM::Tool
8
14
  def self.name = 'database_query'
9
15
 
16
+ def available?
17
+ SEQUEL_AVAILABLE
18
+ end
19
+
10
20
  description <<~'DESCRIPTION'
11
21
  Execute safe, read-only database queries with automatic connection management and security controls.
12
22
  This tool is designed for secure data retrieval operations only, restricting access to SELECT statements
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
3
4
 
4
5
  module SharedTools
5
6
  module Tools
@@ -7,13 +8,18 @@ module SharedTools
7
8
  # @example
8
9
  # tool = SharedTools::Tools::Eval::ShellEvalTool.new
9
10
  # tool.execute(command: "ls -la")
11
+ # tool.execute(command: "ls -la", workdir: "/tmp")
10
12
  class ShellEvalTool < ::RubyLLM::Tool
13
+ STDOUT_MAX = 5000
14
+ STDERR_MAX = 2000
15
+
11
16
  def self.name = 'eval_shell'
12
17
 
13
18
  description "Execute a shell command safely and return the result."
14
19
 
15
20
  params do
16
21
  string :command, description: "The shell command to execute"
22
+ string :workdir, description: "Working directory for the command (default: current directory)", required: false
17
23
  end
18
24
 
19
25
  # @param logger [Logger] optional logger
@@ -22,9 +28,10 @@ module SharedTools
22
28
  end
23
29
 
24
30
  # @param command [String] shell command to execute
31
+ # @param workdir [String, nil] working directory for the command
25
32
  #
26
33
  # @return [Hash] execution result
27
- def execute(command:)
34
+ def execute(command:, workdir: nil)
28
35
  @logger.info("Requesting permission to execute command: '#{command}'")
29
36
 
30
37
  if command.strip.empty?
@@ -33,7 +40,12 @@ module SharedTools
33
40
  return { error: error_msg }
34
41
  end
35
42
 
36
- # Show user the command and ask for confirmation
43
+ if workdir
44
+ unless File.directory?(workdir)
45
+ return { error: "Working directory not found: #{workdir}" }
46
+ end
47
+ end
48
+
37
49
  allowed = SharedTools.execute?(tool: self.class.to_s, stuff: command)
38
50
 
39
51
  unless allowed
@@ -43,9 +55,11 @@ module SharedTools
43
55
 
44
56
  @logger.info("Executing command: '#{command}'")
45
57
 
46
- # Use Open3 for safer command execution with proper error handling
47
- require "open3"
48
- stdout, stderr, status = Open3.capture3(command)
58
+ opts = workdir ? { chdir: workdir } : {}
59
+ stdout, stderr, status = Open3.capture3(command, **opts)
60
+
61
+ stdout = truncate(stdout, STDOUT_MAX)
62
+ stderr = truncate(stderr, STDERR_MAX)
49
63
 
50
64
  if status.success?
51
65
  @logger.debug("Command execution completed successfully with #{stdout.bytesize} bytes of output")
@@ -58,6 +72,13 @@ module SharedTools
58
72
  @logger.error("Command execution failed: #{e.message}")
59
73
  { error: e.message }
60
74
  end
75
+
76
+ private
77
+
78
+ def truncate(str, max)
79
+ return str if str.length <= max
80
+ str.slice(0, max) + "\n[truncated]"
81
+ end
61
82
  end
62
83
  end
63
84
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ # Search for a term across files using ripgrep (rg) with grep as fallback.
8
+ # Read-only — no authorization prompt required.
9
+ #
10
+ # @example
11
+ # tool = SharedTools::Tools::SearchCodebaseTool.new
12
+ # tool.execute(term: "def execute")
13
+ # tool.execute(term: "RubyLLM", extension: "rb", max_results: 20)
14
+ class SearchCodebaseTool < ::RubyLLM::Tool
15
+ MAX_RESULTS_CAP = 500
16
+
17
+ def self.name = 'search_codebase'
18
+
19
+ description "Search for a term or pattern across files. Uses ripgrep (rg) when available, falls back to grep. Returns matching lines with file path and line number."
20
+
21
+ params do
22
+ string :term, description: "The search term or regex pattern to find"
23
+ string :path, description: "Directory to search in (default: current directory)", required: false
24
+ string :extension, description: "File extension to restrict the search to, without the dot (e.g. 'rb', 'js')", required: false
25
+ integer :max_results, description: "Maximum number of matching lines to return (default: 50, max: 500)", required: false
26
+ end
27
+
28
+ def initialize(logger: nil)
29
+ @logger = logger || RubyLLM.logger
30
+ end
31
+
32
+ def execute(term:, path: ".", extension: nil, max_results: 50)
33
+ @logger.info("SearchCodebaseTool#execute term=#{term.inspect} path=#{path} extension=#{extension.inspect}")
34
+
35
+ return { error: "Search term cannot be empty" } if term.strip.empty?
36
+
37
+ max_results = [[max_results.to_i, 1].max, MAX_RESULTS_CAP].min
38
+ search_path = File.expand_path(path)
39
+
40
+ return { error: "Path not found: #{path}" } unless File.exist?(search_path)
41
+
42
+ stdout, _stderr, _status = run_search(term: term, path: search_path, extension: extension)
43
+
44
+ all_lines = stdout.lines.map(&:chomp).reject(&:empty?)
45
+ matches = all_lines.first(max_results)
46
+ truncated = all_lines.size > max_results
47
+
48
+ {
49
+ matches: matches,
50
+ count: matches.size,
51
+ truncated: truncated,
52
+ tool: ripgrep_available? ? "rg" : "grep"
53
+ }
54
+ rescue => e
55
+ @logger.error("SearchCodebaseTool error: #{e.message}")
56
+ { error: e.message }
57
+ end
58
+
59
+ private
60
+
61
+ def run_search(term:, path:, extension:)
62
+ if ripgrep_available?
63
+ run_ripgrep(term: term, path: path, extension: extension)
64
+ else
65
+ run_grep(term: term, path: path, extension: extension)
66
+ end
67
+ end
68
+
69
+ def run_ripgrep(term:, path:, extension:)
70
+ args = ["rg", "--no-heading", "-n"]
71
+ args += ["-g", "*.#{extension}"] if extension
72
+ args << term
73
+ args << path
74
+ Open3.capture3(*args)
75
+ end
76
+
77
+ def run_grep(term:, path:, extension:)
78
+ args = ["grep", "-r", "-n"]
79
+ args += ["--include=*.#{extension}"] if extension
80
+ args << term
81
+ args << path
82
+ Open3.capture3(*args)
83
+ end
84
+
85
+ def ripgrep_available?
86
+ return @ripgrep_available unless @ripgrep_available.nil?
87
+ @ripgrep_available = system("which", "rg", out: File::NULL, err: File::NULL)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,12 +1,22 @@
1
1
  # weather_tool.rb - API integration example
2
2
  require 'ruby_llm/tool'
3
- require 'openweathermap'
3
+
4
+ OPENWEATHERMAP_AVAILABLE = begin
5
+ require 'openweathermap'
6
+ true
7
+ rescue LoadError, Gem::LoadError
8
+ false
9
+ end
4
10
 
5
11
  module SharedTools
6
12
  module Tools
7
13
  class WeatherTool < RubyLLM::Tool
8
14
  def self.name = 'weather_tool'
9
15
 
16
+ def available?
17
+ OPENWEATHERMAP_AVAILABLE
18
+ end
19
+
10
20
  description <<~'DESCRIPTION'
11
21
  Retrieve comprehensive current weather information for any city worldwide using the OpenWeatherMap API.
12
22
  This tool provides real-time weather data including temperature, atmospheric conditions, humidity,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SharedTools
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shared_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: bigdecimal
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '4.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '4.0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: dentaku
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -72,7 +86,7 @@ dependencies:
72
86
  - - ">="
73
87
  - !ruby/object:Gem::Version
74
88
  version: '0'
75
- type: :runtime
89
+ type: :development
76
90
  prerelease: false
77
91
  version_requirements: !ruby/object:Gem::Requirement
78
92
  requirements:
@@ -86,7 +100,7 @@ dependencies:
86
100
  - - ">="
87
101
  - !ruby/object:Gem::Version
88
102
  version: '0'
89
- type: :runtime
103
+ type: :development
90
104
  prerelease: false
91
105
  version_requirements: !ruby/object:Gem::Requirement
92
106
  requirements:
@@ -100,7 +114,7 @@ dependencies:
100
114
  - - ">="
101
115
  - !ruby/object:Gem::Version
102
116
  version: '0'
103
- type: :runtime
117
+ type: :development
104
118
  prerelease: false
105
119
  version_requirements: !ruby/object:Gem::Requirement
106
120
  requirements:
@@ -297,6 +311,7 @@ files:
297
311
  - lib/shared_tools/mcp/hugging_face_client.rb
298
312
  - lib/shared_tools/mcp/memory_client.rb
299
313
  - lib/shared_tools/mcp/notion_client.rb
314
+ - lib/shared_tools/mcp/playwright_client.rb
300
315
  - lib/shared_tools/mcp/sequential_thinking_client.rb
301
316
  - lib/shared_tools/mcp/slack_client.rb
302
317
  - lib/shared_tools/mcp/streamable_http_patch.rb
@@ -384,6 +399,7 @@ files:
384
399
  - lib/shared_tools/tools/notification/mac_driver.rb
385
400
  - lib/shared_tools/tools/notification/null_driver.rb
386
401
  - lib/shared_tools/tools/notification_tool.rb
402
+ - lib/shared_tools/tools/search_codebase_tool.rb
387
403
  - lib/shared_tools/tools/secure_tool_template.rb
388
404
  - lib/shared_tools/tools/system_info_tool.rb
389
405
  - lib/shared_tools/tools/version.rb
@@ -414,7 +430,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
414
430
  - !ruby/object:Gem::Version
415
431
  version: '0'
416
432
  requirements: []
417
- rubygems_version: 4.0.9
433
+ rubygems_version: 4.0.13
418
434
  specification_version: 4
419
435
  summary: Shared utilities and AI tools for Ruby applications with configurable logging
420
436
  test_files: []