tidewave 0.5.0 → 0.5.2
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 +105 -30
- data/lib/tidewave/version.rb +1 -1
- data/lib/tidewave.rb +87 -34
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c64d42ec2ee75f35b02997081d24e69fe547d8b9a20a230daeb1d1995a30cd0a
|
|
4
|
+
data.tar.gz: 74e1cc3e89cdea2b1d4701990ca3fbe3b4b3a5c828735bef5251c91a1d8c074a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2cf21787f51875cbec78b12b5800c7f03b8d516360fd960110f8717dc762f47e58f4f7a4b243c3f0120e254075f17495e86d6c3538d22dd8abe6ac046d3fc6d
|
|
7
|
+
data.tar.gz: 789f139be709d1bc9a0d55b8cb661b9212b3ce2b4fa546d2c644b3a676a6a2843687407e2efdc87c8a0785290c2aab1806693b07698cd71f9fb9e7f130714786
|
data/README.md
CHANGED
|
@@ -1,32 +1,119 @@
|
|
|
1
|
-
# Tidewave
|
|
1
|
+
# Tidewave Rails
|
|
2
2
|
|
|
3
|
-
Tidewave is
|
|
3
|
+
Tidewave Rails is an MCP server that provides runtime-level tools for developing Ruby on Rails apps using coding agents.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Your agent will be able to use this MCP server to talk to your running Rails app in development to:
|
|
6
|
+
|
|
7
|
+
- execute code in the context of the running app (like a Rails console for agents)
|
|
8
|
+
- read the app's live logs
|
|
9
|
+
- query your development database
|
|
10
|
+
- get source locations of classes and methods
|
|
11
|
+
- read documentation pinned to the exact gem versions your app depends on
|
|
12
|
+
|
|
13
|
+
This MCP server is an open-source component of [Tidewave](https://tidewave.ai), the agentic development environment for Rails and Phoenix.
|
|
14
|
+
|
|
15
|
+
You can use this project as a standalone MCP server or integrated with the [Tidewave product](https://tidewave.ai).
|
|
16
|
+
|
|
17
|
+
To use it as a standalone MCP server, follow the installation instructions below.
|
|
6
18
|
|
|
7
19
|
## Installation
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
### 1. Add the Tidewave gem to your app
|
|
22
|
+
|
|
23
|
+
You can add Tidewave Rails to your app by running:
|
|
10
24
|
|
|
11
25
|
```shell
|
|
12
26
|
bundle add tidewave --group development
|
|
13
27
|
```
|
|
14
28
|
|
|
15
|
-
or by
|
|
29
|
+
or by manually adding the `tidewave` gem to the development group in your Gemfile:
|
|
16
30
|
|
|
17
31
|
```ruby
|
|
18
32
|
gem "tidewave", group: :development
|
|
19
33
|
```
|
|
20
34
|
|
|
21
|
-
|
|
35
|
+
### 2. Add the Tidewave MCP to your agent/editor
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
Add the Tidewave MCP server to your editor or MCP client configuration as the type "http" (streamable), pointing to the `/tidewave/mcp` path and port your web application is running at. For example, `http://localhost:3000/tidewave/mcp`.
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
We also have specific instructions for:
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
- [Claude Code](https://tidewave.hexdocs.pm/mcp_claude_code.html)
|
|
42
|
+
- [Codex](https://tidewave.hexdocs.pm/mcp_codex.html)
|
|
43
|
+
- [Cursor](https://tidewave.hexdocs.pm/mcp_cursor.html)
|
|
44
|
+
- [Neovim](https://tidewave.hexdocs.pm/mcp_neovim.html)
|
|
45
|
+
- [OpenCode](https://tidewave.hexdocs.pm/mcp_opencode.html)
|
|
46
|
+
- [VS Code](https://tidewave.hexdocs.pm/mcp_vscode.html)
|
|
47
|
+
- [Zed](https://tidewave.hexdocs.pm/mcp_zed.html)
|
|
48
|
+
- [Others](https://tidewave.hexdocs.pm/mcp.html)
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
As with any other MCP server, your agent will call the tools exposed by the Tidewave MCP whenever it sees fit. But you can also prompt it to call them explicitly.
|
|
53
|
+
|
|
54
|
+
## Available MCP tools
|
|
55
|
+
|
|
56
|
+
### `project_eval`
|
|
57
|
+
|
|
58
|
+
Evaluates Ruby code in the context of your running application, with access to its runtime, loaded dependencies, and in-memory data, returning the result plus anything printed to standard output. It's like a Rails console for the agent.
|
|
59
|
+
|
|
60
|
+
[](https://asciinema.org/a/v0Bs9WtOUARzewC3)
|
|
61
|
+
|
|
62
|
+
Your agent can use it when it would rather run code than assume behavior, grounding its next step in what the running app actually does. For example, calling a method to see what comes back or reproducing a failing code path against live app state to debug it.
|
|
63
|
+
|
|
64
|
+
### `execute_sql_query`
|
|
65
|
+
|
|
66
|
+
Runs a SQL query against your app's development database and returns the rows to the agent.
|
|
67
|
+
|
|
68
|
+
[](https://asciinema.org/a/qJtkEDf2YAPqjBHI)
|
|
69
|
+
|
|
70
|
+
Your agent can use it to run any SQL against your development database. For example, ask it to insert some test records to see how a page looks with realistic data. Or, after a create action, the agent can verify whether the record was saved with the expected values.
|
|
71
|
+
|
|
72
|
+
### `get_docs`
|
|
73
|
+
|
|
74
|
+
Looks up the documentation for a class, method, or constant, reading from the exact gem versions locked in your app's Gemfile.lock.
|
|
75
|
+
|
|
76
|
+
[](https://asciinema.org/a/Aa0u915syzncFYH8)
|
|
77
|
+
|
|
78
|
+
Your agent can use it when it's unsure how a class or method works, so the code it generates is grounded in the docs for the exact versions of the gems your app uses, rather than training data that may be stale or a generic docs lookup that can't guarantee it matches the version your app depends on.
|
|
79
|
+
|
|
80
|
+
### `get_logs`
|
|
81
|
+
|
|
82
|
+
Returns output from your running server's log.
|
|
83
|
+
|
|
84
|
+
[](https://asciinema.org/a/1260413)
|
|
85
|
+
|
|
86
|
+
Your agent can use it to see what happened after a request. For example, reading the request log and backtrace when something misbehaves, or checking the log after an action to confirm the request came in with the expected params.
|
|
87
|
+
|
|
88
|
+
### `get_models`
|
|
89
|
+
|
|
90
|
+
Lists all of your app's models and where each one is defined, by file and line.
|
|
91
|
+
|
|
92
|
+
[](https://asciinema.org/a/qeELC7wEgMEI5T7n)
|
|
93
|
+
|
|
94
|
+
Your agent can use it to map the data domain and find where each model lives before opening files, rather than grepping around for class definitions.
|
|
95
|
+
|
|
96
|
+
### `get_source_location`
|
|
97
|
+
|
|
98
|
+
Returns the file and line where a class, module, or method is defined, across both your app and its dependencies.
|
|
99
|
+
|
|
100
|
+
[](https://asciinema.org/a/1260415)
|
|
101
|
+
|
|
102
|
+
Your agent can use it to jump straight to where a class or method is defined, by file and line, instead of grepping for it, including when the definition lives in a gem dependency.
|
|
103
|
+
|
|
104
|
+
Also, because it resolves the location from your running app instead of parsing source text, it handles metaprogramming, where a method is generated at runtime and does not appear as a literal `def` for grep to find.
|
|
105
|
+
|
|
106
|
+
> [!NOTE]
|
|
107
|
+
> #### Why no tools for routes, associations, etc?
|
|
108
|
+
>
|
|
109
|
+
> Tidewave does not include tools for listing your routes, associations, etc. because
|
|
110
|
+
> agents are better off reading their respective source files, which gives agents more
|
|
111
|
+
> context and enables them to perform any necessary edit without additional tool calls.
|
|
112
|
+
>
|
|
113
|
+
> Instead, Tidewave aims to fill in missing gaps, such as evaluating code inside your
|
|
114
|
+
> Rails app (without starting new instances) and finding source location, which can be
|
|
115
|
+
> tricky, even with grepping, due to metaprogramming and the different places Bundler
|
|
116
|
+
> can install your dependencies.
|
|
30
117
|
|
|
31
118
|
## Troubleshooting
|
|
32
119
|
|
|
@@ -74,29 +161,17 @@ The following config is available:
|
|
|
74
161
|
|
|
75
162
|
* `team` - set your Tidewave Team configuration, such as `config.tidewave.team = { id: "my-company" }`
|
|
76
163
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
- `execute_sql_query` - executes a SQL query within your application
|
|
80
|
-
database, useful for the agent to verify the result of an action
|
|
81
|
-
|
|
82
|
-
- `get_docs` - get the documentation for a given module/class/method.
|
|
83
|
-
It consults the exact versions used by the project, ensuring you always
|
|
84
|
-
get correct information
|
|
85
|
-
|
|
86
|
-
- `get_logs` - reads logs written by the server
|
|
87
|
-
|
|
88
|
-
- `get_models` - lists all modules in the application and their location
|
|
89
|
-
for quick discovery
|
|
164
|
+
## Acknowledgements
|
|
90
165
|
|
|
91
|
-
|
|
92
|
-
so an agent can directly read the source skipping search
|
|
166
|
+
A thank you to Yorick Jacquin for the initial version of this project.
|
|
93
167
|
|
|
94
|
-
|
|
95
|
-
access to your runtime, dependencies, and in-memory data
|
|
168
|
+
## Development
|
|
96
169
|
|
|
97
|
-
|
|
170
|
+
Run the Minitest suite with:
|
|
98
171
|
|
|
99
|
-
|
|
172
|
+
```shell
|
|
173
|
+
bundle exec ruby -Itest test/all_test.rb
|
|
174
|
+
```
|
|
100
175
|
|
|
101
176
|
## License
|
|
102
177
|
|
data/lib/tidewave/version.rb
CHANGED
data/lib/tidewave.rb
CHANGED
|
@@ -60,7 +60,9 @@ class Tidewave
|
|
|
60
60
|
|
|
61
61
|
if path[0] == TIDEWAVE_ROUTE
|
|
62
62
|
return forbidden(INVALID_IP) unless valid_client_ip?(request)
|
|
63
|
-
|
|
63
|
+
if request.get_header("HTTP_ORIGIN") && !origin_allowed_path?(path)
|
|
64
|
+
return forbidden(INVALID_ORIGIN)
|
|
65
|
+
end
|
|
64
66
|
|
|
65
67
|
case [ request.request_method, path ]
|
|
66
68
|
when [ "GET", [ TIDEWAVE_ROUTE ] ]
|
|
@@ -70,7 +72,9 @@ class Tidewave
|
|
|
70
72
|
when [ "POST", [ TIDEWAVE_ROUTE, MCP_ROUTE ] ]
|
|
71
73
|
mcp_endpoint(request)
|
|
72
74
|
else
|
|
73
|
-
|
|
75
|
+
# The MCP Streamable HTTP transport requires the MCP endpoint to answer
|
|
76
|
+
# non-POST methods with 405 (GET without SSE support, DELETE, etc.)
|
|
77
|
+
path == [ TIDEWAVE_ROUTE, MCP_ROUTE ] ? method_not_allowed() : not_found()
|
|
74
78
|
end
|
|
75
79
|
else
|
|
76
80
|
strip_x_frame_options(@app.call(env))
|
|
@@ -81,7 +85,7 @@ class Tidewave
|
|
|
81
85
|
|
|
82
86
|
def strip_x_frame_options(response)
|
|
83
87
|
status, headers, body = response
|
|
84
|
-
headers.delete("
|
|
88
|
+
headers.delete("x-frame-options")
|
|
85
89
|
[ status, headers, body ]
|
|
86
90
|
end
|
|
87
91
|
|
|
@@ -102,41 +106,61 @@ class Tidewave
|
|
|
102
106
|
[ 200, response_headers("text/html", body), [ body ] ]
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
def config_endpoint(
|
|
106
|
-
json_response(config_data)
|
|
109
|
+
def config_endpoint(request)
|
|
110
|
+
json_response(config_data(request), headers: { "access-control-allow-origin" => "*" })
|
|
107
111
|
end
|
|
108
112
|
|
|
109
113
|
def mcp_endpoint(request)
|
|
110
|
-
|
|
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?
|
|
114
|
+
message = JSON.parse(request.body.read)
|
|
118
115
|
|
|
119
|
-
|
|
116
|
+
if message.is_a?(Array)
|
|
117
|
+
handle_mcp_batch(message)
|
|
118
|
+
else
|
|
119
|
+
handle_mcp_single(message)
|
|
120
|
+
end
|
|
120
121
|
rescue JSON::ParserError
|
|
121
|
-
jsonrpc_error_response(nil, -32700, "Parse error")
|
|
122
|
+
jsonrpc_error_response(nil, -32700, "Parse error", status: 400)
|
|
122
123
|
rescue StandardError => error
|
|
123
124
|
@logger&.error("Error handling MCP request: #{error.message}")
|
|
124
125
|
jsonrpc_error_response(nil, -32603, "Internal error")
|
|
125
126
|
end
|
|
126
127
|
|
|
127
|
-
def
|
|
128
|
+
def handle_mcp_single(message)
|
|
129
|
+
validation_error = validate_jsonrpc_message(message)
|
|
130
|
+
return jsonrpc_error_response(nil, -32600, validation_error, status: 400) if validation_error
|
|
131
|
+
|
|
132
|
+
response = handle_mcp_message(message)
|
|
133
|
+
response.nil? ? accepted_response : json_response(response)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def handle_mcp_batch(messages)
|
|
137
|
+
return jsonrpc_error_response(nil, -32600, "Invalid Request", status: 400) if messages.empty?
|
|
138
|
+
|
|
139
|
+
responses = messages.map { |message| handle_mcp_batch_message(message) }.compact
|
|
140
|
+
responses.empty? ? accepted_response : json_response(responses)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def handle_mcp_batch_message(message)
|
|
144
|
+
validation_error = validate_jsonrpc_message(message)
|
|
145
|
+
return jsonrpc_error_response_body(nil, -32600, validation_error) if validation_error
|
|
146
|
+
|
|
147
|
+
handle_mcp_message(message)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def config_data(request)
|
|
128
151
|
{
|
|
129
152
|
"project_name" => @options[:project_name],
|
|
130
153
|
"framework_type" => @options[:framework_type],
|
|
131
154
|
"orm_adapter" => @options[:orm_adapter],
|
|
132
155
|
"team" => @options[:team] || {},
|
|
133
|
-
"tidewave_version" => VERSION
|
|
156
|
+
"tidewave_version" => VERSION,
|
|
157
|
+
"local_port" => local_port(request)
|
|
134
158
|
}
|
|
135
159
|
end
|
|
136
160
|
|
|
137
|
-
def json_response(payload, status: 200)
|
|
161
|
+
def json_response(payload, status: 200, headers: {})
|
|
138
162
|
body = JSON.generate(payload)
|
|
139
|
-
[ status, response_headers("application/json", body), [ body ] ]
|
|
163
|
+
[ status, response_headers("application/json", body).merge(headers), [ body ] ]
|
|
140
164
|
end
|
|
141
165
|
|
|
142
166
|
def forbidden(message)
|
|
@@ -148,17 +172,42 @@ class Tidewave
|
|
|
148
172
|
text_response(404, "Not Found")
|
|
149
173
|
end
|
|
150
174
|
|
|
175
|
+
def method_not_allowed
|
|
176
|
+
status, headers, body = text_response(405, "Method Not Allowed")
|
|
177
|
+
[ status, headers.merge("allow" => "POST"), body ]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def accepted_response
|
|
181
|
+
[ 202, { "content-length" => "0" }, [] ]
|
|
182
|
+
end
|
|
183
|
+
|
|
151
184
|
def text_response(status, message)
|
|
152
185
|
[ status, response_headers("text/plain; charset=utf-8", message), [ message ] ]
|
|
153
186
|
end
|
|
154
187
|
|
|
188
|
+
# Rack 3 requires response header keys to be lowercase. Capitalized keys break
|
|
189
|
+
# case-sensitive middleware such as Rack::Deflater, which strips "content-length"
|
|
190
|
+
# before gzipping; a surviving "Content-Length" leaves a stale (uncompressed)
|
|
191
|
+
# length on the compressed body and hangs spec-compliant HTTP clients.
|
|
155
192
|
def response_headers(content_type, body)
|
|
156
193
|
{
|
|
157
|
-
"
|
|
158
|
-
"
|
|
194
|
+
"content-type" => content_type,
|
|
195
|
+
"content-length" => body.bytesize.to_s
|
|
159
196
|
}
|
|
160
197
|
end
|
|
161
198
|
|
|
199
|
+
def origin_allowed_path?(path)
|
|
200
|
+
path == [ TIDEWAVE_ROUTE ] || path == [ TIDEWAVE_ROUTE, CONFIG_ROUTE ]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def local_port(request)
|
|
204
|
+
sock = request.env["puma.socket"]
|
|
205
|
+
return unless sock
|
|
206
|
+
|
|
207
|
+
addr = sock.respond_to?(:local_address) ? sock.local_address : sock.to_io.local_address
|
|
208
|
+
addr.ip? ? addr.ip_port : nil
|
|
209
|
+
end
|
|
210
|
+
|
|
162
211
|
def valid_client_ip?(request)
|
|
163
212
|
return true if @options[:allow_remote_access]
|
|
164
213
|
|
|
@@ -177,7 +226,7 @@ class Tidewave
|
|
|
177
226
|
|
|
178
227
|
has_id = message.key?("id")
|
|
179
228
|
has_method = message.key?("method")
|
|
180
|
-
has_result = message.key?("result")
|
|
229
|
+
has_result = message.key?("result") || message.key?("error")
|
|
181
230
|
|
|
182
231
|
return nil if has_method
|
|
183
232
|
return nil if has_id && has_result
|
|
@@ -185,14 +234,17 @@ class Tidewave
|
|
|
185
234
|
"Invalid JSON-RPC message structure"
|
|
186
235
|
end
|
|
187
236
|
|
|
237
|
+
# Returns the JSON-RPC response for a request, or nil for messages that
|
|
238
|
+
# must not be replied to (notifications and client-sent responses), which
|
|
239
|
+
# the transport acknowledges with 202 Accepted.
|
|
188
240
|
def handle_mcp_message(message)
|
|
241
|
+
return nil unless message.key?("method") && message.key?("id")
|
|
242
|
+
|
|
189
243
|
method = message["method"]
|
|
190
244
|
request_id = message["id"]
|
|
191
245
|
params = message["params"].is_a?(Hash) ? message["params"] : {}
|
|
192
246
|
|
|
193
247
|
case method
|
|
194
|
-
when "notifications/initialized", "notifications/cancelled"
|
|
195
|
-
nil
|
|
196
248
|
when "ping"
|
|
197
249
|
jsonrpc_success_response_body(request_id, {})
|
|
198
250
|
when "initialize"
|
|
@@ -201,6 +253,12 @@ class Tidewave
|
|
|
201
253
|
jsonrpc_success_response_body(request_id, { "tools" => tool_definitions })
|
|
202
254
|
when "tools/call"
|
|
203
255
|
handle_tool_call(request_id, params)
|
|
256
|
+
when "prompts/list"
|
|
257
|
+
jsonrpc_success_response_body(request_id, { "prompts" => [] })
|
|
258
|
+
when "resources/list"
|
|
259
|
+
jsonrpc_success_response_body(request_id, { "resources" => [] })
|
|
260
|
+
when "resources/templates/list"
|
|
261
|
+
jsonrpc_success_response_body(request_id, { "resourceTemplates" => [] })
|
|
204
262
|
else
|
|
205
263
|
{
|
|
206
264
|
"jsonrpc" => "2.0",
|
|
@@ -218,14 +276,9 @@ class Tidewave
|
|
|
218
276
|
client_version = params["protocolVersion"]
|
|
219
277
|
return jsonrpc_error_response_body(request_id, -32602, "Protocol version is required") if client_version.nil? || client_version.empty?
|
|
220
278
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
-32602,
|
|
225
|
-
"Unsupported protocol version. Server supports #{PROTOCOL_VERSION} or later"
|
|
226
|
-
)
|
|
227
|
-
end
|
|
228
|
-
|
|
279
|
+
# Version negotiation: when the client requests a version we don't
|
|
280
|
+
# support, we respond with the version we do support and the client
|
|
281
|
+
# decides whether to continue or disconnect.
|
|
229
282
|
jsonrpc_success_response_body(request_id, {
|
|
230
283
|
"protocolVersion" => PROTOCOL_VERSION,
|
|
231
284
|
"capabilities" => { "tools" => { "listChanged" => false } },
|
|
@@ -261,8 +314,8 @@ class Tidewave
|
|
|
261
314
|
}
|
|
262
315
|
end
|
|
263
316
|
|
|
264
|
-
def jsonrpc_error_response(request_id, code, message)
|
|
265
|
-
json_response(jsonrpc_error_response_body(request_id, code, message))
|
|
317
|
+
def jsonrpc_error_response(request_id, code, message, status: 200)
|
|
318
|
+
json_response(jsonrpc_error_response_body(request_id, code, message), status: status)
|
|
266
319
|
end
|
|
267
320
|
|
|
268
321
|
def jsonrpc_error_response_body(request_id, code, message)
|
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.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yorick Jacquin
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-07-09 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rack
|