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 +4 -4
- data/README.md +3 -1
- data/lib/tidewave/configuration.rb +2 -1
- data/lib/tidewave/middleware.rb +28 -12
- data/lib/tidewave/railtie.rb +4 -2
- data/lib/tidewave/streamable_http_transport.rb +131 -0
- data/lib/tidewave/tools/execute_sql_query.rb +6 -0
- data/lib/tidewave/tools/project_eval.rb +6 -0
- data/lib/tidewave/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 728311a2ea0525028ccf9209d06bc1079f9258e35d534409f4ac867ff5212d32
|
4
|
+
data.tar.gz: 11c1b092dc830897e59cb39f71de745b840acfd6f8845bf058c9010a06258e75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/tidewave/middleware.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 ] ]
|
data/lib/tidewave/railtie.rb
CHANGED
@@ -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.
|
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
|
data/lib/tidewave/version.rb
CHANGED
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.
|
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-
|
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.
|
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.
|
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
|