actionmcp 0.50.1 → 0.50.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/app/controllers/action_mcp/{unified_controller.rb → application_controller.rb} +118 -144
- data/app/models/action_mcp/session/message.rb +25 -16
- data/app/models/action_mcp/session/sse_event.rb +1 -0
- data/app/models/action_mcp/session.rb +31 -27
- data/app/models/concerns/mcp_console_helpers.rb +3 -3
- data/app/models/concerns/mcp_message_inspect.rb +4 -4
- data/config/routes.rb +3 -3
- data/db/migrate/20250512154359_consolidated_migration.rb +28 -27
- data/exe/actionmcp_cli +1 -1
- data/lib/action_mcp/client.rb +4 -4
- data/lib/action_mcp/engine.rb +27 -0
- data/lib/action_mcp/instrumentation/controller_runtime.rb +1 -1
- data/lib/action_mcp/json_rpc_handler_base.rb +1 -0
- data/lib/action_mcp/log_subscriber.rb +160 -0
- data/lib/action_mcp/resource_template.rb +1 -3
- data/lib/action_mcp/server/capabilities.rb +11 -8
- data/lib/action_mcp/server/configuration.rb +5 -2
- data/lib/action_mcp/server/json_rpc_handler.rb +155 -88
- data/lib/action_mcp/server/registry_management.rb +2 -0
- data/lib/action_mcp/server/simple_pub_sub.rb +7 -6
- data/lib/action_mcp/server/solid_cable_adapter.rb +12 -13
- data/lib/action_mcp/server/tools.rb +2 -2
- data/lib/action_mcp/server.rb +5 -4
- data/lib/action_mcp/tool.rb +1 -1
- data/lib/action_mcp/version.rb +1 -1
- metadata +16 -17
- data/app/controllers/action_mcp/mcp_controller.rb +0 -79
@@ -4,37 +4,30 @@ module ActionMCP
|
|
4
4
|
module Server
|
5
5
|
class JsonRpcHandler < JsonRpcHandlerBase
|
6
6
|
# Handle server-specific methods
|
7
|
-
# @param
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
rescue MultiJson::ParseError => e
|
18
|
-
Rails.logger.error("Failed to parse JSON: #{e.message}")
|
19
|
-
return
|
20
|
-
end
|
21
|
-
else
|
22
|
-
line
|
7
|
+
# @param request [JSON_RPC::Request, JSON_RPC::Notification, JSON_RPC::Response]
|
8
|
+
def call(request)
|
9
|
+
read(request.to_h)
|
10
|
+
case request
|
11
|
+
when JSON_RPC::Request
|
12
|
+
handle_request(request)
|
13
|
+
when JSON_RPC::Notification
|
14
|
+
handle_notification(request)
|
15
|
+
when JSON_RPC::Response
|
16
|
+
handle_response(request)
|
23
17
|
end
|
24
|
-
|
25
|
-
# Store the request ID for error responses
|
26
|
-
@current_request_id = request["id"] if request.is_a?(Hash)
|
27
|
-
|
28
|
-
process_request(request)
|
29
18
|
end
|
30
19
|
|
31
|
-
|
32
|
-
|
33
|
-
|
20
|
+
private
|
21
|
+
|
22
|
+
def handle_request(request)
|
23
|
+
@current_request_id = id = request.id
|
24
|
+
rpc_method = request.method
|
25
|
+
params = request.params
|
34
26
|
|
35
27
|
case rpc_method
|
36
28
|
when "initialize"
|
37
|
-
transport.send_capabilities(id, params)
|
29
|
+
message = transport.send_capabilities(id, params)
|
30
|
+
extract_message_payload(message, id)
|
38
31
|
when %r{^prompts/}
|
39
32
|
process_prompts(rpc_method, id, params)
|
40
33
|
when %r{^resources/}
|
@@ -44,104 +37,178 @@ module ActionMCP
|
|
44
37
|
when "completion/complete"
|
45
38
|
process_completion_complete(id, params)
|
46
39
|
else
|
47
|
-
|
40
|
+
error_response(id, :method_not_found, "Method not found: #{rpc_method}")
|
48
41
|
end
|
49
|
-
rescue StandardError => e
|
50
|
-
Rails.logger.error("Error handling method #{rpc_method}: #{e.message}")
|
51
|
-
transport.send_jsonrpc_error(id, :internal_error, "Internal error: #{e.message}")
|
52
42
|
end
|
53
43
|
|
44
|
+
def handle_notification(notification)
|
45
|
+
@current_request_id = nil
|
54
46
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# "name": "language",
|
66
|
-
# "value": "py"
|
67
|
-
# }
|
68
|
-
# }
|
69
|
-
# @return [Hash]
|
70
|
-
# @example {
|
71
|
-
# "completion": {
|
72
|
-
# "values": ["python", "pytorch", "pyside"],
|
73
|
-
# "total": 10,
|
74
|
-
# "hasMore": true
|
75
|
-
# }
|
76
|
-
# }
|
77
|
-
def process_completion_complete(id, params)
|
78
|
-
# TODO: Not Implemented, but to remove the error message in the inspector
|
79
|
-
transport.send_jsonrpc_response(id, result: { completion: { values: [], total: 0, hasMore: false } })
|
80
|
-
case params["ref"]["type"]
|
81
|
-
when "ref/prompt"
|
82
|
-
# TODO: Implement completion
|
83
|
-
when "ref/resource"
|
84
|
-
# TODO: Implement completion
|
47
|
+
begin
|
48
|
+
method_name = notification.method.to_s
|
49
|
+
params = notification.params || {}
|
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 }
|
85
57
|
end
|
86
58
|
end
|
87
59
|
|
88
|
-
|
89
|
-
|
90
|
-
|
60
|
+
def handle_response(response)
|
61
|
+
Rails.logger.debug("Received response: #{response.inspect}")
|
62
|
+
{
|
63
|
+
type: :responses,
|
64
|
+
request_id: response.id,
|
65
|
+
payload: {
|
66
|
+
jsonrpc: "2.0",
|
67
|
+
id: response.id,
|
68
|
+
result: response.result
|
69
|
+
}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
91
73
|
def process_prompts(rpc_method, id, params)
|
74
|
+
params ||= {}
|
75
|
+
|
92
76
|
case rpc_method
|
93
|
-
when "prompts/get"
|
94
|
-
|
95
|
-
|
96
|
-
transport.
|
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)
|
97
85
|
else
|
98
86
|
Rails.logger.warn("Unknown prompts method: #{rpc_method}")
|
87
|
+
error_response(id, :method_not_found, "Unknown prompts method: #{rpc_method}")
|
99
88
|
end
|
100
89
|
end
|
101
90
|
|
102
|
-
# @param rpc_method [String]
|
103
|
-
# @param id [String]
|
104
|
-
# @param params [Hash]
|
105
91
|
def process_tools(rpc_method, id, params)
|
92
|
+
params ||= {}
|
93
|
+
|
106
94
|
case rpc_method
|
107
|
-
when "tools/list"
|
108
|
-
transport.send_tools_list(id, params)
|
109
|
-
|
110
|
-
|
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)
|
111
106
|
else
|
112
107
|
Rails.logger.warn("Unknown tools method: #{rpc_method}")
|
108
|
+
error_response(id, :method_not_found, "Unknown tools method: #{rpc_method}")
|
113
109
|
end
|
114
110
|
end
|
115
111
|
|
116
|
-
# @param rpc_method [String]
|
117
|
-
# @param id [String]
|
118
|
-
# @param params [Hash]
|
119
112
|
def process_resources(rpc_method, id, params)
|
113
|
+
params ||= {}
|
114
|
+
|
120
115
|
case rpc_method
|
121
|
-
when "resources/list"
|
122
|
-
transport.send_resources_list(id)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
when "resources/
|
128
|
-
|
129
|
-
|
130
|
-
transport.
|
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)
|
131
139
|
else
|
132
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
|
+
def process_completion_complete(id, params)
|
146
|
+
params ||= {}
|
147
|
+
|
148
|
+
result = transport.send_jsonrpc_response(id, result: {
|
149
|
+
completion: { values: [], total: 0, hasMore: false }
|
150
|
+
})
|
151
|
+
|
152
|
+
if result.is_a?(ActionMCP::Session::Message)
|
153
|
+
extract_message_payload(result, id)
|
154
|
+
else
|
155
|
+
wrap_transport_result(result, id)
|
133
156
|
end
|
134
157
|
end
|
135
158
|
|
136
159
|
def process_notifications(rpc_method, params)
|
137
160
|
case rpc_method
|
138
|
-
when "notifications/initialized"
|
139
|
-
|
161
|
+
when "notifications/initialized"
|
162
|
+
Rails.logger.info "Client notified initialization complete"
|
140
163
|
transport.initialize!
|
141
164
|
else
|
142
165
|
super
|
143
166
|
end
|
144
167
|
end
|
168
|
+
|
169
|
+
def extract_message_payload(message, id)
|
170
|
+
if message.is_a?(ActionMCP::Session::Message)
|
171
|
+
{
|
172
|
+
type: :responses,
|
173
|
+
request_id: id,
|
174
|
+
payload: message.message_json
|
175
|
+
}
|
176
|
+
else
|
177
|
+
message
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def wrap_transport_result(transport_result, id)
|
182
|
+
if transport_result.is_a?(Hash) && transport_result[:type]
|
183
|
+
transport_result
|
184
|
+
else
|
185
|
+
{
|
186
|
+
type: :responses,
|
187
|
+
request_id: id,
|
188
|
+
payload: transport_result
|
189
|
+
}
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def error_response(id, code, message)
|
194
|
+
error_code = case code
|
195
|
+
when :method_not_found then -32_601
|
196
|
+
when :invalid_params then -32_602
|
197
|
+
when :internal_error then -32_603
|
198
|
+
else -32_000
|
199
|
+
end
|
200
|
+
|
201
|
+
{
|
202
|
+
type: :error,
|
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
|
210
|
+
}
|
211
|
+
end
|
145
212
|
end
|
146
213
|
end
|
147
214
|
end
|
@@ -13,7 +13,7 @@ module ActionMCP
|
|
13
13
|
DEFAULT_MIN_THREADS = 5
|
14
14
|
DEFAULT_MAX_THREADS = 10
|
15
15
|
DEFAULT_MAX_QUEUE = 100
|
16
|
-
DEFAULT_THREAD_TIMEOUT = 60
|
16
|
+
DEFAULT_THREAD_TIMEOUT = 60 # seconds
|
17
17
|
|
18
18
|
def initialize(options = {})
|
19
19
|
@subscriptions = Concurrent::Map.new
|
@@ -58,6 +58,7 @@ module ActionMCP
|
|
58
58
|
def subscribed_to?(channel)
|
59
59
|
channel_subs = @channels[channel]
|
60
60
|
return false if channel_subs.nil?
|
61
|
+
|
61
62
|
!channel_subs.empty?
|
62
63
|
end
|
63
64
|
|
@@ -91,11 +92,9 @@ module ActionMCP
|
|
91
92
|
next unless subscription && subscription[:message_callback]
|
92
93
|
|
93
94
|
@thread_pool.post do
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
log_error("Error in message callback: #{e.message}\n#{e.backtrace.join("\n")}")
|
98
|
-
end
|
95
|
+
subscription[:message_callback].call(message)
|
96
|
+
rescue StandardError => e
|
97
|
+
log_error("Error in message callback: #{e.message}\n#{e.backtrace.join("\n")}")
|
99
98
|
end
|
100
99
|
end
|
101
100
|
end
|
@@ -106,6 +105,7 @@ module ActionMCP
|
|
106
105
|
def has_subscribers?(channel)
|
107
106
|
subscribers = @channels[channel]
|
108
107
|
return false unless subscribers
|
108
|
+
|
109
109
|
!subscribers.empty?
|
110
110
|
end
|
111
111
|
|
@@ -138,6 +138,7 @@ module ActionMCP
|
|
138
138
|
|
139
139
|
def log_error(message)
|
140
140
|
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
141
|
+
|
141
142
|
Rails.logger.error("SimplePubSub: #{message}")
|
142
143
|
end
|
143
144
|
end
|
@@ -39,13 +39,13 @@ module ActionMCP
|
|
39
39
|
DEFAULT_MIN_THREADS = 5
|
40
40
|
DEFAULT_MAX_THREADS = 10
|
41
41
|
DEFAULT_MAX_QUEUE = 100
|
42
|
-
DEFAULT_THREAD_TIMEOUT = 60
|
42
|
+
DEFAULT_THREAD_TIMEOUT = 60 # seconds
|
43
43
|
|
44
44
|
def initialize(options = {})
|
45
45
|
@options = options
|
46
46
|
@subscriptions = Concurrent::Map.new
|
47
47
|
@channels = Concurrent::Map.new
|
48
|
-
@channel_subscribed = Concurrent::Map.new
|
48
|
+
@channel_subscribed = Concurrent::Map.new # Track channel subscription status
|
49
49
|
|
50
50
|
# Initialize thread pool for callbacks
|
51
51
|
pool_options = {
|
@@ -69,15 +69,13 @@ module ActionMCP
|
|
69
69
|
end
|
70
70
|
|
71
71
|
# If there's a connects_to option, pass it along
|
72
|
-
if @options["connects_to"]
|
73
|
-
pubsub_options[:connects_to] = @options["connects_to"]
|
74
|
-
end
|
72
|
+
pubsub_options[:connects_to] = @options["connects_to"] if @options["connects_to"]
|
75
73
|
|
76
74
|
# Use mock version for testing or real version in production
|
77
|
-
if defined?(SolidCable) && !testing?
|
78
|
-
|
75
|
+
@solid_cable_pubsub = if defined?(SolidCable) && !testing?
|
76
|
+
SolidCable::PubSub.new(pubsub_options)
|
79
77
|
else
|
80
|
-
|
78
|
+
MockSolidCablePubSub.new(pubsub_options)
|
81
79
|
end
|
82
80
|
end
|
83
81
|
|
@@ -147,6 +145,7 @@ module ActionMCP
|
|
147
145
|
def has_subscribers?(channel)
|
148
146
|
subscribers = @channels[channel]
|
149
147
|
return false unless subscribers
|
148
|
+
|
150
149
|
!subscribers.empty?
|
151
150
|
end
|
152
151
|
|
@@ -156,6 +155,7 @@ module ActionMCP
|
|
156
155
|
def subscribed_to?(channel)
|
157
156
|
channel_subs = @channels[channel]
|
158
157
|
return false if channel_subs.nil?
|
158
|
+
|
159
159
|
!channel_subs.empty?
|
160
160
|
end
|
161
161
|
|
@@ -185,11 +185,9 @@ module ActionMCP
|
|
185
185
|
next unless subscription && subscription[:message_callback]
|
186
186
|
|
187
187
|
@thread_pool.post do
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
log_error("Error in message callback: #{e.message}\n#{e.backtrace.join("\n")}")
|
192
|
-
end
|
188
|
+
subscription[:message_callback].call(message)
|
189
|
+
rescue StandardError => e
|
190
|
+
log_error("Error in message callback: #{e.message}\n#{e.backtrace.join("\n")}")
|
193
191
|
end
|
194
192
|
end
|
195
193
|
end
|
@@ -215,6 +213,7 @@ module ActionMCP
|
|
215
213
|
|
216
214
|
def log_error(message)
|
217
215
|
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
216
|
+
|
218
217
|
Rails.logger.error("SolidCableAdapter: #{message}")
|
219
218
|
end
|
220
219
|
end
|
@@ -18,9 +18,9 @@ module ActionMCP
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Use session's registered tools instead of global registry
|
21
|
-
tools = session.registered_tools.map
|
21
|
+
tools = session.registered_tools.map do |tool_class|
|
22
22
|
tool_class.to_h(protocol_version: protocol_version)
|
23
|
-
|
23
|
+
end
|
24
24
|
|
25
25
|
# Send completion progress notification if token is provided
|
26
26
|
if progress_token
|
data/lib/action_mcp/server.rb
CHANGED
@@ -23,6 +23,7 @@ module ActionMCP
|
|
23
23
|
# Shut down the server and clean up resources
|
24
24
|
def shutdown
|
25
25
|
return unless @server
|
26
|
+
|
26
27
|
@server.shutdown
|
27
28
|
@server = nil
|
28
29
|
end
|
@@ -49,7 +50,7 @@ module ActionMCP
|
|
49
50
|
def configure(config_path)
|
50
51
|
shutdown_pubsub if @pubsub
|
51
52
|
@configuration = Configuration.new(config_path)
|
52
|
-
@pubsub = nil
|
53
|
+
@pubsub = nil # Reset pubsub so it will be recreated with new config
|
53
54
|
end
|
54
55
|
|
55
56
|
# Gracefully shut down the server and its resources
|
@@ -61,11 +62,11 @@ module ActionMCP
|
|
61
62
|
|
62
63
|
# Shut down the pubsub adapter gracefully
|
63
64
|
def shutdown_pubsub
|
64
|
-
return unless @pubsub
|
65
|
+
return unless @pubsub.respond_to?(:shutdown)
|
65
66
|
|
66
67
|
begin
|
67
68
|
@pubsub.shutdown
|
68
|
-
rescue => e
|
69
|
+
rescue StandardError => e
|
69
70
|
message = "Error shutting down pubsub adapter: #{e.message}"
|
70
71
|
Rails.logger.error(message) if defined?(Rails) && Rails.respond_to?(:logger)
|
71
72
|
ensure
|
@@ -87,7 +88,7 @@ module ActionMCP
|
|
87
88
|
rescue NameError, LoadError => e
|
88
89
|
message = "Error creating adapter #{adapter_name}: #{e.message}"
|
89
90
|
Rails.logger.error(message) if defined?(Rails) && Rails.respond_to?(:logger)
|
90
|
-
SimplePubSub.new
|
91
|
+
SimplePubSub.new # Fallback to simple pubsub
|
91
92
|
end
|
92
93
|
end
|
93
94
|
end
|
data/lib/action_mcp/tool.rb
CHANGED
@@ -61,7 +61,7 @@ module ActionMCP
|
|
61
61
|
end
|
62
62
|
|
63
63
|
# Return annotations for the tool
|
64
|
-
def annotations_for_protocol(
|
64
|
+
def annotations_for_protocol(_protocol_version = nil)
|
65
65
|
# Always include annotations now that we only support 2025+
|
66
66
|
_annotations
|
67
67
|
end
|
data/lib/action_mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -23,6 +23,20 @@ dependencies:
|
|
23
23
|
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 8.0.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: concurrent-ruby
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.3.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.3.1
|
26
40
|
- !ruby/object:Gem::Dependency
|
27
41
|
name: jsonrpc-rails
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -79,20 +93,6 @@ dependencies:
|
|
79
93
|
- - "~>"
|
80
94
|
- !ruby/object:Gem::Version
|
81
95
|
version: '2.6'
|
82
|
-
- !ruby/object:Gem::Dependency
|
83
|
-
name: concurrent-ruby
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
85
|
-
requirements:
|
86
|
-
- - ">="
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: 1.3.1
|
89
|
-
type: :runtime
|
90
|
-
prerelease: false
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
92
|
-
requirements:
|
93
|
-
- - ">="
|
94
|
-
- !ruby/object:Gem::Version
|
95
|
-
version: 1.3.1
|
96
96
|
description: It offers base classes and helpers for creating MCP applications, making
|
97
97
|
it easier to integrate your Ruby/Rails application with the MCP standard
|
98
98
|
email:
|
@@ -105,8 +105,7 @@ files:
|
|
105
105
|
- MIT-LICENSE
|
106
106
|
- README.md
|
107
107
|
- Rakefile
|
108
|
-
- app/controllers/action_mcp/
|
109
|
-
- app/controllers/action_mcp/unified_controller.rb
|
108
|
+
- app/controllers/action_mcp/application_controller.rb
|
110
109
|
- app/models/action_mcp.rb
|
111
110
|
- app/models/action_mcp/application_record.rb
|
112
111
|
- app/models/action_mcp/session.rb
|
@@ -1,79 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
class MCPController < ActionController::Metal
|
5
|
-
abstract!
|
6
|
-
ActionController::API.without_modules(:StrongParameters, :ParamsWrapper).each do |left|
|
7
|
-
include left
|
8
|
-
end
|
9
|
-
include Engine.routes.url_helpers
|
10
|
-
|
11
|
-
# Header name for MCP Session ID (as per 2025-03-26 spec)
|
12
|
-
MCP_SESSION_ID_HEADER = "Mcp-Session-Id"
|
13
|
-
|
14
|
-
# Provides the ActionMCP::Session for the current request.
|
15
|
-
# Handles finding existing sessions via header/param or initializing a new one.
|
16
|
-
# Specific controllers/handlers might need to enforce session ID presence based on context.
|
17
|
-
# @return [ActionMCP::Session] The session object (might be unsaved if new)
|
18
|
-
def mcp_session
|
19
|
-
@mcp_session ||= find_or_initialize_session
|
20
|
-
end
|
21
|
-
|
22
|
-
# Provides a unique key for caching or pub/sub based on the session ID.
|
23
|
-
# Ensures mcp_session is called first to establish the session ID.
|
24
|
-
# @return [String] The session key string.
|
25
|
-
def session_key
|
26
|
-
@session_key ||= "action_mcp-sessions-#{mcp_session.id}"
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
# Finds an existing session based on header or param, or initializes a new one.
|
32
|
-
# Note: This doesn't save the new session; that happens upon first use or explicitly.
|
33
|
-
def find_or_initialize_session
|
34
|
-
session_id = extract_session_id
|
35
|
-
if session_id
|
36
|
-
session = Session.find_by(id: session_id)
|
37
|
-
if session && session.protocol_version != self.class::REQUIRED_PROTOCOL_VERSION
|
38
|
-
# Update existing session to use 2025 protocol
|
39
|
-
session.update!(protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
|
40
|
-
end
|
41
|
-
session
|
42
|
-
else
|
43
|
-
# Create new session with 2025 protocol
|
44
|
-
Session.new(protocol_version: self.class::REQUIRED_PROTOCOL_VERSION)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Renders a 400 Bad Request response with a JSON-RPC-like error structure.
|
49
|
-
def render_bad_request(message = "Bad Request")
|
50
|
-
# Using -32600 for Invalid Request based on JSON-RPC spec
|
51
|
-
render json: { jsonrpc: "2.0", error: { code: -32_600, message: message } }
|
52
|
-
end
|
53
|
-
|
54
|
-
# Renders a 404 Not Found response with a JSON-RPC-like error structure.
|
55
|
-
def render_not_found(message = "Not Found")
|
56
|
-
# Using a custom code or a generic server error range code might be appropriate.
|
57
|
-
# Let's use -32001 for a generic server error.
|
58
|
-
render json: { jsonrpc: "2.0", error: { code: -32_001, message: message } }
|
59
|
-
end
|
60
|
-
|
61
|
-
# Renders a 405 Method Not Allowed response.
|
62
|
-
def render_method_not_allowed(message = "Method Not Allowed")
|
63
|
-
# Using -32601 Method not found from JSON-RPC spec seems applicable
|
64
|
-
render json: { jsonrpc: "2.0", error: { code: -32_601, message: message } }
|
65
|
-
end
|
66
|
-
|
67
|
-
# Renders a 406 Not Acceptable response.
|
68
|
-
def render_not_acceptable(message = "Not Acceptable")
|
69
|
-
# No direct JSON-RPC equivalent, using a generic server error code.
|
70
|
-
render json: { jsonrpc: "2.0", error: { code: -32_002, message: message } }
|
71
|
-
end
|
72
|
-
|
73
|
-
# Renders a 501 Not Implemented response.
|
74
|
-
def render_not_implemented(message = "Not Implemented")
|
75
|
-
# No direct JSON-RPC equivalent, using a generic server error code.
|
76
|
-
render json: { jsonrpc: "2.0", error: { code: -32_003, message: message } }
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|