actionmcp 0.71.1 → 0.80.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 +187 -16
- data/app/controllers/action_mcp/application_controller.rb +64 -49
- data/app/models/action_mcp/session/message.rb +31 -20
- data/app/models/action_mcp/session/resource.rb +35 -20
- data/app/models/action_mcp/session/sse_event.rb +23 -17
- data/app/models/action_mcp/session/subscription.rb +22 -15
- data/app/models/action_mcp/session.rb +71 -113
- data/config/routes.rb +0 -11
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +9 -11
- data/lib/action_mcp/client/elicitation.rb +4 -4
- data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
- data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
- data/lib/action_mcp/client.rb +6 -26
- data/lib/action_mcp/configuration.rb +65 -63
- data/lib/action_mcp/engine.rb +1 -10
- data/lib/action_mcp/filtered_logger.rb +3 -7
- data/lib/action_mcp/gateway.rb +7 -11
- data/lib/action_mcp/gateway_identifier.rb +187 -3
- data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
- data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
- data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
- data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
- data/lib/action_mcp/gateway_identifiers.rb +26 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/prompt.rb +2 -0
- data/lib/action_mcp/renderable.rb +1 -1
- data/lib/action_mcp/resource_template.rb +6 -2
- data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
- data/lib/action_mcp/server/base_session_store.rb +86 -0
- data/lib/action_mcp/server/capabilities.rb +2 -1
- data/lib/action_mcp/server/elicitation.rb +3 -9
- data/lib/action_mcp/server/error_handling.rb +14 -1
- data/lib/action_mcp/server/handlers/router.rb +31 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
- data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
- data/lib/action_mcp/server/prompts.rb +4 -4
- data/lib/action_mcp/server/resources.rb +23 -4
- data/lib/action_mcp/server/session_store_factory.rb +1 -1
- data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
- data/lib/action_mcp/server/tools.rb +62 -43
- data/lib/action_mcp/server/transport_handler.rb +2 -4
- data/lib/action_mcp/server/volatile_session_store.rb +1 -93
- data/lib/action_mcp/tagged_stream_logging.rb +2 -2
- data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
- data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
- data/lib/action_mcp/tool.rb +48 -37
- data/lib/action_mcp/types/float_array_type.rb +5 -3
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -7
- data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
- data/lib/generators/action_mcp/install/install_generator.rb +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +18 -100
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
- data/app/models/action_mcp/oauth_client.rb +0 -157
- data/app/models/action_mcp/oauth_token.rb +0 -141
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
- data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
- data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
- data/lib/action_mcp/jwt_decoder.rb +0 -26
- data/lib/action_mcp/jwt_identifier.rb +0 -28
- data/lib/action_mcp/none_identifier.rb +0 -19
- data/lib/action_mcp/o_auth_identifier.rb +0 -34
- data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
- data/lib/action_mcp/oauth/error.rb +0 -79
- data/lib/action_mcp/oauth/memory_storage.rb +0 -134
- data/lib/action_mcp/oauth/middleware.rb +0 -133
- data/lib/action_mcp/oauth/provider.rb +0 -426
- data/lib/action_mcp/oauth.rb +0 -12
- data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
- data/lib/action_mcp/server/notifications.rb +0 -58
@@ -1,26 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_sse_events"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
7
|
+
# columns = [
|
8
|
+
# { name = "id", type = "integer", primary_key = true, nullable = false },
|
9
|
+
# { name = "session_id", type = "string", nullable = false },
|
10
|
+
# { name = "event_id", type = "integer", nullable = false },
|
11
|
+
# { name = "data", type = "text", nullable = false },
|
12
|
+
# { name = "created_at", type = "datetime", nullable = false },
|
13
|
+
# { name = "updated_at", type = "datetime", nullable = false }
|
14
|
+
# ]
|
6
15
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# session_id :string not null
|
16
|
+
# indexes = [
|
17
|
+
# { name = "index_action_mcp_sse_events_on_created_at", columns = ["created_at"] },
|
18
|
+
# { name = "index_action_mcp_sse_events_on_session_id_and_event_id", columns = ["session_id", "event_id"], unique = true },
|
19
|
+
# { name = "index_action_mcp_sse_events_on_session_id", columns = ["session_id"] }
|
20
|
+
# ]
|
13
21
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# index_action_mcp_sse_events_on_session_id (session_id)
|
18
|
-
# index_action_mcp_sse_events_on_session_id_and_event_id (session_id,event_id) UNIQUE
|
19
|
-
#
|
20
|
-
# Foreign Keys
|
21
|
-
#
|
22
|
-
# session_id (session_id => action_mcp_sessions.id)
|
22
|
+
# foreign_keys = [
|
23
|
+
# { column = "session_id", references_table = "action_mcp_sessions", references_column = "id" }
|
24
|
+
# ]
|
23
25
|
#
|
26
|
+
# == Notes
|
27
|
+
# - Association 'session' should specify inverse_of
|
28
|
+
# - String column 'session_id' has no length limit - consider adding one
|
29
|
+
# <rails-lens:schema:end>
|
24
30
|
module ActionMCP
|
25
31
|
class Session
|
26
32
|
# Represents a Server-Sent Event (SSE) in an MCP session
|
@@ -1,24 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_session_subscriptions"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
7
|
+
# columns = [
|
8
|
+
# { name = "id", type = "integer", primary_key = true, nullable = false },
|
9
|
+
# { name = "session_id", type = "string", nullable = false },
|
10
|
+
# { name = "uri", type = "string", nullable = false },
|
11
|
+
# { name = "last_notification_at", type = "datetime", nullable = true },
|
12
|
+
# { name = "created_at", type = "datetime", nullable = false },
|
13
|
+
# { name = "updated_at", type = "datetime", nullable = false }
|
14
|
+
# ]
|
6
15
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# created_at :datetime not null
|
11
|
-
# updated_at :datetime not null
|
12
|
-
# session_id :string not null
|
16
|
+
# indexes = [
|
17
|
+
# { name = "index_action_mcp_session_subscriptions_on_session_id", columns = ["session_id"] }
|
18
|
+
# ]
|
13
19
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# Foreign Keys
|
19
|
-
#
|
20
|
-
# session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade
|
20
|
+
# foreign_keys = [
|
21
|
+
# { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade" }
|
22
|
+
# ]
|
21
23
|
#
|
24
|
+
# == Notes
|
25
|
+
# - Consider adding counter cache for 'session'
|
26
|
+
# - String column 'session_id' has no length limit - consider adding one
|
27
|
+
# - String column 'uri' has no length limit - consider adding one
|
28
|
+
# <rails-lens:schema:end>
|
22
29
|
module ActionMCP
|
23
30
|
class Session
|
24
31
|
#
|
@@ -1,38 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_sessions"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# status :string default("pre_initialize"), not null
|
26
|
-
# tool_registry :json
|
27
|
-
# created_at :datetime not null
|
28
|
-
# updated_at :datetime not null
|
29
|
-
#
|
30
|
-
# Indexes
|
31
|
-
#
|
32
|
-
# index_action_mcp_sessions_on_authentication_method (authentication_method)
|
33
|
-
# index_action_mcp_sessions_on_oauth_access_token (oauth_access_token) UNIQUE
|
34
|
-
# index_action_mcp_sessions_on_oauth_token_expires_at (oauth_token_expires_at)
|
7
|
+
# columns = [
|
8
|
+
# { name = "id", type = "string", primary_key = true, nullable = false },
|
9
|
+
# { name = "role", type = "string", nullable = false, default = "server" },
|
10
|
+
# { name = "status", type = "string", nullable = false, default = "pre_initialize" },
|
11
|
+
# { name = "ended_at", type = "datetime", nullable = true },
|
12
|
+
# { name = "protocol_version", type = "string", nullable = true },
|
13
|
+
# { name = "server_capabilities", type = "json", nullable = true },
|
14
|
+
# { name = "client_capabilities", type = "json", nullable = true },
|
15
|
+
# { name = "server_info", type = "json", nullable = true },
|
16
|
+
# { name = "client_info", type = "json", nullable = true },
|
17
|
+
# { name = "initialized", type = "boolean", nullable = false, default = "0" },
|
18
|
+
# { name = "messages_count", type = "integer", nullable = false, default = "0" },
|
19
|
+
# { name = "sse_event_counter", type = "integer", nullable = false, default = "0" },
|
20
|
+
# { name = "tool_registry", type = "json", nullable = true, default = "[]" },
|
21
|
+
# { name = "prompt_registry", type = "json", nullable = true, default = "[]" },
|
22
|
+
# { name = "resource_registry", type = "json", nullable = true, default = "[]" },
|
23
|
+
# { name = "created_at", type = "datetime", nullable = false },
|
24
|
+
# { name = "updated_at", type = "datetime", nullable = false },
|
25
|
+
# { name = "consents", type = "json", nullable = false, default = "{}" }
|
26
|
+
# ]
|
35
27
|
#
|
28
|
+
# == Notes
|
29
|
+
# - Association 'messages' has N+1 query risk. Consider using includes/preload
|
30
|
+
# - Association 'subscriptions' has N+1 query risk. Consider using includes/preload
|
31
|
+
# - Association 'resources' has N+1 query risk. Consider using includes/preload
|
32
|
+
# - Association 'sse_events' has N+1 query risk. Consider using includes/preload
|
33
|
+
# - Column 'protocol_version' should probably have NOT NULL constraint
|
34
|
+
# - Column 'server_capabilities' should probably have NOT NULL constraint
|
35
|
+
# - Column 'client_capabilities' should probably have NOT NULL constraint
|
36
|
+
# - Column 'server_info' should probably have NOT NULL constraint
|
37
|
+
# - Column 'client_info' should probably have NOT NULL constraint
|
38
|
+
# - Column 'tool_registry' should probably have NOT NULL constraint
|
39
|
+
# - Column 'prompt_registry' should probably have NOT NULL constraint
|
40
|
+
# - Column 'resource_registry' should probably have NOT NULL constraint
|
41
|
+
# - String column 'id' has no length limit - consider adding one
|
42
|
+
# - String column 'role' has no length limit - consider adding one
|
43
|
+
# - String column 'status' has no length limit - consider adding one
|
44
|
+
# - String column 'protocol_version' has no length limit - consider adding one
|
45
|
+
# - Column 'status' is commonly used in queries - consider adding an index
|
46
|
+
# <rails-lens:schema:end>
|
36
47
|
module ActionMCP
|
37
48
|
##
|
38
49
|
# Represents an MCP session, which is a connection between a client and a server.
|
@@ -40,6 +51,10 @@ module ActionMCP
|
|
40
51
|
# such as client and server capabilities, protocol version, and session status.
|
41
52
|
# It also manages the association with messages and subscriptions related to the session.
|
42
53
|
class Session < ApplicationRecord
|
54
|
+
after_initialize do
|
55
|
+
self.consents = {} if consents == "{}" || consents.nil?
|
56
|
+
end
|
57
|
+
|
43
58
|
include MCPConsoleHelpers
|
44
59
|
attribute :id, :string, default: -> { SecureRandom.hex(6) }
|
45
60
|
has_many :messages,
|
@@ -180,9 +195,7 @@ module ActionMCP
|
|
180
195
|
# Maintain cache limit by removing oldest events if needed
|
181
196
|
count = sse_events.count
|
182
197
|
excess = count - max_events
|
183
|
-
if excess.positive?
|
184
|
-
sse_events.order(event_id: :asc).limit(excess).delete_all
|
185
|
-
end
|
198
|
+
sse_events.order(event_id: :asc).limit(excess).delete_all if excess.positive?
|
186
199
|
|
187
200
|
event
|
188
201
|
end
|
@@ -359,92 +372,37 @@ module ActionMCP
|
|
359
372
|
resource_registry == [ "*" ]
|
360
373
|
end
|
361
374
|
|
362
|
-
# OAuth Session Management
|
363
|
-
# Required by MCP 2025-03-26 specification for session binding
|
364
375
|
|
365
|
-
#
|
366
|
-
|
367
|
-
update!(
|
368
|
-
oauth_access_token: access_token,
|
369
|
-
oauth_refresh_token: refresh_token,
|
370
|
-
oauth_token_expires_at: expires_at,
|
371
|
-
oauth_user_context: user_context,
|
372
|
-
authentication_method: "oauth"
|
373
|
-
)
|
374
|
-
end
|
376
|
+
# Consent management methods as per MCP specification
|
377
|
+
# These methods manage user consents for tools and resources
|
375
378
|
|
376
|
-
#
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
refresh_token: oauth_refresh_token,
|
383
|
-
expires_at: oauth_token_expires_at,
|
384
|
-
user_context: oauth_user_context || {},
|
385
|
-
authentication_method: authentication_method
|
386
|
-
}
|
379
|
+
# Checks if consent has been granted for a specific key
|
380
|
+
# @param key [String] The consent key (e.g., tool name or resource URI)
|
381
|
+
# @return [Boolean] true if consent is granted, false otherwise
|
382
|
+
def consent_granted_for?(key)
|
383
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
384
|
+
consents_hash&.key?(key) && consents_hash[key] == true
|
387
385
|
end
|
388
386
|
|
389
|
-
#
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
# Clear OAuth token data
|
398
|
-
def clear_oauth_token!
|
399
|
-
update!(
|
400
|
-
oauth_access_token: nil,
|
401
|
-
oauth_refresh_token: nil,
|
402
|
-
oauth_token_expires_at: nil,
|
403
|
-
oauth_user_context: nil,
|
404
|
-
authentication_method: "none"
|
405
|
-
)
|
406
|
-
end
|
407
|
-
|
408
|
-
# Update OAuth token (for refresh flow)
|
409
|
-
def update_oauth_token(access_token:, refresh_token: nil, expires_at:)
|
410
|
-
update!(
|
411
|
-
oauth_access_token: access_token,
|
412
|
-
oauth_refresh_token: refresh_token,
|
413
|
-
oauth_token_expires_at: expires_at
|
414
|
-
)
|
415
|
-
end
|
416
|
-
|
417
|
-
# Get user information from OAuth context
|
418
|
-
def oauth_user
|
419
|
-
return nil unless oauth_user_context.is_a?(Hash)
|
420
|
-
|
421
|
-
OpenStruct.new(oauth_user_context)
|
422
|
-
end
|
423
|
-
|
424
|
-
# Check if session is authenticated via OAuth
|
425
|
-
def oauth_authenticated?
|
426
|
-
authentication_method == "oauth" && oauth_token_valid?
|
387
|
+
# Grants consent for a specific key
|
388
|
+
# @param key [String] The consent key to grant
|
389
|
+
# @return [Boolean] true if saved successfully
|
390
|
+
def grant_consent(key)
|
391
|
+
self.consents = JSON.parse(consents) if consents.is_a?(String)
|
392
|
+
self.consents ||= {}
|
393
|
+
self.consents[key] = true
|
394
|
+
save!
|
427
395
|
end
|
428
396
|
|
429
|
-
#
|
430
|
-
|
431
|
-
|
432
|
-
|
397
|
+
# Revokes consent for a specific key
|
398
|
+
# @param key [String] The consent key to revoke
|
399
|
+
# @return [void]
|
400
|
+
def revoke_consent(key)
|
401
|
+
self.consents = JSON.parse(self.consents) if self.consents.is_a?(String)
|
402
|
+
return unless consents&.key?(key)
|
433
403
|
|
434
|
-
|
435
|
-
|
436
|
-
where("oauth_token_expires_at IS NOT NULL AND oauth_token_expires_at < ?", Time.current)
|
437
|
-
end
|
438
|
-
|
439
|
-
# Cleanup expired OAuth tokens (class method)
|
440
|
-
def self.cleanup_expired_oauth_tokens
|
441
|
-
with_expired_oauth_tokens.update_all(
|
442
|
-
oauth_access_token: nil,
|
443
|
-
oauth_refresh_token: nil,
|
444
|
-
oauth_token_expires_at: nil,
|
445
|
-
oauth_user_context: nil,
|
446
|
-
authentication_method: "none"
|
447
|
-
)
|
404
|
+
consents.delete(key)
|
405
|
+
save!
|
448
406
|
end
|
449
407
|
|
450
408
|
private
|
data/config/routes.rb
CHANGED
@@ -3,17 +3,6 @@
|
|
3
3
|
ActionMCP::Engine.routes.draw do
|
4
4
|
get "/up", to: "/rails/health#show", as: :action_mcp_health_check
|
5
5
|
|
6
|
-
# OAuth 2.1 metadata endpoints
|
7
|
-
get "/.well-known/oauth-authorization-server", to: "oauth/metadata#authorization_server", as: :oauth_authorization_server_metadata
|
8
|
-
get "/.well-known/oauth-protected-resource", to: "oauth/metadata#protected_resource", as: :oauth_protected_resource_metadata
|
9
|
-
|
10
|
-
# OAuth 2.1 endpoints
|
11
|
-
get "/oauth/authorize", to: "oauth/endpoints#authorize", as: :oauth_authorize
|
12
|
-
post "/oauth/token", to: "oauth/endpoints#token", as: :oauth_token
|
13
|
-
post "/oauth/introspect", to: "oauth/endpoints#introspect", as: :oauth_introspect
|
14
|
-
post "/oauth/revoke", to: "oauth/endpoints#revoke", as: :oauth_revoke
|
15
|
-
post "/oauth/register", to: "oauth/registration#create", as: :oauth_register
|
16
|
-
|
17
6
|
# MCP 2025-03-26 Spec routes
|
18
7
|
get "/", to: "application#show", as: :mcp_get
|
19
8
|
post "/", to: "application#create", as: :mcp_post
|
@@ -133,9 +133,9 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
133
133
|
return unless column_exists?(:action_mcp_session_messages, :direction)
|
134
134
|
|
135
135
|
# SQLite3 doesn't support changing column comments
|
136
|
-
|
137
|
-
|
138
|
-
|
136
|
+
return unless connection.adapter_name.downcase != 'sqlite'
|
137
|
+
|
138
|
+
change_column_comment :action_mcp_session_messages, :direction, 'The message recipient'
|
139
139
|
end
|
140
140
|
|
141
141
|
private
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RemoveOauthSupport < ActiveRecord::Migration[8.0]
|
4
|
+
def change
|
5
|
+
# Remove OAuth tables
|
6
|
+
drop_table :action_mcp_oauth_clients, if_exists: true do |t|
|
7
|
+
t.string :client_id, null: false
|
8
|
+
t.string :client_secret
|
9
|
+
t.string :client_name
|
10
|
+
t.json :redirect_uris, default: []
|
11
|
+
t.json :grant_types, default: [ "authorization_code" ]
|
12
|
+
t.json :response_types, default: [ "code" ]
|
13
|
+
t.string :token_endpoint_auth_method, default: "client_secret_basic"
|
14
|
+
t.text :scope
|
15
|
+
t.boolean :active, default: true
|
16
|
+
t.integer :client_id_issued_at
|
17
|
+
t.integer :client_secret_expires_at
|
18
|
+
t.string :registration_access_token
|
19
|
+
t.json :metadata, default: {}
|
20
|
+
t.datetime :created_at, null: false
|
21
|
+
t.datetime :updated_at, null: false
|
22
|
+
end
|
23
|
+
|
24
|
+
drop_table :action_mcp_oauth_tokens, if_exists: true do |t|
|
25
|
+
t.string :token, null: false
|
26
|
+
t.string :token_type, null: false
|
27
|
+
t.string :client_id, null: false
|
28
|
+
t.string :user_id
|
29
|
+
t.text :scope
|
30
|
+
t.datetime :expires_at
|
31
|
+
t.boolean :revoked, default: false
|
32
|
+
t.string :redirect_uri
|
33
|
+
t.string :code_challenge
|
34
|
+
t.string :code_challenge_method
|
35
|
+
t.string :access_token
|
36
|
+
t.json :metadata, default: {}
|
37
|
+
t.datetime :created_at, null: false
|
38
|
+
t.datetime :updated_at, null: false
|
39
|
+
end
|
40
|
+
|
41
|
+
# Remove OAuth columns from sessions table
|
42
|
+
if table_exists?(:action_mcp_sessions)
|
43
|
+
remove_column :action_mcp_sessions, :oauth_access_token, :string if column_exists?(:action_mcp_sessions, :oauth_access_token)
|
44
|
+
remove_column :action_mcp_sessions, :oauth_refresh_token, :string if column_exists?(:action_mcp_sessions, :oauth_refresh_token)
|
45
|
+
remove_column :action_mcp_sessions, :oauth_token_expires_at, :datetime if column_exists?(:action_mcp_sessions, :oauth_token_expires_at)
|
46
|
+
remove_column :action_mcp_sessions, :oauth_user_context, :json if column_exists?(:action_mcp_sessions, :oauth_user_context)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def table_exists?(table_name)
|
53
|
+
ActiveRecord::Base.connection.table_exists?(table_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
def column_exists?(table_name, column_name)
|
57
|
+
ActiveRecord::Base.connection.column_exists?(table_name, column_name)
|
58
|
+
end
|
59
|
+
end
|
@@ -26,8 +26,8 @@ module ActionMCP
|
|
26
26
|
def initialize(transport:, logger: ActionMCP.logger, protocol_version: nil, **options)
|
27
27
|
@logger = logger
|
28
28
|
@transport = transport
|
29
|
-
@session = nil
|
30
|
-
@session_id = options[:session_id]
|
29
|
+
@session = nil # Session will be created/loaded based on server response
|
30
|
+
@session_id = options[:session_id] # Optional session ID for resumption
|
31
31
|
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
32
32
|
@server_capabilities = nil
|
33
33
|
@connection_error = nil
|
@@ -91,7 +91,7 @@ module ActionMCP
|
|
91
91
|
|
92
92
|
begin
|
93
93
|
# Only write to session if it exists (after initialization)
|
94
|
-
session
|
94
|
+
session&.write(payload)
|
95
95
|
data = payload.to_json unless payload.is_a?(String)
|
96
96
|
@transport.send_message(data)
|
97
97
|
true
|
@@ -109,11 +109,11 @@ module ActionMCP
|
|
109
109
|
end
|
110
110
|
|
111
111
|
# Only update session if it exists
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
112
|
+
return unless @session
|
113
|
+
|
114
|
+
@session.server_capabilities = server.capabilities
|
115
|
+
@session.server_info = server.server_info
|
116
|
+
@session.save
|
117
117
|
end
|
118
118
|
|
119
119
|
def initialized?
|
@@ -176,9 +176,7 @@ module ActionMCP
|
|
176
176
|
log_debug("Sending client capabilities")
|
177
177
|
|
178
178
|
# If we have a session_id, we're trying to resume
|
179
|
-
if @session_id
|
180
|
-
log_debug("Attempting to resume session: #{@session_id}")
|
181
|
-
end
|
179
|
+
log_debug("Attempting to resume session: #{@session_id}") if @session_id
|
182
180
|
|
183
181
|
params = {
|
184
182
|
protocolVersion: @protocol_version,
|
@@ -8,15 +8,15 @@ module ActionMCP
|
|
8
8
|
# @param id [String, Integer] The request ID
|
9
9
|
# @param params [Hash] The elicitation parameters
|
10
10
|
def process_elicitation_request(id, params)
|
11
|
-
|
12
|
-
|
11
|
+
params["message"]
|
12
|
+
params["requestedSchema"]
|
13
13
|
|
14
14
|
# In a real implementation, this would prompt the user
|
15
15
|
# For now, we'll just return a decline response
|
16
16
|
# Actual implementations should override this method
|
17
17
|
send_jsonrpc_response(id, result: {
|
18
|
-
|
19
|
-
|
18
|
+
action: "decline"
|
19
|
+
})
|
20
20
|
end
|
21
21
|
|
22
22
|
# Send elicitation response
|
@@ -53,15 +53,13 @@ module ActionMCP
|
|
53
53
|
case rpc_method
|
54
54
|
when Methods::ELICITATION_CREATE
|
55
55
|
client.process_elicitation_request(id, params)
|
56
|
-
when
|
56
|
+
when %r{^roots/}
|
57
57
|
process_roots(rpc_method, id)
|
58
|
-
when
|
58
|
+
when %r{^sampling/}
|
59
59
|
process_sampling(rpc_method, id, params)
|
60
60
|
else
|
61
61
|
common_result = handle_common_methods(rpc_method, id, params)
|
62
|
-
if common_result.nil?
|
63
|
-
client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}")
|
64
|
-
end
|
62
|
+
client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}") if common_result.nil?
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
@@ -161,7 +159,7 @@ module ActionMCP
|
|
161
159
|
client.log_error("Unknown error: #{id} #{error}")
|
162
160
|
end
|
163
161
|
|
164
|
-
def handle_initialize_response(
|
162
|
+
def handle_initialize_response(_request_id, result)
|
165
163
|
# Session ID comes from HTTP headers, not the response body
|
166
164
|
# The transport should have already extracted it
|
167
165
|
session_id = transport.instance_variable_get(:@session_id)
|
@@ -179,13 +177,13 @@ module ActionMCP
|
|
179
177
|
else
|
180
178
|
# Create a new session with the server-provided ID
|
181
179
|
client.instance_variable_set(:@session, ActionMCP::Session.from_client.new(
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
180
|
+
id: session_id,
|
181
|
+
protocol_version: result["protocolVersion"] || ActionMCP::DEFAULT_PROTOCOL_VERSION,
|
182
|
+
client_info: client.client_info,
|
183
|
+
client_capabilities: client.client_capabilities,
|
184
|
+
server_info: result["serverInfo"],
|
185
|
+
server_capabilities: result["capabilities"]
|
186
|
+
))
|
189
187
|
client.session.save
|
190
188
|
client.log_info("Created new session: #{session_id}")
|
191
189
|
end
|