actionmcp 0.71.1 → 0.72.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -15
  3. data/app/controllers/action_mcp/application_controller.rb +45 -38
  4. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +11 -10
  5. data/app/controllers/action_mcp/oauth/metadata_controller.rb +6 -10
  6. data/app/controllers/action_mcp/oauth/registration_controller.rb +15 -20
  7. data/app/models/action_mcp/oauth_client.rb +7 -5
  8. data/app/models/action_mcp/oauth_token.rb +2 -1
  9. data/app/models/action_mcp/session.rb +40 -5
  10. data/config/routes.rb +4 -2
  11. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  12. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +17 -8
  13. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +7 -5
  14. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +3 -1
  15. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  16. data/lib/action_mcp/base_response.rb +1 -1
  17. data/lib/action_mcp/client/base.rb +9 -11
  18. data/lib/action_mcp/client/elicitation.rb +4 -4
  19. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  20. data/lib/action_mcp/client/jwt_client_provider.rb +6 -5
  21. data/lib/action_mcp/client/oauth_client_provider.rb +8 -8
  22. data/lib/action_mcp/client/streamable_http_transport.rb +29 -39
  23. data/lib/action_mcp/client.rb +6 -3
  24. data/lib/action_mcp/configuration.rb +28 -53
  25. data/lib/action_mcp/engine.rb +1 -3
  26. data/lib/action_mcp/filtered_logger.rb +1 -1
  27. data/lib/action_mcp/gateway.rb +7 -11
  28. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  29. data/lib/action_mcp/jwt_decoder.rb +4 -2
  30. data/lib/action_mcp/oauth/active_record_storage.rb +1 -1
  31. data/lib/action_mcp/oauth/memory_storage.rb +1 -3
  32. data/lib/action_mcp/oauth/middleware.rb +13 -18
  33. data/lib/action_mcp/oauth/provider.rb +45 -65
  34. data/lib/action_mcp/omniauth/mcp_strategy.rb +23 -37
  35. data/lib/action_mcp/prompt.rb +2 -0
  36. data/lib/action_mcp/renderable.rb +1 -1
  37. data/lib/action_mcp/resource_template.rb +6 -2
  38. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +39 -26
  39. data/lib/action_mcp/server/base_session_store.rb +86 -0
  40. data/lib/action_mcp/server/capabilities.rb +2 -1
  41. data/lib/action_mcp/server/elicitation.rb +3 -9
  42. data/lib/action_mcp/server/error_handling.rb +14 -1
  43. data/lib/action_mcp/server/handlers/router.rb +31 -0
  44. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  45. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  46. data/lib/action_mcp/server/prompts.rb +4 -4
  47. data/lib/action_mcp/server/resources.rb +23 -4
  48. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  49. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  50. data/lib/action_mcp/server/tools.rb +62 -43
  51. data/lib/action_mcp/server/transport_handler.rb +2 -4
  52. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  53. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  54. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  55. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  56. data/lib/action_mcp/tool.rb +48 -37
  57. data/lib/action_mcp/types/float_array_type.rb +5 -3
  58. data/lib/action_mcp/version.rb +1 -1
  59. data/lib/action_mcp.rb +1 -1
  60. data/lib/generators/action_mcp/install/templates/application_gateway.rb +1 -0
  61. data/lib/tasks/action_mcp_tasks.rake +7 -5
  62. metadata +20 -18
  63. data/lib/action_mcp/server/notifications.rb +0 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 203f9e44a8802e007a19f41ddb89e2961fae87fb14bc0157e398dd6b80561cc3
4
- data.tar.gz: d644c46fe4e73c1c5532d14f2e1541d511e4311479c15ef5d946aa135c4f290d
3
+ metadata.gz: 2875f1eaab23887a9cafbff393229ab35a733822b4b20487cff5b4556cd602c9
4
+ data.tar.gz: ac60fbba55e7e06960644c70f790cd0f4f8909d1b6d1137b3f002bdeda29575c
5
5
  SHA512:
6
- metadata.gz: c9472a20f2aafc0c4ac4b74d8924e741b4cb94dd69154d44b060d2a75fcc01edcda857be40611c7c24b6d1c4ee5410705b776999534c35521301fef737e57d35
7
- data.tar.gz: 78afc85939383e260a89726dbc10c516a4c7da3dded794d55545c51ed98ba2179b433d402a552c1964f15a95c9e51c0e2e6ac531f18e1e145ae4a9ebe47b893e
6
+ metadata.gz: 9fe34b128238c04f1dcb8a08af77b17aafcf7c0bf5fd1ce670d0c8bfdd0aefcea6d284d1c223cb3572645e5101aa71021381eb1d0379851114c075102311c156
7
+ data.tar.gz: 3817b2aa867d773afad8cb9db19fca5e1dc1d1425d1b02912a62cd15fba6cde28b3e7333114a98ccc8fe551c15d481f77d02c6eb549bf3ae971d95a2a2ff013b
data/README.md CHANGED
@@ -24,7 +24,15 @@ This means an AI (like an LLM) can request information or actions from your appl
24
24
 
25
25
  ## Protocol Support
26
26
 
27
- ActionMCP supports **MCP 2025-06-18** (current) with backward compatibility for **MCP 2025-03-26**. For a detailed (and entertaining) breakdown of protocol versions, features, and our design decisions, see [The Hitchhiker's Guide to MCP](The_Hitchhikers_Guide_to_MCP.md).
27
+ ActionMCP supports **MCP 2025-06-18** (current) with backward compatibility for **MCP 2025-03-26**. The protocol implementation is fully compliant with the MCP specification, including:
28
+
29
+ - **JSON-RPC 2.0** transport layer
30
+ - **Capability negotiation** during initialization
31
+ - **Error handling** with proper error codes (-32601 for method not found, -32002 for consent required)
32
+ - **Session management** with resumable sessions
33
+ - **Change notifications** for dynamic capability updates
34
+
35
+ For a detailed (and entertaining) breakdown of protocol versions, features, and our design decisions, see [The Hitchhiker's Guide to MCP](The_Hitchhikers_Guide_to_MCP.md).
28
36
 
29
37
  *Don't Panic: The guide contains everything you need to know about surviving MCP protocol versions.*
30
38
 
@@ -42,25 +50,27 @@ In short, ActionMCP helps you build an MCP server (the component that exposes ca
42
50
 
43
51
  To start using ActionMCP, add it to your project:
44
52
 
45
- - **Using Bundler (Rails or Ruby projects):** Add the gem to your Gemfile and run bundle install:
53
+ ```bash
54
+ # Add gem to your Gemfile
55
+ $ bundle add actionmcp
46
56
 
47
- ```bash
48
- $ bundle add actionmcp
49
- ```
57
+ # Install dependencies
58
+ bundle install
50
59
 
51
- After adding the gem, run the install generator to set up the basic ActionMCP structure:
60
+ # Copy migrations from the engine
61
+ bin/rails action_mcp:install:migrations
52
62
 
53
- ```bash
54
- bundle install
55
- bin/rails action_mcp:install:migrations # Copy migrations from the engine
56
- bin/rails generate action_mcp:install # Creates base classes and configuration
57
- bin/rails db:migrate # Creates necessary database tables
63
+ # Generate base classes and configuration
64
+ bin/rails generate action_mcp:install
65
+
66
+ # Create necessary database tables
67
+ bin/rails db:migrate
58
68
  ```
59
69
 
60
70
  The `action_mcp:install` generator will:
61
71
  - Create base application classes (ApplicationGateway, ApplicationMCPTool, etc.)
62
72
  - Generate the MCP configuration file (`config/mcp.yml`)
63
- - Set up the basic directory structure for MCP components
73
+ - Set up the basic directory structure for MCP components (`app/mcp/`)
64
74
 
65
75
  Database migrations are copied separately using `bin/rails action_mcp:install:migrations`.
66
76
 
@@ -127,6 +137,7 @@ Key features:
127
137
  - Return multiple response types (text, images, errors)
128
138
  - Progressive responses with multiple render calls
129
139
  - Automatic input validation based on property definitions
140
+ - **Consent management for sensitive operations**
130
141
 
131
142
  **Example:**
132
143
 
@@ -160,6 +171,47 @@ class CalculateSumTool < ApplicationMCPTool
160
171
  end
161
172
  ```
162
173
 
174
+ #### Consent Management
175
+
176
+ For tools that perform sensitive operations (file system access, database modifications, external API calls), you can require explicit user consent:
177
+
178
+ ```ruby
179
+ class FileSystemTool < ApplicationMCPTool
180
+ tool_name "read_file"
181
+ description "Read contents of a file"
182
+
183
+ # Require explicit consent before execution
184
+ requires_consent!
185
+
186
+ property :file_path, type: "string", description: "Path to file", required: true
187
+
188
+ def perform
189
+ # This code only runs after user grants consent
190
+ content = File.read(file_path)
191
+ render(text: "File contents: #{content}")
192
+ end
193
+ end
194
+ ```
195
+
196
+ **Consent Flow:**
197
+ 1. When a consent-required tool is called without consent, it returns a JSON-RPC error with code `-32002`
198
+ 2. The client must explicitly grant consent for the specific tool
199
+ 3. Once granted, the tool can execute normally for that session
200
+ 4. Consent is session-scoped and can be revoked at any time
201
+
202
+ **Managing Consent:**
203
+
204
+ ```ruby
205
+ # Check if consent is granted
206
+ session.consent_granted_for?("read_file")
207
+
208
+ # Grant consent for a tool
209
+ session.grant_consent("read_file")
210
+
211
+ # Revoke consent
212
+ session.revoke_consent("read_file")
213
+ ```
214
+
163
215
  Tools can be executed by instantiating them and calling the `call` method:
164
216
 
165
217
  ```ruby
@@ -751,17 +803,97 @@ class ToolTest < ActiveSupport::TestCase
751
803
  end
752
804
  ```
753
805
 
806
+ The TestHelper provides several assertion methods:
807
+ - `assert_tool_findable(name)` - Verifies a tool exists and is registered
808
+ - `assert_prompt_findable(name)` - Verifies a prompt exists and is registered
809
+ - `execute_tool(name, **args)` - Executes a tool with arguments
810
+ - `execute_prompt(name, **args)` - Executes a prompt with arguments
811
+ - `assert_tool_output(result, expected)` - Asserts tool output matches expected text
812
+ - `assert_prompt_output(result)` - Extracts and returns prompt output for assertions
813
+
754
814
  ## Inspecting Your MCP Server
755
815
 
756
816
  You can use the MCP Inspector to test your server implementation:
757
817
 
758
818
  ```bash
759
- npx @modelcontextprotocol/inspector
819
+ # Start your MCP server
820
+ bundle exec rails s -c mcp.ru -p 62770
821
+
822
+ # In another terminal, run the inspector
823
+ npx @modelcontextprotocol/inspector --url http://localhost:62770
824
+ ```
825
+
826
+ The MCP Inspector provides an interactive interface to:
827
+ - Test tool executions with custom arguments
828
+ - Validate prompt responses
829
+ - Inspect resource templates and their outputs
830
+ - Debug protocol compliance and error handling
831
+
832
+ ## Development Commands
833
+
834
+ ActionMCP includes several rake tasks for development and debugging:
835
+
836
+ ```bash
837
+ # List all MCP components
838
+ bundle exec rails action_mcp:list
839
+
840
+ # List specific component types
841
+ bundle exec rails action_mcp:list_tools
842
+ bundle exec rails action_mcp:list_prompts
843
+ bundle exec rails action_mcp:list_resources
844
+ bundle exec rails action_mcp:list_profiles
845
+
846
+ # Show configuration and statistics
847
+ bundle exec rails action_mcp:info
848
+ bundle exec rails action_mcp:stats
849
+
850
+ # Show profile configuration
851
+ bundle exec rails action_mcp:show_profile[profile_name]
760
852
  ```
761
853
 
762
- The default path will be http://localhost:3000/action_mcp
854
+ ## Error Handling and Troubleshooting
855
+
856
+ ActionMCP provides comprehensive error handling following the JSON-RPC 2.0 specification:
857
+
858
+ ### Error Codes
859
+
860
+ - **-32601**: Method not found - The requested method doesn't exist
861
+ - **-32002**: Consent required - Tool requires user consent to execute
862
+ - **-32603**: Internal error - Server encountered an unexpected error
863
+ - **-32600**: Invalid request - The request is malformed
864
+
865
+ ### Context-Aware Error Messages
763
866
 
764
- Here's a section you can add to explain the profile system in ActionMCP:
867
+ Tools should return clear error messages to the LLM using the `render` method:
868
+
869
+ ```ruby
870
+ class MyTool < ApplicationMCPTool
871
+ def perform
872
+ # Check for error conditions and return clear messages
873
+ if some_error_condition?
874
+ render(error: ["Clear error message for the LLM"])
875
+ return
876
+ end
877
+
878
+ # Normal processing
879
+ render(text: "Success message")
880
+ end
881
+ end
882
+ ```
883
+
884
+ ### Common Issues
885
+
886
+ 1. **Session not found**: Ensure sessions are properly created and saved in the session store
887
+ 2. **Tool not registered**: Verify tools are properly defined and inherit from ApplicationMCPTool
888
+ 3. **Consent required**: Grant consent using `session.grant_consent(tool_name)`
889
+ 4. **Middleware conflicts**: Use `mcp_vanilla.ru` to avoid web-specific middleware
890
+
891
+ ### Debugging Tips
892
+
893
+ - Check server logs for detailed error information
894
+ - Use `bundle exec rails action_mcp:info` to verify configuration
895
+ - Test with MCP Inspector to isolate protocol issues
896
+ - Ensure proper session management in production environments
765
897
 
766
898
  ## Profiles
767
899
 
@@ -875,3 +1007,42 @@ Profiles are particularly useful for:
875
1007
  5. **Progressive enhancement**: Start with a minimal profile and gradually add capabilities
876
1008
 
877
1009
  By leveraging profiles, you can maintain a single ActionMCP codebase while providing tailored MCP capabilities for different contexts.
1010
+
1011
+ ## Client Usage
1012
+
1013
+ ActionMCP includes a client for connecting to remote MCP servers. The client handles session management, protocol negotiation, and provides a simple API for interacting with MCP servers.
1014
+
1015
+ For comprehensive client documentation, including examples, session management, transport configuration, and API usage, see [CLIENTUSAGE.md](CLIENTUSAGE.md).
1016
+
1017
+ ## Production Considerations
1018
+
1019
+ ### Security
1020
+
1021
+ - **Never expose sensitive data** through MCP components
1022
+ - **Use authentication** via Gateway for production deployments
1023
+ - **Implement proper authorization** in your tools and prompts
1024
+ - **Validate all inputs** using property definitions and Rails validations
1025
+ - **Use consent management** for sensitive operations
1026
+
1027
+ ### Performance
1028
+
1029
+ - **Configure appropriate thread pools** for high-traffic scenarios
1030
+ - **Use Redis or SolidMCP** for production pub/sub
1031
+ - **Choose ActiveRecord session store** for session persistence
1032
+ - **Monitor session cleanup** to prevent memory leaks
1033
+ - **Use profiles** to limit exposed capabilities
1034
+
1035
+ ### Monitoring
1036
+
1037
+ - **Enable logging** and configure appropriate log levels
1038
+ - **Monitor session statistics** using `action_mcp:stats`
1039
+ - **Track tool usage** and performance metrics
1040
+ - **Set up alerts** for error rates and response times
1041
+
1042
+ ### Deployment
1043
+
1044
+ - **Use Falcon** for optimal performance with streaming workloads
1045
+ - **Deploy on dedicated ports** or Unix sockets
1046
+ - **Use reverse proxies** (Nginx, Apache) for SSL termination
1047
+ - **Implement health checks** for your MCP endpoints
1048
+ - **Use `mcp_vanilla.ru`** to avoid middleware conflicts
@@ -12,7 +12,6 @@ module ActionMCP
12
12
  include ActionController::Live
13
13
  include ActionController::Instrumentation
14
14
 
15
-
16
15
  # Provides the ActionMCP::Session for the current request.
17
16
  # Handles finding existing sessions via header/param or initializing a new one.
18
17
  # Specific controllers/handlers might need to enforce session ID presence based on context.
@@ -52,7 +51,9 @@ module ActionMCP
52
51
  return if performed?
53
52
 
54
53
  last_event_id = request.headers["Last-Event-ID"].presence
55
- Rails.logger.info "Unified SSE (GET): Resuming from Last-Event-ID: #{last_event_id}" if last_event_id && ActionMCP.configuration.verbose_logging
54
+ if last_event_id && ActionMCP.configuration.verbose_logging
55
+ Rails.logger.info "Unified SSE (GET): Resuming from Last-Event-ID: #{last_event_id}"
56
+ end
56
57
 
57
58
  response.headers["Content-Type"] = "text/event-stream"
58
59
  response.headers["X-Accel-Buffering"] = "no"
@@ -61,7 +62,9 @@ module ActionMCP
61
62
  # Add MCP-Protocol-Version header for established sessions
62
63
  response.headers["MCP-Protocol-Version"] = session.protocol_version
63
64
 
64
- Rails.logger.info "Unified SSE (GET): Starting stream for session: #{session.id}" if ActionMCP.configuration.verbose_logging
65
+ if ActionMCP.configuration.verbose_logging
66
+ Rails.logger.info "Unified SSE (GET): Starting stream for session: #{session.id}"
67
+ end
65
68
 
66
69
  sse = SSE.new(response.stream)
67
70
  listener = SSEListener.new(session)
@@ -85,12 +88,16 @@ module ActionMCP
85
88
  begin
86
89
  missed_events = session.get_sse_events_after(last_event_id.to_i)
87
90
  if missed_events.any?
88
- Rails.logger.info "Unified SSE (GET): Sending #{missed_events.size} missed events for session: #{session.id}" if ActionMCP.configuration.verbose_logging
91
+ if ActionMCP.configuration.verbose_logging
92
+ Rails.logger.info "Unified SSE (GET): Sending #{missed_events.size} missed events for session: #{session.id}"
93
+ end
89
94
  missed_events.each do |event|
90
95
  sse.write(event.to_sse)
91
96
  end
92
- else
93
- Rails.logger.info "Unified SSE (GET): No missed events to send for session: #{session.id}" if ActionMCP.configuration.verbose_logging
97
+ elsif ActionMCP.configuration.verbose_logging
98
+ if ActionMCP.configuration.verbose_logging
99
+ Rails.logger.info "Unified SSE (GET): No missed events to send for session: #{session.id}"
100
+ end
94
101
  end
95
102
  rescue StandardError => e
96
103
  Rails.logger.error "Unified SSE (GET): Error sending missed events: #{e.message}"
@@ -116,7 +123,9 @@ module ActionMCP
116
123
  Rails.logger.warn "Unified SSE (GET): Heartbeat timed out for session: #{session.id}, closing."
117
124
  connection_active.make_false
118
125
  rescue StandardError => e
119
- Rails.logger.debug "Unified SSE (GET): Heartbeat error for session: #{session.id}: #{e.message}" if ActionMCP.configuration.verbose_logging
126
+ if ActionMCP.configuration.verbose_logging
127
+ Rails.logger.debug "Unified SSE (GET): Heartbeat error for session: #{session.id}: #{e.message}"
128
+ end
120
129
  connection_active.make_false
121
130
  end
122
131
  else
@@ -127,11 +136,15 @@ module ActionMCP
127
136
  heartbeat_task = Concurrent::ScheduledTask.execute(heartbeat_interval, &heartbeat_sender)
128
137
  sleep 0.1 while connection_active.true? && !response.stream.closed?
129
138
  rescue ActionController::Live::ClientDisconnected, IOError => e
130
- Rails.logger.debug "Unified SSE (GET): Client disconnected for session: #{session&.id}: #{e.message}" if ActionMCP.configuration.verbose_logging
139
+ if ActionMCP.configuration.verbose_logging
140
+ Rails.logger.debug "Unified SSE (GET): Client disconnected for session: #{session&.id}: #{e.message}"
141
+ end
131
142
  rescue StandardError => e
132
143
  Rails.logger.error "Unified SSE (GET): Unexpected error for session: #{session&.id}: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
133
144
  ensure
134
- Rails.logger.debug "Unified SSE (GET): Cleaning up connection for session: #{session&.id}" if ActionMCP.configuration.verbose_logging
145
+ if ActionMCP.configuration.verbose_logging
146
+ Rails.logger.debug "Unified SSE (GET): Cleaning up connection for session: #{session&.id}"
147
+ end
135
148
  heartbeat_active&.make_false
136
149
  heartbeat_task&.cancel
137
150
  listener&.stop
@@ -153,9 +166,7 @@ module ActionMCP
153
166
  end
154
167
 
155
168
  # Reject JSON-RPC batch requests as per MCP 2025-06-18 spec
156
- if jsonrpc_params_batch?
157
- return render_bad_request("JSON-RPC batch requests are not supported", nil)
158
- end
169
+ return render_bad_request("JSON-RPC batch requests are not supported", nil) if jsonrpc_params_batch?
159
170
 
160
171
  is_initialize_request = check_if_initialize_request(jsonrpc_params)
161
172
  session_initially_missing = extract_session_id.nil?
@@ -197,7 +208,9 @@ module ActionMCP
197
208
  result = json_rpc_handler.call(jsonrpc_params)
198
209
  process_handler_results(result, session, session_initially_missing, is_initialize_request)
199
210
  rescue ActionController::Live::ClientDisconnected, IOError => e
200
- Rails.logger.debug "Unified SSE (POST): Client disconnected during response: #{e.message}" if ActionMCP.configuration.verbose_logging
211
+ if ActionMCP.configuration.verbose_logging
212
+ Rails.logger.debug "Unified SSE (POST): Client disconnected during response: #{e.message}"
213
+ end
201
214
  begin
202
215
  response.stream&.close
203
216
  rescue StandardError
@@ -205,7 +218,11 @@ module ActionMCP
205
218
  end
206
219
  rescue StandardError => e
207
220
  Rails.logger.error "Unified POST Error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
208
- id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil rescue nil
221
+ id = begin
222
+ jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
223
+ rescue StandardError
224
+ nil
225
+ end
209
226
  render_internal_server_error("An unexpected error occurred.", id) unless performed?
210
227
  end
211
228
 
@@ -255,9 +272,7 @@ module ActionMCP
255
272
  end
256
273
 
257
274
  # Handle array values (take the last one as per TypeScript SDK)
258
- if header_version.is_a?(Array)
259
- header_version = header_version.last
260
- end
275
+ header_version = header_version.last if header_version.is_a?(Array)
261
276
 
262
277
  # Check if the header version is supported
263
278
  unless ActionMCP::SUPPORTED_VERSIONS.include?(header_version)
@@ -268,7 +283,7 @@ module ActionMCP
268
283
  end
269
284
 
270
285
  # If we have an initialized session, check if the header matches the negotiated version
271
- if session && session.initialized?
286
+ if session&.initialized?
272
287
  negotiated_version = session.protocol_version
273
288
  if header_version != negotiated_version
274
289
  ActionMCP.logger.warn "MCP-Protocol-Version mismatch: header=#{header_version}, negotiated=#{negotiated_version}"
@@ -340,9 +355,7 @@ module ActionMCP
340
355
  # Processes the results from the JsonRpcHandler.
341
356
  def process_handler_results(result, session, session_initially_missing, is_initialize_request)
342
357
  # Handle empty result (notifications)
343
- if result.nil?
344
- return head :accepted
345
- end
358
+ return head :accepted if result.nil?
346
359
 
347
360
  # Convert to hash for rendering
348
361
  payload = if result.respond_to?(:to_h)
@@ -369,9 +382,7 @@ module ActionMCP
369
382
  def render_json_response(payload, session, add_session_header)
370
383
  response.headers[MCP_SESSION_ID_HEADER] = session.id if add_session_header
371
384
  # Add MCP-Protocol-Version header if session has been initialized
372
- if session && session.initialized?
373
- response.headers["MCP-Protocol-Version"] = session.protocol_version
374
- end
385
+ response.headers["MCP-Protocol-Version"] = session.protocol_version if session&.initialized?
375
386
  response.headers["Content-Type"] = "application/json"
376
387
  render json: payload, status: :ok
377
388
  end
@@ -380,9 +391,7 @@ module ActionMCP
380
391
  def render_sse_response(payload, session, add_session_header)
381
392
  response.headers[MCP_SESSION_ID_HEADER] = session.id if add_session_header
382
393
  # Add MCP-Protocol-Version header if session has been initialized
383
- if session && session.initialized?
384
- response.headers["MCP-Protocol-Version"] = session.protocol_version
385
- end
394
+ response.headers["MCP-Protocol-Version"] = session.protocol_version if session&.initialized?
386
395
  response.headers["Content-Type"] = "text/event-stream"
387
396
  response.headers["X-Accel-Buffering"] = "no"
388
397
  response.headers["Cache-Control"] = "no-cache"
@@ -425,13 +434,13 @@ module ActionMCP
425
434
 
426
435
  # Helper to clean up old SSE events for a session
427
436
  def cleanup_old_sse_events(session)
428
- begin
429
- retention_period = session.sse_event_retention_period
430
- count = session.cleanup_old_sse_events(retention_period)
431
- Rails.logger.debug "Cleaned up #{count} old SSE events for session: #{session.id}" if count.positive? && ActionMCP.configuration.verbose_logging
432
- rescue StandardError => e
433
- Rails.logger.error "Error cleaning up old SSE events: #{e.message}"
437
+ retention_period = session.sse_event_retention_period
438
+ count = session.cleanup_old_sse_events(retention_period)
439
+ if count.positive? && ActionMCP.configuration.verbose_logging
440
+ Rails.logger.debug "Cleaned up #{count} old SSE events for session: #{session.id}"
434
441
  end
442
+ rescue StandardError => e
443
+ Rails.logger.error "Error cleaning up old SSE events: #{e.message}"
435
444
  end
436
445
 
437
446
  def format_tools_list(tools, session)
@@ -505,8 +514,8 @@ module ActionMCP
505
514
  # Authenticates the request using the configured gateway
506
515
  def authenticate_gateway!
507
516
  # Skip authentication for initialization-related requests in POST method
508
- if request.post? && defined?(jsonrpc_params) && jsonrpc_params
509
- return if initialization_related_request?(jsonrpc_params)
517
+ if request.post? && defined?(jsonrpc_params) && jsonrpc_params && initialization_related_request?(jsonrpc_params)
518
+ return
510
519
  end
511
520
 
512
521
  gateway_class = ActionMCP.configuration.gateway_class
@@ -524,9 +533,7 @@ module ActionMCP
524
533
 
525
534
  # Add WWW-Authenticate header for OAuth discovery as per spec
526
535
  auth_methods = ActionMCP.configuration.authentication_methods || []
527
- if auth_methods.include?("oauth")
528
- response.headers["WWW-Authenticate"] = 'Bearer realm="MCP API"'
529
- end
536
+ response.headers["WWW-Authenticate"] = 'Bearer realm="MCP API"' if auth_methods.include?("oauth")
530
537
 
531
538
  render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }, status: :unauthorized
532
539
  end
@@ -92,7 +92,7 @@ module ActionMCP
92
92
  return render_introspection_error unless token
93
93
 
94
94
  # Authenticate client for introspection
95
- client_id, client_secret = extract_client_credentials
95
+ client_id, = extract_client_credentials
96
96
  return render_introspection_error unless client_id
97
97
 
98
98
  begin
@@ -112,7 +112,7 @@ module ActionMCP
112
112
  return head :bad_request unless token
113
113
 
114
114
  # Authenticate client
115
- client_id, client_secret = extract_client_credentials
115
+ client_id, = extract_client_credentials
116
116
  return head :unauthorized unless client_id
117
117
 
118
118
  begin
@@ -127,9 +127,9 @@ module ActionMCP
127
127
 
128
128
  def check_oauth_enabled
129
129
  auth_methods = ActionMCP.configuration.authentication_methods
130
- unless auth_methods&.include?("oauth")
131
- head :not_found
132
- end
130
+ return if auth_methods&.include?("oauth")
131
+
132
+ head :not_found
133
133
  end
134
134
 
135
135
  def oauth_config
@@ -144,9 +144,7 @@ module ActionMCP
144
144
  code_verifier = params[:code_verifier]
145
145
 
146
146
  # Extract client credentials from Authorization header if not in params
147
- if client_id.blank?
148
- client_id, client_secret = extract_client_credentials
149
- end
147
+ client_id, client_secret = extract_client_credentials if client_id.blank?
150
148
 
151
149
  return render_token_error("invalid_request", "Missing required parameters") if code.blank? || client_id.blank?
152
150
 
@@ -170,7 +168,10 @@ module ActionMCP
170
168
  client_id ||= params[:client_id]
171
169
  client_secret ||= params[:client_secret]
172
170
 
173
- return render_token_error("invalid_request", "Missing required parameters") if refresh_token.blank? || client_id.blank?
171
+ if refresh_token.blank? || client_id.blank?
172
+ return render_token_error("invalid_request",
173
+ "Missing required parameters")
174
+ end
174
175
 
175
176
  token_response = ActionMCP::OAuth::Provider.refresh_access_token(
176
177
  refresh_token: refresh_token,
@@ -251,7 +252,7 @@ module ActionMCP
251
252
  render json: { active: false }, status: :bad_request
252
253
  end
253
254
 
254
- def render_consent_page(client_id, redirect_uri, scope, state, code_challenge, code_challenge_method)
255
+ def render_consent_page(_client_id, _redirect_uri, _scope, _state, _code_challenge, _code_challenge_method)
255
256
  # In production, this would render a proper consent page
256
257
  # For now, just auto-deny unknown clients
257
258
  render json: {
@@ -25,13 +25,9 @@ module ActionMCP
25
25
  }
26
26
 
27
27
  # Add optional fields based on configuration
28
- if oauth_config[:enable_dynamic_registration]
29
- metadata[:registration_endpoint] = registration_endpoint
30
- end
28
+ metadata[:registration_endpoint] = registration_endpoint if oauth_config[:enable_dynamic_registration]
31
29
 
32
- if oauth_config[:jwks_uri]
33
- metadata[:jwks_uri] = oauth_config[:jwks_uri]
34
- end
30
+ metadata[:jwks_uri] = oauth_config[:jwks_uri] if oauth_config[:jwks_uri]
35
31
 
36
32
  render json: metadata
37
33
  end
@@ -54,9 +50,9 @@ module ActionMCP
54
50
 
55
51
  def check_oauth_enabled
56
52
  auth_methods = ActionMCP.configuration.authentication_methods
57
- unless auth_methods&.include?("oauth")
58
- head :not_found
59
- end
53
+ return if auth_methods&.include?("oauth")
54
+
55
+ head :not_found
60
56
  end
61
57
 
62
58
  def oauth_config
@@ -99,7 +95,7 @@ module ActionMCP
99
95
  end
100
96
 
101
97
  def token_endpoint_auth_methods_supported
102
- methods = [ "client_secret_basic", "client_secret_post" ]
98
+ methods = %w[client_secret_basic client_secret_post]
103
99
  methods << "none" if oauth_config[:allow_public_clients]
104
100
  methods
105
101
  end
@@ -23,9 +23,7 @@ module ActionMCP
23
23
  client_secret = nil # Public clients by default
24
24
 
25
25
  # Generate client secret for confidential clients
26
- if client_metadata["token_endpoint_auth_method"] != "none"
27
- client_secret = generate_client_secret
28
- end
26
+ client_secret = generate_client_secret if client_metadata["token_endpoint_auth_method"] != "none"
29
27
 
30
28
  # Store client registration
31
29
  client_info = {
@@ -72,15 +70,15 @@ module ActionMCP
72
70
 
73
71
  def check_oauth_enabled
74
72
  auth_methods = ActionMCP.configuration.authentication_methods
75
- unless auth_methods&.include?("oauth")
76
- head :not_found
77
- end
73
+ return if auth_methods&.include?("oauth")
74
+
75
+ head :not_found
78
76
  end
79
77
 
80
78
  def check_registration_enabled
81
- unless oauth_config[:enable_dynamic_registration]
82
- head :not_found
83
- end
79
+ return if oauth_config[:enable_dynamic_registration]
80
+
81
+ head :not_found
84
82
  end
85
83
 
86
84
  def oauth_config
@@ -129,23 +127,20 @@ module ActionMCP
129
127
  end
130
128
 
131
129
  # Validate token endpoint auth method
132
- if metadata["token_endpoint_auth_method"]
133
- unless supported_auth_methods.include?(metadata["token_endpoint_auth_method"])
134
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported token endpoint auth method"
135
- end
136
- end
130
+ return unless metadata["token_endpoint_auth_method"]
131
+ return if supported_auth_methods.include?(metadata["token_endpoint_auth_method"])
132
+
133
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported token endpoint auth method"
137
134
  end
138
135
 
139
136
  def validate_redirect_uri(uri)
140
137
  parsed = URI.parse(uri)
141
138
 
142
139
  # Must be absolute URI
143
- unless parsed.absolute?
144
- raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must be absolute"
145
- end
140
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must be absolute" unless parsed.absolute?
146
141
 
147
142
  # For non-localhost, must use HTTPS
148
- unless parsed.host == "localhost" || parsed.host == "127.0.0.1" || parsed.scheme == "https"
143
+ unless [ "localhost", "127.0.0.1" ].include?(parsed.host) || parsed.scheme == "https"
149
144
  raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must use HTTPS"
150
145
  end
151
146
  rescue URI::InvalidURIError
@@ -162,7 +157,7 @@ module ActionMCP
162
157
  SecureRandom.urlsafe_base64(32)
163
158
  end
164
159
 
165
- def generate_registration_access_token(client_id)
160
+ def generate_registration_access_token(_client_id)
166
161
  # Generate a token for managing this registration
167
162
  SecureRandom.urlsafe_base64(32)
168
163
  end
@@ -183,7 +178,7 @@ module ActionMCP
183
178
  end
184
179
 
185
180
  def supported_auth_methods
186
- methods = [ "client_secret_basic", "client_secret_post" ]
181
+ methods = %w[client_secret_basic client_secret_post]
187
182
  methods << "none" if oauth_config.fetch(:allow_public_clients, true)
188
183
  methods
189
184
  end