actionmcp 0.2.0 → 0.2.3
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 +133 -30
- data/Rakefile +0 -2
- data/exe/actionmcp_cli +221 -0
- data/lib/action_mcp/capability.rb +52 -0
- data/lib/action_mcp/client.rb +243 -1
- data/lib/action_mcp/configuration.rb +50 -1
- data/lib/action_mcp/content/audio.rb +9 -0
- data/lib/action_mcp/content/image.rb +9 -0
- data/lib/action_mcp/content/resource.rb +13 -0
- data/lib/action_mcp/content/text.rb +7 -0
- data/lib/action_mcp/content.rb +11 -6
- data/lib/action_mcp/engine.rb +34 -0
- data/lib/action_mcp/gem_version.rb +2 -2
- data/lib/action_mcp/integer_array.rb +6 -0
- data/lib/action_mcp/json_rpc/json_rpc_error.rb +21 -0
- data/lib/action_mcp/json_rpc/notification.rb +8 -0
- data/lib/action_mcp/json_rpc/request.rb +14 -0
- data/lib/action_mcp/json_rpc/response.rb +32 -1
- data/lib/action_mcp/json_rpc.rb +1 -6
- data/lib/action_mcp/json_rpc_handler.rb +106 -0
- data/lib/action_mcp/logging.rb +19 -0
- data/lib/action_mcp/prompt.rb +30 -46
- data/lib/action_mcp/prompts_registry.rb +13 -1
- data/lib/action_mcp/registry_base.rb +47 -28
- data/lib/action_mcp/renderable.rb +26 -0
- data/lib/action_mcp/resource.rb +3 -1
- data/lib/action_mcp/server.rb +4 -1
- data/lib/action_mcp/string_array.rb +5 -0
- data/lib/action_mcp/tool.rb +16 -53
- data/lib/action_mcp/tools_registry.rb +14 -1
- data/lib/action_mcp/transport/capabilities.rb +21 -0
- data/lib/action_mcp/transport/messaging.rb +20 -0
- data/lib/action_mcp/transport/prompts.rb +19 -0
- data/lib/action_mcp/transport/sse_client.rb +309 -0
- data/lib/action_mcp/transport/stdio_client.rb +117 -0
- data/lib/action_mcp/transport/tools.rb +20 -0
- data/lib/action_mcp/transport/transport_base.rb +125 -0
- data/lib/action_mcp/transport.rb +1 -235
- data/lib/action_mcp/transport_handler.rb +54 -0
- data/lib/action_mcp/version.rb +4 -5
- data/lib/action_mcp.rb +36 -33
- data/lib/generators/action_mcp/prompt/templates/prompt.rb.erb +3 -1
- data/lib/generators/action_mcp/tool/templates/tool.rb.erb +5 -1
- data/lib/tasks/action_mcp_tasks.rake +28 -5
- metadata +62 -9
- data/exe/action_mcp_stdio +0 -0
- data/lib/action_mcp/railtie.rb +0 -27
- data/lib/action_mcp/resources_bank.rb +0 -94
@@ -2,14 +2,25 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
module JsonRpc
|
5
|
+
# Represents a JSON-RPC response.
|
5
6
|
Response = Data.define(:id, :result, :error) do
|
7
|
+
# Initializes a new Response.
|
8
|
+
#
|
9
|
+
# @param id [String, Numeric] The request identifier.
|
10
|
+
# @param result [Object, nil] The result data (optional).
|
11
|
+
# @param error [Object, nil] The error data (optional).
|
12
|
+
# @raise [ArgumentError] if neither result nor error is provided, or if both are provided.
|
6
13
|
def initialize(id:, result: nil, error: nil)
|
7
14
|
validate_presence_of_result_or_error!(result, error)
|
8
15
|
validate_absence_of_both_result_and_error!(result, error)
|
16
|
+
result, error = transform_value_to_hash!(result, error)
|
9
17
|
|
10
|
-
super
|
18
|
+
super(id: id, result: result, error: error)
|
11
19
|
end
|
12
20
|
|
21
|
+
# Returns a hash representation of the response.
|
22
|
+
#
|
23
|
+
# @return [Hash] The hash representation.
|
13
24
|
def to_h
|
14
25
|
{
|
15
26
|
jsonrpc: "2.0",
|
@@ -19,15 +30,35 @@ module ActionMCP
|
|
19
30
|
}.compact
|
20
31
|
end
|
21
32
|
|
33
|
+
def is_error?
|
34
|
+
error.present?
|
35
|
+
end
|
36
|
+
|
22
37
|
private
|
23
38
|
|
39
|
+
# Validates that either result or error is present.
|
40
|
+
#
|
41
|
+
# @param result [Object, nil] The result data.
|
42
|
+
# @param error [Object, nil] The error data.
|
43
|
+
# @raise [ArgumentError] if neither result nor error is provided.
|
24
44
|
def validate_presence_of_result_or_error!(result, error)
|
25
45
|
raise ArgumentError, "Either result or error must be provided." if result.nil? && error.nil?
|
26
46
|
end
|
27
47
|
|
48
|
+
# Validates that both result and error are not present simultaneously.
|
49
|
+
#
|
50
|
+
# @param result [Object, nil] The result data.
|
51
|
+
# @param error [Object, nil] The error data.
|
52
|
+
# @raise [ArgumentError] if both result and error are provided.
|
28
53
|
def validate_absence_of_both_result_and_error!(result, error)
|
29
54
|
raise ArgumentError, "Both result and error cannot be provided simultaneously." if result && error
|
30
55
|
end
|
56
|
+
|
57
|
+
def transform_value_to_hash!(result, error)
|
58
|
+
result = result.is_a?(String) ? (MultiJson.load(result) rescue result) : result
|
59
|
+
error = error.is_a?(String) ? (MultiJson.load(error) rescue error) : error
|
60
|
+
[ result, error ]
|
61
|
+
end
|
31
62
|
end
|
32
63
|
end
|
33
64
|
end
|
data/lib/action_mcp/json_rpc.rb
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActionMCP
|
4
|
+
# Module for handling JSON-RPC communication.
|
4
5
|
module JsonRpc
|
5
|
-
extend ActiveSupport::Autoload
|
6
|
-
|
7
|
-
autoload :JsonRpcError
|
8
|
-
autoload :Notification
|
9
|
-
autoload :Request
|
10
|
-
autoload :Response
|
11
6
|
end
|
12
7
|
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
class JsonRpcHandler
|
5
|
+
attr_reader :transport
|
6
|
+
|
7
|
+
def initialize(transport)
|
8
|
+
@transport = transport
|
9
|
+
end
|
10
|
+
|
11
|
+
# Process a single line of input.
|
12
|
+
def call(line)
|
13
|
+
request = if line.is_a?(String)
|
14
|
+
line.strip!
|
15
|
+
return if line.empty?
|
16
|
+
begin
|
17
|
+
MultiJson.load(line)
|
18
|
+
rescue MultiJson::ParseError => e
|
19
|
+
Rails.logger.error("Failed to parse JSON: #{e.message}")
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
else
|
24
|
+
line
|
25
|
+
end
|
26
|
+
process_request(request)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def process_request(request)
|
32
|
+
unless request["jsonrpc"] == "2.0"
|
33
|
+
puts "Invalid request: #{request}"
|
34
|
+
return
|
35
|
+
end
|
36
|
+
method = request["method"]
|
37
|
+
id = request["id"]
|
38
|
+
params = request["params"]
|
39
|
+
|
40
|
+
case method
|
41
|
+
when "initialize"
|
42
|
+
puts "\e[31mSending capabilities\e[0m"
|
43
|
+
transport.send_capabilities(id, params)
|
44
|
+
when "ping"
|
45
|
+
transport.send_pong(id)
|
46
|
+
when /^notifications\//
|
47
|
+
puts "\e[31mProcessing notifications\e[0m"
|
48
|
+
process_notifications(method, id, params)
|
49
|
+
when /^prompts\//
|
50
|
+
process_prompts(method, id, params)
|
51
|
+
when /^resources\//
|
52
|
+
process_resources(method, id, params)
|
53
|
+
when /^tools\//
|
54
|
+
process_tools(method, id, params)
|
55
|
+
else
|
56
|
+
puts "\e[31mUnknown method: #{method}\e[0m"
|
57
|
+
Rails.logger.warn("Unknown method: #{method}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def process_notifications(method, _id, _params)
|
62
|
+
case method
|
63
|
+
when "notifications/initialized"
|
64
|
+
puts "\e[31mInitialized\e[0m"
|
65
|
+
transport.initialized!
|
66
|
+
else
|
67
|
+
Rails.logger.warn("Unknown notifications method: #{method}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def process_prompts(method, id, params)
|
72
|
+
case method
|
73
|
+
when "prompts/get"
|
74
|
+
transport.send_prompts_get(id, params&.dig("name"), params&.dig("arguments"))
|
75
|
+
when "prompts/list"
|
76
|
+
transport.send_prompts_list(id)
|
77
|
+
else
|
78
|
+
Rails.logger.warn("Unknown prompts method: #{method}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def process_resources(method, id, params)
|
83
|
+
case method
|
84
|
+
when "resources/list"
|
85
|
+
transport.send_resources_list(id)
|
86
|
+
when "resources/templates/list"
|
87
|
+
transport.send_resource_templates_list(id)
|
88
|
+
when "resources/read"
|
89
|
+
transport.send_resource_read(id, params)
|
90
|
+
else
|
91
|
+
Rails.logger.warn("Unknown resources method: #{method}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_tools(method, id, params)
|
96
|
+
case method
|
97
|
+
when "tools/list"
|
98
|
+
transport.send_tools_list(id)
|
99
|
+
when "tools/call"
|
100
|
+
transport.send_tools_call(id, params&.dig("name"), params&.dig("arguments"))
|
101
|
+
else
|
102
|
+
Rails.logger.warn("Unknown tools method: #{method}")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/tagged_logging"
|
4
|
+
require "active_support/logger"
|
5
|
+
require "logger"
|
6
|
+
|
7
|
+
module ActionMCP
|
8
|
+
# Module for providing logging functionality to ActionMCP transport.
|
9
|
+
module Logging
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
# Included hook to configure the logger.
|
13
|
+
included do
|
14
|
+
logger_instance = ActiveSupport::Logger.new(STDOUT)
|
15
|
+
logger_instance.level = Logger.const_get(ActionMCP.configuration.logging_level.to_s.upcase)
|
16
|
+
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(logger_instance)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/action_mcp/prompt.rb
CHANGED
@@ -2,66 +2,44 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
# Abstract base class for Prompts
|
5
|
-
|
6
|
-
class Prompt
|
7
|
-
include ActiveModel::Model
|
8
|
-
include ActiveModel::Attributes
|
9
|
-
include Renderable
|
10
|
-
|
11
|
-
class_attribute :_prompt_name, instance_accessor: false
|
12
|
-
class_attribute :_description, instance_accessor: false, default: ""
|
5
|
+
class Prompt < Capability
|
13
6
|
class_attribute :_argument_definitions, instance_accessor: false, default: []
|
14
|
-
class_attribute :abstract_prompt, instance_accessor: false, default: false
|
15
|
-
|
16
|
-
def self.inherited(subclass)
|
17
|
-
super
|
18
|
-
return if subclass == Prompt
|
19
|
-
return if subclass.name == "ApplicationPrompt"
|
20
|
-
|
21
|
-
subclass.abstract_prompt = false
|
22
|
-
|
23
|
-
# Automatically register the subclass with the PromptsRegistry
|
24
|
-
PromptsRegistry.register(subclass.prompt_name, subclass)
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.abstract!
|
28
|
-
self.abstract_prompt = true
|
29
|
-
# If already registered, you might want to unregister it here.
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.abstract?
|
33
|
-
abstract_prompt
|
34
|
-
end
|
35
7
|
|
36
8
|
# ---------------------------------------------------
|
37
9
|
# Prompt Name
|
38
10
|
# ---------------------------------------------------
|
11
|
+
# Gets or sets the prompt name.
|
12
|
+
#
|
13
|
+
# @param name [String, nil] The prompt name to set.
|
14
|
+
# @return [String] The prompt name.
|
39
15
|
def self.prompt_name(name = nil)
|
40
16
|
if name
|
41
|
-
self.
|
17
|
+
self._capability_name = name
|
42
18
|
else
|
43
|
-
|
19
|
+
_capability_name || default_prompt_name
|
44
20
|
end
|
45
21
|
end
|
46
22
|
|
23
|
+
# Returns the default prompt name based on the class name.
|
24
|
+
#
|
25
|
+
# @return [String] The default prompt name.
|
47
26
|
def self.default_prompt_name
|
48
|
-
name.demodulize.underscore.
|
27
|
+
name.demodulize.underscore.sub(/_prompt$/, "")
|
49
28
|
end
|
50
|
-
|
51
|
-
|
52
|
-
# Description
|
53
|
-
# ---------------------------------------------------
|
54
|
-
def self.description(text = nil)
|
55
|
-
if text
|
56
|
-
self._description = text
|
57
|
-
else
|
58
|
-
_description
|
59
|
-
end
|
29
|
+
class << self
|
30
|
+
alias default_capability_name default_prompt_name
|
60
31
|
end
|
61
32
|
|
62
33
|
# ---------------------------------------------------
|
63
34
|
# Argument DSL
|
64
35
|
# ---------------------------------------------------
|
36
|
+
# Defines an argument for the prompt.
|
37
|
+
#
|
38
|
+
# @param arg_name [Symbol] The name of the argument.
|
39
|
+
# @param description [String] The description of the argument.
|
40
|
+
# @param required [Boolean] Whether the argument is required.
|
41
|
+
# @param default [Object] The default value of the argument.
|
42
|
+
# @return [void]
|
65
43
|
def self.argument(arg_name, description: "", required: false, default: nil)
|
66
44
|
arg_def = {
|
67
45
|
name: arg_name.to_s,
|
@@ -78,6 +56,9 @@ module ActionMCP
|
|
78
56
|
validates arg_name, presence: true
|
79
57
|
end
|
80
58
|
|
59
|
+
# Returns the list of argument definitions.
|
60
|
+
#
|
61
|
+
# @return [Array<Hash>] The list of argument definitions.
|
81
62
|
def self.arguments
|
82
63
|
_argument_definitions
|
83
64
|
end
|
@@ -85,6 +66,7 @@ module ActionMCP
|
|
85
66
|
# ---------------------------------------------------
|
86
67
|
# Convert prompt definition to Hash
|
87
68
|
# ---------------------------------------------------
|
69
|
+
# @return [Hash] The prompt definition as a Hash.
|
88
70
|
def self.to_h
|
89
71
|
{
|
90
72
|
name: prompt_name,
|
@@ -106,6 +88,8 @@ module ActionMCP
|
|
106
88
|
# Raises:
|
107
89
|
# ActionMCP::JsonRpc::JsonRpcError(:invalid_params) if validation fails.
|
108
90
|
#
|
91
|
+
# @param params [Hash] The parameters for the prompt.
|
92
|
+
# @return [Object] The result of the prompt's call method.
|
109
93
|
def self.call(params)
|
110
94
|
prompt = new(params) # Initialize an instance with provided params
|
111
95
|
unless prompt.valid?
|
@@ -131,14 +115,14 @@ module ActionMCP
|
|
131
115
|
#
|
132
116
|
# Usage: Called internally after validation in self.call
|
133
117
|
#
|
118
|
+
# @raise [NotImplementedError] Subclasses must implement the call method.
|
119
|
+
# @return [Array<Content>] Array of Content objects is expected as return value
|
134
120
|
def call
|
135
121
|
raise NotImplementedError, "Subclasses must implement the call method"
|
136
122
|
# Default implementation (no-op)
|
137
123
|
# In a real subclass, you might do:
|
138
|
-
#
|
139
|
-
#
|
140
|
-
# # Return something meaningful.
|
141
|
-
# end
|
124
|
+
# # Perform logic, e.g. analyze code, etc.
|
125
|
+
# # Return something meaningful.
|
142
126
|
end
|
143
127
|
end
|
144
128
|
end
|
@@ -1,11 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActionMCP
|
4
|
+
# Registry for managing prompts.
|
4
5
|
class PromptsRegistry < RegistryBase
|
5
6
|
class << self
|
7
|
+
# @!method prompts
|
8
|
+
# Returns all registered prompts.
|
9
|
+
# @return [Hash] A hash of registered prompts.
|
6
10
|
alias prompts items
|
7
|
-
alias available_prompts enabled
|
8
11
|
|
12
|
+
# Calls a prompt with the given name and arguments.
|
13
|
+
#
|
14
|
+
# @param prompt_name [String] The name of the prompt to call.
|
15
|
+
# @param arguments [Hash] The arguments to pass to the prompt.
|
16
|
+
# @return [Hash] A hash containing the prompt's response.
|
9
17
|
def prompt_call(prompt_name, arguments)
|
10
18
|
prompt = find(prompt_name)
|
11
19
|
prompt = prompt.new(arguments)
|
@@ -24,6 +32,10 @@ module ActionMCP
|
|
24
32
|
}
|
25
33
|
end
|
26
34
|
end
|
35
|
+
|
36
|
+
def item_klass
|
37
|
+
Prompt
|
38
|
+
end
|
27
39
|
end
|
28
40
|
end
|
29
41
|
end
|
@@ -1,55 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActionMCP
|
4
|
+
# Base class for registries.
|
4
5
|
class RegistryBase
|
6
|
+
# Error raised when an item is not found in the registry.
|
5
7
|
class NotFound < StandardError; end
|
6
8
|
|
7
9
|
class << self
|
10
|
+
# Returns all registered items.
|
11
|
+
#
|
12
|
+
# @return [Hash] A hash of registered items.
|
8
13
|
def items
|
9
|
-
@items
|
14
|
+
@items = item_klass.descendants.each_with_object({}) do |klass, hash|
|
15
|
+
next if klass.abstract?
|
16
|
+
hash[klass.capability_name] = klass
|
17
|
+
end
|
10
18
|
end
|
11
19
|
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
items[name] = { klass: klass, enabled: true }
|
18
|
-
end
|
19
|
-
|
20
|
-
# Retrieve an item’s metadata by name.
|
20
|
+
# Retrieve an item by name.
|
21
|
+
#
|
22
|
+
# @param name [String] The name of the item to find.
|
23
|
+
# @raise [NotFound] if the item is not found.
|
24
|
+
# @return [Class] The class of the item.
|
21
25
|
def find(name)
|
22
26
|
item = items[name]
|
23
27
|
raise NotFound, "Item '#{name}' not found." if item.nil?
|
24
28
|
|
25
|
-
item
|
29
|
+
item
|
26
30
|
end
|
27
31
|
|
28
32
|
# Return the number of registered items, ignoring abstract ones.
|
33
|
+
#
|
34
|
+
# @return [Integer] The number of registered items.
|
29
35
|
def size
|
30
|
-
items.
|
31
|
-
end
|
32
|
-
|
33
|
-
def unregister(name)
|
34
|
-
items.delete(name)
|
36
|
+
items.size
|
35
37
|
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# Chainable scope: returns only enabled, non-abstract items.
|
42
|
-
def enabled
|
39
|
+
# Chainable scope: returns only non-abstract items.
|
40
|
+
#
|
41
|
+
# @return [RegistryScope] A RegistryScope instance.
|
42
|
+
def non_abstract
|
43
43
|
RegistryScope.new(items)
|
44
44
|
end
|
45
45
|
|
46
46
|
private
|
47
47
|
|
48
48
|
# Helper to determine if an item is abstract.
|
49
|
-
|
50
|
-
|
49
|
+
#
|
50
|
+
# @param klass [Class] The class to check.
|
51
|
+
# @return [Boolean] True if the class is abstract, false otherwise.
|
52
|
+
def abstract_item?(klass)
|
51
53
|
klass.respond_to?(:abstract?) && klass.abstract?
|
52
54
|
end
|
55
|
+
|
56
|
+
def item_klass
|
57
|
+
raise NotImplementedError, "Implement in subclass"
|
58
|
+
end
|
53
59
|
end
|
54
60
|
|
55
61
|
# Query object for chainable registry scopes.
|
@@ -59,22 +65,35 @@ module ActionMCP
|
|
59
65
|
# Using a Data type for items.
|
60
66
|
Item = Data.define(:name, :klass)
|
61
67
|
|
68
|
+
# Initializes a new RegistryScope instance.
|
69
|
+
#
|
70
|
+
# @param items [Hash] The items to scope.
|
71
|
+
# @return [void]
|
62
72
|
def initialize(items)
|
63
|
-
@items = items.reject do |_name,
|
64
|
-
RegistryBase.send(:abstract_item?,
|
65
|
-
end.map { |name,
|
73
|
+
@items = items.reject do |_name, klass|
|
74
|
+
RegistryBase.send(:abstract_item?, klass)
|
75
|
+
end.map { |name, klass| Item.new(name, klass) }
|
66
76
|
end
|
67
77
|
|
78
|
+
# Iterates over the items in the scope.
|
79
|
+
#
|
80
|
+
# @yield [Item] The item to yield.
|
81
|
+
# @return [void]
|
68
82
|
def each(&)
|
69
83
|
@items.each(&)
|
70
84
|
end
|
71
85
|
|
72
|
-
# Returns the names (keys) of all
|
86
|
+
# Returns the names (keys) of all non-abstract items.
|
87
|
+
#
|
88
|
+
# @return [Array<String>] The names of all non-abstract items.
|
73
89
|
def keys
|
74
90
|
@items.map(&:name)
|
75
91
|
end
|
76
92
|
|
77
93
|
# Chainable finder for available tools by name.
|
94
|
+
#
|
95
|
+
# @param name [String] The name of the tool to find.
|
96
|
+
# @return [Class, nil] The class of the tool, or nil if not found.
|
78
97
|
def find_available_tool(name)
|
79
98
|
item = @items.find { |i| i.name == name }
|
80
99
|
item&.klass
|
@@ -1,23 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActionMCP
|
4
|
+
# Module for rendering content.
|
4
5
|
module Renderable
|
6
|
+
# Renders text content.
|
7
|
+
#
|
8
|
+
# @param text [String] The text to render.
|
9
|
+
# @return [Content::Text] The rendered text content.
|
5
10
|
def render_text(text)
|
6
11
|
Content::Text.new(text)
|
7
12
|
end
|
8
13
|
|
14
|
+
# Renders audio content.
|
15
|
+
#
|
16
|
+
# @param data [String] The audio data.
|
17
|
+
# @param mime_type [String] The MIME type of the audio data.
|
18
|
+
# @return [Content::Audio] The rendered audio content.
|
9
19
|
def render_audio(data, mime_type)
|
10
20
|
Content::Audio.new(data, mime_type)
|
11
21
|
end
|
12
22
|
|
23
|
+
# Renders image content.
|
24
|
+
#
|
25
|
+
# @param data [String] The image data.
|
26
|
+
# @param mime_type [String] The MIME type of the image data.
|
27
|
+
# @return [Content::Image] The rendered image content.
|
13
28
|
def render_image(data, mime_type)
|
14
29
|
Content::Image.new(data, mime_type)
|
15
30
|
end
|
16
31
|
|
32
|
+
# Renders a resource.
|
33
|
+
#
|
34
|
+
# @param uri [String] The URI of the resource.
|
35
|
+
# @param mime_type [String] The MIME type of the resource.
|
36
|
+
# @param text [String, nil] The text associated with the resource.
|
37
|
+
# @param blob [String, nil] The blob associated with the resource.
|
38
|
+
# @return [Content::Resource] The rendered resource content.
|
17
39
|
def render_resource(uri, mime_type, text: nil, blob: nil)
|
18
40
|
Content::Resource.new(uri, mime_type, text: text, blob: blob)
|
19
41
|
end
|
20
42
|
|
43
|
+
# Renders an error.
|
44
|
+
#
|
45
|
+
# @param errors [Array<String>] The errors to render.
|
46
|
+
# @return [Hash] A hash containing the error information.
|
21
47
|
def render_error(errors)
|
22
48
|
{
|
23
49
|
isError: true,
|
data/lib/action_mcp/resource.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActionMCP
|
4
|
+
# Represents a resource with its metadata.
|
4
5
|
Resource = Data.define(:uri, :name, :description, :mime_type, :size) do
|
5
6
|
# Convert the resource to a hash with the keys expected by MCP.
|
6
7
|
# Note: The key for mime_type is converted to 'mimeType' as specified.
|
8
|
+
#
|
9
|
+
# @return [Hash] A hash representation of the resource.
|
7
10
|
def to_h
|
8
11
|
hash = { uri: uri, name: name }
|
9
12
|
hash[:description] = description if description
|
@@ -12,7 +15,6 @@ module ActionMCP
|
|
12
15
|
hash
|
13
16
|
end
|
14
17
|
|
15
|
-
# Convert the resource to a JSON string.
|
16
18
|
def to_json(*)
|
17
19
|
MultiJson.dump(to_h, *)
|
18
20
|
end
|
data/lib/action_mcp/server.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
|
3
2
|
# TODO: move all server related code here before version 1.0.0
|
4
3
|
module ActionMCP
|
4
|
+
# Module for server-related functionality.
|
5
5
|
module Server
|
6
|
+
module_function def server
|
7
|
+
@server ||= ActionCable::Server::Base.new
|
8
|
+
end
|
6
9
|
end
|
7
10
|
end
|
@@ -1,7 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActionMCP
|
4
|
+
# Custom type for handling arrays of strings in ActiveModel.
|
4
5
|
class StringArray < ActiveModel::Type::Value
|
6
|
+
# Casts the given value to an array of strings.
|
7
|
+
#
|
8
|
+
# @param value [Object] The value to cast.
|
9
|
+
# @return [Array<String>] The array of strings.
|
5
10
|
def cast(value)
|
6
11
|
Array(value).map(&:to_s) # Ensure all elements are strings
|
7
12
|
end
|