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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d75b5659655095637ad8830051ef50f06e018339520d093c51854cec9c320fd7
4
- data.tar.gz: 9325b6a4ea7ac4e1444a677bd1dc785bc6f8046e8492a5d1b70267c9424ca9a3
3
+ metadata.gz: c64d42ec2ee75f35b02997081d24e69fe547d8b9a20a230daeb1d1995a30cd0a
4
+ data.tar.gz: 74e1cc3e89cdea2b1d4701990ca3fbe3b4b3a5c828735bef5251c91a1d8c074a
5
5
  SHA512:
6
- metadata.gz: 9fe7d353f945a3a3735dfdd56486188df12034736c2ac78561dca478b05b6f15fba9608dd8e8209f3d9a66659f4908c2814ec65ccdf13030f56b1387a9190fde
7
- data.tar.gz: 6782f79c55813d99d5cffeb973812116aaf581570119de0ebcd739d2f65d1aa0fbc3ed3bc1cc1d478ec0b30ac18fbc2ab9db6037df18ec433b8ef50cce7eb947
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 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
22
36
 
23
- ## Development
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
- Run the Minitest suite with:
39
+ We also have specific instructions for:
26
40
 
27
- ```shell
28
- bundle exec ruby -Itest test/all_test.rb
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
+ [![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.
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
- ## Available tools
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
- - `get_source_location` - get the source location for a given module/class/method,
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
- - `project_eval` - evaluates code within the Rails application itself, giving the agent
95
- access to your runtime, dependencies, and in-memory data
168
+ ## Development
96
169
 
97
- ## Acknowledgements
170
+ Run the Minitest suite with:
98
171
 
99
- A thank you to Yorick Jacquin for the initial version of this project.
172
+ ```shell
173
+ bundle exec ruby -Itest test/all_test.rb
174
+ ```
100
175
 
101
176
  ## License
102
177
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Tidewave
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.2"
5
5
  end
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
- return forbidden(INVALID_ORIGIN) if request.get_header("HTTP_ORIGIN") && path != [ TIDEWAVE_ROUTE ]
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
- 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()
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("X-Frame-Options")
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(_request)
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
- 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?
114
+ message = JSON.parse(request.body.read)
118
115
 
119
- json_response(response)
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 config_data
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
- "Content-Type" => content_type,
158
- "Content-Length" => body.bytesize.to_s
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
- 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
-
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.0
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-05-29 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