Module: Hitch::MCP::Internal::CorsPolicy

Defined in:
app/models/hitch/mcp/internal/cors_policy.rb

Overview

CORS decisions for the endpoint: an exact origin allowlist (plus loopback in development and test), and a fixed preflight contract — POST only, a closed request-header set.

Constant Summary collapse

ALLOWED_REQUEST_HEADERS =
%w[
  Content-Type
  Authorization
  MCP-Protocol-Version
  Mcp-Method
  Mcp-Name
].freeze
LOOPBACK_ORIGIN =
%r{\Ahttps?://(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\z}
PREFLIGHT_RESPONSE_HEADERS =
{
  "Access-Control-Allow-Methods" => "POST",
  "Access-Control-Allow-Headers" => ALLOWED_REQUEST_HEADERS.join(", "),
  "Access-Control-Max-Age" => "600"
}.freeze

Class Method Summary collapse

Class Method Details

.origin_allowed?(origin) ⇒ Boolean

Returns:

  • (Boolean)


26
27
28
29
30
31
32
# File 'app/models/hitch/mcp/internal/cors_policy.rb', line 26

def origin_allowed?(origin)
  return false unless origin.is_a?(String) && origin.valid_encoding?
  return false if origin.empty? || origin.include?(",") || HeaderField::CONTROLS.match?(origin)
  return true if Hitch.configuration.allowed_origins.include?(origin)

  Rails.env.local? && LOOPBACK_ORIGIN.match?(origin)
end

.preflight_allowed?(requested_method:, requested_headers:) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
37
38
39
40
41
# File 'app/models/hitch/mcp/internal/cors_policy.rb', line 34

def preflight_allowed?(requested_method:, requested_headers:)
  method = HeaderField.single(requested_method)
  headers = requested_header_list(requested_headers)
  return false unless method == "POST" && headers

  allowed = ALLOWED_REQUEST_HEADERS.map(&:downcase)
  headers.all? { |header| allowed.include?(header.downcase) }
end

.requested_header_list(value) ⇒ Object



43
44
45
46
47
48
49
# File 'app/models/hitch/mcp/internal/cors_policy.rb', line 43

def requested_header_list(value)
  return [] if value.nil? || value.empty?
  return unless value.is_a?(String) && value.valid_encoding? && !HeaderField::CONTROLS.match?(value)

  values = value.split(",", -1).map { |entry| HeaderField.trim_ows(entry) }
  values unless values.any? { |entry| entry.nil? || entry.empty? }
end