mcp_on_ruby 1.0.0 → 2.0.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -4
  3. data/LICENSE.txt +1 -1
  4. data/README.md +350 -163
  5. data/config/routes.rb +5 -0
  6. data/lib/generators/mcp_on_ruby/expose/expose_generator.rb +108 -0
  7. data/lib/generators/mcp_on_ruby/install/install_generator.rb +37 -0
  8. data/lib/generators/mcp_on_ruby/install/templates/initializer.rb +22 -0
  9. data/lib/mcp_on_ruby/call_context.rb +29 -0
  10. data/lib/mcp_on_ruby/configuration.rb +63 -59
  11. data/lib/mcp_on_ruby/controller.rb +60 -0
  12. data/lib/mcp_on_ruby/declaration.rb +24 -0
  13. data/lib/mcp_on_ruby/dispatcher.rb +97 -0
  14. data/lib/mcp_on_ruby/doctor.rb +67 -0
  15. data/lib/mcp_on_ruby/endpoint.rb +43 -0
  16. data/lib/mcp_on_ruby/engine.rb +34 -0
  17. data/lib/mcp_on_ruby/registry.rb +146 -0
  18. data/lib/mcp_on_ruby/result.rb +83 -0
  19. data/lib/mcp_on_ruby/rspec.rb +8 -0
  20. data/lib/mcp_on_ruby/schema.rb +149 -0
  21. data/lib/mcp_on_ruby/server_factory.rb +70 -0
  22. data/lib/mcp_on_ruby/stdio.rb +42 -0
  23. data/lib/mcp_on_ruby/test_helper.rb +86 -0
  24. data/lib/mcp_on_ruby/tool_spec.rb +87 -0
  25. data/lib/mcp_on_ruby/tool_table.rb +31 -0
  26. data/lib/mcp_on_ruby/version.rb +2 -3
  27. data/lib/mcp_on_ruby/warden_hook.rb +21 -0
  28. data/lib/mcp_on_ruby.rb +44 -122
  29. data/lib/tasks/mcp_on_ruby.rake +20 -0
  30. metadata +65 -136
  31. data/CODE_OF_CONDUCT.md +0 -56
  32. data/CONTRIBUTING.md +0 -86
  33. data/bin/console +0 -11
  34. data/bin/setup +0 -6
  35. data/docs/advanced-usage.md +0 -132
  36. data/docs/api-reference.md +0 -35
  37. data/docs/testing.md +0 -55
  38. data/examples/claude/README.md +0 -171
  39. data/examples/claude/claude-bridge.js +0 -122
  40. data/lib/mcp_on_ruby/errors.rb +0 -137
  41. data/lib/mcp_on_ruby/generators/install_generator.rb +0 -46
  42. data/lib/mcp_on_ruby/generators/resource_generator.rb +0 -63
  43. data/lib/mcp_on_ruby/generators/templates/README +0 -31
  44. data/lib/mcp_on_ruby/generators/templates/application_resource.rb +0 -20
  45. data/lib/mcp_on_ruby/generators/templates/application_tool.rb +0 -18
  46. data/lib/mcp_on_ruby/generators/templates/initializer.rb +0 -41
  47. data/lib/mcp_on_ruby/generators/templates/resource.rb +0 -50
  48. data/lib/mcp_on_ruby/generators/templates/resource_spec.rb +0 -67
  49. data/lib/mcp_on_ruby/generators/templates/sample_resource.rb +0 -57
  50. data/lib/mcp_on_ruby/generators/templates/sample_tool.rb +0 -59
  51. data/lib/mcp_on_ruby/generators/templates/tool.rb +0 -38
  52. data/lib/mcp_on_ruby/generators/templates/tool_spec.rb +0 -55
  53. data/lib/mcp_on_ruby/generators/tool_generator.rb +0 -51
  54. data/lib/mcp_on_ruby/railtie.rb +0 -108
  55. data/lib/mcp_on_ruby/resource.rb +0 -161
  56. data/lib/mcp_on_ruby/server.rb +0 -378
  57. data/lib/mcp_on_ruby/tool.rb +0 -134
  58. data/lib/mcp_on_ruby/transport.rb +0 -330
data/README.md CHANGED
@@ -1,240 +1,427 @@
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
42
79
  ```
43
80
 
44
- ## Quick Start
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
89
+ ```
90
+
91
+ **3. Connect an agent**
92
+
93
+ Over HTTP, sending a token your app already accepts:
94
+
95
+ ```bash
96
+ claude mcp add --transport http my-app http://localhost:3000/mcp \
97
+ --header "Authorization: Bearer $MY_APP_API_TOKEN"
98
+ ```
45
99
 
46
- ### 1. Install and Configure
100
+ Or locally over stdio, acting as a user you choose ([set up `stdio_user`](#authentication) first):
47
101
 
48
102
  ```bash
49
- # Generate MCP server files
50
- rails generate mcp_on_ruby:install
103
+ claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
104
+ ```
105
+
106
+ Then ask: *"Which of my products cost more than $50?"*
107
+
108
+ ## How it works
109
+
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>
116
+
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.
118
+
119
+ 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.
120
+
121
+ ## Guides
51
122
 
52
- # Create a tool
53
- rails generate mcp_on_ruby:tool UserManager --description "Manage application users"
123
+ ### Declaring tools
54
124
 
55
- # Create a resource
56
- rails generate mcp_on_ruby:resource UserStats --uri "users/{id}/stats" --template
125
+ ```ruby
126
+ mcp_tool :index,
127
+ description: "Search products by name", # required: what the agent reads
128
+ title: "Search products", # optional display name
129
+ name: "search_products", # optional; defaults to "products_index"
130
+ params: {
131
+ q: :string, # shorthand
132
+ sort: { type: :string, enum: %w[name price], default: "name" },
133
+ limit: { type: :integer, minimum: 1, maximum: 100 }
134
+ }
57
135
  ```
58
136
 
59
- ### 2. Configure MCP Server
137
+ 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.
138
+
139
+ Use a block for nested input:
140
+
141
+ ```ruby
142
+ mcp_tool :create, description: "Place an order", write: true do
143
+ param :order, required: true do
144
+ param :sku, type: :string, required: true
145
+ param :quantity, type: :integer, minimum: 1
146
+ param :tags, type: :array, items: :string
147
+ param :note, type: :string, nullable: true
148
+ end
149
+ end
150
+ ```
151
+
152
+ 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.
153
+
154
+ 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.
155
+
156
+ ### Authentication
157
+
158
+ MCP on Ruby never asks you to build a new auth system. It hands the caller's identity to the auth you already have.
159
+
160
+ **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.
161
+
162
+ **Over stdio** there is no HTTP request, so you choose who local sessions act as:
60
163
 
61
164
  ```ruby
62
165
  # config/initializers/mcp_on_ruby.rb
63
166
  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
167
+ # Devise / Warden apps: sign in as a user for each tool call (nothing is stored in the session).
168
+ config.stdio_user = -> { User.find_by!(email: ENV.fetch("MCP_USER")) }
69
169
 
70
- Rails.application.configure do
71
- config.mcp.enabled = true
72
- config.mcp.auto_register_tools = true
73
- config.mcp.auto_register_resources = true
170
+ # Token-authenticated apps: send headers your app already accepts.
171
+ config.stdio_headers = -> { { "Authorization" => "Bearer #{ENV.fetch("MCP_TOKEN")}" } }
74
172
  end
75
173
  ```
76
174
 
77
- ### 3. Create Tools
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`.
176
+
177
+ If authentication fails, the agent gets a clear "Not authenticated (401)" tool error and nothing runs.
178
+
179
+ ### Authorization
180
+
181
+ 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)".
182
+
183
+ Inside a controller, `mcp_request?` tells you whether an agent is calling, and `mcp_tool_name` tells you which tool:
78
184
 
79
185
  ```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
186
+ before_action :require_admin, if: :mcp_request?
187
+ ```
97
188
 
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
189
+ ### Safety
190
+
191
+ Agents act on behalf of real users, so the defaults are conservative:
192
+
193
+ - **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`.
194
+ - **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:`.
195
+ - **A kill switch.** `config.allow_writes = false` hides and refuses every write tool, for example on a read-only replica or a demo environment.
196
+ - **No accidental exposure.** Only declared actions are reachable, and declarations without a route raise. `bin/rails mcp:tools` shows exactly what is exposed.
197
+ - **No leaked internals.** Server errors reach the agent as a generic error. Details go to `Rails.error` and your logs.
198
+ - **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.
199
+
200
+ See [SECURITY.md](SECURITY.md) for the full threat model.
201
+
202
+ ### Results and errors
203
+
204
+ | Your controller returns | The agent receives |
205
+ |---|---|
206
+ | `2xx` with JSON | `structuredContent` plus the JSON as text |
207
+ | `2xx` with plain text | The text |
208
+ | `204 No Content` | A short success message |
209
+ | `401` / `403` / `404` | Tool error: not authenticated / not permitted / not found |
210
+ | `422` with JSON | Tool error including your validation messages, so the agent can correct itself |
211
+ | 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) |
213
+ | HTML | Tool error asking for JSON. Use `respond_to` with `format.json`. |
214
+ | `5xx` or an exception | Generic tool error. Details are reported to `Rails.error`, never sent. |
215
+
216
+ ### Tools without a controller
113
217
 
114
- def authorize(context)
115
- # Add your authorization logic
116
- context[:authenticated] == true
218
+ For logic that has no endpoint, add a tool class from the official SDK. It runs alongside your controller tools:
219
+
220
+ ```ruby
221
+ class ReindexSearchTool < MCP::Tool
222
+ description "Rebuild the search index"
223
+
224
+ def self.call(server_context:)
225
+ SearchIndex.rebuild!
226
+ MCP::Tool::Response.new([{ type: "text", text: "Reindexed" }])
117
227
  end
118
228
  end
229
+
230
+ McpOnRuby.configure { |config| config.tools = [ReindexSearchTool] }
119
231
  ```
120
232
 
121
- ### 4. Create Resources
233
+ These tools bypass your controllers, so they also bypass your controller authentication. Use them for things that are safe for every caller.
122
234
 
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
235
+ ### Observability
134
236
 
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
237
+ Every tool call emits `tool_call.mcp_on_ruby` through `ActiveSupport::Notifications`:
151
238
 
152
- def authorize(context)
153
- # Check if user can access this data
154
- context[:authenticated] == true
155
- end
239
+ ```ruby
240
+ ActiveSupport::Notifications.subscribe("tool_call.mcp_on_ruby") do |event|
241
+ Rails.logger.info(
242
+ "MCP #{event.payload[:tool]} → #{event.payload[:status]} " \
243
+ "(#{event.payload[:transport]}, #{event.duration.round}ms)"
244
+ )
156
245
  end
157
246
  ```
158
247
 
159
- ### 5. Start Your Server
248
+ 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.
249
+
250
+ ### Connecting clients
251
+
252
+ <details>
253
+ <summary><b>Claude Code</b></summary>
160
254
 
161
255
  ```bash
162
- rails server
163
- # MCP server available at http://localhost:3000/mcp
256
+ # Remote or production
257
+ claude mcp add --transport http my-app https://my-app.example.com/mcp \
258
+ --header "Authorization: Bearer $MY_APP_API_TOKEN"
259
+
260
+ # Local, over stdio (run from your app's directory)
261
+ claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
164
262
  ```
263
+ </details>
264
+
265
+ <details>
266
+ <summary><b>Claude Desktop</b></summary>
165
267
 
166
- ## Architecture
268
+ Claude Desktop does not load your shell profile, so start Rails through a login shell to pick up rbenv, asdf, or chruby:
167
269
 
270
+ ```json
271
+ {
272
+ "mcpServers": {
273
+ "my-app": {
274
+ "command": "bash",
275
+ "args": ["-lc", "cd /path/to/my-app && bin/rails mcp:stdio"],
276
+ "env": { "MCP_USER": "alice@example.com" }
277
+ }
278
+ }
279
+ }
168
280
  ```
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
- └─────────────────────────────────────────────────────────────┘
281
+ </details>
282
+
283
+ <details>
284
+ <summary><b>Cursor</b></summary>
285
+
286
+ `.cursor/mcp.json`:
287
+
288
+ ```json
289
+ {
290
+ "mcpServers": {
291
+ "my-app": {
292
+ "url": "http://localhost:3000/mcp",
293
+ "headers": { "Authorization": "Bearer YOUR_API_TOKEN" }
294
+ }
295
+ }
296
+ }
187
297
  ```
298
+ </details>
188
299
 
189
- ## Examples
300
+ <details>
301
+ <summary><b>MCP Inspector</b></summary>
190
302
 
191
- Connect your Rails MCP server with different AI clients:
303
+ ```bash
304
+ # Browse and call tools in the browser
305
+ npx @modelcontextprotocol/inspector bin/rails mcp:stdio
192
306
 
193
- 👉 **[Claude Desktop](examples/claude/)** - Complete setup guide with bridge script
307
+ # Or from the command line
308
+ npx @modelcontextprotocol/inspector --cli http://localhost:3000/mcp --transport http \
309
+ --header "Authorization: Bearer $MY_APP_API_TOKEN" --method tools/list
310
+ ```
311
+ </details>
194
312
 
195
- ## Documentation
313
+ <details>
314
+ <summary><b>MCP registries and gateways</b></summary>
196
315
 
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
316
+ 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.
317
+ </details>
200
318
 
319
+ ## Testing
201
320
 
202
- ## Requirements
321
+ Test tools the way agents call them: through the MCP server and your real controllers.
203
322
 
204
- - Ruby 2.7.0 or higher
205
- - Rails 6.0 or higher (for full integration)
206
- - JSON Schema validation support
323
+ ```ruby
324
+ # spec/rails_helper.rb
325
+ require "mcp_on_ruby/rspec"
207
326
 
208
- ## Dependencies
327
+ # test/test_helper.rb
328
+ class ActiveSupport::TestCase
329
+ include McpOnRuby::TestHelper
330
+ end
331
+ ```
209
332
 
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
333
+ ```ruby
334
+ it "shows agents only their own orders" do
335
+ result = mcp_call :orders_index, { status: "pending" }, headers: { "Authorization" => "Bearer #{alice.api_token}" }
214
336
 
215
- ## Contributing
337
+ expect(result).to be_success
338
+ expect(result.structured.pluck("id")).to eq([alice_order.id])
339
+ end
216
340
 
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
341
+ it "stops agents from reading other users' orders" do
342
+ result = mcp_call :orders_show, { id: bob_order.id }, as: alice # signs in through Devise/Warden
224
343
 
225
- ## License
344
+ expect(result).to be_error
345
+ expect(result.status).to eq(404)
346
+ end
347
+ ```
348
+
349
+ `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.
350
+
351
+ ## Configuration
352
+
353
+ ```ruby
354
+ # config/initializers/mcp_on_ruby.rb
355
+ McpOnRuby.configure do |config|
356
+ config.server_name = "my-app"
357
+ config.instructions = "Tools for managing orders in My App. Amounts are in cents."
358
+ config.stdio_user = -> { User.find_by!(email: ENV.fetch("MCP_USER")) }
359
+ end
360
+ ```
226
361
 
227
- This project is licensed under the MIT License - see the [LICENSE](LICENSE.txt) file for details.
362
+ | Option | Default | Description |
363
+ |---|---|---|
364
+ | `server_name` | app name, dasherized | Name reported to clients |
365
+ | `server_version` | `nil` | Version reported to clients |
366
+ | `instructions` | `nil` | Guidance sent to the agent when it connects |
367
+ | `stdio_user` | `nil` | Callable returning the user `mcp:stdio` sessions sign in as (Devise/Warden) |
368
+ | `stdio_headers` | `nil` | Callable returning headers `mcp:stdio` sessions send (token auth) |
369
+ | `warden_scope` | `:user` | Warden scope used for `stdio_user` and `mcp_call(as:)` |
370
+ | `allow_writes` | `true` | Set `false` to hide and refuse every `write: true` tool |
371
+ | `tools` | `[]` | Extra `MCP::Tool` classes served alongside controller tools |
372
+ | `allowed_origins` | `[]` | Browser origins allowed to call the endpoint, in addition to same-origin |
373
+ | `max_request_bytes` | 4 MiB | Largest MCP request body accepted |
374
+ | `tool_name` | `"<controller>_<action>"` | Callable `(controller_path, action)` returning default tool names |
375
+
376
+ ### Commands
377
+
378
+ | Command | What it does |
379
+ |---|---|
380
+ | `bin/rails g mcp_on_ruby:install [--path=/mcp]` | Adds the initializer and mounts the endpoint |
381
+ | `bin/rails g mcp_on_ruby:expose CONTROLLER ACTION...` | Adds `mcp_tool` declarations to a controller |
382
+ | `bin/rails mcp:tools` | Lists exposed tools, their routes, and their access level |
383
+ | `bin/rails mcp:stdio` | Serves your tools over stdio for local agents |
384
+ | `bin/rails mcp:doctor` | Checks the mount, declarations, and stdio identity; exits non-zero on errors |
385
+
386
+ ## Compatibility
387
+
388
+ | | Supported |
389
+ |---|---|
390
+ | Ruby | 3.2, 3.3, 3.4, 4.0 |
391
+ | Rails | 7.1, 7.2, 8.0, 8.1 |
392
+ | MCP protocol | Every version the official `mcp` gem supports, negotiated per client |
393
+ | Transports | Streamable HTTP (stateless), stdio |
394
+ | Controllers | `ActionController::Base` and `ActionController::API` |
395
+
396
+ CI runs the full suite on every supported Ruby and Rails combination.
397
+
398
+ ## When to use something else
399
+
400
+ MCP on Ruby is for exposing an **existing Rails app** to agents. Other tools fit other jobs:
401
+
402
+ - **[`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.
403
+ - **[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.
404
+ - **[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
405
 
229
- ## 🙏 Acknowledgments
406
+ ## Contributing
230
407
 
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
408
+ Bug reports, ideas, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
233
409
 
234
- ---
410
+ ```bash
411
+ git clone https://github.com/nagstler/mcp_on_ruby && cd mcp_on_ruby
412
+ bin/setup
413
+ bundle exec rake # specs + RuboCop
414
+ RAILS_VERSION=7.2 bundle update && bundle exec rspec # another Rails series
415
+ ```
235
416
 
236
- <div align="center">
417
+ Release notes are in [CHANGELOG.md](CHANGELOG.md).
418
+
419
+ ## License
420
+
421
+ Released under the [MIT License](LICENSE.txt).
237
422
 
238
- Made with ❤️ for the Ruby community
423
+ ## Acknowledgments
239
424
 
240
- </div>
425
+ - The [Model Context Protocol](https://modelcontextprotocol.io) team for the specification.
426
+ - The maintainers of the official [Ruby SDK](https://github.com/modelcontextprotocol/ruby-sdk), on whose work this gem is built.
427
+ - 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