Module: Otto::MCP::Options
- Defined in:
- lib/otto/mcp/options.rb
Overview
Normalization of MCP configuration options.
Otto.new and Otto#enable_mcp! historically accepted different spellings of the same settings and the constructor dropped all but the endpoint (issue #258). Every path now funnels through Options.normalize.
One vocabulary: the canonical keys, their +mcp_+-prefixed variants (the
constructor needs a namespace inside an options hash that also
configures the rest of Otto), and tool_calls_per_minute, which is the
name the rate-limiting middleware itself uses. Bare generic names such
as endpoint, validation and rate_limiting are NOT accepted:
rate_limiting: is Otto's own general rate-limiting option (a Hash),
and the others were documented before #258 but never read.
Two strictness rules, selected with the scope argument:
:constructor (used by Otto.new / #configure_mcp)
The constructor forwards its ENTIRE options hash here, most of which
configures things other than MCP, so unrecognized keys are ignored.
Unknown +mcp_+-prefixed keys still fail loud, since such a key can
only have been meant for MCP.
:explicit (used by Otto#enable_mcp!)
The caller is configuring MCP and nothing else, so any unrecognized
key raises. That turns enable_mcp!(auth_token: 'x') — a
singular-vs-plural typo that silently left the endpoint open — into a
boot failure.
The +mcp_+-prefixed gating keys (+mcp_enabled+, mcp_http, mcp_stdio)
decide whether MCP is enabled and are read by the constructor itself
(Otto::Core::Configuration#configure_mcp) through Options.gating_options. The
:constructor scope tolerates them; the :explicit scope rejects them,
because enable_mcp!(mcp_http: false) would otherwise be accepted and
still mount the endpoint. Their values must be exactly true or
false: the constructor disables the endpoint on mcp_http == false,
so a String "false" (from ENV.fetch or YAML) or nil (from an
unset ENV[...]) would otherwise mount it.
Keys may be Strings or Symbols; they are symbolized before anything is
read, so "auth_tokens" => [...] configures authentication exactly like
auth_tokens:. A String key and its Symbol twin are two spellings of one
option and conflict when their values differ.
Both scopes accept the canonical output of Options.normalize, so normalization is idempotent under either.
Constant Summary collapse
- OPTION_ALIASES =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Canonical MCP option keys and every accepted alias.
{ http_endpoint: %i[http_endpoint mcp_endpoint], auth_tokens: %i[auth_tokens mcp_auth_tokens], enable_validation: %i[enable_validation mcp_validation], enable_rate_limiting: %i[enable_rate_limiting mcp_rate_limiting], requests_per_minute: %i[requests_per_minute mcp_requests_per_minute], tools_per_minute: %i[tools_per_minute tool_calls_per_minute mcp_tool_calls_per_minute], allow_unauthenticated: %i[allow_unauthenticated mcp_allow_unauthenticated], }.freeze
- SCOPES =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
%i[constructor explicit].freeze
- OPTION_DEFAULTS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Canonical defaults applied when no alias supplies a value.
{ http_endpoint: '/_mcp', auth_tokens: [].freeze, enable_validation: true, enable_rate_limiting: true, requests_per_minute: 60, tools_per_minute: 20, allow_unauthenticated: false, }.freeze
- GATING_KEYS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
+mcp_+-prefixed constructor keys that gate whether MCP is enabled rather than configure the server. Otto.new reads them itself, so the :constructor scope tolerates them (and never emits them). #enable_mcp! cannot honour them, so the :explicit scope rejects them.
%i[mcp_enabled mcp_http mcp_stdio].freeze
- RECOGNIZED_KEYS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Every key normalize recognizes, per scope.
{ constructor: (OPTION_ALIASES.values.flatten + GATING_KEYS).freeze, explicit: OPTION_ALIASES.values.flatten.freeze, }.freeze
Class Method Summary collapse
-
.gating_options(opts) ⇒ Hash{Symbol=>Boolean}
Read the constructor-only gating keys (+mcp_enabled+,
mcp_http,mcp_stdio) with the same String/Symbol tolerance as Options.normalize. -
.normalize(opts = {}, scope = :explicit) ⇒ Hash
Normalize a constructor- or #enable_mcp!-style option hash into the single canonical shape consumed by Server#enable!.
Class Method Details
.gating_options(opts) ⇒ Hash{Symbol=>Boolean}
Read the constructor-only gating keys (+mcp_enabled+, mcp_http,
mcp_stdio) with the same String/Symbol tolerance as normalize.
Otto.new decides whether to enable MCP from these before it
normalizes the rest, and it used to read them as raw Symbol keys, so
Otto.new(nil, "mcp_enabled" => true) silently did nothing while the
String-keyed "auth_tokens" beside it was documented as accepted.
Every gating value present must be exactly true or false.
#configure_mcp disables the HTTP endpoint only on mcp_http == false,
so mcp_http: ENV.fetch('MCP_HTTP', 'false') (a String, truthy) or
mcp_http: ENV['MCP_HTTP'] with the variable unset (+nil+, not
false) would each mount the endpoint the caller meant to disable.
An explicit nil is rejected rather than treated as absent for that
reason, matching the other boolean options and auth_tokens: nil.
150 151 152 153 154 |
# File 'lib/otto/mcp/options.rb', line 150 def self.(opts) gating = symbolize_keys(opts.to_h).slice(*GATING_KEYS) gating.each { |key, value| coerce_boolean!(key, value) } gating end |
.normalize(opts = {}, scope = :explicit) ⇒ Hash
Normalize a constructor- or #enable_mcp!-style option hash into the single canonical shape consumed by Server#enable!.
scope is positional, not a keyword, so a brace-less hash at the call
site (+normalize(auth_tokens: ['t'])+) binds to opts as intended.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/otto/mcp/options.rb', line 109 def self.normalize(opts = {}, scope = :explicit) raise ArgumentError, "Unknown MCP option scope #{scope.inspect}; expected one of #{SCOPES.inspect}" unless SCOPES.include?(scope) opts = symbolize_keys(opts.to_h) reject_unrecognized_keys!(opts, scope) canonical = OPTION_DEFAULTS.dup OPTION_ALIASES.each do |key, key_aliases| supplied = key_aliases.select { |a| opts.key?(a) } next if supplied.empty? values = supplied.map { |a| opts[a] } raise_conflict!(key, supplied.map { |a| [a, opts[a]] }) if values.uniq.size > 1 canonical[key] = coerce_option(key, values.first) end canonical end |