actionmcp 0.50.5 → 0.50.7
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/app/controllers/action_mcp/application_controller.rb +7 -4
- data/app/models/action_mcp/session.rb +3 -1
- data/lib/action_mcp/client/collection.rb +4 -5
- data/lib/action_mcp/content/image.rb +1 -2
- data/lib/action_mcp/json_rpc_handler_base.rb +44 -66
- data/lib/action_mcp/server/capabilities.rb +3 -3
- data/lib/action_mcp/server/error_aware.rb +31 -0
- data/lib/action_mcp/server/error_handling.rb +40 -0
- data/lib/action_mcp/server/handlers/prompt_handler.rb +55 -0
- data/lib/action_mcp/server/handlers/resource_handler.rb +68 -0
- data/lib/action_mcp/server/handlers/tool_handler.rb +51 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +46 -117
- data/lib/action_mcp/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17696f3f1e92327c6de4d8fa4ae5b6585bf8d9e173acb47beaf713dd9d1f8b07
|
4
|
+
data.tar.gz: 536f24be37c5c2fbc4f440fbf1c977eadee964cd6717be5981d63cbd35081cb7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf5c45e416e66526ee4550d611e0bbea71a3150b2f8f02af58899450308399c27e3f710348feed1d8478b27acc6701b1d01e9d6e6ab4e6f763f43ede2051d526
|
7
|
+
data.tar.gz: 8a27fae5101d5deb20e5f3b3b7da0a2d3d5bdfdef04681737de63f985a34a7f8cb4e3f8a9f08ae2cf886662f7855c453b19325eb5ae3ce5ec9bca141cc5bcde7
|
@@ -137,9 +137,7 @@ module ActionMCP
|
|
137
137
|
# Handles POST requests containing client JSON-RPC messages according to 2025-03-26 spec.
|
138
138
|
# @route POST /mcp
|
139
139
|
def create
|
140
|
-
unless post_accept_headers_valid?
|
141
|
-
return render_not_acceptable(post_accept_headers_error_message)
|
142
|
-
end
|
140
|
+
return render_not_acceptable(post_accept_headers_error_message) unless post_accept_headers_valid?
|
143
141
|
|
144
142
|
is_initialize_request = check_if_initialize_request(jsonrpc_params)
|
145
143
|
session_initially_missing = extract_session_id.nil?
|
@@ -209,7 +207,9 @@ module ActionMCP
|
|
209
207
|
session = Session.find_by(id: session_id)
|
210
208
|
if session
|
211
209
|
if ActionMCP.configuration.vibed_ignore_version
|
212
|
-
|
210
|
+
if session.protocol_version != self.class::REQUIRED_PROTOCOL_VERSION
|
211
|
+
session.update!(protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
|
212
|
+
end
|
213
213
|
elsif session.protocol_version != self.class::REQUIRED_PROTOCOL_VERSION
|
214
214
|
session.update!(protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
|
215
215
|
end
|
@@ -252,6 +252,7 @@ module ActionMCP
|
|
252
252
|
# Checks if the parsed body represents an 'initialize' request.
|
253
253
|
def check_if_initialize_request(payload)
|
254
254
|
return false unless payload.is_a?(JSON_RPC::Request) && !jsonrpc_params_batch?
|
255
|
+
|
255
256
|
payload.method == "initialize"
|
256
257
|
end
|
257
258
|
|
@@ -332,6 +333,7 @@ module ActionMCP
|
|
332
333
|
sse_event = "id: #{event_id}\ndata: #{data}\n\n"
|
333
334
|
sse.write(sse_event)
|
334
335
|
return unless ActionMCP.configuration.enable_sse_resumability
|
336
|
+
|
335
337
|
begin
|
336
338
|
session.store_sse_event(event_id, payload, session.max_stored_sse_events)
|
337
339
|
rescue StandardError => e
|
@@ -342,6 +344,7 @@ module ActionMCP
|
|
342
344
|
# Helper to clean up old SSE events for a session
|
343
345
|
def cleanup_old_sse_events(session)
|
344
346
|
return unless ActionMCP.configuration.enable_sse_resumability
|
347
|
+
|
345
348
|
begin
|
346
349
|
retention_period = session.sse_event_retention_period
|
347
350
|
count = session.cleanup_old_sse_events(retention_period)
|
@@ -64,7 +64,9 @@ module ActionMCP
|
|
64
64
|
before_create :set_server_info, if: -> { role == "server" }
|
65
65
|
before_create :set_server_capabilities, if: -> { role == "server" }
|
66
66
|
|
67
|
-
validates :protocol_version, inclusion: { in: SUPPORTED_VERSIONS }, allow_nil: true, unless:
|
67
|
+
validates :protocol_version, inclusion: { in: SUPPORTED_VERSIONS }, allow_nil: true, unless: lambda {
|
68
|
+
ActionMCP.configuration.vibed_ignore_version
|
69
|
+
}
|
68
70
|
|
69
71
|
def close!
|
70
72
|
dummy_callback = ->(*) { } # this callback seem broken
|
@@ -58,6 +58,7 @@ module ActionMCP
|
|
58
58
|
# @return [Array<Object>] The next page of items, or empty array if no more pages
|
59
59
|
def next_page(limit: nil)
|
60
60
|
return [] unless has_more_pages?
|
61
|
+
|
61
62
|
page(cursor: @next_cursor, limit: limit)
|
62
63
|
end
|
63
64
|
|
@@ -66,7 +67,7 @@ module ActionMCP
|
|
66
67
|
# @param limit [Integer, nil] Optional limit for page size
|
67
68
|
# @yield [page] Block to process each page
|
68
69
|
# @yieldparam page [Array<Object>] A page of items
|
69
|
-
def each_page(limit: nil
|
70
|
+
def each_page(limit: nil)
|
70
71
|
return unless block_given?
|
71
72
|
|
72
73
|
current_page = page(limit: limit)
|
@@ -125,12 +126,10 @@ module ActionMCP
|
|
125
126
|
params[:cursor] = cursor if cursor
|
126
127
|
params[:limit] = limit if limit
|
127
128
|
|
128
|
-
|
129
|
+
client.send(@load_method, params)
|
129
130
|
|
130
131
|
start_time = Time.now
|
131
|
-
while !@loaded && (Time.now - start_time) < timeout
|
132
|
-
sleep(0.1)
|
133
|
-
end
|
132
|
+
sleep(0.1) while !@loaded && (Time.now - start_time) < timeout
|
134
133
|
|
135
134
|
# Update @loaded status even if we timed out
|
136
135
|
@loaded = true
|
@@ -2,6 +2,34 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
class JsonRpcHandlerBase
|
5
|
+
module Methods
|
6
|
+
# Common methods
|
7
|
+
PING = "ping"
|
8
|
+
|
9
|
+
# Server methods
|
10
|
+
INITIALIZE = "initialize"
|
11
|
+
COMPLETION_COMPLETE = "completion/complete"
|
12
|
+
|
13
|
+
# Resource methods
|
14
|
+
RESOURCES_LIST = "resources/list"
|
15
|
+
RESOURCES_TEMPLATES_LIST = "resources/templates/list"
|
16
|
+
RESOURCES_READ = "resources/read"
|
17
|
+
RESOURCES_SUBSCRIBE = "resources/subscribe"
|
18
|
+
RESOURCES_UNSUBSCRIBE = "resources/unsubscribe"
|
19
|
+
|
20
|
+
# Prompt methods
|
21
|
+
PROMPTS_GET = "prompts/get"
|
22
|
+
PROMPTS_LIST = "prompts/list"
|
23
|
+
|
24
|
+
# Tool methods
|
25
|
+
TOOLS_LIST = "tools/list"
|
26
|
+
TOOLS_CALL = "tools/call"
|
27
|
+
|
28
|
+
# Notification methods
|
29
|
+
NOTIFICATIONS_INITIALIZED = "notifications/initialized"
|
30
|
+
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
|
31
|
+
end
|
32
|
+
|
5
33
|
delegate :initialize!, :initialized?, to: :transport
|
6
34
|
delegate :write, :read, to: :transport
|
7
35
|
attr_reader :transport
|
@@ -11,38 +39,14 @@ module ActionMCP
|
|
11
39
|
@transport = transport
|
12
40
|
end
|
13
41
|
|
14
|
-
# Process a
|
15
|
-
# @param
|
16
|
-
def call(
|
17
|
-
|
18
|
-
line.strip!
|
19
|
-
return if line.empty?
|
20
|
-
|
21
|
-
begin
|
22
|
-
MultiJson.load(line)
|
23
|
-
rescue MultiJson::ParseError => e
|
24
|
-
Rails.logger.error("Failed to parse JSON: #{e.message}")
|
25
|
-
return
|
26
|
-
end
|
27
|
-
else
|
28
|
-
line
|
29
|
-
end
|
30
|
-
process_request(request)
|
42
|
+
# Process a request object.
|
43
|
+
# @param request [JSON_RPC::Request, JSON_RPC::Notification, JSON_RPC::Response]
|
44
|
+
def call(request)
|
45
|
+
raise NotImplementedError, "Subclasses must implement call"
|
31
46
|
end
|
32
47
|
|
33
48
|
protected
|
34
49
|
|
35
|
-
# Validate if the request follows JSON-RPC 2.0 specification
|
36
|
-
# @param request [Hash]
|
37
|
-
# @return [Boolean]
|
38
|
-
def valid_request?(request)
|
39
|
-
if request["jsonrpc"] != "2.0"
|
40
|
-
puts "Invalid request: #{request}"
|
41
|
-
return false
|
42
|
-
end
|
43
|
-
true
|
44
|
-
end
|
45
|
-
|
46
50
|
# Handle common methods for both client and server
|
47
51
|
# @param rpc_method [String]
|
48
52
|
# @param id [String, Integer]
|
@@ -50,11 +54,10 @@ module ActionMCP
|
|
50
54
|
# @return [Boolean] true if handled, false otherwise
|
51
55
|
def handle_common_methods(rpc_method, id, params)
|
52
56
|
case rpc_method
|
53
|
-
when
|
57
|
+
when Methods::PING
|
54
58
|
transport.send_pong(id)
|
55
59
|
true
|
56
60
|
when %r{^notifications/}
|
57
|
-
puts "\e[31mProcessing notifications\e[0m"
|
58
61
|
process_notifications(rpc_method, params)
|
59
62
|
true
|
60
63
|
else
|
@@ -62,47 +65,22 @@ module ActionMCP
|
|
62
65
|
end
|
63
66
|
end
|
64
67
|
|
65
|
-
#
|
66
|
-
# @param rpc_method [String]
|
67
|
-
# @param id [String, Integer]
|
68
|
-
# @param params [Hash]
|
69
|
-
def handle_method(rpc_method, id, params)
|
70
|
-
raise NotImplementedError, "Subclasses must implement handle_method"
|
71
|
-
end
|
72
|
-
|
73
|
-
private
|
74
|
-
|
75
|
-
# @param request [Hash]
|
76
|
-
def process_request(request)
|
77
|
-
return unless valid_request?(request)
|
78
|
-
|
79
|
-
request = request.with_indifferent_access
|
80
|
-
|
81
|
-
read(request)
|
82
|
-
id = request["id"]
|
83
|
-
|
84
|
-
unless (rpc_method = request["method"])
|
85
|
-
# this is a response or a bobo
|
86
|
-
return process_response(id, request["result"]) if request["result"]
|
87
|
-
return process_error(id, request["error"]) if request["error"]
|
88
|
-
end
|
89
|
-
|
90
|
-
params = request["params"]
|
91
|
-
# Try to handle common methods first
|
92
|
-
return if handle_common_methods(rpc_method, id, params)
|
93
|
-
|
94
|
-
# Delegate to subclass-specific handling
|
95
|
-
handle_method(rpc_method, id, params)
|
96
|
-
end
|
97
|
-
|
68
|
+
# Process notification methods
|
98
69
|
def process_notifications(rpc_method, params)
|
99
70
|
case rpc_method
|
100
|
-
when
|
101
|
-
|
102
|
-
# we don't need to do anything here
|
71
|
+
when Methods::NOTIFICATIONS_CANCELLED
|
72
|
+
handle_cancelled_notification(params)
|
103
73
|
else
|
104
74
|
Rails.logger.warn("Unknown notifications method: #{rpc_method}")
|
105
75
|
end
|
106
76
|
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# Handle cancelled notification
|
81
|
+
def handle_cancelled_notification(params)
|
82
|
+
Rails.logger.warn "\e[31m Request #{params['requestId']} cancelled: #{params['reason']}\e[0m"
|
83
|
+
# we don't need to do anything here
|
84
|
+
end
|
107
85
|
end
|
108
86
|
end
|
@@ -57,10 +57,10 @@ module ActionMCP
|
|
57
57
|
capabilities_payload = session.server_capabilities_payload
|
58
58
|
# If vibed_ignore_version is true, always use the latest supported version in the response
|
59
59
|
# Otherwise, use the client's requested version
|
60
|
-
if ActionMCP.configuration.vibed_ignore_version
|
61
|
-
|
60
|
+
capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
|
61
|
+
PROTOCOL_VERSION
|
62
62
|
else
|
63
|
-
|
63
|
+
client_protocol_version
|
64
64
|
end
|
65
65
|
|
66
66
|
send_jsonrpc_response(request_id, result: capabilities_payload)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Server
|
5
|
+
module ErrorAware
|
6
|
+
private
|
7
|
+
|
8
|
+
# Validate required parameter and raise error if missing
|
9
|
+
def validate_required_param(params, key, error_message)
|
10
|
+
value = params[key] || params[key.to_sym]
|
11
|
+
raise JSON_RPC::JsonRpcError.new(:invalid_params, message: error_message) if value.nil?
|
12
|
+
|
13
|
+
value
|
14
|
+
end
|
15
|
+
|
16
|
+
# Validate params is not nil or empty
|
17
|
+
def validate_params_present(params, error_message)
|
18
|
+
raise JSON_RPC::JsonRpcError.new(:invalid_params, message: error_message) if params.nil? || params.empty?
|
19
|
+
|
20
|
+
params
|
21
|
+
end
|
22
|
+
|
23
|
+
# Safe execution with JSON-RPC error handling
|
24
|
+
def with_error_handling(request_id)
|
25
|
+
yield
|
26
|
+
rescue JSON_RPC::JsonRpcError => e
|
27
|
+
error_response(request_id, e)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Server
|
5
|
+
module ErrorHandling
|
6
|
+
private
|
7
|
+
|
8
|
+
def error_response(id, error_or_symbol, message = nil, data = nil)
|
9
|
+
json_rpc_error = case error_or_symbol
|
10
|
+
when JSON_RPC::JsonRpcError
|
11
|
+
error_or_symbol
|
12
|
+
when Symbol
|
13
|
+
JSON_RPC::JsonRpcError.new(error_or_symbol, message: message, data: data)
|
14
|
+
else
|
15
|
+
# If it's already an error hash
|
16
|
+
error_or_symbol
|
17
|
+
end
|
18
|
+
|
19
|
+
{
|
20
|
+
type: :error,
|
21
|
+
request_id: id,
|
22
|
+
payload: {
|
23
|
+
jsonrpc: "2.0",
|
24
|
+
id: id,
|
25
|
+
error: json_rpc_error.to_h
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Helper method to create error response from any exception
|
31
|
+
def error_response_from_exception(id, exception)
|
32
|
+
if exception.is_a?(JSON_RPC::JsonRpcError)
|
33
|
+
error_response(id, exception)
|
34
|
+
else
|
35
|
+
error_response(id, :internal_error, exception.message)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Server
|
5
|
+
module Handlers
|
6
|
+
module PromptHandler
|
7
|
+
include ErrorAware
|
8
|
+
|
9
|
+
def process_prompts(rpc_method, id, params)
|
10
|
+
params ||= {}
|
11
|
+
|
12
|
+
with_error_handling(id) do
|
13
|
+
handler = prompt_method_handlers[rpc_method]
|
14
|
+
if handler
|
15
|
+
send(handler, id, params)
|
16
|
+
else
|
17
|
+
Rails.logger.warn("Unknown prompts method: #{rpc_method}")
|
18
|
+
raise JSON_RPC::JsonRpcError.new(:method_not_found, message: "Unknown prompts method: #{rpc_method}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def prompt_method_handlers
|
26
|
+
{
|
27
|
+
JsonRpcHandlerBase::Methods::PROMPTS_GET => :handle_prompts_get,
|
28
|
+
JsonRpcHandlerBase::Methods::PROMPTS_LIST => :handle_prompts_list
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_prompts_get(id, params)
|
33
|
+
name = extract_name(params)
|
34
|
+
arguments = extract_arguments(params)
|
35
|
+
|
36
|
+
message = transport.send_prompts_get(id, name, arguments)
|
37
|
+
extract_message_payload(message, id)
|
38
|
+
end
|
39
|
+
|
40
|
+
def handle_prompts_list(id, _params)
|
41
|
+
message = transport.send_prompts_list(id)
|
42
|
+
extract_message_payload(message, id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_name(params)
|
46
|
+
params["name"] || params[:name]
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_arguments(params)
|
50
|
+
params["arguments"] || params[:arguments] || {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Server
|
5
|
+
module Handlers
|
6
|
+
module ResourceHandler
|
7
|
+
include ErrorAware
|
8
|
+
|
9
|
+
def process_resources(rpc_method, id, params)
|
10
|
+
params ||= {}
|
11
|
+
|
12
|
+
with_error_handling(id) do
|
13
|
+
handler = resource_method_handlers[rpc_method]
|
14
|
+
if handler
|
15
|
+
send(handler, id, params)
|
16
|
+
else
|
17
|
+
Rails.logger.warn("Unknown resources method: #{rpc_method}")
|
18
|
+
raise JSON_RPC::JsonRpcError.new(:method_not_found, message: "Unknown resources method: #{rpc_method}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def resource_method_handlers
|
26
|
+
{
|
27
|
+
JsonRpcHandlerBase::Methods::RESOURCES_LIST => :handle_resources_list,
|
28
|
+
JsonRpcHandlerBase::Methods::RESOURCES_TEMPLATES_LIST => :handle_resources_templates_list,
|
29
|
+
JsonRpcHandlerBase::Methods::RESOURCES_READ => :handle_resources_read,
|
30
|
+
JsonRpcHandlerBase::Methods::RESOURCES_SUBSCRIBE => :handle_resources_subscribe,
|
31
|
+
JsonRpcHandlerBase::Methods::RESOURCES_UNSUBSCRIBE => :handle_resources_unsubscribe
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle_resources_list(id, _params)
|
36
|
+
message = transport.send_resources_list(id)
|
37
|
+
extract_message_payload(message, id)
|
38
|
+
end
|
39
|
+
|
40
|
+
def handle_resources_templates_list(id, _params)
|
41
|
+
message = transport.send_resource_templates_list(id)
|
42
|
+
extract_message_payload(message, id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle_resources_read(id, params)
|
46
|
+
validate_params_present(params, "Resource URI is required")
|
47
|
+
|
48
|
+
message = transport.send_resource_read(id, params)
|
49
|
+
extract_message_payload(message, id)
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_resources_subscribe(id, params)
|
53
|
+
uri = validate_required_param(params, "uri", "Resource URI is required")
|
54
|
+
|
55
|
+
message = transport.send_resource_subscribe(id, uri)
|
56
|
+
extract_message_payload(message, id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_resources_unsubscribe(id, params)
|
60
|
+
uri = validate_required_param(params, "uri", "Resource URI is required")
|
61
|
+
|
62
|
+
message = transport.send_resource_unsubscribe(id, uri)
|
63
|
+
extract_message_payload(message, id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Server
|
5
|
+
module Handlers
|
6
|
+
module ToolHandler
|
7
|
+
include ErrorAware
|
8
|
+
|
9
|
+
def process_tools(rpc_method, id, params)
|
10
|
+
params ||= {}
|
11
|
+
|
12
|
+
with_error_handling(id) do
|
13
|
+
handler = tool_method_handlers[rpc_method]
|
14
|
+
if handler
|
15
|
+
send(handler, id, params)
|
16
|
+
else
|
17
|
+
Rails.logger.warn("Unknown tools method: #{rpc_method}")
|
18
|
+
raise JSON_RPC::JsonRpcError.new(:method_not_found, message: "Unknown tools method: #{rpc_method}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def tool_method_handlers
|
26
|
+
{
|
27
|
+
JsonRpcHandlerBase::Methods::TOOLS_LIST => :handle_tools_list,
|
28
|
+
JsonRpcHandlerBase::Methods::TOOLS_CALL => :handle_tools_call
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_tools_list(id, params)
|
33
|
+
message = transport.send_tools_list(id, params)
|
34
|
+
extract_message_payload(message, id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def handle_tools_call(id, params)
|
38
|
+
name = validate_required_param(params, "name", "Tool name is required")
|
39
|
+
arguments = extract_arguments(params)
|
40
|
+
|
41
|
+
message = transport.send_tools_call(id, name, arguments)
|
42
|
+
extract_message_payload(message, id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_arguments(params)
|
46
|
+
params["arguments"] || params[:arguments] || {}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -3,10 +3,17 @@
|
|
3
3
|
module ActionMCP
|
4
4
|
module Server
|
5
5
|
class JsonRpcHandler < JsonRpcHandlerBase
|
6
|
+
include Handlers::ResourceHandler
|
7
|
+
include Handlers::ToolHandler
|
8
|
+
include Handlers::PromptHandler
|
9
|
+
include ErrorHandling
|
10
|
+
include ErrorAware
|
11
|
+
|
6
12
|
# Handle server-specific methods
|
7
13
|
# @param request [JSON_RPC::Request, JSON_RPC::Notification, JSON_RPC::Response]
|
8
14
|
def call(request)
|
9
15
|
read(request.to_h)
|
16
|
+
|
10
17
|
case request
|
11
18
|
when JSON_RPC::Request
|
12
19
|
handle_request(request)
|
@@ -20,134 +27,63 @@ module ActionMCP
|
|
20
27
|
private
|
21
28
|
|
22
29
|
def handle_request(request)
|
23
|
-
|
30
|
+
id = request.id
|
24
31
|
rpc_method = request.method
|
25
32
|
params = request.params
|
26
33
|
|
34
|
+
with_error_handling(id) do
|
35
|
+
# Try to handle common methods first (like ping)
|
36
|
+
return if handle_common_methods(rpc_method, id, params)
|
37
|
+
|
38
|
+
# Route to appropriate handler
|
39
|
+
route_to_handler(rpc_method, id, params)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def route_to_handler(rpc_method, id, params)
|
27
44
|
case rpc_method
|
28
|
-
when
|
29
|
-
|
30
|
-
extract_message_payload(message, id)
|
45
|
+
when Methods::INITIALIZE
|
46
|
+
handle_initialize(id, params)
|
31
47
|
when %r{^prompts/}
|
32
48
|
process_prompts(rpc_method, id, params)
|
33
49
|
when %r{^resources/}
|
34
50
|
process_resources(rpc_method, id, params)
|
35
51
|
when %r{^tools/}
|
36
52
|
process_tools(rpc_method, id, params)
|
37
|
-
when
|
53
|
+
when Methods::COMPLETION_COMPLETE
|
38
54
|
process_completion_complete(id, params)
|
39
55
|
else
|
40
|
-
|
56
|
+
raise JSON_RPC::JsonRpcError.new(:method_not_found, message: "Method not found: #{rpc_method}")
|
41
57
|
end
|
42
58
|
end
|
43
59
|
|
60
|
+
def handle_initialize(id, params)
|
61
|
+
message = transport.send_capabilities(id, params)
|
62
|
+
extract_message_payload(message, id)
|
63
|
+
end
|
64
|
+
|
44
65
|
def handle_notification(notification)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
process_notifications(method_name, params)
|
52
|
-
{ type: :notifications_only }
|
53
|
-
rescue StandardError => e
|
54
|
-
Rails.logger.error("Error handling notification #{notification.method}: #{e.message}")
|
55
|
-
Rails.logger.error(e.backtrace.join("\n"))
|
56
|
-
{ type: :notifications_only }
|
57
|
-
end
|
66
|
+
method_name = notification.method.to_s
|
67
|
+
params = notification.params || {}
|
68
|
+
|
69
|
+
process_notifications(method_name, params)
|
70
|
+
{ type: :notifications_only }
|
58
71
|
end
|
59
72
|
|
60
73
|
def handle_response(response)
|
61
74
|
Rails.logger.debug("Received response: #{response.inspect}")
|
75
|
+
|
62
76
|
{
|
63
77
|
type: :responses,
|
64
78
|
request_id: response.id,
|
65
|
-
payload:
|
66
|
-
jsonrpc: "2.0",
|
67
|
-
id: response.id,
|
68
|
-
result: response.result
|
69
|
-
}
|
79
|
+
payload: build_response_payload(response)
|
70
80
|
}
|
71
81
|
end
|
72
82
|
|
73
|
-
def process_prompts(rpc_method, id, params)
|
74
|
-
params ||= {}
|
75
|
-
|
76
|
-
case rpc_method
|
77
|
-
when "prompts/get"
|
78
|
-
name = params["name"] || params[:name]
|
79
|
-
arguments = params["arguments"] || params[:arguments] || {}
|
80
|
-
message = transport.send_prompts_get(id, name, arguments)
|
81
|
-
extract_message_payload(message, id)
|
82
|
-
when "prompts/list"
|
83
|
-
message = transport.send_prompts_list(id)
|
84
|
-
extract_message_payload(message, id)
|
85
|
-
else
|
86
|
-
Rails.logger.warn("Unknown prompts method: #{rpc_method}")
|
87
|
-
error_response(id, :method_not_found, "Unknown prompts method: #{rpc_method}")
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def process_tools(rpc_method, id, params)
|
92
|
-
params ||= {}
|
93
|
-
|
94
|
-
case rpc_method
|
95
|
-
when "tools/list"
|
96
|
-
message = transport.send_tools_list(id, params)
|
97
|
-
extract_message_payload(message, id)
|
98
|
-
when "tools/call"
|
99
|
-
name = params["name"] || params[:name]
|
100
|
-
arguments = params["arguments"] || params[:arguments] || {}
|
101
|
-
|
102
|
-
return error_response(id, :invalid_params, "Tool name is required") if name.nil?
|
103
|
-
|
104
|
-
message = transport.send_tools_call(id, name, arguments)
|
105
|
-
extract_message_payload(message, id)
|
106
|
-
else
|
107
|
-
Rails.logger.warn("Unknown tools method: #{rpc_method}")
|
108
|
-
error_response(id, :method_not_found, "Unknown tools method: #{rpc_method}")
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def process_resources(rpc_method, id, params)
|
113
|
-
params ||= {}
|
114
|
-
|
115
|
-
case rpc_method
|
116
|
-
when "resources/list"
|
117
|
-
message = transport.send_resources_list(id)
|
118
|
-
extract_message_payload(message, id)
|
119
|
-
when "resources/templates/list"
|
120
|
-
message = transport.send_resource_templates_list(id)
|
121
|
-
extract_message_payload(message, id)
|
122
|
-
when "resources/read"
|
123
|
-
return error_response(id, :invalid_params, "Resource URI is required") if params.nil? || params.empty?
|
124
|
-
|
125
|
-
message = transport.send_resource_read(id, params)
|
126
|
-
extract_message_payload(message, id)
|
127
|
-
when "resources/subscribe"
|
128
|
-
uri = params["uri"] || params[:uri]
|
129
|
-
return error_response(id, :invalid_params, "Resource URI is required") if uri.nil?
|
130
|
-
|
131
|
-
message = transport.send_resource_subscribe(id, uri)
|
132
|
-
extract_message_payload(message, id)
|
133
|
-
when "resources/unsubscribe"
|
134
|
-
uri = params["uri"] || params[:uri]
|
135
|
-
return error_response(id, :invalid_params, "Resource URI is required") if uri.nil?
|
136
|
-
|
137
|
-
message = transport.send_resource_unsubscribe(id, uri)
|
138
|
-
extract_message_payload(message, id)
|
139
|
-
else
|
140
|
-
Rails.logger.warn("Unknown resources method: #{rpc_method}")
|
141
|
-
error_response(id, :method_not_found, "Unknown resources method: #{rpc_method}")
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
83
|
def process_completion_complete(id, params)
|
146
84
|
params ||= {}
|
147
85
|
|
148
|
-
result = transport.send_jsonrpc_response(id, result:
|
149
|
-
completion: { values: [], total: 0, hasMore: false }
|
150
|
-
})
|
86
|
+
result = transport.send_jsonrpc_response(id, result: build_completion_result)
|
151
87
|
|
152
88
|
if result.is_a?(ActionMCP::Session::Message)
|
153
89
|
extract_message_payload(result, id)
|
@@ -158,8 +94,7 @@ module ActionMCP
|
|
158
94
|
|
159
95
|
def process_notifications(rpc_method, params)
|
160
96
|
case rpc_method
|
161
|
-
when
|
162
|
-
Rails.logger.info "Client notified initialization complete"
|
97
|
+
when Methods::NOTIFICATIONS_INITIALIZED
|
163
98
|
transport.initialize!
|
164
99
|
else
|
165
100
|
super
|
@@ -174,7 +109,7 @@ module ActionMCP
|
|
174
109
|
payload: message.message_json
|
175
110
|
}
|
176
111
|
else
|
177
|
-
|
112
|
+
message
|
178
113
|
end
|
179
114
|
end
|
180
115
|
|
@@ -190,23 +125,17 @@ module ActionMCP
|
|
190
125
|
end
|
191
126
|
end
|
192
127
|
|
193
|
-
def
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
128
|
+
def build_response_payload(response)
|
129
|
+
{
|
130
|
+
jsonrpc: "2.0",
|
131
|
+
id: response.id,
|
132
|
+
result: response.result
|
133
|
+
}
|
134
|
+
end
|
200
135
|
|
136
|
+
def build_completion_result
|
201
137
|
{
|
202
|
-
|
203
|
-
request_id: id,
|
204
|
-
payload: {
|
205
|
-
jsonrpc: "2.0",
|
206
|
-
id: id,
|
207
|
-
error: { code: error_code, message: message }
|
208
|
-
},
|
209
|
-
status: :bad_request
|
138
|
+
completion: { values: [], total: 0, hasMore: false }
|
210
139
|
}
|
211
140
|
end
|
212
141
|
end
|
data/lib/action_mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionmcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.50.
|
4
|
+
version: 0.50.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-05-
|
11
|
+
date: 2025-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -169,6 +169,11 @@ files:
|
|
169
169
|
- lib/action_mcp/server.rb
|
170
170
|
- lib/action_mcp/server/capabilities.rb
|
171
171
|
- lib/action_mcp/server/configuration.rb
|
172
|
+
- lib/action_mcp/server/error_aware.rb
|
173
|
+
- lib/action_mcp/server/error_handling.rb
|
174
|
+
- lib/action_mcp/server/handlers/prompt_handler.rb
|
175
|
+
- lib/action_mcp/server/handlers/resource_handler.rb
|
176
|
+
- lib/action_mcp/server/handlers/tool_handler.rb
|
172
177
|
- lib/action_mcp/server/json_rpc_handler.rb
|
173
178
|
- lib/action_mcp/server/messaging.rb
|
174
179
|
- lib/action_mcp/server/notifications.rb
|