Class: McpAuthorization::Configuration

Inherits:
Object
  • Object
show all
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_schema must have an object root — Anthropic and OpenAI reject oneOf/allOf/anyOf at the top level — so both facade strategies keep a flat object root and differ only in where the per-tool schemas go: :vendor_extension carries them on the facade's _meta; :lazy omits 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
DEFAULT_FACADE_SUFFIX =

Default suffix appended to a category to form its facade tool name (e.g. category :ordersorders_tools). Overridable per domain via facet_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

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

: () -> void



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/mcp_authorization/configuration.rb', line 145

def initialize
  @server_name = "mcp-authorization"
  @server_version = "1.0.0"
  @tool_paths = %w[app/mcp]
  @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_summariesObject (readonly)

Group summaries keyed by category symbol. Populated by categories. : Hash[Symbol, String]



120
121
122
# File 'lib/mcp_authorization/configuration.rb', line 120

def category_summaries
  @category_summaries
end

#cli_context_builderObject

Lambda that builds a server context for CLI/rake usage. Same duck-type contract as context_builder. : (^(domain: String, role: String) -> untyped)?



74
75
76
# File 'lib/mcp_authorization/configuration.rb', line 74

def cli_context_builder
  @cli_context_builder
end

#context_builderObject

Lambda that builds a server context from a Rack request. The returned object must satisfy the context contract above. : (^(untyped) -> untyped)?



69
70
71
# File 'lib/mcp_authorization/configuration.rb', line 69

def context_builder
  @context_builder
end

#default_domainObject

Domain name used when the request URL has no :domain segment. : String



60
61
62
# File 'lib/mcp_authorization/configuration.rb', line 60

def default_domain
  @default_domain
end

#faceted_domainsObject (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. : Hash[String, Hash[Symbol, untyped]]



116
117
118
# File 'lib/mcp_authorization/configuration.rb', line 116

def faceted_domains
  @faceted_domains
end

#mount_pathObject

URL prefix where the Engine mounts its routes. : String



64
65
66
# File 'lib/mcp_authorization/configuration.rb', line 64

def mount_path
  @mount_path
end

#server_nameObject

Server name reported in the MCP initialize handshake. : String



42
43
44
# File 'lib/mcp_authorization/configuration.rb', line 42

def server_name
  @server_name
end

#server_versionObject

Server version reported in the MCP initialize handshake. : String



46
47
48
# File 'lib/mcp_authorization/configuration.rb', line 46

def server_version
  @server_version
end

#shared_type_pathsObject

Directories (relative to Rails.root) where shared .rbs type files live. Used by RbsSchemaCompiler to resolve # @rbs import. : Array



56
57
58
# File 'lib/mcp_authorization/configuration.rb', line 56

def shared_type_paths
  @shared_type_paths
end

#strict_schemaObject

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. : bool



80
81
82
# File 'lib/mcp_authorization/configuration.rb', line 80

def strict_schema
  @strict_schema
end

#tool_pathsObject

Directories (relative to Rails.root) that contain tool classes. Added to autoload_paths and eager_load_paths by the Engine. : Array



51
52
53
# File 'lib/mcp_authorization/configuration.rb', line 51

def tool_paths
  @tool_paths
end

#tools_list_cacheObject

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. : untyped



92
93
94
# File 'lib/mcp_authorization/configuration.rb', line 92

def tools_list_cache
  @tools_list_cache
end

#tools_list_cache_redisObject

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. : untyped



104
105
106
# File 'lib/mcp_authorization/configuration.rb', line 104

def tools_list_cache_redis
  @tools_list_cache_redis
end

#tools_list_cache_redis_urlObject

Optional explicit Redis URL for the :redis store. : String?



108
109
110
# File 'lib/mcp_authorization/configuration.rb', line 108

def tools_list_cache_redis_url
  @tools_list_cache_redis_url
end

#tools_list_cache_ttlObject

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. : Integer



98
99
100
# File 'lib/mcp_authorization/configuration.rb', line 98

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

: () { () -> void } -> void



232
233
234
235
# File 'lib/mcp_authorization/configuration.rb', line 232

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. : (Symbol) -> String?



239
240
241
# File 'lib/mcp_authorization/configuration.rb', line 239

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. : (String) -> Hash[Symbol, untyped]?



220
221
222
# File 'lib/mcp_authorization/configuration.rb', line 220

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 :ordersorders_#{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. : (Symbol | String, group_by: Symbol, ?schema_strategy: Symbol, ?uncategorized: Symbol, ?facade_suffix: String | Symbol) -> void



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/mcp_authorization/configuration.rb', line 188

def facet_domain(domain, group_by:, schema_strategy: :vendor_extension, uncategorized: :fallback, facade_suffix: DEFAULT_FACADE_SUFFIX)
  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. : (String) -> bool

Returns:

  • (Boolean)


214
215
216
# File 'lib/mcp_authorization/configuration.rb', line 214

def faceted?(domain)
  @faceted_domains.key?(domain.to_s)
end