Class: McpAuthorization::Configuration
- Inherits:
-
Object
- Object
- McpAuthorization::Configuration
- Defined in:
- lib/mcp_authorization/configuration.rb
Overview
Holds gem-wide settings. A single global instance is created lazily by McpAuthorization.configuration and configured in a Rails initializer:
McpAuthorization.configure do |c|
c.server_name = "my-app"
c.server_version = MyApp::VERSION
c.tool_paths = %w[app/mcp]
c.context_builder = ->(request) { ... }
end
Required settings
context_builder must be set before the first MCP request. Everything
else has sensible defaults.
The context contract
Both context_builder and cli_context_builder must return an object
whose current_user responds to:
current_user.can?(:symbol) # required — gates field/tool visibility
current_user.default_for(:symbol) # optional — populates @default_for tags
The context object itself can implement predicate methods for generic
tag filtering. Any @tag(:value) not in the known constraint list
calls context.tag?(value):
context.requires?(flag) # optional — for @requires, falls back to current_user.can?
context.feature?(flag) # optional — for @feature (account-level feature flags)
context.tier?(name) # optional — for @tier (plan-level gating)
For public/anonymous MCP interfaces, supply a context with minimum-viable
permissions rather than current_user: nil. A nil user causes @requires
fields to be silently excluded (no user = no permissions).
See RbsSchemaCompiler.predicate_excluded? for the full protocol.
Defined Under Namespace
Classes: CategoryCollector
Constant Summary collapse
- SCHEMA_STRATEGIES =
Schema strategies FacadeBuilder knows how to emit. LLM tool
input_schemamust have an object root — Anthropic and OpenAI rejectoneOf/allOf/anyOfat the top level — so both facade strategies keep a flat object root and differ only in where the per-tool schemas go::vendor_extensioncarries them on the facade's_meta;:lazyomits them (enforced at dispatch). A correlated inline shape (tool_name → its argument schema) would require a root combinator and is therefore not offered. %i[vendor_extension lazy].freeze
- UNCATEGORIZED_MODES =
Behaviors for a tool in a faceted domain that declares no
category. %i[fallback error].freeze
- GROUP_BY_KEYS =
Grouping keys
facet_domainknows how to group by. Only:categoryexists today (thecategoryDSL is the only grouping key); accepted explicitly so a future key is an additive, validated change. %i[category].freeze
- DEFAULT_FACADE_SUFFIX =
Default suffix appended to a category to form its facade tool name (e.g. category
:orders→orders_tools). Overridable per domain viafacet_domain(..., facade_suffix:). "tools"- FACADE_SUFFIX_FORMAT =
A facade suffix must be a bare identifier fragment so the derived facade name (+"#category_#suffix"+) stays a valid MCP tool name.
/\A[a-z0-9]+(?:_[a-z0-9]+)*\z/
Instance Attribute Summary collapse
-
#category_summaries ⇒ Object
readonly
Group summaries keyed by category symbol.
-
#cli_context_builder ⇒ Object
Lambda that builds a server context for CLI/rake usage.
-
#context_builder ⇒ Object
Lambda that builds a server context from a Rack request.
-
#default_domain ⇒ Object
Domain name used when the request URL has no
:domainsegment. -
#faceted_domains ⇒ Object
readonly
Per-domain facet (tool-grouping) configuration, keyed by domain name.
-
#mount_path ⇒ Object
URL prefix where the Engine mounts its routes.
-
#server_name ⇒ Object
Server name reported in the MCP
initializehandshake. -
#server_version ⇒ Object
Server version reported in the MCP
initializehandshake. -
#shared_type_paths ⇒ Object
Directories (relative to
Rails.root) where shared.rbstype files live. -
#strict_schema ⇒ Object
When true, strips JSON Schema keywords that cause 400 errors in Anthropic's strict tool use mode (minLength, maximum, maxItems, etc.) and adds additionalProperties: false to all objects.
-
#tool_paths ⇒ Object
Directories (relative to
Rails.root) that contain tool classes. -
#tool_producers ⇒ Object
Callables that register tool classes the host generates at runtime, rather than defining in a file under
tool_paths. -
#tools_list_cache ⇒ Object
Cache for the
tools/listresponse. -
#tools_list_cache_redis ⇒ Object
Optional explicit Redis client for the :redis store.
-
#tools_list_cache_redis_url ⇒ Object
Optional explicit Redis URL for the :redis store.
-
#tools_list_cache_ttl ⇒ Object
TTL (seconds) for cached
tools/listentries.
Instance Method Summary collapse
-
#categories(&block) ⇒ Object
Declare one summary line per group.
-
#category_summary(category) ⇒ Object
The group summary for a category, or nil when none was declared.
-
#facet_config(domain) ⇒ Object
Facet config Hash for a domain, or nil when the domain is not faceted.
-
#facet_domain(domain, group_by:, schema_strategy: :vendor_extension, uncategorized: :fallback, facade_suffix: DEFAULT_FACADE_SUFFIX) ⇒ Object
Present a domain as grouped facade tools instead of a flat tool list.
-
#faceted?(domain) ⇒ Boolean
True when the given domain is presented as grouped facades.
-
#initialize ⇒ Configuration
constructor
A new instance of Configuration.
Constructor Details
#initialize ⇒ Configuration
Returns a new instance of Configuration.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/mcp_authorization/configuration.rb', line 171 def initialize @server_name = "mcp-authorization" @server_version = "1.0.0" @tool_paths = %w[app/mcp] @tool_producers = [] @shared_type_paths = %w[sig/shared] @default_domain = "default" @mount_path = "/mcp" @context_builder = nil @cli_context_builder = nil @strict_schema = false @tools_list_cache = nil @tools_list_cache_ttl = 3600 @tools_list_cache_redis = nil @tools_list_cache_redis_url = nil @faceted_domains = {} @category_summaries = {} end |
Instance Attribute Details
#category_summaries ⇒ Object (readonly)
Group summaries keyed by category symbol. Populated by categories.
141 142 143 |
# File 'lib/mcp_authorization/configuration.rb', line 141 def category_summaries @category_summaries end |
#cli_context_builder ⇒ Object
Lambda that builds a server context for CLI/rake usage.
Same duck-type contract as context_builder.
95 96 97 |
# File 'lib/mcp_authorization/configuration.rb', line 95 def cli_context_builder @cli_context_builder end |
#context_builder ⇒ Object
Lambda that builds a server context from a Rack request. The returned object must satisfy the context contract above.
90 91 92 |
# File 'lib/mcp_authorization/configuration.rb', line 90 def context_builder @context_builder end |
#default_domain ⇒ Object
Domain name used when the request URL has no :domain segment.
81 82 83 |
# File 'lib/mcp_authorization/configuration.rb', line 81 def default_domain @default_domain end |
#faceted_domains ⇒ Object (readonly)
Per-domain facet (tool-grouping) configuration, keyed by domain name.
Each value is a Hash: { group_by:, schema_strategy:, uncategorized:,
facade_suffix: }.
Populated by facet_domain; read by ToolRegistry / FacadeBuilder.
See docs/designs/tool-grouping-facades.md.
137 138 139 |
# File 'lib/mcp_authorization/configuration.rb', line 137 def faceted_domains @faceted_domains end |
#mount_path ⇒ Object
URL prefix where the Engine mounts its routes.
85 86 87 |
# File 'lib/mcp_authorization/configuration.rb', line 85 def mount_path @mount_path end |
#server_name ⇒ Object
Server name reported in the MCP initialize handshake.
42 43 44 |
# File 'lib/mcp_authorization/configuration.rb', line 42 def server_name @server_name end |
#server_version ⇒ Object
Server version reported in the MCP initialize handshake.
46 47 48 |
# File 'lib/mcp_authorization/configuration.rb', line 46 def server_version @server_version end |
#shared_type_paths ⇒ Object
Directories (relative to Rails.root) where shared .rbs type
files live. Used by RbsSchemaCompiler to resolve # @rbs import.
77 78 79 |
# File 'lib/mcp_authorization/configuration.rb', line 77 def shared_type_paths @shared_type_paths end |
#strict_schema ⇒ Object
When true, strips JSON Schema keywords that cause 400 errors in Anthropic's strict tool use mode (minLength, maximum, maxItems, etc.) and adds additionalProperties: false to all objects.
101 102 103 |
# File 'lib/mcp_authorization/configuration.rb', line 101 def strict_schema @strict_schema end |
#tool_paths ⇒ Object
Directories (relative to Rails.root) that contain tool classes.
Added to autoload_paths and eager_load_paths by the Engine.
51 52 53 |
# File 'lib/mcp_authorization/configuration.rb', line 51 def tool_paths @tool_paths end |
#tool_producers ⇒ Object
Callables that register tool classes the host generates at runtime,
rather than defining in a file under tool_paths.
config.tool_producers << -> { MyApp::GeneratedTools.register_all! }
Invoked by ToolRegistry.ensure_tools_loaded! after the tool_paths
eager-load, on the first read of an empty registry. Because a producer
runs from a registry read rather than a Rails boot callback, it cannot
execute before the framework is fully configured — see
ToolRegistry.ensure_tools_loaded! for why that matters.
A producer must be idempotent: register dedupes by object identity,
not by tool_name, so minting a fresh class on every call registers a
second tool under the same name and leaves find_tool resolving an
arbitrary one. Reuse the class while its inputs are unchanged.
Exceptions propagate — a malformed generated tool fails the read rather than silently vanishing from the surface.
72 73 74 |
# File 'lib/mcp_authorization/configuration.rb', line 72 def tool_producers @tool_producers end |
#tools_list_cache ⇒ Object
Cache for the tools/list response. Opt-in; defaults to no caching.
Accepts:
nil / false — no caching (default)
:memory — process-local MemoryStore
:redis — shared RedisStore (connection resolved from
+tools_list_cache_redis+ / +tools_list_cache_redis_url+ /
ENV["REDIS_URL"] / a bare Redis.new — the Rails redis config)
<object> — any store responding to +get+/+set+
See McpAuthorization::Cache for the keying strategy.
113 114 115 |
# File 'lib/mcp_authorization/configuration.rb', line 113 def tools_list_cache @tools_list_cache end |
#tools_list_cache_redis ⇒ Object
Optional explicit Redis client for the :redis store. When nil, the store
resolves a connection from tools_list_cache_redis_url, then
ENV, then a bare Redis.new.
125 126 127 |
# File 'lib/mcp_authorization/configuration.rb', line 125 def tools_list_cache_redis @tools_list_cache_redis end |
#tools_list_cache_redis_url ⇒ Object
Optional explicit Redis URL for the :redis store.
129 130 131 |
# File 'lib/mcp_authorization/configuration.rb', line 129 def tools_list_cache_redis_url @tools_list_cache_redis_url end |
#tools_list_cache_ttl ⇒ Object
TTL (seconds) for cached tools/list entries. Bounds staleness from
out-of-band changes (e.g. a feature flag toggled with no deploy); the
deploy digest invalidates on tool/schema changes independently.
119 120 121 |
# File 'lib/mcp_authorization/configuration.rb', line 119 def tools_list_cache_ttl @tools_list_cache_ttl end |
Instance Method Details
#categories(&block) ⇒ Object
Declare one summary line per group. Evaluated in a small collector so the block reads declaratively:
config.categories do
summary :orders, "Create, inspect, and update orders."
summary :billing, "Invoices, payments, refunds."
end
263 264 265 266 |
# File 'lib/mcp_authorization/configuration.rb', line 263 def categories(&block) collector = CategoryCollector.new(@category_summaries) collector.instance_eval(&block) end |
#category_summary(category) ⇒ Object
The group summary for a category, or nil when none was declared.
270 271 272 |
# File 'lib/mcp_authorization/configuration.rb', line 270 def category_summary(category) @category_summaries[category.to_sym] end |
#facet_config(domain) ⇒ Object
Facet config Hash for a domain, or nil when the domain is not faceted.
251 252 253 |
# File 'lib/mcp_authorization/configuration.rb', line 251 def facet_config(domain) @faceted_domains[domain.to_s] end |
#facet_domain(domain, group_by:, schema_strategy: :vendor_extension, uncategorized: :fallback, facade_suffix: DEFAULT_FACADE_SUFFIX) ⇒ Object
Present a domain as grouped facade tools instead of a flat tool list.
config.facet_domain :admin, group_by: :category
config.facet_domain :admin, group_by: :category,
schema_strategy: :lazy,
uncategorized: :error
group_by is currently always :category (the only grouping key the
category DSL provides); it is accepted explicitly so future grouping
keys are an additive change rather than a behavior switch.
schema_strategy selects where the per-tool argument schemas go (the
facade inputSchema is a flat object either way — see SCHEMA_STRATEGIES).
:vendor_extension (default) carries them on the facade's _meta;
:lazy omits them.
uncategorized controls what happens to a tool in this domain with no
category: :fallback (default) collects them into an uncategorized
group; :error raises at facade-build time.
facade_suffix is the token appended to a category to form its facade
tool name — category :orders → orders_#{suffix}. Defaults to
"tools" (+orders_tools+). Must be a lowercase identifier fragment
(+[a-z0-9_]+) so the derived name stays a valid MCP tool name.
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/mcp_authorization/configuration.rb', line 215 def facet_domain(domain, group_by:, schema_strategy: :vendor_extension, uncategorized: :fallback, facade_suffix: DEFAULT_FACADE_SUFFIX) unless GROUP_BY_KEYS.include?(group_by.to_sym) raise ArgumentError, "unknown group_by #{group_by.inspect}; " \ "expected one of #{GROUP_BY_KEYS.inspect}" end unless SCHEMA_STRATEGIES.include?(schema_strategy) raise ArgumentError, "unknown schema_strategy #{schema_strategy.inspect}; " \ "expected one of #{SCHEMA_STRATEGIES.inspect}" end unless UNCATEGORIZED_MODES.include?(uncategorized) raise ArgumentError, "unknown uncategorized mode #{uncategorized.inspect}; " \ "expected one of #{UNCATEGORIZED_MODES.inspect}" end suffix = facade_suffix.to_s unless FACADE_SUFFIX_FORMAT.match?(suffix) raise ArgumentError, "invalid facade_suffix #{facade_suffix.inspect}; " \ "expected a lowercase identifier fragment matching #{FACADE_SUFFIX_FORMAT.inspect}" end @faceted_domains[domain.to_s] = { group_by: group_by.to_sym, schema_strategy: schema_strategy, uncategorized: uncategorized, facade_suffix: suffix } end |
#faceted?(domain) ⇒ Boolean
True when the given domain is presented as grouped facades.
245 246 247 |
# File 'lib/mcp_authorization/configuration.rb', line 245 def faceted?(domain) @faceted_domains.key?(domain.to_s) end |