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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +2 -0
- data/lib/shared_tools/mcp/playwright_client.rb +34 -0
- data/lib/shared_tools/tools/calculator_tool.rb +11 -1
- data/lib/shared_tools/tools/database_query_tool.rb +11 -1
- data/lib/shared_tools/tools/eval/shell_eval_tool.rb +26 -5
- data/lib/shared_tools/tools/search_codebase_tool.rb +91 -0
- data/lib/shared_tools/tools/weather_tool.rb +11 -1
- data/lib/shared_tools/version.rb +1 -1
- metadata +21 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 679fe640e745e716a1ec2c9cb77fd9af47b67b09ddd5caba23ef9b6fbbf50d24
|
|
4
|
+
data.tar.gz: afa308531e5afc205b244771db0ed2e8965e5532d5f642d81c3284819db6f7b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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,
|
data/lib/shared_tools/version.rb
CHANGED
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.
|
|
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: :
|
|
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: :
|
|
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: :
|
|
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.
|
|
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: []
|