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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -3
  3. data/README.md +359 -163
  4. data/config/routes.rb +5 -0
  5. data/lib/generators/mcp_on_ruby/expose/expose_generator.rb +108 -0
  6. data/lib/generators/mcp_on_ruby/install/install_generator.rb +37 -0
  7. data/lib/generators/mcp_on_ruby/install/templates/initializer.rb +22 -0
  8. data/lib/mcp_on_ruby/call_context.rb +29 -0
  9. data/lib/mcp_on_ruby/configuration.rb +63 -59
  10. data/lib/mcp_on_ruby/controller.rb +60 -0
  11. data/lib/mcp_on_ruby/declaration.rb +24 -0
  12. data/lib/mcp_on_ruby/dispatcher.rb +97 -0
  13. data/lib/mcp_on_ruby/doctor.rb +67 -0
  14. data/lib/mcp_on_ruby/endpoint.rb +43 -0
  15. data/lib/mcp_on_ruby/engine.rb +29 -0
  16. data/lib/mcp_on_ruby/registry.rb +146 -0
  17. data/lib/mcp_on_ruby/result.rb +83 -0
  18. data/lib/mcp_on_ruby/rspec.rb +8 -0
  19. data/lib/mcp_on_ruby/schema.rb +149 -0
  20. data/lib/mcp_on_ruby/server_factory.rb +66 -0
  21. data/lib/mcp_on_ruby/stdio.rb +42 -0
  22. data/lib/mcp_on_ruby/test_helper.rb +86 -0
  23. data/lib/mcp_on_ruby/tool_spec.rb +87 -0
  24. data/lib/mcp_on_ruby/tool_table.rb +31 -0
  25. data/lib/mcp_on_ruby/version.rb +2 -3
  26. data/lib/mcp_on_ruby/warden_hook.rb +21 -0
  27. data/lib/mcp_on_ruby.rb +44 -122
  28. data/lib/tasks/mcp_on_ruby.rake +20 -0
  29. metadata +65 -136
  30. data/CODE_OF_CONDUCT.md +0 -56
  31. data/CONTRIBUTING.md +0 -86
  32. data/bin/console +0 -11
  33. data/bin/setup +0 -6
  34. data/docs/advanced-usage.md +0 -132
  35. data/docs/api-reference.md +0 -35
  36. data/docs/testing.md +0 -55
  37. data/examples/claude/README.md +0 -171
  38. data/examples/claude/claude-bridge.js +0 -122
  39. data/lib/mcp_on_ruby/errors.rb +0 -137
  40. data/lib/mcp_on_ruby/generators/install_generator.rb +0 -46
  41. data/lib/mcp_on_ruby/generators/resource_generator.rb +0 -63
  42. data/lib/mcp_on_ruby/generators/templates/README +0 -31
  43. data/lib/mcp_on_ruby/generators/templates/application_resource.rb +0 -20
  44. data/lib/mcp_on_ruby/generators/templates/application_tool.rb +0 -18
  45. data/lib/mcp_on_ruby/generators/templates/initializer.rb +0 -41
  46. data/lib/mcp_on_ruby/generators/templates/resource.rb +0 -50
  47. data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +0 -67
  48. data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +0 -57
  49. data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +0 -59
  50. data/lib/mcp_on_ruby/generators/templates/tool.rb +0 -38
  51. data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +0 -55
  52. data/lib/mcp_on_ruby/generators/tool_generator.rb +0 -51
  53. data/lib/mcp_on_ruby/railtie.rb +0 -108
  54. data/lib/mcp_on_ruby/resource.rb +0 -161
  55. data/lib/mcp_on_ruby/server.rb +0 -378
  56. data/lib/mcp_on_ruby/tool.rb +0 -134
  57. 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
- [![Gem Version](https://badge.fury.io/rb/mcp_on_ruby.svg)](https://badge.fury.io/rb/mcp_on_ruby)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Ruby Version](https://img.shields.io/badge/Ruby-2.7%2B-red.svg)](https://www.ruby-lang.org/)
3
+ # MCP on Ruby
4
+
5
+ **Make your Rails app usable by AI agents.**
8
6
 
9
- **Model Context Protocol (MCP) server for Rails applications**
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
- Expose your Rails app as an AI accessible interface — define tools and resources the Rails way.
10
+ [![Gem Version](https://img.shields.io/gem/v/mcp_on_ruby)](https://rubygems.org/gems/mcp_on_ruby)
11
+ [![CI](https://github.com/nagstler/mcp_on_ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/nagstler/mcp_on_ruby/actions/workflows/ci.yml)
12
+ [![Ruby](https://img.shields.io/badge/ruby-3.2%2B-CC342D)](#compatibility)
13
+ [![Rails](https://img.shields.io/badge/rails-7.1%20%E2%80%93%208.1-D30001)](#compatibility)
14
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE.txt)
12
15
 
13
- [Documentation](https://rubydoc.info/gems/mcp_on_ruby) | [Contributing](#contributing)
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
- ## Features
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
- 🚀 **Production-Ready** - Authentication, rate limiting, error handling, security
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
- ## Installation
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
- Add to your `Gemfile`:
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
- gem 'mcp_on_ruby'
71
+ # config/routes.rb
72
+ mount McpOnRuby::Engine, at: "/mcp"
35
73
  ```
36
74
 
37
- Then run:
75
+ **2. Expose some actions**
38
76
 
39
77
  ```bash
40
- bundle install
41
- rails generate mcp_on_ruby:install
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
- ## Quick Start
91
+ **3. Connect an agent**
45
92
 
46
- ### 1. Install and Configure
93
+ Over HTTP, sending a token your app already accepts:
47
94
 
48
95
  ```bash
49
- # Generate MCP server files
50
- rails generate mcp_on_ruby:install
96
+ claude mcp add --transport http my-app http://localhost:3000/mcp \
97
+ --header "Authorization: Bearer $MY_APP_API_TOKEN"
98
+ ```
51
99
 
52
- # Create a tool
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
- # Create a resource
56
- rails generate mcp_on_ruby:resource UserStats --uri "users/{id}/stats" --template
102
+ ```bash
103
+ claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
57
104
  ```
58
105
 
59
- ### 2. Configure MCP Server
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
- config.authentication_required = true
65
- config.authentication_token = ENV['MCP_AUTH_TOKEN']
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
- Rails.application.configure do
71
- config.mcp.enabled = true
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
- ### 3. Create Tools
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
- # app/tools/user_manager_tool.rb
81
- class UserManagerTool < ApplicationTool
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
- protected
99
-
100
- def execute(arguments, context)
101
- case arguments['action']
102
- when 'create'
103
- user = User.create!(arguments['attributes'])
104
- { success: true, user: user.as_json }
105
- when 'update'
106
- user = User.find(arguments['user_id'])
107
- user.update!(arguments['attributes'])
108
- { success: true, user: user.as_json }
109
- else
110
- { error: 'Unsupported action' }
111
- end
112
- end
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
- def authorize(context)
115
- # Add your authorization logic
116
- context[:authenticated] == true
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
- ### 4. Create Resources
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
- ```ruby
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
- protected
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
- def authorize(context)
153
- # Check if user can access this data
154
- context[:authenticated] == true
155
- end
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
- ### 5. Start Your Server
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
- rails server
163
- # MCP server available at http://localhost:3000/mcp
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
- ## Architecture
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
- │ Rails App │
171
- ├─────────────────────────────────────────────────────────────┤
172
- │ app/tools/ │ app/resources/ │
173
- │ ├── application_tool.rb ├── application_resource.rb │
174
- │ ├── user_manager_tool.rb ├── user_stats_resource.rb │
175
- │ └── ... └── ... │
176
- ├─────────────────────────────────────────────────────────────┤
177
- │ MCP Server Core │
178
- │ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
179
- │ │ Tools │ │ Resources │ │ Transport │ │
180
- │ │ - Validation │ │ - Templating │ │ - HTTP │ │
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
- ## Examples
322
+ <details>
323
+ <summary><b>MCP registries and gateways</b></summary>
190
324
 
191
- Connect your Rails MCP server with different AI clients:
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
- 👉 **[Claude Desktop](examples/claude/)** - Complete setup guide with bridge script
328
+ ## Testing
194
329
 
195
- ## Documentation
330
+ Test tools the way agents call them: through the MCP server and your real controllers.
196
331
 
197
- 👉 **[Advanced Usage](docs/advanced-usage.md)** - Custom authorization, caching, manual configuration
198
- 👉 **[Testing Guide](docs/testing.md)** - RSpec integration and testing patterns
199
- 👉 **[API Reference](docs/api-reference.md)** - Complete API documentation
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
- ## Requirements
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
- - Ruby 2.7.0 or higher
205
- - Rails 6.0 or higher (for full integration)
206
- - JSON Schema validation support
346
+ expect(result).to be_success
347
+ expect(result.structured.pluck("id")).to eq([alice_order.id])
348
+ end
207
349
 
208
- ## Dependencies
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
- Production dependencies (minimal footprint):
211
- - `json-schema` (~> 3.0) - JSON Schema validation
212
- - `rack` (~> 2.2) - HTTP transport layer
213
- - `webrick` (~> 1.7) - HTTP server
353
+ expect(result).to be_error
354
+ expect(result.status).to eq(404)
355
+ end
356
+ ```
214
357
 
215
- ## Contributing
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
- 1. Fork the repository
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
- ## License
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
- This project is licensed under the MIT License - see the [LICENSE](LICENSE.txt) file for details.
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
- ## 🙏 Acknowledgments
415
+ ## Contributing
230
416
 
231
- - The [Model Context Protocol](https://modelcontextprotocol.io) team at Anthropic for creating the specification
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
- <div align="center">
426
+ Release notes are in [CHANGELOG.md](CHANGELOG.md).
427
+
428
+ ## License
429
+
430
+ Released under the [MIT License](LICENSE.txt).
237
431
 
238
- Made with ❤️ for the Ruby community
432
+ ## Acknowledgments
239
433
 
240
- </div>
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.
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ McpOnRuby::Engine.routes.draw do
4
+ match "/", to: McpOnRuby::Endpoint.new, via: :all, as: :endpoint
5
+ end