actionmcp 0.1.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 +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
|