tidewave 0.1.3 → 0.3.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 +31 -32
- data/lib/tidewave/configuration.rb +7 -5
- data/lib/tidewave/database_adapter.rb +33 -0
- data/lib/tidewave/database_adapters/active_record.rb +39 -0
- data/lib/tidewave/database_adapters/sequel.rb +37 -0
- data/lib/tidewave/exceptions_middleware.rb +88 -0
- data/lib/tidewave/middleware.rb +184 -0
- data/lib/tidewave/quiet_requests_middleware.rb +15 -0
- data/lib/tidewave/railtie.rb +38 -32
- data/lib/tidewave/tools/execute_sql_query.rb +2 -20
- data/lib/tidewave/tools/get_docs.rb +64 -0
- data/lib/tidewave/tools/get_logs.rb +10 -3
- data/lib/tidewave/tools/get_models.rb +21 -10
- data/lib/tidewave/tools/get_source_location.rb +30 -10
- data/lib/tidewave/tools/project_eval.rb +49 -9
- data/lib/tidewave/version.rb +1 -1
- data/lib/tidewave.rb +6 -3
- metadata +10 -11
- data/lib/tidewave/file_tracker.rb +0 -101
- data/lib/tidewave/tools/edit_project_file.rb +0 -47
- data/lib/tidewave/tools/get_package_location.rb +0 -41
- data/lib/tidewave/tools/grep_project_files.rb +0 -110
- data/lib/tidewave/tools/list_project_files.rb +0 -26
- data/lib/tidewave/tools/package_search.rb +0 -43
- data/lib/tidewave/tools/read_project_file.rb +0 -25
- data/lib/tidewave/tools/shell_eval.rb +0 -36
- data/lib/tidewave/tools/write_project_file.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d140360315f54166fc14e330f33fc5dd62e561c49ba3d6cb5044a8e7b5247e41
|
4
|
+
data.tar.gz: c69a1a0363ce0f50a396599f2b551896a602431929731dd9fbf4fbe283823cf1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f3203bbce1c31681fcb2b21575ea62a8714a469f61ec30be8d59ccd22028592b7a33317b4f434e0d7a4051caaf1db1aa82247d395a961d4a09f1a1538cc6cd8
|
7
|
+
data.tar.gz: 968742eed4690b5c2a1f8c0ecf233e4742a19bc2ac8a700d4f64eca2878bea8f70d1d2228cffc472019462790eb122601b7acfc608fd7950c100974f3c9053d4
|
data/README.md
CHANGED
@@ -1,21 +1,8 @@
|
|
1
1
|
# Tidewave
|
2
2
|
|
3
|
-
Tidewave
|
4
|
-
how it runs, and what it delivers. Our current release connects your editor's
|
5
|
-
assistant to your web framework runtime via [MCP](https://modelcontextprotocol.io/).
|
3
|
+
Tidewave is the coding agent for full-stack web app development, deeply integrated with Rails, from the database to the UI. [See our website](https://tidewave.ai) for more information.
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
## Key Features
|
10
|
-
|
11
|
-
Tidewave provides tools that allow your LLM of choice to:
|
12
|
-
|
13
|
-
- inspect your application logs to help debugging errors
|
14
|
-
- execute SQL queries and inspect your database
|
15
|
-
- evaluate custom Ruby code in the context of your project
|
16
|
-
- find Rubygems packages and source code locations
|
17
|
-
|
18
|
-
and more.
|
5
|
+
This project can also be used as a standalone Model Context Protocol server for your editors.
|
19
6
|
|
20
7
|
## Installation
|
21
8
|
|
@@ -25,40 +12,52 @@ You can install Tidewave by adding the `tidewave` gem to the development group i
|
|
25
12
|
gem "tidewave", group: :development
|
26
13
|
```
|
27
14
|
|
28
|
-
|
29
|
-
In particular, the MCP is located by default at http://localhost:3000/tidewave/mcp.
|
30
|
-
[You must configure your editor and AI assistants accordingly](https://hexdocs.pm/tidewave/mcp.html).
|
15
|
+
Now access `/tidewave` route of your web application to enjoy Tidewave Web!
|
31
16
|
|
32
|
-
##
|
17
|
+
## Troubleshooting
|
18
|
+
|
19
|
+
### Localhost requirement
|
33
20
|
|
34
|
-
|
21
|
+
Tidewave expects your web application to be running on `localhost`. If you are not running on localhost, you may need to set some additional configuration. In particular, you must configure Tidewave to allow `allow_remote_access` and [optionally configure your Rails hosts](https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization). For example, in your `config/environments/development.rb`:
|
35
22
|
|
36
23
|
```ruby
|
37
|
-
|
24
|
+
config.hosts << "company.local"
|
25
|
+
config.tidewave.allow_remote_access = true
|
38
26
|
```
|
39
27
|
|
40
|
-
|
28
|
+
If you want to use Docker for development, you either need to enable the configuration above or automatically redirect the relevant ports, as done by [devcontainers](https://code.visualstudio.com/docs/devcontainers/containers). See our [containers](https://hexdocs.pm/tidewave/containers.html) guide for more information.
|
41
29
|
|
42
|
-
|
30
|
+
### Content security policy
|
43
31
|
|
44
|
-
|
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.
|
45
33
|
|
46
|
-
|
34
|
+
### Web server requirements
|
47
35
|
|
48
|
-
|
36
|
+
At the moment, Tidewave requires all requests to be processed by the same process. In case Tidewave cannot connect to your application, consider starting your Rails application as follows:
|
49
37
|
|
50
|
-
|
38
|
+
RAILS_MAX_THREADS=1 WEB_CONCURRENCY=1 rails server
|
51
39
|
|
52
40
|
### Production Environment
|
53
41
|
|
54
|
-
Tidewave is a powerful tool that can help you develop your web application faster and more efficiently.
|
55
|
-
However, it is important to note that Tidewave is not meant to be used in a production environment.
|
42
|
+
Tidewave is a powerful tool that can help you develop your web application faster and more efficiently. However, it is important to note that Tidewave is not meant to be used in a production environment.
|
56
43
|
|
57
|
-
Tidewave will raise an error if it is used in
|
44
|
+
Tidewave will raise an error if it is used in any environment where code reloading is disabled (which typically includes production).
|
58
45
|
|
59
|
-
|
46
|
+
## Configuration
|
47
|
+
|
48
|
+
You may configure `tidewave` using the following syntax:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
config.tidewave.allow_remote_access = true
|
52
|
+
```
|
53
|
+
|
54
|
+
The following config is available:
|
55
|
+
|
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
|
+
|
58
|
+
* `preferred_orm` - which ORM to use, either `:active_record` (default) or `:sequel`
|
60
59
|
|
61
|
-
|
60
|
+
* `team` - set your team configuration, such as `config.tidewave.team = { id: "my-company }`
|
62
61
|
|
63
62
|
## Acknowledgements
|
64
63
|
|
@@ -2,13 +2,15 @@
|
|
2
2
|
|
3
3
|
module Tidewave
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :logger, :
|
5
|
+
attr_accessor :logger, :allow_remote_access, :preferred_orm, :credentials, :client_url, :team
|
6
6
|
|
7
7
|
def initialize
|
8
|
-
@logger =
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
8
|
+
@logger = nil
|
9
|
+
@allow_remote_access = true
|
10
|
+
@preferred_orm = :active_record
|
11
|
+
@credentials = {}
|
12
|
+
@client_url = "https://tidewave.ai"
|
13
|
+
@team = {}
|
12
14
|
end
|
13
15
|
end
|
14
16
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tidewave
|
4
|
+
class DatabaseAdapter
|
5
|
+
class << self
|
6
|
+
def current
|
7
|
+
@current ||= create_adapter
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_adapter
|
11
|
+
orm_type = Rails.application.config.tidewave.preferred_orm
|
12
|
+
case orm_type
|
13
|
+
when :active_record
|
14
|
+
require_relative "database_adapters/active_record"
|
15
|
+
DatabaseAdapters::ActiveRecord.new
|
16
|
+
when :sequel
|
17
|
+
require_relative "database_adapters/sequel"
|
18
|
+
DatabaseAdapters::Sequel.new
|
19
|
+
else
|
20
|
+
raise "Unknown preferred ORM: #{orm_type}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute_query(query, arguments = [])
|
26
|
+
raise NotImplementedError, "Subclasses must implement execute_query"
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_base_class
|
30
|
+
raise NotImplementedError, "Subclasses must implement get_base_class"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tidewave
|
4
|
+
module DatabaseAdapters
|
5
|
+
class ActiveRecord < DatabaseAdapter
|
6
|
+
RESULT_LIMIT = 50
|
7
|
+
|
8
|
+
def execute_query(query, arguments = [])
|
9
|
+
conn = ::ActiveRecord::Base.connection
|
10
|
+
|
11
|
+
# Execute the query with prepared statement and arguments
|
12
|
+
if arguments.any?
|
13
|
+
result = conn.exec_query(query, "SQL", arguments)
|
14
|
+
else
|
15
|
+
result = conn.exec_query(query)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Format the result
|
19
|
+
{
|
20
|
+
columns: result.columns,
|
21
|
+
rows: result.rows.first(RESULT_LIMIT),
|
22
|
+
row_count: result.rows.length,
|
23
|
+
adapter: conn.adapter_name,
|
24
|
+
database: database_name
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_base_class
|
29
|
+
::ActiveRecord::Base
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def database_name
|
35
|
+
Rails.configuration.database_configuration.dig(Rails.env, "database")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tidewave
|
4
|
+
module DatabaseAdapters
|
5
|
+
class Sequel < DatabaseAdapter
|
6
|
+
RESULT_LIMIT = 50
|
7
|
+
|
8
|
+
def execute_query(query, arguments = [])
|
9
|
+
db = ::Sequel::Model.db
|
10
|
+
|
11
|
+
# Execute the query with arguments
|
12
|
+
result = if arguments.any?
|
13
|
+
db.fetch(query, *arguments)
|
14
|
+
else
|
15
|
+
db.fetch(query)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert to array of hashes and extract metadata
|
19
|
+
rows = result.all
|
20
|
+
columns = rows.first&.keys || []
|
21
|
+
|
22
|
+
# Format the result similar to ActiveRecord
|
23
|
+
{
|
24
|
+
columns: columns.map(&:to_s),
|
25
|
+
rows: rows.first(RESULT_LIMIT).map(&:values),
|
26
|
+
row_count: rows.length,
|
27
|
+
adapter: db.adapter_scheme.to_s.upcase,
|
28
|
+
database: db.opts[:database]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_base_class
|
33
|
+
::Sequel::Model
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Tidewave::ExceptionsMiddleware
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
request = ActionDispatch::Request.new(env)
|
10
|
+
status, headers, body = @app.call(env)
|
11
|
+
|
12
|
+
exception = request.get_header("tidewave.exception")
|
13
|
+
|
14
|
+
if exception
|
15
|
+
formatted = format_exception_html(exception, request)
|
16
|
+
body, headers = append_body(body, headers, formatted)
|
17
|
+
end
|
18
|
+
|
19
|
+
[ status, headers, body ]
|
20
|
+
rescue => error
|
21
|
+
Rails.logger.error("Failure in Tidewave::ExceptionsMiddleware, message: #{error.message}")
|
22
|
+
raise error
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def format_exception_html(exception, request)
|
28
|
+
backtrace = Rails.backtrace_cleaner.clean(exception.backtrace)
|
29
|
+
|
30
|
+
parameters = request_parameters(request)
|
31
|
+
|
32
|
+
text = exception.class.name.dup
|
33
|
+
|
34
|
+
if parameters["controller"]
|
35
|
+
text << " in #{parameters["controller"].camelize}Controller"
|
36
|
+
|
37
|
+
if parameters["action"]
|
38
|
+
text << "##{parameters["action"]}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
text << "\n\n## Message\n\n#{exception.message}"
|
43
|
+
|
44
|
+
if backtrace.any?
|
45
|
+
text << "\n\n## Backtrace\n\n#{backtrace.join("\n")}"
|
46
|
+
end
|
47
|
+
|
48
|
+
text << "\n\n"
|
49
|
+
|
50
|
+
text << <<~TEXT.chomp
|
51
|
+
## Request info
|
52
|
+
|
53
|
+
* URI: #{request.base_url + request.path}
|
54
|
+
* Query string: #{request.query_string}
|
55
|
+
|
56
|
+
## Session
|
57
|
+
|
58
|
+
#{request.session.to_hash.inspect}
|
59
|
+
TEXT
|
60
|
+
|
61
|
+
%Q(<textarea style="display: none;" data-tidewave-exception-info>#{ERB::Util.html_escape(text)}</textarea>)
|
62
|
+
end
|
63
|
+
|
64
|
+
def request_parameters(request)
|
65
|
+
request.parameters
|
66
|
+
rescue ActionController::BadRequest
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
|
70
|
+
def append_body(body, headers, content)
|
71
|
+
new_body = "".dup
|
72
|
+
|
73
|
+
body.each { |part| new_body << part }
|
74
|
+
body.close if body.respond_to?(:close)
|
75
|
+
|
76
|
+
if position = new_body.rindex("</body>")
|
77
|
+
new_body.insert(position, content)
|
78
|
+
else
|
79
|
+
new_body << content
|
80
|
+
end
|
81
|
+
|
82
|
+
if headers[Rack::CONTENT_LENGTH]
|
83
|
+
headers[Rack::CONTENT_LENGTH] = new_body.bytesize.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
[ [ new_body ], headers ]
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
require "ipaddr"
|
5
|
+
require "fast_mcp"
|
6
|
+
require "rack/request"
|
7
|
+
require "active_support/core_ext/class"
|
8
|
+
require "active_support/core_ext/object/blank"
|
9
|
+
require "json"
|
10
|
+
require "erb"
|
11
|
+
|
12
|
+
class Tidewave::Middleware
|
13
|
+
TIDEWAVE_ROUTE = "tidewave".freeze
|
14
|
+
EMPTY_ROUTE = "empty".freeze
|
15
|
+
SSE_ROUTE = "mcp".freeze
|
16
|
+
MESSAGES_ROUTE = "mcp/message".freeze
|
17
|
+
SHELL_ROUTE = "shell".freeze
|
18
|
+
|
19
|
+
INVALID_IP = <<~TEXT.freeze
|
20
|
+
For security reasons, Tidewave does not accept remote connections by default.
|
21
|
+
|
22
|
+
If you really want to allow remote connections, set `config.tidewave.allow_remote_access = true`.
|
23
|
+
TEXT
|
24
|
+
|
25
|
+
def initialize(app, config)
|
26
|
+
@allow_remote_access = config.allow_remote_access
|
27
|
+
@client_url = config.client_url
|
28
|
+
@team = config.team
|
29
|
+
@project_name = Rails.application.class.module_parent.name
|
30
|
+
|
31
|
+
@app = FastMcp.rack_middleware(app,
|
32
|
+
name: "tidewave",
|
33
|
+
version: Tidewave::VERSION,
|
34
|
+
path_prefix: "/" + TIDEWAVE_ROUTE,
|
35
|
+
messages_route: MESSAGES_ROUTE,
|
36
|
+
sse_route: SSE_ROUTE,
|
37
|
+
logger: config.logger || Logger.new(Rails.root.join("log", "tidewave.log")),
|
38
|
+
# Rails runs the HostAuthorization in dev, so we skip this
|
39
|
+
allowed_origins: [],
|
40
|
+
# We validate this one in Tidewave::Middleware
|
41
|
+
localhost_only: false
|
42
|
+
) do |server|
|
43
|
+
server.filter_tools do |request, tools|
|
44
|
+
if request.params["include_fs_tools"] != "true"
|
45
|
+
tools.reject { |tool| tool.tags.include?(:file_system_tool) }
|
46
|
+
else
|
47
|
+
tools
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
server.register_tools(*Tidewave::Tools::Base.descendants)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def call(env)
|
56
|
+
request = Rack::Request.new(env)
|
57
|
+
path = request.path.split("/").reject(&:empty?)
|
58
|
+
|
59
|
+
if path[0] == TIDEWAVE_ROUTE
|
60
|
+
return forbidden(INVALID_IP) unless valid_client_ip?(request)
|
61
|
+
|
62
|
+
# The MCP routes are handled downstream by FastMCP
|
63
|
+
case [ request.request_method, path ]
|
64
|
+
when [ "GET", [ TIDEWAVE_ROUTE ] ]
|
65
|
+
return home(request)
|
66
|
+
when [ "GET", [ TIDEWAVE_ROUTE, EMPTY_ROUTE ] ]
|
67
|
+
return empty(request)
|
68
|
+
when [ "POST", [ TIDEWAVE_ROUTE, SHELL_ROUTE ] ]
|
69
|
+
return shell(request)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
@app.call(env)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def home(request)
|
79
|
+
config = {
|
80
|
+
"project_name" => @project_name,
|
81
|
+
"framework_type" => "rails",
|
82
|
+
"tidewave_version" => Tidewave::VERSION,
|
83
|
+
"team" => @team
|
84
|
+
}
|
85
|
+
|
86
|
+
html = <<~HTML
|
87
|
+
<html>
|
88
|
+
<head>
|
89
|
+
<meta charset="UTF-8" />
|
90
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
91
|
+
<meta name="tidewave:config" content="#{ERB::Util.html_escape(JSON.generate(config))}" />
|
92
|
+
<script type="module" src="#{@client_url}/tc/tc.js"></script>
|
93
|
+
</head>
|
94
|
+
<body></body>
|
95
|
+
</html>
|
96
|
+
HTML
|
97
|
+
|
98
|
+
[ 200, { "Content-Type" => "text/html" }, [ html ] ]
|
99
|
+
end
|
100
|
+
|
101
|
+
def empty(request)
|
102
|
+
html = ""
|
103
|
+
[ 200, { "Content-Type" => "text/html" }, [ html ] ]
|
104
|
+
end
|
105
|
+
|
106
|
+
def forbidden(message)
|
107
|
+
Rails.logger.warn(message)
|
108
|
+
[ 403, { "Content-Type" => "text/plain" }, [ message ] ]
|
109
|
+
end
|
110
|
+
|
111
|
+
def shell(request)
|
112
|
+
body = request.body.read
|
113
|
+
return [ 400, { "Content-Type" => "text/plain" }, [ "Command body is required" ] ] if body.blank?
|
114
|
+
|
115
|
+
begin
|
116
|
+
parsed_body = JSON.parse(body)
|
117
|
+
cmd = parsed_body["command"]
|
118
|
+
return [ 400, { "Content-Type" => "text/plain" }, [ "Command field is required" ] ] if cmd.blank?
|
119
|
+
rescue JSON::ParserError
|
120
|
+
return [ 400, { "Content-Type" => "text/plain" }, [ "Invalid JSON in request body" ] ]
|
121
|
+
end
|
122
|
+
|
123
|
+
response = Rack::Response.new
|
124
|
+
response.status = 200
|
125
|
+
response.headers["Content-Type"] = "text/plain"
|
126
|
+
|
127
|
+
response.finish do |res|
|
128
|
+
begin
|
129
|
+
Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
|
130
|
+
stdin.close
|
131
|
+
|
132
|
+
# Merge stdout and stderr streams
|
133
|
+
ios = [ stdout, stderr ]
|
134
|
+
|
135
|
+
until ios.empty?
|
136
|
+
ready = IO.select(ios, nil, nil, 0.1)
|
137
|
+
next unless ready
|
138
|
+
|
139
|
+
ready[0].each do |io|
|
140
|
+
begin
|
141
|
+
data = io.read_nonblock(4096)
|
142
|
+
if data
|
143
|
+
# Write binary chunk: type (0 for data) + 4-byte length + data
|
144
|
+
chunk = [ 0, data.bytesize ].pack("CN") + data
|
145
|
+
res.write(chunk)
|
146
|
+
end
|
147
|
+
rescue IO::WaitReadable
|
148
|
+
# No data available right now
|
149
|
+
rescue EOFError
|
150
|
+
# Stream ended
|
151
|
+
ios.delete(io)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Wait for process to complete and get exit status
|
157
|
+
exit_status = wait_thr.value.exitstatus
|
158
|
+
status_json = JSON.generate({ status: exit_status })
|
159
|
+
# Write binary chunk: type (1 for status) + 4-byte length + JSON data
|
160
|
+
chunk = [ 1, status_json.bytesize ].pack("CN") + status_json
|
161
|
+
res.write(chunk)
|
162
|
+
end
|
163
|
+
rescue => e
|
164
|
+
error_json = JSON.generate({ status: 213 })
|
165
|
+
chunk = [ 1, error_json.bytesize ].pack("CN") + error_json
|
166
|
+
res.write(chunk)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def valid_client_ip?(request)
|
172
|
+
return true if @allow_remote_access
|
173
|
+
|
174
|
+
ip = request.ip
|
175
|
+
return false unless ip
|
176
|
+
|
177
|
+
addr = IPAddr.new(ip)
|
178
|
+
|
179
|
+
addr.loopback? ||
|
180
|
+
addr == IPAddr.new("127.0.0.1") ||
|
181
|
+
addr == IPAddr.new("::1") ||
|
182
|
+
addr == IPAddr.new("::ffff:127.0.0.1") # IPv4-mapped IPv6
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Tidewave::QuietRequestsMiddleware
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
if env["PATH_INFO"].start_with?("/tidewave")
|
10
|
+
Rails.logger.silence { @app.call(env) }
|
11
|
+
else
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/tidewave/railtie.rb
CHANGED
@@ -1,49 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fast_mcp"
|
4
3
|
require "logger"
|
5
4
|
require "fileutils"
|
6
5
|
require "tidewave/configuration"
|
7
|
-
require "
|
6
|
+
require "tidewave/middleware"
|
7
|
+
require "tidewave/exceptions_middleware"
|
8
|
+
require "tidewave/quiet_requests_middleware"
|
8
9
|
|
9
10
|
gem_tools_path = File.expand_path("tools/**/*.rb", __dir__)
|
10
11
|
Dir[gem_tools_path].each { |f| require f }
|
11
12
|
|
12
13
|
module Tidewave
|
13
14
|
class Railtie < Rails::Railtie
|
14
|
-
config.tidewave = Tidewave::Configuration.new
|
15
|
-
|
16
|
-
initializer "tidewave.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
config = app.config.tidewave
|
21
|
-
|
22
|
-
# Set up MCP server with the host application
|
23
|
-
FastMcp.mount_in_rails(
|
24
|
-
app,
|
25
|
-
name: "tidewave",
|
26
|
-
version: Tidewave::VERSION,
|
27
|
-
path_prefix: Tidewave::PATH_PREFIX,
|
28
|
-
messages_route: Tidewave::MESSAGES_ROUTE,
|
29
|
-
sse_route: Tidewave::SSE_ROUTE,
|
30
|
-
logger: config.logger,
|
31
|
-
allowed_origins: config.allowed_origins,
|
32
|
-
localhost_only: config.localhost_only,
|
33
|
-
allowed_ips: config.allowed_ips
|
34
|
-
) do |server|
|
35
|
-
app.config.before_initialize do
|
36
|
-
server.filter_tools do |request, tools|
|
37
|
-
if request.params["include_fs_tools"] != "true"
|
38
|
-
tools.reject { |tool| tool.tags.include?(:file_system_tool) }
|
39
|
-
else
|
40
|
-
tools
|
41
|
-
end
|
42
|
-
end
|
15
|
+
config.tidewave = Tidewave::Configuration.new()
|
16
|
+
|
17
|
+
initializer "tidewave.setup" do |app|
|
18
|
+
unless app.config.enable_reloading
|
19
|
+
raise "For security reasons, Tidewave is only supported in environments where config.enable_reloading is true (typically development)"
|
20
|
+
end
|
43
21
|
|
44
|
-
|
22
|
+
app.config.middleware.insert_after(
|
23
|
+
ActionDispatch::Callbacks,
|
24
|
+
Tidewave::Middleware,
|
25
|
+
app.config.tidewave
|
26
|
+
)
|
27
|
+
|
28
|
+
app.config.after_initialize do
|
29
|
+
# If the user configured CSP, we need to alter it in dev
|
30
|
+
# to allow TC to run browser_eval.
|
31
|
+
app.config.content_security_policy.try do |content_security_policy|
|
32
|
+
content_security_policy.directives["script-src"].try do |script_src|
|
33
|
+
script_src << "'unsafe-eval'" unless script_src.include?("'unsafe-eval'")
|
34
|
+
end
|
45
35
|
end
|
46
36
|
end
|
47
37
|
end
|
38
|
+
|
39
|
+
initializer "tidewave.intercept_exceptions" do |app|
|
40
|
+
# We intercept exceptions from DebugExceptions, format the
|
41
|
+
# information as text and inject into the exception page html.
|
42
|
+
|
43
|
+
ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
|
44
|
+
request.set_header("tidewave.exception", exception)
|
45
|
+
end
|
46
|
+
|
47
|
+
app.middleware.insert_before(ActionDispatch::DebugExceptions, Tidewave::ExceptionsMiddleware)
|
48
|
+
end
|
49
|
+
|
50
|
+
initializer "tidewave.logging" do |app|
|
51
|
+
# Do not pollute user logs with tidewave requests.
|
52
|
+
app.middleware.insert_before(Rails::Rack::Logger, Tidewave::QuietRequestsMiddleware)
|
53
|
+
end
|
48
54
|
end
|
49
55
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
|
4
4
|
tool_name "execute_sql_query"
|
5
5
|
description <<~DESCRIPTION
|
6
|
-
Executes the given SQL query against the
|
6
|
+
Executes the given SQL query against the database connection.
|
7
7
|
Returns the result as a Ruby data structure.
|
8
8
|
|
9
9
|
Note that the output is limited to 50 rows at a time. If you need to see more, perform additional calls
|
@@ -25,24 +25,6 @@ class Tidewave::Tools::ExecuteSqlQuery < Tidewave::Tools::Base
|
|
25
25
|
RESULT_LIMIT = 50
|
26
26
|
|
27
27
|
def call(query:, arguments: [])
|
28
|
-
|
29
|
-
conn = ActiveRecord::Base.connection
|
30
|
-
|
31
|
-
# Execute the query with prepared statement and arguments
|
32
|
-
if arguments.any?
|
33
|
-
result = conn.exec_query(query, "SQL", arguments)
|
34
|
-
else
|
35
|
-
result = conn.exec_query(query)
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
# Format the result
|
40
|
-
{
|
41
|
-
columns: result.columns,
|
42
|
-
rows: result.rows.first(RESULT_LIMIT),
|
43
|
-
row_count: result.rows.length,
|
44
|
-
adapter: conn.adapter_name,
|
45
|
-
database: Rails.configuration.database_configuration.dig(Rails.env, "database")
|
46
|
-
}
|
28
|
+
Tidewave::DatabaseAdapter.current.execute_query(query, arguments)
|
47
29
|
end
|
48
30
|
end
|