tidewave 0.4.2 → 0.5.0
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/README.md +10 -2
- data/lib/tidewave/configuration.rb +3 -2
- data/lib/tidewave/database_adapter.rb +2 -7
- data/lib/tidewave/database_adapters/active_record.rb +2 -8
- data/lib/tidewave/database_adapters/sequel.rb +14 -2
- data/lib/tidewave/railtie.rb +14 -29
- data/lib/tidewave/tool.rb +128 -0
- data/lib/tidewave/tools/execute_sql_query.rb +31 -14
- data/lib/tidewave/tools/get_docs.rb +26 -8
- data/lib/tidewave/tools/get_logs.rb +38 -10
- data/lib/tidewave/tools/get_models.rb +37 -18
- data/lib/tidewave/tools/get_source_location.rb +50 -37
- data/lib/tidewave/tools/project_eval.rb +43 -21
- data/lib/tidewave/version.rb +2 -2
- data/lib/tidewave.rb +312 -5
- metadata +3 -33
- data/lib/tidewave/middleware.rb +0 -127
- data/lib/tidewave/streamable_http_transport.rb +0 -135
- data/lib/tidewave/tools/base.rb +0 -8
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
tool_name "get_source_location"
|
|
3
|
+
require "pathname"
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
class Tidewave::Tools::GetSourceLocation < Tidewave::Tool
|
|
6
|
+
DESCRIPTION = <<~DESCRIPTION.freeze
|
|
7
7
|
Returns the source location for the given reference.
|
|
8
8
|
|
|
9
9
|
The reference may be a constant, most commonly classes and modules
|
|
@@ -17,55 +17,50 @@ class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
|
|
|
17
17
|
You may also get the root location of a gem, you can use "dep:PACKAGE_NAME".
|
|
18
18
|
DESCRIPTION
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
def initialize(options = {})
|
|
21
|
+
@root = options[:root] ? Pathname.new(options[:root].to_s) : Pathname.pwd
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
end
|
|
41
|
-
else
|
|
42
|
-
raise NameError, "could not find source location for #{reference}"
|
|
43
|
-
end
|
|
24
|
+
def definition
|
|
25
|
+
{
|
|
26
|
+
"name" => "get_source_location",
|
|
27
|
+
"description" => DESCRIPTION,
|
|
28
|
+
"inputSchema" => {
|
|
29
|
+
"type" => "object",
|
|
30
|
+
"properties" => {
|
|
31
|
+
"reference" => {
|
|
32
|
+
"type" => "string",
|
|
33
|
+
"description" => "The constant/method to lookup, such String, String#gsub or File.executable?",
|
|
34
|
+
"minLength" => 1
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"required" => [ "reference" ]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
44
40
|
end
|
|
45
41
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
specs = Bundler.load.specs
|
|
42
|
+
def call(arguments)
|
|
43
|
+
reference = arguments.fetch("reference")
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
spec.full_gem_path
|
|
45
|
+
if reference.start_with?("dep:")
|
|
46
|
+
get_package_location(reference.delete_prefix("dep:"))
|
|
53
47
|
else
|
|
54
|
-
|
|
48
|
+
file_path, line_number = self.class.get_source_location(reference)
|
|
49
|
+
raise NameError, "could not find source location for #{reference}" unless file_path
|
|
50
|
+
|
|
51
|
+
format_source_location(file_path, line_number)
|
|
55
52
|
end
|
|
56
53
|
end
|
|
57
54
|
|
|
58
55
|
def self.get_source_location(reference)
|
|
59
56
|
constant_path, selector, method_name = reference.rpartition(/\.|#/)
|
|
60
|
-
|
|
61
|
-
# There are no selectors, so the method_name is a constant path
|
|
62
57
|
return Object.const_source_location(method_name) if selector.empty?
|
|
63
58
|
|
|
64
59
|
begin
|
|
65
60
|
mod = Object.const_get(constant_path)
|
|
66
|
-
rescue NameError =>
|
|
67
|
-
raise
|
|
68
|
-
rescue
|
|
61
|
+
rescue NameError => error
|
|
62
|
+
raise error
|
|
63
|
+
rescue StandardError
|
|
69
64
|
raise "wrong or invalid reference #{reference}"
|
|
70
65
|
end
|
|
71
66
|
|
|
@@ -77,4 +72,22 @@ class Tidewave::Tools::GetSourceLocation < Tidewave::Tools::Base
|
|
|
77
72
|
mod.method(method_name).source_location
|
|
78
73
|
end
|
|
79
74
|
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def format_source_location(file_path, line_number)
|
|
79
|
+
relative_path = Pathname.new(file_path).relative_path_from(@root)
|
|
80
|
+
"#{relative_path}:#{line_number}"
|
|
81
|
+
rescue ArgumentError
|
|
82
|
+
"#{file_path}:#{line_number}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_package_location(package)
|
|
86
|
+
raise "dep: prefix only works with projects using Bundler" unless defined?(Bundler)
|
|
87
|
+
|
|
88
|
+
spec = Bundler.load.specs.find { |loaded_spec| loaded_spec.name == package }
|
|
89
|
+
return spec.full_gem_path if spec
|
|
90
|
+
|
|
91
|
+
raise "Package #{package} not found. Check your Gemfile for available packages."
|
|
92
|
+
end
|
|
80
93
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "timeout"
|
|
4
3
|
require "json"
|
|
4
|
+
require "stringio"
|
|
5
|
+
require "timeout"
|
|
5
6
|
|
|
6
|
-
class Tidewave::Tools::ProjectEval < Tidewave::
|
|
7
|
-
|
|
8
|
-
description <<~DESCRIPTION
|
|
7
|
+
class Tidewave::Tools::ProjectEval < Tidewave::Tool
|
|
8
|
+
DESCRIPTION = <<~DESCRIPTION.freeze
|
|
9
9
|
Evaluates Ruby code in the context of the project.
|
|
10
10
|
|
|
11
11
|
The current Ruby version is: #{RUBY_VERSION}
|
|
@@ -16,20 +16,44 @@ class Tidewave::Tools::ProjectEval < Tidewave::Tools::Base
|
|
|
16
16
|
output. DO NOT use shell tools to evaluate Ruby code.
|
|
17
17
|
DESCRIPTION
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
DEFAULT_TIMEOUT = 30_000
|
|
20
|
+
|
|
21
|
+
def definition
|
|
22
|
+
{
|
|
23
|
+
"name" => "project_eval",
|
|
24
|
+
"description" => DESCRIPTION,
|
|
25
|
+
"inputSchema" => {
|
|
26
|
+
"type" => "object",
|
|
27
|
+
"properties" => {
|
|
28
|
+
"arguments" => {
|
|
29
|
+
"description" => "The arguments to pass to evaluation. They are available inside the evaluated code as `arguments`.",
|
|
30
|
+
"items" => {},
|
|
31
|
+
"type" => "array"
|
|
32
|
+
},
|
|
33
|
+
"code" => {
|
|
34
|
+
"description" => "The Ruby code to evaluate",
|
|
35
|
+
"type" => "string",
|
|
36
|
+
"minLength" => 1
|
|
37
|
+
},
|
|
38
|
+
"timeout" => {
|
|
39
|
+
"description" => "The timeout in milliseconds. If the evaluation takes longer than this, it will be terminated. Defaults to 30000 (30 seconds).",
|
|
40
|
+
"type" => "integer",
|
|
41
|
+
"not" => {
|
|
42
|
+
"type" => "null"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required" => [ "code" ]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
24
49
|
end
|
|
25
50
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
def call(arguments_hash)
|
|
52
|
+
code = arguments_hash.fetch("code")
|
|
53
|
+
arguments = arguments_hash.fetch("arguments", [])
|
|
54
|
+
timeout = arguments_hash.fetch("timeout", DEFAULT_TIMEOUT)
|
|
55
|
+
json = arguments_hash.fetch("json", false)
|
|
31
56
|
|
|
32
|
-
def call(code:, arguments: [], timeout: 30_000, json: false)
|
|
33
57
|
original_stdout = $stdout
|
|
34
58
|
original_stderr = $stderr
|
|
35
59
|
|
|
@@ -56,14 +80,12 @@ class Tidewave::Tools::ProjectEval < Tidewave::Tools::Base
|
|
|
56
80
|
|
|
57
81
|
if json
|
|
58
82
|
JSON.generate({
|
|
59
|
-
result
|
|
60
|
-
success
|
|
61
|
-
stdout
|
|
62
|
-
stderr
|
|
83
|
+
"result" => result,
|
|
84
|
+
"success" => success,
|
|
85
|
+
"stdout" => stdout,
|
|
86
|
+
"stderr" => stderr
|
|
63
87
|
})
|
|
64
88
|
elsif stdout.empty? && stderr.empty?
|
|
65
|
-
# We explicitly call to_s so the result is not accidentally
|
|
66
|
-
# parsed as a JSON response by FastMCP.
|
|
67
89
|
result.to_s
|
|
68
90
|
else
|
|
69
91
|
<<~OUTPUT
|
data/lib/tidewave/version.rb
CHANGED
data/lib/tidewave.rb
CHANGED
|
@@ -1,13 +1,320 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "json"
|
|
5
|
+
require "rack/request"
|
|
3
6
|
require "tidewave/version"
|
|
4
|
-
require "tidewave/
|
|
7
|
+
require "tidewave/tool"
|
|
5
8
|
require "tidewave/database_adapter"
|
|
9
|
+
require "tidewave/railtie" if defined?(Rails::Railtie)
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
module Tidewave
|
|
11
|
+
class Tidewave
|
|
9
12
|
module DatabaseAdapters
|
|
10
|
-
# This module is defined here to ensure it's available for autoloading
|
|
11
|
-
# Individual adapters are loaded on-demand in database_adapter.rb
|
|
13
|
+
# This module is defined here to ensure it's available for autoloading.
|
|
14
|
+
# Individual adapters are loaded on-demand in database_adapter.rb.
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Tools
|
|
18
|
+
# This module is defined here to ensure it's available for autoloading.
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
gem_tools_path = File.expand_path("tidewave/tools/**/*.rb", __dir__)
|
|
23
|
+
Dir[gem_tools_path].sort.each do |file|
|
|
24
|
+
require file
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Tidewave
|
|
28
|
+
TIDEWAVE_ROUTE = "tidewave".freeze
|
|
29
|
+
MCP_ROUTE = "mcp".freeze
|
|
30
|
+
CONFIG_ROUTE = "config".freeze
|
|
31
|
+
PROTOCOL_VERSION = "2025-03-26".freeze
|
|
32
|
+
|
|
33
|
+
INVALID_IP = <<~TEXT.freeze
|
|
34
|
+
For security reasons, Tidewave does not accept remote connections by default.
|
|
35
|
+
|
|
36
|
+
If you really want to allow remote connections, configure Tidewave with the `allow_remote_access: true` option
|
|
37
|
+
TEXT
|
|
38
|
+
|
|
39
|
+
INVALID_ORIGIN = "For security reasons, Tidewave does not accept requests with an origin header for this endpoint.".freeze
|
|
40
|
+
|
|
41
|
+
DEFAULT_OPTIONS = {
|
|
42
|
+
allow_remote_access: false,
|
|
43
|
+
client_url: "https://tidewave.ai",
|
|
44
|
+
framework_type: "rack",
|
|
45
|
+
team: {}
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
def initialize(app, options = {})
|
|
49
|
+
@app = app
|
|
50
|
+
@options = DEFAULT_OPTIONS.merge(options || {})
|
|
51
|
+
raise ArgumentError, "project_name is required" if @options[:project_name].to_s.empty?
|
|
52
|
+
|
|
53
|
+
@logger = @options[:logger]
|
|
54
|
+
@tools = build_tool_registry
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call(env)
|
|
58
|
+
request = Rack::Request.new(env)
|
|
59
|
+
path = request.path.split("/").reject(&:empty?)
|
|
60
|
+
|
|
61
|
+
if path[0] == TIDEWAVE_ROUTE
|
|
62
|
+
return forbidden(INVALID_IP) unless valid_client_ip?(request)
|
|
63
|
+
return forbidden(INVALID_ORIGIN) if request.get_header("HTTP_ORIGIN") && path != [ TIDEWAVE_ROUTE ]
|
|
64
|
+
|
|
65
|
+
case [ request.request_method, path ]
|
|
66
|
+
when [ "GET", [ TIDEWAVE_ROUTE ] ]
|
|
67
|
+
home_endpoint(request)
|
|
68
|
+
when [ "GET", [ TIDEWAVE_ROUTE, CONFIG_ROUTE ] ]
|
|
69
|
+
config_endpoint(request)
|
|
70
|
+
when [ "POST", [ TIDEWAVE_ROUTE, MCP_ROUTE ] ]
|
|
71
|
+
mcp_endpoint(request)
|
|
72
|
+
else
|
|
73
|
+
not_found
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
strip_x_frame_options(@app.call(env))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def strip_x_frame_options(response)
|
|
83
|
+
status, headers, body = response
|
|
84
|
+
headers.delete("X-Frame-Options")
|
|
85
|
+
[ status, headers, body ]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def home_endpoint(_request)
|
|
89
|
+
client_url = @options[:client_url].to_s.sub(%r{/\z}, "")
|
|
90
|
+
body = <<~HTML
|
|
91
|
+
<!DOCTYPE html>
|
|
92
|
+
<html>
|
|
93
|
+
<head>
|
|
94
|
+
<meta charset="UTF-8" />
|
|
95
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
96
|
+
<script type="module" src="#{client_url}/tc/tc.js"></script>
|
|
97
|
+
</head>
|
|
98
|
+
<body></body>
|
|
99
|
+
</html>
|
|
100
|
+
HTML
|
|
101
|
+
|
|
102
|
+
[ 200, response_headers("text/html", body), [ body ] ]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def config_endpoint(_request)
|
|
106
|
+
json_response(config_data)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def mcp_endpoint(request)
|
|
110
|
+
body = request.body.read
|
|
111
|
+
|
|
112
|
+
message = JSON.parse(body)
|
|
113
|
+
validation_error = validate_jsonrpc_message(message)
|
|
114
|
+
return jsonrpc_error_response(nil, -32600, validation_error) if validation_error
|
|
115
|
+
|
|
116
|
+
response = handle_mcp_message(message)
|
|
117
|
+
return json_response({ "status" => "ok" }, status: 202) if response.nil?
|
|
118
|
+
|
|
119
|
+
json_response(response)
|
|
120
|
+
rescue JSON::ParserError
|
|
121
|
+
jsonrpc_error_response(nil, -32700, "Parse error")
|
|
122
|
+
rescue StandardError => error
|
|
123
|
+
@logger&.error("Error handling MCP request: #{error.message}")
|
|
124
|
+
jsonrpc_error_response(nil, -32603, "Internal error")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def config_data
|
|
128
|
+
{
|
|
129
|
+
"project_name" => @options[:project_name],
|
|
130
|
+
"framework_type" => @options[:framework_type],
|
|
131
|
+
"orm_adapter" => @options[:orm_adapter],
|
|
132
|
+
"team" => @options[:team] || {},
|
|
133
|
+
"tidewave_version" => VERSION
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def json_response(payload, status: 200)
|
|
138
|
+
body = JSON.generate(payload)
|
|
139
|
+
[ status, response_headers("application/json", body), [ body ] ]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def forbidden(message)
|
|
143
|
+
@logger&.warn(message)
|
|
144
|
+
text_response(403, message)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def not_found
|
|
148
|
+
text_response(404, "Not Found")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def text_response(status, message)
|
|
152
|
+
[ status, response_headers("text/plain; charset=utf-8", message), [ message ] ]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def response_headers(content_type, body)
|
|
156
|
+
{
|
|
157
|
+
"Content-Type" => content_type,
|
|
158
|
+
"Content-Length" => body.bytesize.to_s
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def valid_client_ip?(request)
|
|
163
|
+
return true if @options[:allow_remote_access]
|
|
164
|
+
|
|
165
|
+
ip = request.get_header("REMOTE_ADDR")
|
|
166
|
+
return false if ip.nil? || ip.empty?
|
|
167
|
+
|
|
168
|
+
address = IPAddr.new(ip)
|
|
169
|
+
address.loopback? || address == IPAddr.new("::ffff:127.0.0.1")
|
|
170
|
+
rescue IPAddr::InvalidAddressError
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def validate_jsonrpc_message(message)
|
|
175
|
+
return "Message must be a JSON object" unless message.is_a?(Hash)
|
|
176
|
+
return "Invalid JSON-RPC version" unless message["jsonrpc"] == "2.0"
|
|
177
|
+
|
|
178
|
+
has_id = message.key?("id")
|
|
179
|
+
has_method = message.key?("method")
|
|
180
|
+
has_result = message.key?("result")
|
|
181
|
+
|
|
182
|
+
return nil if has_method
|
|
183
|
+
return nil if has_id && has_result
|
|
184
|
+
|
|
185
|
+
"Invalid JSON-RPC message structure"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def handle_mcp_message(message)
|
|
189
|
+
method = message["method"]
|
|
190
|
+
request_id = message["id"]
|
|
191
|
+
params = message["params"].is_a?(Hash) ? message["params"] : {}
|
|
192
|
+
|
|
193
|
+
case method
|
|
194
|
+
when "notifications/initialized", "notifications/cancelled"
|
|
195
|
+
nil
|
|
196
|
+
when "ping"
|
|
197
|
+
jsonrpc_success_response_body(request_id, {})
|
|
198
|
+
when "initialize"
|
|
199
|
+
handle_initialize(request_id, params)
|
|
200
|
+
when "tools/list"
|
|
201
|
+
jsonrpc_success_response_body(request_id, { "tools" => tool_definitions })
|
|
202
|
+
when "tools/call"
|
|
203
|
+
handle_tool_call(request_id, params)
|
|
204
|
+
else
|
|
205
|
+
{
|
|
206
|
+
"jsonrpc" => "2.0",
|
|
207
|
+
"id" => request_id,
|
|
208
|
+
"error" => {
|
|
209
|
+
"code" => -32601,
|
|
210
|
+
"message" => "Method not found",
|
|
211
|
+
"data" => { "name" => method }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def handle_initialize(request_id, params)
|
|
218
|
+
client_version = params["protocolVersion"]
|
|
219
|
+
return jsonrpc_error_response_body(request_id, -32602, "Protocol version is required") if client_version.nil? || client_version.empty?
|
|
220
|
+
|
|
221
|
+
if client_version < PROTOCOL_VERSION
|
|
222
|
+
return jsonrpc_error_response_body(
|
|
223
|
+
request_id,
|
|
224
|
+
-32602,
|
|
225
|
+
"Unsupported protocol version. Server supports #{PROTOCOL_VERSION} or later"
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
jsonrpc_success_response_body(request_id, {
|
|
230
|
+
"protocolVersion" => PROTOCOL_VERSION,
|
|
231
|
+
"capabilities" => { "tools" => { "listChanged" => false } },
|
|
232
|
+
"serverInfo" => {
|
|
233
|
+
"name" => "tidewave",
|
|
234
|
+
"version" => VERSION
|
|
235
|
+
},
|
|
236
|
+
"tools" => tool_definitions
|
|
237
|
+
})
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def handle_tool_call(request_id, params)
|
|
241
|
+
tool_name = params["name"]
|
|
242
|
+
arguments = params["arguments"].is_a?(Hash) ? params["arguments"] : {}
|
|
243
|
+
|
|
244
|
+
return jsonrpc_error_response_body(request_id, -32602, "Tool name is required") if tool_name.nil? || tool_name.empty?
|
|
245
|
+
|
|
246
|
+
tool = @tools[tool_name]
|
|
247
|
+
return jsonrpc_error_response_body(request_id, -32601, "Tool '#{tool_name}' not found") if tool.nil?
|
|
248
|
+
|
|
249
|
+
result = tool.validate_and_call(arguments)
|
|
250
|
+
jsonrpc_success_response_body(request_id, tool_result(result))
|
|
251
|
+
rescue StandardError => error
|
|
252
|
+
@logger&.error("Tool execution error: #{error.message}")
|
|
253
|
+
jsonrpc_success_response_body(request_id, tool_error_result("Tool execution failed: #{error.message}"))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def jsonrpc_success_response_body(request_id, result)
|
|
257
|
+
{
|
|
258
|
+
"jsonrpc" => "2.0",
|
|
259
|
+
"id" => request_id,
|
|
260
|
+
"result" => result
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def jsonrpc_error_response(request_id, code, message)
|
|
265
|
+
json_response(jsonrpc_error_response_body(request_id, code, message))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def jsonrpc_error_response_body(request_id, code, message)
|
|
269
|
+
{
|
|
270
|
+
"jsonrpc" => "2.0",
|
|
271
|
+
"id" => request_id,
|
|
272
|
+
"error" => {
|
|
273
|
+
"code" => code,
|
|
274
|
+
"message" => message
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def tool_definitions
|
|
280
|
+
@tools.values.map(&:definition)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def tool_error_result(message)
|
|
284
|
+
{
|
|
285
|
+
"content" => [ text_content(message) ],
|
|
286
|
+
"isError" => true
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def tool_result(result)
|
|
291
|
+
if result.is_a?(Hash)
|
|
292
|
+
{
|
|
293
|
+
"content" => [ text_content(JSON.generate(result)) ],
|
|
294
|
+
"structuredContent" => result
|
|
295
|
+
}
|
|
296
|
+
else
|
|
297
|
+
{
|
|
298
|
+
"content" => [ text_content(result.to_s) ]
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def text_content(text)
|
|
304
|
+
{
|
|
305
|
+
"type" => "text",
|
|
306
|
+
"text" => text
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def build_tool_registry
|
|
311
|
+
Tidewave::Tool.descendants.each_with_object({}) do |tool_class, registry|
|
|
312
|
+
tool = tool_class.new(@options)
|
|
313
|
+
definition = tool.definition
|
|
314
|
+
next if definition.nil?
|
|
315
|
+
|
|
316
|
+
name = definition["name"]
|
|
317
|
+
registry[name] = tool if name
|
|
318
|
+
end
|
|
12
319
|
end
|
|
13
320
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tidewave
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yorick Jacquin
|
|
@@ -9,36 +9,8 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-05-29 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
|
-
- !ruby/object:Gem::Dependency
|
|
15
|
-
name: rails
|
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
|
17
|
-
requirements:
|
|
18
|
-
- - ">="
|
|
19
|
-
- !ruby/object:Gem::Version
|
|
20
|
-
version: 7.1.0
|
|
21
|
-
type: :runtime
|
|
22
|
-
prerelease: false
|
|
23
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
-
requirements:
|
|
25
|
-
- - ">="
|
|
26
|
-
- !ruby/object:Gem::Version
|
|
27
|
-
version: 7.1.0
|
|
28
|
-
- !ruby/object:Gem::Dependency
|
|
29
|
-
name: fast-mcp
|
|
30
|
-
requirement: !ruby/object:Gem::Requirement
|
|
31
|
-
requirements:
|
|
32
|
-
- - "~>"
|
|
33
|
-
- !ruby/object:Gem::Version
|
|
34
|
-
version: 1.6.0
|
|
35
|
-
type: :runtime
|
|
36
|
-
prerelease: false
|
|
37
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
-
requirements:
|
|
39
|
-
- - "~>"
|
|
40
|
-
- !ruby/object:Gem::Version
|
|
41
|
-
version: 1.6.0
|
|
42
14
|
- !ruby/object:Gem::Dependency
|
|
43
15
|
name: rack
|
|
44
16
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -69,11 +41,9 @@ files:
|
|
|
69
41
|
- lib/tidewave/database_adapters/active_record.rb
|
|
70
42
|
- lib/tidewave/database_adapters/sequel.rb
|
|
71
43
|
- lib/tidewave/exceptions_middleware.rb
|
|
72
|
-
- lib/tidewave/middleware.rb
|
|
73
44
|
- lib/tidewave/quiet_requests_middleware.rb
|
|
74
45
|
- lib/tidewave/railtie.rb
|
|
75
|
-
- lib/tidewave/
|
|
76
|
-
- lib/tidewave/tools/base.rb
|
|
46
|
+
- lib/tidewave/tool.rb
|
|
77
47
|
- lib/tidewave/tools/execute_sql_query.rb
|
|
78
48
|
- lib/tidewave/tools/get_docs.rb
|
|
79
49
|
- lib/tidewave/tools/get_logs.rb
|