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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80a98c944044fd2164929251e642cd2c35713a3bcd2a427485aa9aed5b9c2780
4
- data.tar.gz: dac0861994231e0cfb23eb9203c43c9ed310f0fbb0041e1d674443df1e592323
3
+ metadata.gz: c64d42ec2ee75f35b02997081d24e69fe547d8b9a20a230daeb1d1995a30cd0a
4
+ data.tar.gz: 74e1cc3e89cdea2b1d4701990ca3fbe3b4b3a5c828735bef5251c91a1d8c074a
5
5
  SHA512:
6
- metadata.gz: 2e0cd1d84fc3aa6e2f9da2a4c1cd7dcc613e29552ed20f0b73bdcb5770bef22f5555846e290638c77bb1c5df3c1c2d53285b3fb03cd41c127394630f785e60b2
7
- data.tar.gz: 50ac0f18928027d78246fe69b0c77f552a3b05772875f8b01f016d05cffbbfeca4262359bff6407781ecb7509a99b2955788e3c1080bac110061f950c7afbd37
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 the coding agent for full-stack web app development. Integrate Claude Code, OpenAI Codex, and other agents with your web app and web framework at every layer, from UI to database. [See our website](https://tidewave.ai) for more information.
3
+ Tidewave Rails is an MCP server that provides runtime-level tools for developing Ruby on Rails apps using coding agents.
4
4
 
5
- This project can also be used as [a standalone Model Context Protocol server](https://hexdocs.pm/tidewave/mcp.html).
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
- You can install Tidewave by running:
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 manully adding the `tidewave` gem to the development group in your Gemfile:
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
- Now make sure [Tidewave is installed](https://hexdocs.pm/tidewave/installation.html) and you are ready to connect Tidewave to your app.
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
+ [![project_eval demo](docs/assets/project_eval-poster.png)](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
+ [![execute_sql_query demo](docs/assets/execute_sql_query-poster.png)](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
+ [![get_docs demo](docs/assets/get_docs-poster.png)](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
+ [![get_logs demo](docs/assets/get_logs-poster.png)](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
+ [![get_models demo](docs/assets/get_models-poster.png)](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
+ [![get_source_location demo](docs/assets/get_source_location-poster.png)](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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Tidewave
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.2"
5
5
  end
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
- not_found
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("X-Frame-Options")
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: { "Access-Control-Allow-Origin" => "*" })
110
+ json_response(config_data(request), headers: { "access-control-allow-origin" => "*" })
109
111
  end
110
112
 
111
113
  def mcp_endpoint(request)
112
- body = request.body.read
114
+ message = JSON.parse(request.body.read)
113
115
 
114
- message = JSON.parse(body)
115
- validation_error = validate_jsonrpc_message(message)
116
- return jsonrpc_error_response(nil, -32600, validation_error) if validation_error
117
-
118
- response = handle_mcp_message(message)
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
- "Content-Type" => content_type,
161
- "Content-Length" => body.bytesize.to_s
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
- if client_version < PROTOCOL_VERSION
237
- return jsonrpc_error_response_body(
238
- request_id,
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.1
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-06-14 00:00:00.000000000 Z
12
+ date: 2026-07-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack