mcp_on_ruby 0.3.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 +90 -26
- data/LICENSE.txt +2 -2
- data/README.md +308 -462
- 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 +78 -0
- 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 +5 -0
- data/lib/mcp_on_ruby/warden_hook.rb +21 -0
- data/lib/mcp_on_ruby.rb +64 -0
- data/lib/tasks/mcp_on_ruby.rake +20 -0
- metadata +63 -245
- data/CODE_OF_CONDUCT.md +0 -84
- data/CONTRIBUTING.md +0 -92
- data/lib/ruby_mcp/client.rb +0 -43
- data/lib/ruby_mcp/configuration.rb +0 -90
- data/lib/ruby_mcp/errors.rb +0 -17
- data/lib/ruby_mcp/models/context.rb +0 -52
- data/lib/ruby_mcp/models/engine.rb +0 -31
- data/lib/ruby_mcp/models/message.rb +0 -60
- data/lib/ruby_mcp/providers/anthropic.rb +0 -269
- data/lib/ruby_mcp/providers/base.rb +0 -57
- data/lib/ruby_mcp/providers/openai.rb +0 -265
- data/lib/ruby_mcp/schemas.rb +0 -56
- data/lib/ruby_mcp/server/app.rb +0 -84
- data/lib/ruby_mcp/server/base_controller.rb +0 -49
- data/lib/ruby_mcp/server/content_controller.rb +0 -68
- data/lib/ruby_mcp/server/contexts_controller.rb +0 -67
- data/lib/ruby_mcp/server/controller.rb +0 -29
- data/lib/ruby_mcp/server/engines_controller.rb +0 -34
- data/lib/ruby_mcp/server/generate_controller.rb +0 -140
- data/lib/ruby_mcp/server/messages_controller.rb +0 -30
- data/lib/ruby_mcp/server/router.rb +0 -84
- data/lib/ruby_mcp/storage/active_record.rb +0 -414
- data/lib/ruby_mcp/storage/base.rb +0 -43
- data/lib/ruby_mcp/storage/error.rb +0 -8
- data/lib/ruby_mcp/storage/memory.rb +0 -69
- data/lib/ruby_mcp/storage/redis.rb +0 -197
- data/lib/ruby_mcp/storage_factory.rb +0 -43
- data/lib/ruby_mcp/validator.rb +0 -45
- data/lib/ruby_mcp/version.rb +0 -6
- data/lib/ruby_mcp.rb +0 -71
data/README.md
CHANGED
|
@@ -1,590 +1,436 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
3
|
# MCP on Ruby
|
|
4
|
-
[](https://badge.fury.io/rb/mcp_on_ruby)
|
|
5
|
-
[](https://github.com/nagstler/ruby_mcp/actions/workflows/build.yml)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://github.com/nagstler/ruby_mcp/actions/workflows/rubocop.yml)
|
|
8
|
-
[](https://github.com/nagstler/ruby_mcp/actions/workflows/test.yml)
|
|
9
|
-
[](https://codecov.io/github/nagstler/ruby_mcp)
|
|
10
4
|
|
|
11
|
-
|
|
5
|
+
**Make your Rails app usable by AI agents.**
|
|
6
|
+
|
|
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.
|
|
9
|
+
|
|
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)
|
|
15
|
+
|
|
16
|
+
[Quick start](#quick-start) ·
|
|
17
|
+
[How it works](#how-it-works) ·
|
|
18
|
+
[Guides](#guides) ·
|
|
19
|
+
[Configuration](#configuration) ·
|
|
20
|
+
[Testing](#testing)
|
|
12
21
|
|
|
13
22
|
</div>
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
The [Model Context Protocol](https://modelcontextprotocol.io) standardizes how applications interact with AI models, serving as the "REST for LLMs." **MCP on Ruby** brings this standard to the Ruby ecosystem. Create contexts, manage conversations, connect to multiple providers, and handle streaming responses with clean, Ruby code.
|
|
24
|
+
---
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
```ruby
|
|
27
|
+
class OrdersController < ApplicationController
|
|
28
|
+
before_action :authenticate_user!
|
|
19
29
|
|
|
20
|
-
|
|
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"
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
def index
|
|
35
|
+
render json: current_user.orders.where(params.permit(:status))
|
|
36
|
+
end
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- [⚙️ Configuration Options](#️-configuration-options)
|
|
30
|
-
- [🛣️ Server Endpoints](#️-server-endpoints)
|
|
31
|
-
- [📚 Detailed Usage](#-detailed-usage)
|
|
32
|
-
- [Creating a Context](#creating-a-context)
|
|
33
|
-
- [Adding a Message](#adding-a-message)
|
|
34
|
-
- [Generating a Response](#generating-a-response)
|
|
35
|
-
- [Streaming a Response](#streaming-a-response)
|
|
36
|
-
- [Uploading Content](#uploading-content)
|
|
37
|
-
- [Using Tool Calls](#using-tool-calls)
|
|
38
|
-
- [🚄 Rails Integration](#-rails-integration)
|
|
39
|
-
- [💾 Storage Backend](#-storage-backends)
|
|
40
|
-
- [🔒 Authentication](#-authentication)
|
|
41
|
-
- [🛠️ Development](#️-development)
|
|
42
|
-
- [🗺️ Roadmap](#️-roadmap)
|
|
43
|
-
- [👥 Contributing](#-contributing)
|
|
44
|
-
- [📄 License](#-license)
|
|
38
|
+
def show
|
|
39
|
+
render json: current_user.orders.find(params[:id])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
45
43
|
|
|
46
|
-
|
|
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.
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
## Why MCP on Ruby
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
- **Complete Protocol Implementation:** Fully implements the MCP specification for compatibility
|
|
52
|
-
- **Conversation Management:** Context handling for multi-turn conversations
|
|
53
|
-
- **Flexible Storage:** Extensible storage backends
|
|
54
|
-
- **Streaming Support:** Real-time response streaming for dynamic UIs
|
|
55
|
-
- **File Handling:** Upload and reference files in conversations
|
|
56
|
-
- **Tool Calling:** Support for LLM function calling capabilities
|
|
57
|
-
- **Battle-Tested:** Comprehensive test suite ensures reliability
|
|
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.
|
|
58
49
|
|
|
59
|
-
|
|
50
|
+
MCP on Ruby takes the opposite approach. **Your app already has an API: your controllers.** MCP on Ruby makes it callable by agents.
|
|
60
51
|
|
|
61
|
-
|
|
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+.
|
|
62
58
|
|
|
63
|
-
|
|
59
|
+
## Quick start
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
**1. Install**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bundle add mcp_on_ruby
|
|
65
|
+
bin/rails generate mcp_on_ruby:install
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
The generator creates `config/initializers/mcp_on_ruby.rb` and mounts the endpoint:
|
|
70
69
|
|
|
71
|
-
```
|
|
72
|
-
|
|
70
|
+
```ruby
|
|
71
|
+
# config/routes.rb
|
|
72
|
+
mount McpOnRuby::Engine, at: "/mcp"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
**2. Expose some actions**
|
|
76
76
|
|
|
77
|
+
```bash
|
|
78
|
+
bin/rails generate mcp_on_ruby:expose products index show create
|
|
77
79
|
```
|
|
78
|
-
|
|
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
|
|
79
89
|
```
|
|
80
90
|
|
|
81
|
-
|
|
91
|
+
**3. Connect an agent**
|
|
82
92
|
|
|
83
|
-
|
|
93
|
+
Over HTTP, sending a token your app already accepts:
|
|
84
94
|
|
|
85
|
-
```
|
|
86
|
-
|
|
95
|
+
```bash
|
|
96
|
+
claude mcp add --transport http my-app http://localhost:3000/mcp \
|
|
97
|
+
--header "Authorization: Bearer $MY_APP_API_TOKEN"
|
|
98
|
+
```
|
|
87
99
|
|
|
88
|
-
#
|
|
89
|
-
RubyMCP.configure do |config|
|
|
90
|
-
config.providers = {
|
|
91
|
-
openai: { api_key: ENV['OPENAI_API_KEY'] },
|
|
92
|
-
anthropic: { api_key: ENV['ANTHROPIC_API_KEY'] }
|
|
93
|
-
}
|
|
94
|
-
end
|
|
100
|
+
Or locally over stdio, acting as a user you choose ([set up `stdio_user`](#authentication) first):
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
server.start
|
|
102
|
+
```bash
|
|
103
|
+
claude mcp add my-app -e MCP_USER=alice@example.com -- bin/rails mcp:stdio
|
|
99
104
|
```
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
Then ask: *"Which of my products cost more than $50?"*
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
## How it works
|
|
104
109
|
|
|
105
|
-
```bash
|
|
106
|
-
# Terminal 1: Start the server
|
|
107
|
-
cd examples/simple_server
|
|
108
|
-
ruby server.rb
|
|
109
|
-
|
|
110
|
-
# Terminal 2: Run the client
|
|
111
|
-
cd examples/simple_server
|
|
112
|
-
ruby client.rb
|
|
113
|
-
|
|
114
|
-
# ActiveRecord Storage Demo
|
|
115
|
-
# Demonstrates database storage with SQLite
|
|
116
|
-
cd examples/simple_server
|
|
117
|
-
ruby activerecord_demo.rb
|
|
118
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.
|
|
119
127
|
|
|
120
|
-
|
|
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.
|
|
121
129
|
|
|
122
|
-
##
|
|
130
|
+
## Guides
|
|
123
131
|
|
|
124
|
-
|
|
132
|
+
### Declaring tools
|
|
125
133
|
|
|
126
134
|
```ruby
|
|
127
|
-
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
api_key: ENV['ANTHROPIC_API_KEY']
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
# Storage backend
|
|
140
|
-
|
|
141
|
-
# Option 1: Memory storage (default)
|
|
142
|
-
config.storage = :memory
|
|
143
|
-
|
|
144
|
-
# Option 2: Redis storage
|
|
145
|
-
config.storage = :redis
|
|
146
|
-
config.redis = {
|
|
147
|
-
url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'),
|
|
148
|
-
namespace: 'my_app_mcp',
|
|
149
|
-
ttl: 86400 # 1 day in seconds
|
|
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 }
|
|
150
143
|
}
|
|
151
|
-
|
|
152
|
-
# Option 3: ActiveRecord storage
|
|
153
|
-
config.storage = :active_record
|
|
154
|
-
config.active_record = {
|
|
155
|
-
# Connection settings (not needed in Rails)
|
|
156
|
-
connection: {
|
|
157
|
-
adapter: 'sqlite3',
|
|
158
|
-
database: 'db/mcp.sqlite3'
|
|
159
|
-
},
|
|
160
|
-
# Table prefix to avoid name collisions
|
|
161
|
-
table_prefix: 'mcp_'
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
# Server settings
|
|
165
|
-
config.server_port = 3000
|
|
166
|
-
config.server_host = "0.0.0.0"
|
|
167
|
-
|
|
168
|
-
# Authentication settings
|
|
169
|
-
config.auth_required = false
|
|
170
|
-
config.jwt_secret = ENV['JWT_SECRET']
|
|
171
|
-
config.token_expiry = 3600 # 1 hour
|
|
172
|
-
|
|
173
|
-
# Limits
|
|
174
|
-
config.max_contexts = 1000
|
|
175
|
-
end
|
|
176
144
|
```
|
|
177
145
|
|
|
178
|
-
|
|
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.
|
|
179
147
|
|
|
180
|
-
|
|
148
|
+
Use a block for nested input:
|
|
181
149
|
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
```
|
|
184
160
|
|
|
185
|
-
|
|
186
|
-
- `POST /contexts` - Create a new conversation context
|
|
187
|
-
- `GET /contexts` - List existing contexts
|
|
188
|
-
- `GET /contexts/:id` - Get details of a specific context
|
|
189
|
-
- `DELETE /contexts/:id` - Delete a context
|
|
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.
|
|
190
162
|
|
|
191
|
-
|
|
192
|
-
- `POST /messages` - Add a message to a context
|
|
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.
|
|
193
164
|
|
|
194
|
-
###
|
|
195
|
-
- `POST /generate` - Generate a response from a language model
|
|
196
|
-
- `POST /generate/stream` - Stream a response with incremental updates
|
|
165
|
+
### Authentication
|
|
197
166
|
|
|
198
|
-
|
|
199
|
-
- `POST /content` - Upload content (files)
|
|
200
|
-
- `GET /content/:context_id/:id` - Retrieve uploaded content
|
|
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.
|
|
201
168
|
|
|
202
|
-
|
|
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.
|
|
203
170
|
|
|
204
|
-
|
|
171
|
+
**Over stdio** there is no HTTP request, so you choose who local sessions act as:
|
|
205
172
|
|
|
206
173
|
```ruby
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
messages: [
|
|
212
|
-
{
|
|
213
|
-
role: "system",
|
|
214
|
-
content: "You are a helpful assistant."
|
|
215
|
-
}
|
|
216
|
-
],
|
|
217
|
-
metadata: {
|
|
218
|
-
user_id: "user_123",
|
|
219
|
-
conversation_name: "Technical Support"
|
|
220
|
-
}
|
|
221
|
-
}.to_json,
|
|
222
|
-
"Content-Type" => "application/json"
|
|
223
|
-
)
|
|
174
|
+
# config/initializers/mcp_on_ruby.rb
|
|
175
|
+
McpOnRuby.configure do |config|
|
|
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")) }
|
|
224
178
|
|
|
225
|
-
|
|
179
|
+
# Token-authenticated apps: send headers your app already accepts.
|
|
180
|
+
config.stdio_headers = -> { { "Authorization" => "Bearer #{ENV.fetch("MCP_TOKEN")}" } }
|
|
181
|
+
end
|
|
226
182
|
```
|
|
227
183
|
|
|
228
|
-
|
|
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`.
|
|
229
185
|
|
|
230
|
-
|
|
231
|
-
Faraday.post(
|
|
232
|
-
"http://localhost:3000/messages",
|
|
233
|
-
{
|
|
234
|
-
context_id: context_id,
|
|
235
|
-
role: "user",
|
|
236
|
-
content: "What is the capital of France?"
|
|
237
|
-
}.to_json,
|
|
238
|
-
"Content-Type" => "application/json"
|
|
239
|
-
)
|
|
240
|
-
```
|
|
186
|
+
If authentication fails, the agent gets a clear "Not authenticated (401)" tool error and nothing runs.
|
|
241
187
|
|
|
242
|
-
###
|
|
188
|
+
### Authorization
|
|
243
189
|
|
|
244
|
-
|
|
245
|
-
response = Faraday.post(
|
|
246
|
-
"http://localhost:3000/generate",
|
|
247
|
-
{
|
|
248
|
-
context_id: context_id,
|
|
249
|
-
engine_id: "anthropic/claude-3-sonnet-20240229",
|
|
250
|
-
max_tokens: 1000,
|
|
251
|
-
temperature: 0.7
|
|
252
|
-
}.to_json,
|
|
253
|
-
"Content-Type" => "application/json"
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
assistant_response = JSON.parse(response.body)["content"]
|
|
257
|
-
```
|
|
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)".
|
|
258
191
|
|
|
259
|
-
|
|
192
|
+
Inside a controller, `mcp_request?` tells you whether an agent is calling, and `mcp_tool_name` tells you which tool:
|
|
260
193
|
|
|
261
194
|
```ruby
|
|
262
|
-
|
|
263
|
-
f.request :json
|
|
264
|
-
f.response :json
|
|
265
|
-
f.adapter :net_http
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
conn.post("http://localhost:3000/generate/stream") do |req|
|
|
269
|
-
req.headers["Content-Type"] = "application/json"
|
|
270
|
-
req.body = {
|
|
271
|
-
context_id: context_id,
|
|
272
|
-
engine_id: "openai/gpt-4",
|
|
273
|
-
temperature: 0.7
|
|
274
|
-
}.to_json
|
|
275
|
-
|
|
276
|
-
req.options.on_data = Proc.new do |chunk, size, total|
|
|
277
|
-
event_data = chunk.split("data: ").last.strip
|
|
278
|
-
next if event_data.empty? || event_data == "[DONE]"
|
|
279
|
-
|
|
280
|
-
event = JSON.parse(event_data)
|
|
281
|
-
if event["event"] == "generation.content" && event["content"]
|
|
282
|
-
print event["content"]
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
end
|
|
195
|
+
before_action :require_admin, if: :mcp_request?
|
|
286
196
|
```
|
|
287
197
|
|
|
288
|
-
###
|
|
198
|
+
### Safety
|
|
289
199
|
|
|
290
|
-
|
|
291
|
-
file_data = Base64.strict_encode64(File.read("example.pdf"))
|
|
292
|
-
|
|
293
|
-
Faraday.post(
|
|
294
|
-
"http://localhost:3000/content",
|
|
295
|
-
{
|
|
296
|
-
context_id: context_id,
|
|
297
|
-
type: "file",
|
|
298
|
-
filename: "example.pdf",
|
|
299
|
-
content_type: "application/pdf",
|
|
300
|
-
file_data: file_data
|
|
301
|
-
}.to_json,
|
|
302
|
-
"Content-Type" => "application/json"
|
|
303
|
-
)
|
|
304
|
-
```
|
|
200
|
+
Agents act on behalf of real users, so the defaults are conservative:
|
|
305
201
|
|
|
306
|
-
|
|
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.
|
|
307
208
|
|
|
308
|
-
|
|
309
|
-
tools = [
|
|
310
|
-
{
|
|
311
|
-
type: "function",
|
|
312
|
-
function: {
|
|
313
|
-
name: "get_weather",
|
|
314
|
-
description: "Get the current weather for a location",
|
|
315
|
-
parameters: {
|
|
316
|
-
type: "object",
|
|
317
|
-
properties: {
|
|
318
|
-
location: {
|
|
319
|
-
type: "string",
|
|
320
|
-
description: "City and state, e.g., San Francisco, CA"
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
required: ["location"]
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
]
|
|
328
|
-
|
|
329
|
-
response = Faraday.post(
|
|
330
|
-
"http://localhost:3000/generate",
|
|
331
|
-
{
|
|
332
|
-
context_id: context_id,
|
|
333
|
-
engine_id: "openai/gpt-4",
|
|
334
|
-
tools: tools
|
|
335
|
-
}.to_json,
|
|
336
|
-
"Content-Type" => "application/json"
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
if response.body["tool_calls"]
|
|
340
|
-
# Handle tool calls
|
|
341
|
-
tool_calls = response.body["tool_calls"]
|
|
342
|
-
# Process tool calls and add tool response message
|
|
343
|
-
end
|
|
344
|
-
```
|
|
209
|
+
See [SECURITY.md](SECURITY.md) for the full threat model.
|
|
345
210
|
|
|
346
|
-
|
|
211
|
+
### Results and errors
|
|
347
212
|
|
|
348
|
-
|
|
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. |
|
|
224
|
+
|
|
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:
|
|
349
228
|
|
|
350
229
|
```ruby
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
# Use memory storage in development, consider persistent storage in production
|
|
358
|
-
if Rails.env.development? || Rails.env.test?
|
|
359
|
-
config.storage = :memory
|
|
360
|
-
else
|
|
361
|
-
# Use ActiveRecord for production (uses your Rails database)
|
|
362
|
-
config.storage = :active_record
|
|
363
|
-
config.active_record = {
|
|
364
|
-
table_prefix: "mcp_#{Rails.env}_" # Environment-specific prefix
|
|
365
|
-
}
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
# Enable authentication in production
|
|
369
|
-
if Rails.env.production?
|
|
370
|
-
config.auth_required = true
|
|
371
|
-
config.jwt_secret = ENV["JWT_SECRET"]
|
|
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" }])
|
|
372
236
|
end
|
|
373
237
|
end
|
|
238
|
+
|
|
239
|
+
McpOnRuby.configure { |config| config.tools = [ReindexSearchTool] }
|
|
374
240
|
```
|
|
375
241
|
|
|
376
|
-
|
|
242
|
+
These tools bypass your controllers, so they also bypass your controller authentication. Use them for things that are safe for every caller.
|
|
243
|
+
|
|
244
|
+
### Observability
|
|
245
|
+
|
|
246
|
+
Every tool call emits `tool_call.mcp_on_ruby` through `ActiveSupport::Notifications`:
|
|
377
247
|
|
|
378
248
|
```ruby
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
env["SCRIPT_NAME"] = mount_mcp_at if env["PATH_INFO"].start_with?(mount_mcp_at)
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
mount RubyMCP::Server::App.new.rack_app, at: mount_mcp_at
|
|
388
|
-
|
|
389
|
-
# Rest of your routes
|
|
390
|
-
# ...
|
|
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
|
+
)
|
|
391
254
|
end
|
|
392
255
|
```
|
|
393
256
|
|
|
394
|
-
|
|
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.
|
|
395
258
|
|
|
396
|
-
###
|
|
259
|
+
### Connecting clients
|
|
397
260
|
|
|
398
|
-
|
|
261
|
+
<details>
|
|
262
|
+
<summary><b>Claude Code</b></summary>
|
|
399
263
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
2. Configure Redis storage:
|
|
406
|
-
```ruby
|
|
407
|
-
RubyMCP.configure do |config|
|
|
408
|
-
config.storage = :redis
|
|
409
|
-
config.redis = {
|
|
410
|
-
url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'),
|
|
411
|
-
namespace: "app_mcp_#{Rails.env}",
|
|
412
|
-
ttl: 86400 # 1 day in seconds
|
|
413
|
-
}
|
|
414
|
-
end
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
3. Access the configured client:
|
|
418
|
-
```ruby
|
|
419
|
-
client = RubyMCP.client
|
|
420
|
-
```
|
|
264
|
+
```bash
|
|
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"
|
|
421
268
|
|
|
422
|
-
|
|
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
|
|
271
|
+
```
|
|
272
|
+
</details>
|
|
423
273
|
|
|
424
|
-
|
|
274
|
+
<details>
|
|
275
|
+
<summary><b>Claude Desktop</b></summary>
|
|
425
276
|
|
|
426
|
-
|
|
277
|
+
Claude Desktop does not load your shell profile, so start Rails through a login shell to pick up rbenv, asdf, or chruby:
|
|
427
278
|
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
config.active_record = {
|
|
437
|
-
# Connection (not needed in Rails)
|
|
438
|
-
connection: {
|
|
439
|
-
adapter: 'sqlite3',
|
|
440
|
-
database: 'db/mcp.sqlite3'
|
|
441
|
-
},
|
|
442
|
-
# Table prefix to avoid name collisions
|
|
443
|
-
table_prefix: 'mcp_'
|
|
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
|
+
}
|
|
444
287
|
}
|
|
445
|
-
|
|
288
|
+
}
|
|
446
289
|
```
|
|
290
|
+
</details>
|
|
447
291
|
|
|
448
|
-
|
|
292
|
+
<details>
|
|
293
|
+
<summary><b>Cursor</b></summary>
|
|
449
294
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
295
|
+
`.cursor/mcp.json`:
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"mcpServers": {
|
|
300
|
+
"my-app": {
|
|
301
|
+
"url": "http://localhost:3000/mcp",
|
|
302
|
+
"headers": { "Authorization": "Bearer YOUR_API_TOKEN" }
|
|
303
|
+
}
|
|
456
304
|
}
|
|
457
|
-
|
|
305
|
+
}
|
|
458
306
|
```
|
|
307
|
+
</details>
|
|
459
308
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
### Custom storage
|
|
463
|
-
You can implement custom storage backends by extending the base storage class:
|
|
309
|
+
<details>
|
|
310
|
+
<summary><b>MCP Inspector</b></summary>
|
|
464
311
|
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
super
|
|
469
|
-
@redis = Redis.new(options)
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
def create_context(context)
|
|
473
|
-
@redis.set("context:#{context.id}", JSON.dump(context.to_h))
|
|
474
|
-
context
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
def get_context(context_id)
|
|
478
|
-
data = @redis.get("context:#{context_id}")
|
|
479
|
-
raise RubyMCP::Errors::ContextError, "Context not found: #{context_id}" unless data
|
|
480
|
-
|
|
481
|
-
hash = JSON.parse(data, symbolize_names: true)
|
|
482
|
-
|
|
483
|
-
# Create message objects
|
|
484
|
-
messages = hash[:messages].map do |msg|
|
|
485
|
-
RubyMCP::Models::Message.new(
|
|
486
|
-
role: msg[:role],
|
|
487
|
-
content: msg[:content],
|
|
488
|
-
id: msg[:id],
|
|
489
|
-
metadata: msg[:metadata]
|
|
490
|
-
)
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
# Create the context
|
|
494
|
-
RubyMCP::Models::Context.new(
|
|
495
|
-
id: hash[:id],
|
|
496
|
-
messages: messages,
|
|
497
|
-
metadata: hash[:metadata]
|
|
498
|
-
)
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# Implement other required methods...
|
|
502
|
-
end
|
|
312
|
+
```bash
|
|
313
|
+
# Browse and call tools in the browser
|
|
314
|
+
npx @modelcontextprotocol/inspector bin/rails mcp:stdio
|
|
503
315
|
|
|
504
|
-
#
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
end
|
|
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
|
|
508
319
|
```
|
|
320
|
+
</details>
|
|
509
321
|
|
|
510
|
-
|
|
322
|
+
<details>
|
|
323
|
+
<summary><b>MCP registries and gateways</b></summary>
|
|
511
324
|
|
|
512
|
-
|
|
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>
|
|
327
|
+
|
|
328
|
+
## Testing
|
|
329
|
+
|
|
330
|
+
Test tools the way agents call them: through the MCP server and your real controllers.
|
|
513
331
|
|
|
514
332
|
```ruby
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
333
|
+
# spec/rails_helper.rb
|
|
334
|
+
require "mcp_on_ruby/rspec"
|
|
335
|
+
|
|
336
|
+
# test/test_helper.rb
|
|
337
|
+
class ActiveSupport::TestCase
|
|
338
|
+
include McpOnRuby::TestHelper
|
|
519
339
|
end
|
|
520
340
|
```
|
|
521
341
|
|
|
522
|
-
Then, create and use JWT tokens:
|
|
523
|
-
|
|
524
342
|
```ruby
|
|
525
|
-
|
|
526
|
-
|
|
343
|
+
it "shows agents only their own orders" do
|
|
344
|
+
result = mcp_call :orders_index, { status: "pending" }, headers: { "Authorization" => "Bearer #{alice.api_token}" }
|
|
527
345
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
346
|
+
expect(result).to be_success
|
|
347
|
+
expect(result.structured.pluck("id")).to eq([alice_order.id])
|
|
348
|
+
end
|
|
532
349
|
|
|
533
|
-
|
|
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
|
|
534
352
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
f.request :json
|
|
538
|
-
f.response :json
|
|
539
|
-
f.adapter :net_http
|
|
353
|
+
expect(result).to be_error
|
|
354
|
+
expect(result.status).to eq(404)
|
|
540
355
|
end
|
|
356
|
+
```
|
|
357
|
+
|
|
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.
|
|
359
|
+
|
|
360
|
+
## Configuration
|
|
541
361
|
|
|
542
|
-
|
|
543
|
-
|
|
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")) }
|
|
544
368
|
end
|
|
545
369
|
```
|
|
546
370
|
|
|
547
|
-
|
|
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 |
|
|
548
384
|
|
|
549
|
-
|
|
385
|
+
### Commands
|
|
550
386
|
|
|
551
|
-
|
|
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 |
|
|
552
394
|
|
|
553
|
-
|
|
554
|
-
bundle exec rspec
|
|
555
|
-
```
|
|
395
|
+
## Compatibility
|
|
556
396
|
|
|
557
|
-
|
|
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` |
|
|
558
404
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
405
|
+
CI runs the full suite on every supported Ruby and Rails combination.
|
|
406
|
+
|
|
407
|
+
## When to use something else
|
|
562
408
|
|
|
563
|
-
|
|
409
|
+
MCP on Ruby is for exposing an **existing Rails app** to agents. Other tools fit other jobs:
|
|
564
410
|
|
|
565
|
-
|
|
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*.
|
|
566
414
|
|
|
567
|
-
|
|
568
|
-
- [x] ActiveRecord storage backend
|
|
569
|
-
- [ ] Complete test coverage, including integration tests
|
|
570
|
-
- [ ] Improved error handling and recovery strategies
|
|
571
|
-
- [ ] Rate limiting for provider APIs
|
|
572
|
-
- [ ] Proper tokenization for context window management
|
|
573
|
-
- [ ] More robust streaming implementation
|
|
574
|
-
- [ ] Additional provider integrations
|
|
415
|
+
## Contributing
|
|
575
416
|
|
|
576
|
-
|
|
417
|
+
Bug reports, ideas, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
418
|
+
|
|
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
|
+
```
|
|
577
425
|
|
|
578
|
-
|
|
426
|
+
Release notes are in [CHANGELOG.md](CHANGELOG.md).
|
|
579
427
|
|
|
580
|
-
|
|
581
|
-
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
|
582
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
583
|
-
4. Push to the branch (`git push origin feature/my-new-feature`)
|
|
584
|
-
5. Create a new Pull Request
|
|
428
|
+
## License
|
|
585
429
|
|
|
586
|
-
|
|
430
|
+
Released under the [MIT License](LICENSE.txt).
|
|
587
431
|
|
|
588
|
-
##
|
|
432
|
+
## Acknowledgments
|
|
589
433
|
|
|
590
|
-
The
|
|
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.
|