mcp_on_ruby 2.0.1 → 2.0.2

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: ac6effb14117c29ebe4aab9f0178aa7ded445b448c80f7438956cbfc088c1934
4
- data.tar.gz: 052b0c6053db07aee0ebe0991949bae91ec268d9911b5512527b54fb2fb7c503
3
+ metadata.gz: a99b9f1fff3f7a9639c85c82826690264ca0a2032321c227a3424719d73fbdd5
4
+ data.tar.gz: a977652f5dd0e15d31142a1561f9ab9302a1f34abcfc21a759d5b4ce3600b460
5
5
  SHA512:
6
- metadata.gz: deafb7ff0545ab491e495fba46d21d27ff66e9c397d7466c1800b74206c98aa19569855148f4b0015e9cd5ce3a78bdc29b591a7599ca95b2afdf97d191b54054
7
- data.tar.gz: 5da7f45bc3087d683a1f17283709f14b3e020b8f69ab270fa00156953abec498593e843c01278644ee57bf08fca227e2df1a9745ea9e747cb9b79974e1727a9e
6
+ metadata.gz: 1b8dda36e0e1a44f7b67cbb5f1bcbfe91065ff6304c60682a289f524187532a625e1e691c99ef5bd71424ee19f35a2c9151561ffdfe53b34c1aa56919956e25e
7
+ data.tar.gz: e484ae0cee070df784e3621e63d78b982b5bd70bcd02d566f3a74127781c026a0bf683a271814936118ce67416879fd0d4bbf596646d8952d5e61c5ec3e5eea8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project are documented in this file.
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Entries from 2.0.0 onward
6
6
  are generated by [release-please](https://github.com/googleapis/release-please) from Conventional Commit PR titles.
7
7
 
8
+ ## [2.0.2](https://github.com/nagstler/mcp_on_ruby/compare/v2.0.1...v2.0.2) (2026-10-31)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * leave belongs_to foreign keys out of scaffolded params ([#37](https://github.com/nagstler/mcp_on_ruby/issues/37)) ([7b7ec5b](https://github.com/nagstler/mcp_on_ruby/commit/7b7ec5bbe40f0dfeec88da94c9deef6de90167bc))
14
+ * report redirects to a sign-in page as not authenticated ([#38](https://github.com/nagstler/mcp_on_ruby/issues/38)) ([949a340](https://github.com/nagstler/mcp_on_ruby/commit/949a34059e925bdad17586cf5bfdab0a68aca031))
15
+
8
16
  ## [2.0.1](https://github.com/nagstler/mcp_on_ruby/compare/v2.0.0...v2.0.1) (2026-10-31)
9
17
 
10
18
 
data/README.md CHANGED
@@ -172,7 +172,30 @@ McpOnRuby.configure do |config|
172
172
  end
173
173
  ```
174
174
 
175
- For Devise and Warden, the user is signed in with the `:user` scope (change it with `config.warden_scope`). Apps with custom authentication can read the stdio user with `mcp_user` in their own `before_action`.
175
+ For Devise and Warden, the user is signed in with the `:user` scope (change it with `config.warden_scope`). Other apps read the stdio user with `mcp_user`.
176
+
177
+ **Rails 8 authentication** (`bin/rails generate authentication`) uses neither Warden nor tokens. Teach its concern about API tokens for HTTP clients and about `mcp_user` for stdio:
178
+
179
+ ```ruby
180
+ # app/controllers/concerns/authentication.rb
181
+ def resume_session
182
+ Current.session ||= find_session_by_cookie || find_session_by_token || find_session_for_mcp
183
+ end
184
+
185
+ # HTTP clients send `Authorization: Bearer <api_token>` (add `has_secure_token :api_token` to User).
186
+ def find_session_by_token
187
+ authenticate_with_http_token do |token, _options|
188
+ User.find_by(api_token: token)&.sessions&.new(user_agent: request.user_agent, ip_address: request.remote_ip)
189
+ end
190
+ end
191
+
192
+ # `bin/rails mcp:stdio` sessions act as config.stdio_user.
193
+ def find_session_for_mcp
194
+ mcp_user&.sessions&.new
195
+ end
196
+ ```
197
+
198
+ The sessions are built but never saved, so tool calls don't add rows to `sessions` or set cookies.
176
199
 
177
200
  If authentication fails, the agent gets a clear "Not authenticated (401)" tool error and nothing runs.
178
201
 
@@ -209,7 +232,8 @@ See [SECURITY.md](SECURITY.md) for the full threat model.
209
232
  | `401` / `403` / `404` | Tool error: not authenticated / not permitted / not found |
210
233
  | `422` with JSON | Tool error including your validation messages, so the agent can correct itself |
211
234
  | Other `4xx` | Tool error with the status, plus the body when it is JSON |
212
- | `3xx` | Tool error naming the redirect target (redirects are not followed) |
235
+ | `3xx` to a sign-in page | Tool error: not authenticated (Rails 8 authentication and Devise redirect unauthenticated requests) |
236
+ | Other `3xx` | Tool error naming the redirect target (redirects are not followed) |
213
237
  | HTML | Tool error asking for JSON. Use `respond_to` with `format.json`. |
214
238
  | `5xx` or an exception | Generic tool error. Details are reported to `Rails.error`, never sent. |
215
239
 
@@ -265,19 +289,26 @@ claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
265
289
  <details>
266
290
  <summary><b>Claude Desktop</b></summary>
267
291
 
268
- Claude Desktop does not load your shell profile, so start Rails through a login shell to pick up rbenv, asdf, or chruby:
292
+ Claude Desktop starts servers without your shell environment, so rbenv, asdf, and mise aren't loaded. Point it at your app's Ruby directly, and use `-C` to run from the app's directory:
293
+
294
+ ```bash
295
+ # In your app directory, print the Ruby to use:
296
+ ruby -e 'puts RbConfig.ruby'
297
+ ```
269
298
 
270
299
  ```json
271
300
  {
272
301
  "mcpServers": {
273
302
  "my-app": {
274
- "command": "bash",
275
- "args": ["-lc", "cd /path/to/my-app && bin/rails mcp:stdio"],
303
+ "command": "/Users/you/.rbenv/versions/3.4.7/bin/ruby",
304
+ "args": ["-C", "/path/to/my-app", "bin/rails", "mcp:stdio"],
276
305
  "env": { "MCP_USER": "alice@example.com" }
277
306
  }
278
307
  }
279
308
  }
280
309
  ```
310
+
311
+ Update `command` when you upgrade Ruby.
281
312
  </details>
282
313
 
283
314
  <details>
@@ -90,12 +90,19 @@ module McpOnRuby
90
90
  }.fetch(action) { "TODO: describe what #{action} does for the agent" }
91
91
  end
92
92
 
93
+ # Foreign keys of belongs_to associations usually come from the signed-in user or the URL, not the agent.
94
+ def owner_keys(model)
95
+ return [] unless model.respond_to?(:reflect_on_all_associations)
96
+
97
+ model.reflect_on_all_associations(:belongs_to).map { |association| association.foreign_key.to_s }
98
+ end
99
+
93
100
  # A params block built from the model's columns, when the controller has an ActiveRecord model.
94
101
  def model_params
95
102
  model = controller.split("/").last.classify.safe_constantize
96
103
  return unless model.respond_to?(:columns_hash)
97
104
 
98
- columns = model.columns_hash.except(model.primary_key, *SKIPPED_COLUMNS)
105
+ columns = model.columns_hash.except(model.primary_key, *SKIPPED_COLUMNS, *owner_keys(model))
99
106
  return if columns.empty?
100
107
 
101
108
  lines = columns.map do |name, column|
@@ -6,6 +6,10 @@ module McpOnRuby
6
6
  JSON_TYPE = %r{\Aapplication/(?:[\w.+-]+\+)?json\b}i
7
7
  HTML_TYPE = %r{\Atext/html\b}i
8
8
 
9
+ # Sign-in pages apps redirect unauthenticated requests to: Rails 8 authentication (/session/new),
10
+ # Devise (/users/sign_in), and common /login and /signin routes.
11
+ SIGN_IN_PATH = %r{/(?:session/new|sign_?in|log_?in)/?\z}i
12
+
9
13
  module_function
10
14
 
11
15
  def from_rack(status, headers, body)
@@ -15,8 +19,7 @@ module McpOnRuby
15
19
 
16
20
  case status
17
21
  when 200..299 then success(status, text, json, content_type)
18
- when 300..399 then error(status, "Redirected to #{header(headers, "location") || "another page"}; " \
19
- "redirects are not followed")
22
+ when 300..399 then redirect(status, header(headers, "location"))
20
23
  when 400..499 then client_error(status, text, json)
21
24
  else error(status, "Internal server error (#{status})")
22
25
  end
@@ -36,6 +39,16 @@ module McpOnRuby
36
39
  text_response(text)
37
40
  end
38
41
 
42
+ def redirect(status, location)
43
+ if location && URI(location).path.to_s.match?(SIGN_IN_PATH)
44
+ error(status, "Not authenticated: the app redirected to its sign-in page (#{location})")
45
+ else
46
+ error(status, "Redirected to #{location || "another page"}; redirects are not followed")
47
+ end
48
+ rescue URI::InvalidURIError
49
+ error(status, "Redirected to #{location}; redirects are not followed")
50
+ end
51
+
39
52
  def client_error(status, text, json)
40
53
  message = {
41
54
  401 => "Not authenticated (401)",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module McpOnRuby
4
- VERSION = "2.0.1"
4
+ VERSION = "2.0.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp_on_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nagendra Dhanakeerthi