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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +187 -16
  3. data/app/controllers/action_mcp/application_controller.rb +64 -49
  4. data/app/models/action_mcp/session/message.rb +31 -20
  5. data/app/models/action_mcp/session/resource.rb +35 -20
  6. data/app/models/action_mcp/session/sse_event.rb +23 -17
  7. data/app/models/action_mcp/session/subscription.rb +22 -15
  8. data/app/models/action_mcp/session.rb +71 -113
  9. data/config/routes.rb +0 -11
  10. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  11. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  12. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  13. data/lib/action_mcp/base_response.rb +1 -1
  14. data/lib/action_mcp/client/base.rb +9 -11
  15. data/lib/action_mcp/client/elicitation.rb +4 -4
  16. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  17. data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
  18. data/lib/action_mcp/client.rb +6 -26
  19. data/lib/action_mcp/configuration.rb +65 -63
  20. data/lib/action_mcp/engine.rb +1 -10
  21. data/lib/action_mcp/filtered_logger.rb +3 -7
  22. data/lib/action_mcp/gateway.rb +7 -11
  23. data/lib/action_mcp/gateway_identifier.rb +187 -3
  24. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  25. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  26. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  27. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  28. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  29. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  30. data/lib/action_mcp/prompt.rb +2 -0
  31. data/lib/action_mcp/renderable.rb +1 -1
  32. data/lib/action_mcp/resource_template.rb +6 -2
  33. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
  34. data/lib/action_mcp/server/base_session_store.rb +86 -0
  35. data/lib/action_mcp/server/capabilities.rb +2 -1
  36. data/lib/action_mcp/server/elicitation.rb +3 -9
  37. data/lib/action_mcp/server/error_handling.rb +14 -1
  38. data/lib/action_mcp/server/handlers/router.rb +31 -0
  39. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  40. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  41. data/lib/action_mcp/server/prompts.rb +4 -4
  42. data/lib/action_mcp/server/resources.rb +23 -4
  43. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  44. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  45. data/lib/action_mcp/server/tools.rb +62 -43
  46. data/lib/action_mcp/server/transport_handler.rb +2 -4
  47. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  48. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  49. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  50. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  51. data/lib/action_mcp/tool.rb +48 -37
  52. data/lib/action_mcp/types/float_array_type.rb +5 -3
  53. data/lib/action_mcp/version.rb +1 -1
  54. data/lib/action_mcp.rb +2 -7
  55. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  56. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  57. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  58. data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
  59. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  60. data/lib/tasks/action_mcp_tasks.rake +7 -5
  61. metadata +18 -100
  62. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
  63. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
  64. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
  65. data/app/models/action_mcp/oauth_client.rb +0 -157
  66. data/app/models/action_mcp/oauth_token.rb +0 -141
  67. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
  68. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
  69. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
  70. data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
  71. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  72. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  73. data/lib/action_mcp/jwt_decoder.rb +0 -26
  74. data/lib/action_mcp/jwt_identifier.rb +0 -28
  75. data/lib/action_mcp/none_identifier.rb +0 -19
  76. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  77. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  78. data/lib/action_mcp/oauth/error.rb +0 -79
  79. data/lib/action_mcp/oauth/memory_storage.rb +0 -134
  80. data/lib/action_mcp/oauth/middleware.rb +0 -133
  81. data/lib/action_mcp/oauth/provider.rb +0 -426
  82. data/lib/action_mcp/oauth.rb +0 -12
  83. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
  84. data/lib/action_mcp/server/notifications.rb +0 -58
@@ -1,26 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_sse_events"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_sse_events
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
- # id :integer not null, primary key
8
- # data :text not null
9
- # created_at :datetime not null
10
- # updated_at :datetime not null
11
- # event_id :integer not null
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
- # Indexes
15
- #
16
- # index_action_mcp_sse_events_on_created_at (created_at)
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
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_session_subscriptions"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_session_subscriptions
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
- # id :integer not null, primary key
8
- # last_notification_at :datetime
9
- # uri :string not null
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
- # Indexes
15
- #
16
- # index_action_mcp_session_subscriptions_on_session_id (session_id)
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
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_sessions"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_sessions
6
- #
7
- # id :string not null, primary key
8
- # authentication_method :string default("none")
9
- # client_capabilities :json
10
- # client_info :json
11
- # ended_at :datetime
12
- # initialized :boolean default(FALSE), not null
13
- # messages_count :integer default(0), not null
14
- # oauth_access_token :string
15
- # oauth_refresh_token :string
16
- # oauth_token_expires_at :datetime
17
- # oauth_user_context :json
18
- # prompt_registry :json
19
- # protocol_version :string
20
- # resource_registry :json
21
- # role :string default("server"), not null
22
- # server_capabilities :json
23
- # server_info :json
24
- # sse_event_counter :integer default(0), not null
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
- # Store OAuth token and user context in session
366
- def store_oauth_token(access_token:, refresh_token: nil, expires_at:, user_context: {})
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
- # Retrieve OAuth token information
377
- def oauth_token_info
378
- return nil unless oauth_access_token
379
-
380
- {
381
- access_token: oauth_access_token,
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
- # Check if OAuth token is valid and not expired
390
- def oauth_token_valid?
391
- return false unless oauth_access_token
392
- return true unless oauth_token_expires_at
393
-
394
- oauth_token_expires_at > Time.current
395
- end
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
- # Find session by OAuth access token (class method)
430
- def self.find_by_oauth_token(access_token)
431
- find_by(oauth_access_token: access_token)
432
- end
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
- # Find sessions with expired OAuth tokens (class method)
435
- def self.with_expired_oauth_tokens
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
- if connection.adapter_name.downcase != 'sqlite'
137
- change_column_comment :action_mcp_session_messages, :direction, 'The message recipient'
138
- end
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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddConsentsToActionMCPSess < ActiveRecord::Migration[8.0]
4
+ def change
5
+ add_column :action_mcp_sessions, :consents, :json, default: {}, null: false
6
+ end
7
+ end
@@ -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
@@ -19,7 +19,7 @@ module ActionMCP
19
19
  end
20
20
 
21
21
  # Convert to hash format expected by MCP protocol
22
- def to_h(options = nil)
22
+ def to_h(_options = nil)
23
23
  if @is_error
24
24
  JSON_RPC::JsonRpcError.new(@symbol, message: @error_message, data: @error_data).to_h
25
25
  else
@@ -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 # Session will be created/loaded based on server response
30
- @session_id = options[:session_id] # Optional session ID for resumption
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.write(payload) if 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
- if @session
113
- @session.server_capabilities = server.capabilities
114
- @session.server_info = server.server_info
115
- @session.save
116
- end
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
- message = params["message"]
12
- requested_schema = params["requestedSchema"]
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
- action: "decline"
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 /^roots\//
56
+ when %r{^roots/}
57
57
  process_roots(rpc_method, id)
58
- when /^sampling\//
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(request_id, result)
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
- id: session_id,
183
- protocol_version: result["protocolVersion"] || ActionMCP::DEFAULT_PROTOCOL_VERSION,
184
- client_info: client.client_info,
185
- client_capabilities: client.client_capabilities,
186
- server_info: result["serverInfo"],
187
- server_capabilities: result["capabilities"]
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