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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9497890ce1878eb9de6a624e8db95ecaa6e5b8e6d011e1392757cd62630d31f
4
- data.tar.gz: a6910fcd4b686d11a5eb5e99dd57cea4b9fbb5fde7120d26d387a16f7ccada13
3
+ metadata.gz: 133bbaee4fe93f387ffcca1b1a7b32e5d661ee97b5c260cafaaf51dfc0564f96
4
+ data.tar.gz: f8e4425ee88cb23810391f01c8ab087692095f424adf0888da51d61ef107e3b4
5
5
  SHA512:
6
- metadata.gz: e3f176ad0d260adecf0ebd94229ad1bd81a8ca37613efc96883c32f1bcfe057c9ad5888bce9b5ed45a25b3062842e3e037ace65a95d665d7dcc10717abfc74b1
7
- data.tar.gz: 1608b80a2888a30830ca9d2b96c3b305b32e5880702bc456b990f032d83a4b74a3594f90bcef36193b9c0885aa4fabe1d295ef9ffaf91b6ff402bc180ec009b5
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
- [![CI](https://github.com/pauloancheta/rails-mcp/actions/workflows/main.yml/badge.svg)](https://github.com/pauloancheta/rails-mcp/actions/workflows/main.yml)
3
+ [![CI](https://github.com/pauloancheta/activerecord-mcp/actions/workflows/main.yml/badge.svg)](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/rails-mcp.
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("Bearer token required") if token_string.nil?
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
- return unauthorized("Invalid or expired token") if token.nil? || token.revoked? || token.expired?
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
- return insufficient_scope(required) if required && !required.empty? && !token.scopes.include?(required)
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
- .reject { |col| RailsMcp.configuration.column_denied?(col.name) }
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.map do |assoc|
39
- { name: assoc.name.to_s, macro: assoc.macro.to_s, class_name: assoc.class_name }
40
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsMcp
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
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/rails-mcp"
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
- # frozen_string_literal: true
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.0
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-23 00:00:00.000000000 Z
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/rails-mcp
119
+ homepage: https://github.com/pauloancheta/activerecord-mcp
119
120
  licenses:
120
121
  - MIT
121
122
  metadata:
122
- homepage_uri: https://github.com/pauloancheta/rails-mcp
123
- source_code_uri: https://github.com/pauloancheta/rails-mcp
124
- changelog_uri: https://github.com/pauloancheta/rails-mcp/blob/main/CHANGELOG.md
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.4.20
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