actionmcp 0.71.0 → 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.
- checksums.yaml +4 -4
- data/README.md +186 -15
- data/app/controllers/action_mcp/application_controller.rb +47 -40
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +11 -10
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +6 -10
- data/app/controllers/action_mcp/oauth/registration_controller.rb +15 -20
- data/app/models/action_mcp/oauth_client.rb +7 -5
- data/app/models/action_mcp/oauth_token.rb +2 -1
- data/app/models/action_mcp/session.rb +40 -5
- data/config/routes.rb +4 -2
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +17 -8
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +7 -5
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +3 -1
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +12 -13
- data/lib/action_mcp/client/collection.rb +3 -3
- 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/jwt_client_provider.rb +6 -5
- data/lib/action_mcp/client/oauth_client_provider.rb +8 -8
- data/lib/action_mcp/client/streamable_http_transport.rb +63 -27
- data/lib/action_mcp/client.rb +19 -4
- data/lib/action_mcp/configuration.rb +28 -53
- data/lib/action_mcp/engine.rb +5 -1
- data/lib/action_mcp/filtered_logger.rb +1 -1
- data/lib/action_mcp/gateway.rb +47 -137
- data/lib/action_mcp/gateway_identifier.rb +29 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/jwt_decoder.rb +4 -2
- data/lib/action_mcp/jwt_identifier.rb +28 -0
- data/lib/action_mcp/none_identifier.rb +19 -0
- data/lib/action_mcp/o_auth_identifier.rb +34 -0
- data/lib/action_mcp/oauth/active_record_storage.rb +1 -1
- data/lib/action_mcp/oauth/memory_storage.rb +1 -3
- data/lib/action_mcp/oauth/middleware.rb +13 -18
- data/lib/action_mcp/oauth/provider.rb +45 -65
- data/lib/action_mcp/omniauth/mcp_strategy.rb +23 -37
- 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} +39 -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 +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +1 -0
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +24 -18
- data/lib/action_mcp/server/notifications.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2875f1eaab23887a9cafbff393229ab35a733822b4b20487cff5b4556cd602c9
|
4
|
+
data.tar.gz: ac60fbba55e7e06960644c70f790cd0f4f8909d1b6d1137b3f002bdeda29575c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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**.
|
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
|
-
|
53
|
+
```bash
|
54
|
+
# Add gem to your Gemfile
|
55
|
+
$ bundle add actionmcp
|
46
56
|
|
47
|
-
|
48
|
-
|
49
|
-
```
|
57
|
+
# Install dependencies
|
58
|
+
bundle install
|
50
59
|
|
51
|
-
|
60
|
+
# Copy migrations from the engine
|
61
|
+
bin/rails action_mcp:install:migrations
|
52
62
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
bin/rails db:migrate
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
429
|
-
|
430
|
-
|
431
|
-
Rails.logger.debug "Cleaned up #{count} old SSE events for session: #{session.id}"
|
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,15 +514,15 @@ 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
|
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
|
513
522
|
return unless gateway_class # Skip if no gateway configured
|
514
523
|
|
515
|
-
gateway = gateway_class.new
|
516
|
-
gateway.call
|
524
|
+
gateway = gateway_class.new(request)
|
525
|
+
gateway.call
|
517
526
|
rescue ActionMCP::UnauthorizedError => e
|
518
527
|
render_unauthorized(e.message)
|
519
528
|
end
|
@@ -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,
|
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,
|
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
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
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(
|
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
|
-
|
58
|
-
|
59
|
-
|
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 = [
|
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
|
-
|
76
|
-
|
77
|
-
|
73
|
+
return if auth_methods&.include?("oauth")
|
74
|
+
|
75
|
+
head :not_found
|
78
76
|
end
|
79
77
|
|
80
78
|
def check_registration_enabled
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
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(
|
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 = [
|
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
|