active_mcp 0.3.3 โ 0.3.5
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/README.md +178 -197
- data/app/controllers/active_mcp/base_controller.rb +1 -67
- data/app/controllers/concerns/active_mcp/request_handler.rb +91 -0
- data/app/models/active_mcp/tool_executor.rb +25 -14
- data/app/views/active_mcp/cancelled.json.jbuilder +2 -0
- data/app/views/active_mcp/initialize.json.jbuilder +21 -0
- data/app/views/active_mcp/initialized.json.jbuilder +2 -0
- data/app/views/active_mcp/no_method.json.jbuilder +4 -0
- data/app/views/active_mcp/tools_call.json.jbuilder +9 -0
- data/app/views/active_mcp/tools_list.json.jbuilder +10 -0
- data/lib/active_mcp/tool.rb +17 -1
- data/lib/active_mcp/version.rb +1 -1
- metadata +22 -10
- data/app/models/active_mcp/response/cancelled.rb +0 -12
- data/app/models/active_mcp/response/initialize.rb +0 -31
- data/app/models/active_mcp/response/initialized.rb +0 -12
- data/app/models/active_mcp/response/no_method.rb +0 -11
- data/app/models/active_mcp/response/tools.rb +0 -17
- data/app/models/active_mcp/response/tools_call/json.rb +0 -11
- data/app/models/active_mcp/response/tools_call/jsonrpc.rb +0 -16
- data/app/models/active_mcp/response/tools_list/json.rb +0 -13
- data/app/models/active_mcp/response/tools_list/jsonrpc.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36a188cb2b45d872122cddac68b6d7ce052c432d11e36dab471b0b050b86cecc
|
4
|
+
data.tar.gz: 1704a7bb34a996d3168a419bc425147eccc941d1f9c4b11f14abc38c3632e41a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0380f2010e79b71a2beb552448129eebe646895fb2af2c35490d093b93714e45453da3403f6c1a85b3bd3ea8df84ab4fff6d6d21ba38dc7f94c7b14340d789bf'
|
7
|
+
data.tar.gz: 758c49617295edeba4231a8b80cf64758739b14e722b23a3904695c29d0b4ff1e3a003d195e589c69a6b933019bf4f2a27ed847dc831ad4fcdf2bdf7ef65b7b8
|
data/README.md
CHANGED
@@ -1,8 +1,54 @@
|
|
1
|
-
# Active MCP
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
# Active MCP ๐
|
2
|
+
|
3
|
+
<div align="center">
|
4
|
+
|
5
|
+
[](https://badge.fury.io/rb/active_mcp)
|
6
|
+
[](LICENSE)
|
7
|
+
[](https://rubyonrails.org/)
|
8
|
+
|
9
|
+
A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) - connect your Rails apps to AI tools with minimal effort.
|
10
|
+
</div>
|
11
|
+
|
12
|
+
## ๐ Table of Contents
|
13
|
+
|
14
|
+
- [Active MCP ๐](#active-mcp-)
|
15
|
+
- [๐ Table of Contents](#-table-of-contents)
|
16
|
+
- [โจ Features](#-features)
|
17
|
+
- [๐ฆ Installation](#-installation)
|
18
|
+
- [๐ Setup](#-setup)
|
19
|
+
- [Using the Install Generator (Recommended)](#using-the-install-generator-recommended)
|
20
|
+
- [Manual Setup](#manual-setup)
|
21
|
+
- [๐ MCP Connection Methods](#-mcp-connection-methods)
|
22
|
+
- [1. Direct HTTP Connection](#1-direct-http-connection)
|
23
|
+
- [2. Standalone MCP Server](#2-standalone-mcp-server)
|
24
|
+
- [๐ Rails Generators](#-rails-generators)
|
25
|
+
- [Install Generator](#install-generator)
|
26
|
+
- [Tool Generator](#tool-generator)
|
27
|
+
- [๐งฐ Creating MCP Tools](#-creating-mcp-tools)
|
28
|
+
- [๐ Input Schema](#-input-schema)
|
29
|
+
- [๐ Authorization \& Authentication](#-authorization--authentication)
|
30
|
+
- [Authorization for Tools](#authorization-for-tools)
|
31
|
+
- [Authentication Options](#authentication-options)
|
32
|
+
- [1. Server Configuration](#1-server-configuration)
|
33
|
+
- [2. Token Verification in Tools](#2-token-verification-in-tools)
|
34
|
+
- [โ๏ธ Advanced Configuration](#๏ธ-advanced-configuration)
|
35
|
+
- [Custom Controller](#custom-controller)
|
36
|
+
- [๐ก Best Practices](#-best-practices)
|
37
|
+
- [1. Create Specific Tool Classes](#1-create-specific-tool-classes)
|
38
|
+
- [2. Validate and Sanitize Inputs](#2-validate-and-sanitize-inputs)
|
39
|
+
- [3. Return Structured Responses](#3-return-structured-responses)
|
40
|
+
- [๐งช Development](#-development)
|
41
|
+
- [๐ฅ Contributing](#-contributing)
|
42
|
+
- [๐ License](#-license)
|
43
|
+
|
44
|
+
## โจ Features
|
45
|
+
|
46
|
+
- **Simple Integration**: Easily expose Rails functionality as MCP tools
|
47
|
+
- **Powerful Generators**: Quickly scaffold MCP tools with Rails generators
|
48
|
+
- **Authentication Support**: Built-in authentication and authorization capabilities
|
49
|
+
- **Flexible Configuration**: Multiple deployment and connection options
|
50
|
+
|
51
|
+
## ๐ฆ Installation
|
6
52
|
|
7
53
|
Add this line to your application's Gemfile:
|
8
54
|
|
@@ -22,7 +68,7 @@ Or install it yourself as:
|
|
22
68
|
$ gem install active_mcp
|
23
69
|
```
|
24
70
|
|
25
|
-
## Setup
|
71
|
+
## ๐ Setup
|
26
72
|
|
27
73
|
### Using the Install Generator (Recommended)
|
28
74
|
|
@@ -36,6 +82,8 @@ This generator will:
|
|
36
82
|
|
37
83
|
1. Create a configuration initializer at `config/initializers/active_mcp.rb`
|
38
84
|
2. Mount the ActiveMcp engine in your routes
|
85
|
+
3. Create an MCP server script at `script/mcp_server.rb`
|
86
|
+
4. Show instructions for next steps
|
39
87
|
|
40
88
|
After running the generator, follow the displayed instructions to create and configure your MCP tools.
|
41
89
|
|
@@ -57,50 +105,58 @@ end
|
|
57
105
|
|
58
106
|
```ruby
|
59
107
|
class CreateNoteTool < ActiveMcp::Tool
|
60
|
-
description "Create Note
|
108
|
+
description "Create Note"
|
61
109
|
|
62
|
-
argument :title, :string
|
63
|
-
argument :content, :string
|
110
|
+
argument :title, :string, required: true
|
111
|
+
argument :content, :string, required: true
|
64
112
|
|
65
113
|
def call(title:, content:)
|
66
|
-
Note.create(title
|
114
|
+
note = Note.create(title: title, content: content)
|
67
115
|
|
68
|
-
"Created
|
116
|
+
"Created note with ID: #{note.id}"
|
69
117
|
end
|
70
118
|
end
|
71
119
|
```
|
72
120
|
|
73
|
-
|
121
|
+
## ๐ MCP Connection Methods
|
122
|
+
|
123
|
+
Active MCP supports two connection methods:
|
74
124
|
|
75
|
-
|
125
|
+
### 1. Direct HTTP Connection
|
126
|
+
|
127
|
+
Set your MCP client to connect directly to your Rails application:
|
128
|
+
|
129
|
+
```
|
130
|
+
https://your-app.example.com/mcp
|
131
|
+
```
|
76
132
|
|
77
|
-
|
133
|
+
### 2. Standalone MCP Server
|
78
134
|
|
79
|
-
Start
|
135
|
+
Start a dedicated MCP server that communicates with your Rails app:
|
80
136
|
|
81
137
|
```ruby
|
82
|
-
#
|
138
|
+
# script/mcp_server.rb
|
83
139
|
server = ActiveMcp::Server.new(
|
84
|
-
name: "
|
140
|
+
name: "My App MCP Server",
|
85
141
|
uri: 'https://your-app.example.com/mcp'
|
86
142
|
)
|
87
143
|
server.start
|
88
144
|
```
|
89
145
|
|
90
|
-
|
146
|
+
Then configure your MCP client:
|
91
147
|
|
92
148
|
```json
|
93
149
|
{
|
94
150
|
"mcpServers": {
|
95
|
-
"
|
151
|
+
"my-rails-app": {
|
96
152
|
"command": "/path/to/ruby",
|
97
|
-
"args": ["/path/to/
|
153
|
+
"args": ["/path/to/script/mcp_server.rb"]
|
98
154
|
}
|
99
155
|
}
|
100
156
|
}
|
101
157
|
```
|
102
158
|
|
103
|
-
## Rails Generators
|
159
|
+
## ๐ Rails Generators
|
104
160
|
|
105
161
|
Active MCP provides generators to help you quickly set up and extend your MCP integration:
|
106
162
|
|
@@ -112,39 +168,46 @@ Initialize Active MCP in your Rails application:
|
|
112
168
|
$ rails generate active_mcp:install
|
113
169
|
```
|
114
170
|
|
115
|
-
This sets up all necessary configuration files and mounts the MCP engine in your routes.
|
116
|
-
|
117
171
|
### Tool Generator
|
118
172
|
|
119
173
|
Create new MCP tools quickly:
|
120
174
|
|
121
175
|
```bash
|
122
|
-
# Generate a new MCP tool
|
123
176
|
$ rails generate active_mcp:tool search_users
|
124
177
|
```
|
125
178
|
|
126
|
-
This creates a new tool file at `app/tools/search_users_tool.rb` with
|
179
|
+
This creates a new tool file at `app/tools/search_users_tool.rb` with ready-to-customize starter code.
|
180
|
+
|
181
|
+
## ๐งฐ Creating MCP Tools
|
182
|
+
|
183
|
+
MCP tools are Ruby classes that inherit from `ActiveMcp::Tool` and define an interface for AI to interact with your application:
|
127
184
|
|
128
185
|
```ruby
|
129
186
|
class SearchUsersTool < ActiveMcp::Tool
|
130
|
-
description 'Search users'
|
187
|
+
description 'Search users by criteria'
|
131
188
|
|
132
|
-
argument :
|
133
|
-
argument :
|
134
|
-
|
189
|
+
argument :email, :string, required: false, description: 'Email to search for'
|
190
|
+
argument :name, :string, required: false, description: 'Name to search for'
|
191
|
+
argument :limit, :integer, required: false, description: 'Maximum number of records to return'
|
135
192
|
|
136
|
-
def call(
|
137
|
-
|
193
|
+
def call(email: nil, name: nil, limit: 10)
|
194
|
+
criteria = {}
|
195
|
+
criteria[:email] = email if email.present?
|
196
|
+
criteria[:name] = name if name.present?
|
138
197
|
|
139
|
-
|
140
|
-
|
198
|
+
users = User.where(criteria).limit(limit)
|
199
|
+
|
200
|
+
{
|
201
|
+
type: "text",
|
202
|
+
content: users.to_json(only: [:id, :name, :email, :created_at])
|
203
|
+
}
|
141
204
|
end
|
142
205
|
end
|
143
206
|
```
|
144
207
|
|
145
|
-
|
208
|
+
## ๐ Input Schema
|
146
209
|
|
147
|
-
|
210
|
+
Define arguments for your tools using the `argument` method:
|
148
211
|
|
149
212
|
```ruby
|
150
213
|
argument :name, :string, required: true, description: 'User name'
|
@@ -153,45 +216,35 @@ argument :addresses, :array, required: false, description: 'User addresses'
|
|
153
216
|
argument :preferences, :object, required: false, description: 'User preferences'
|
154
217
|
```
|
155
218
|
|
156
|
-
Supported types
|
157
|
-
|
158
|
-
- `:string`
|
159
|
-
- `:integer`
|
160
|
-
- `:number` (float/decimal)
|
161
|
-
- `:boolean`
|
162
|
-
- `:array`
|
163
|
-
- `:object` (hash/dictionary)
|
164
|
-
- `:null`
|
165
|
-
|
166
|
-
## Using with MCP Clients
|
219
|
+
Supported types:
|
167
220
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
221
|
+
| Type | Description |
|
222
|
+
| --- | --- |
|
223
|
+
| `:string` | Text values |
|
224
|
+
| `:integer` | Whole numbers |
|
225
|
+
| `:number` | Decimal numbers (float/decimal) |
|
226
|
+
| `:boolean` | True/false values |
|
227
|
+
| `:array` | Lists of values |
|
228
|
+
| `:object` | Hash/dictionary structures |
|
229
|
+
| `:null` | Null values |
|
173
230
|
|
174
|
-
|
175
|
-
|
176
|
-
## Authorization & Authentication
|
177
|
-
|
178
|
-
ActiveMcp supports both authentication (verifying who a user is) and authorization (controlling what resources they can access).
|
231
|
+
## ๐ Authorization & Authentication
|
179
232
|
|
180
233
|
### Authorization for Tools
|
181
234
|
|
182
|
-
|
235
|
+
Control access to tools by overriding the `visible?` class method:
|
183
236
|
|
184
237
|
```ruby
|
185
238
|
class AdminOnlyTool < ActiveMcp::Tool
|
186
|
-
description "
|
239
|
+
description "Admin-only tool"
|
187
240
|
|
188
|
-
argument :command, :string, required: true, description: "Admin command
|
241
|
+
argument :command, :string, required: true, description: "Admin command"
|
189
242
|
|
190
|
-
#
|
243
|
+
# Only allow admins to access this tool
|
191
244
|
def self.visible?(auth_info)
|
192
245
|
return false unless auth_info
|
193
246
|
return false unless auth_info[:type] == :bearer
|
194
|
-
|
247
|
+
|
195
248
|
# Check if the token belongs to an admin
|
196
249
|
auth_info[:token] == "admin-token" || User.find_by_token(auth_info[:token])&.admin?
|
197
250
|
end
|
@@ -202,125 +255,56 @@ class AdminOnlyTool < ActiveMcp::Tool
|
|
202
255
|
end
|
203
256
|
```
|
204
257
|
|
205
|
-
|
206
|
-
|
207
|
-
1. Only tools that return `true` from their `authorized?` method will be included in the tools list
|
208
|
-
2. Users can only call tools that they're authorized to use
|
209
|
-
3. Unauthorized access attempts will return a 403 Forbidden response
|
210
|
-
|
211
|
-
This makes it easy to create role-based access control for your MCP tools.
|
258
|
+
### Authentication Options
|
212
259
|
|
213
|
-
|
214
|
-
|
215
|
-
ActiveMcp supports receiving authentication credentials from MCP clients and forwarding them to your Rails application. There are two ways to handle authentication:
|
216
|
-
|
217
|
-
### 1. Using Server Configuration
|
218
|
-
|
219
|
-
When creating your MCP server, you can pass authentication options that will be included in every request:
|
260
|
+
#### 1. Server Configuration
|
220
261
|
|
221
262
|
```ruby
|
222
263
|
server = ActiveMcp::Server.new(
|
223
|
-
name: "
|
264
|
+
name: "My Secure MCP Server",
|
224
265
|
uri: 'http://localhost:3000/mcp',
|
225
266
|
auth: {
|
226
|
-
type: :bearer,
|
227
|
-
token: ENV[
|
267
|
+
type: :bearer,
|
268
|
+
token: ENV['MCP_AUTH_TOKEN']
|
228
269
|
}
|
229
270
|
)
|
230
271
|
server.start
|
231
272
|
```
|
232
273
|
|
233
|
-
|
234
|
-
|
235
|
-
For more advanced authentication, create a custom controller that handles the authentication flow:
|
274
|
+
#### 2. Token Verification in Tools
|
236
275
|
|
237
276
|
```ruby
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
def authenticate
|
244
|
-
# Extract auth from MCP request
|
245
|
-
auth_header = request.headers['Authorization']
|
246
|
-
|
247
|
-
if auth_header.present?
|
248
|
-
# Process the auth header (Bearer token, etc.)
|
249
|
-
token = auth_header.split(' ').last
|
250
|
-
|
251
|
-
# Validate the token against your auth system
|
252
|
-
user = User.find_by_token(token)
|
253
|
-
|
254
|
-
unless user
|
255
|
-
render_error(-32600, "Authentication failed")
|
256
|
-
return false
|
257
|
-
end
|
258
|
-
|
259
|
-
# Set current user for tool access
|
260
|
-
Current.user = user
|
261
|
-
else
|
262
|
-
render_error(-32600, "Authentication required")
|
263
|
-
return false
|
264
|
-
end
|
277
|
+
def call(resource_id:, auth_info: nil, **args)
|
278
|
+
# Check if authentication is provided
|
279
|
+
unless auth_info.present?
|
280
|
+
raise "Authentication required"
|
265
281
|
end
|
266
|
-
end
|
267
|
-
```
|
268
|
-
|
269
|
-
### 3. Using Auth in Tools
|
270
|
-
|
271
|
-
Authentication information is automatically passed to your tools through the `auth_info` parameter:
|
272
|
-
|
273
|
-
```ruby
|
274
|
-
class SecuredDataTool < ActiveMcp::Tool
|
275
|
-
description 'Access secured data'
|
276
|
-
|
277
|
-
argument :resource_id, :string, required: true, description: 'ID of the resource to access'
|
278
|
-
|
279
|
-
def call(resource_id:, auth_info: nil, **args)
|
280
|
-
# Check if auth info exists
|
281
|
-
unless auth_info.present?
|
282
|
-
raise "Authentication required to access this resource"
|
283
|
-
end
|
284
|
-
|
285
|
-
# Extract token from auth info
|
286
|
-
token = auth_info[:token]
|
287
|
-
|
288
|
-
# Validate token and get user
|
289
|
-
user = User.authenticate_with_token(token)
|
290
|
-
|
291
|
-
unless user
|
292
|
-
raise "Invalid authentication token"
|
293
|
-
end
|
294
|
-
|
295
|
-
# Check if user has access to the resource
|
296
|
-
resource = Resource.find(resource_id)
|
297
|
-
|
298
|
-
if resource.user_id != user.id
|
299
|
-
raise "Access denied to this resource"
|
300
|
-
end
|
301
282
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
283
|
+
# Verify the token
|
284
|
+
user = User.authenticate_with_token(auth_info[:token])
|
285
|
+
|
286
|
+
unless user
|
287
|
+
raise "Invalid authentication token"
|
307
288
|
end
|
289
|
+
|
290
|
+
# Proceed with authenticated operation
|
291
|
+
# ...
|
308
292
|
end
|
309
293
|
```
|
310
294
|
|
311
|
-
## Advanced Configuration
|
295
|
+
## โ๏ธ Advanced Configuration
|
312
296
|
|
313
297
|
### Custom Controller
|
314
298
|
|
315
|
-
|
299
|
+
Create a custom controller for advanced needs:
|
316
300
|
|
317
301
|
```ruby
|
318
|
-
class
|
319
|
-
#
|
302
|
+
class CustomMcpController < ActiveMcp::BaseController
|
303
|
+
# Custom MCP handling logic
|
320
304
|
end
|
321
305
|
```
|
322
306
|
|
323
|
-
|
307
|
+
Update routes:
|
324
308
|
|
325
309
|
```ruby
|
326
310
|
Rails.application.routes.draw do
|
@@ -328,72 +312,69 @@ Rails.application.routes.draw do
|
|
328
312
|
end
|
329
313
|
```
|
330
314
|
|
331
|
-
## Best Practices
|
315
|
+
## ๐ก Best Practices
|
332
316
|
|
333
|
-
### Create
|
317
|
+
### 1. Create Specific Tool Classes
|
334
318
|
|
335
|
-
|
336
|
-
|
337
|
-
1. Increases security by avoiding dynamic class loading
|
338
|
-
2. Makes your tools more explicit and easier to understand
|
339
|
-
3. Provides better validation and error handling specific to each model
|
340
|
-
|
341
|
-
For example, instead of creating a generic search tool, create specific search tools for each model:
|
319
|
+
Create dedicated tool classes for each model or operation instead of generic tools:
|
342
320
|
|
343
321
|
```ruby
|
344
|
-
#
|
322
|
+
# โ
GOOD: Specific tool for a single purpose
|
345
323
|
class SearchUsersTool < ActiveMcp::Tool
|
346
|
-
|
324
|
+
# ...specific implementation
|
325
|
+
end
|
347
326
|
|
348
|
-
|
349
|
-
|
350
|
-
|
327
|
+
# โ BAD: Generic tool that dynamically loads models
|
328
|
+
class GenericSearchTool < ActiveMcp::Tool
|
329
|
+
# Avoid this pattern - security and maintainability issues
|
330
|
+
end
|
331
|
+
```
|
351
332
|
|
352
|
-
|
353
|
-
criteria = {}
|
354
|
-
criteria[:email] = email if email.present?
|
355
|
-
criteria[:name] = name if name.present?
|
333
|
+
### 2. Validate and Sanitize Inputs
|
356
334
|
|
357
|
-
|
335
|
+
Always validate and sanitize inputs in your tool implementations:
|
358
336
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
337
|
+
```ruby
|
338
|
+
def call(user_id:, **args)
|
339
|
+
# Validate input
|
340
|
+
unless user_id.is_a?(Integer) || user_id.to_s.match?(/^\d+$/)
|
341
|
+
return { error: "Invalid user ID format" }
|
363
342
|
end
|
343
|
+
|
344
|
+
# Proceed with validated data
|
345
|
+
user = User.find_by(id: user_id)
|
346
|
+
# ...
|
364
347
|
end
|
348
|
+
```
|
365
349
|
|
366
|
-
|
367
|
-
class SearchPostsTool < ActiveMcp::Tool
|
368
|
-
description 'Search posts by criteria'
|
369
|
-
|
370
|
-
argument :title, :string, required: false, description: 'Title to search for'
|
371
|
-
argument :author_id, :integer, required: false, description: 'Author ID to filter by'
|
372
|
-
argument :limit, :integer, required: false, description: 'Maximum number of records to return'
|
373
|
-
|
374
|
-
def call(title: nil, author_id: nil, limit: 10)
|
375
|
-
criteria = {}
|
376
|
-
criteria[:title] = title if title.present?
|
377
|
-
criteria[:author_id] = author_id if author_id.present?
|
350
|
+
### 3. Return Structured Responses
|
378
351
|
|
379
|
-
|
352
|
+
Return structured responses that are easy for AI to parse:
|
380
353
|
|
381
|
-
|
382
|
-
|
383
|
-
|
354
|
+
```ruby
|
355
|
+
def call(query:, **args)
|
356
|
+
results = User.search(query)
|
357
|
+
|
358
|
+
{
|
359
|
+
type: "text",
|
360
|
+
content: results.to_json(only: [:id, :name, :email]),
|
361
|
+
metadata: {
|
362
|
+
count: results.size,
|
363
|
+
query: query
|
384
364
|
}
|
385
|
-
|
365
|
+
}
|
386
366
|
end
|
387
367
|
```
|
388
368
|
|
389
|
-
## Development
|
369
|
+
## ๐งช Development
|
390
370
|
|
391
371
|
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
392
372
|
|
393
|
-
## Contributing
|
373
|
+
## ๐ฅ Contributing
|
394
374
|
|
395
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
375
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/moekiorg/active_mcp. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/moekiorg/active_mcp/blob/main/CODE_OF_CONDUCT.md).
|
396
376
|
|
397
|
-
## License
|
377
|
+
## ๐ License
|
398
378
|
|
399
379
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
380
|
+
|
@@ -2,72 +2,6 @@
|
|
2
2
|
|
3
3
|
module ActiveMcp
|
4
4
|
class BaseController < ActionController::Base
|
5
|
-
|
6
|
-
skip_before_action :verify_authenticity_token
|
7
|
-
before_action :authenticate, only: [:index]
|
8
|
-
|
9
|
-
def index
|
10
|
-
if params[:jsonrpc]
|
11
|
-
process_request_from_mcp_client
|
12
|
-
else
|
13
|
-
process_request_from_mcp_server
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def process_request_from_mcp_server
|
20
|
-
case params[:method]
|
21
|
-
when Method::TOOLS_LIST
|
22
|
-
result = Response::ToolsList::Json.call(
|
23
|
-
tools: Response::Tools.to_hash(auth_info: @auth_info)
|
24
|
-
)
|
25
|
-
when Method::TOOLS_CALL
|
26
|
-
result = Response::ToolsCall::Json.call(params:, auth_info: @auth_info)
|
27
|
-
else
|
28
|
-
result = Response::NoMethod.call
|
29
|
-
end
|
30
|
-
|
31
|
-
render json: result, status: 200
|
32
|
-
end
|
33
|
-
|
34
|
-
def process_request_from_mcp_client
|
35
|
-
case params[:method]
|
36
|
-
when Method::INITIALIZE
|
37
|
-
result = Response::Initialize.call(id: params[:id])
|
38
|
-
when Method::INITIALIZED
|
39
|
-
result = Response::Initialized.call
|
40
|
-
when Method::CANCELLED
|
41
|
-
result = Response::Cancelled.call
|
42
|
-
when Method::TOOLS_LIST
|
43
|
-
result = Response::ToolsList::Jsonrpc.call(
|
44
|
-
id: params[:id],
|
45
|
-
tools: Response::Tools.to_hash(auth_info: @auth_info)
|
46
|
-
)
|
47
|
-
when Method::TOOLS_CALL
|
48
|
-
result = Response::ToolsCall::Jsonrpc.call(id: params[:id], params:, auth_info: @auth_info)
|
49
|
-
else
|
50
|
-
result = Response::NoMethod.call
|
51
|
-
end
|
52
|
-
|
53
|
-
render json: result, status: 200
|
54
|
-
end
|
55
|
-
|
56
|
-
def authenticate
|
57
|
-
auth_header = request.headers["Authorization"]
|
58
|
-
if auth_header.present?
|
59
|
-
@auth_info = {
|
60
|
-
header: auth_header,
|
61
|
-
type: if auth_header.start_with?("Bearer ")
|
62
|
-
:bearer
|
63
|
-
elsif auth_header.start_with?("Basic ")
|
64
|
-
:basic
|
65
|
-
else
|
66
|
-
:unknown
|
67
|
-
end,
|
68
|
-
token: auth_header.split(" ").last
|
69
|
-
}
|
70
|
-
end
|
71
|
-
end
|
5
|
+
include RequestHandler
|
72
6
|
end
|
73
7
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMcp
|
4
|
+
module RequestHandler
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
protect_from_forgery with: :null_session
|
9
|
+
skip_before_action :verify_authenticity_token
|
10
|
+
before_action :authenticate, only: [:index]
|
11
|
+
end
|
12
|
+
|
13
|
+
def index
|
14
|
+
if json_rpc_request?
|
15
|
+
handle_mcp_client_request
|
16
|
+
else
|
17
|
+
handle_mcp_server_request
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def json_rpc_request?
|
24
|
+
params[:jsonrpc].present?
|
25
|
+
end
|
26
|
+
|
27
|
+
def handle_mcp_client_request
|
28
|
+
@id = params[:id]
|
29
|
+
@auth_info = auth_info
|
30
|
+
|
31
|
+
case params[:method]
|
32
|
+
when Method::INITIALIZE
|
33
|
+
render 'active_mcp/initialize', formats: :json
|
34
|
+
when Method::INITIALIZED
|
35
|
+
render 'active_mcp/initialized', formats: :json
|
36
|
+
when Method::CANCELLED
|
37
|
+
render 'active_mcp/cancelled', formats: :json
|
38
|
+
when Method::TOOLS_LIST
|
39
|
+
@tools = ActiveMcp::Tool.authorized_tools(auth_info)
|
40
|
+
@format = :jsonrpc
|
41
|
+
render 'active_mcp/tools_list', formats: :json
|
42
|
+
when Method::TOOLS_CALL
|
43
|
+
@tool_result = ActiveMcp::ToolExecutor.execute(params: params, auth_info: auth_info)
|
44
|
+
@format = :jsonrpc
|
45
|
+
render 'active_mcp/tools_call', formats: :json
|
46
|
+
else
|
47
|
+
@format = :jsonrpc
|
48
|
+
render 'active_mcp/no_method', formats: :json
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_mcp_server_request
|
53
|
+
@auth_info = auth_info
|
54
|
+
|
55
|
+
case params[:method]
|
56
|
+
when Method::TOOLS_LIST
|
57
|
+
@tools = ActiveMcp::Tool.authorized_tools(auth_info)
|
58
|
+
@format = :json
|
59
|
+
render 'active_mcp/tools_list', formats: :json
|
60
|
+
when Method::TOOLS_CALL
|
61
|
+
@tool_result = ActiveMcp::ToolExecutor.execute(params: params, auth_info: auth_info)
|
62
|
+
@format = :json
|
63
|
+
render 'active_mcp/tools_call', formats: :json
|
64
|
+
else
|
65
|
+
@format = :json
|
66
|
+
render 'active_mcp/no_method', formats: :json
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def authenticate
|
71
|
+
auth_header = request.headers["Authorization"]
|
72
|
+
if auth_header.present?
|
73
|
+
@auth_info = {
|
74
|
+
header: auth_header,
|
75
|
+
type: if auth_header.start_with?("Bearer ")
|
76
|
+
:bearer
|
77
|
+
elsif auth_header.start_with?("Basic ")
|
78
|
+
:basic
|
79
|
+
else
|
80
|
+
:unknown
|
81
|
+
end,
|
82
|
+
token: auth_header.split(" ").last
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def auth_info
|
88
|
+
@auth_info
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -1,8 +1,14 @@
|
|
1
1
|
module ActiveMcp
|
2
|
-
|
3
|
-
def self.
|
4
|
-
|
5
|
-
|
2
|
+
class ToolExecutor
|
3
|
+
def self.execute(params:, auth_info:)
|
4
|
+
if params[:jsonrpc].present?
|
5
|
+
tool_name = params[:params][:name]
|
6
|
+
tool_params = params[:params][:arguments]
|
7
|
+
else
|
8
|
+
tool_name = params[:name]
|
9
|
+
tool_params = params[:arguments]
|
10
|
+
end
|
11
|
+
|
6
12
|
unless tool_name
|
7
13
|
return {
|
8
14
|
isError: true,
|
@@ -18,7 +24,7 @@ module ActiveMcp
|
|
18
24
|
tool_class = Tool.registered_tools.find do |tc|
|
19
25
|
tc.tool_name == tool_name
|
20
26
|
end
|
21
|
-
|
27
|
+
|
22
28
|
unless tool_class
|
23
29
|
return {
|
24
30
|
isError: true,
|
@@ -30,7 +36,7 @@ module ActiveMcp
|
|
30
36
|
]
|
31
37
|
}
|
32
38
|
end
|
33
|
-
|
39
|
+
|
34
40
|
unless tool_class.visible?(auth_info)
|
35
41
|
return {
|
36
42
|
isError: true,
|
@@ -43,12 +49,16 @@ module ActiveMcp
|
|
43
49
|
}
|
44
50
|
end
|
45
51
|
|
46
|
-
|
47
|
-
|
48
|
-
value
|
49
|
-
|
50
|
-
|
52
|
+
if tool_params
|
53
|
+
arguments = tool_params.permit!.to_hash.symbolize_keys.transform_values do |value|
|
54
|
+
if !value.is_a?(String)
|
55
|
+
value
|
56
|
+
else
|
57
|
+
value.match(/^\d+$/) ? value.to_i : value
|
58
|
+
end
|
51
59
|
end
|
60
|
+
else
|
61
|
+
arguments = {}
|
52
62
|
end
|
53
63
|
|
54
64
|
tool = tool_class.new
|
@@ -65,10 +75,11 @@ module ActiveMcp
|
|
65
75
|
]
|
66
76
|
}
|
67
77
|
end
|
68
|
-
|
78
|
+
|
79
|
+
# Execute the tool
|
69
80
|
begin
|
70
81
|
arguments[:auth_info] = auth_info if auth_info.present?
|
71
|
-
|
82
|
+
|
72
83
|
return {
|
73
84
|
content: [
|
74
85
|
{
|
@@ -89,7 +100,7 @@ module ActiveMcp
|
|
89
100
|
}
|
90
101
|
end
|
91
102
|
end
|
92
|
-
|
103
|
+
|
93
104
|
def self.formatted(object)
|
94
105
|
case object
|
95
106
|
when String
|
@@ -0,0 +1,21 @@
|
|
1
|
+
json.jsonrpc ActiveMcp::JSON_RPC_VERSION
|
2
|
+
json.id @id
|
3
|
+
json.result do
|
4
|
+
json.protocolVersion ActiveMcp::PROTOCOL_VERSION
|
5
|
+
json.capabilities do
|
6
|
+
json.logging Hash.new
|
7
|
+
json.capabilities do
|
8
|
+
json.resources do
|
9
|
+
json.subscribe false
|
10
|
+
json.listChanged false
|
11
|
+
end
|
12
|
+
json.tools do
|
13
|
+
json.listChanged false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
json.serverInfo do
|
18
|
+
json.name ActiveMcp.config.respond_to?(:server_name) ? ActiveMcp.config.server_name : "Active MCP Server"
|
19
|
+
json.version ActiveMcp.config.respond_to?(:server_version) ? ActiveMcp.config.server_version : ActiveMcp::VERSION
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
json.jsonrpc ActiveMcp::JSON_RPC_VERSION if @format == :jsonrpc
|
2
|
+
json.id @id if @format == :jsonrpc && @id.present?
|
3
|
+
|
4
|
+
if @format == :jsonrpc
|
5
|
+
json.result @tool_result
|
6
|
+
else
|
7
|
+
json.isError @tool_result[:isError] if @tool_result[:isError]
|
8
|
+
json.content @tool_result[:content]
|
9
|
+
end
|
data/lib/active_mcp/tool.rb
CHANGED
@@ -40,7 +40,23 @@ module ActiveMcp
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def visible?(auth_info)
|
43
|
-
|
43
|
+
if respond_to?(:authorized?)
|
44
|
+
authorized?(auth_info)
|
45
|
+
else
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def authorized_tools(auth_info = nil)
|
51
|
+
registered_tools.select do |tool_class|
|
52
|
+
tool_class.visible?(auth_info)
|
53
|
+
end.map do |tool_class|
|
54
|
+
{
|
55
|
+
name: tool_class.tool_name,
|
56
|
+
description: tool_class.desc,
|
57
|
+
inputSchema: tool_class.schema
|
58
|
+
}
|
59
|
+
end
|
44
60
|
end
|
45
61
|
end
|
46
62
|
|
data/lib/active_mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_mcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Moeki Kawakami
|
@@ -43,6 +43,20 @@ dependencies:
|
|
43
43
|
- - ">="
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: jbuilder
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '2.7'
|
53
|
+
type: :runtime
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '2.7'
|
46
60
|
description: A Rails engine that provides MCP capabilities to your Rails application
|
47
61
|
email:
|
48
62
|
- hi@moeki.org
|
@@ -54,16 +68,14 @@ files:
|
|
54
68
|
- README.md
|
55
69
|
- Rakefile
|
56
70
|
- app/controllers/active_mcp/base_controller.rb
|
57
|
-
- app/
|
58
|
-
- app/models/active_mcp/response/initialize.rb
|
59
|
-
- app/models/active_mcp/response/initialized.rb
|
60
|
-
- app/models/active_mcp/response/no_method.rb
|
61
|
-
- app/models/active_mcp/response/tools.rb
|
62
|
-
- app/models/active_mcp/response/tools_call/json.rb
|
63
|
-
- app/models/active_mcp/response/tools_call/jsonrpc.rb
|
64
|
-
- app/models/active_mcp/response/tools_list/json.rb
|
65
|
-
- app/models/active_mcp/response/tools_list/jsonrpc.rb
|
71
|
+
- app/controllers/concerns/active_mcp/request_handler.rb
|
66
72
|
- app/models/active_mcp/tool_executor.rb
|
73
|
+
- app/views/active_mcp/cancelled.json.jbuilder
|
74
|
+
- app/views/active_mcp/initialize.json.jbuilder
|
75
|
+
- app/views/active_mcp/initialized.json.jbuilder
|
76
|
+
- app/views/active_mcp/no_method.json.jbuilder
|
77
|
+
- app/views/active_mcp/tools_call.json.jbuilder
|
78
|
+
- app/views/active_mcp/tools_list.json.jbuilder
|
67
79
|
- config/routes.rb
|
68
80
|
- lib/active_mcp.rb
|
69
81
|
- lib/active_mcp/configuration.rb
|
@@ -1,31 +0,0 @@
|
|
1
|
-
module ActiveMcp
|
2
|
-
module Response
|
3
|
-
class Initialize
|
4
|
-
def self.call(id:)
|
5
|
-
{
|
6
|
-
jsonrpc: JSON_RPC_VERSION,
|
7
|
-
id:,
|
8
|
-
result: {
|
9
|
-
protocolVersion: PROTOCOL_VERSION,
|
10
|
-
capabilities: {
|
11
|
-
logging: {},
|
12
|
-
capabilities: {
|
13
|
-
resources: {
|
14
|
-
subscribe: false,
|
15
|
-
listChanged: false
|
16
|
-
},
|
17
|
-
tools: {
|
18
|
-
listChanged: false
|
19
|
-
}
|
20
|
-
},
|
21
|
-
},
|
22
|
-
serverInfo: {
|
23
|
-
name: ActiveMcp.config.server_name,
|
24
|
-
version: ActiveMcp.config.server_version
|
25
|
-
}
|
26
|
-
}
|
27
|
-
}
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module ActiveMcp
|
2
|
-
module Response
|
3
|
-
class Tools
|
4
|
-
def self.to_hash(auth_info:)
|
5
|
-
Tool.registered_tools.select do |tool_class|
|
6
|
-
tool_class.visible?(auth_info)
|
7
|
-
end.map do |tool_class|
|
8
|
-
{
|
9
|
-
name: tool_class.tool_name,
|
10
|
-
description: tool_class.desc,
|
11
|
-
inputSchema: tool_class.schema
|
12
|
-
}
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
module ActiveMcp
|
2
|
-
module Response
|
3
|
-
module ToolsCall
|
4
|
-
class Jsonrpc
|
5
|
-
def self.call(id:, params:, auth_info:)
|
6
|
-
result = ActiveMcp::ToolExecutor.call(params: params[:params], auth_info:)
|
7
|
-
{
|
8
|
-
jsonrpc: JSON_RPC_VERSION,
|
9
|
-
id:,
|
10
|
-
result:
|
11
|
-
}
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|