rails-pg-extras-mcp 0.2.5 → 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 +28 -22
- 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,28 +66,9 @@ 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
|
-
|
88
|
-
# Prevent multiple queries in one request
|
89
|
-
sql_query = sql_query.gsub(/;/, "")
|
90
72
|
|
91
73
|
connection.execute("BEGIN;")
|
92
74
|
begin
|
@@ -112,8 +94,14 @@ class ExplainTool < ExplainBaseTool
|
|
112
94
|
end
|
113
95
|
|
114
96
|
def call(sql_query:)
|
115
|
-
if sql_query.
|
116
|
-
|
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
|
117
105
|
end
|
118
106
|
|
119
107
|
super(sql_query: "EXPLAIN #{sql_query}")
|
@@ -132,6 +120,16 @@ class ExplainAnalyzeTool < ExplainBaseTool
|
|
132
120
|
end
|
133
121
|
|
134
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
|
+
|
135
133
|
super(sql_query: "EXPLAIN ANALYZE #{sql_query}")
|
136
134
|
end
|
137
135
|
end
|
@@ -144,6 +142,10 @@ class IndexInfoTool < FastMcp::Tool
|
|
144
142
|
end
|
145
143
|
|
146
144
|
def call(table_name:)
|
145
|
+
if table_name.to_s.empty?
|
146
|
+
return "table_name param is required"
|
147
|
+
end
|
148
|
+
|
147
149
|
RailsPgExtras.index_info(args: { table_name: table_name }, in_format: :hash)
|
148
150
|
end
|
149
151
|
|
@@ -160,6 +162,10 @@ class TableInfoTool < FastMcp::Tool
|
|
160
162
|
end
|
161
163
|
|
162
164
|
def call(table_name:)
|
165
|
+
if table_name.to_s.empty?
|
166
|
+
return "table_name param is required"
|
167
|
+
end
|
168
|
+
|
163
169
|
RailsPgExtras.table_info(args: { table_name: table_name }, in_format: :hash)
|
164
170
|
end
|
165
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
|