rails-pg-extras-mcp 0.2.4 → 0.2.6
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/lib/rails-pg-extras-mcp.rb +37 -24
- data/lib/rails_pg_extras_mcp/validate_query.rb +50 -0
- data/lib/rails_pg_extras_mcp/version.rb +1 -1
- data/rails-pg-extras-mcp.gemspec +1 -0
- data/spec/smoke_spec.rb +1 -1
- data/spec/validate_query_spec.rb +66 -0
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b7ef071e268c78304c699be7ffef21a2797bff1eac7d16223b8383b7112d755
|
|
4
|
+
data.tar.gz: fb974d7e4ce85b429c8a7b29cbf4c1a2425132afd326a81bc3500d5577c4db81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ad442345f1c4879c0310ebe6f0fbc6f3c431176bfd146fb86cda194f50b34a89599c202d5683cf27a52e5a4e0dac9db76ad08d8a1decb5d97ebce68b91adc57
|
|
7
|
+
data.tar.gz: 22e94431fdad9ba2f87a37ab3741592f8ff3074b254c9e09052a9f2f2a6a0fee463f89afe981fb5777aa745ffe3d1a3b9ba9a2d3d56bfae94fff69c0c199b831
|
data/lib/rails-pg-extras-mcp.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "rack"
|
|
|
5
5
|
require "ruby-pg-extras"
|
|
6
6
|
require "rails-pg-extras"
|
|
7
7
|
require "rails_pg_extras_mcp/version"
|
|
8
|
+
require "rails_pg_extras_mcp/validate_query"
|
|
8
9
|
|
|
9
10
|
SKIP_QUERIES = %i[
|
|
10
11
|
add_extensions
|
|
@@ -65,31 +66,19 @@ class DiagnoseTool < FastMcp::Tool
|
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
class ExplainBaseTool < FastMcp::Tool
|
|
68
|
-
|
|
69
|
-
delete,
|
|
70
|
-
insert,
|
|
71
|
-
update,
|
|
72
|
-
truncate,
|
|
73
|
-
drop,
|
|
74
|
-
alter,
|
|
75
|
-
create,
|
|
76
|
-
grant,
|
|
77
|
-
begin,
|
|
78
|
-
commit
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
def call(sql_query:)
|
|
69
|
+
def call(sql_query: nil)
|
|
82
70
|
connection = RailsPgExtras.connection
|
|
83
71
|
|
|
84
|
-
if DENYLIST.any? { |deny| sql_query.downcase.include?(deny) }
|
|
85
|
-
raise "This query is not allowed. It contains a denied keyword. Denylist: #{DENYLIST.join(", ")}"
|
|
86
|
-
end
|
|
87
72
|
|
|
88
|
-
connection.execute("BEGIN")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
73
|
+
connection.execute("BEGIN;")
|
|
74
|
+
begin
|
|
75
|
+
result = connection.execute("#{sql_query}")
|
|
76
|
+
connection.execute("ROLLBACK;")
|
|
77
|
+
result.to_a
|
|
78
|
+
rescue => e
|
|
79
|
+
connection.execute("ROLLBACK;") rescue nil
|
|
80
|
+
raise e
|
|
81
|
+
end
|
|
93
82
|
end
|
|
94
83
|
end
|
|
95
84
|
|
|
@@ -105,8 +94,14 @@ class ExplainTool < ExplainBaseTool
|
|
|
105
94
|
end
|
|
106
95
|
|
|
107
96
|
def call(sql_query:)
|
|
108
|
-
if sql_query.
|
|
109
|
-
|
|
97
|
+
if sql_query.to_s.empty?
|
|
98
|
+
return "sql_query param is required"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
ValidateQuery.new(sql_query).call
|
|
103
|
+
rescue ValidateQuery::InvalidQueryError => e
|
|
104
|
+
return e.message
|
|
110
105
|
end
|
|
111
106
|
|
|
112
107
|
super(sql_query: "EXPLAIN #{sql_query}")
|
|
@@ -125,6 +120,16 @@ class ExplainAnalyzeTool < ExplainBaseTool
|
|
|
125
120
|
end
|
|
126
121
|
|
|
127
122
|
def call(sql_query:)
|
|
123
|
+
if sql_query.to_s.empty?
|
|
124
|
+
return "sql_query param is required"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
ValidateQuery.new(sql_query).call
|
|
129
|
+
rescue ValidateQuery::InvalidQueryError => e
|
|
130
|
+
return e.message
|
|
131
|
+
end
|
|
132
|
+
|
|
128
133
|
super(sql_query: "EXPLAIN ANALYZE #{sql_query}")
|
|
129
134
|
end
|
|
130
135
|
end
|
|
@@ -137,6 +142,10 @@ class IndexInfoTool < FastMcp::Tool
|
|
|
137
142
|
end
|
|
138
143
|
|
|
139
144
|
def call(table_name:)
|
|
145
|
+
if table_name.to_s.empty?
|
|
146
|
+
return "table_name param is required"
|
|
147
|
+
end
|
|
148
|
+
|
|
140
149
|
RailsPgExtras.index_info(args: { table_name: table_name }, in_format: :hash)
|
|
141
150
|
end
|
|
142
151
|
|
|
@@ -153,6 +162,10 @@ class TableInfoTool < FastMcp::Tool
|
|
|
153
162
|
end
|
|
154
163
|
|
|
155
164
|
def call(table_name:)
|
|
165
|
+
if table_name.to_s.empty?
|
|
166
|
+
return "table_name param is required"
|
|
167
|
+
end
|
|
168
|
+
|
|
156
169
|
RailsPgExtras.table_info(args: { table_name: table_name }, in_format: :hash)
|
|
157
170
|
end
|
|
158
171
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pg_query'
|
|
4
|
+
|
|
5
|
+
class ValidateQuery
|
|
6
|
+
InvalidQueryError = Class.new(StandardError)
|
|
7
|
+
|
|
8
|
+
DENYLIST = %w[
|
|
9
|
+
delete
|
|
10
|
+
insert
|
|
11
|
+
update
|
|
12
|
+
truncate
|
|
13
|
+
drop
|
|
14
|
+
alter
|
|
15
|
+
create
|
|
16
|
+
grant
|
|
17
|
+
begin
|
|
18
|
+
commit
|
|
19
|
+
explain
|
|
20
|
+
analyze
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def initialize(sql_query)
|
|
24
|
+
@sql_query = sql_query.to_s.strip
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call
|
|
28
|
+
raise InvalidQueryError, "Query is empty" if @sql_query.empty?
|
|
29
|
+
|
|
30
|
+
if DENYLIST.any? { |word| @sql_query.downcase.include?(word) }
|
|
31
|
+
raise InvalidQueryError, "Query contains denied keyword. Denylist: #{DENYLIST.join(', ')}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
tree = PgQuery.parse(@sql_query)
|
|
36
|
+
rescue PgQuery::ParseError => e
|
|
37
|
+
raise InvalidQueryError, "Invalid SQL syntax: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if tree.tree.stmts.size > 1
|
|
41
|
+
raise InvalidQueryError, "Multiple SQL statements are not allowed"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless tree.tree.stmts.all? { |stmt| stmt.stmt.select_stmt }
|
|
45
|
+
raise InvalidQueryError, "Only SELECT statements are allowed"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
end
|
data/rails-pg-extras-mcp.gemspec
CHANGED
|
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
|
|
|
18
18
|
s.add_dependency "rails-pg-extras", "~> 5.6.12"
|
|
19
19
|
s.add_dependency "rails"
|
|
20
20
|
s.add_dependency "fast-mcp"
|
|
21
|
+
s.add_dependency "pg_query"
|
|
21
22
|
s.add_development_dependency "rake"
|
|
22
23
|
s.add_development_dependency "rspec"
|
|
23
24
|
s.add_development_dependency "rufo"
|
data/spec/smoke_spec.rb
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "rails-pg-extras-mcp"
|
|
5
|
+
|
|
6
|
+
RSpec.describe ValidateQuery do
|
|
7
|
+
subject { described_class.new(query) }
|
|
8
|
+
|
|
9
|
+
describe "#call" do
|
|
10
|
+
context "with a valid SELECT query" do
|
|
11
|
+
let(:query) { "SELECT * FROM users" }
|
|
12
|
+
|
|
13
|
+
it "returns true" do
|
|
14
|
+
expect(subject.call).to eq(true)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context "with nested SELECT statements" do
|
|
19
|
+
let(:query) { "SELECT * FROM users WHERE id IN (SELECT user_id FROM posts WHERE published = true)" }
|
|
20
|
+
|
|
21
|
+
it "returns true" do
|
|
22
|
+
expect(subject.call).to eq(true)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context "with multiple statements" do
|
|
27
|
+
let(:query) { "SELECT * FROM users; SELECT * FROM posts" }
|
|
28
|
+
|
|
29
|
+
it "raises an error" do
|
|
30
|
+
expect { subject.call }.to raise_error(ValidateQuery::InvalidQueryError, /Multiple SQL statements/)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context "with a denied keyword" do
|
|
35
|
+
let(:query) { "DROP TABLE users" }
|
|
36
|
+
|
|
37
|
+
it "raises an error" do
|
|
38
|
+
expect { subject.call }.to raise_error(ValidateQuery::InvalidQueryError, /denied keyword/)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context "with a non-SELECT query" do
|
|
43
|
+
let(:query) { "EXPLAIN SELECT * FROM users" }
|
|
44
|
+
|
|
45
|
+
it "raises an error" do
|
|
46
|
+
expect { subject.call }.to raise_error(ValidateQuery::InvalidQueryError)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context "with invalid SQL syntax" do
|
|
51
|
+
let(:query) { "SELECT FROM WHERE" }
|
|
52
|
+
|
|
53
|
+
it "raises an error" do
|
|
54
|
+
expect { subject.call }.to raise_error(ValidateQuery::InvalidQueryError, /Invalid SQL syntax/)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context "with an empty query" do
|
|
59
|
+
let(:query) { " " }
|
|
60
|
+
|
|
61
|
+
it "raises an error" do
|
|
62
|
+
expect { subject.call }.to raise_error(ValidateQuery::InvalidQueryError, /Query is empty/)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-pg-extras-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- pawurb
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-07-
|
|
11
|
+
date: 2025-07-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails-pg-extras
|
|
@@ -52,6 +52,20 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: pg_query
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
70
|
name: rake
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -110,11 +124,13 @@ files:
|
|
|
110
124
|
- README.md
|
|
111
125
|
- Rakefile
|
|
112
126
|
- lib/rails-pg-extras-mcp.rb
|
|
127
|
+
- lib/rails_pg_extras_mcp/validate_query.rb
|
|
113
128
|
- lib/rails_pg_extras_mcp/version.rb
|
|
114
129
|
- pg-extras-mcp.png
|
|
115
130
|
- rails-pg-extras-mcp.gemspec
|
|
116
131
|
- spec/smoke_spec.rb
|
|
117
132
|
- spec/spec_helper.rb
|
|
133
|
+
- spec/validate_query_spec.rb
|
|
118
134
|
homepage: http://github.com/pawurb/rails-pg-extras-mcp
|
|
119
135
|
licenses:
|
|
120
136
|
- MIT
|
|
@@ -142,3 +158,4 @@ summary: MCP interface for rails-pg-extras
|
|
|
142
158
|
test_files:
|
|
143
159
|
- spec/smoke_spec.rb
|
|
144
160
|
- spec/spec_helper.rb
|
|
161
|
+
- spec/validate_query_spec.rb
|