actionmcp 0.71.1 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +187 -16
- data/app/controllers/action_mcp/application_controller.rb +64 -49
- data/app/models/action_mcp/session/message.rb +31 -20
- data/app/models/action_mcp/session/resource.rb +35 -20
- data/app/models/action_mcp/session/sse_event.rb +23 -17
- data/app/models/action_mcp/session/subscription.rb +22 -15
- data/app/models/action_mcp/session.rb +71 -113
- data/config/routes.rb +0 -11
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +9 -11
- data/lib/action_mcp/client/elicitation.rb +4 -4
- data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
- data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
- data/lib/action_mcp/client.rb +6 -26
- data/lib/action_mcp/configuration.rb +65 -63
- data/lib/action_mcp/engine.rb +1 -10
- data/lib/action_mcp/filtered_logger.rb +3 -7
- data/lib/action_mcp/gateway.rb +7 -11
- data/lib/action_mcp/gateway_identifier.rb +187 -3
- data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
- data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
- data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
- data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
- data/lib/action_mcp/gateway_identifiers.rb +26 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/prompt.rb +2 -0
- data/lib/action_mcp/renderable.rb +1 -1
- data/lib/action_mcp/resource_template.rb +6 -2
- data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
- data/lib/action_mcp/server/base_session_store.rb +86 -0
- data/lib/action_mcp/server/capabilities.rb +2 -1
- data/lib/action_mcp/server/elicitation.rb +3 -9
- data/lib/action_mcp/server/error_handling.rb +14 -1
- data/lib/action_mcp/server/handlers/router.rb +31 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
- data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
- data/lib/action_mcp/server/prompts.rb +4 -4
- data/lib/action_mcp/server/resources.rb +23 -4
- data/lib/action_mcp/server/session_store_factory.rb +1 -1
- data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
- data/lib/action_mcp/server/tools.rb +62 -43
- data/lib/action_mcp/server/transport_handler.rb +2 -4
- data/lib/action_mcp/server/volatile_session_store.rb +1 -93
- data/lib/action_mcp/tagged_stream_logging.rb +2 -2
- data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
- data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
- data/lib/action_mcp/tool.rb +48 -37
- data/lib/action_mcp/types/float_array_type.rb +5 -3
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -7
- data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
- data/lib/generators/action_mcp/install/install_generator.rb +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +18 -100
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
- data/app/models/action_mcp/oauth_client.rb +0 -157
- data/app/models/action_mcp/oauth_token.rb +0 -141
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
- data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
- data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
- data/lib/action_mcp/jwt_decoder.rb +0 -26
- data/lib/action_mcp/jwt_identifier.rb +0 -28
- data/lib/action_mcp/none_identifier.rb +0 -19
- data/lib/action_mcp/o_auth_identifier.rb +0 -34
- data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
- data/lib/action_mcp/oauth/error.rb +0 -79
- data/lib/action_mcp/oauth/memory_storage.rb +0 -134
- data/lib/action_mcp/oauth/middleware.rb +0 -133
- data/lib/action_mcp/oauth/provider.rb +0 -426
- data/lib/action_mcp/oauth.rb +0 -12
- data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
- data/lib/action_mcp/server/notifications.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea96963047c8bb5e9fa9fd88163a14798c8780a531fa1600e3681bd92ca1b4fe
|
4
|
+
data.tar.gz: aae9fc897bd554fba0442735b184d9eac03a381c3b07e052869ce76ce714555d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 47947a7bba2e39a33b1793cbf8af4a38dcf26e0eebcc19989b143d4279f8f71fc05c646169d4739dd8e52f23c2f34df5a232d4cd414e6872cc636ab4c5a22ae5
|
7
|
+
data.tar.gz: 8e2b06ddaf6b1174035dc3e4a6315aaf8d624ac5a096d647f252c40c533f5d20ebf02b3b8b5355d81617de98dfccf63d4342ec540558f8dbc5072043aee11c4a
|
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
|
@@ -524,7 +576,7 @@ This will create:
|
|
524
576
|
|
525
577
|
ActionMCP provides a Gateway system similar to ActionCable's Connection for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components.
|
526
578
|
|
527
|
-
ActionMCP
|
579
|
+
ActionMCP uses a Gateway pattern with pluggable identifiers for authentication. You can implement custom authentication strategies using session-based auth, API keys, bearer tokens, or integrate with existing authentication systems like Warden, Devise, or external OAuth providers.
|
528
580
|
|
529
581
|
### Creating an ApplicationGateway
|
530
582
|
|
@@ -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.
|
@@ -29,7 +28,9 @@ module ActionMCP
|
|
29
28
|
end
|
30
29
|
|
31
30
|
# Handles GET requests for establishing server-initiated SSE streams (2025-03-26 spec).
|
32
|
-
#
|
31
|
+
# <rails-lens:routes:begin>
|
32
|
+
# ROUTE: /, name: mcp_get, via: GET
|
33
|
+
# <rails-lens:routes:end>
|
33
34
|
def show
|
34
35
|
unless request.accepts.any? { |type| type.to_s == "text/event-stream" }
|
35
36
|
return render_not_acceptable("Client must accept 'text/event-stream' for GET requests.")
|
@@ -52,7 +53,9 @@ module ActionMCP
|
|
52
53
|
return if performed?
|
53
54
|
|
54
55
|
last_event_id = request.headers["Last-Event-ID"].presence
|
55
|
-
|
56
|
+
if last_event_id && ActionMCP.configuration.verbose_logging
|
57
|
+
Rails.logger.info "Unified SSE (GET): Resuming from Last-Event-ID: #{last_event_id}"
|
58
|
+
end
|
56
59
|
|
57
60
|
response.headers["Content-Type"] = "text/event-stream"
|
58
61
|
response.headers["X-Accel-Buffering"] = "no"
|
@@ -61,7 +64,9 @@ module ActionMCP
|
|
61
64
|
# Add MCP-Protocol-Version header for established sessions
|
62
65
|
response.headers["MCP-Protocol-Version"] = session.protocol_version
|
63
66
|
|
64
|
-
|
67
|
+
if ActionMCP.configuration.verbose_logging
|
68
|
+
Rails.logger.info "Unified SSE (GET): Starting stream for session: #{session.id}"
|
69
|
+
end
|
65
70
|
|
66
71
|
sse = SSE.new(response.stream)
|
67
72
|
listener = SSEListener.new(session)
|
@@ -85,12 +90,16 @@ module ActionMCP
|
|
85
90
|
begin
|
86
91
|
missed_events = session.get_sse_events_after(last_event_id.to_i)
|
87
92
|
if missed_events.any?
|
88
|
-
|
93
|
+
if ActionMCP.configuration.verbose_logging
|
94
|
+
Rails.logger.info "Unified SSE (GET): Sending #{missed_events.size} missed events for session: #{session.id}"
|
95
|
+
end
|
89
96
|
missed_events.each do |event|
|
90
97
|
sse.write(event.to_sse)
|
91
98
|
end
|
92
|
-
|
93
|
-
|
99
|
+
elsif ActionMCP.configuration.verbose_logging
|
100
|
+
if ActionMCP.configuration.verbose_logging
|
101
|
+
Rails.logger.info "Unified SSE (GET): No missed events to send for session: #{session.id}"
|
102
|
+
end
|
94
103
|
end
|
95
104
|
rescue StandardError => e
|
96
105
|
Rails.logger.error "Unified SSE (GET): Error sending missed events: #{e.message}"
|
@@ -116,7 +125,9 @@ module ActionMCP
|
|
116
125
|
Rails.logger.warn "Unified SSE (GET): Heartbeat timed out for session: #{session.id}, closing."
|
117
126
|
connection_active.make_false
|
118
127
|
rescue StandardError => e
|
119
|
-
|
128
|
+
if ActionMCP.configuration.verbose_logging
|
129
|
+
Rails.logger.debug "Unified SSE (GET): Heartbeat error for session: #{session.id}: #{e.message}"
|
130
|
+
end
|
120
131
|
connection_active.make_false
|
121
132
|
end
|
122
133
|
else
|
@@ -127,11 +138,15 @@ module ActionMCP
|
|
127
138
|
heartbeat_task = Concurrent::ScheduledTask.execute(heartbeat_interval, &heartbeat_sender)
|
128
139
|
sleep 0.1 while connection_active.true? && !response.stream.closed?
|
129
140
|
rescue ActionController::Live::ClientDisconnected, IOError => e
|
130
|
-
|
141
|
+
if ActionMCP.configuration.verbose_logging
|
142
|
+
Rails.logger.debug "Unified SSE (GET): Client disconnected for session: #{session&.id}: #{e.message}"
|
143
|
+
end
|
131
144
|
rescue StandardError => e
|
132
145
|
Rails.logger.error "Unified SSE (GET): Unexpected error for session: #{session&.id}: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
|
133
146
|
ensure
|
134
|
-
|
147
|
+
if ActionMCP.configuration.verbose_logging
|
148
|
+
Rails.logger.debug "Unified SSE (GET): Cleaning up connection for session: #{session&.id}"
|
149
|
+
end
|
135
150
|
heartbeat_active&.make_false
|
136
151
|
heartbeat_task&.cancel
|
137
152
|
listener&.stop
|
@@ -145,7 +160,9 @@ module ActionMCP
|
|
145
160
|
end
|
146
161
|
|
147
162
|
# Handles POST requests containing client JSON-RPC messages according to 2025-03-26 spec.
|
148
|
-
#
|
163
|
+
# <rails-lens:routes:begin>
|
164
|
+
# ROUTE: /, name: mcp_post, via: POST
|
165
|
+
# <rails-lens:routes:end>
|
149
166
|
def create
|
150
167
|
unless post_accept_headers_valid?
|
151
168
|
id = extract_jsonrpc_id_from_request
|
@@ -153,9 +170,7 @@ module ActionMCP
|
|
153
170
|
end
|
154
171
|
|
155
172
|
# 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
|
173
|
+
return render_bad_request("JSON-RPC batch requests are not supported", nil) if jsonrpc_params_batch?
|
159
174
|
|
160
175
|
is_initialize_request = check_if_initialize_request(jsonrpc_params)
|
161
176
|
session_initially_missing = extract_session_id.nil?
|
@@ -197,7 +212,9 @@ module ActionMCP
|
|
197
212
|
result = json_rpc_handler.call(jsonrpc_params)
|
198
213
|
process_handler_results(result, session, session_initially_missing, is_initialize_request)
|
199
214
|
rescue ActionController::Live::ClientDisconnected, IOError => e
|
200
|
-
|
215
|
+
if ActionMCP.configuration.verbose_logging
|
216
|
+
Rails.logger.debug "Unified SSE (POST): Client disconnected during response: #{e.message}"
|
217
|
+
end
|
201
218
|
begin
|
202
219
|
response.stream&.close
|
203
220
|
rescue StandardError
|
@@ -205,12 +222,18 @@ module ActionMCP
|
|
205
222
|
end
|
206
223
|
rescue StandardError => e
|
207
224
|
Rails.logger.error "Unified POST Error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
|
208
|
-
id =
|
225
|
+
id = begin
|
226
|
+
jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
|
227
|
+
rescue StandardError
|
228
|
+
nil
|
229
|
+
end
|
209
230
|
render_internal_server_error("An unexpected error occurred.", id) unless performed?
|
210
231
|
end
|
211
232
|
|
212
233
|
# Handles DELETE requests for session termination (2025-03-26 spec).
|
213
|
-
#
|
234
|
+
# <rails-lens:routes:begin>
|
235
|
+
# ROUTE: /, name: mcp_delete, via: DELETE
|
236
|
+
# <rails-lens:routes:end>
|
214
237
|
def destroy
|
215
238
|
session_id_from_header = extract_session_id
|
216
239
|
return render_bad_request("Mcp-Session-Id header is required for DELETE requests.") unless session_id_from_header
|
@@ -255,9 +278,7 @@ module ActionMCP
|
|
255
278
|
end
|
256
279
|
|
257
280
|
# 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
|
281
|
+
header_version = header_version.last if header_version.is_a?(Array)
|
261
282
|
|
262
283
|
# Check if the header version is supported
|
263
284
|
unless ActionMCP::SUPPORTED_VERSIONS.include?(header_version)
|
@@ -268,7 +289,7 @@ module ActionMCP
|
|
268
289
|
end
|
269
290
|
|
270
291
|
# If we have an initialized session, check if the header matches the negotiated version
|
271
|
-
if session
|
292
|
+
if session&.initialized?
|
272
293
|
negotiated_version = session.protocol_version
|
273
294
|
if header_version != negotiated_version
|
274
295
|
ActionMCP.logger.warn "MCP-Protocol-Version mismatch: header=#{header_version}, negotiated=#{negotiated_version}"
|
@@ -340,9 +361,7 @@ module ActionMCP
|
|
340
361
|
# Processes the results from the JsonRpcHandler.
|
341
362
|
def process_handler_results(result, session, session_initially_missing, is_initialize_request)
|
342
363
|
# Handle empty result (notifications)
|
343
|
-
if result.nil?
|
344
|
-
return head :accepted
|
345
|
-
end
|
364
|
+
return head :accepted if result.nil?
|
346
365
|
|
347
366
|
# Convert to hash for rendering
|
348
367
|
payload = if result.respond_to?(:to_h)
|
@@ -369,9 +388,7 @@ module ActionMCP
|
|
369
388
|
def render_json_response(payload, session, add_session_header)
|
370
389
|
response.headers[MCP_SESSION_ID_HEADER] = session.id if add_session_header
|
371
390
|
# Add MCP-Protocol-Version header if session has been initialized
|
372
|
-
|
373
|
-
response.headers["MCP-Protocol-Version"] = session.protocol_version
|
374
|
-
end
|
391
|
+
response.headers["MCP-Protocol-Version"] = session.protocol_version if session&.initialized?
|
375
392
|
response.headers["Content-Type"] = "application/json"
|
376
393
|
render json: payload, status: :ok
|
377
394
|
end
|
@@ -380,9 +397,7 @@ module ActionMCP
|
|
380
397
|
def render_sse_response(payload, session, add_session_header)
|
381
398
|
response.headers[MCP_SESSION_ID_HEADER] = session.id if add_session_header
|
382
399
|
# Add MCP-Protocol-Version header if session has been initialized
|
383
|
-
|
384
|
-
response.headers["MCP-Protocol-Version"] = session.protocol_version
|
385
|
-
end
|
400
|
+
response.headers["MCP-Protocol-Version"] = session.protocol_version if session&.initialized?
|
386
401
|
response.headers["Content-Type"] = "text/event-stream"
|
387
402
|
response.headers["X-Accel-Buffering"] = "no"
|
388
403
|
response.headers["Cache-Control"] = "no-cache"
|
@@ -425,13 +440,13 @@ module ActionMCP
|
|
425
440
|
|
426
441
|
# Helper to clean up old SSE events for a session
|
427
442
|
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}"
|
443
|
+
retention_period = session.sse_event_retention_period
|
444
|
+
count = session.cleanup_old_sse_events(retention_period)
|
445
|
+
if count.positive? && ActionMCP.configuration.verbose_logging
|
446
|
+
Rails.logger.debug "Cleaned up #{count} old SSE events for session: #{session.id}"
|
434
447
|
end
|
448
|
+
rescue StandardError => e
|
449
|
+
Rails.logger.error "Error cleaning up old SSE events: #{e.message}"
|
435
450
|
end
|
436
451
|
|
437
452
|
def format_tools_list(tools, session)
|
@@ -505,30 +520,30 @@ module ActionMCP
|
|
505
520
|
# Authenticates the request using the configured gateway
|
506
521
|
def authenticate_gateway!
|
507
522
|
# Skip authentication for initialization-related requests in POST method
|
508
|
-
if request.post? && defined?(jsonrpc_params) && jsonrpc_params
|
509
|
-
return
|
523
|
+
if request.post? && defined?(jsonrpc_params) && jsonrpc_params && initialization_related_request?(jsonrpc_params)
|
524
|
+
return
|
510
525
|
end
|
511
526
|
|
512
527
|
gateway_class = ActionMCP.configuration.gateway_class
|
513
528
|
return unless gateway_class # Skip if no gateway configured
|
514
529
|
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
530
|
+
begin
|
531
|
+
gateway = gateway_class.new(request)
|
532
|
+
gateway.call
|
533
|
+
rescue ActionMCP::UnauthorizedError => e
|
534
|
+
render_unauthorized(e.message)
|
535
|
+
rescue StandardError => e
|
536
|
+
Rails.logger.error "Gateway authentication error: #{e.class} - #{e.message}"
|
537
|
+
render_unauthorized("Authentication system error")
|
538
|
+
end
|
519
539
|
end
|
520
540
|
|
521
541
|
# Renders an unauthorized response
|
522
542
|
def render_unauthorized(message = "Unauthorized", id = nil)
|
523
543
|
id ||= extract_jsonrpc_id_from_request
|
524
544
|
|
525
|
-
#
|
526
|
-
|
527
|
-
if auth_methods.include?("oauth")
|
528
|
-
response.headers["WWW-Authenticate"] = 'Bearer realm="MCP API"'
|
529
|
-
end
|
530
|
-
|
531
|
-
render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }, status: :unauthorized
|
545
|
+
# Return JSON-RPC error with 200 status as per MCP specification
|
546
|
+
render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }
|
532
547
|
end
|
533
548
|
end
|
534
549
|
end
|
@@ -1,29 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_session_messages"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
7
|
+
# columns = [
|
8
|
+
# { name = "id", type = "integer", primary_key = true, nullable = false },
|
9
|
+
# { name = "session_id", type = "string", nullable = false },
|
10
|
+
# { name = "direction", type = "string", nullable = false, default = "client" },
|
11
|
+
# { name = "message_type", type = "string", nullable = false },
|
12
|
+
# { name = "jsonrpc_id", type = "string", nullable = true },
|
13
|
+
# { name = "message_json", type = "json", nullable = true },
|
14
|
+
# { name = "is_ping", type = "boolean", nullable = false, default = "0" },
|
15
|
+
# { name = "request_acknowledged", type = "boolean", nullable = false, default = "0" },
|
16
|
+
# { name = "request_cancelled", type = "boolean", nullable = false, default = "0" },
|
17
|
+
# { name = "created_at", type = "datetime", nullable = false },
|
18
|
+
# { name = "updated_at", type = "datetime", nullable = false }
|
19
|
+
# ]
|
6
20
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# message_json :json
|
11
|
-
# message_type :string not null
|
12
|
-
# request_acknowledged :boolean default(FALSE), not null
|
13
|
-
# request_cancelled :boolean default(FALSE), not null
|
14
|
-
# created_at :datetime not null
|
15
|
-
# updated_at :datetime not null
|
16
|
-
# jsonrpc_id :string
|
17
|
-
# session_id :string not null
|
21
|
+
# indexes = [
|
22
|
+
# { name = "index_action_mcp_session_messages_on_session_id", columns = ["session_id"] }
|
23
|
+
# ]
|
18
24
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# Foreign Keys
|
24
|
-
#
|
25
|
-
# session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
|
25
|
+
# foreign_keys = [
|
26
|
+
# { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade", on_update = "cascade" }
|
27
|
+
# ]
|
26
28
|
#
|
29
|
+
# == Notes
|
30
|
+
# - Column 'message_json' should probably have NOT NULL constraint
|
31
|
+
# - String column 'session_id' has no length limit - consider adding one
|
32
|
+
# - String column 'direction' has no length limit - consider adding one
|
33
|
+
# - String column 'message_type' has no length limit - consider adding one
|
34
|
+
# - String column 'jsonrpc_id' has no length limit - consider adding one
|
35
|
+
# - Column 'message_type' is commonly used in queries - consider adding an index
|
36
|
+
# - Column 'is_ping' uses non-conventional prefix - consider removing 'is_' or 'has_'
|
37
|
+
# <rails-lens:schema:end>
|
27
38
|
module ActionMCP
|
28
39
|
class Session
|
29
40
|
#
|
@@ -1,29 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_session_resources"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
7
|
+
# columns = [
|
8
|
+
# { name = "id", type = "integer", primary_key = true, nullable = false },
|
9
|
+
# { name = "session_id", type = "string", nullable = false },
|
10
|
+
# { name = "uri", type = "string", nullable = false },
|
11
|
+
# { name = "name", type = "string", nullable = true },
|
12
|
+
# { name = "description", type = "text", nullable = true },
|
13
|
+
# { name = "mime_type", type = "string", nullable = false },
|
14
|
+
# { name = "created_by_tool", type = "boolean", nullable = true, default = "0" },
|
15
|
+
# { name = "last_accessed_at", type = "datetime", nullable = true },
|
16
|
+
# { name = "metadata", type = "json", nullable = true },
|
17
|
+
# { name = "created_at", type = "datetime", nullable = false },
|
18
|
+
# { name = "updated_at", type = "datetime", nullable = false }
|
19
|
+
# ]
|
6
20
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# last_accessed_at :datetime
|
11
|
-
# metadata :json
|
12
|
-
# mime_type :string not null
|
13
|
-
# name :string
|
14
|
-
# uri :string not null
|
15
|
-
# created_at :datetime not null
|
16
|
-
# updated_at :datetime not null
|
17
|
-
# session_id :string not null
|
21
|
+
# indexes = [
|
22
|
+
# { name = "index_action_mcp_session_resources_on_session_id", columns = ["session_id"] }
|
23
|
+
# ]
|
18
24
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# Foreign Keys
|
24
|
-
#
|
25
|
-
# session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade
|
25
|
+
# foreign_keys = [
|
26
|
+
# { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade" }
|
27
|
+
# ]
|
26
28
|
#
|
29
|
+
# == Notes
|
30
|
+
# - Association 'session' should specify inverse_of
|
31
|
+
# - Column 'name' should probably have NOT NULL constraint
|
32
|
+
# - Column 'description' should probably have NOT NULL constraint
|
33
|
+
# - Column 'created_by_tool' should probably have NOT NULL constraint
|
34
|
+
# - Column 'metadata' should probably have NOT NULL constraint
|
35
|
+
# - String column 'session_id' has no length limit - consider adding one
|
36
|
+
# - String column 'uri' has no length limit - consider adding one
|
37
|
+
# - String column 'name' has no length limit - consider adding one
|
38
|
+
# - String column 'mime_type' has no length limit - consider adding one
|
39
|
+
# - Large text column 'description' is frequently queried - consider separate storage
|
40
|
+
# - Column 'mime_type' is commonly used in queries - consider adding an index
|
41
|
+
# <rails-lens:schema:end>
|
27
42
|
module ActionMCP
|
28
43
|
class Session
|
29
44
|
#
|