activerecord-mcp 0.1.0 → 0.1.1
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/.ruby-version +1 -0
- data/CHANGELOG.md +8 -0
- data/README.md +2 -2
- data/lib/rails_mcp/auth/token_validator.rb +13 -3
- data/lib/rails_mcp/database/model_resolver.rb +4 -0
- data/lib/rails_mcp/tools/describe_model.rb +12 -4
- data/lib/rails_mcp/version.rb +1 -1
- data/rails-mcp.gemspec +1 -1
- data/test/tmp/generator_output/config/initializers/rails_mcp.rb +1 -69
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 133bbaee4fe93f387ffcca1b1a7b32e5d661ee97b5c260cafaaf51dfc0564f96
|
|
4
|
+
data.tar.gz: f8e4425ee88cb23810391f01c8ab087692095f424adf0888da51d61ef107e3b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 452333fc726756a40787d8d02d6ca353d0464fb289db354543eef426283d5ad6e4c5bffd1b914e6f9f86e81db3e38558c61ef8685d917ba51950e5f8a94ea89f
|
|
7
|
+
data.tar.gz: cd72dc9e527051cc0cb47f74db4e7f08eb928f7ae13218a69415466580ece88a2bbf9cb1ef2b4b111d5197a0bbc3c3573f021c6ea54792fcc3d61cd433a30158
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.3.4
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.1] - 2026-05-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `describe_model` now routes column and association output through `ColumnPolicy`, so `schema_file` allowlists are correctly respected (previously all columns were exposed regardless of allowlist)
|
|
14
|
+
- Associations pointing to denied or inaccessible models are now filtered from `describe_model` output
|
|
15
|
+
- Auth failures (invalid token, insufficient scope) are now logged via `Rails.logger.warn` with request method, path, client IP, and rejection reason
|
|
16
|
+
|
|
9
17
|
## [0.1.0] - 2026-05-23
|
|
10
18
|
|
|
11
19
|
### Added
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# activerecord-mcp
|
|
2
2
|
|
|
3
|
-
[](https://github.com/pauloancheta/activerecord-mcp/actions/workflows/main.yml)
|
|
4
4
|
|
|
5
5
|
The read-only MCP server Rails developers have been looking for.
|
|
6
6
|
|
|
@@ -130,4 +130,4 @@ bundle install
|
|
|
130
130
|
bundle exec rake test
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
Bug reports and pull requests welcome at https://github.com/pauloancheta/
|
|
133
|
+
Bug reports and pull requests welcome at https://github.com/pauloancheta/activerecord-mcp.
|
|
@@ -17,13 +17,17 @@ module RailsMcp
|
|
|
17
17
|
return @app.call(env) if request.path.start_with?(WELL_KNOWN_PREFIX)
|
|
18
18
|
|
|
19
19
|
token_string = extract_bearer_token(env)
|
|
20
|
-
return unauthorized
|
|
20
|
+
return log_and_reject(request, :unauthorized, "Bearer token required") if token_string.nil?
|
|
21
21
|
|
|
22
22
|
token = Doorkeeper::AccessToken.by_token(token_string)
|
|
23
|
-
|
|
23
|
+
if token.nil? || token.revoked? || token.expired?
|
|
24
|
+
return log_and_reject(request, :unauthorized, "Invalid or expired token")
|
|
25
|
+
end
|
|
24
26
|
|
|
25
27
|
required = RailsMcp.configuration.scope
|
|
26
|
-
|
|
28
|
+
if required && !required.empty? && !token.scopes.include?(required)
|
|
29
|
+
return log_and_reject(request, :insufficient_scope, required)
|
|
30
|
+
end
|
|
27
31
|
|
|
28
32
|
env["rails_mcp.access_token"] = token
|
|
29
33
|
@app.call(env)
|
|
@@ -38,6 +42,12 @@ module RailsMcp
|
|
|
38
42
|
auth.delete_prefix("Bearer ").strip
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
def log_and_reject(request, type, detail)
|
|
46
|
+
Rails.logger.warn "[activerecord-mcp] Auth rejected #{request.request_method} #{request.path} " \
|
|
47
|
+
"ip=#{request.ip} reason=#{type} detail=#{detail.inspect}"
|
|
48
|
+
type == :insufficient_scope ? insufficient_scope(detail) : unauthorized(detail)
|
|
49
|
+
end
|
|
50
|
+
|
|
41
51
|
def unauthorized(message)
|
|
42
52
|
body = { error: "invalid_token", error_description: message }.to_json
|
|
43
53
|
[
|
|
@@ -37,6 +37,10 @@ module RailsMcp
|
|
|
37
37
|
raise AccessDenied, "Model #{klass.name} is not accessible" unless accessible?(klass)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def self.accessible_model?(klass)
|
|
41
|
+
accessible?(klass)
|
|
42
|
+
end
|
|
43
|
+
|
|
40
44
|
private_class_method def self.accessible?(klass)
|
|
41
45
|
# Schema file takes precedence over allowed_models/denied_models
|
|
42
46
|
schema = RailsMcp.schema_config
|
|
@@ -29,15 +29,23 @@ module RailsMcp
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def self.column_info(klass)
|
|
32
|
+
allowed = Database::ColumnPolicy.allowed_for(klass)
|
|
32
33
|
klass.columns
|
|
33
|
-
.
|
|
34
|
+
.select { |col| allowed.include?(col.name) }
|
|
34
35
|
.map { |col| { name: col.name, type: col.type.to_s, null: col.null, default: col.default } }
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def self.association_info(klass)
|
|
38
|
-
klass.reflect_on_all_associations
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
klass.reflect_on_all_associations
|
|
40
|
+
.reject { |assoc| denied_association?(assoc) }
|
|
41
|
+
.map { |assoc| { name: assoc.name.to_s, macro: assoc.macro.to_s, class_name: assoc.class_name } }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.denied_association?(assoc)
|
|
45
|
+
klass = assoc.class_name.safe_constantize
|
|
46
|
+
return false unless klass && klass < ActiveRecord::Base
|
|
47
|
+
|
|
48
|
+
!Database::ModelResolver.accessible_model?(klass)
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
private_class_method :column_info, :association_info
|
data/lib/rails_mcp/version.rb
CHANGED
data/rails-mcp.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = "A Rails Engine that implements a Model Context Protocol (MCP) server using " \
|
|
13
13
|
"HTTP-only Streamable HTTP transport. Provides built-in ActiveRecord query tools " \
|
|
14
14
|
"with configurable database roles, field filtering, and OAuth 2.1 + PKCE auth via Doorkeeper."
|
|
15
|
-
spec.homepage = "https://github.com/pauloancheta/
|
|
15
|
+
spec.homepage = "https://github.com/pauloancheta/activerecord-mcp"
|
|
16
16
|
spec.license = "MIT"
|
|
17
17
|
spec.required_ruby_version = ">= 3.1.0"
|
|
18
18
|
|
|
@@ -1,69 +1 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
RailsMcp.configure do |config|
|
|
4
|
-
# The database role used for every query.
|
|
5
|
-
# Queries run via ActiveRecord's connected_to(role:), so this maps directly
|
|
6
|
-
# to a role defined in your database.yml. The default :reading role works
|
|
7
|
-
# out of the box with Rails' standard replica setup. Set to :writing if
|
|
8
|
-
# your app uses a single database with no named roles.
|
|
9
|
-
#
|
|
10
|
-
# config.database_role = :reading
|
|
11
|
-
|
|
12
|
-
# Columns returned when no fields are specified in a tool call.
|
|
13
|
-
# These are also automatically included when a schema_file is configured,
|
|
14
|
-
# even if the file omits them.
|
|
15
|
-
#
|
|
16
|
-
# config.default_fields = [:id, :created_at, :updated_at]
|
|
17
|
-
|
|
18
|
-
# Allowlist of model names the MCP can access.
|
|
19
|
-
# When non-empty, any model not in this list returns an error.
|
|
20
|
-
# Ignored when schema_file is set — the file's model list takes precedence.
|
|
21
|
-
#
|
|
22
|
-
# config.allowed_models = %w[User Post Order]
|
|
23
|
-
|
|
24
|
-
# Denylist of model names that are never accessible, regardless of allowed_models.
|
|
25
|
-
# Ignored when schema_file is set.
|
|
26
|
-
#
|
|
27
|
-
# config.denied_models = %w[AdminUser AuditLog]
|
|
28
|
-
|
|
29
|
-
# Columns that are completely invisible across all models and all tools.
|
|
30
|
-
# Accepts exact strings and/or regexes. Matching columns cannot be returned,
|
|
31
|
-
# used in conditions, or used in order — even if they appear in schema_file.
|
|
32
|
-
# Applied as the final layer, so it always wins over every other config.
|
|
33
|
-
#
|
|
34
|
-
# config.denied_columns = [
|
|
35
|
-
# "password_digest",
|
|
36
|
-
# "encrypted_password",
|
|
37
|
-
# /token/i,
|
|
38
|
-
# /secret/i,
|
|
39
|
-
# /api_key/i,
|
|
40
|
-
# ]
|
|
41
|
-
|
|
42
|
-
# Maximum number of records a single query_records call can return.
|
|
43
|
-
# Client-supplied limit values are silently capped to this. Nil or zero
|
|
44
|
-
# limits also resolve to this value.
|
|
45
|
-
#
|
|
46
|
-
# config.max_limit = 100
|
|
47
|
-
|
|
48
|
-
# Maximum offset value accepted by query_records.
|
|
49
|
-
# Unlike max_limit, exceeding this raises an error rather than silently
|
|
50
|
-
# clamping — a clamped offset would return the wrong page without any
|
|
51
|
-
# indication to the caller.
|
|
52
|
-
#
|
|
53
|
-
# config.max_offset = 10_000
|
|
54
|
-
|
|
55
|
-
# Path to a YAML file that defines exactly which models and columns are
|
|
56
|
-
# accessible. When set, allowed_models and denied_models are ignored —
|
|
57
|
-
# the file's model list is the authoritative allowlist. id, created_at,
|
|
58
|
-
# and updated_at are still auto-included from default_fields even if
|
|
59
|
-
# omitted from the file. denied_columns still applies on top.
|
|
60
|
-
#
|
|
61
|
-
# config.schema_file = Rails.root.join("config/rails_mcp.yml")
|
|
62
|
-
|
|
63
|
-
# OAuth scope that every Bearer token must include. Tokens that are
|
|
64
|
-
# otherwise valid (not expired, not revoked) but lack this scope are
|
|
65
|
-
# rejected with 403 insufficient_scope. Set to nil to disable the check.
|
|
66
|
-
# Your Doorkeeper config must declare the same scope via optional_scopes.
|
|
67
|
-
#
|
|
68
|
-
# config.scope = "mcp"
|
|
69
|
-
end
|
|
1
|
+
# existing content
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Ancheta
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: doorkeeper
|
|
@@ -63,6 +63,7 @@ extensions: []
|
|
|
63
63
|
extra_rdoc_files: []
|
|
64
64
|
files:
|
|
65
65
|
- ".rubocop.yml"
|
|
66
|
+
- ".ruby-version"
|
|
66
67
|
- CHANGELOG.md
|
|
67
68
|
- LICENSE
|
|
68
69
|
- README.md
|
|
@@ -115,13 +116,13 @@ files:
|
|
|
115
116
|
- test/unit/tools/find_record_test.rb
|
|
116
117
|
- test/unit/tools/list_models_test.rb
|
|
117
118
|
- test/unit/tools/query_records_test.rb
|
|
118
|
-
homepage: https://github.com/pauloancheta/
|
|
119
|
+
homepage: https://github.com/pauloancheta/activerecord-mcp
|
|
119
120
|
licenses:
|
|
120
121
|
- MIT
|
|
121
122
|
metadata:
|
|
122
|
-
homepage_uri: https://github.com/pauloancheta/
|
|
123
|
-
source_code_uri: https://github.com/pauloancheta/
|
|
124
|
-
changelog_uri: https://github.com/pauloancheta/
|
|
123
|
+
homepage_uri: https://github.com/pauloancheta/activerecord-mcp
|
|
124
|
+
source_code_uri: https://github.com/pauloancheta/activerecord-mcp
|
|
125
|
+
changelog_uri: https://github.com/pauloancheta/activerecord-mcp/blob/main/CHANGELOG.md
|
|
125
126
|
rubygems_mfa_required: 'true'
|
|
126
127
|
post_install_message:
|
|
127
128
|
rdoc_options: []
|
|
@@ -138,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
139
|
- !ruby/object:Gem::Version
|
|
139
140
|
version: '0'
|
|
140
141
|
requirements: []
|
|
141
|
-
rubygems_version: 3.
|
|
142
|
+
rubygems_version: 3.5.11
|
|
142
143
|
signing_key:
|
|
143
144
|
specification_version: 4
|
|
144
145
|
summary: MCP server for Rails apps — safe, role-aware database query tools over Streamable
|