tidewave 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f3737d76d054bed63cb68932692d012571e6dc6592e4acb3d38cfc996e136c9
4
- data.tar.gz: 826dda40d4b76b7e399f91bf696463a1b8c70554ebc29a96ddea0d3aa43cef4a
3
+ metadata.gz: 728311a2ea0525028ccf9209d06bc1079f9258e35d534409f4ac867ff5212d32
4
+ data.tar.gz: 11c1b092dc830897e59cb39f71de745b840acfd6f8845bf058c9010a06258e75
5
5
  SHA512:
6
- metadata.gz: 7f48e43388215ba5d6db70be3e450d7f3358921f82ae4b6c855801ad3ee48aeea3ff1cdd2b7d00acbf072556cf8b4ab5982541f894b470a502dbf149c63a798f
7
- data.tar.gz: d4acf2d1625913bbbbae42d74c9816f200dba1ab2e25dff182a83c63aff9f236c21062b8745568beb2dd4378e39cc23073d7c39cde725a31c36a74e0ba2edfd2
6
+ metadata.gz: f9b49181b3f571b668de00287f0fec47088256ee335e31036385cedcb3b8c34274529d70a4f21d9c114279d9a1f5e52a56cab75b8465546863e0fd539fd1b390
7
+ data.tar.gz: 1201a00a28892246592c158bc0e90cb5a6ce5d69a6a3533ef81bb8c853d2124d3544d029e8346613fa4d099010869f83cae49bcad82d890d1e935ee9f54903f4
data/README.md CHANGED
@@ -29,7 +29,7 @@ If you want to use Docker for development, you either need to enable the configu
29
29
 
30
30
  ### Content security policy
31
31
 
32
- If you have enabled Content-Security-Policy, Tidewave will automatically enable "unsafe-eval" under `script-src` in order for contextual browser testing to work correctly.
32
+ If you have enabled Content-Security-Policy, Tidewave will automatically enable "unsafe-eval" under `script-src` in order for contextual browser testing to work correctly. It also disables the `frame-ancestors` directive.
33
33
 
34
34
  ### Web server requirements
35
35
 
@@ -55,6 +55,8 @@ The following config is available:
55
55
 
56
56
  * `allow_remote_access` - Tidewave only allows requests from localhost by default, even if your server listens on other interfaces as well. If you trust your network and need to access Tidewave from a different machine, this configuration can be set to `true`
57
57
 
58
+ * `logger_middleware` - The logger middleware Tidewave should wrap to silence its own logs
59
+
58
60
  * `preferred_orm` - which ORM to use, either `:active_record` (default) or `:sequel`
59
61
 
60
62
  * `team` - set your Tidewave Team configuration, such as `config.tidewave.team = { id: "my-company" }`
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Tidewave
4
4
  class Configuration
5
- attr_accessor :logger, :allow_remote_access, :preferred_orm, :dev, :client_url, :team
5
+ attr_accessor :logger, :allow_remote_access, :preferred_orm, :dev, :client_url, :team, :logger_middleware
6
6
 
7
7
  def initialize
8
8
  @logger = nil
@@ -11,6 +11,7 @@ module Tidewave
11
11
  @dev = false
12
12
  @client_url = "https://tidewave.ai"
13
13
  @team = {}
14
+ @logger_middleware = nil
14
15
  end
15
16
  end
16
17
  end
@@ -8,12 +8,13 @@ require "active_support/core_ext/class"
8
8
  require "active_support/core_ext/object/blank"
9
9
  require "json"
10
10
  require "erb"
11
+ require_relative "streamable_http_transport"
11
12
 
12
13
  class Tidewave::Middleware
13
14
  TIDEWAVE_ROUTE = "tidewave".freeze
14
- SSE_ROUTE = "mcp".freeze
15
- MESSAGES_ROUTE = "mcp/message".freeze
15
+ MCP_ROUTE = "mcp".freeze
16
16
  SHELL_ROUTE = "shell".freeze
17
+ CONFIG_ROUTE = "config".freeze
17
18
 
18
19
  INVALID_IP = <<~TEXT.freeze
19
20
  For security reasons, Tidewave does not accept remote connections by default.
@@ -30,9 +31,8 @@ class Tidewave::Middleware
30
31
  @app = FastMcp.rack_middleware(app,
31
32
  name: "tidewave",
32
33
  version: Tidewave::VERSION,
33
- path_prefix: "/" + TIDEWAVE_ROUTE,
34
- messages_route: MESSAGES_ROUTE,
35
- sse_route: SSE_ROUTE,
34
+ path_prefix: "/" + TIDEWAVE_ROUTE + "/" + MCP_ROUTE,
35
+ transport: Tidewave::StreamableHttpTransport,
36
36
  logger: config.logger || Logger.new(Rails.root.join("log", "tidewave.log")),
37
37
  # Rails runs the HostAuthorization in dev, so we skip this
38
38
  allowed_origins: [],
@@ -62,23 +62,26 @@ class Tidewave::Middleware
62
62
  case [ request.request_method, path ]
63
63
  when [ "GET", [ TIDEWAVE_ROUTE ] ]
64
64
  return home(request)
65
+ when [ "GET", [ TIDEWAVE_ROUTE, CONFIG_ROUTE ] ]
66
+ return config_endpoint(request)
65
67
  when [ "POST", [ TIDEWAVE_ROUTE, SHELL_ROUTE ] ]
66
68
  return shell(request)
67
69
  end
68
70
  end
69
71
 
70
- @app.call(env)
72
+ status, headers, body = @app.call(env)
73
+
74
+ # Remove X-Frame-Options headers for non-Tidewave routes to allow embedding.
75
+ # CSP headers are configured in the CSP application environment.
76
+ headers.delete("X-Frame-Options")
77
+
78
+ [ status, headers, body ]
71
79
  end
72
80
 
73
81
  private
74
82
 
75
83
  def home(request)
76
- config = {
77
- "project_name" => @project_name,
78
- "framework_type" => "rails",
79
- "tidewave_version" => Tidewave::VERSION,
80
- "team" => @team
81
- }
84
+ config = config_data
82
85
 
83
86
  html = <<~HTML
84
87
  <html>
@@ -95,6 +98,19 @@ class Tidewave::Middleware
95
98
  [ 200, { "Content-Type" => "text/html" }, [ html ] ]
96
99
  end
97
100
 
101
+ def config_endpoint(request)
102
+ [ 200, { "Content-Type" => "application/json" }, [ JSON.generate(config_data) ] ]
103
+ end
104
+
105
+ def config_data
106
+ {
107
+ "project_name" => @project_name,
108
+ "framework_type" => "rails",
109
+ "tidewave_version" => Tidewave::VERSION,
110
+ "team" => @team
111
+ }
112
+ end
113
+
98
114
  def forbidden(message)
99
115
  Rails.logger.warn(message)
100
116
  [ 403, { "Content-Type" => "text/plain" }, [ message ] ]
@@ -32,6 +32,8 @@ module Tidewave
32
32
  content_security_policy.directives["script-src"].try do |script_src|
33
33
  script_src << "'unsafe-eval'" unless script_src.include?("'unsafe-eval'")
34
34
  end
35
+
36
+ content_security_policy.directives.delete("frame-ancestors")
35
37
  end
36
38
  end
37
39
  end
@@ -39,7 +41,6 @@ module Tidewave
39
41
  initializer "tidewave.intercept_exceptions" do |app|
40
42
  # We intercept exceptions from DebugExceptions, format the
41
43
  # information as text and inject into the exception page html.
42
-
43
44
  ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
44
45
  request.set_header("tidewave.exception", exception)
45
46
  end
@@ -49,7 +50,8 @@ module Tidewave
49
50
 
50
51
  initializer "tidewave.logging" do |app|
51
52
  # Do not pollute user logs with tidewave requests.
52
- app.middleware.insert_before(Rails::Rack::Logger, Tidewave::QuietRequestsMiddleware)
53
+ logger_middleware = app.config.tidewave.logger_middleware || Rails::Rack::Logger
54
+ app.middleware.insert_before(logger_middleware, Tidewave::QuietRequestsMiddleware)
53
55
  end
54
56
  end
55
57
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rack"
5
+ require "fast_mcp"
6
+
7
+ module Tidewave
8
+ # Streamable HTTP transport for MCP (POST-only, no SSE)
9
+ # This transport implements a simplified version of the MCP Streamable HTTP protocol
10
+ # that only supports POST requests for JSON-RPC messages. Unlike the full protocol,
11
+ # it does not support Server-Sent Events (SSE) for streaming responses.
12
+ class StreamableHttpTransport < FastMcp::Transports::BaseTransport
13
+ attr_reader :app, :path
14
+
15
+ def initialize(app, server, options = {})
16
+ super(server, logger: options[:logger])
17
+ @app = app
18
+ @path = options[:path_prefix] || "/mcp"
19
+ @running = false
20
+ end
21
+
22
+ def start
23
+ @logger.debug("Starting Streamable HTTP transport (POST-only) at path: #{@path}")
24
+ @running = true
25
+ end
26
+
27
+ def stop
28
+ @logger.debug("Stopping Streamable HTTP transport")
29
+ @running = false
30
+ end
31
+
32
+ # Send a message - capture response for synchronous HTTP
33
+ # Required by FastMCP::Transports::BaseTransport interface
34
+ def send_message(message)
35
+ @logger.debug("send_message called, capturing response: #{message.inspect}")
36
+ @captured_response = message
37
+ end
38
+
39
+ def call(env)
40
+ request = Rack::Request.new(env)
41
+
42
+ if request.path == @path
43
+ @server.transport = self
44
+ handle_mcp_request(request, env)
45
+ else
46
+ @app.call(env)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def handle_mcp_request(request, env)
53
+ if request.post?
54
+ handle_post_request(request)
55
+ else
56
+ method_not_allowed_response
57
+ end
58
+ end
59
+
60
+ def handle_post_request(request)
61
+ @logger.debug("Received POST request to MCP endpoint")
62
+
63
+ begin
64
+ body = request.body.read
65
+ message = JSON.parse(body)
66
+
67
+ @logger.debug("Processing message: #{message.inspect}")
68
+
69
+ unless valid_jsonrpc_message?(message)
70
+ return json_rpc_error_response(400, -32600, "Invalid Request", nil)
71
+ end
72
+
73
+ # Capture the response that will be sent via send_message
74
+ @captured_response = nil
75
+ @server.handle_json_request(message)
76
+
77
+ @logger.debug("Sending response: #{@captured_response.inspect}")
78
+
79
+ [
80
+ 200,
81
+ { "Content-Type" => "application/json" },
82
+ [ JSON.generate(@captured_response) ]
83
+ ]
84
+ rescue JSON::ParserError => e
85
+ @logger.error("Invalid JSON in request: #{e.message}")
86
+ json_rpc_error_response(400, -32700, "Parse error", nil)
87
+ rescue => e
88
+ @logger.error("Error processing message: #{e.message}")
89
+ @logger.error(e.backtrace.join("\n")) if e.backtrace
90
+ json_rpc_error_response(500, -32603, "Internal error", nil)
91
+ end
92
+ end
93
+
94
+ def valid_jsonrpc_message?(message)
95
+ return false unless message.is_a?(Hash)
96
+ return false unless message["jsonrpc"] == "2.0"
97
+
98
+ message.key?("method") || message.key?("result") || message.key?("error")
99
+ end
100
+
101
+ def method_not_allowed_response
102
+ [
103
+ 405,
104
+ { "Content-Type" => "application/json" },
105
+ [ JSON.generate({
106
+ jsonrpc: "2.0",
107
+ error: {
108
+ code: -32601,
109
+ message: "Method not allowed. This endpoint only supports POST requests."
110
+ },
111
+ id: nil
112
+ }) ]
113
+ ]
114
+ end
115
+
116
+ def json_rpc_error_response(http_status, code, message, id)
117
+ [
118
+ http_status,
119
+ { "Content-Type" => "application/json" },
120
+ [ JSON.generate({
121
+ jsonrpc: "2.0",
122
+ error: {
123
+ code: code,
124
+ message: message
125
+ },
126
+ id: id
127
+ }) ]
128
+ ]
129
+ end
130
+ end
131
+ end
@@ -22,6 +22,12 @@ class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
22
22
  optional(:arguments).value(:array).description("The arguments to pass to the query. The query must contain corresponding parameter placeholders.")
23
23
  end
24
24
 
25
+ def @input_schema.json_schema
26
+ schema = super
27
+ schema[:properties][:arguments][:items] = {}
28
+ schema
29
+ end
30
+
25
31
  RESULT_LIMIT = 50
26
32
 
27
33
  def call(query:, arguments: [])
@@ -23,6 +23,12 @@ class Tidewave::Tools::ProjectEval < Tidewave::Tools::Base
23
23
  optional(:json).hidden().filled(:bool).description("Whether to return the result as JSON with structured output containing result, success, stdout, and stderr fields. Defaults to false.")
24
24
  end
25
25
 
26
+ def @input_schema.json_schema
27
+ schema = super
28
+ schema[:properties][:arguments][:items] = {}
29
+ schema
30
+ end
31
+
26
32
  def call(code:, arguments: [], timeout: 30_000, json: false)
27
33
  original_stdout = $stdout
28
34
  original_stderr = $stderr
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tidewave
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  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.3.1
4
+ version: 0.4.0
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: 2025-09-16 00:00:00.000000000 Z
12
+ date: 2025-10-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -31,14 +31,14 @@ dependencies:
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: 1.5.0
34
+ version: 1.6.0
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 1.5.0
41
+ version: 1.6.0
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: rack
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -72,6 +72,7 @@ files:
72
72
  - lib/tidewave/middleware.rb
73
73
  - lib/tidewave/quiet_requests_middleware.rb
74
74
  - lib/tidewave/railtie.rb
75
+ - lib/tidewave/streamable_http_transport.rb
75
76
  - lib/tidewave/tools/base.rb
76
77
  - lib/tidewave/tools/execute_sql_query.rb
77
78
  - lib/tidewave/tools/get_docs.rb