actionmcp 0.14.0 → 0.16.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 +152 -148
- data/Rakefile +1 -1
- data/app/controllers/action_mcp/{application_controller.rb → mcp_controller.rb} +3 -1
- data/app/controllers/action_mcp/messages_controller.rb +7 -5
- data/app/controllers/action_mcp/sse_controller.rb +19 -13
- data/app/models/action_mcp/session/message.rb +95 -90
- data/app/models/action_mcp/session/resource.rb +10 -6
- data/app/models/action_mcp/session/subscription.rb +9 -5
- data/app/models/action_mcp/session.rb +22 -13
- data/app/models/action_mcp.rb +2 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20250308122801_create_action_mcp_sessions.rb +12 -10
- data/db/migrate/20250314230152_add_is_ping_to_session_message.rb +2 -0
- data/db/migrate/20250316005021_create_action_mcp_session_subscriptions.rb +3 -1
- data/db/migrate/20250316005649_create_action_mcp_session_resources.rb +4 -2
- data/exe/actionmcp_cli +57 -55
- data/lib/action_mcp/base_json_rpc_handler.rb +97 -0
- data/lib/action_mcp/callbacks.rb +122 -0
- data/lib/action_mcp/capability.rb +6 -3
- data/lib/action_mcp/client.rb +20 -26
- data/lib/action_mcp/client_json_rpc_handler.rb +69 -0
- data/lib/action_mcp/configuration.rb +8 -8
- data/lib/action_mcp/gem_version.rb +2 -0
- data/lib/action_mcp/instrumentation/controller_runtime.rb +38 -0
- data/lib/action_mcp/instrumentation/instrumentation.rb +26 -0
- data/lib/action_mcp/instrumentation/log_subscriber.rb +39 -0
- data/lib/action_mcp/instrumentation/resource_instrumentation.rb +40 -0
- data/lib/action_mcp/json_rpc/response.rb +18 -2
- data/lib/action_mcp/json_rpc_handler.rb +93 -21
- data/lib/action_mcp/log_subscriber.rb +28 -0
- data/lib/action_mcp/logging.rb +1 -3
- data/lib/action_mcp/prompt.rb +15 -6
- data/lib/action_mcp/prompt_response.rb +1 -1
- data/lib/action_mcp/prompts_registry.rb +1 -0
- data/lib/action_mcp/registry_base.rb +1 -0
- data/lib/action_mcp/resource_callbacks.rb +156 -0
- data/lib/action_mcp/resource_template.rb +18 -19
- data/lib/action_mcp/resource_templates_registry.rb +19 -25
- data/lib/action_mcp/sampling_request.rb +113 -0
- data/lib/action_mcp/server.rb +4 -1
- data/lib/action_mcp/server_json_rpc_handler.rb +90 -0
- data/lib/action_mcp/test_helper.rb +6 -2
- data/lib/action_mcp/tool.rb +12 -3
- data/lib/action_mcp/tool_response.rb +3 -2
- data/lib/action_mcp/transport/capabilities.rb +5 -1
- data/lib/action_mcp/transport/messaging.rb +2 -0
- data/lib/action_mcp/transport/prompts.rb +2 -0
- data/lib/action_mcp/transport/resources.rb +23 -6
- data/lib/action_mcp/transport/roots.rb +11 -0
- data/lib/action_mcp/transport/sampling.rb +14 -0
- data/lib/action_mcp/transport/sse_client.rb +11 -15
- data/lib/action_mcp/transport/stdio_client.rb +12 -14
- data/lib/action_mcp/transport/tools.rb +2 -0
- data/lib/action_mcp/transport/transport_base.rb +16 -15
- data/lib/action_mcp/transport.rb +2 -0
- data/lib/action_mcp/transport_handler.rb +3 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +8 -2
- data/lib/generators/action_mcp/install/install_generator.rb +4 -1
- data/lib/generators/action_mcp/install/templates/application_mcp_res_template.rb +2 -0
- data/lib/generators/action_mcp/resource_template/resource_template_generator.rb +2 -0
- data/lib/generators/action_mcp/resource_template/templates/resource_template.rb.erb +1 -1
- data/lib/tasks/action_mcp_tasks.rake +11 -6
- metadata +27 -14
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
class SamplingRequest
|
5
|
+
class << self
|
6
|
+
attr_reader :default_messages, :default_system_prompt, :default_context,
|
7
|
+
:default_model_hints, :default_intelligence_priority,
|
8
|
+
:default_max_tokens, :default_temperature
|
9
|
+
|
10
|
+
def configure
|
11
|
+
yield self
|
12
|
+
end
|
13
|
+
|
14
|
+
def messages(messages = nil)
|
15
|
+
if messages
|
16
|
+
@default_messages = messages.map do |msg|
|
17
|
+
mutate_content(msg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
@default_messages ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
def system_prompt(prompt = nil)
|
24
|
+
@default_system_prompt = prompt if prompt
|
25
|
+
@default_system_prompt
|
26
|
+
end
|
27
|
+
|
28
|
+
def include_context(context = nil)
|
29
|
+
@default_context = context if context
|
30
|
+
@default_context
|
31
|
+
end
|
32
|
+
|
33
|
+
def model_hints(hints = nil)
|
34
|
+
@default_model_hints = hints if hints
|
35
|
+
@model_hints ||= []
|
36
|
+
end
|
37
|
+
|
38
|
+
def intelligence_priority(priority = nil)
|
39
|
+
@default_intelligence_priority = priority if priority
|
40
|
+
@intelligence_priority ||= 0.9
|
41
|
+
end
|
42
|
+
|
43
|
+
def max_tokens(tokens = nil)
|
44
|
+
@default_max_tokens = tokens if tokens
|
45
|
+
@max_tokens ||= 500
|
46
|
+
end
|
47
|
+
|
48
|
+
def temperature(temp = nil)
|
49
|
+
@default_temperature = temp if temp
|
50
|
+
@temperature ||= 0.7
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def mutate_content(msg)
|
56
|
+
content = msg[:content]
|
57
|
+
if content.is_a?(ActionMCP::Content) || (content.respond_to?(:to_h) && !content.is_a?(Hash))
|
58
|
+
{ role: msg[:role], content: content.to_h }
|
59
|
+
else
|
60
|
+
msg
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_accessor :system_prompt, :model_hints, :intelligence_priority, :max_tokens, :temperature
|
66
|
+
attr_reader :messages, :context
|
67
|
+
|
68
|
+
def initialize
|
69
|
+
@messages = self.class.default_messages.dup
|
70
|
+
@system_prompt = self.class.default_system_prompt
|
71
|
+
@context = self.class.default_context
|
72
|
+
@model_hints = self.class.default_model_hints.dup
|
73
|
+
@intelligence_priority = self.class.default_intelligence_priority
|
74
|
+
@max_tokens = self.class.default_max_tokens
|
75
|
+
@temperature = self.class.default_temperature
|
76
|
+
|
77
|
+
yield self if block_given?
|
78
|
+
end
|
79
|
+
|
80
|
+
def messages=(value)
|
81
|
+
@messages = value.map do |msg|
|
82
|
+
self.class.send(:mutate_content, msg)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def include_context=(value)
|
87
|
+
@context = value
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_message(content, role: "user")
|
91
|
+
if content.is_a?(Content::Base) || (content.respond_to?(:to_h) && !content.is_a?(Hash))
|
92
|
+
@messages << { role: role, content: content.to_h }
|
93
|
+
else
|
94
|
+
content = Content::Text.new(content).to_h if content.is_a?(String)
|
95
|
+
@messages << { role: role, content: content }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_h
|
100
|
+
{
|
101
|
+
messages: messages.map { |msg| { role: msg[:role], content: msg[:content] } },
|
102
|
+
systemPrompt: system_prompt,
|
103
|
+
includeContext: context,
|
104
|
+
modelPreferences: {
|
105
|
+
hints: model_hints.map { |name| { name: name } },
|
106
|
+
intelligencePriority: intelligence_priority
|
107
|
+
},
|
108
|
+
maxTokens: max_tokens,
|
109
|
+
temperature: temperature
|
110
|
+
}.compact
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/action_mcp/server.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
3
|
# TODO: move all server related code here before version 1.0.0
|
3
4
|
module ActionMCP
|
4
5
|
# Module for server-related functionality.
|
5
6
|
module Server
|
6
|
-
module_function
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def server
|
7
10
|
@server ||= ActionCable::Server::Base.new
|
8
11
|
end
|
9
12
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Handler for server-side requests (client -> server)
|
5
|
+
class ServerJsonRpcHandler < BaseJsonRpcHandler
|
6
|
+
def handle_initialize(id, params)
|
7
|
+
# Server-specific initialization
|
8
|
+
transport.send_capabilities(id, params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_specific_method(rpc_method, id, params)
|
12
|
+
case rpc_method
|
13
|
+
when %r{^prompts/} # [SERVER] Prompt-related requests
|
14
|
+
process_prompts(rpc_method, id, params)
|
15
|
+
when %r{^resources/} # [SERVER] Resource-related requests
|
16
|
+
process_resources(rpc_method, id, params)
|
17
|
+
when %r{^tools/} # [SERVER] Tool-related requests
|
18
|
+
process_tools(rpc_method, id, params)
|
19
|
+
when "completion/complete" # [SERVER] Completion requests
|
20
|
+
process_completion_complete(id, params)
|
21
|
+
else
|
22
|
+
Rails.logger.warn("Unknown server method: #{rpc_method}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
def handle_specific_notification(rpc_method, _params)
|
26
|
+
# Server-specific notifications would go here
|
27
|
+
case rpc_method
|
28
|
+
when "notifications/initialized" # [SERVER] Initialization complete
|
29
|
+
puts "Initialized"
|
30
|
+
transport.initialize!
|
31
|
+
else
|
32
|
+
Rails.logger.warn("Unknown server notification: #{rpc_method}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# All the server-specific methods below...
|
39
|
+
|
40
|
+
def process_completion_complete(id, params)
|
41
|
+
# Implementation as in original code
|
42
|
+
transport.send_jsonrpc_response(id, result: { completion: { values: [], total: 0, hasMore: false } })
|
43
|
+
case params["ref"]["type"]
|
44
|
+
when "ref/prompt"
|
45
|
+
# TODO: Implement completion
|
46
|
+
when "ref/resource"
|
47
|
+
# TODO: Implement completion
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_prompts(rpc_method, id, params)
|
52
|
+
case rpc_method
|
53
|
+
when "prompts/get" # [SERVER] Get specific prompt
|
54
|
+
transport.send_prompts_get(id, params["name"], params["arguments"])
|
55
|
+
when "prompts/list" # [SERVER] List available prompts
|
56
|
+
transport.send_prompts_list(id)
|
57
|
+
else
|
58
|
+
Rails.logger.warn("Unknown prompts method: #{rpc_method}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def process_resources(rpc_method, id, params)
|
63
|
+
case rpc_method
|
64
|
+
when "resources/list" # [SERVER] List available resources
|
65
|
+
transport.send_resources_list(id)
|
66
|
+
when "resources/templates/list" # [SERVER] List resource templates
|
67
|
+
transport.send_resource_templates_list(id)
|
68
|
+
when "resources/read" # [SERVER] Read resource content
|
69
|
+
transport.send_resource_read(id, params)
|
70
|
+
when "resources/subscribe" # [SERVER] Subscribe to resource updates
|
71
|
+
transport.send_resource_subscribe(id, params["uri"])
|
72
|
+
when "resources/unsubscribe" # [SERVER] Unsubscribe from resource updates
|
73
|
+
transport.send_resource_unsubscribe(id, params["uri"])
|
74
|
+
else
|
75
|
+
Rails.logger.warn("Unknown resources method: #{rpc_method}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def process_tools(rpc_method, id, params)
|
80
|
+
case rpc_method
|
81
|
+
when "tools/list" # [SERVER] List available tools
|
82
|
+
transport.send_tools_list(id)
|
83
|
+
when "tools/call" # [SERVER] Call a tool
|
84
|
+
transport.send_tools_call(id, params&.dig("name"), params&.dig("arguments"))
|
85
|
+
else
|
86
|
+
Rails.logger.warn("Unknown tools method: #{rpc_method}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_support/testing/assertions"
|
2
4
|
|
3
5
|
module ActionMCP
|
@@ -38,14 +40,16 @@ module ActionMCP
|
|
38
40
|
# @param [Hash] expected_output
|
39
41
|
# @param [ActionMCP::ToolResponse] result
|
40
42
|
def assert_tool_output(expected_output, result)
|
41
|
-
|
43
|
+
assert_equal expected_output, result.to_h[:content],
|
44
|
+
"Tool output did not match expected output #{expected_output} != #{result.to_h[:content]}"
|
42
45
|
end
|
43
46
|
|
44
47
|
# Asserts that the output of a prompt is equal to the expected output.
|
45
48
|
# @param [Hash] expected_output
|
46
49
|
# @param [ActionMCP::PromptResponse] result
|
47
50
|
def assert_prompt_output(expected_output, result)
|
48
|
-
assert_equal expected_output, result.to_h[:messages],
|
51
|
+
assert_equal expected_output, result.to_h[:messages],
|
52
|
+
"Prompt output did not match expected output #{expected_output} != #{result.to_h[:messages]}"
|
49
53
|
end
|
50
54
|
end
|
51
55
|
end
|
data/lib/action_mcp/tool.rb
CHANGED
@@ -6,6 +6,7 @@ module ActionMCP
|
|
6
6
|
# Provides a DSL for specifying metadata, properties, and nested collection schemas.
|
7
7
|
# Tools are registered automatically in the ToolsRegistry unless marked as abstract.
|
8
8
|
class Tool < Capability
|
9
|
+
include ActionMCP::Callbacks
|
9
10
|
# --------------------------------------------------------------------------
|
10
11
|
# Class Attributes for Tool Metadata and Schema
|
11
12
|
# --------------------------------------------------------------------------
|
@@ -40,6 +41,10 @@ module ActionMCP
|
|
40
41
|
|
41
42
|
class << self
|
42
43
|
alias default_capability_name default_tool_name
|
44
|
+
|
45
|
+
def type
|
46
|
+
:tool
|
47
|
+
end
|
43
48
|
end
|
44
49
|
|
45
50
|
# --------------------------------------------------------------------------
|
@@ -131,8 +136,10 @@ module ActionMCP
|
|
131
136
|
# Check validations before proceeding
|
132
137
|
if valid?
|
133
138
|
begin
|
134
|
-
perform
|
135
|
-
|
139
|
+
run_callbacks :perform do
|
140
|
+
perform # Invoke the subclass-specific logic if valid
|
141
|
+
end
|
142
|
+
rescue StandardError => e
|
136
143
|
# Handle exceptions during execution
|
137
144
|
@response.mark_as_error!(:internal_error, message: e.message)
|
138
145
|
end
|
@@ -155,7 +162,9 @@ module ActionMCP
|
|
155
162
|
|
156
163
|
errors_info = errors.any? ? ", errors: #{errors.full_messages}" : ""
|
157
164
|
|
158
|
-
"#<#{self.class.name} #{attributes_hash.map
|
165
|
+
"#<#{self.class.name} #{attributes_hash.map do |k, v|
|
166
|
+
"#{k}: #{v.inspect}"
|
167
|
+
end.join(', ')}, #{response_info}#{errors_info}>"
|
159
168
|
end
|
160
169
|
|
161
170
|
# Override render to collect Content objects
|
@@ -5,6 +5,7 @@ module ActionMCP
|
|
5
5
|
class ToolResponse
|
6
6
|
include Enumerable
|
7
7
|
attr_reader :contents, :is_error
|
8
|
+
|
8
9
|
delegate :empty?, :size, :each, :find, :map, to: :contents
|
9
10
|
|
10
11
|
def initialize
|
@@ -33,13 +34,13 @@ module ActionMCP
|
|
33
34
|
JsonRpc::JsonRpcError.new(@symbol, message: @error_message, data: @error_data).to_h
|
34
35
|
else
|
35
36
|
{
|
36
|
-
content: @contents.map
|
37
|
+
content: @contents.map(&:to_h)
|
37
38
|
}
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
42
|
# Alias as_json to to_h for consistency
|
42
|
-
|
43
|
+
alias as_json to_h
|
43
44
|
|
44
45
|
# Handle to_json directly
|
45
46
|
def to_json(options = nil)
|
@@ -1,7 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionMCP
|
2
4
|
module Transport
|
3
5
|
module Capabilities
|
4
6
|
def send_capabilities(request_id, params = {})
|
7
|
+
# TODO fix this if client send incorrect params
|
8
|
+
# TODO refuse connection if protocol version is not supported
|
5
9
|
@protocol_version = params["protocolVersion"]
|
6
10
|
@client_info = params["clientInfo"]
|
7
11
|
@client_capabilities = params["capabilities"]
|
@@ -9,7 +13,7 @@ module ActionMCP
|
|
9
13
|
session.store_client_capabilities(@client_capabilities)
|
10
14
|
session.set_protocol_version(@protocol_version)
|
11
15
|
session.save
|
12
|
-
# TODO , if the server don't support the protocol version, send a response with error
|
16
|
+
# TODO: , if the server don't support the protocol version, send a response with error
|
13
17
|
send_jsonrpc_response(request_id, result: session.server_capabilities_payload)
|
14
18
|
end
|
15
19
|
end
|
@@ -26,10 +26,8 @@ module ActionMCP
|
|
26
26
|
# @example Output:
|
27
27
|
# # Sends: {"jsonrpc":"2.0","id":"req-456","result":{"resourceTemplates":[{"uriTemplate":"db://{table}","name":"Database Table"}]}}
|
28
28
|
def send_resource_templates_list(request_id)
|
29
|
-
templates = ActionMCP::ResourceTemplatesRegistry.resource_templates.values.map
|
30
|
-
|
31
|
-
end
|
32
|
-
# TODO add pagination support
|
29
|
+
templates = ActionMCP::ResourceTemplatesRegistry.resource_templates.values.map(&:to_h)
|
30
|
+
# TODO: add pagination support
|
33
31
|
# TODO add autocomplete
|
34
32
|
log_resource_templates
|
35
33
|
send_jsonrpc_response(request_id, result: { resourceTemplates: templates })
|
@@ -47,9 +45,9 @@ module ActionMCP
|
|
47
45
|
# @example Output:
|
48
46
|
# # Sends: {"jsonrpc":"2.0","id":"req-789","result":{"contents":[{"uri":"file:///example.txt","text":"Example content"}]}}
|
49
47
|
def send_resource_read(id, params)
|
50
|
-
if
|
48
|
+
if (template = ResourceTemplatesRegistry.find_template_for_uri(params[:uri]))
|
51
49
|
record = template.process(params[:uri])
|
52
|
-
if (resource = record.
|
50
|
+
if (resource = record.resolve)
|
53
51
|
# if resource is a array or a collection, return each item then it ok
|
54
52
|
# else wrap it in a array
|
55
53
|
resource = [ resource ] unless resource.respond_to?(:each)
|
@@ -63,6 +61,25 @@ module ActionMCP
|
|
63
61
|
end
|
64
62
|
end
|
65
63
|
|
64
|
+
def send_resource_subscribe(id, uri)
|
65
|
+
session.resource_subscribe(uri)
|
66
|
+
send_jsonrpc_response(id, result: {})
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_resource_unsubscribe(id, uri)
|
70
|
+
session.resource_unsubscribe(uri)
|
71
|
+
send_jsonrpc_response(id, result: {})
|
72
|
+
end
|
73
|
+
|
74
|
+
# Client logging
|
75
|
+
def set_client_logging_level(id, level)
|
76
|
+
# Store the client's preferred log level
|
77
|
+
@client_log_level = level
|
78
|
+
send_jsonrpc_response(id, result: {})
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
66
83
|
# Log all registered resource templates
|
67
84
|
#
|
68
85
|
# @example Input:
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Transport
|
5
|
+
module Sampling
|
6
|
+
# @param [String] id
|
7
|
+
# @param [SamplingRequest] request
|
8
|
+
def send_sampling_create_message(id, request)
|
9
|
+
params = request.is_a?(SamplingRequest) ? request.to_h : request
|
10
|
+
send_jsonrpc_request(id, "sampling/createMessage", params)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "faraday"
|
2
4
|
require "uri"
|
3
5
|
|
@@ -64,7 +66,7 @@ module ActionMCP
|
|
64
66
|
|
65
67
|
if error
|
66
68
|
log_error(error)
|
67
|
-
raise ConnectionError
|
69
|
+
raise ConnectionError, error
|
68
70
|
end
|
69
71
|
|
70
72
|
# If we have the endpoint, consider the connection successful
|
@@ -86,7 +88,7 @@ module ActionMCP
|
|
86
88
|
end
|
87
89
|
|
88
90
|
validate_post_endpoint
|
89
|
-
log_debug("\e[34m
|
91
|
+
log_debug("\e[34m--> #{json_rpc}\e[0m")
|
90
92
|
send_http_request(json_rpc)
|
91
93
|
end
|
92
94
|
|
@@ -131,8 +133,6 @@ module ActionMCP
|
|
131
133
|
@endpoint_mutex.synchronize { @endpoint_received }
|
132
134
|
end
|
133
135
|
|
134
|
-
private
|
135
|
-
|
136
136
|
# The listen_sse method should NOT mark connection as successful at the end
|
137
137
|
def listen_sse
|
138
138
|
log_info("Starting SSE listener...")
|
@@ -142,7 +142,7 @@ module ActionMCP
|
|
142
142
|
req.headers["Accept"] = "text/event-stream"
|
143
143
|
req.headers["Cache-Control"] = "no-cache"
|
144
144
|
|
145
|
-
req.options.on_data =
|
145
|
+
req.options.on_data = proc do |chunk, bytes|
|
146
146
|
handle_sse_data(chunk, bytes)
|
147
147
|
end
|
148
148
|
end
|
@@ -151,7 +151,7 @@ module ActionMCP
|
|
151
151
|
# as the SSE connection stays open
|
152
152
|
rescue Faraday::ConnectionFailed => e
|
153
153
|
handle_connection_error(format_connection_error(e))
|
154
|
-
rescue => e
|
154
|
+
rescue StandardError => e
|
155
155
|
handle_connection_error("Unexpected error: #{e.message}")
|
156
156
|
end
|
157
157
|
end
|
@@ -203,7 +203,6 @@ module ActionMCP
|
|
203
203
|
process_buffer while @buffer.include?("\n")
|
204
204
|
end
|
205
205
|
|
206
|
-
|
207
206
|
def process_buffer
|
208
207
|
line, _sep, rest = @buffer.partition("\n")
|
209
208
|
@buffer = rest
|
@@ -241,9 +240,7 @@ module ActionMCP
|
|
241
240
|
end
|
242
241
|
|
243
242
|
# If no "data:" prefix was found, treat the entire event as data
|
244
|
-
unless has_data_prefix
|
245
|
-
event_data[:data] = lines.join("\n")
|
246
|
-
end
|
243
|
+
event_data[:data] = lines.join("\n") unless has_data_prefix
|
247
244
|
event_data
|
248
245
|
end
|
249
246
|
|
@@ -284,15 +281,14 @@ module ActionMCP
|
|
284
281
|
def send_http_request(json_rpc)
|
285
282
|
response = @conn.post(@post_url,
|
286
283
|
json_rpc,
|
287
|
-
{ "Content-Type" => "application/json" }
|
288
|
-
)
|
284
|
+
{ "Content-Type" => "application/json" })
|
289
285
|
handle_http_response(response)
|
290
286
|
end
|
291
287
|
|
292
288
|
def handle_http_response(response)
|
293
|
-
|
294
|
-
|
295
|
-
|
289
|
+
return if response.success?
|
290
|
+
|
291
|
+
log_error("HTTP POST failed: #{response.status} - #{response.body}")
|
296
292
|
end
|
297
293
|
|
298
294
|
def cleanup_sse_thread
|
@@ -45,17 +45,17 @@ module ActionMCP
|
|
45
45
|
|
46
46
|
# Mark the client as ready and send initial capabilities if not already sent
|
47
47
|
def mark_ready_and_send_capabilities
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
48
|
+
return if @received_server_message
|
49
|
+
|
50
|
+
@received_server_message = true
|
51
|
+
log_info("Received first server message")
|
52
|
+
|
53
|
+
# Send initial capabilities if not already sent
|
54
|
+
return if @capabilities_sent
|
55
|
+
|
56
|
+
log_info("Server is ready, sending initial capabilities...")
|
57
|
+
send_initial_capabilities
|
58
|
+
@capabilities_sent = true
|
59
59
|
end
|
60
60
|
|
61
61
|
private
|
@@ -78,9 +78,7 @@ module ActionMCP
|
|
78
78
|
log_info(line)
|
79
79
|
|
80
80
|
# Check stderr for server messages
|
81
|
-
if line.include?("MCP Server") || line.include?("running on stdio")
|
82
|
-
mark_ready_and_send_capabilities
|
83
|
-
end
|
81
|
+
mark_ready_and_send_capabilities if line.include?("MCP Server") || line.include?("running on stdio")
|
84
82
|
end
|
85
83
|
end
|
86
84
|
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionMCP
|
2
4
|
module Transport
|
3
5
|
class TransportBase
|
4
6
|
attr_reader :logger, :client_capabilities, :server_capabilities
|
5
7
|
|
6
|
-
def initialize(logger: Logger.new(
|
8
|
+
def initialize(logger: Logger.new($stdout))
|
7
9
|
@logger = logger
|
8
10
|
@on_message = nil
|
9
11
|
@on_error = nil
|
@@ -43,14 +45,13 @@ module ActionMCP
|
|
43
45
|
end
|
44
46
|
|
45
47
|
def handle_initialize_response(response)
|
46
|
-
|
48
|
+
return if @server_capabilities
|
47
49
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
50
|
+
if response.result
|
51
|
+
@server_capabilities = response.result["capabilities"]
|
52
|
+
send_initialized_notification
|
53
|
+
else
|
54
|
+
log_error("Server initialization failed: #{response.error}")
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
@@ -65,24 +66,24 @@ module ActionMCP
|
|
65
66
|
response = nil
|
66
67
|
|
67
68
|
if msg_hash.key?("jsonrpc")
|
68
|
-
if msg_hash.key?("id")
|
69
|
-
|
69
|
+
response = if msg_hash.key?("id")
|
70
|
+
JsonRpc::Response.new(**msg_hash.slice("id", "result", "error").symbolize_keys)
|
70
71
|
else
|
71
|
-
|
72
|
+
JsonRpc::Notification.new(**msg_hash.slice("method", "params").symbolize_keys)
|
72
73
|
end
|
73
74
|
end
|
74
75
|
# Check if this is a response to our initialize request
|
75
76
|
if response && @initialize_request_id && response.id == @initialize_request_id
|
76
77
|
handle_initialize_response(response)
|
77
|
-
|
78
|
-
@on_message&.call(response)
|
78
|
+
elsif response
|
79
|
+
@on_message&.call(response)
|
79
80
|
end
|
80
81
|
rescue MultiJson::ParseError => e
|
81
82
|
log_error("JSON parse error: #{e} (raw: #{raw})")
|
82
|
-
@on_error&.call(e)
|
83
|
+
@on_error&.call(e)
|
83
84
|
rescue StandardError => e
|
84
85
|
log_error("Error handling message: #{e} (raw: #{raw})")
|
85
|
-
@on_error&.call(e)
|
86
|
+
@on_error&.call(e)
|
86
87
|
end
|
87
88
|
end
|
88
89
|
|
data/lib/action_mcp/transport.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
module ActionMCP
|
4
4
|
class TransportHandler
|
5
5
|
attr_reader :session
|
6
|
+
|
6
7
|
delegate :initialize!, :initialized?, to: :session
|
7
8
|
delegate :read, :write, to: :session
|
8
9
|
include Logging
|
@@ -13,6 +14,8 @@ module ActionMCP
|
|
13
14
|
include Transport::Prompts
|
14
15
|
include Transport::Resources
|
15
16
|
include Transport::Notifications
|
17
|
+
include Transport::Sampling
|
18
|
+
include Transport::Roots
|
16
19
|
|
17
20
|
# @param [ActionMCP::Session] session
|
18
21
|
def initialize(session)
|