actionmcp 0.60.1 → 0.70.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 +0 -18
- data/app/controllers/action_mcp/application_controller.rb +28 -13
- data/app/models/action_mcp/session/message.rb +12 -12
- data/app/models/action_mcp/session.rb +46 -21
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/capability.rb +2 -0
- data/lib/action_mcp/client/json_rpc_handler.rb +9 -9
- data/lib/action_mcp/configuration.rb +63 -7
- data/lib/action_mcp/engine.rb +26 -1
- data/lib/action_mcp/prompt.rb +14 -0
- data/lib/action_mcp/registry_base.rb +25 -4
- data/lib/action_mcp/resource_response.rb +110 -0
- data/lib/action_mcp/resource_template.rb +30 -2
- data/lib/action_mcp/server/capabilities.rb +3 -11
- data/lib/action_mcp/server/memory_session.rb +0 -1
- data/lib/action_mcp/server/prompts.rb +8 -1
- data/lib/action_mcp/server/resources.rb +8 -5
- data/lib/action_mcp/server/tools.rb +11 -2
- data/lib/action_mcp/server.rb +6 -3
- data/lib/action_mcp/tool.rb +14 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/tasks/action_mcp_tasks.rake +238 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b498d3bdd7cde670eef99ba3458e4af3808ce1eaf2cb29c369bdaa3048a9098
|
4
|
+
data.tar.gz: a3f0f8db133018b5f9a2ee822f35a59c0cbf79301be7b69c894ef4daa2f99472
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4880debf4726112a664348076862e92c92578c9d4cafd6a0ac923944e37d5c6929bab0b3779bcc322fd478b19f57635f629be76100e9b3254b4fd3895c3eff13
|
7
|
+
data.tar.gz: dba72df2677959588c73431c99cd7a3a3d176ac3fcbfea99d5ea2c53e2ff96aac228a5a46d5f3dc8c7b1f79a9f2cb1467d0a784236d613deeddb135ddcf300bd
|
data/README.md
CHANGED
@@ -237,30 +237,12 @@ module Tron
|
|
237
237
|
config.action_mcp.version = "1.2.3" # defaults to "0.0.1"
|
238
238
|
config.action_mcp.logging_enabled = true # defaults to true
|
239
239
|
config.action_mcp.logging_level = :info # defaults to :info, can be :debug, :info, :warn, :error, :fatal
|
240
|
-
config.action_mcp.vibed_ignore_version = false # defaults to false, set to true to ignore client protocol version mismatches
|
241
240
|
end
|
242
241
|
end
|
243
242
|
```
|
244
243
|
|
245
244
|
For dynamic versioning, consider adding the `rails_app_version` gem.
|
246
245
|
|
247
|
-
### Protocol Version Compatibility
|
248
|
-
|
249
|
-
By default, ActionMCP requires clients to use the exact protocol version supported by the server (currently "2025-03-26"). If the client specifies a different version during initialization, the request will be rejected with an error.
|
250
|
-
|
251
|
-
To support clients with incompatible protocol versions, you can enable the `vibed_ignore_version` option:
|
252
|
-
|
253
|
-
```ruby
|
254
|
-
# In config/application.rb or an initializer
|
255
|
-
Rails.application.config.action_mcp.vibed_ignore_version = true
|
256
|
-
```
|
257
|
-
|
258
|
-
When enabled, the server will ignore protocol version mismatches from clients and always use the latest supported version. This is useful for:
|
259
|
-
- Development environments with older client libraries
|
260
|
-
- Supporting clients that cannot be easily updated
|
261
|
-
- Situations where protocol differences are minor and known to be compatible
|
262
|
-
|
263
|
-
> **Note:** Using `vibed_ignore_version = true` in production is not recommended as it may lead to unexpected behavior if clients rely on specific protocol features that differ between versions.
|
264
246
|
|
265
247
|
### PubSub Configuration
|
266
248
|
|
@@ -12,6 +12,7 @@ module ActionMCP
|
|
12
12
|
include ActionController::Live
|
13
13
|
include ActionController::Instrumentation
|
14
14
|
|
15
|
+
|
15
16
|
# Provides the ActionMCP::Session for the current request.
|
16
17
|
# Handles finding existing sessions via header/param or initializing a new one.
|
17
18
|
# Specific controllers/handlers might need to enforce session ID presence based on context.
|
@@ -143,7 +144,7 @@ module ActionMCP
|
|
143
144
|
# @route POST /mcp
|
144
145
|
def create
|
145
146
|
unless post_accept_headers_valid?
|
146
|
-
id =
|
147
|
+
id = extract_jsonrpc_id_from_request
|
147
148
|
return render_not_acceptable(post_accept_headers_error_message, id)
|
148
149
|
end
|
149
150
|
|
@@ -157,8 +158,7 @@ module ActionMCP
|
|
157
158
|
session = mcp_session
|
158
159
|
|
159
160
|
# Validate MCP-Protocol-Version header for non-initialize requests
|
160
|
-
|
161
|
-
# return unless validate_protocol_version_header
|
161
|
+
return unless validate_protocol_version_header
|
162
162
|
|
163
163
|
unless is_initialize_request
|
164
164
|
if session_initially_missing
|
@@ -228,18 +228,26 @@ module ActionMCP
|
|
228
228
|
# Skip validation for initialize requests
|
229
229
|
return true if check_if_initialize_request(jsonrpc_params)
|
230
230
|
|
231
|
-
|
231
|
+
# Check for both case variations of the header (spec uses MCP-Protocol-Version)
|
232
|
+
header_version = request.headers["MCP-Protocol-Version"] || request.headers["mcp-protocol-version"]
|
232
233
|
session = mcp_session
|
233
234
|
|
234
|
-
# If header is missing, assume 2025-03-26 for backward compatibility
|
235
|
+
# If header is missing, assume 2025-03-26 for backward compatibility as per spec
|
235
236
|
if header_version.nil?
|
236
|
-
|
237
|
+
ActionMCP.logger.debug "MCP-Protocol-Version header missing, assuming 2025-03-26 for backward compatibility"
|
237
238
|
return true
|
238
239
|
end
|
239
240
|
|
241
|
+
# Handle array values (take the last one as per TypeScript SDK)
|
242
|
+
if header_version.is_a?(Array)
|
243
|
+
header_version = header_version.last
|
244
|
+
end
|
245
|
+
|
240
246
|
# Check if the header version is supported
|
241
247
|
unless ActionMCP::SUPPORTED_VERSIONS.include?(header_version)
|
242
|
-
|
248
|
+
supported_versions = ActionMCP::SUPPORTED_VERSIONS.join(", ")
|
249
|
+
ActionMCP.logger.warn "Unsupported MCP-Protocol-Version: #{header_version}. Supported versions: #{supported_versions}"
|
250
|
+
render_protocol_version_error("Unsupported MCP-Protocol-Version: #{header_version}. Supported versions: #{supported_versions}")
|
243
251
|
return false
|
244
252
|
end
|
245
253
|
|
@@ -247,12 +255,13 @@ module ActionMCP
|
|
247
255
|
if session && session.initialized?
|
248
256
|
negotiated_version = session.protocol_version
|
249
257
|
if header_version != negotiated_version
|
250
|
-
|
251
|
-
|
258
|
+
ActionMCP.logger.warn "MCP-Protocol-Version mismatch: header=#{header_version}, negotiated=#{negotiated_version}"
|
259
|
+
render_protocol_version_error("MCP-Protocol-Version header (#{header_version}) does not match negotiated version (#{negotiated_version})")
|
252
260
|
return false
|
253
261
|
end
|
254
262
|
end
|
255
263
|
|
264
|
+
ActionMCP.logger.debug "MCP-Protocol-Version header validation passed: #{header_version}"
|
256
265
|
true
|
257
266
|
end
|
258
267
|
|
@@ -260,12 +269,12 @@ module ActionMCP
|
|
260
269
|
# Note: This doesn't save the new session; that happens upon first use or explicitly.
|
261
270
|
def find_or_initialize_session
|
262
271
|
session_id = extract_session_id
|
272
|
+
session_store = ActionMCP::Server.session_store
|
273
|
+
|
263
274
|
if session_id
|
264
|
-
|
265
|
-
# Session protocol version is set during initialization and should not be overridden
|
266
|
-
session
|
275
|
+
session_store.load_session(session_id)
|
267
276
|
else
|
268
|
-
|
277
|
+
session_store.create_session(nil, protocol_version: ActionMCP::DEFAULT_PROTOCOL_VERSION)
|
269
278
|
end
|
270
279
|
end
|
271
280
|
|
@@ -415,6 +424,12 @@ module ActionMCP
|
|
415
424
|
render json: { jsonrpc: "2.0", id: id, error: { code: -32_600, message: message } }
|
416
425
|
end
|
417
426
|
|
427
|
+
# Renders a 400 Bad Request response for protocol version errors as per MCP spec
|
428
|
+
def render_protocol_version_error(message = "Protocol Version Error", id = nil)
|
429
|
+
id ||= extract_jsonrpc_id_from_request
|
430
|
+
render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }, status: :bad_request
|
431
|
+
end
|
432
|
+
|
418
433
|
# Renders a 404 Not Found response with a JSON-RPC-like error structure.
|
419
434
|
def render_not_found(message = "Not Found", id = nil)
|
420
435
|
id ||= extract_jsonrpc_id_from_request
|
@@ -4,17 +4,17 @@
|
|
4
4
|
#
|
5
5
|
# Table name: action_mcp_session_messages
|
6
6
|
#
|
7
|
-
# id
|
8
|
-
# direction(The message recipient)
|
9
|
-
# is_ping
|
10
|
-
# message_json
|
11
|
-
# message_type
|
12
|
-
# request_acknowledged
|
13
|
-
# request_cancelled
|
14
|
-
# created_at
|
15
|
-
# updated_at
|
16
|
-
# jsonrpc_id
|
17
|
-
# session_id
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# direction(The message recipient) :string default("client"), not null
|
9
|
+
# is_ping :boolean default(FALSE), not null
|
10
|
+
# message_json :json
|
11
|
+
# message_type :string not null
|
12
|
+
# request_acknowledged :boolean default(FALSE), not null
|
13
|
+
# request_cancelled :boolean default(FALSE), not null
|
14
|
+
# created_at :datetime not null
|
15
|
+
# updated_at :datetime not null
|
16
|
+
# jsonrpc_id :string
|
17
|
+
# session_id :string not null
|
18
18
|
#
|
19
19
|
# Indexes
|
20
20
|
#
|
@@ -22,7 +22,7 @@
|
|
22
22
|
#
|
23
23
|
# Foreign Keys
|
24
24
|
#
|
25
|
-
#
|
25
|
+
# fk_rails_... (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
|
26
26
|
#
|
27
27
|
module ActionMCP
|
28
28
|
class Session
|
@@ -75,9 +75,7 @@ module ActionMCP
|
|
75
75
|
before_create :set_server_info, if: -> { role == "server" }
|
76
76
|
before_create :set_server_capabilities, if: -> { role == "server" }
|
77
77
|
|
78
|
-
validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true
|
79
|
-
ActionMCP.configuration.vibed_ignore_version
|
80
|
-
}
|
78
|
+
validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true
|
81
79
|
|
82
80
|
def close!
|
83
81
|
dummy_callback = ->(*) { } # this callback seem broken
|
@@ -115,8 +113,6 @@ module ActionMCP
|
|
115
113
|
end
|
116
114
|
|
117
115
|
def set_protocol_version(version)
|
118
|
-
# If vibed_ignore_version is true, always use the latest supported version
|
119
|
-
version = PROTOCOL_VERSION if ActionMCP.configuration.vibed_ignore_version
|
120
116
|
update(protocol_version: version)
|
121
117
|
end
|
122
118
|
|
@@ -311,29 +307,58 @@ module ActionMCP
|
|
311
307
|
|
312
308
|
# Get registered items for this session
|
313
309
|
def registered_tools
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
310
|
+
# Special case: ['*'] means use all available tools dynamically
|
311
|
+
if tool_registry == [ "*" ]
|
312
|
+
# filtered_tools returns a RegistryScope with Item objects, need to extract the klass
|
313
|
+
ActionMCP.configuration.filtered_tools.map(&:klass)
|
314
|
+
else
|
315
|
+
(self.tool_registry || []).filter_map do |tool_name|
|
316
|
+
ActionMCP::ToolsRegistry.find(tool_name)
|
317
|
+
rescue StandardError
|
318
|
+
nil
|
319
|
+
end
|
318
320
|
end
|
319
321
|
end
|
320
322
|
|
321
323
|
def registered_prompts
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
324
|
+
if prompt_registry == [ "*" ]
|
325
|
+
# filtered_prompts returns a RegistryScope with Item objects, need to extract the klass
|
326
|
+
ActionMCP.configuration.filtered_prompts.map(&:klass)
|
327
|
+
else
|
328
|
+
(self.prompt_registry || []).filter_map do |prompt_name|
|
329
|
+
ActionMCP::PromptsRegistry.find(prompt_name)
|
330
|
+
rescue StandardError
|
331
|
+
nil
|
332
|
+
end
|
326
333
|
end
|
327
334
|
end
|
328
335
|
|
329
336
|
def registered_resource_templates
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
337
|
+
if resource_registry == [ "*" ]
|
338
|
+
# filtered_resources returns a RegistryScope with Item objects, need to extract the klass
|
339
|
+
ActionMCP.configuration.filtered_resources.map(&:klass)
|
340
|
+
else
|
341
|
+
(self.resource_registry || []).filter_map do |template_name|
|
342
|
+
ActionMCP::ResourceTemplatesRegistry.find(template_name)
|
343
|
+
rescue StandardError
|
344
|
+
nil
|
345
|
+
end
|
334
346
|
end
|
335
347
|
end
|
336
348
|
|
349
|
+
# Helper methods to check if using all capabilities
|
350
|
+
def uses_all_tools?
|
351
|
+
tool_registry == [ "*" ]
|
352
|
+
end
|
353
|
+
|
354
|
+
def uses_all_prompts?
|
355
|
+
prompt_registry == [ "*" ]
|
356
|
+
end
|
357
|
+
|
358
|
+
def uses_all_resources?
|
359
|
+
resource_registry == [ "*" ]
|
360
|
+
end
|
361
|
+
|
337
362
|
# OAuth Session Management
|
338
363
|
# Required by MCP 2025-03-26 specification for session binding
|
339
364
|
|
@@ -443,10 +468,10 @@ module ActionMCP
|
|
443
468
|
end
|
444
469
|
|
445
470
|
def initialize_registries
|
446
|
-
#
|
447
|
-
self.tool_registry =
|
448
|
-
self.prompt_registry =
|
449
|
-
self.resource_registry =
|
471
|
+
# Default to using all available capabilities with '*'
|
472
|
+
self.tool_registry = [ "*" ]
|
473
|
+
self.prompt_registry = [ "*" ]
|
474
|
+
self.resource_registry = [ "*" ]
|
450
475
|
end
|
451
476
|
|
452
477
|
def normalize_name(class_or_name, type)
|
@@ -35,12 +35,12 @@ module ActionMCP
|
|
35
35
|
|
36
36
|
def handle_notification(notification)
|
37
37
|
# Handle server notifications to client
|
38
|
-
|
38
|
+
client.log_debug("Received notification: #{notification.method}")
|
39
39
|
end
|
40
40
|
|
41
41
|
def handle_response(response)
|
42
42
|
# Handle server responses to client requests
|
43
|
-
|
43
|
+
client.log_debug("Received response: #{response.id} - #{response.result ? 'success' : 'error'}")
|
44
44
|
end
|
45
45
|
|
46
46
|
protected
|
@@ -60,7 +60,7 @@ module ActionMCP
|
|
60
60
|
else
|
61
61
|
common_result = handle_common_methods(rpc_method, id, params)
|
62
62
|
if common_result.nil?
|
63
|
-
|
63
|
+
client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}")
|
64
64
|
end
|
65
65
|
end
|
66
66
|
end
|
@@ -94,19 +94,19 @@ module ActionMCP
|
|
94
94
|
def process_notifications(rpc_method, params)
|
95
95
|
case rpc_method
|
96
96
|
when "notifications/resources/updated" # Resource update notification
|
97
|
-
|
97
|
+
client.log_debug("Resource #{params['uri']} was updated")
|
98
98
|
# Handle resource update notification
|
99
99
|
# TODO: fetch updated resource or mark it as stale
|
100
100
|
when "notifications/tools/list_changed" # Tool list change notification
|
101
|
-
|
101
|
+
client.log_debug("Tool list has changed")
|
102
102
|
# Handle tool list change notification
|
103
103
|
# TODO: fetch new tools or mark them as stale
|
104
104
|
when "notifications/prompts/list_changed" # Prompt list change notification
|
105
|
-
|
105
|
+
client.log_debug("Prompt list has changed")
|
106
106
|
# Handle prompt list change notification
|
107
107
|
# TODO: fetch new prompts or mark them as stale
|
108
108
|
when "notifications/resources/list_changed" # Resource list change notification
|
109
|
-
|
109
|
+
client.log_debug("Resource list has changed")
|
110
110
|
# Handle resource list change notification
|
111
111
|
# TODO: fetch new resources or mark them as stale
|
112
112
|
else
|
@@ -153,12 +153,12 @@ module ActionMCP
|
|
153
153
|
return true
|
154
154
|
end
|
155
155
|
|
156
|
-
|
156
|
+
client.log_warn("Unknown response: #{id} #{result}")
|
157
157
|
end
|
158
158
|
|
159
159
|
def process_error(id, error)
|
160
160
|
# Do something ?
|
161
|
-
|
161
|
+
client.log_error("Unknown error: #{id} #{error}")
|
162
162
|
end
|
163
163
|
|
164
164
|
def handle_initialize_response(request_id, result)
|
@@ -33,8 +33,6 @@ module ActionMCP
|
|
33
33
|
:sse_heartbeat_interval,
|
34
34
|
:post_response_preference, # :json or :sse
|
35
35
|
:protocol_version,
|
36
|
-
# --- VibedIgnoreVersion Option ---
|
37
|
-
:vibed_ignore_version,
|
38
36
|
# --- SSE Resumability Options ---
|
39
37
|
:sse_event_retention_period,
|
40
38
|
:max_stored_sse_events,
|
@@ -68,7 +66,6 @@ module ActionMCP
|
|
68
66
|
@sse_heartbeat_interval = 30
|
69
67
|
@post_response_preference = :json
|
70
68
|
@protocol_version = "2025-03-26" # Default to legacy for backwards compatibility
|
71
|
-
@vibed_ignore_version = false
|
72
69
|
|
73
70
|
# Resumability defaults
|
74
71
|
@sse_event_retention_period = 15.minutes
|
@@ -178,15 +175,25 @@ module ActionMCP
|
|
178
175
|
# Returns capabilities based on active profile
|
179
176
|
def capabilities
|
180
177
|
capabilities = {}
|
178
|
+
profile = @profiles[active_profile]
|
181
179
|
|
182
|
-
#
|
183
|
-
|
180
|
+
# Check profile configuration instead of registry contents
|
181
|
+
# If profile includes tools (either "all" or specific tools), advertise tools capability
|
182
|
+
if profile && profile[:tools] && profile[:tools].any?
|
183
|
+
capabilities[:tools] = { listChanged: @list_changed }
|
184
|
+
end
|
184
185
|
|
185
|
-
|
186
|
+
# If profile includes prompts, advertise prompts capability
|
187
|
+
if profile && profile[:prompts] && profile[:prompts].any?
|
188
|
+
capabilities[:prompts] = { listChanged: @list_changed }
|
189
|
+
end
|
186
190
|
|
187
191
|
capabilities[:logging] = {} if @logging_enabled
|
188
192
|
|
189
|
-
|
193
|
+
# If profile includes resources, advertise resources capability
|
194
|
+
if profile && profile[:resources] && profile[:resources].any?
|
195
|
+
capabilities[:resources] = { subscribe: @resources_subscribe }
|
196
|
+
end
|
190
197
|
|
191
198
|
capabilities[:elicitation] = {} if @elicitation_enabled
|
192
199
|
|
@@ -214,6 +221,20 @@ module ActionMCP
|
|
214
221
|
@resources_subscribe = options[:resources_subscribe] unless options[:resources_subscribe].nil?
|
215
222
|
end
|
216
223
|
|
224
|
+
def eager_load_if_needed
|
225
|
+
profile = @profiles[active_profile]
|
226
|
+
return unless profile
|
227
|
+
|
228
|
+
# Check if any component type includes "all"
|
229
|
+
needs_eager_load = profile[:tools]&.include?("all") ||
|
230
|
+
profile[:prompts]&.include?("all") ||
|
231
|
+
profile[:resources]&.include?("all")
|
232
|
+
|
233
|
+
if needs_eager_load
|
234
|
+
ensure_mcp_components_loaded
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
217
238
|
private
|
218
239
|
|
219
240
|
def default_profiles
|
@@ -303,6 +324,41 @@ module ActionMCP
|
|
303
324
|
rescue LoadError
|
304
325
|
false
|
305
326
|
end
|
327
|
+
|
328
|
+
def ensure_mcp_components_loaded
|
329
|
+
# Only load if we haven't loaded yet - but in development, always reload
|
330
|
+
return if @mcp_components_loaded && !Rails.env.development?
|
331
|
+
|
332
|
+
# Use Zeitwerk eager loading if available (in to_prepare phase)
|
333
|
+
mcp_path = Rails.root.join("app/mcp")
|
334
|
+
if mcp_path.exist? && Rails.autoloaders.main.respond_to?(:eager_load_dir)
|
335
|
+
# This will trigger all inherited hooks properly
|
336
|
+
Rails.autoloaders.main.eager_load_dir(mcp_path)
|
337
|
+
elsif mcp_path.exist?
|
338
|
+
# Fallback for initialization phase - use require_dependency
|
339
|
+
# Load base classes first in specific order
|
340
|
+
base_files = [
|
341
|
+
mcp_path.join("application_gateway.rb"),
|
342
|
+
mcp_path.join("tools/application_mcp_tool.rb"),
|
343
|
+
mcp_path.join("prompts/application_mcp_prompt.rb"),
|
344
|
+
mcp_path.join("resource_templates/application_mcp_res_template.rb"),
|
345
|
+
# Load ArithmeticTool before other tools that inherit from it
|
346
|
+
mcp_path.join("tools/arithmetic_tool.rb")
|
347
|
+
]
|
348
|
+
|
349
|
+
base_files.each do |file|
|
350
|
+
require_dependency file.to_s if file.exist?
|
351
|
+
end
|
352
|
+
|
353
|
+
# Then load all other files
|
354
|
+
Dir.glob(mcp_path.join("**/*.rb")).sort.each do |file|
|
355
|
+
# Skip base classes we already loaded
|
356
|
+
next if base_files.any? { |base| file == base.to_s }
|
357
|
+
require_dependency file
|
358
|
+
end
|
359
|
+
end
|
360
|
+
@mcp_components_loaded = true unless Rails.env.development?
|
361
|
+
end
|
306
362
|
end
|
307
363
|
|
308
364
|
class << self
|
data/lib/action_mcp/engine.rb
CHANGED
@@ -18,8 +18,33 @@ module ActionMCP
|
|
18
18
|
# Provide a configuration namespace for ActionMCP
|
19
19
|
config.action_mcp = ActionMCP.configuration
|
20
20
|
|
21
|
+
# Create the ActiveSupport load hooks
|
22
|
+
ActiveSupport.on_load(:action_mcp_tool) do
|
23
|
+
# Register the tool when it's loaded
|
24
|
+
ActionMCP::ToolsRegistry.register(self) unless abstract?
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveSupport.on_load(:action_mcp_prompt) do
|
28
|
+
# Register the prompt when it's loaded
|
29
|
+
ActionMCP::PromptsRegistry.register(self) unless abstract?
|
30
|
+
end
|
31
|
+
|
32
|
+
ActiveSupport.on_load(:action_mcp_resource_template) do
|
33
|
+
# Register the resource template when it's loaded
|
34
|
+
ActionMCP::ResourceTemplatesRegistry.register(self) unless abstract?
|
35
|
+
end
|
36
|
+
|
21
37
|
config.to_prepare do
|
22
|
-
|
38
|
+
# Only clear registries if we're in development mode
|
39
|
+
if Rails.env.development?
|
40
|
+
ActionMCP::ResourceTemplate.registered_templates.clear
|
41
|
+
ActionMCP::ToolsRegistry.clear!
|
42
|
+
ActionMCP::PromptsRegistry.clear!
|
43
|
+
end
|
44
|
+
|
45
|
+
# Eager load MCP components if profile includes "all"
|
46
|
+
# This runs after Zeitwerk is fully set up
|
47
|
+
ActionMCP.configuration.eager_load_if_needed
|
23
48
|
end
|
24
49
|
|
25
50
|
config.middleware.use JSONRPC_Rails::Middleware::Validator, [ "/" ]
|
data/lib/action_mcp/prompt.rb
CHANGED
@@ -27,6 +27,7 @@ module ActionMCP
|
|
27
27
|
#
|
28
28
|
# @return [String] The default prompt name.
|
29
29
|
def self.default_prompt_name
|
30
|
+
return "" if name.nil?
|
30
31
|
name.demodulize.underscore.sub(/_prompt$/, "")
|
31
32
|
end
|
32
33
|
|
@@ -37,6 +38,19 @@ module ActionMCP
|
|
37
38
|
:prompt
|
38
39
|
end
|
39
40
|
|
41
|
+
def unregister_from_registry
|
42
|
+
ActionMCP::PromptsRegistry.unregister(self) if ActionMCP::PromptsRegistry.items.values.include?(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Hook called when a class inherits from Prompt
|
46
|
+
def inherited(subclass)
|
47
|
+
super
|
48
|
+
# Run the ActiveSupport load hook when a prompt is defined
|
49
|
+
subclass.class_eval do
|
50
|
+
ActiveSupport.run_load_hooks(:action_mcp_prompt, subclass)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
40
54
|
# Sets or retrieves the _meta field
|
41
55
|
def meta(data = nil)
|
42
56
|
if data
|
@@ -11,11 +11,25 @@ module ActionMCP
|
|
11
11
|
#
|
12
12
|
# @return [Hash] A hash of registered items.
|
13
13
|
def items
|
14
|
-
@items
|
15
|
-
|
14
|
+
@items ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Register an item explicitly
|
18
|
+
#
|
19
|
+
# @param klass [Class] The class to register
|
20
|
+
# @return [void]
|
21
|
+
def register(klass)
|
22
|
+
return if klass.abstract?
|
23
|
+
|
24
|
+
items[klass.capability_name] = klass
|
25
|
+
end
|
16
26
|
|
17
|
-
|
18
|
-
|
27
|
+
# Unregister an item
|
28
|
+
#
|
29
|
+
# @param klass [Class] The class to unregister
|
30
|
+
# @return [void]
|
31
|
+
def unregister(klass)
|
32
|
+
items.delete(klass.capability_name)
|
19
33
|
end
|
20
34
|
|
21
35
|
# Retrieve an item by name.
|
@@ -44,6 +58,13 @@ module ActionMCP
|
|
44
58
|
RegistryScope.new(items)
|
45
59
|
end
|
46
60
|
|
61
|
+
# Clears the registry cache.
|
62
|
+
#
|
63
|
+
# @return [void]
|
64
|
+
def clear!
|
65
|
+
@items = nil
|
66
|
+
end
|
67
|
+
|
47
68
|
private
|
48
69
|
|
49
70
|
# Helper to determine if an item is abstract.
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Manages resource resolution results and errors for ResourceTemplate operations.
|
5
|
+
# Unlike ToolResponse, ResourceResponse only uses JSON-RPC protocol errors per MCP spec.
|
6
|
+
class ResourceResponse < BaseResponse
|
7
|
+
attr_reader :contents
|
8
|
+
|
9
|
+
delegate :empty?, :size, :each, :find, :map, to: :contents
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
@contents = []
|
14
|
+
end
|
15
|
+
|
16
|
+
# Add a resource content item to the response
|
17
|
+
def add_content(content)
|
18
|
+
@contents << content
|
19
|
+
content # Return the content for chaining
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add multiple content items
|
23
|
+
def add_contents(contents_array)
|
24
|
+
@contents.concat(contents_array)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Mark as error with ResourceTemplate-specific error types
|
29
|
+
def mark_as_template_not_found!(uri)
|
30
|
+
mark_as_error!(
|
31
|
+
:invalid_params,
|
32
|
+
message: "Resource template not found for URI: #{uri}",
|
33
|
+
data: { uri: uri, error_type: "TEMPLATE_NOT_FOUND" }
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def mark_as_parameter_validation_failed!(missing_params, uri)
|
38
|
+
mark_as_error!(
|
39
|
+
:invalid_params,
|
40
|
+
message: "Required parameters missing: #{missing_params.join(', ')}",
|
41
|
+
data: {
|
42
|
+
uri: uri,
|
43
|
+
missing_parameters: missing_params,
|
44
|
+
error_type: "PARAMETER_VALIDATION_FAILED"
|
45
|
+
}
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def mark_as_resolution_failed!(uri, reason = nil)
|
50
|
+
message = "Resource resolution failed for URI: #{uri}"
|
51
|
+
message += " - #{reason}" if reason
|
52
|
+
|
53
|
+
mark_as_error!(
|
54
|
+
:internal_error,
|
55
|
+
message: message,
|
56
|
+
data: {
|
57
|
+
uri: uri,
|
58
|
+
reason: reason,
|
59
|
+
error_type: "RESOLUTION_FAILED"
|
60
|
+
}
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def mark_as_callback_aborted!(uri)
|
65
|
+
mark_as_error!(
|
66
|
+
:internal_error,
|
67
|
+
message: "Resource resolution was aborted by callback chain",
|
68
|
+
data: {
|
69
|
+
uri: uri,
|
70
|
+
error_type: "CALLBACK_ABORTED"
|
71
|
+
}
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def mark_as_not_found!(uri)
|
76
|
+
# Use method_not_found for resource not found (closest standard JSON-RPC error)
|
77
|
+
mark_as_error!(
|
78
|
+
:method_not_found, # -32601 - closest standard error for "not found"
|
79
|
+
message: "Resource not found",
|
80
|
+
data: { uri: uri }
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Implementation of build_success_hash for ResourceResponse
|
85
|
+
def build_success_hash
|
86
|
+
{
|
87
|
+
contents: @contents.map(&:to_h)
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# Implementation of compare_with_same_class for ResourceResponse
|
92
|
+
def compare_with_same_class(other)
|
93
|
+
contents == other.contents && is_error == other.is_error
|
94
|
+
end
|
95
|
+
|
96
|
+
# Implementation of hash_components for ResourceResponse
|
97
|
+
def hash_components
|
98
|
+
[ contents, is_error ]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Pretty print for better debugging
|
102
|
+
def inspect
|
103
|
+
if is_error
|
104
|
+
"#<#{self.class.name} error: #{@error_message}>"
|
105
|
+
else
|
106
|
+
"#<#{self.class.name} contents: #{contents.size} items>"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -26,6 +26,8 @@ module ActionMCP
|
|
26
26
|
|
27
27
|
def abstract!
|
28
28
|
@abstract = true
|
29
|
+
# Unregister from the appropriate registry if already registered
|
30
|
+
ActionMCP::ResourceTemplatesRegistry.unregister(self) if ActionMCP::ResourceTemplatesRegistry.items.values.include?(self)
|
29
31
|
end
|
30
32
|
|
31
33
|
def inherited(subclass)
|
@@ -33,6 +35,11 @@ module ActionMCP
|
|
33
35
|
subclass.instance_variable_set(:@abstract, false)
|
34
36
|
# Create a copy of validation requirements for subclasses
|
35
37
|
subclass.instance_variable_set(:@required_parameters, [])
|
38
|
+
|
39
|
+
# Run the ActiveSupport load hook when a resource template is defined
|
40
|
+
subclass.class_eval do
|
41
|
+
ActiveSupport.run_load_hooks(:action_mcp_resource_template, subclass)
|
42
|
+
end
|
36
43
|
end
|
37
44
|
|
38
45
|
# Track required parameters for validation
|
@@ -109,6 +116,7 @@ module ActionMCP
|
|
109
116
|
end
|
110
117
|
|
111
118
|
def capability_name
|
119
|
+
return "" if name.nil?
|
112
120
|
@capability_name ||= name.demodulize.underscore.sub(/_template$/, "")
|
113
121
|
end
|
114
122
|
|
@@ -245,9 +253,29 @@ module ActionMCP
|
|
245
253
|
end
|
246
254
|
|
247
255
|
def call
|
248
|
-
|
249
|
-
|
256
|
+
@response = ResourceResponse.new
|
257
|
+
|
258
|
+
# Validate parameters first
|
259
|
+
unless valid?
|
260
|
+
missing_params = errors.full_messages
|
261
|
+
@response.mark_as_parameter_validation_failed!(missing_params, "template://#{self.class.name}")
|
262
|
+
return @response
|
263
|
+
end
|
264
|
+
|
265
|
+
begin
|
266
|
+
run_callbacks :resolve do
|
267
|
+
result = resolve
|
268
|
+
if result.nil?
|
269
|
+
@response.mark_as_not_found!("template://#{self.class.name}")
|
270
|
+
else
|
271
|
+
@response.add_content(result)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
rescue StandardError => e
|
275
|
+
@response.mark_as_resolution_failed!("template://#{self.class.name}", e.message)
|
250
276
|
end
|
277
|
+
|
278
|
+
@response
|
251
279
|
end
|
252
280
|
end
|
253
281
|
end
|
@@ -18,7 +18,7 @@ module ActionMCP
|
|
18
18
|
unless client_protocol_version.is_a?(String) && client_protocol_version.present?
|
19
19
|
return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
|
20
20
|
end
|
21
|
-
unless ActionMCP
|
21
|
+
unless ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
|
22
22
|
error_message = "Unsupported protocol version. Client requested '#{client_protocol_version}' but server supports #{ActionMCP::SUPPORTED_VERSIONS.join(', ')}"
|
23
23
|
error_data = {
|
24
24
|
supported: ActionMCP::SUPPORTED_VERSIONS,
|
@@ -44,11 +44,7 @@ module ActionMCP
|
|
44
44
|
|
45
45
|
# Return existing session info
|
46
46
|
capabilities_payload = existing_session.server_capabilities_payload
|
47
|
-
capabilities_payload[:protocolVersion] =
|
48
|
-
ActionMCP::LATEST_VERSION
|
49
|
-
else
|
50
|
-
client_protocol_version
|
51
|
-
end
|
47
|
+
capabilities_payload[:protocolVersion] = client_protocol_version
|
52
48
|
return send_jsonrpc_response(request_id, result: capabilities_payload)
|
53
49
|
else
|
54
50
|
Rails.logger.warn("Session #{session_id} not found or not initialized, creating new session")
|
@@ -65,11 +61,7 @@ module ActionMCP
|
|
65
61
|
end
|
66
62
|
|
67
63
|
capabilities_payload = session.server_capabilities_payload
|
68
|
-
capabilities_payload[:protocolVersion] =
|
69
|
-
ActionMCP::LATEST_VERSION
|
70
|
-
else
|
71
|
-
client_protocol_version
|
72
|
-
end
|
64
|
+
capabilities_payload[:protocolVersion] = client_protocol_version
|
73
65
|
|
74
66
|
send_jsonrpc_response(request_id, result: capabilities_payload)
|
75
67
|
end
|
@@ -18,7 +18,14 @@ module ActionMCP
|
|
18
18
|
prompt = prompt_class.new(params)
|
19
19
|
prompt.with_context({ session: session })
|
20
20
|
|
21
|
-
|
21
|
+
# Wrap prompt execution with Rails reloader for development
|
22
|
+
result = if Rails.env.development? && defined?(Rails.application.reloader)
|
23
|
+
Rails.application.reloader.wrap do
|
24
|
+
prompt.call
|
25
|
+
end
|
26
|
+
else
|
27
|
+
prompt.call
|
28
|
+
end
|
22
29
|
|
23
30
|
if result.is_error
|
24
31
|
send_jsonrpc_response(request_id, error: result)
|
@@ -50,12 +50,15 @@ module ActionMCP
|
|
50
50
|
record = template.process(params[:uri])
|
51
51
|
record.with_context({ session: session })
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
53
|
+
response = record.call
|
54
|
+
|
55
|
+
if response.error?
|
56
|
+
# Convert ResourceResponse errors to JSON-RPC errors
|
57
|
+
error_info = response.to_h
|
58
|
+
send_jsonrpc_error(id, error_info[:code], error_info[:message], error_info[:data])
|
57
59
|
else
|
58
|
-
|
60
|
+
# Handle successful response - ResourceResponse.contents is already an array
|
61
|
+
send_jsonrpc_response(id, result: { contents: response.contents.map(&:to_h) })
|
59
62
|
end
|
60
63
|
else
|
61
64
|
send_jsonrpc_error(id, :invalid_params, "Invalid resource URI")
|
@@ -18,7 +18,9 @@ module ActionMCP
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# Use session's registered tools instead of global registry
|
21
|
-
|
21
|
+
registered_tools = session.registered_tools
|
22
|
+
|
23
|
+
tools = registered_tools.map do |tool_class|
|
22
24
|
tool_class.to_h(protocol_version: protocol_version)
|
23
25
|
end
|
24
26
|
|
@@ -52,7 +54,14 @@ module ActionMCP
|
|
52
54
|
}
|
53
55
|
})
|
54
56
|
|
55
|
-
|
57
|
+
# Wrap tool execution with Rails reloader for development
|
58
|
+
result = if Rails.env.development? && defined?(Rails.application.reloader)
|
59
|
+
Rails.application.reloader.wrap do
|
60
|
+
tool.call
|
61
|
+
end
|
62
|
+
else
|
63
|
+
tool.call
|
64
|
+
end
|
56
65
|
|
57
66
|
if result.is_error
|
58
67
|
# Convert ToolResponse error to proper JSON-RPC error format
|
data/lib/action_mcp/server.rb
CHANGED
@@ -30,9 +30,12 @@ module ActionMCP
|
|
30
30
|
|
31
31
|
# Access the session store
|
32
32
|
def session_store
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
current_type = ActionMCP.configuration.server_session_store_type
|
34
|
+
if @session_store.nil? || @session_store_type != current_type
|
35
|
+
@session_store_type = current_type
|
36
|
+
@session_store = SessionStoreFactory.create(current_type)
|
37
|
+
end
|
38
|
+
@session_store
|
36
39
|
end
|
37
40
|
|
38
41
|
# Available pubsub adapter types
|
data/lib/action_mcp/tool.rb
CHANGED
@@ -43,6 +43,7 @@ module ActionMCP
|
|
43
43
|
#
|
44
44
|
# @return [String] The default tool name.
|
45
45
|
def self.default_tool_name
|
46
|
+
return "" if name.nil?
|
46
47
|
name.demodulize.underscore.sub(/_tool$/, "")
|
47
48
|
end
|
48
49
|
|
@@ -53,6 +54,19 @@ module ActionMCP
|
|
53
54
|
:tool
|
54
55
|
end
|
55
56
|
|
57
|
+
def unregister_from_registry
|
58
|
+
ActionMCP::ToolsRegistry.unregister(self) if ActionMCP::ToolsRegistry.items.values.include?(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Hook called when a class inherits from Tool
|
62
|
+
def inherited(subclass)
|
63
|
+
super
|
64
|
+
# Run the ActiveSupport load hook when a tool is defined
|
65
|
+
subclass.class_eval do
|
66
|
+
ActiveSupport.run_load_hooks(:action_mcp_tool, subclass)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
56
70
|
def annotate(key, value)
|
57
71
|
self._annotations = _annotations.merge(key.to_s => value)
|
58
72
|
end
|
data/lib/action_mcp/version.rb
CHANGED
@@ -124,4 +124,242 @@ namespace :action_mcp do
|
|
124
124
|
task list: %i[list_tools list_prompts list_resources list_profiles] do
|
125
125
|
# This task lists all tools, prompts, resources and profiles
|
126
126
|
end
|
127
|
+
|
128
|
+
# bin/rails action_mcp:info
|
129
|
+
# bin/rails action_mcp:info[test]
|
130
|
+
desc "Display ActionMCP configuration for current or specified environment"
|
131
|
+
task :info, [ :env ] => :environment do |_t, args|
|
132
|
+
env = args[:env] || Rails.env
|
133
|
+
|
134
|
+
# Load configuration for the specified environment
|
135
|
+
original_env = Rails.env
|
136
|
+
Rails.env = env.to_s
|
137
|
+
|
138
|
+
# Reload configuration to get the environment-specific settings
|
139
|
+
config = ActionMCP::Configuration.new
|
140
|
+
config.load_profiles
|
141
|
+
|
142
|
+
puts "\e[35mActionMCP Configuration (#{env})\e[0m"
|
143
|
+
puts "\e[35m#{'=' * (25 + env.length)}\e[0m"
|
144
|
+
|
145
|
+
# Basic Information
|
146
|
+
puts "\n\e[36mBasic Information:\e[0m"
|
147
|
+
puts " Name: #{config.name}"
|
148
|
+
puts " Version: #{config.version}"
|
149
|
+
puts " Protocol Version: #{config.protocol_version}"
|
150
|
+
puts " Active Profile: #{config.active_profile}"
|
151
|
+
|
152
|
+
# Session Storage
|
153
|
+
puts "\n\e[36mSession Storage:\e[0m"
|
154
|
+
puts " Session Store Type: #{config.session_store_type}"
|
155
|
+
puts " Client Session Store: #{config.client_session_store_type || 'default'}"
|
156
|
+
puts " Server Session Store: #{config.server_session_store_type || 'default'}"
|
157
|
+
|
158
|
+
# Transport Configuration
|
159
|
+
puts "\n\e[36mTransport Configuration:\e[0m"
|
160
|
+
puts " SSE Heartbeat Interval: #{config.sse_heartbeat_interval}s"
|
161
|
+
puts " Post Response Preference: #{config.post_response_preference}"
|
162
|
+
puts " SSE Event Retention Period: #{config.sse_event_retention_period}"
|
163
|
+
puts " Max Stored SSE Events: #{config.max_stored_sse_events}"
|
164
|
+
|
165
|
+
# Pub/Sub Adapter
|
166
|
+
puts "\n\e[36mPub/Sub Adapter:\e[0m"
|
167
|
+
puts " Adapter: #{config.adapter || 'not configured'}"
|
168
|
+
if config.adapter
|
169
|
+
puts " Polling Interval: #{config.polling_interval}" if config.polling_interval
|
170
|
+
puts " Min Threads: #{config.min_threads}" if config.min_threads
|
171
|
+
puts " Max Threads: #{config.max_threads}" if config.max_threads
|
172
|
+
puts " Max Queue: #{config.max_queue}" if config.max_queue
|
173
|
+
end
|
174
|
+
|
175
|
+
# Authentication
|
176
|
+
puts "\n\e[36mAuthentication:\e[0m"
|
177
|
+
puts " Methods: #{config.authentication_methods.join(', ')}"
|
178
|
+
if config.oauth_config && config.oauth_config.any?
|
179
|
+
puts " OAuth Provider: #{config.oauth_config['provider']}"
|
180
|
+
puts " OAuth Scopes: #{config.oauth_config['scopes_supported']&.join(', ')}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Logging
|
184
|
+
puts "\n\e[36mLogging:\e[0m"
|
185
|
+
puts " Logging Enabled: #{config.logging_enabled}"
|
186
|
+
puts " Logging Level: #{config.logging_level}"
|
187
|
+
|
188
|
+
# Gateway
|
189
|
+
puts "\n\e[36mGateway:\e[0m"
|
190
|
+
puts " Gateway Class: #{config.gateway_class}"
|
191
|
+
|
192
|
+
# Capabilities
|
193
|
+
puts "\n\e[36mEnabled Capabilities:\e[0m"
|
194
|
+
capabilities = config.capabilities
|
195
|
+
if capabilities.any?
|
196
|
+
capabilities.each do |cap_name, cap_config|
|
197
|
+
puts " #{cap_name}: #{cap_config.inspect}"
|
198
|
+
end
|
199
|
+
else
|
200
|
+
puts " None"
|
201
|
+
end
|
202
|
+
|
203
|
+
# Available Profiles
|
204
|
+
puts "\n\e[36mAvailable Profiles:\e[0m"
|
205
|
+
config.profiles.each_key do |profile_name|
|
206
|
+
puts " - #{profile_name}"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Restore original environment
|
210
|
+
Rails.env = original_env
|
211
|
+
|
212
|
+
puts "\n"
|
213
|
+
end
|
214
|
+
|
215
|
+
# bin/rails action_mcp:stats
|
216
|
+
desc "Display ActionMCP session and database statistics"
|
217
|
+
task stats: :environment do
|
218
|
+
puts "\e[35mActionMCP Statistics\e[0m"
|
219
|
+
puts "\e[35m===================\e[0m"
|
220
|
+
|
221
|
+
# Debug database connection
|
222
|
+
puts "\n\e[36mDatabase Debug:\e[0m"
|
223
|
+
puts " Rails Environment: #{Rails.env}"
|
224
|
+
puts " Rails Root: #{Rails.root}"
|
225
|
+
puts " Database Config: #{ActionMCP::ApplicationRecord.connection_db_config.configuration_hash.inspect}"
|
226
|
+
if ActionMCP::ApplicationRecord.connection.adapter_name.downcase.include?("sqlite")
|
227
|
+
db_path = ActionMCP::ApplicationRecord.connection_db_config.database
|
228
|
+
puts " Database Path: #{db_path}"
|
229
|
+
puts " Database Exists?: #{File.exist?(db_path)}" if db_path
|
230
|
+
end
|
231
|
+
|
232
|
+
# Session Statistics
|
233
|
+
puts "\n\e[36mSession Statistics:\e[0m"
|
234
|
+
|
235
|
+
begin
|
236
|
+
total_sessions = ActionMCP::Session.count
|
237
|
+
puts " Total Sessions: #{total_sessions}"
|
238
|
+
|
239
|
+
if total_sessions > 0
|
240
|
+
# Sessions by status
|
241
|
+
sessions_by_status = ActionMCP::Session.group(:status).count
|
242
|
+
puts " Sessions by Status:"
|
243
|
+
sessions_by_status.each do |status, count|
|
244
|
+
puts " #{status}: #{count}"
|
245
|
+
end
|
246
|
+
|
247
|
+
# Sessions by protocol version
|
248
|
+
sessions_by_protocol = ActionMCP::Session.group(:protocol_version).count
|
249
|
+
puts " Sessions by Protocol Version:"
|
250
|
+
sessions_by_protocol.each do |version, count|
|
251
|
+
puts " #{version}: #{count}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# Active sessions (initialized and not ended)
|
255
|
+
active_sessions = ActionMCP::Session.where(status: "initialized", ended_at: nil).count
|
256
|
+
puts " Active Sessions: #{active_sessions}"
|
257
|
+
|
258
|
+
# Recent activity
|
259
|
+
recent_sessions = ActionMCP::Session.where("created_at > ?", 1.hour.ago).count
|
260
|
+
puts " Sessions Created (Last Hour): #{recent_sessions}"
|
261
|
+
|
262
|
+
# Session with most messages
|
263
|
+
if ActionMCP::Session.maximum(:messages_count)
|
264
|
+
busiest_session = ActionMCP::Session.order(messages_count: :desc).first
|
265
|
+
puts " Most Active Session: #{busiest_session.id} (#{busiest_session.messages_count} messages)"
|
266
|
+
end
|
267
|
+
|
268
|
+
# Average messages per session
|
269
|
+
avg_messages = ActionMCP::Session.average(:messages_count).to_f.round(2)
|
270
|
+
puts " Average Messages per Session: #{avg_messages}"
|
271
|
+
end
|
272
|
+
rescue StandardError => e
|
273
|
+
puts " Error accessing session data: #{e.message}"
|
274
|
+
puts " (Session store might be using volatile storage)"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Message Statistics (if messages table exists)
|
278
|
+
puts "\n\e[36mMessage Statistics:\e[0m"
|
279
|
+
|
280
|
+
begin
|
281
|
+
if ActionMCP::ApplicationRecord.connection.table_exists?("action_mcp_session_messages")
|
282
|
+
total_messages = ActionMCP::Session::Message.count
|
283
|
+
puts " Total Messages: #{total_messages}"
|
284
|
+
|
285
|
+
if total_messages > 0
|
286
|
+
# Messages by direction
|
287
|
+
messages_by_direction = ActionMCP::Session::Message.group(:direction).count
|
288
|
+
puts " Messages by Direction:"
|
289
|
+
messages_by_direction.each do |direction, count|
|
290
|
+
puts " #{direction}: #{count}"
|
291
|
+
end
|
292
|
+
|
293
|
+
# Messages by type
|
294
|
+
messages_by_type = ActionMCP::Session::Message.group(:message_type).count.sort_by { |_type, count| -count }.first(10)
|
295
|
+
puts " Top Message Types:"
|
296
|
+
messages_by_type.each do |type, count|
|
297
|
+
puts " #{type}: #{count}"
|
298
|
+
end
|
299
|
+
|
300
|
+
# Recent messages
|
301
|
+
recent_messages = ActionMCP::Session::Message.where("created_at > ?", 1.hour.ago).count
|
302
|
+
puts " Messages (Last Hour): #{recent_messages}"
|
303
|
+
end
|
304
|
+
else
|
305
|
+
puts " Message table not found"
|
306
|
+
end
|
307
|
+
rescue StandardError => e
|
308
|
+
puts " Error accessing message data: #{e.message}"
|
309
|
+
end
|
310
|
+
|
311
|
+
# SSE Event Statistics (if table exists)
|
312
|
+
puts "\n\e[36mSSE Event Statistics:\e[0m"
|
313
|
+
|
314
|
+
begin
|
315
|
+
if ActionMCP::ApplicationRecord.connection.table_exists?("action_mcp_sse_events")
|
316
|
+
total_events = ActionMCP::Session::SSEEvent.count
|
317
|
+
puts " Total SSE Events: #{total_events}"
|
318
|
+
|
319
|
+
if total_events > 0
|
320
|
+
# Recent events
|
321
|
+
recent_events = ActionMCP::Session::SSEEvent.where("created_at > ?", 1.hour.ago).count
|
322
|
+
puts " SSE Events (Last Hour): #{recent_events}"
|
323
|
+
|
324
|
+
# Events by session
|
325
|
+
events_by_session = ActionMCP::Session::SSEEvent.joins(:session)
|
326
|
+
.group("action_mcp_sessions.id")
|
327
|
+
.count
|
328
|
+
.sort_by { |_session_id, count| -count }
|
329
|
+
.first(5)
|
330
|
+
puts " Top Sessions by SSE Events:"
|
331
|
+
events_by_session.each do |session_id, count|
|
332
|
+
puts " #{session_id}: #{count} events"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
else
|
336
|
+
puts " SSE Events table not found"
|
337
|
+
end
|
338
|
+
rescue StandardError => e
|
339
|
+
puts " Error accessing SSE event data: #{e.message}"
|
340
|
+
end
|
341
|
+
|
342
|
+
# Storage Information
|
343
|
+
puts "\n\e[36mStorage Information:\e[0m"
|
344
|
+
puts " Session Store Type: #{ActionMCP.configuration.session_store_type}"
|
345
|
+
puts " Database Adapter: #{ActionMCP::ApplicationRecord.connection.adapter_name}"
|
346
|
+
|
347
|
+
# Database size (if SQLite)
|
348
|
+
begin
|
349
|
+
if ActionMCP::ApplicationRecord.connection.adapter_name.downcase.include?("sqlite")
|
350
|
+
db_config = Rails.application.config.database_configuration[Rails.env]
|
351
|
+
if db_config && db_config["database"]
|
352
|
+
db_file = db_config["database"]
|
353
|
+
if File.exist?(db_file)
|
354
|
+
size_mb = (File.size(db_file) / 1024.0 / 1024.0).round(2)
|
355
|
+
puts " Database Size: #{size_mb} MB"
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
rescue StandardError => e
|
360
|
+
puts " Could not determine database size: #{e.message}"
|
361
|
+
end
|
362
|
+
|
363
|
+
puts "\n"
|
364
|
+
end
|
127
365
|
end
|
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.
|
4
|
+
version: 0.70.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -284,6 +284,7 @@ files:
|
|
284
284
|
- lib/action_mcp/renderable.rb
|
285
285
|
- lib/action_mcp/resource.rb
|
286
286
|
- lib/action_mcp/resource_callbacks.rb
|
287
|
+
- lib/action_mcp/resource_response.rb
|
287
288
|
- lib/action_mcp/resource_template.rb
|
288
289
|
- lib/action_mcp/resource_templates_registry.rb
|
289
290
|
- lib/action_mcp/server.rb
|