mcp_on_ruby 2.0.0 → 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 +4 -4
- data/CHANGELOG.md +18 -8
- data/LICENSE.txt +1 -1
- data/README.md +43 -21
- data/lib/generators/mcp_on_ruby/expose/expose_generator.rb +8 -1
- data/lib/mcp_on_ruby/engine.rb +8 -3
- data/lib/mcp_on_ruby/result.rb +15 -2
- data/lib/mcp_on_ruby/server_factory.rb +8 -4
- data/lib/mcp_on_ruby/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a99b9f1fff3f7a9639c85c82826690264ca0a2032321c227a3424719d73fbdd5
|
|
4
|
+
data.tar.gz: a977652f5dd0e15d31142a1561f9ab9302a1f34abcfc21a759d5b4ce3600b460
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b8dda36e0e1a44f7b67cbb5f1bcbfe91065ff6304c60682a289f524187532a625e1e691c99ef5bd71424ee19f35a2c9151561ffdfe53b34c1aa56919956e25e
|
|
7
|
+
data.tar.gz: e484ae0cee070df784e3621e63d78b982b5bd70bcd02d566f3a74127781c026a0bf683a271814936118ce67416879fd0d4bbf596646d8952d5e61c5ec3e5eea8
|
data/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Entries from 2.0.0 onward
|
|
6
|
+
are generated by [release-please](https://github.com/googleapis/release-please) from Conventional Commit PR titles.
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [2.0.2](https://github.com/nagstler/mcp_on_ruby/compare/v2.0.1...v2.0.2) (2026-10-31)
|
|
9
9
|
|
|
10
|
-
|
|
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
|
+
|
|
16
|
+
## [2.0.1](https://github.com/nagstler/mcp_on_ruby/compare/v2.0.0...v2.0.1) (2026-10-31)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* reuse tool classes when writes are disabled ([#33](https://github.com/nagstler/mcp_on_ruby/issues/33)) ([44be48c](https://github.com/nagstler/mcp_on_ruby/commit/44be48cafcb508a559b718f676e27990277bd41b))
|
|
22
|
+
* validate declarations once routes are loaded ([#32](https://github.com/nagstler/mcp_on_ruby/issues/32)) ([bf96834](https://github.com/nagstler/mcp_on_ruby/commit/bf968346f9131c6f898bdcf152726973079f3429))
|
|
23
|
+
|
|
24
|
+
## [2.0.0](https://github.com/nagstler/mcp_on_ruby/compare/v1.0.0...v2.0.0) (2026-10-31)
|
|
11
25
|
|
|
12
26
|
MCP on Ruby is now a Rails engine that exposes existing controller actions as MCP tools,
|
|
13
27
|
built on the official [`mcp`](https://github.com/modelcontextprotocol/ruby-sdk) gem.
|
|
@@ -99,7 +113,3 @@ built on the official [`mcp`](https://github.com/modelcontextprotocol/ruby-sdk)
|
|
|
99
113
|
- Resource management
|
|
100
114
|
- Prompt handling
|
|
101
115
|
- Root filesystem access
|
|
102
|
-
|
|
103
|
-
[Unreleased]: https://github.com/nagstler/mcp_on_ruby/compare/v2.0.0...HEAD
|
|
104
|
-
[2.0.0]: https://github.com/nagstler/mcp_on_ruby/compare/v1.0.0...v2.0.0
|
|
105
|
-
[1.0.0]: https://github.com/nagstler/mcp_on_ruby/releases/tag/v1.0.0
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -107,21 +107,12 @@ Then ask: *"Which of my products cost more than $50?"*
|
|
|
107
107
|
|
|
108
108
|
## How it works
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
▼
|
|
117
|
-
MCP on Ruby
|
|
118
|
-
Registry which controller actions are exposed, with their routes and schemas
|
|
119
|
-
Dispatcher builds an in-process request: GET /orders?status=pending
|
|
120
|
-
Identity the caller's Authorization header and cookies, or the stdio user
|
|
121
|
-
Result 2xx JSON → structured result; 4xx/5xx → readable tool error
|
|
122
|
-
▼
|
|
123
|
-
Your Rails app: middleware → routes → before_action → controller → render json
|
|
124
|
-
```
|
|
110
|
+
<p align="center">
|
|
111
|
+
<picture>
|
|
112
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/how-it-works-dark.svg">
|
|
113
|
+
<img src="docs/assets/how-it-works.svg" width="960" alt="An MCP client sends a tool call over HTTP or stdio to the official mcp gem, which hands it to MCP on Ruby. MCP on Ruby runs it as an ordinary Rails request through your app, as the caller, and the JSON response comes back as the tool result.">
|
|
114
|
+
</picture>
|
|
115
|
+
</p>
|
|
125
116
|
|
|
126
117
|
Each tool call becomes an ordinary request through your full middleware stack. Anything that protects your API, including `rate_limit`, Rack::Attack, tenant scoping, and audit logging, protects agent traffic too.
|
|
127
118
|
|
|
@@ -181,7 +172,30 @@ McpOnRuby.configure do |config|
|
|
|
181
172
|
end
|
|
182
173
|
```
|
|
183
174
|
|
|
184
|
-
For Devise and Warden, the user is signed in with the `:user` scope (change it with `config.warden_scope`).
|
|
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.
|
|
185
199
|
|
|
186
200
|
If authentication fails, the agent gets a clear "Not authenticated (401)" tool error and nothing runs.
|
|
187
201
|
|
|
@@ -218,7 +232,8 @@ See [SECURITY.md](SECURITY.md) for the full threat model.
|
|
|
218
232
|
| `401` / `403` / `404` | Tool error: not authenticated / not permitted / not found |
|
|
219
233
|
| `422` with JSON | Tool error including your validation messages, so the agent can correct itself |
|
|
220
234
|
| Other `4xx` | Tool error with the status, plus the body when it is JSON |
|
|
221
|
-
| `3xx` | Tool error
|
|
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) |
|
|
222
237
|
| HTML | Tool error asking for JSON. Use `respond_to` with `format.json`. |
|
|
223
238
|
| `5xx` or an exception | Generic tool error. Details are reported to `Rails.error`, never sent. |
|
|
224
239
|
|
|
@@ -274,19 +289,26 @@ claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
|
|
|
274
289
|
<details>
|
|
275
290
|
<summary><b>Claude Desktop</b></summary>
|
|
276
291
|
|
|
277
|
-
Claude Desktop
|
|
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
|
+
```
|
|
278
298
|
|
|
279
299
|
```json
|
|
280
300
|
{
|
|
281
301
|
"mcpServers": {
|
|
282
302
|
"my-app": {
|
|
283
|
-
"command": "
|
|
284
|
-
"args": ["-
|
|
303
|
+
"command": "/Users/you/.rbenv/versions/3.4.7/bin/ruby",
|
|
304
|
+
"args": ["-C", "/path/to/my-app", "bin/rails", "mcp:stdio"],
|
|
285
305
|
"env": { "MCP_USER": "alice@example.com" }
|
|
286
306
|
}
|
|
287
307
|
}
|
|
288
308
|
}
|
|
289
309
|
```
|
|
310
|
+
|
|
311
|
+
Update `command` when you upgrade Ruby.
|
|
290
312
|
</details>
|
|
291
313
|
|
|
292
314
|
<details>
|
|
@@ -396,7 +418,7 @@ end
|
|
|
396
418
|
|
|
397
419
|
| | Supported |
|
|
398
420
|
|---|---|
|
|
399
|
-
| Ruby | 3.2, 3.3, 3.4 |
|
|
421
|
+
| Ruby | 3.2, 3.3, 3.4, 4.0 |
|
|
400
422
|
| Rails | 7.1, 7.2, 8.0, 8.1 |
|
|
401
423
|
| MCP protocol | Every version the official `mcp` gem supports, negotiated per client |
|
|
402
424
|
| Transports | Streamable HTTP (stateless), stdio |
|
|
@@ -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|
|
data/lib/mcp_on_ruby/engine.rb
CHANGED
|
@@ -20,10 +20,15 @@ module McpOnRuby
|
|
|
20
20
|
McpOnRuby.registry.reset!
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
config.after_initialize do |app|
|
|
23
|
+
config.after_initialize do
|
|
25
24
|
McpOnRuby::WardenHook.install!
|
|
26
|
-
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Fail at boot, not on the first agent request, when declarations do not match routes.
|
|
28
|
+
# Rails runs after_initialize before it loads routes, so validate once routes are loaded,
|
|
29
|
+
# which happens during boot (after eager loading) when eager_load is on.
|
|
30
|
+
initializer "mcp_on_ruby.validate_declarations" do |app|
|
|
31
|
+
app.config.after_routes_loaded { McpOnRuby.registry.tools } if app.config.eager_load
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
end
|
data/lib/mcp_on_ruby/result.rb
CHANGED
|
@@ -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
|
|
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)",
|
|
@@ -45,12 +45,16 @@ module McpOnRuby
|
|
|
45
45
|
@dispatcher ||= Dispatcher.new
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# Tool classes are rebuilt only when the registry is (on boot and code reload)
|
|
48
|
+
# Tool classes are rebuilt only when the registry is (on boot and code reload) or when
|
|
49
|
+
# `allow_writes` changes, never per request.
|
|
49
50
|
def mcp_tools
|
|
50
|
-
|
|
51
|
+
key = [registry.tools, configuration.allow_writes]
|
|
51
52
|
@mutex.synchronize do
|
|
52
|
-
@cache
|
|
53
|
-
|
|
53
|
+
unless @cache && @cache[:registry].equal?(key.first) && @cache[:allow_writes] == key.last
|
|
54
|
+
@cache = { registry: key.first, allow_writes: key.last,
|
|
55
|
+
tools: served_specs.map { |spec| spec.to_mcp_tool(handler) } }
|
|
56
|
+
end
|
|
57
|
+
@cache[:tools]
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
data/lib/mcp_on_ruby/version.rb
CHANGED