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-03-26 specification. Supports GET for server-initiated SSE streams, POST for client messages (responding with JSON or SSE), and optionally DELETE for session termination.

Constant Summary collapse

MCP_SESSION_ID_HEADER =
"Mcp-Session-Id"

Instance Method Summary collapse

Instance Method Details

#createObject

Handles POST requests containing client JSON-RPC messages according to 2025-03-26 spec. <rails-lens:routes:begin> ROUTE: /, name: mcp_post, via: POST <rails-lens:routes:end>



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
# File 'app/controllers/action_mcp/application_controller.rb', line 44

def create
  unless post_accept_headers_valid?
    id = extract_jsonrpc_id_from_request
    return render_not_acceptable(post_accept_headers_error_message, id)
  end

  # Reject JSON-RPC batch requests as per MCP 2025-06-18 spec
  return render_bad_request("JSON-RPC batch requests are not supported", nil) if jsonrpc_params_batch?

  is_initialize_request = check_if_initialize_request(jsonrpc_params)
  session_initially_missing = extract_session_id.nil?
  session = mcp_session

  # Validate MCP-Protocol-Version header for non-initialize requests
  return unless validate_protocol_version_header

  unless initialization_related_request?(jsonrpc_params)
    if session_initially_missing
      id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
      return render_bad_request("Mcp-Session-Id header is required for this request.", id)
    elsif session.nil? || session.new_record?
      id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
      return render_not_found("Session not found.", id)
    elsif session.status == "closed"
      id = jsonrpc_params.respond_to?(:id) ? jsonrpc_params.id : nil
      return render_not_found("Session has been terminated.", id)
    end
  end

  if session.new_record?
    session.save!
    response.headers[MCP_SESSION_ID_HEADER] = session.id
  end

  # Authenticate the request via gateway (skipped for initialization-related requests)
  if initialization_related_request?(jsonrpc_params)
    # Skipping authentication for initialization request: #{jsonrpc_params.method}
  else
    authenticate_gateway!
    return if performed?
  end

  # 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, session_initially_missing, is_initialize_request)
rescue StandardError => e
  Rails.logger.error "Unified POST Error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}"
  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 (2025-03-26 spec). <rails-lens:routes:begin> ROUTE: /, name: mcp_delete, via: DELETE <rails-lens:routes:end>



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'app/controllers/action_mcp/application_controller.rb', line 106

def destroy
  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 = Server.session_store.load_session(session_id_from_header)
  if session.nil?
    return render_not_found("Session not found.")
  elsif session.status == "closed"
    return head :no_content
  end

  # Authenticate the request via gateway
  authenticate_gateway!
  return if performed?

  begin
    session.close!
    Rails.logger.info "Unified DELETE: Terminated session: #{session.id}" if ActionMCP.configuration.verbose_logging
    head :no_content
  rescue StandardError => e
    Rails.logger.error "Unified DELETE: Error terminating session #{session.id}: #{e.class} - #{e.message}"
    render_internal_server_error("Failed to terminate session.")
  end
end

#mcp_sessionActionMCP::Session

Provides the ActionMCP::Session for the current request. Handles finding existing sessions via header/param or initializing a new one. Specific controllers/handlers might need to enforce session ID presence based on context.

Returns:



18
19
20
# File 'app/controllers/action_mcp/application_controller.rb', line 18

def mcp_session
  @mcp_session ||= find_or_initialize_session
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.



25
26
27
# File 'app/controllers/action_mcp/application_controller.rb', line 25

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>



34
35
36
37
38
# File 'app/controllers/action_mcp/application_controller.rb', line 34

def show
  # 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.
  head :method_not_allowed
end