mcp_on_ruby 1.0.0 → 2.0.0
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 +39 -3
- data/README.md +359 -163
- data/config/routes.rb +5 -0
- data/lib/generators/mcp_on_ruby/expose/expose_generator.rb +108 -0
- data/lib/generators/mcp_on_ruby/install/install_generator.rb +37 -0
- data/lib/generators/mcp_on_ruby/install/templates/initializer.rb +22 -0
- data/lib/mcp_on_ruby/call_context.rb +29 -0
- data/lib/mcp_on_ruby/configuration.rb +63 -59
- data/lib/mcp_on_ruby/controller.rb +60 -0
- data/lib/mcp_on_ruby/declaration.rb +24 -0
- data/lib/mcp_on_ruby/dispatcher.rb +97 -0
- data/lib/mcp_on_ruby/doctor.rb +67 -0
- data/lib/mcp_on_ruby/endpoint.rb +43 -0
- data/lib/mcp_on_ruby/engine.rb +29 -0
- data/lib/mcp_on_ruby/registry.rb +146 -0
- data/lib/mcp_on_ruby/result.rb +83 -0
- data/lib/mcp_on_ruby/rspec.rb +8 -0
- data/lib/mcp_on_ruby/schema.rb +149 -0
- data/lib/mcp_on_ruby/server_factory.rb +66 -0
- data/lib/mcp_on_ruby/stdio.rb +42 -0
- data/lib/mcp_on_ruby/test_helper.rb +86 -0
- data/lib/mcp_on_ruby/tool_spec.rb +87 -0
- data/lib/mcp_on_ruby/tool_table.rb +31 -0
- data/lib/mcp_on_ruby/version.rb +2 -3
- data/lib/mcp_on_ruby/warden_hook.rb +21 -0
- data/lib/mcp_on_ruby.rb +44 -122
- data/lib/tasks/mcp_on_ruby.rake +20 -0
- metadata +65 -136
- data/CODE_OF_CONDUCT.md +0 -56
- data/CONTRIBUTING.md +0 -86
- data/bin/console +0 -11
- data/bin/setup +0 -6
- data/docs/advanced-usage.md +0 -132
- data/docs/api-reference.md +0 -35
- data/docs/testing.md +0 -55
- data/examples/claude/README.md +0 -171
- data/examples/claude/claude-bridge.js +0 -122
- data/lib/mcp_on_ruby/errors.rb +0 -137
- data/lib/mcp_on_ruby/generators/install_generator.rb +0 -46
- data/lib/mcp_on_ruby/generators/resource_generator.rb +0 -63
- data/lib/mcp_on_ruby/generators/templates/README +0 -31
- data/lib/mcp_on_ruby/generators/templates/application_resource.rb +0 -20
- data/lib/mcp_on_ruby/generators/templates/application_tool.rb +0 -18
- data/lib/mcp_on_ruby/generators/templates/initializer.rb +0 -41
- data/lib/mcp_on_ruby/generators/templates/resource.rb +0 -50
- data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +0 -67
- data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +0 -57
- data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +0 -59
- data/lib/mcp_on_ruby/generators/templates/tool.rb +0 -38
- data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +0 -55
- data/lib/mcp_on_ruby/generators/tool_generator.rb +0 -51
- data/lib/mcp_on_ruby/railtie.rb +0 -108
- data/lib/mcp_on_ruby/resource.rb +0 -161
- data/lib/mcp_on_ruby/server.rb +0 -378
- data/lib/mcp_on_ruby/tool.rb +0 -134
- data/lib/mcp_on_ruby/transport.rb +0 -330
data/README.md
CHANGED
|
@@ -1,240 +1,436 @@
|
|
|
1
|
-
# MCP on Ruby
|
|
2
|
-
|
|
3
1
|
<div align="center">
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
# MCP on Ruby
|
|
4
|
+
|
|
5
|
+
**Make your Rails app usable by AI agents.**
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
Your existing controllers become [Model Context Protocol](https://modelcontextprotocol.io) tools, running through<br>
|
|
8
|
+
the same authentication, policies, params, and JSON you already ship.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
[](https://rubygems.org/gems/mcp_on_ruby)
|
|
11
|
+
[](https://github.com/nagstler/mcp_on_ruby/actions/workflows/ci.yml)
|
|
12
|
+
[](#compatibility)
|
|
13
|
+
[](#compatibility)
|
|
14
|
+
[](LICENSE.txt)
|
|
12
15
|
|
|
13
|
-
[
|
|
16
|
+
[Quick start](#quick-start) ·
|
|
17
|
+
[How it works](#how-it-works) ·
|
|
18
|
+
[Guides](#guides) ·
|
|
19
|
+
[Configuration](#configuration) ·
|
|
20
|
+
[Testing](#testing)
|
|
14
21
|
|
|
15
22
|
</div>
|
|
16
23
|
|
|
17
24
|
---
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
```ruby
|
|
27
|
+
class OrdersController < ApplicationController
|
|
28
|
+
before_action :authenticate_user!
|
|
29
|
+
|
|
30
|
+
mcp_tool :index, description: "Search the current user's orders",
|
|
31
|
+
params: { status: { type: :string, enum: %w[pending paid shipped] } }
|
|
32
|
+
mcp_tool :show, description: "Fetch one order by id"
|
|
33
|
+
|
|
34
|
+
def index
|
|
35
|
+
render json: current_user.orders.where(params.permit(:status))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def show
|
|
39
|
+
render json: current_user.orders.find(params[:id])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That's the whole integration. Claude, Cursor, or any MCP client can now search and read orders **as the signed-in user**. Your `before_action` authenticates the agent, your scopes and policies decide what it can see, and your existing JSON comes back to it. You don't write new tool classes or a second permission system.
|
|
45
|
+
|
|
46
|
+
## Why MCP on Ruby
|
|
20
47
|
|
|
21
|
-
|
|
22
|
-
🔧 **Rails Integration** - Generators, autoloading, middleware, Railtie
|
|
23
|
-
🛠️ **Tools System** - Callable functions with JSON Schema validation
|
|
24
|
-
📊 **Resources System** - Data exposure with URI templating
|
|
25
|
-
🔒 **Security** - DNS rebinding protection, CORS, token authentication
|
|
26
|
-
⚡ **Real-time** - Server Events (SSE) foundation (full implementation coming soon)
|
|
27
|
-
🎯 **Developer-Friendly** - Clean DSL, generators, testing support
|
|
48
|
+
Most MCP libraries ask you to build a parallel API for agents: new tool classes, new argument parsing, new authorization checks. Over time that second API drifts from the real one, and every rule has to be secured twice.
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
MCP on Ruby takes the opposite approach. **Your app already has an API: your controllers.** MCP on Ruby makes it callable by agents.
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
- **One source of truth.** Agents go through the same routes, filters, policies, validations, and serializers as your users.
|
|
53
|
+
- **Real identity.** Every tool call runs as a real user, using the auth you already have: API tokens, Devise, or Warden.
|
|
54
|
+
- **Safe by default.** Actions that change data must be marked `write: true`, and every tool tells the client whether it is read-only or destructive.
|
|
55
|
+
- **Local and remote.** Serve Streamable HTTP in production, and stdio for Claude Code, Claude Desktop, and Cursor on your machine.
|
|
56
|
+
- **Built on the official SDK.** The protocol layer is the official [`mcp`](https://github.com/modelcontextprotocol/ruby-sdk) gem, so protocol updates arrive without a rewrite.
|
|
57
|
+
- **Works with the Rails you run today.** Rails 7.1 through 8.1 on Ruby 3.2+.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
**1. Install**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bundle add mcp_on_ruby
|
|
65
|
+
bin/rails generate mcp_on_ruby:install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The generator creates `config/initializers/mcp_on_ruby.rb` and mounts the endpoint:
|
|
32
69
|
|
|
33
70
|
```ruby
|
|
34
|
-
|
|
71
|
+
# config/routes.rb
|
|
72
|
+
mount McpOnRuby::Engine, at: "/mcp"
|
|
35
73
|
```
|
|
36
74
|
|
|
37
|
-
|
|
75
|
+
**2. Expose some actions**
|
|
38
76
|
|
|
39
77
|
```bash
|
|
40
|
-
|
|
41
|
-
|
|
78
|
+
bin/rails generate mcp_on_ruby:expose products index show create
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This adds `mcp_tool` declarations to `ProductsController`, with a params schema for `create` built from the model's columns. Edit the descriptions: they are what the agent reads. Then check what agents will see:
|
|
82
|
+
|
|
83
|
+
```console
|
|
84
|
+
$ bin/rails mcp:tools
|
|
85
|
+
TOOL ACTION VERB PATH ACCESS
|
|
86
|
+
products_create ProductsController#create POST /products write
|
|
87
|
+
products_index ProductsController#index GET /products read-only
|
|
88
|
+
products_show ProductsController#show GET /products/:id read-only
|
|
42
89
|
```
|
|
43
90
|
|
|
44
|
-
|
|
91
|
+
**3. Connect an agent**
|
|
45
92
|
|
|
46
|
-
|
|
93
|
+
Over HTTP, sending a token your app already accepts:
|
|
47
94
|
|
|
48
95
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
96
|
+
claude mcp add --transport http my-app http://localhost:3000/mcp \
|
|
97
|
+
--header "Authorization: Bearer $MY_APP_API_TOKEN"
|
|
98
|
+
```
|
|
51
99
|
|
|
52
|
-
|
|
53
|
-
rails generate mcp_on_ruby:tool UserManager --description "Manage application users"
|
|
100
|
+
Or locally over stdio, acting as a user you choose ([set up `stdio_user`](#authentication) first):
|
|
54
101
|
|
|
55
|
-
|
|
56
|
-
|
|
102
|
+
```bash
|
|
103
|
+
claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
|
|
57
104
|
```
|
|
58
105
|
|
|
59
|
-
|
|
106
|
+
Then ask: *"Which of my products cost more than $50?"*
|
|
107
|
+
|
|
108
|
+
## How it works
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
MCP client (Claude, Cursor, …)
|
|
112
|
+
│ Streamable HTTP (/mcp) or stdio (bin/rails mcp:stdio)
|
|
113
|
+
▼
|
|
114
|
+
official mcp gem ──────── protocol, version negotiation, schema validation
|
|
115
|
+
│ tools/call orders_index { "status": "pending" }
|
|
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
|
+
```
|
|
125
|
+
|
|
126
|
+
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
|
+
|
|
128
|
+
HTTP is served statelessly: every request is self-contained, so any Puma worker or server behind your load balancer can handle any MCP request, with no sticky sessions.
|
|
129
|
+
|
|
130
|
+
## Guides
|
|
131
|
+
|
|
132
|
+
### Declaring tools
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
mcp_tool :index,
|
|
136
|
+
description: "Search products by name", # required: what the agent reads
|
|
137
|
+
title: "Search products", # optional display name
|
|
138
|
+
name: "search_products", # optional; defaults to "products_index"
|
|
139
|
+
params: {
|
|
140
|
+
q: :string, # shorthand
|
|
141
|
+
sort: { type: :string, enum: %w[name price], default: "name" },
|
|
142
|
+
limit: { type: :integer, minimum: 1, maximum: 100 }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Path parameters such as `:id` in `/products/:id` are read from your routes and added to the schema as required parameters. You don't declare them.
|
|
147
|
+
|
|
148
|
+
Use a block for nested input:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
mcp_tool :create, description: "Place an order", write: true do
|
|
152
|
+
param :order, required: true do
|
|
153
|
+
param :sku, type: :string, required: true
|
|
154
|
+
param :quantity, type: :integer, minimum: 1
|
|
155
|
+
param :tags, type: :array, items: :string
|
|
156
|
+
param :note, type: :string, nullable: true
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Supported types: `:string`, `:integer`, `:number`, `:boolean`, `:array`, `:object`, `:null`, or an array of them. Options use Ruby names for JSON Schema keywords: `description`, `enum`, `default`, `format`, `pattern`, `minimum`, `maximum`, `min_length`, `max_length`, `min_items`, `max_items`, `unique_items`, and more. Unknown types and options raise immediately, so typos don't ship.
|
|
162
|
+
|
|
163
|
+
The official SDK validates arguments against this schema before your controller runs. Arguments reach `params` exactly as they would from a JSON client, so strong params work unchanged. Read tools receive them as query parameters, write tools as a JSON body.
|
|
164
|
+
|
|
165
|
+
### Authentication
|
|
166
|
+
|
|
167
|
+
MCP on Ruby never asks you to build a new auth system. It hands the caller's identity to the auth you already have.
|
|
168
|
+
|
|
169
|
+
**Over HTTP**, the client's `Authorization` header and cookies are forwarded to your controller. If your app accepts API tokens, as most API apps and Devise token setups do, there is nothing to configure.
|
|
170
|
+
|
|
171
|
+
**Over stdio** there is no HTTP request, so you choose who local sessions act as:
|
|
60
172
|
|
|
61
173
|
```ruby
|
|
62
174
|
# config/initializers/mcp_on_ruby.rb
|
|
63
175
|
McpOnRuby.configure do |config|
|
|
64
|
-
|
|
65
|
-
config.
|
|
66
|
-
config.rate_limit_per_minute = 60
|
|
67
|
-
config.allowed_origins = [/\.yourdomain\.com$/]
|
|
68
|
-
end
|
|
176
|
+
# Devise / Warden apps: sign in as a user for each tool call (nothing is stored in the session).
|
|
177
|
+
config.stdio_user = -> { User.find_by!(email: ENV.fetch("MCP_USER")) }
|
|
69
178
|
|
|
70
|
-
|
|
71
|
-
config.
|
|
72
|
-
config.mcp.auto_register_tools = true
|
|
73
|
-
config.mcp.auto_register_resources = true
|
|
179
|
+
# Token-authenticated apps: send headers your app already accepts.
|
|
180
|
+
config.stdio_headers = -> { { "Authorization" => "Bearer #{ENV.fetch("MCP_TOKEN")}" } }
|
|
74
181
|
end
|
|
75
182
|
```
|
|
76
183
|
|
|
77
|
-
|
|
184
|
+
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`.
|
|
185
|
+
|
|
186
|
+
If authentication fails, the agent gets a clear "Not authenticated (401)" tool error and nothing runs.
|
|
187
|
+
|
|
188
|
+
### Authorization
|
|
189
|
+
|
|
190
|
+
There is nothing new to learn, because tool calls run your controller code. `authorize`, `policy_scope`, `can?`, `current_user.orders.find`, and `head :forbidden` behave exactly as they do for any other client. A 403 reaches the agent as "Not permitted (403)".
|
|
191
|
+
|
|
192
|
+
Inside a controller, `mcp_request?` tells you whether an agent is calling, and `mcp_tool_name` tells you which tool:
|
|
78
193
|
|
|
79
194
|
```ruby
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def initialize
|
|
83
|
-
super(
|
|
84
|
-
name: 'user_manager',
|
|
85
|
-
description: 'Manage application users',
|
|
86
|
-
input_schema: {
|
|
87
|
-
type: 'object',
|
|
88
|
-
properties: {
|
|
89
|
-
action: { type: 'string', enum: ['create', 'update', 'delete'] },
|
|
90
|
-
user_id: { type: 'integer' },
|
|
91
|
-
attributes: { type: 'object' }
|
|
92
|
-
},
|
|
93
|
-
required: ['action']
|
|
94
|
-
}
|
|
95
|
-
)
|
|
96
|
-
end
|
|
195
|
+
before_action :require_admin, if: :mcp_request?
|
|
196
|
+
```
|
|
97
197
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
198
|
+
### Safety
|
|
199
|
+
|
|
200
|
+
Agents act on behalf of real users, so the defaults are conservative:
|
|
201
|
+
|
|
202
|
+
- **Writes are explicit.** Declaring `mcp_tool` on an action routed with `POST`, `PUT`, `PATCH`, or `DELETE` without `write: true` raises `McpOnRuby::ConfigurationError`. This happens at boot in production and when running `bin/rails mcp:doctor`.
|
|
203
|
+
- **Honest hints.** Every tool is annotated from its HTTP verb (`readOnlyHint`, `destructiveHint`, `idempotentHint`), so clients like Claude can ask before destructive calls. Override with `annotations:`.
|
|
204
|
+
- **A kill switch.** `config.allow_writes = false` hides and refuses every write tool, for example on a read-only replica or a demo environment.
|
|
205
|
+
- **No accidental exposure.** Only declared actions are reachable, and declarations without a route raise. `bin/rails mcp:tools` shows exactly what is exposed.
|
|
206
|
+
- **No leaked internals.** Server errors reach the agent as a generic error. Details go to `Rails.error` and your logs.
|
|
207
|
+
- **Browser protection.** The endpoint rejects requests from foreign browser origins. Requests dispatched by MCP skip Rails' CSRF token check, which is safe because only the dispatcher can mark a request as MCP. Regular requests to the same actions stay protected.
|
|
208
|
+
|
|
209
|
+
See [SECURITY.md](SECURITY.md) for the full threat model.
|
|
210
|
+
|
|
211
|
+
### Results and errors
|
|
212
|
+
|
|
213
|
+
| Your controller returns | The agent receives |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `2xx` with JSON | `structuredContent` plus the JSON as text |
|
|
216
|
+
| `2xx` with plain text | The text |
|
|
217
|
+
| `204 No Content` | A short success message |
|
|
218
|
+
| `401` / `403` / `404` | Tool error: not authenticated / not permitted / not found |
|
|
219
|
+
| `422` with JSON | Tool error including your validation messages, so the agent can correct itself |
|
|
220
|
+
| Other `4xx` | Tool error with the status, plus the body when it is JSON |
|
|
221
|
+
| `3xx` | Tool error naming the redirect target (redirects are not followed) |
|
|
222
|
+
| HTML | Tool error asking for JSON. Use `respond_to` with `format.json`. |
|
|
223
|
+
| `5xx` or an exception | Generic tool error. Details are reported to `Rails.error`, never sent. |
|
|
113
224
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
225
|
+
### Tools without a controller
|
|
226
|
+
|
|
227
|
+
For logic that has no endpoint, add a tool class from the official SDK. It runs alongside your controller tools:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class ReindexSearchTool < MCP::Tool
|
|
231
|
+
description "Rebuild the search index"
|
|
232
|
+
|
|
233
|
+
def self.call(server_context:)
|
|
234
|
+
SearchIndex.rebuild!
|
|
235
|
+
MCP::Tool::Response.new([{ type: "text", text: "Reindexed" }])
|
|
117
236
|
end
|
|
118
237
|
end
|
|
238
|
+
|
|
239
|
+
McpOnRuby.configure { |config| config.tools = [ReindexSearchTool] }
|
|
119
240
|
```
|
|
120
241
|
|
|
121
|
-
|
|
242
|
+
These tools bypass your controllers, so they also bypass your controller authentication. Use them for things that are safe for every caller.
|
|
122
243
|
|
|
123
|
-
|
|
124
|
-
# app/resources/user_stats_resource.rb
|
|
125
|
-
class UserStatsResource < ApplicationResource
|
|
126
|
-
def initialize
|
|
127
|
-
super(
|
|
128
|
-
uri: 'users/{id}/stats',
|
|
129
|
-
name: 'User Statistics',
|
|
130
|
-
description: 'Get detailed user statistics',
|
|
131
|
-
mime_type: 'application/json'
|
|
132
|
-
)
|
|
133
|
-
end
|
|
244
|
+
### Observability
|
|
134
245
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def fetch_content(params, context)
|
|
138
|
-
user = User.find(params['id'])
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
user_id: user.id,
|
|
142
|
-
statistics: {
|
|
143
|
-
posts_count: user.posts.count,
|
|
144
|
-
comments_count: user.comments.count,
|
|
145
|
-
last_login: user.last_login_at,
|
|
146
|
-
account_created: user.created_at
|
|
147
|
-
},
|
|
148
|
-
generated_at: Time.current.iso8601
|
|
149
|
-
}
|
|
150
|
-
end
|
|
246
|
+
Every tool call emits `tool_call.mcp_on_ruby` through `ActiveSupport::Notifications`:
|
|
151
247
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
248
|
+
```ruby
|
|
249
|
+
ActiveSupport::Notifications.subscribe("tool_call.mcp_on_ruby") do |event|
|
|
250
|
+
Rails.logger.info(
|
|
251
|
+
"MCP #{event.payload[:tool]} → #{event.payload[:status]} " \
|
|
252
|
+
"(#{event.payload[:transport]}, #{event.duration.round}ms)"
|
|
253
|
+
)
|
|
156
254
|
end
|
|
157
255
|
```
|
|
158
256
|
|
|
159
|
-
|
|
257
|
+
Payload: `tool`, `controller`, `action`, `transport` (`:http` or `:stdio`), `status`, and `error`. Tool calls also show up in your normal request logs and APM traces as requests to the underlying routes.
|
|
258
|
+
|
|
259
|
+
### Connecting clients
|
|
260
|
+
|
|
261
|
+
<details>
|
|
262
|
+
<summary><b>Claude Code</b></summary>
|
|
160
263
|
|
|
161
264
|
```bash
|
|
162
|
-
|
|
163
|
-
|
|
265
|
+
# Remote or production
|
|
266
|
+
claude mcp add --transport http my-app https://my-app.example.com/mcp \
|
|
267
|
+
--header "Authorization: Bearer $MY_APP_API_TOKEN"
|
|
268
|
+
|
|
269
|
+
# Local, over stdio (run from your app's directory)
|
|
270
|
+
claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
|
|
164
271
|
```
|
|
272
|
+
</details>
|
|
273
|
+
|
|
274
|
+
<details>
|
|
275
|
+
<summary><b>Claude Desktop</b></summary>
|
|
276
|
+
|
|
277
|
+
Claude Desktop does not load your shell profile, so start Rails through a login shell to pick up rbenv, asdf, or chruby:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"mcpServers": {
|
|
282
|
+
"my-app": {
|
|
283
|
+
"command": "bash",
|
|
284
|
+
"args": ["-lc", "cd /path/to/my-app && bin/rails mcp:stdio"],
|
|
285
|
+
"env": { "MCP_USER": "alice@example.com" }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
</details>
|
|
291
|
+
|
|
292
|
+
<details>
|
|
293
|
+
<summary><b>Cursor</b></summary>
|
|
165
294
|
|
|
166
|
-
|
|
295
|
+
`.cursor/mcp.json`:
|
|
167
296
|
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"mcpServers": {
|
|
300
|
+
"my-app": {
|
|
301
|
+
"url": "http://localhost:3000/mcp",
|
|
302
|
+
"headers": { "Authorization": "Bearer YOUR_API_TOKEN" }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
168
306
|
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
│ │ - Authorization│ │ - Authorization│ │ - SSE │ │
|
|
182
|
-
│ │ - Execution │ │ - Content │ │ - Security │ │
|
|
183
|
-
│ └─────────────────┘ └─────────────────┘ └──────────────┘ │
|
|
184
|
-
├─────────────────────────────────────────────────────────────┤
|
|
185
|
-
│ JSON-RPC Protocol │
|
|
186
|
-
└─────────────────────────────────────────────────────────────┘
|
|
307
|
+
</details>
|
|
308
|
+
|
|
309
|
+
<details>
|
|
310
|
+
<summary><b>MCP Inspector</b></summary>
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# Browse and call tools in the browser
|
|
314
|
+
npx @modelcontextprotocol/inspector bin/rails mcp:stdio
|
|
315
|
+
|
|
316
|
+
# Or from the command line
|
|
317
|
+
npx @modelcontextprotocol/inspector --cli http://localhost:3000/mcp --transport http \
|
|
318
|
+
--header "Authorization: Bearer $MY_APP_API_TOKEN" --method tools/list
|
|
187
319
|
```
|
|
320
|
+
</details>
|
|
188
321
|
|
|
189
|
-
|
|
322
|
+
<details>
|
|
323
|
+
<summary><b>MCP registries and gateways</b></summary>
|
|
190
324
|
|
|
191
|
-
|
|
325
|
+
Register your `/mcp` endpoint with an MCP-compatible registry or gateway when you want discovery, access control, audit trails, and approvals managed centrally. Rails still owns the tools and enforces your app's own authorization on every call.
|
|
326
|
+
</details>
|
|
192
327
|
|
|
193
|
-
|
|
328
|
+
## Testing
|
|
194
329
|
|
|
195
|
-
|
|
330
|
+
Test tools the way agents call them: through the MCP server and your real controllers.
|
|
196
331
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
332
|
+
```ruby
|
|
333
|
+
# spec/rails_helper.rb
|
|
334
|
+
require "mcp_on_ruby/rspec"
|
|
200
335
|
|
|
336
|
+
# test/test_helper.rb
|
|
337
|
+
class ActiveSupport::TestCase
|
|
338
|
+
include McpOnRuby::TestHelper
|
|
339
|
+
end
|
|
340
|
+
```
|
|
201
341
|
|
|
202
|
-
|
|
342
|
+
```ruby
|
|
343
|
+
it "shows agents only their own orders" do
|
|
344
|
+
result = mcp_call :orders_index, { status: "pending" }, headers: { "Authorization" => "Bearer #{alice.api_token}" }
|
|
203
345
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
346
|
+
expect(result).to be_success
|
|
347
|
+
expect(result.structured.pluck("id")).to eq([alice_order.id])
|
|
348
|
+
end
|
|
207
349
|
|
|
208
|
-
|
|
350
|
+
it "stops agents from reading other users' orders" do
|
|
351
|
+
result = mcp_call :orders_show, { id: bob_order.id }, as: alice # signs in through Devise/Warden
|
|
209
352
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
353
|
+
expect(result).to be_error
|
|
354
|
+
expect(result.status).to eq(404)
|
|
355
|
+
end
|
|
356
|
+
```
|
|
214
357
|
|
|
215
|
-
|
|
358
|
+
`mcp_call` returns a result with `success?`, `error?`, `structured` (the parsed JSON), `status` (for failures), `text` (what the agent reads), and `protocol_error` (unknown tool, invalid arguments). `mcp_tools` lists the tool names an agent sees.
|
|
216
359
|
|
|
217
|
-
|
|
218
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
219
|
-
3. Write tests for your changes
|
|
220
|
-
4. Ensure all tests pass (`bundle exec rspec`)
|
|
221
|
-
5. Commit your changes (`git commit -am 'Add some feature'`)
|
|
222
|
-
6. Push to the branch (`git push origin my-new-feature`)
|
|
223
|
-
7. Create a Pull Request
|
|
360
|
+
## Configuration
|
|
224
361
|
|
|
225
|
-
|
|
362
|
+
```ruby
|
|
363
|
+
# config/initializers/mcp_on_ruby.rb
|
|
364
|
+
McpOnRuby.configure do |config|
|
|
365
|
+
config.server_name = "my-app"
|
|
366
|
+
config.instructions = "Tools for managing orders in My App. Amounts are in cents."
|
|
367
|
+
config.stdio_user = -> { User.find_by!(email: ENV.fetch("MCP_USER")) }
|
|
368
|
+
end
|
|
369
|
+
```
|
|
226
370
|
|
|
227
|
-
|
|
371
|
+
| Option | Default | Description |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| `server_name` | app name, dasherized | Name reported to clients |
|
|
374
|
+
| `server_version` | `nil` | Version reported to clients |
|
|
375
|
+
| `instructions` | `nil` | Guidance sent to the agent when it connects |
|
|
376
|
+
| `stdio_user` | `nil` | Callable returning the user `mcp:stdio` sessions sign in as (Devise/Warden) |
|
|
377
|
+
| `stdio_headers` | `nil` | Callable returning headers `mcp:stdio` sessions send (token auth) |
|
|
378
|
+
| `warden_scope` | `:user` | Warden scope used for `stdio_user` and `mcp_call(as:)` |
|
|
379
|
+
| `allow_writes` | `true` | Set `false` to hide and refuse every `write: true` tool |
|
|
380
|
+
| `tools` | `[]` | Extra `MCP::Tool` classes served alongside controller tools |
|
|
381
|
+
| `allowed_origins` | `[]` | Browser origins allowed to call the endpoint, in addition to same-origin |
|
|
382
|
+
| `max_request_bytes` | 4 MiB | Largest MCP request body accepted |
|
|
383
|
+
| `tool_name` | `"<controller>_<action>"` | Callable `(controller_path, action)` returning default tool names |
|
|
384
|
+
|
|
385
|
+
### Commands
|
|
386
|
+
|
|
387
|
+
| Command | What it does |
|
|
388
|
+
|---|---|
|
|
389
|
+
| `bin/rails g mcp_on_ruby:install [--path=/mcp]` | Adds the initializer and mounts the endpoint |
|
|
390
|
+
| `bin/rails g mcp_on_ruby:expose CONTROLLER ACTION...` | Adds `mcp_tool` declarations to a controller |
|
|
391
|
+
| `bin/rails mcp:tools` | Lists exposed tools, their routes, and their access level |
|
|
392
|
+
| `bin/rails mcp:stdio` | Serves your tools over stdio for local agents |
|
|
393
|
+
| `bin/rails mcp:doctor` | Checks the mount, declarations, and stdio identity; exits non-zero on errors |
|
|
394
|
+
|
|
395
|
+
## Compatibility
|
|
396
|
+
|
|
397
|
+
| | Supported |
|
|
398
|
+
|---|---|
|
|
399
|
+
| Ruby | 3.2, 3.3, 3.4 |
|
|
400
|
+
| Rails | 7.1, 7.2, 8.0, 8.1 |
|
|
401
|
+
| MCP protocol | Every version the official `mcp` gem supports, negotiated per client |
|
|
402
|
+
| Transports | Streamable HTTP (stateless), stdio |
|
|
403
|
+
| Controllers | `ActionController::Base` and `ActionController::API` |
|
|
404
|
+
|
|
405
|
+
CI runs the full suite on every supported Ruby and Rails combination.
|
|
406
|
+
|
|
407
|
+
## When to use something else
|
|
408
|
+
|
|
409
|
+
MCP on Ruby is for exposing an **existing Rails app** to agents. Other tools fit other jobs:
|
|
410
|
+
|
|
411
|
+
- **[`mcp`](https://github.com/modelcontextprotocol/ruby-sdk)**, the official Ruby SDK, if you're building a standalone MCP server. MCP on Ruby runs on top of it.
|
|
412
|
+
- **[ActionMCP](https://github.com/seuros/action_mcp)** if you're on Rails 8.1+ and prefer dedicated tool, prompt, and resource classes with a full MCP feature set.
|
|
413
|
+
- **[Tidewave](https://tidewave.ai)** if you want coding agents to understand your codebase while you develop. MCP on Ruby is about your app's *runtime* behavior for your *users*.
|
|
228
414
|
|
|
229
|
-
##
|
|
415
|
+
## Contributing
|
|
230
416
|
|
|
231
|
-
|
|
232
|
-
- The Ruby on Rails community for inspiration and conventions
|
|
417
|
+
Bug reports, ideas, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
233
418
|
|
|
234
|
-
|
|
419
|
+
```bash
|
|
420
|
+
git clone https://github.com/nagstler/mcp_on_ruby && cd mcp_on_ruby
|
|
421
|
+
bin/setup
|
|
422
|
+
bundle exec rake # specs + RuboCop
|
|
423
|
+
RAILS_VERSION=7.2 bundle update && bundle exec rspec # another Rails series
|
|
424
|
+
```
|
|
235
425
|
|
|
236
|
-
|
|
426
|
+
Release notes are in [CHANGELOG.md](CHANGELOG.md).
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
Released under the [MIT License](LICENSE.txt).
|
|
237
431
|
|
|
238
|
-
|
|
432
|
+
## Acknowledgments
|
|
239
433
|
|
|
240
|
-
|
|
434
|
+
- The [Model Context Protocol](https://modelcontextprotocol.io) team for the specification.
|
|
435
|
+
- The maintainers of the official [Ruby SDK](https://github.com/modelcontextprotocol/ruby-sdk), on whose work this gem is built.
|
|
436
|
+
- The Rails community, whose conventions this gem tries hard not to break.
|