Class: ActionMCP::ApplicationController

Inherits:
ActionController::API
  • Object
show all
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

Instance Method Details

#createObject

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(post_accept_headers_error_message, id)
  end

  payload = jsonrpc_params
  is_initialize_request = initialize_request?(payload)
  session_id = extract_session_id

  return unless authenticate_gateway!

  if (validation_error = ProtocolValidator.client_message_validation_error(payload))
    if payload.is_a?(JSON_RPC::Notification)
      return render_notification_error(validation_error)
    end

    return render_jsonrpc_error(validation_error.code, validation_error.message, 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

#destroyObject

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_sessionActionMCP::Session?

Provides the ActionMCP::Session for the current request.

Returns:



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_keyString

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.

Returns:

  • (String)

    The session key string.



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

#showObject

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