tidewave 0.5.1 → 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 +101 -38
- data/lib/tidewave/version.rb +1 -1
- data/lib/tidewave.rb +66 -28
- 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,24 +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
|
|
36
|
+
|
|
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`.
|
|
38
|
+
|
|
39
|
+
We also have specific instructions for:
|
|
40
|
+
|
|
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.
|
|
22
117
|
|
|
23
118
|
## Troubleshooting
|
|
24
119
|
|
|
@@ -66,38 +161,6 @@ The following config is available:
|
|
|
66
161
|
|
|
67
162
|
* `team` - set your Tidewave Team configuration, such as `config.tidewave.team = { id: "my-company" }`
|
|
68
163
|
|
|
69
|
-
## Available tools
|
|
70
|
-
|
|
71
|
-
- `execute_sql_query` - executes a SQL query within your application
|
|
72
|
-
database, useful for the agent to verify the result of an action
|
|
73
|
-
|
|
74
|
-
- `get_docs` - get the documentation for a given module/class/method.
|
|
75
|
-
It consults the exact versions used by the project, ensuring you always
|
|
76
|
-
get correct information
|
|
77
|
-
|
|
78
|
-
- `get_logs` - reads logs written by the server
|
|
79
|
-
|
|
80
|
-
- `get_models` - lists all modules in the application and their location
|
|
81
|
-
for quick discovery
|
|
82
|
-
|
|
83
|
-
- `get_source_location` - get the source location for a given module/class/method,
|
|
84
|
-
so an agent can directly read the source skipping search
|
|
85
|
-
|
|
86
|
-
- `project_eval` - evaluates code within the Rails application itself, giving the agent
|
|
87
|
-
access to your runtime, dependencies, and in-memory data
|
|
88
|
-
|
|
89
|
-
> [!NOTE]
|
|
90
|
-
> #### Why no tools for routes, associations, etc?
|
|
91
|
-
>
|
|
92
|
-
> Tidewave does not include tools for listing your routes, associations, etc. because
|
|
93
|
-
> agents are better off reading their respective source files, which gives agents more
|
|
94
|
-
> context and enable them to perform any necessary edit without additional tools calls.
|
|
95
|
-
>
|
|
96
|
-
> Instead, Tidewave aims to fill in missing gaps, such as evaluating code inside your
|
|
97
|
-
> Rails app (without starting new instances) and finding source location, which can be
|
|
98
|
-
> tricky, even with grepping, due to meta-programming and the different places Bundler
|
|
99
|
-
> can install your dependencies.
|
|
100
|
-
|
|
101
164
|
## Acknowledgements
|
|
102
165
|
|
|
103
166
|
A thank you to Yorick Jacquin for the initial version of this project.
|
data/lib/tidewave/version.rb
CHANGED
data/lib/tidewave.rb
CHANGED
|
@@ -72,7 +72,9 @@ class Tidewave
|
|
|
72
72
|
when [ "POST", [ TIDEWAVE_ROUTE, MCP_ROUTE ] ]
|
|
73
73
|
mcp_endpoint(request)
|
|
74
74
|
else
|
|
75
|
-
|
|
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()
|
|
76
78
|
end
|
|
77
79
|
else
|
|
78
80
|
strip_x_frame_options(@app.call(env))
|
|
@@ -83,7 +85,7 @@ class Tidewave
|
|
|
83
85
|
|
|
84
86
|
def strip_x_frame_options(response)
|
|
85
87
|
status, headers, body = response
|
|
86
|
-
headers.delete("
|
|
88
|
+
headers.delete("x-frame-options")
|
|
87
89
|
[ status, headers, body ]
|
|
88
90
|
end
|
|
89
91
|
|
|
@@ -105,27 +107,46 @@ class Tidewave
|
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
def config_endpoint(request)
|
|
108
|
-
json_response(config_data(request), headers: { "
|
|
110
|
+
json_response(config_data(request), headers: { "access-control-allow-origin" => "*" })
|
|
109
111
|
end
|
|
110
112
|
|
|
111
113
|
def mcp_endpoint(request)
|
|
112
|
-
|
|
114
|
+
message = JSON.parse(request.body.read)
|
|
113
115
|
|
|
114
|
-
message
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return json_response({ "status" => "ok" }, status: 202) if response.nil?
|
|
120
|
-
|
|
121
|
-
json_response(response)
|
|
116
|
+
if message.is_a?(Array)
|
|
117
|
+
handle_mcp_batch(message)
|
|
118
|
+
else
|
|
119
|
+
handle_mcp_single(message)
|
|
120
|
+
end
|
|
122
121
|
rescue JSON::ParserError
|
|
123
|
-
jsonrpc_error_response(nil, -32700, "Parse error")
|
|
122
|
+
jsonrpc_error_response(nil, -32700, "Parse error", status: 400)
|
|
124
123
|
rescue StandardError => error
|
|
125
124
|
@logger&.error("Error handling MCP request: #{error.message}")
|
|
126
125
|
jsonrpc_error_response(nil, -32603, "Internal error")
|
|
127
126
|
end
|
|
128
127
|
|
|
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
|
+
|
|
129
150
|
def config_data(request)
|
|
130
151
|
{
|
|
131
152
|
"project_name" => @options[:project_name],
|
|
@@ -151,14 +172,27 @@ class Tidewave
|
|
|
151
172
|
text_response(404, "Not Found")
|
|
152
173
|
end
|
|
153
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
|
+
|
|
154
184
|
def text_response(status, message)
|
|
155
185
|
[ status, response_headers("text/plain; charset=utf-8", message), [ message ] ]
|
|
156
186
|
end
|
|
157
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.
|
|
158
192
|
def response_headers(content_type, body)
|
|
159
193
|
{
|
|
160
|
-
"
|
|
161
|
-
"
|
|
194
|
+
"content-type" => content_type,
|
|
195
|
+
"content-length" => body.bytesize.to_s
|
|
162
196
|
}
|
|
163
197
|
end
|
|
164
198
|
|
|
@@ -192,7 +226,7 @@ class Tidewave
|
|
|
192
226
|
|
|
193
227
|
has_id = message.key?("id")
|
|
194
228
|
has_method = message.key?("method")
|
|
195
|
-
has_result = message.key?("result")
|
|
229
|
+
has_result = message.key?("result") || message.key?("error")
|
|
196
230
|
|
|
197
231
|
return nil if has_method
|
|
198
232
|
return nil if has_id && has_result
|
|
@@ -200,14 +234,17 @@ class Tidewave
|
|
|
200
234
|
"Invalid JSON-RPC message structure"
|
|
201
235
|
end
|
|
202
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.
|
|
203
240
|
def handle_mcp_message(message)
|
|
241
|
+
return nil unless message.key?("method") && message.key?("id")
|
|
242
|
+
|
|
204
243
|
method = message["method"]
|
|
205
244
|
request_id = message["id"]
|
|
206
245
|
params = message["params"].is_a?(Hash) ? message["params"] : {}
|
|
207
246
|
|
|
208
247
|
case method
|
|
209
|
-
when "notifications/initialized", "notifications/cancelled"
|
|
210
|
-
nil
|
|
211
248
|
when "ping"
|
|
212
249
|
jsonrpc_success_response_body(request_id, {})
|
|
213
250
|
when "initialize"
|
|
@@ -216,6 +253,12 @@ class Tidewave
|
|
|
216
253
|
jsonrpc_success_response_body(request_id, { "tools" => tool_definitions })
|
|
217
254
|
when "tools/call"
|
|
218
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" => [] })
|
|
219
262
|
else
|
|
220
263
|
{
|
|
221
264
|
"jsonrpc" => "2.0",
|
|
@@ -233,14 +276,9 @@ class Tidewave
|
|
|
233
276
|
client_version = params["protocolVersion"]
|
|
234
277
|
return jsonrpc_error_response_body(request_id, -32602, "Protocol version is required") if client_version.nil? || client_version.empty?
|
|
235
278
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
-32602,
|
|
240
|
-
"Unsupported protocol version. Server supports #{PROTOCOL_VERSION} or later"
|
|
241
|
-
)
|
|
242
|
-
end
|
|
243
|
-
|
|
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.
|
|
244
282
|
jsonrpc_success_response_body(request_id, {
|
|
245
283
|
"protocolVersion" => PROTOCOL_VERSION,
|
|
246
284
|
"capabilities" => { "tools" => { "listChanged" => false } },
|
|
@@ -276,8 +314,8 @@ class Tidewave
|
|
|
276
314
|
}
|
|
277
315
|
end
|
|
278
316
|
|
|
279
|
-
def jsonrpc_error_response(request_id, code, message)
|
|
280
|
-
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)
|
|
281
319
|
end
|
|
282
320
|
|
|
283
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
|