actionmcp 0.50.6 → 0.50.8
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 +6 -6
- data/app/models/action_mcp/session.rb +3 -1
- data/lib/action_mcp/client/collection.rb +4 -5
- data/lib/action_mcp/configuration.rb +0 -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 +45 -119
- data/lib/action_mcp/version.rb +1 -1
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 94384a0f8fcb5f0f77bac5d814fea53addbc8732007a3771b843f2bcca24ab47
|
4
|
+
data.tar.gz: 812125b6acf82b418257610ba5b58b3af4cb01c5b3152ac995a1c614df02a263
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e693c3b47925b0f1babb7d1912605766851b054f764ba6c0b6270f5f6d34739231300654a3dca83cb3d4a12a00cc29f20400bfdd7e93b4358bd296f9b9881ae
|
7
|
+
data.tar.gz: fba759c19cab4145614ac93b7c14866261f663a41f7539d5741712708136f34c29d43fcedbedbe06834f5989ac6c6d9d2aa2aa6b6a3f7e44b46e891a160bc224
|
@@ -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
|
|
@@ -331,7 +332,7 @@ module ActionMCP
|
|
331
332
|
data = payload.is_a?(String) ? payload : MultiJson.dump(payload)
|
332
333
|
sse_event = "id: #{event_id}\ndata: #{data}\n\n"
|
333
334
|
sse.write(sse_event)
|
334
|
-
|
335
|
+
|
335
336
|
begin
|
336
337
|
session.store_sse_event(event_id, payload, session.max_stored_sse_events)
|
337
338
|
rescue StandardError => e
|
@@ -341,7 +342,6 @@ module ActionMCP
|
|
341
342
|
|
342
343
|
# Helper to clean up old SSE events for a session
|
343
344
|
def cleanup_old_sse_events(session)
|
344
|
-
return unless ActionMCP.configuration.enable_sse_resumability
|
345
345
|
begin
|
346
346
|
retention_period = session.sse_event_retention_period
|
347
347
|
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
|
@@ -29,7 +29,6 @@ module ActionMCP
|
|
29
29
|
# --- VibedIgnoreVersion Option ---
|
30
30
|
:vibed_ignore_version,
|
31
31
|
# --- SSE Resumability Options ---
|
32
|
-
:enable_sse_resumability,
|
33
32
|
:sse_event_retention_period,
|
34
33
|
:max_stored_sse_events
|
35
34
|
|
@@ -46,7 +45,6 @@ module ActionMCP
|
|
46
45
|
@vibed_ignore_version = false
|
47
46
|
|
48
47
|
# Resumability defaults
|
49
|
-
@enable_sse_resumability = true
|
50
48
|
@sse_event_retention_period = 15.minutes
|
51
49
|
@max_stored_sse_events = 100
|
52
50
|
end
|
@@ -141,9 +139,6 @@ module ActionMCP
|
|
141
139
|
|
142
140
|
capabilities[:resources] = { subscribe: @resources_subscribe } if filtered_resources.any?
|
143
141
|
|
144
|
-
# Add resumability capability if enabled
|
145
|
-
capabilities[:resumability] = { enabled: @enable_sse_resumability } if @enable_sse_resumability
|
146
|
-
|
147
142
|
capabilities
|
148
143
|
end
|
149
144
|
|
@@ -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,137 +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
|
|
27
|
-
|
28
|
-
|
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
|
29
42
|
|
43
|
+
def route_to_handler(rpc_method, id, params)
|
30
44
|
case rpc_method
|
31
|
-
when
|
32
|
-
|
33
|
-
extract_message_payload(message, id)
|
45
|
+
when Methods::INITIALIZE
|
46
|
+
handle_initialize(id, params)
|
34
47
|
when %r{^prompts/}
|
35
48
|
process_prompts(rpc_method, id, params)
|
36
49
|
when %r{^resources/}
|
37
50
|
process_resources(rpc_method, id, params)
|
38
51
|
when %r{^tools/}
|
39
52
|
process_tools(rpc_method, id, params)
|
40
|
-
when
|
53
|
+
when Methods::COMPLETION_COMPLETE
|
41
54
|
process_completion_complete(id, params)
|
42
55
|
else
|
43
|
-
|
56
|
+
raise JSON_RPC::JsonRpcError.new(:method_not_found, message: "Method not found: #{rpc_method}")
|
44
57
|
end
|
45
58
|
end
|
46
59
|
|
60
|
+
def handle_initialize(id, params)
|
61
|
+
message = transport.send_capabilities(id, params)
|
62
|
+
extract_message_payload(message, id)
|
63
|
+
end
|
64
|
+
|
47
65
|
def handle_notification(notification)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
process_notifications(method_name, params)
|
55
|
-
{ type: :notifications_only }
|
56
|
-
rescue StandardError => e
|
57
|
-
Rails.logger.error("Error handling notification #{notification.method}: #{e.message}")
|
58
|
-
Rails.logger.error(e.backtrace.join("\n"))
|
59
|
-
{ type: :notifications_only }
|
60
|
-
end
|
66
|
+
method_name = notification.method.to_s
|
67
|
+
params = notification.params || {}
|
68
|
+
|
69
|
+
process_notifications(method_name, params)
|
70
|
+
{ type: :notifications_only }
|
61
71
|
end
|
62
72
|
|
63
73
|
def handle_response(response)
|
64
74
|
Rails.logger.debug("Received response: #{response.inspect}")
|
75
|
+
|
65
76
|
{
|
66
77
|
type: :responses,
|
67
78
|
request_id: response.id,
|
68
|
-
payload:
|
69
|
-
jsonrpc: "2.0",
|
70
|
-
id: response.id,
|
71
|
-
result: response.result
|
72
|
-
}
|
79
|
+
payload: build_response_payload(response)
|
73
80
|
}
|
74
81
|
end
|
75
82
|
|
76
|
-
def process_prompts(rpc_method, id, params)
|
77
|
-
params ||= {}
|
78
|
-
|
79
|
-
case rpc_method
|
80
|
-
when "prompts/get"
|
81
|
-
name = params["name"] || params[:name]
|
82
|
-
arguments = params["arguments"] || params[:arguments] || {}
|
83
|
-
message = transport.send_prompts_get(id, name, arguments)
|
84
|
-
extract_message_payload(message, id)
|
85
|
-
when "prompts/list"
|
86
|
-
message = transport.send_prompts_list(id)
|
87
|
-
extract_message_payload(message, id)
|
88
|
-
else
|
89
|
-
Rails.logger.warn("Unknown prompts method: #{rpc_method}")
|
90
|
-
error_response(id, :method_not_found, "Unknown prompts method: #{rpc_method}")
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def process_tools(rpc_method, id, params)
|
95
|
-
params ||= {}
|
96
|
-
|
97
|
-
case rpc_method
|
98
|
-
when "tools/list"
|
99
|
-
message = transport.send_tools_list(id, params)
|
100
|
-
extract_message_payload(message, id)
|
101
|
-
when "tools/call"
|
102
|
-
name = params["name"] || params[:name]
|
103
|
-
arguments = params["arguments"] || params[:arguments] || {}
|
104
|
-
|
105
|
-
return error_response(id, :invalid_params, "Tool name is required") if name.nil?
|
106
|
-
|
107
|
-
message = transport.send_tools_call(id, name, arguments)
|
108
|
-
extract_message_payload(message, id)
|
109
|
-
else
|
110
|
-
Rails.logger.warn("Unknown tools method: #{rpc_method}")
|
111
|
-
error_response(id, :method_not_found, "Unknown tools method: #{rpc_method}")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def process_resources(rpc_method, id, params)
|
116
|
-
params ||= {}
|
117
|
-
|
118
|
-
case rpc_method
|
119
|
-
when "resources/list"
|
120
|
-
message = transport.send_resources_list(id)
|
121
|
-
extract_message_payload(message, id)
|
122
|
-
when "resources/templates/list"
|
123
|
-
message = transport.send_resource_templates_list(id)
|
124
|
-
extract_message_payload(message, id)
|
125
|
-
when "resources/read"
|
126
|
-
return error_response(id, :invalid_params, "Resource URI is required") if params.nil? || params.empty?
|
127
|
-
|
128
|
-
message = transport.send_resource_read(id, params)
|
129
|
-
extract_message_payload(message, id)
|
130
|
-
when "resources/subscribe"
|
131
|
-
uri = params["uri"] || params[:uri]
|
132
|
-
return error_response(id, :invalid_params, "Resource URI is required") if uri.nil?
|
133
|
-
|
134
|
-
message = transport.send_resource_subscribe(id, uri)
|
135
|
-
extract_message_payload(message, id)
|
136
|
-
when "resources/unsubscribe"
|
137
|
-
uri = params["uri"] || params[:uri]
|
138
|
-
return error_response(id, :invalid_params, "Resource URI is required") if uri.nil?
|
139
|
-
|
140
|
-
message = transport.send_resource_unsubscribe(id, uri)
|
141
|
-
extract_message_payload(message, id)
|
142
|
-
else
|
143
|
-
Rails.logger.warn("Unknown resources method: #{rpc_method}")
|
144
|
-
error_response(id, :method_not_found, "Unknown resources method: #{rpc_method}")
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
83
|
def process_completion_complete(id, params)
|
149
84
|
params ||= {}
|
150
85
|
|
151
|
-
result = transport.send_jsonrpc_response(id, result:
|
152
|
-
completion: { values: [], total: 0, hasMore: false }
|
153
|
-
})
|
86
|
+
result = transport.send_jsonrpc_response(id, result: build_completion_result)
|
154
87
|
|
155
88
|
if result.is_a?(ActionMCP::Session::Message)
|
156
89
|
extract_message_payload(result, id)
|
@@ -161,8 +94,7 @@ module ActionMCP
|
|
161
94
|
|
162
95
|
def process_notifications(rpc_method, params)
|
163
96
|
case rpc_method
|
164
|
-
when
|
165
|
-
Rails.logger.info "Client notified initialization complete"
|
97
|
+
when Methods::NOTIFICATIONS_INITIALIZED
|
166
98
|
transport.initialize!
|
167
99
|
else
|
168
100
|
super
|
@@ -177,7 +109,7 @@ module ActionMCP
|
|
177
109
|
payload: message.message_json
|
178
110
|
}
|
179
111
|
else
|
180
|
-
|
112
|
+
message
|
181
113
|
end
|
182
114
|
end
|
183
115
|
|
@@ -193,23 +125,17 @@ module ActionMCP
|
|
193
125
|
end
|
194
126
|
end
|
195
127
|
|
196
|
-
def
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
128
|
+
def build_response_payload(response)
|
129
|
+
{
|
130
|
+
jsonrpc: "2.0",
|
131
|
+
id: response.id,
|
132
|
+
result: response.result
|
133
|
+
}
|
134
|
+
end
|
203
135
|
|
136
|
+
def build_completion_result
|
204
137
|
{
|
205
|
-
|
206
|
-
request_id: id,
|
207
|
-
payload: {
|
208
|
-
jsonrpc: "2.0",
|
209
|
-
id: id,
|
210
|
-
error: { code: error_code, message: message }
|
211
|
-
},
|
212
|
-
status: :bad_request
|
138
|
+
completion: { values: [], total: 0, hasMore: false }
|
213
139
|
}
|
214
140
|
end
|
215
141
|
end
|
data/lib/action_mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +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.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
8
|
+
autorequire:
|
8
9
|
bindir: exe
|
9
10
|
cert_chain: []
|
10
|
-
date:
|
11
|
+
date: 2025-05-15 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: activerecord
|
@@ -168,6 +169,11 @@ files:
|
|
168
169
|
- lib/action_mcp/server.rb
|
169
170
|
- lib/action_mcp/server/capabilities.rb
|
170
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
|
171
177
|
- lib/action_mcp/server/json_rpc_handler.rb
|
172
178
|
- lib/action_mcp/server/messaging.rb
|
173
179
|
- lib/action_mcp/server/notifications.rb
|
@@ -214,6 +220,7 @@ metadata:
|
|
214
220
|
source_code_uri: https://github.com/seuros/action_mcp
|
215
221
|
changelog_uri: https://github.com/seuros/action_mcp/blob/master/CHANGELOG.md
|
216
222
|
rubygems_mfa_required: 'true'
|
223
|
+
post_install_message:
|
217
224
|
rdoc_options: []
|
218
225
|
require_paths:
|
219
226
|
- lib
|
@@ -228,7 +235,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
228
235
|
- !ruby/object:Gem::Version
|
229
236
|
version: '0'
|
230
237
|
requirements: []
|
231
|
-
rubygems_version: 3.
|
238
|
+
rubygems_version: 3.5.22
|
239
|
+
signing_key:
|
232
240
|
specification_version: 4
|
233
241
|
summary: Provides essential tooling for building Model Context Protocol (MCP) capable
|
234
242
|
servers
|