actionmcp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +57 -0
- data/Rakefile +9 -0
- data/exe/action_mcp_stdio +0 -0
- data/lib/action_mcp/content/audio.rb +20 -0
- data/lib/action_mcp/content/image.rb +20 -0
- data/lib/action_mcp/content/resource.rb +27 -0
- data/lib/action_mcp/content/text.rb +19 -0
- data/lib/action_mcp/content.rb +21 -0
- data/lib/action_mcp/gem_version.rb +6 -0
- data/lib/action_mcp/json_rpc/base.rb +12 -0
- data/lib/action_mcp/json_rpc/json_rpc_error.rb +70 -0
- data/lib/action_mcp/json_rpc/notification.rb +20 -0
- data/lib/action_mcp/json_rpc/request.rb +22 -0
- data/lib/action_mcp/json_rpc/response.rb +53 -0
- data/lib/action_mcp/json_rpc.rb +13 -0
- data/lib/action_mcp/prompt.rb +139 -0
- data/lib/action_mcp/prompts_registry.rb +10 -0
- data/lib/action_mcp/railtie.rb +14 -0
- data/lib/action_mcp/registry_base.rb +71 -0
- data/lib/action_mcp/resource.rb +20 -0
- data/lib/action_mcp/resources_bank.rb +95 -0
- data/lib/action_mcp/tool.rb +147 -0
- data/lib/action_mcp/tools_registry.rb +12 -0
- data/lib/action_mcp/transport.rb +244 -0
- data/lib/action_mcp/version.rb +11 -0
- data/lib/action_mcp.rb +39 -0
- data/lib/actionmcp.rb +3 -0
- data/lib/tasks/action_mcp_tasks.rake +6 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1183f05df43cca3948c84aa105f6e38c5f7cd414c1b38619cfb6c9afa528d442
|
4
|
+
data.tar.gz: 02bdfed4f0a3b26d04203430c4b62183c615fb0a47a3b28d281b42f07928e5a8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 77b9a85435b5e19a35268db9fbcc3139222c01abc7c67e5af61a44a460d55fe5ec6b9819209fa8bfd493a3d0c648ba7d7fa5c440f889871556880f5f961a1da0
|
7
|
+
data.tar.gz: e77783308752dbbe95498ca9d3548471b44bafbfe72a130f6e070c906220eeeecd48fb8d719f150d2f69a14a416d7f0cba19c3fd33b6479eab54f564633eed73
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Abdelkader Boudih 2024
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# ActionMCP
|
2
|
+
|
3
|
+
**ActionMCP** is a Ruby gem that provides essential tooling for building Model Context Protocol (MCP) capable servers.
|
4
|
+
It offers base classes and helpers for creating MCP applications, making it easier to integrate your Ruby/Rails application with the MCP standard.
|
5
|
+
With ActionMCP, you can focus on your app's logic while it handles the boilerplate for MCP compliance.
|
6
|
+
|
7
|
+
## Introduction
|
8
|
+
|
9
|
+
**Model Context Protocol (MCP)** is an open protocol that standardizes how applications provide context to large language models (LLMs) ([Introduction - Model Context Protocol](https://modelcontextprotocol.io/introduction#:~:text=MCP%20is%20an%20open%20protocol,different%20data%20sources%20and%20tools)).
|
10
|
+
|
11
|
+
Think of it as a universal interface for connecting AI assistants to external data sources and tools.
|
12
|
+
|
13
|
+
MCP allows AI systems to plug into various resources in a consistent, secure way, enabling two-way integration between your data and AI-powered applications ([Introducing the Model Context Protocol \ Anthropic](https://www.anthropic.com/news/model-context-protocol#:~:text=The%20Model%20Context%20Protocol%20is,that%20connect%20to%20these%20servers)).
|
14
|
+
|
15
|
+
This means an AI (like an LLM) can request information or actions from your application through a well-defined protocol, and your app can provide context or perform tasks for the AI in return.
|
16
|
+
|
17
|
+
**ActionMCP** is targeted at developers building MCP-enabled applications.
|
18
|
+
It simplifies the process of integrating Ruby and Rails apps with the MCP standard by providing a set of base classes and an easy-to-use server interface.
|
19
|
+
|
20
|
+
Instead of implementing MCP support from scratch, you can subclass and configure the provided **Prompt**, **Tool**, and **Resource** classes to expose your app’s functionality to LLMs.
|
21
|
+
|
22
|
+
ActionMCP handles the underlying MCP message format and routing, so you can adhere to the open standard with minimal effort.
|
23
|
+
|
24
|
+
In short, ActionMCP helps you build an MCP server (the component that exposes capabilities to AI) more quickly and with fewer mistakes.
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
To start using ActionMCP, add it to your project:
|
29
|
+
|
30
|
+
- **Using Bundler (Rails or Ruby projects):** Add the gem to your Gemfile and run bundle install:
|
31
|
+
|
32
|
+
execute:
|
33
|
+
```
|
34
|
+
$ bundle add actionmcp
|
35
|
+
```
|
36
|
+
|
37
|
+
After installing, include the gem in your code by requiring it:
|
38
|
+
|
39
|
+
This will load the ActionMCP library so you can start defining MCP prompts, tools, and resources in your application.
|
40
|
+
|
41
|
+
## Core Components
|
42
|
+
|
43
|
+
ActionMCP provides three core abstractions to streamline MCP server development: **Prompt**, **Tool**, and **Resource**.
|
44
|
+
These correspond to key MCP concepts and let you define what context or capabilities your server exposes to LLMs.
|
45
|
+
Below is an overview of each component and how you might use it:
|
46
|
+
|
47
|
+
### ActionMCP::Prompt
|
48
|
+
|
49
|
+
Make Rails Say Sexy stuff
|
50
|
+
|
51
|
+
### ActionMCP::Tool
|
52
|
+
|
53
|
+
Make Rails Do Sexy stuff and serve beer to Clients.
|
54
|
+
|
55
|
+
### ActionMCP::Resource
|
56
|
+
|
57
|
+
I dont need this for now
|
data/Rakefile
ADDED
File without changes
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Content
|
5
|
+
# Audio content includes a base64-encoded audio clip and its MIME type.
|
6
|
+
class Audio < Base
|
7
|
+
attr_reader :data, :mime_type
|
8
|
+
|
9
|
+
def initialize(data, mime_type)
|
10
|
+
super("audio")
|
11
|
+
@data = data
|
12
|
+
@mime_type = mime_type
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
super.merge(data: @data, mimeType: @mime_type)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Content
|
5
|
+
# Image content includes a base64-encoded image and its MIME type.
|
6
|
+
class Image < Base
|
7
|
+
attr_reader :data, :mime_type
|
8
|
+
|
9
|
+
def initialize(data, mime_type)
|
10
|
+
super("image")
|
11
|
+
@data = data
|
12
|
+
@mime_type = mime_type
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
super.merge(data: @data, mimeType: @mime_type)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Content
|
5
|
+
# Resource content references a server-managed resource.
|
6
|
+
# It includes a URI, MIME type, and optionally text content or a base64-encoded blob.
|
7
|
+
class Resource < Base
|
8
|
+
attr_reader :uri, :mime_type, :text, :blob
|
9
|
+
|
10
|
+
def initialize(uri, mime_type, text: nil, blob: nil)
|
11
|
+
super("resource")
|
12
|
+
@uri = uri
|
13
|
+
@mime_type = mime_type
|
14
|
+
@text = text
|
15
|
+
@blob = blob
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
resource_data = { uri: @uri, mimeType: @mime_type }
|
20
|
+
resource_data[:text] = @text if @text
|
21
|
+
resource_data[:blob] = @blob if @blob
|
22
|
+
|
23
|
+
super.merge(resource: resource_data)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Content
|
5
|
+
# Text content represents plain text messages.
|
6
|
+
class Text < Base
|
7
|
+
attr_reader :text
|
8
|
+
|
9
|
+
def initialize(text)
|
10
|
+
super("text")
|
11
|
+
@text = text
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
super.merge(text: @text)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActionMCP
|
2
|
+
module Content
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
# Base class for MCP content items.
|
5
|
+
class Base
|
6
|
+
attr_reader :type
|
7
|
+
|
8
|
+
def initialize(type)
|
9
|
+
@type = type
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
{ type: @type }
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_json(*args)
|
17
|
+
to_h.to_json(*args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module JsonRpc
|
5
|
+
private
|
6
|
+
|
7
|
+
def validate_id(id)
|
8
|
+
raise Error, "ID must be a string or number" unless id.is_a?(String) || id.is_a?(Numeric)
|
9
|
+
raise Error, "ID must not be null" if id.nil?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module JsonRpc
|
5
|
+
class JsonRpcError < StandardError
|
6
|
+
# Define the standard JSON-RPC 2.0 error codes
|
7
|
+
ERROR_CODES = {
|
8
|
+
parse_error: {
|
9
|
+
code: -32_700,
|
10
|
+
message: "Parse error"
|
11
|
+
},
|
12
|
+
invalid_request: {
|
13
|
+
code: -32_600,
|
14
|
+
message: "Invalid request"
|
15
|
+
},
|
16
|
+
method_not_found: {
|
17
|
+
code: -32_601,
|
18
|
+
message: "Method not found"
|
19
|
+
},
|
20
|
+
invalid_params: {
|
21
|
+
code: -32_602,
|
22
|
+
message: "Invalid params"
|
23
|
+
},
|
24
|
+
internal_error: {
|
25
|
+
code: -32_603,
|
26
|
+
message: "Internal error"
|
27
|
+
},
|
28
|
+
server_error: {
|
29
|
+
code: -32_000,
|
30
|
+
message: "Server error"
|
31
|
+
}
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
attr_reader :code, :data
|
35
|
+
|
36
|
+
# Retrieve error details by symbol.
|
37
|
+
def self.[](symbol)
|
38
|
+
ERROR_CODES[symbol] or raise ArgumentError, "Unknown error code: #{symbol}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Build an error hash, allowing custom message or data to override defaults.
|
42
|
+
def self.build(symbol, message: nil, data: nil)
|
43
|
+
error = self[symbol].dup
|
44
|
+
error[:message] = message if message
|
45
|
+
error[:data] = data if data
|
46
|
+
error
|
47
|
+
end
|
48
|
+
|
49
|
+
# Initialize the error using a symbol key, with optional custom message and data.
|
50
|
+
def initialize(symbol, message: nil, data: nil)
|
51
|
+
error_details = self.class.build(symbol, message: message, data: data)
|
52
|
+
@code = error_details[:code]
|
53
|
+
@data = error_details[:data]
|
54
|
+
super(error_details[:message])
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a hash formatted for a JSON-RPC error response.
|
58
|
+
def as_json
|
59
|
+
hash = { code: code, message: message }
|
60
|
+
hash[:data] = data if data
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
|
64
|
+
# Converts the error hash to a JSON string.
|
65
|
+
def to_json(*_args)
|
66
|
+
as_json.to_json
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module JsonRpc
|
5
|
+
Notification = Data.define(:method, :params) do
|
6
|
+
def initialize(method:, params: nil)
|
7
|
+
super(method: method, params: params)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
hash = {
|
12
|
+
jsonrpc: "2.0",
|
13
|
+
method: method
|
14
|
+
}
|
15
|
+
hash[:params] = params if params
|
16
|
+
hash
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module JsonRpc
|
5
|
+
Request = Data.define(:jsonrpc, :id, :method, :params) do
|
6
|
+
def initialize(id:, method:, params: nil)
|
7
|
+
validate_id(id)
|
8
|
+
super(id: id, method: method, params: params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
hash = {
|
13
|
+
jsonrpc: "2.0",
|
14
|
+
id: id,
|
15
|
+
method: method
|
16
|
+
}
|
17
|
+
hash[:params] = params if params
|
18
|
+
hash
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module JsonRpc
|
5
|
+
Response = Data.define(:id, :result, :error) do
|
6
|
+
def initialize(id:, result: nil, error: nil)
|
7
|
+
processed_error = process_error(error)
|
8
|
+
processed_result = error ? nil : result
|
9
|
+
validate_result_error!(processed_result, processed_error)
|
10
|
+
super(id: id, result: processed_result, error: processed_error)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
hash = {
|
15
|
+
jsonrpc: "2.0",
|
16
|
+
id: id
|
17
|
+
}
|
18
|
+
if error
|
19
|
+
hash[:error] = {
|
20
|
+
code: error[:code],
|
21
|
+
message: error[:message]
|
22
|
+
}
|
23
|
+
hash[:error][:data] = error[:data] if error[:data]
|
24
|
+
else
|
25
|
+
hash[:result] = result
|
26
|
+
end
|
27
|
+
hash
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def process_error(error)
|
33
|
+
case error
|
34
|
+
when Symbol
|
35
|
+
ErrorCodes[error]
|
36
|
+
when Hash
|
37
|
+
validate_error!(error)
|
38
|
+
error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_error!(error)
|
43
|
+
raise Error, "Error code must be an integer" unless error[:code].is_a?(Integer)
|
44
|
+
raise Error, "Error message is required" unless error[:message].is_a?(String)
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_result_error!(result, error)
|
48
|
+
raise Error, "Either result or error must be set" unless result || error
|
49
|
+
raise Error, "Cannot set both result and error" if result && error
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Abstract base class for Prompts
|
5
|
+
# Defines: name, description, arguments, plus auto-registration.
|
6
|
+
class Prompt
|
7
|
+
include ActiveModel::Model
|
8
|
+
include ActiveModel::Attributes
|
9
|
+
|
10
|
+
class_attribute :_prompt_name, instance_accessor: false
|
11
|
+
class_attribute :_description, instance_accessor: false, default: ""
|
12
|
+
class_attribute :_argument_definitions, instance_accessor: false, default: []
|
13
|
+
class_attribute :abstract_prompt, instance_accessor: false, default: false
|
14
|
+
|
15
|
+
def self.inherited(subclass)
|
16
|
+
super
|
17
|
+
return if subclass == Prompt
|
18
|
+
return if "ApplicationPrompt" == subclass.name
|
19
|
+
subclass.abstract_prompt = false
|
20
|
+
|
21
|
+
# Automatically register the subclass with the PromptsRegistry
|
22
|
+
PromptsRegistry.register(subclass.prompt_name, subclass)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.abstract!
|
26
|
+
self.abstract_prompt = true
|
27
|
+
# If already registered, you might want to unregister it here.
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.abstract?
|
31
|
+
self.abstract_prompt
|
32
|
+
end
|
33
|
+
|
34
|
+
# ---------------------------------------------------
|
35
|
+
# Prompt Name
|
36
|
+
# ---------------------------------------------------
|
37
|
+
def self.prompt_name(name = nil)
|
38
|
+
if name
|
39
|
+
self._prompt_name = name
|
40
|
+
else
|
41
|
+
_prompt_name || default_prompt_name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.default_prompt_name
|
46
|
+
name.demodulize.underscore.dasherize.sub(/-prompt$/, "")
|
47
|
+
end
|
48
|
+
|
49
|
+
# ---------------------------------------------------
|
50
|
+
# Description
|
51
|
+
# ---------------------------------------------------
|
52
|
+
def self.description(text = nil)
|
53
|
+
if text
|
54
|
+
self._description = text
|
55
|
+
else
|
56
|
+
_description
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# ---------------------------------------------------
|
61
|
+
# Argument DSL
|
62
|
+
# ---------------------------------------------------
|
63
|
+
def self.argument(arg_name, description: "", required: false, default: nil)
|
64
|
+
arg_def = {
|
65
|
+
name: arg_name.to_s,
|
66
|
+
description: description,
|
67
|
+
required: required,
|
68
|
+
default: default
|
69
|
+
}
|
70
|
+
self._argument_definitions += [ arg_def ]
|
71
|
+
|
72
|
+
# Register the attribute so it's recognized by ActiveModel
|
73
|
+
attribute arg_name, :string, default: default
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.arguments
|
77
|
+
_argument_definitions
|
78
|
+
end
|
79
|
+
|
80
|
+
# ---------------------------------------------------
|
81
|
+
# Convert prompt definition to Hash
|
82
|
+
# ---------------------------------------------------
|
83
|
+
def self.to_h
|
84
|
+
{
|
85
|
+
name: prompt_name,
|
86
|
+
description: description.presence,
|
87
|
+
arguments: arguments.map { |arg| arg.slice(:name, :description, :required) }
|
88
|
+
}.compact
|
89
|
+
end
|
90
|
+
|
91
|
+
# ---------------------------------------------------
|
92
|
+
# Class-level call method
|
93
|
+
# ---------------------------------------------------
|
94
|
+
# Receives a Hash of params, initializes a prompt instance,
|
95
|
+
# validates it, and if valid, calls the instance call method.
|
96
|
+
# If invalid, raises a JsonRpcError with code :invalid_params.
|
97
|
+
#
|
98
|
+
# Usage:
|
99
|
+
# result = MyPromptClass.call(params)
|
100
|
+
#
|
101
|
+
# Raises:
|
102
|
+
# ActionMCP::JsonRpc::JsonRpcError(:invalid_params) if validation fails.
|
103
|
+
#
|
104
|
+
def self.call(params)
|
105
|
+
prompt = new(params) # Initialize an instance with provided params
|
106
|
+
unless prompt.valid?
|
107
|
+
# Collect all validation errors into a single string or array
|
108
|
+
errors_str = prompt.errors.full_messages.join(", ")
|
109
|
+
|
110
|
+
raise ActionMCP::JsonRpc::JsonRpcError.new(
|
111
|
+
:invalid_params,
|
112
|
+
message: "Prompt validation failed: #{errors_str}",
|
113
|
+
data: { errors: prompt.errors }
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
# If we reach here, the prompt is valid
|
118
|
+
prompt.call
|
119
|
+
end
|
120
|
+
|
121
|
+
# ---------------------------------------------------
|
122
|
+
# Instance call method
|
123
|
+
# ---------------------------------------------------
|
124
|
+
# By default, does nothing. Override in your subclasses to
|
125
|
+
# perform custom prompt processing. (Return a payload if needed)
|
126
|
+
#
|
127
|
+
# Usage: Called internally after validation in self.call
|
128
|
+
#
|
129
|
+
def call
|
130
|
+
raise NotImplementedError, "Subclasses must implement the call method"
|
131
|
+
# Default implementation (no-op)
|
132
|
+
# In a real subclass, you might do:
|
133
|
+
# def call
|
134
|
+
# # Perform logic, e.g. analyze code, etc.
|
135
|
+
# # Return something meaningful.
|
136
|
+
# end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "rails"
|
2
|
+
require "active_model/railtie"
|
3
|
+
|
4
|
+
module ActionMCP
|
5
|
+
class Railtie < Rails::Railtie # :nodoc:
|
6
|
+
# TODO: fix this to be a proper railtie if you going to to opensource it
|
7
|
+
initializer "action_mcp.clear_registry" do |app|
|
8
|
+
app.config.to_prepare do
|
9
|
+
ActionMCP::ToolsRegistry.clear!
|
10
|
+
ActionMCP::PromptsRegistry.clear!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
class RegistryBase
|
5
|
+
class << self
|
6
|
+
def items
|
7
|
+
@items ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Register an item by unique name
|
11
|
+
def register(name, item_class)
|
12
|
+
raise ArgumentError, "Name can't be blank" if name.blank?
|
13
|
+
raise ArgumentError, "Name '#{name}' is already registered." if items.key?(name)
|
14
|
+
|
15
|
+
items[name] = { class: item_class, enabled: true }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Fetch an item’s metadata
|
19
|
+
# Returns { class: <Class>, enabled: <Boolean> } or nil
|
20
|
+
def fetch(name)
|
21
|
+
items[name]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Number of registered items, ignoring abstract ones.
|
25
|
+
def size
|
26
|
+
items.values.reject { |item| abstract_item?(item) }.size
|
27
|
+
end
|
28
|
+
|
29
|
+
def unregister(name)
|
30
|
+
items.delete(name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear!
|
34
|
+
items.clear
|
35
|
+
end
|
36
|
+
|
37
|
+
# List of currently available items, excluding abstract ones.
|
38
|
+
def enabled
|
39
|
+
items
|
40
|
+
.reject { |_name, item| item[:class].abstract? }
|
41
|
+
.select { |_name, item| item[:enabled] }
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_available_tool(name)
|
45
|
+
enabled[name]&.fetch(:class)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Enable an item by name
|
49
|
+
def enable(name)
|
50
|
+
raise ArgumentError, "Name '#{name}' not found." unless items.key?(name)
|
51
|
+
|
52
|
+
items[name][:enabled] = true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Disable an item by name
|
56
|
+
def disable(name)
|
57
|
+
raise ArgumentError, "Name '#{name}' not found." unless items.key?(name)
|
58
|
+
|
59
|
+
items[name][:enabled] = false
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Helper to determine if an item is abstract.
|
65
|
+
def abstract_item?(item)
|
66
|
+
klass = item[:class]
|
67
|
+
klass.respond_to?(:abstract?) && klass.abstract?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
Resource = Data.define(:uri, :name, :description, :mime_type, :size) do
|
5
|
+
# Convert the resource to a hash with the keys expected by MCP.
|
6
|
+
# Note: The key for mime_type is converted to 'mimeType' as specified.
|
7
|
+
def to_h
|
8
|
+
hash = { uri: uri, name: name }
|
9
|
+
hash[:description] = description if description
|
10
|
+
hash[:mimeType] = mime_type if mime_type
|
11
|
+
hash[:size] = size if size
|
12
|
+
hash
|
13
|
+
end
|
14
|
+
|
15
|
+
# Convert the resource to a JSON string.
|
16
|
+
def to_json(*args)
|
17
|
+
to_h.to_json(*args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|