Class: ActionMCP::ApplicationController
- Inherits:
-
ActionController::API
- Object
- ActionController::API
- ActionMCP::ApplicationController
- Includes:
- ActionController::Instrumentation, JSONRPC_Rails::ControllerHelpers
- Defined in:
- app/controllers/action_mcp/application_controller.rb
Overview
Implements the MCP endpoints according to the 2025-11-25 specification. POST returns one JSON message, DELETE terminates sessions, and GET returns 405 because the built-in transport does not provide SSE streams.
Constant Summary collapse
- MCP_SESSION_ID_HEADER =
"Mcp-Session-Id"- POST_ACCEPT_MEDIA_TYPES =
%w[application/json text/event-stream].freeze
Instance Method Summary collapse
-
#create ⇒ Object
Handles POST requests containing one client JSON-RPC message.
-
#destroy ⇒ Object
Handles DELETE requests for session termination.
-
#mcp_session ⇒ ActionMCP::Session?
Provides the ActionMCP::Session for the current request.
-
#session_key ⇒ String
Provides a unique key for caching or pub/sub based on the session ID.
-
#show ⇒ Object
Handles GET requests - returns 405 Method Not Allowed as per MCP spec.
Instance Method Details
#create ⇒ Object
Handles POST requests containing one client JSON-RPC message. rails-lens:routes:begin ROUTE: /, name: mcp_post, via: POST rails-lens:routes:end
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'app/controllers/action_mcp/application_controller.rb', line 61 def create unless post_accept_headers_valid? id = extract_jsonrpc_id_from_request return render_not_acceptable(, id) end payload = jsonrpc_params is_initialize_request = initialize_request?(payload) session_id = extract_session_id return unless authenticate_gateway! if (validation_error = ProtocolValidator.(payload)) if payload.is_a?(JSON_RPC::Notification) return render_notification_error(validation_error) end return render_jsonrpc_error(validation_error.code, validation_error., request_id(payload)) end session = if is_initialize_request if session_id existing_session = load_session(session_id) return render_not_found("Session not found.", request_id(payload)) unless existing_session if existing_session.status == "closed" return render_not_found("Session has been terminated.", request_id(payload)) end return render_bad_request("Initialize requests must not include an Mcp-Session-Id header.", request_id(payload)) end create_session else return render_bad_request("Mcp-Session-Id header is required for this request.", request_id(payload)) unless session_id existing_session = load_session(session_id) return render_not_found("Session not found.", request_id(payload)) unless existing_session if existing_session.status == "closed" return render_not_found("Session has been terminated.", request_id(payload)) end existing_session end @mcp_session = session unless configure_gateway_session!(session) Server.session_store.delete_session(session.id) if is_initialize_request return end return unless is_initialize_request || validate_protocol_version_header(session) return unless lifecycle_allows?(payload, session, is_initialize_request) # Use return mode for the transport handler when we need to capture responses transport_handler = Server::TransportHandler.new(session, messaging_mode: :return) json_rpc_handler = Server::JsonRpcHandler.new(transport_handler) result = json_rpc_handler.call(jsonrpc_params) process_handler_results(result, session, is_initialize_request) rescue StandardError => e Server.session_store.delete_session(session.id) if is_initialize_request && session Rails.error.report(e, handled: true, severity: :error) id = begin jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil rescue StandardError nil end render_internal_server_error("An unexpected error occurred.", id) unless performed? end |
#destroy ⇒ Object
Handles DELETE requests for session termination. rails-lens:routes:begin ROUTE: /, name: mcp_delete, via: DELETE rails-lens:routes:end
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'app/controllers/action_mcp/application_controller.rb', line 134 def destroy return unless authenticate_gateway! session_id_from_header = extract_session_id return render_bad_request("Mcp-Session-Id header is required for DELETE requests.") unless session_id_from_header session = load_session(session_id_from_header) if session.nil? return render_not_found("Session not found.") elsif session.status == "closed" return render_not_found("Session has been terminated.") end @mcp_session = session return unless configure_gateway_session!(session) return unless validate_protocol_version_header(session) begin session.close! Rails.logger.info "Unified DELETE: Terminated session: #{session.id}" if ActionMCP.configuration.verbose_logging head :no_content rescue StandardError => e Rails.error.report(e, handled: true, severity: :error) render_internal_server_error("Failed to terminate session.") end end |
#mcp_session ⇒ ActionMCP::Session?
Provides the ActionMCP::Session for the current request.
20 21 22 23 24 |
# File 'app/controllers/action_mcp/application_controller.rb', line 20 def mcp_session return @mcp_session if defined?(@mcp_session) @mcp_session = load_session(extract_session_id) end |
#session_key ⇒ String
Provides a unique key for caching or pub/sub based on the session ID. Ensures mcp_session is called first to establish the session ID.
29 30 31 |
# File 'app/controllers/action_mcp/application_controller.rb', line 29 def session_key @session_key ||= "action_mcp-sessions-#{mcp_session.id}" end |
#show ⇒ Object
Handles GET requests - returns 405 Method Not Allowed as per MCP spec. SSE streaming is not supported. Clients should use Tasks for async operations. rails-lens:routes:begin ROUTE: /, name: mcp_get, via: GET rails-lens:routes:end
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'app/controllers/action_mcp/application_controller.rb', line 38 def show return unless authenticate_gateway! if (session_id = extract_session_id) session = load_session(session_id) return render_not_found("Session not found.") unless session return render_not_found("Session has been terminated.") if session.status == "closed" @mcp_session = session return unless configure_gateway_session!(session) return unless validate_protocol_version_header(session) end # MCP Streamable HTTP spec allows servers to return 405 if they don't support SSE. # ActionMCP uses Tasks for async operations instead of SSE streaming. response.headers["Allow"] = "POST, DELETE" head :method_not_allowed end |