active_mcp 0.6.0 → 0.8.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/README.md +130 -99
- data/app/controllers/active_mcp/base_controller.rb +27 -3
- data/app/controllers/concerns/active_mcp/authenticatable.rb +29 -0
- data/app/controllers/concerns/active_mcp/request_handlable.rb +22 -46
- data/app/controllers/concerns/active_mcp/resource_readable.rb +5 -13
- data/app/controllers/concerns/active_mcp/tool_executable.rb +6 -9
- data/app/views/active_mcp/completion_complete.json.jbuilder +16 -0
- data/app/views/active_mcp/resources_list.json.jbuilder +2 -2
- data/app/views/active_mcp/tools_list.json.jbuilder +14 -2
- data/lib/active_mcp/completion.rb +18 -0
- data/lib/active_mcp/controller/base.rb +7 -0
- data/lib/active_mcp/engine.rb +11 -14
- data/lib/active_mcp/resource/base.rb +29 -0
- data/lib/active_mcp/schema/base.rb +46 -0
- data/lib/active_mcp/server/method.rb +1 -0
- data/lib/active_mcp/server/protocol_handler.rb +17 -0
- data/lib/active_mcp/tool/base.rb +52 -0
- data/lib/active_mcp/version.rb +1 -1
- data/lib/active_mcp.rb +5 -1
- data/lib/generators/active_mcp/install/install_generator.rb +3 -7
- data/lib/generators/active_mcp/resource/resource_generator.rb +1 -1
- data/lib/generators/active_mcp/resource/templates/resource.rb.erb +4 -12
- data/lib/generators/active_mcp/tool/templates/tool.rb.erb +15 -9
- data/lib/generators/active_mcp/tool/tool_generator.rb +1 -1
- metadata +9 -3
- data/lib/active_mcp/tool.rb +0 -82
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 664064f2b6638585937706546194313e1b9beddcec7952bde94b34b39e39f851
|
4
|
+
data.tar.gz: d97d6e74d9a1235fa99f7362e0c38102069705b7c6ccb8f45d292ed6f45f08f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd746883f2345681dafaf3ba99d1925c11eb8bf0a2ee2dc25fd17d4f1c3906b371f961a64298edc998cc58b6c1424a55efdfbbd0195c5e6e7a21dd160376821d
|
7
|
+
data.tar.gz: dd3ccaa91bd1f807d6f2ea7fc9dc03dc363b30b4bcad3335448f34d978c70f98d63b0ac4641455e9600639d2b88c94ca6c47cd945c8a82edecc5577b4ecbe65c
|
data/README.md
CHANGED
@@ -17,8 +17,6 @@ A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelconte
|
|
17
17
|
- [✨ Features](#-features)
|
18
18
|
- [📦 Installation](#-installation)
|
19
19
|
- [🚀 Setup](#-setup)
|
20
|
-
- [Using the Install Generator (Recommended)](#using-the-install-generator-recommended)
|
21
|
-
- [Manual Setup](#manual-setup)
|
22
20
|
- [🔌 MCP Connection Methods](#-mcp-connection-methods)
|
23
21
|
- [1. Direct HTTP Connection](#1-direct-http-connection)
|
24
22
|
- [2. Standalone MCP Server](#2-standalone-mcp-server)
|
@@ -38,8 +36,6 @@ A Ruby on Rails engine for the [Model Context Protocol (MCP)](https://modelconte
|
|
38
36
|
- [Resource Types](#resource-types)
|
39
37
|
- [📦 MCP Resource Templates](#-mcp-resource-templates)
|
40
38
|
- [Creating Resource Templates](#creating-resource-templates)
|
41
|
-
- [⚙️ Advanced Configuration](#️-advanced-configuration)
|
42
|
-
- [Custom Controller](#custom-controller)
|
43
39
|
- [💡 Best Practices](#-best-practices)
|
44
40
|
- [1. Create Specific Tool Classes](#1-create-specific-tool-classes)
|
45
41
|
- [2. Validate and Sanitize Inputs](#2-validate-and-sanitize-inputs)
|
@@ -78,7 +74,7 @@ $ gem install active_mcp
|
|
78
74
|
|
79
75
|
## 🚀 Setup
|
80
76
|
|
81
|
-
|
77
|
+
1. Initialize
|
82
78
|
|
83
79
|
The easiest way to set up Active MCP in your Rails application is to use the install generator:
|
84
80
|
|
@@ -86,46 +82,61 @@ The easiest way to set up Active MCP in your Rails application is to use the ins
|
|
86
82
|
$ rails generate active_mcp:install
|
87
83
|
```
|
88
84
|
|
89
|
-
This generator will
|
85
|
+
This generator will create a configuration initializer at `config/initializers/active_mcp.rb`
|
90
86
|
|
91
|
-
|
92
|
-
2. Mount the ActiveMcp engine in your routes
|
93
|
-
3. Create an MCP server script at `script/mcp_server.rb`
|
94
|
-
4. Show instructions for next steps
|
87
|
+
2. Create a tool by inheriting from `ActiveMcp::Tool::Base`:
|
95
88
|
|
96
|
-
|
89
|
+
```bash
|
90
|
+
$ rails generate active_mcp:tool create_note
|
91
|
+
```
|
97
92
|
|
98
|
-
|
93
|
+
```ruby
|
94
|
+
class CreateNoteTool < ActiveMcp::Tool::Base
|
95
|
+
def name
|
96
|
+
"Create Note"
|
97
|
+
end
|
99
98
|
|
100
|
-
|
99
|
+
def description
|
100
|
+
"Create Note"
|
101
|
+
end
|
101
102
|
|
102
|
-
|
103
|
+
argument :title, :string, required: true
|
104
|
+
argument :content, :string, required: true
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
-
mount ActiveMcp::Engine, at: "/mcp"
|
106
|
+
def call(title:, content:, context:)
|
107
|
+
note = Note.create(title: title, content: content)
|
107
108
|
|
108
|
-
|
109
|
+
"Created note with ID: #{note.id}"
|
110
|
+
end
|
109
111
|
end
|
110
112
|
```
|
111
113
|
|
112
|
-
|
114
|
+
3. Create schema for your application:
|
113
115
|
|
114
116
|
```ruby
|
115
|
-
class
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
argument :content, :string, required: true
|
117
|
+
class MySchema < ActiveMcp::Schema::Base
|
118
|
+
tool CreateNoteTool.new
|
119
|
+
end
|
120
|
+
```
|
120
121
|
|
121
|
-
|
122
|
-
note = Note.create(title: title, content: content)
|
122
|
+
4. Create controller ans set up routing:
|
123
123
|
|
124
|
-
|
124
|
+
```ruby
|
125
|
+
class MyMcpController < ActiveMcp::Controller::Base
|
126
|
+
def schema
|
127
|
+
MySchema.new(context:)
|
125
128
|
end
|
126
129
|
end
|
127
130
|
```
|
128
131
|
|
132
|
+
```ruby
|
133
|
+
Rails.application.routes.draw do
|
134
|
+
post "/mcp", to: "my_mcp#index"
|
135
|
+
|
136
|
+
# Your other routes
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
129
140
|
## 🔌 MCP Connection Methods
|
130
141
|
|
131
142
|
Active MCP supports two connection methods:
|
@@ -184,7 +195,7 @@ Create new MCP tools quickly:
|
|
184
195
|
$ rails generate active_mcp:tool search_users
|
185
196
|
```
|
186
197
|
|
187
|
-
This creates a new tool file at `app/tools/search_users_tool.rb` with ready-to-customize starter code.
|
198
|
+
This creates a new tool file at `app/mcp/tools/search_users_tool.rb` with ready-to-customize starter code.
|
188
199
|
|
189
200
|
### Resource Generator
|
190
201
|
|
@@ -194,21 +205,27 @@ Generate new MCP resources to share data with AI:
|
|
194
205
|
$ rails generate active_mcp:resource profile_image
|
195
206
|
```
|
196
207
|
|
197
|
-
This creates a new resource file at `app/resources/profile_image_resource.rb` that you can customize to provide various types of content to AI assistants.
|
208
|
+
This creates a new resource file at `app/mcp/resources/profile_image_resource.rb` that you can customize to provide various types of content to AI assistants.
|
198
209
|
|
199
210
|
## 🧰 Creating MCP Tools
|
200
211
|
|
201
|
-
MCP tools are Ruby classes that inherit from `ActiveMcp::Tool` and define an interface for AI to interact with your application:
|
212
|
+
MCP tools are Ruby classes that inherit from `ActiveMcp::Tool::Base` and define an interface for AI to interact with your application:
|
202
213
|
|
203
214
|
```ruby
|
204
|
-
class SearchUsersTool < ActiveMcp::Tool
|
205
|
-
|
215
|
+
class SearchUsersTool < ActiveMcp::Tool::Base
|
216
|
+
def name
|
217
|
+
"Search Users"
|
218
|
+
end
|
219
|
+
|
220
|
+
def description
|
221
|
+
'Search users by criteria'
|
222
|
+
end
|
206
223
|
|
207
224
|
argument :email, :string, required: false, description: 'Email to search for'
|
208
225
|
argument :name, :string, required: false, description: 'Name to search for'
|
209
226
|
argument :limit, :integer, required: false, description: 'Maximum number of records to return'
|
210
227
|
|
211
|
-
def call(email: nil, name: nil, limit: 10)
|
228
|
+
def call(email: nil, name: nil, limit: 10, context: {})
|
212
229
|
criteria = {}
|
213
230
|
criteria[:email] = email if email.present?
|
214
231
|
criteria[:name] = name if name.present?
|
@@ -250,21 +267,27 @@ Supported types:
|
|
250
267
|
Control access to tools by overriding the `visible?` class method:
|
251
268
|
|
252
269
|
```ruby
|
253
|
-
class AdminOnlyTool < ActiveMcp::Tool
|
254
|
-
|
270
|
+
class AdminOnlyTool < ActiveMcp::Tool::Base
|
271
|
+
def name
|
272
|
+
"Admin-only tool"
|
273
|
+
end
|
274
|
+
|
275
|
+
def description
|
276
|
+
"Admin-only tool"
|
277
|
+
end
|
255
278
|
|
256
279
|
argument :command, :string, required: true, description: "Admin command"
|
257
280
|
|
258
281
|
# Only allow admins to access this tool
|
259
|
-
def
|
260
|
-
return false unless
|
261
|
-
return false unless auth_info[:type] == :bearer
|
282
|
+
def visible?(context:)
|
283
|
+
return false unless context
|
284
|
+
return false unless context[:auth_info][:type] == :bearer
|
262
285
|
|
263
286
|
# Check if the token belongs to an admin
|
264
|
-
|
287
|
+
context[:auth_info] == "admin-token" || User.find_by_token(context[:auth_info])&.admin?
|
265
288
|
end
|
266
289
|
|
267
|
-
def call(command:,
|
290
|
+
def call(command:, context: {})
|
268
291
|
# Tool implementation
|
269
292
|
end
|
270
293
|
end
|
@@ -289,14 +312,14 @@ server.start
|
|
289
312
|
#### 2. Token Verification in Tools
|
290
313
|
|
291
314
|
```ruby
|
292
|
-
def call(resource_id:,
|
315
|
+
def call(resource_id:, context: {})
|
293
316
|
# Check if authentication is provided
|
294
|
-
unless auth_info.present?
|
317
|
+
unless context[:auth_info].present?
|
295
318
|
raise "Authentication required"
|
296
319
|
end
|
297
320
|
|
298
321
|
# Verify the token
|
299
|
-
user = User.authenticate_with_token(auth_info[:token])
|
322
|
+
user = User.authenticate_with_token(context[:auth_info][:token])
|
300
323
|
|
301
324
|
unless user
|
302
325
|
raise "Invalid authentication token"
|
@@ -316,8 +339,8 @@ MCP Resources allow you to share data and files with AI assistants. Resources ha
|
|
316
339
|
Resources are Ruby classes `**Resource`:
|
317
340
|
|
318
341
|
```ruby
|
319
|
-
class UserResource
|
320
|
-
def initialize(id
|
342
|
+
class UserResource < ActiveMcp::Resource::Base
|
343
|
+
def initialize(id:)
|
321
344
|
@user = User.find(id)
|
322
345
|
@auth_info = auth_info
|
323
346
|
end
|
@@ -338,6 +361,10 @@ class UserResource
|
|
338
361
|
@user.profile
|
339
362
|
end
|
340
363
|
|
364
|
+
def visible?(context:)
|
365
|
+
# Your logic...
|
366
|
+
end
|
367
|
+
|
341
368
|
def text
|
342
369
|
# Return JSON data
|
343
370
|
{
|
@@ -351,13 +378,9 @@ end
|
|
351
378
|
```
|
352
379
|
|
353
380
|
```ruby
|
354
|
-
class
|
355
|
-
|
356
|
-
|
357
|
-
def resource_list
|
358
|
-
User.all.map do |user|
|
359
|
-
UserResource.new(id: user.id)
|
360
|
-
end
|
381
|
+
class MySchema < ActiveMcp::Schema::Base
|
382
|
+
User.all.each do |user|
|
383
|
+
resource UserResource.new(id: user.id)
|
361
384
|
end
|
362
385
|
end
|
363
386
|
```
|
@@ -378,19 +401,21 @@ end
|
|
378
401
|
2. **Binary Content** - Use the `blob` method to return binary files:
|
379
402
|
|
380
403
|
```ruby
|
381
|
-
class ImageResource
|
404
|
+
class ImageResource < ActiveMcp::Resource::Base
|
405
|
+
class << self
|
406
|
+
def mime_type
|
407
|
+
"image/png"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
382
411
|
def name
|
383
|
-
"
|
412
|
+
"Profile Image"
|
384
413
|
end
|
385
414
|
|
386
415
|
def uri
|
387
416
|
"data://localhost/image"
|
388
417
|
end
|
389
418
|
|
390
|
-
def mime_type
|
391
|
-
"image/png"
|
392
|
-
end
|
393
|
-
|
394
419
|
def description
|
395
420
|
"Profile image"
|
396
421
|
end
|
@@ -405,12 +430,12 @@ end
|
|
405
430
|
Resources can be protected using the same authorization mechanism as tools:
|
406
431
|
|
407
432
|
```ruby
|
408
|
-
def visible?
|
409
|
-
return false unless
|
410
|
-
return false unless auth_info[:type] == :bearer
|
433
|
+
def visible?(context: {})
|
434
|
+
return false unless context
|
435
|
+
return false unless context[:auth_info][:type] == :bearer
|
411
436
|
|
412
437
|
# Check if the token belongs to an admin
|
413
|
-
User.find_by_token(auth_info[:token])&.admin?
|
438
|
+
User.find_by_token(context[:auth_info][:token])&.admin?
|
414
439
|
end
|
415
440
|
```
|
416
441
|
|
@@ -423,54 +448,60 @@ MCP Resource Teamplates allow you to define template of resources.
|
|
423
448
|
Resources are Ruby classes `**ResourceTemplates`:
|
424
449
|
|
425
450
|
```ruby
|
426
|
-
class
|
427
|
-
|
428
|
-
|
429
|
-
|
451
|
+
class UserResource < ActiveMcp::Resource::Base
|
452
|
+
class << self
|
453
|
+
def name
|
454
|
+
"Users"
|
455
|
+
end
|
430
456
|
|
431
|
-
|
432
|
-
|
433
|
-
|
457
|
+
def uri_template
|
458
|
+
"data://localhost/users/{id}"
|
459
|
+
end
|
434
460
|
|
435
|
-
|
436
|
-
|
437
|
-
|
461
|
+
def mime_type
|
462
|
+
"application/json"
|
463
|
+
end
|
438
464
|
|
439
|
-
|
440
|
-
|
465
|
+
def description
|
466
|
+
"This is a test."
|
467
|
+
end
|
468
|
+
|
469
|
+
def visible?(context:)
|
470
|
+
# Your logic...
|
471
|
+
end
|
441
472
|
end
|
442
|
-
end
|
443
|
-
```
|
444
473
|
|
445
|
-
|
446
|
-
|
447
|
-
|
474
|
+
argument :id, ->(value) do
|
475
|
+
User.all.pluck(:id).filter { _1.match(value) }
|
476
|
+
end
|
448
477
|
|
449
|
-
def
|
450
|
-
|
451
|
-
UserResourceTemplate.new
|
452
|
-
]
|
478
|
+
def initialize(id:)
|
479
|
+
@user = User.find(id)
|
453
480
|
end
|
454
|
-
end
|
455
|
-
```
|
456
481
|
|
457
|
-
|
482
|
+
def name
|
483
|
+
@user.name
|
484
|
+
end
|
458
485
|
|
459
|
-
|
486
|
+
def description
|
487
|
+
@user.profile
|
488
|
+
end
|
460
489
|
|
461
|
-
|
490
|
+
def uri
|
491
|
+
"data://localhost/users/#{@user.name}"
|
492
|
+
end
|
462
493
|
|
463
|
-
|
464
|
-
|
465
|
-
|
494
|
+
def text
|
495
|
+
{ name: @user.name }
|
496
|
+
end
|
466
497
|
end
|
467
498
|
```
|
468
499
|
|
469
|
-
Update routes:
|
470
|
-
|
471
500
|
```ruby
|
472
|
-
|
473
|
-
|
501
|
+
class MySchema < ActiveMcp::Schema::Base
|
502
|
+
User.all.each do |user|
|
503
|
+
resource UserResource.new(id: user.id)
|
504
|
+
end
|
474
505
|
end
|
475
506
|
```
|
476
507
|
|
@@ -482,12 +513,12 @@ Create dedicated tool classes for each model or operation instead of generic too
|
|
482
513
|
|
483
514
|
```ruby
|
484
515
|
# ✅ GOOD: Specific tool for a single purpose
|
485
|
-
class SearchUsersTool < ActiveMcp::Tool
|
516
|
+
class SearchUsersTool < ActiveMcp::Tool::Base
|
486
517
|
# ...specific implementation
|
487
518
|
end
|
488
519
|
|
489
520
|
# ❌ BAD: Generic tool that dynamically loads models
|
490
|
-
class GenericSearchTool < ActiveMcp::Tool
|
521
|
+
class GenericSearchTool < ActiveMcp::Tool::Base
|
491
522
|
# Avoid this pattern - security and maintainability issues
|
492
523
|
end
|
493
524
|
```
|
@@ -497,7 +528,7 @@ end
|
|
497
528
|
Always validate and sanitize inputs in your tool implementations:
|
498
529
|
|
499
530
|
```ruby
|
500
|
-
def call(user_id:, **args)
|
531
|
+
def call(user_id:, **args, context: {})
|
501
532
|
# Validate input
|
502
533
|
unless user_id.is_a?(Integer) || user_id.to_s.match?(/^\d+$/)
|
503
534
|
raise "Invalid user ID format"
|
@@ -514,7 +545,7 @@ end
|
|
514
545
|
Return structured responses that are easy for AI to parse:
|
515
546
|
|
516
547
|
```ruby
|
517
|
-
def call(query:,
|
548
|
+
def call(query:, context: {})
|
518
549
|
results = User.search(query)
|
519
550
|
|
520
551
|
{
|
@@ -1,9 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../concerns/active_mcp/authenticatable"
|
4
|
+
require_relative "../concerns/active_mcp/request_handlable"
|
5
|
+
require_relative "../concerns/active_mcp/resource_readable"
|
6
|
+
require_relative "../concerns/active_mcp/tool_executable"
|
7
|
+
|
3
8
|
module ActiveMcp
|
4
9
|
class BaseController < ActionController::Base
|
5
|
-
include RequestHandlable
|
6
|
-
include ResourceReadable
|
7
|
-
include ToolExecutable
|
10
|
+
include ::ActiveMcp::RequestHandlable
|
11
|
+
include ::ActiveMcp::ResourceReadable
|
12
|
+
include ::ActiveMcp::ToolExecutable
|
13
|
+
include ::ActiveMcp::Authenticatable
|
14
|
+
|
15
|
+
protect_from_forgery with: :null_session
|
16
|
+
skip_before_action :verify_authenticity_token
|
17
|
+
before_action :authenticate, only: [:index]
|
18
|
+
|
19
|
+
def index
|
20
|
+
if json_rpc_request?
|
21
|
+
handle_mcp_client_request
|
22
|
+
else
|
23
|
+
handle_mcp_server_request
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def schema
|
30
|
+
nil
|
31
|
+
end
|
8
32
|
end
|
9
33
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
module Authenticatable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def authenticate
|
8
|
+
auth_header = request.headers["Authorization"]
|
9
|
+
if auth_header.present?
|
10
|
+
@context ||= {}
|
11
|
+
@context[:auth_info] = {
|
12
|
+
header: auth_header,
|
13
|
+
type: if auth_header.start_with?("Bearer ")
|
14
|
+
:bearer
|
15
|
+
elsif auth_header.start_with?("Basic ")
|
16
|
+
:basic
|
17
|
+
else
|
18
|
+
:unknown
|
19
|
+
end,
|
20
|
+
token: auth_header.split(" ").last
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def context
|
26
|
+
@context
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -4,20 +4,6 @@ module ActiveMcp
|
|
4
4
|
module RequestHandlable
|
5
5
|
extend ActiveSupport::Concern
|
6
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
7
|
private
|
22
8
|
|
23
9
|
def json_rpc_request?
|
@@ -26,7 +12,6 @@ module ActiveMcp
|
|
26
12
|
|
27
13
|
def handle_mcp_client_request
|
28
14
|
@id = params[:id]
|
29
|
-
@auth_info = auth_info
|
30
15
|
|
31
16
|
case params[:method]
|
32
17
|
when Method::INITIALIZE
|
@@ -36,25 +21,30 @@ module ActiveMcp
|
|
36
21
|
when Method::CANCELLED
|
37
22
|
render 'active_mcp/cancelled', formats: :json
|
38
23
|
when Method::RESOURCES_LIST
|
39
|
-
@resources =
|
24
|
+
@resources = schema.resources
|
40
25
|
@format = :jsonrpc
|
41
26
|
render 'active_mcp/resources_list', formats: :json
|
42
27
|
when Method::RESOURCES_TEMPLATES_LIST
|
43
|
-
@resource_templates =
|
28
|
+
@resource_templates = schema.resource_templates
|
44
29
|
@format = :jsonrpc
|
45
30
|
render 'active_mcp/resource_templates_list', formats: :json
|
46
31
|
when Method::RESOURCES_READ
|
47
|
-
@resource = read_resource(params:,
|
32
|
+
@resource = read_resource(params:, context:)
|
48
33
|
@format = :jsonrpc
|
49
34
|
render 'active_mcp/resources_read', formats: :json
|
50
35
|
when Method::TOOLS_LIST
|
51
|
-
@tools =
|
36
|
+
@tools = schema.tools
|
52
37
|
@format = :jsonrpc
|
53
38
|
render 'active_mcp/tools_list', formats: :json
|
54
39
|
when Method::TOOLS_CALL
|
55
|
-
@tool_result = execute_tool(params
|
40
|
+
@tool_result = execute_tool(params:, context:)
|
56
41
|
@format = :jsonrpc
|
57
42
|
render 'active_mcp/tools_call', formats: :json
|
43
|
+
when Method::COMPLETION_COMPLETE
|
44
|
+
type = params.dig(:params, :ref, :type)
|
45
|
+
@completion = ActiveMcp::Completion.new.complete(params: params[:params], context:, refs: type === "ref/resource" ? schema.resource_templates : [])
|
46
|
+
@format = :jsonrpc
|
47
|
+
render "active_mcp/completion_complete", formats: :json
|
58
48
|
else
|
59
49
|
@format = :jsonrpc
|
60
50
|
render 'active_mcp/no_method', formats: :json
|
@@ -62,54 +52,40 @@ module ActiveMcp
|
|
62
52
|
end
|
63
53
|
|
64
54
|
def handle_mcp_server_request
|
65
|
-
@auth_info = auth_info
|
66
|
-
|
67
55
|
case params[:method]
|
68
56
|
when Method::RESOURCES_LIST
|
69
|
-
@resources =
|
57
|
+
@resources = schema.resources
|
70
58
|
@format = :json
|
71
59
|
render 'active_mcp/resources_list', formats: :json
|
72
60
|
when Method::RESOURCES_READ
|
73
|
-
@resource = read_resource(params:,
|
61
|
+
@resource = read_resource(params:, context:)
|
74
62
|
@format = :json
|
75
63
|
render 'active_mcp/resources_read', formats: :json
|
76
64
|
when Method::RESOURCES_TEMPLATES_LIST
|
77
|
-
@resource_templates =
|
65
|
+
@resource_templates = schema.resource_templates
|
78
66
|
@format = :json
|
79
67
|
render 'active_mcp/resource_templates_list', formats: :json
|
80
68
|
when Method::TOOLS_LIST
|
81
|
-
@tools =
|
69
|
+
@tools = schema.tools
|
82
70
|
@format = :json
|
83
71
|
render 'active_mcp/tools_list', formats: :json
|
84
72
|
when Method::TOOLS_CALL
|
85
|
-
@tool_result = execute_tool(params
|
73
|
+
@tool_result = execute_tool(params:, context:)
|
86
74
|
@format = :json
|
87
75
|
render 'active_mcp/tools_call', formats: :json
|
76
|
+
when Method::COMPLETION_COMPLETE
|
77
|
+
type = params.dig(:params, :ref, :type)
|
78
|
+
@completion = ActiveMcp::Completion.new.complete(params: params[:params], context:, refs: type == "ref/resource" ? schema.resource_templates : [])
|
79
|
+
@format = :json
|
80
|
+
render "active_mcp/completion_complete", formats: :json
|
88
81
|
else
|
89
82
|
@format = :json
|
90
83
|
render 'active_mcp/no_method', formats: :json
|
91
84
|
end
|
92
85
|
end
|
93
86
|
|
94
|
-
def
|
95
|
-
|
96
|
-
if auth_header.present?
|
97
|
-
@auth_info = {
|
98
|
-
header: auth_header,
|
99
|
-
type: if auth_header.start_with?("Bearer ")
|
100
|
-
:bearer
|
101
|
-
elsif auth_header.start_with?("Basic ")
|
102
|
-
:basic
|
103
|
-
else
|
104
|
-
:unknown
|
105
|
-
end,
|
106
|
-
token: auth_header.split(" ").last
|
107
|
-
}
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def auth_info
|
112
|
-
@auth_info
|
87
|
+
def context
|
88
|
+
@context ||= {}
|
113
89
|
end
|
114
90
|
end
|
115
91
|
end
|
@@ -4,15 +4,7 @@ module ActiveMcp
|
|
4
4
|
|
5
5
|
private
|
6
6
|
|
7
|
-
def
|
8
|
-
[]
|
9
|
-
end
|
10
|
-
|
11
|
-
def resources_list
|
12
|
-
[]
|
13
|
-
end
|
14
|
-
|
15
|
-
def read_resource(params:, auth_info:)
|
7
|
+
def read_resource(params:, context:)
|
16
8
|
if params[:jsonrpc].present?
|
17
9
|
uri = params[:params][:uri]
|
18
10
|
else
|
@@ -26,7 +18,7 @@ module ActiveMcp
|
|
26
18
|
}
|
27
19
|
end
|
28
20
|
|
29
|
-
resource =
|
21
|
+
resource = schema.resources.find do |r|
|
30
22
|
r.uri == uri
|
31
23
|
end
|
32
24
|
|
@@ -37,7 +29,7 @@ module ActiveMcp
|
|
37
29
|
}
|
38
30
|
end
|
39
31
|
|
40
|
-
if resource.respond_to?(:visible?) && !resource.visible?
|
32
|
+
if resource.respond_to?(:visible?) && !resource.visible?(context:)
|
41
33
|
return {
|
42
34
|
isError: true,
|
43
35
|
contents: []
|
@@ -50,7 +42,7 @@ module ActiveMcp
|
|
50
42
|
contents: [
|
51
43
|
{
|
52
44
|
uri:,
|
53
|
-
mimeType: resource.mime_type,
|
45
|
+
mimeType: resource.class.mime_type,
|
54
46
|
text: formatted(content)
|
55
47
|
}
|
56
48
|
]
|
@@ -60,7 +52,7 @@ module ActiveMcp
|
|
60
52
|
contents: [
|
61
53
|
{
|
62
54
|
uri:,
|
63
|
-
mimeType: resource.mime_type,
|
55
|
+
mimeType: resource.class.mime_type,
|
64
56
|
blob: Base64.strict_encode64(content)
|
65
57
|
}
|
66
58
|
]
|
@@ -4,7 +4,7 @@ module ActiveMcp
|
|
4
4
|
|
5
5
|
private
|
6
6
|
|
7
|
-
def execute_tool(params:,
|
7
|
+
def execute_tool(params:, context: {})
|
8
8
|
if params[:jsonrpc].present?
|
9
9
|
tool_name = params[:params][:name]
|
10
10
|
tool_params = params[:params][:arguments]
|
@@ -25,11 +25,11 @@ module ActiveMcp
|
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
tc.
|
28
|
+
tool = schema.tools.find do |tc|
|
29
|
+
tc.name == tool_name
|
30
30
|
end
|
31
31
|
|
32
|
-
unless
|
32
|
+
unless tool
|
33
33
|
return {
|
34
34
|
isError: true,
|
35
35
|
content: [
|
@@ -41,7 +41,7 @@ module ActiveMcp
|
|
41
41
|
}
|
42
42
|
end
|
43
43
|
|
44
|
-
unless
|
44
|
+
unless tool.visible?(context:)
|
45
45
|
return {
|
46
46
|
isError: true,
|
47
47
|
content: [
|
@@ -69,7 +69,6 @@ module ActiveMcp
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
-
tool = tool_class.new
|
73
72
|
validation_result = tool.validate_arguments(arguments)
|
74
73
|
|
75
74
|
if validation_result.is_a?(Hash) && validation_result[:error]
|
@@ -86,13 +85,11 @@ module ActiveMcp
|
|
86
85
|
|
87
86
|
# Execute the tool
|
88
87
|
begin
|
89
|
-
arguments[:auth_info] = auth_info if auth_info.present?
|
90
|
-
|
91
88
|
return {
|
92
89
|
content: [
|
93
90
|
{
|
94
91
|
type: "text",
|
95
|
-
text: formatted(tool.call(**arguments
|
92
|
+
text: formatted(tool.call(**arguments, context:))
|
96
93
|
}
|
97
94
|
]
|
98
95
|
}
|
@@ -0,0 +1,16 @@
|
|
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 do
|
6
|
+
json.completion do
|
7
|
+
json.values @completion[:values]
|
8
|
+
json.total @completion[:total]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
else
|
12
|
+
json.result do
|
13
|
+
json.values @completion[:values]
|
14
|
+
json.total @completion[:total]
|
15
|
+
end
|
16
|
+
end
|
@@ -7,7 +7,7 @@ if @format == :jsonrpc
|
|
7
7
|
json.array!(@resources) do |resource|
|
8
8
|
json.name resource.name
|
9
9
|
json.uri resource.uri
|
10
|
-
json.mimeType resource.mime_type
|
10
|
+
json.mimeType resource.class.mime_type
|
11
11
|
json.description resource.description
|
12
12
|
end
|
13
13
|
end
|
@@ -17,7 +17,7 @@ else
|
|
17
17
|
json.array!(@resources) do |resource|
|
18
18
|
json.name resource.name
|
19
19
|
json.uri resource.uri
|
20
|
-
json.mimeType resource.mime_type
|
20
|
+
json.mimeType resource.class.mime_type
|
21
21
|
json.description resource.description
|
22
22
|
end
|
23
23
|
end
|
@@ -3,8 +3,20 @@ json.id @id if @format == :jsonrpc && @id.present?
|
|
3
3
|
|
4
4
|
if @format == :jsonrpc
|
5
5
|
json.result do
|
6
|
-
json.tools
|
6
|
+
json.tools do
|
7
|
+
json.array!(@tools) do |tool|
|
8
|
+
json.name tool.name
|
9
|
+
json.description tool.description
|
10
|
+
json.inputSchema tool.class.schema
|
11
|
+
end
|
12
|
+
end
|
7
13
|
end
|
8
14
|
else
|
9
|
-
json.result
|
15
|
+
json.result do
|
16
|
+
json.array!(@tools) do |tool|
|
17
|
+
json.name tool.name
|
18
|
+
json.description tool.description
|
19
|
+
json.inputSchema tool.class.schema
|
20
|
+
end
|
21
|
+
end
|
10
22
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
class Completion
|
3
|
+
def complete(params: {}, context: {}, refs: [])
|
4
|
+
ref_name = params.dig(:ref, :name)
|
5
|
+
uri_template = params.dig(:ref, :uri)
|
6
|
+
arg_name = params.dig(:argument, :name)
|
7
|
+
value = params.dig(:argument, :value)
|
8
|
+
|
9
|
+
if uri_template
|
10
|
+
resource_class = refs.find { _1.uri_template == uri_template }
|
11
|
+
values = resource_class.arguments[arg_name.to_sym].call(value)
|
12
|
+
{ values:, total: values.length }
|
13
|
+
elsif ref_name
|
14
|
+
{ values: [], total: 0 }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/active_mcp/engine.rb
CHANGED
@@ -2,20 +2,17 @@ module ActiveMcp
|
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace ActiveMcp
|
4
4
|
|
5
|
-
initializer "active_mcp.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
if Dir.exist?(tools_path)
|
17
|
-
Dir[tools_path.join("*.rb")].sort.each do |file|
|
18
|
-
require_dependency file
|
5
|
+
initializer "active_mcp.eager_load" do |app|
|
6
|
+
[
|
7
|
+
Rails.root.join("app", "mcp", "tools"),
|
8
|
+
Rails.root.join("app", "mcp", "resources"),
|
9
|
+
Rails.root.join("app", "mcp", "resource_templates"),
|
10
|
+
Rails.root.join("app", "mcp", "schemas")
|
11
|
+
].each do |tools_path|
|
12
|
+
if Dir.exist?(tools_path)
|
13
|
+
Dir[tools_path.join("*.rb")].sort.each do |file|
|
14
|
+
require_dependency file
|
15
|
+
end
|
19
16
|
end
|
20
17
|
end
|
21
18
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "json-schema"
|
2
|
+
|
3
|
+
module ActiveMcp
|
4
|
+
module Resource
|
5
|
+
class Base
|
6
|
+
class << self
|
7
|
+
attr_reader :schema, :arguments
|
8
|
+
|
9
|
+
def argument(name, complete)
|
10
|
+
@arguments = {}
|
11
|
+
@arguments[name] = complete
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
end
|
20
|
+
|
21
|
+
def description
|
22
|
+
end
|
23
|
+
|
24
|
+
def visible?(context: {})
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ActiveMcp
|
2
|
+
module Schema
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
attr_reader :resources, :resource_templates, :tools
|
6
|
+
|
7
|
+
def resource(klass)
|
8
|
+
@resources ||= []
|
9
|
+
@resources << klass
|
10
|
+
|
11
|
+
if klass.class.respond_to?(:uri_template)
|
12
|
+
@resource_templates ||= []
|
13
|
+
@resource_templates << klass.class unless klass.class.in?(@resource_templates)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def tool(klass)
|
18
|
+
@tools ||= []
|
19
|
+
@tools << klass
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(context: {})
|
24
|
+
@context = context
|
25
|
+
end
|
26
|
+
|
27
|
+
def resources
|
28
|
+
self.class.resources.filter do |resource|
|
29
|
+
!resource.respond_to?(:visible?) || resource.visible?(context: @context)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def resource_templates
|
34
|
+
self.class.resource_templates.filter do |template|
|
35
|
+
!template.respond_to?(:visible?) || template.visible?(context: @context)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def tools
|
40
|
+
self.class.tools.filter do |tool|
|
41
|
+
!tool.respond_to?(:visible?) || tool.visible?(context: @context)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -58,6 +58,8 @@ module ActiveMcp
|
|
58
58
|
handle_call_tool(request)
|
59
59
|
when Method::RESOURCES_READ
|
60
60
|
handle_read_resource(request)
|
61
|
+
when Method::COMPLETION_COMPLETE
|
62
|
+
handle_complete(request)
|
61
63
|
else
|
62
64
|
error_response(request[:id], ErrorCode::METHOD_NOT_FOUND, "Unknown method: #{request[:method]}")
|
63
65
|
end
|
@@ -194,6 +196,21 @@ module ActiveMcp
|
|
194
196
|
end
|
195
197
|
end
|
196
198
|
|
199
|
+
def handle_complete(request)
|
200
|
+
begin
|
201
|
+
result = @server.fetch(
|
202
|
+
params: {
|
203
|
+
method: Method::COMPLETION_COMPLETE,
|
204
|
+
params: request[:params],
|
205
|
+
}
|
206
|
+
)
|
207
|
+
success_response(request[:id], { completion: result[:result] })
|
208
|
+
rescue => e
|
209
|
+
Server.("Error reading resource #{uri}", e)
|
210
|
+
error_response(request[:id], ErrorCode::INTERNAL_ERROR, "An error occurred while reading the resource")
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
197
214
|
def success_response(id, result)
|
198
215
|
{
|
199
216
|
jsonrpc: JSON_RPC_VERSION,
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "json-schema"
|
2
|
+
|
3
|
+
module ActiveMcp
|
4
|
+
module Tool
|
5
|
+
class Base
|
6
|
+
class << self
|
7
|
+
attr_reader :schema
|
8
|
+
|
9
|
+
def argument(name, type, required: false, description: nil)
|
10
|
+
@schema ||= default_schema
|
11
|
+
|
12
|
+
@schema["properties"][name.to_s] = {"type" => type.to_s}
|
13
|
+
@schema["properties"][name.to_s]["description"] = description if description
|
14
|
+
@schema["required"] << name.to_s if required
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_schema
|
18
|
+
{
|
19
|
+
"type" => "object",
|
20
|
+
"properties" => {},
|
21
|
+
"required" => []
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
end
|
31
|
+
|
32
|
+
def description
|
33
|
+
end
|
34
|
+
|
35
|
+
def visible?(context: {})
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(context: {}, **args)
|
40
|
+
raise NotImplementedError, "#{self.class.name}#call must be implemented"
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_arguments(args)
|
44
|
+
return true unless self.class.schema
|
45
|
+
|
46
|
+
JSON::Validator.validate!(self.class.schema, args)
|
47
|
+
rescue JSON::Schema::ValidationError => e
|
48
|
+
{error: e.message}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/active_mcp/version.rb
CHANGED
data/lib/active_mcp.rb
CHANGED
@@ -3,11 +3,15 @@
|
|
3
3
|
require "jbuilder"
|
4
4
|
require_relative "active_mcp/version"
|
5
5
|
require_relative "active_mcp/configuration"
|
6
|
-
require_relative "active_mcp/
|
6
|
+
require_relative "active_mcp/schema/base"
|
7
|
+
require_relative "active_mcp/tool/base"
|
8
|
+
require_relative "active_mcp/resource/base"
|
7
9
|
require_relative "active_mcp/server"
|
10
|
+
require_relative "active_mcp/completion"
|
8
11
|
|
9
12
|
if defined? ::Rails
|
10
13
|
require_relative "active_mcp/engine"
|
14
|
+
require_relative "active_mcp/controller/base"
|
11
15
|
end
|
12
16
|
|
13
17
|
module ActiveMcp
|
@@ -2,16 +2,12 @@ module ActiveMcp
|
|
2
2
|
module Generators
|
3
3
|
class InstallGenerator < Rails::Generators::Base
|
4
4
|
source_root File.expand_path('templates', __dir__)
|
5
|
-
|
6
|
-
desc "Creates an Active MCP initializer
|
7
|
-
|
5
|
+
|
6
|
+
desc "Creates an Active MCP initializer"
|
7
|
+
|
8
8
|
def create_initializer_file
|
9
9
|
template "initializer.rb", "config/initializers/active_mcp.rb"
|
10
10
|
end
|
11
|
-
|
12
|
-
def update_routes
|
13
|
-
route "mount ActiveMcp::Engine, at: '/mcp'"
|
14
|
-
end
|
15
11
|
end
|
16
12
|
end
|
17
13
|
end
|
@@ -4,7 +4,7 @@ module ActiveMcp
|
|
4
4
|
source_root File.expand_path("templates", __dir__)
|
5
5
|
|
6
6
|
def create_resource_file
|
7
|
-
template "resource.rb.erb", File.join("app/resources", "#{file_name}_resource.rb")
|
7
|
+
template "resource.rb.erb", File.join("app/mcp/resources", "#{file_name}_resource.rb")
|
8
8
|
end
|
9
9
|
|
10
10
|
private
|
@@ -1,12 +1,4 @@
|
|
1
1
|
class <%= class_name %>
|
2
|
-
def initialize(auth_info:)
|
3
|
-
@auth_info = auth_info
|
4
|
-
|
5
|
-
# Authentication information can be accessed via @auth_info parameter
|
6
|
-
# @auth_info = { type: :bearer, token: "xxx", header: "Bearer xxx" }
|
7
|
-
# or { type: :basic, token: "base64encoded", header: "Basic base64encoded" }
|
8
|
-
end
|
9
|
-
|
10
2
|
def name
|
11
3
|
"<%= file_name %>"
|
12
4
|
end
|
@@ -25,16 +17,16 @@ class <%= class_name %>
|
|
25
17
|
|
26
18
|
# Uncomment and modify this method to implement authorization control
|
27
19
|
# This controls who can see and use this tool
|
28
|
-
# def visible?
|
20
|
+
# def visible?(context: {})
|
29
21
|
# # Example: require authentication
|
30
|
-
# # return false unless
|
22
|
+
# # return false unless context
|
31
23
|
#
|
32
24
|
# # Example: require a specific authentication type
|
33
|
-
# # return false unless
|
25
|
+
# # return false unless context[:auth_info][:type] == :bearer
|
34
26
|
#
|
35
27
|
# # Example: check for admin permissions
|
36
28
|
# # admin_tokens = ["admin-token"]
|
37
|
-
# # return admin_tokens.include?(
|
29
|
+
# # return admin_tokens.include?(context[:auth_info][:token])
|
38
30
|
#
|
39
31
|
# # Default: allow all access
|
40
32
|
# true
|
@@ -1,5 +1,11 @@
|
|
1
|
-
class <%= class_name %> < ActiveMcp::Tool
|
2
|
-
|
1
|
+
class <%= class_name %> < ActiveMcp::Tool::Base
|
2
|
+
def name
|
3
|
+
"<%= file_name.humanize %>"
|
4
|
+
end
|
5
|
+
|
6
|
+
def description
|
7
|
+
"<%= file_name.humanize %>"
|
8
|
+
end
|
3
9
|
|
4
10
|
argument :param1, :string, required: true, description: "First parameter description"
|
5
11
|
argument :param2, :string, required: false, description: "Second parameter description"
|
@@ -7,25 +13,25 @@ class <%= class_name %> < ActiveMcp::Tool
|
|
7
13
|
|
8
14
|
# Uncomment and modify this method to implement authorization control
|
9
15
|
# This controls who can see and use this tool
|
10
|
-
# def
|
16
|
+
# def visible?(context: {})
|
11
17
|
# # Example: require authentication
|
12
|
-
# # return false unless
|
18
|
+
# # return false unless context
|
13
19
|
#
|
14
20
|
# # Example: require a specific authentication type
|
15
|
-
# # return false unless auth_info[:type] == :bearer
|
21
|
+
# # return false unless context[:auth_info][:type] == :bearer
|
16
22
|
#
|
17
23
|
# # Example: check for admin permissions
|
18
24
|
# # admin_tokens = ["admin-token"]
|
19
|
-
# # return admin_tokens.include?(auth_info[:token])
|
25
|
+
# # return admin_tokens.include?(context[:auth_info][:token])
|
20
26
|
#
|
21
27
|
# # Default: allow all access
|
22
28
|
# true
|
23
29
|
# end
|
24
30
|
|
25
|
-
def call(param1:, param2: nil,
|
31
|
+
def call(param1:, param2: nil, context: {})
|
26
32
|
# Authentication information can be accessed via _auth_info parameter
|
27
|
-
#
|
28
|
-
# or { type: :basic, token: "base64encoded", header: "Basic base64encoded" }
|
33
|
+
# context = { auth_info: { type: :bearer, token: "xxx", header: "Bearer xxx" } }
|
34
|
+
# or { auth_info: { type: :basic, token: "base64encoded", header: "Basic base64encoded" } }
|
29
35
|
|
30
36
|
# Implement tool logic here
|
31
37
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_mcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Moeki Kawakami
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-04-
|
10
|
+
date: 2025-04-08 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -68,10 +68,12 @@ files:
|
|
68
68
|
- README.md
|
69
69
|
- Rakefile
|
70
70
|
- app/controllers/active_mcp/base_controller.rb
|
71
|
+
- app/controllers/concerns/active_mcp/authenticatable.rb
|
71
72
|
- app/controllers/concerns/active_mcp/request_handlable.rb
|
72
73
|
- app/controllers/concerns/active_mcp/resource_readable.rb
|
73
74
|
- app/controllers/concerns/active_mcp/tool_executable.rb
|
74
75
|
- app/views/active_mcp/cancelled.json.jbuilder
|
76
|
+
- app/views/active_mcp/completion_complete.json.jbuilder
|
75
77
|
- app/views/active_mcp/initialize.json.jbuilder
|
76
78
|
- app/views/active_mcp/initialized.json.jbuilder
|
77
79
|
- app/views/active_mcp/no_method.json.jbuilder
|
@@ -82,15 +84,19 @@ files:
|
|
82
84
|
- app/views/active_mcp/tools_list.json.jbuilder
|
83
85
|
- config/routes.rb
|
84
86
|
- lib/active_mcp.rb
|
87
|
+
- lib/active_mcp/completion.rb
|
85
88
|
- lib/active_mcp/configuration.rb
|
89
|
+
- lib/active_mcp/controller/base.rb
|
86
90
|
- lib/active_mcp/engine.rb
|
91
|
+
- lib/active_mcp/resource/base.rb
|
92
|
+
- lib/active_mcp/schema/base.rb
|
87
93
|
- lib/active_mcp/server.rb
|
88
94
|
- lib/active_mcp/server/error_codes.rb
|
89
95
|
- lib/active_mcp/server/fetcher.rb
|
90
96
|
- lib/active_mcp/server/method.rb
|
91
97
|
- lib/active_mcp/server/protocol_handler.rb
|
92
98
|
- lib/active_mcp/server/stdio_connection.rb
|
93
|
-
- lib/active_mcp/tool.rb
|
99
|
+
- lib/active_mcp/tool/base.rb
|
94
100
|
- lib/active_mcp/version.rb
|
95
101
|
- lib/generators/active_mcp/install/install_generator.rb
|
96
102
|
- lib/generators/active_mcp/install/templates/initializer.rb
|
data/lib/active_mcp/tool.rb
DELETED
@@ -1,82 +0,0 @@
|
|
1
|
-
require "json-schema"
|
2
|
-
|
3
|
-
module ActiveMcp
|
4
|
-
class Tool
|
5
|
-
class << self
|
6
|
-
attr_reader :desc, :schema
|
7
|
-
|
8
|
-
def tool_name
|
9
|
-
name ? name.underscore.sub(/_tool$/, "") : ""
|
10
|
-
end
|
11
|
-
|
12
|
-
def description(value)
|
13
|
-
@desc = value
|
14
|
-
end
|
15
|
-
|
16
|
-
def property(name, type, required: false, description: nil)
|
17
|
-
@schema ||= default_schema
|
18
|
-
|
19
|
-
@schema["properties"][name.to_s] = {"type" => type.to_s}
|
20
|
-
@schema["properties"][name.to_s]["description"] = description if description
|
21
|
-
@schema["required"] << name.to_s if required
|
22
|
-
end
|
23
|
-
|
24
|
-
def argument(...)
|
25
|
-
property(...)
|
26
|
-
end
|
27
|
-
|
28
|
-
def registered_tools
|
29
|
-
@registered_tools ||= []
|
30
|
-
end
|
31
|
-
|
32
|
-
attr_writer :registered_tools
|
33
|
-
|
34
|
-
def inherited(subclass)
|
35
|
-
registered_tools << subclass
|
36
|
-
end
|
37
|
-
|
38
|
-
def visible?(auth_info)
|
39
|
-
if respond_to?(:authorized?)
|
40
|
-
authorized?(auth_info)
|
41
|
-
else
|
42
|
-
true
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def authorized_tools(auth_info = nil)
|
47
|
-
registered_tools.select do |tool_class|
|
48
|
-
tool_class.visible?(auth_info)
|
49
|
-
end.map do |tool_class|
|
50
|
-
{
|
51
|
-
name: tool_class.tool_name,
|
52
|
-
description: tool_class.desc,
|
53
|
-
inputSchema: tool_class.schema || default_schema
|
54
|
-
}
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def default_schema
|
59
|
-
{
|
60
|
-
"type" => "object",
|
61
|
-
"properties" => {},
|
62
|
-
"required" => []
|
63
|
-
}
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def initialize
|
68
|
-
end
|
69
|
-
|
70
|
-
def call(**args)
|
71
|
-
raise NotImplementedError, "#{self.class.name}#call must be implemented"
|
72
|
-
end
|
73
|
-
|
74
|
-
def validate_arguments(args)
|
75
|
-
return true unless self.class.schema
|
76
|
-
|
77
|
-
JSON::Validator.validate!(self.class.schema, args)
|
78
|
-
rescue JSON::Schema::ValidationError => e
|
79
|
-
{error: e.message}
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|