Class: McpToolkit::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp_toolkit/configuration.rb

Overview

The single, injectable configuration object for an app's MCP server.

Generic but OPINIONATED: every setting has a sensible, vendor-neutral default, so a satellite needs to override only a handful of values. The two things an app almost always sets are server_name and the auth wiring (central_app_url for a satellite, or token_authenticator for the authority).

Whether ANY scope is required is decided PER TOOL, not per app: a resource declares required_permissions_scope "notifications__read" (or the registry declares default_required_permissions_scope once for all resources). There is no app-wide permission setting here.

Accessed through McpToolkit.config (or MCPToolkit.config) and mutated in a McpToolkit.configure { |c| ... } block.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Vendor-neutral defaults; apps override the auth wiring + identity as needed.



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/mcp_toolkit/configuration.rb', line 418

def initialize
  @server_name = "mcp-server"
  @server_version = "1.0.0"
  @server_instructions = nil

  @gateway_client_name = nil
  @gateway_client_version = nil

  @serializer_base = nil # set lazily in #serializer_base to avoid load-order issues

  @auth_role = :satellite
  @central_app_url = nil
  @introspect_path = "/mcp/tokens/introspect"
  @introspection_cache_ttl = 45
  @introspection_timeout = 10
  @account_resolver = ->() {  }

  @token_authenticator = nil

  @cache_store = ActiveSupport::Cache::MemoryStore.new
  initialize_data_path_defaults

  @protocol_version = nil
  @supported_protocol_versions = McpToolkit::Protocol::SUPPORTED_VERSIONS
  @parent_controller = "ActionController::Base"
  @account_meta_key = "mcp-toolkit/account-id"
  @account_id_header = "X-MCP-Account-ID"

  initialize_authority_hook_defaults
  @generic_tool_name_prefix = ""

  @upstream_timeout = 10
  @upstream_list_ttl = 900 # 15 minutes
  @logger = nil

  @registry = McpToolkit::Registry.new
  @upstreams = McpToolkit::Gateway::UpstreamRegistry.new
end

Instance Attribute Details

#account_id_headerString

Returns:

  • (String)


291
292
293
# File 'lib/mcp_toolkit/configuration.rb', line 291

def 
  @account_id_header
end

#account_meta_keyString

Header / meta-key constants. Vendor-neutral defaults; an app on a specific central authority can rename them to match that authority's convention. These are the selectors a superuser/multi-account token uses to pin the active account.

Returns:

  • (String)


289
290
291
# File 'lib/mcp_toolkit/configuration.rb', line 289

def 
  @account_meta_key
end

#account_resolver#call

Resolves the central account id to the satellite's LOCAL scope root.

A satellite stores rows keyed by the central app's account id (synced via Kafka etc.). This callable receives the resolved central account_id and MUST return the object that Resource#scope blocks root on (typically the local Account). Return nil to signal "no local account" (=> Unauthorized).

c. = ->() { Account.find_by(synced_id: ) }

Defaults to the identity function: the resolved central account id is used directly as the scope root (suitable for an app whose scope blocks key on the central id, or for the authority itself).

Returns:

  • (#call)


96
97
98
# File 'lib/mcp_toolkit/configuration.rb', line 96

def 
  @account_resolver
end

#auth_roleSymbol

Returns :satellite (introspect tokens against a central app) or :authority (be the introspection provider + authenticate local tokens). A single app MAY be both — set :authority and still configure a central_app_url if it also exposes its own tools as a satellite.

Returns:

  • (Symbol)

    :satellite (introspect tokens against a central app) or :authority (be the introspection provider + authenticate local tokens). A single app MAY be both — set :authority and still configure a central_app_url if it also exposes its own tools as a satellite.



67
68
69
# File 'lib/mcp_toolkit/configuration.rb', line 67

def auth_role
  @auth_role
end

#bare_filter_value_semanticsSymbol

NOTE: (all data-path settings below): the list/get executors and the schema builder read the PROCESS-GLOBAL McpToolkit.config — a per-instance config bound to a provider affects tool prose only. Configure these globally.

How a BARE (non-operator) filter value is interpreted by the list executor:

:tokenized (default) — a comma-separated string becomes an IN set, the
"null" token (and a JSON null) matches NULL rows, an Array of non-null
scalars is an IN set (nil/Hash/nested-Array elements rejected), and an
empty string means "no filter".
:literal — the value is handed to the WHERE clause verbatim (an op-less
Hash is still rejected). This preserves an EXISTING API contract for a
host whose pre-gem endpoint matched bare values literally: "a,b" is one
literal string, "null" is the literal string, "" matches empty-string
rows, an Array (including nil elements) gets the adapter's native IN /
OR-IS-NULL handling.

Operator conditions ({ op:, value: }) behave identically in both modes.

Returns:

  • (Symbol)

    :tokenized or :literal



192
193
194
# File 'lib/mcp_toolkit/configuration.rb', line 192

def bare_filter_value_semantics
  @bare_filter_value_semantics
end

#cache_storeActiveSupport::Cache::Store, #read

The cache store backing sessions and introspection results. Must satisfy the ActiveSupport::Cache::Store contract (read/write/delete with expires_in:). Defaults to an in-process MemoryStore; a real deployment should set this to Rails.cache (or any shared store) so sessions survive across Puma workers.

Returns:

  • (ActiveSupport::Cache::Store, #read)


124
125
126
# File 'lib/mcp_toolkit/configuration.rb', line 124

def cache_store
  @cache_store
end

#central_app_urlString?

Returns base URL of the central auth app. The satellite POSTs <central_app_url>/mcp/tokens/introspect.

Returns:

  • (String, nil)

    base URL of the central auth app. The satellite POSTs <central_app_url>/mcp/tokens/introspect.



73
74
75
# File 'lib/mcp_toolkit/configuration.rb', line 73

def central_app_url
  @central_app_url
end

#extra_tool_providersArray<#tool_definitions, Class>

Additional tool providers (or bare TOOL classes, auto-wrapped in a SingleToolProvider) composed AFTER the generic Registry-backed tools when tool_provider is not explicitly assigned. The registry provider is always first, so a generic tool name resolves to it; extras only answer their own names.

config.extra_tool_providers = [MyApp::Tools::AuditLog]

Returns:

  • (Array<#tool_definitions, Class>)


392
393
394
# File 'lib/mcp_toolkit/configuration.rb', line 392

def extra_tool_providers
  @extra_tool_providers
end

#filter_operator_overridesHash{Symbol => Array<String>}

Per-column-type overrides for the operator sets advertised by resource_schema and enforced by the list executor, merged over McpToolkit::Filtering::OPERATORS_BY_TYPE. Lets a host preserve an EXISTING operator contract exactly — both the schema bytes and which conditions are accepted — e.g. { text: %w[eq in], date: %w[eq in] } for a pre-gem endpoint that never offered comparisons on those types. Empty by default (the gem's own sets apply).

Returns:

  • (Hash{Symbol => Array<String>})


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

def filter_operator_overrides
  @filter_operator_overrides
end

#gateway_client_nameObject

The gateway handshake client name, defaulting to the server identity when the host hasn't split it. Read (not stored) so a server_name change before the split is set still flows through.



532
533
534
# File 'lib/mcp_toolkit/configuration.rb', line 532

def gateway_client_name
  @gateway_client_name || server_name
end

#gateway_client_versionObject

The gateway handshake client version, defaulting to the server version.



537
538
539
# File 'lib/mcp_toolkit/configuration.rb', line 537

def gateway_client_version
  @gateway_client_version || server_version
end

#generic_tool_name_prefixString

A prefix prepended to the four GENERIC, Registry-backed authority tool names (resources, resource_schema, get, list) served by McpToolkit::Authority::RegistryToolProvider. Lets a host NAMESPACE its generic tools — e.g. set "foo_" and they advertise (and resolve) as foo_resources, foo_resource_schema, foo_get, foo_list — so distinct MCP surfaces don't collide and existing clients keep a stable, host-chosen name. Empty by default, so the tools keep their bare base names. The prefix value is the host's; the gem names no app concept.

Returns:

  • (String)


406
407
408
# File 'lib/mcp_toolkit/configuration.rb', line 406

def generic_tool_name_prefix
  @generic_tool_name_prefix
end

#introspect_pathString?

Returns the introspect path appended to central_app_url.

Returns:

  • (String, nil)

    the introspect path appended to central_app_url.



75
76
77
# File 'lib/mcp_toolkit/configuration.rb', line 75

def introspect_path
  @introspect_path
end

#introspection_cache_ttlInteger

Returns seconds to cache an introspection result (positive AND negative) so a burst of tool calls does not hammer the central app.

Returns:

  • (Integer)

    seconds to cache an introspection result (positive AND negative) so a burst of tool calls does not hammer the central app.



78
79
80
# File 'lib/mcp_toolkit/configuration.rb', line 78

def introspection_cache_ttl
  @introspection_cache_ttl
end

#introspection_timeoutInteger

Returns HTTP open/read timeout for the introspection call.

Returns:

  • (Integer)

    HTTP open/read timeout for the introspection call.



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

def introspection_timeout
  @introspection_timeout
end

#logger#warn, ...

Optional logger for gateway/session diagnostics. All call sites guard with logger&.warn / logger&.error, so nil (the default) silences them. A Rails host typically sets this to Rails.logger.

Returns:

  • (#warn, #error, nil)


415
416
417
# File 'lib/mcp_toolkit/configuration.rb', line 415

def logger
  @logger
end

#max_batch_sizeInteger?

Caps how many JSON-RPC calls a single POST batch may carry on the authority transport. Rate limiting is per-HTTP-request, so an uncapped batch would let one request fan out unbounded work (N tool executions / N upstream calls) under a single rate-limit tick. nil disables the cap.

Returns:

  • (Integer, nil)


258
259
260
# File 'lib/mcp_toolkit/configuration.rb', line 258

def max_batch_size
  @max_batch_size
end

#max_filter_valuesInteger?

Caps how many values an IN-set filter may resolve to, and how many operator conditions may be ANDed on a single attribute, so a valid token can't emit an unbounded IN clause / AND-chain (oversized SQL + Arel AST and expensive planning; rate limiting is opt-in via rate_limit_max_requests). Applies to the default :tokenized bare-value semantics and to { op:, value: } conditions. nil disables the cap.

Returns:

  • (Integer, nil)


250
251
252
# File 'lib/mcp_toolkit/configuration.rb', line 250

def max_filter_values
  @max_filter_values
end

#non_numeric_pk_orderSymbol

Default ordering for a resource whose primary key is NON-numeric (numeric PKs always order by :id):

:created_at (default) — ORDER BY created_at, <pk> (chronological pages
with a total-order tiebreaker).
:primary_key — ORDER BY <pk> only. Preserves an EXISTING API contract for
a host whose pre-gem endpoint ordered every list by id.

Returns:

  • (Symbol)

    :created_at or :primary_key



203
204
205
# File 'lib/mcp_toolkit/configuration.rb', line 203

def non_numeric_pk_order
  @non_numeric_pk_order
end

#parent_controllerString

The parent class (as a String, resolved via constantize) of the gem-provided McpToolkit::ServerController that McpToolkit::Engine mounts. Doorkeeper-style indirection so a satellite mounting the engine can keep ActionController::Base (NOT ::API) — e.g. for a logstasher helper_method hook — by setting c.parent_controller = "ApplicationController".

Returns:

  • (String)


281
282
283
# File 'lib/mcp_toolkit/configuration.rb', line 281

def parent_controller
  @parent_controller
end

#protocol_versionString?

Returns protocol version to pin on the underlying MCP::Server. nil lets the gem negotiate (recommended). Set only to force an older spec.

Returns:

  • (String, nil)

    protocol version to pin on the underlying MCP::Server. nil lets the gem negotiate (recommended). Set only to force an older spec.



264
265
266
# File 'lib/mcp_toolkit/configuration.rb', line 264

def protocol_version
  @protocol_version
end

#rate_limit_max_requestsInteger?

The built-in per-principal request cap enforced by the authority transport (McpToolkit::Authority::ControllerMethods#mcp_rate_limit!), counted against cache_store via McpToolkit::RateLimiter. nil (the default) DISABLES rate limiting entirely, so a pure host is unaffected until it opts in. Set an Integer to cap each principal to that many requests per rate_limit_window. The default mcp_rate_limit! reads this through the overridable mcp_rate_limit_max_requests hook, so a host that keeps the cap in its own constant/model overrides that hook rather than this value.

Returns:

  • (Integer, nil)


141
142
143
# File 'lib/mcp_toolkit/configuration.rb', line 141

def rate_limit_max_requests
  @rate_limit_max_requests
end

#rate_limit_windowInteger

The fixed rate-limit window, in seconds (default 3600 = 1 hour). Ignored while rate_limit_max_requests is nil.

Returns:

  • (Integer)


147
148
149
# File 'lib/mcp_toolkit/configuration.rb', line 147

def rate_limit_window
  @rate_limit_window
end

#rate_limiter#call?

OPTIONAL escape hatch that FULLY REPLACES the built-in limiter: a ->(controller:, principal:) that renders + halts when over the limit (or sets rate-limit headers when under). When set, mcp_rate_limit! delegates to it and the built-in (rate_limit_max_requests) is skipped. nil (the default) means the built-in runs instead. Most hosts want the built-in; reach for this only when the counting itself must live in app code.

Returns:

  • (#call, nil)


335
336
337
# File 'lib/mcp_toolkit/configuration.rb', line 335

def rate_limiter
  @rate_limiter
end

#registryMcpToolkit::Registry

The resource registry for this configuration. Each config carries its own so tests (and, in principle, multiple mounted servers) don't collide. The process-wide convenience McpToolkit.registry delegates to the active config's registry.



299
300
301
# File 'lib/mcp_toolkit/configuration.rb', line 299

def registry
  @registry
end

#serializer_baseObject

The serializer base, lazily defaulting to the gem's bundled DSL base. Lazy so McpToolkit::Serializer::Base is referenced after it has been required, regardless of file load order.



544
545
546
# File 'lib/mcp_toolkit/configuration.rb', line 544

def serializer_base
  @serializer_base ||= McpToolkit::Serializer::Base
end

#server_instructionsString?

Returns human-readable instructions returned on initialize.

Returns:

  • (String, nil)

    human-readable instructions returned on initialize.



26
27
28
# File 'lib/mcp_toolkit/configuration.rb', line 26

def server_instructions
  @server_instructions
end

#server_nameString

Returns the MCP server name advertised in initialize.

Returns:

  • (String)

    the MCP server name advertised in initialize.



22
23
24
# File 'lib/mcp_toolkit/configuration.rb', line 22

def server_name
  @server_name
end

#server_versionString

Returns the MCP server version advertised in initialize.

Returns:

  • (String)

    the MCP server version advertised in initialize.



24
25
26
# File 'lib/mcp_toolkit/configuration.rb', line 24

def server_version
  @server_version
end

#session_data_builder#call?

Builds the opaque payload bound to a session on initialize. ->(principal:) returning a Hash (or nil for none). Lets a host bind e.g. { token_id: principal.id } so a revoked token can kill an in-flight session, WITHOUT overriding the controller's mcp_session_data. nil (the default) => an empty session payload.

Returns:

  • (#call, nil)


357
358
359
# File 'lib/mcp_toolkit/configuration.rb', line 357

def session_data_builder
  @session_data_builder
end

#session_ttlInteger

Returns session sliding-TTL in seconds.

Returns:

  • (Integer)

    session sliding-TTL in seconds.



127
128
129
# File 'lib/mcp_toolkit/configuration.rb', line 127

def session_ttl
  @session_ttl
end

#sql_sanitizer#sanitize_sql_like

Escapes LIKE wildcards in matches / does_not_match filter values so they match literally. Must respond to sanitize_sql_like(string). Defaults to the ActiveRecord-backed McpToolkit::SqlSanitizer; a non-Rails host (or a test) can inject its own.

Returns:

  • (#sanitize_sql_like)


170
171
172
# File 'lib/mcp_toolkit/configuration.rb', line 170

def sql_sanitizer
  @sql_sanitizer
end

#superuser_resolver#call?

Optional resolver deciding whether a principal is a SUPERUSER — a cross-tenant caller that may reach superusers_only! resources. ->(principal) -> Boolean. When set, McpToolkit::Authority::Context#superuser? calls it; when nil (the default) the context falls back to duck-typing principal.superuser? (false when the principal doesn't respond to it). Superuser is FULLY OPTIONAL: a host with no such concept leaves this nil and flags no superusers_only! resource, so no caller is ever a superuser.

Returns:

  • (#call, nil)


160
161
162
# File 'lib/mcp_toolkit/configuration.rb', line 160

def superuser_resolver
  @superuser_resolver
end

#supported_protocol_versionsArray<String>

The protocol versions the hand-rolled AUTHORITY dispatcher (McpToolkit::Dispatcher) negotiates, newest first. initialize echoes the requested version when it is in this set, else the first (latest). Defaults to McpToolkit::Protocol::SUPPORTED_VERSIONS; override to pin a host's own set.

Returns:

  • (Array<String>)


272
273
274
# File 'lib/mcp_toolkit/configuration.rb', line 272

def supported_protocol_versions
  @supported_protocol_versions
end

#token_authenticator#call?

Looks up + verifies a plaintext bearer token locally, returning a token object (duck-typed, see below) or nil. This is the authority's AccessToken.authenticate(plaintext) equivalent. Required for the :authority role; unused by a pure satellite.

c.token_authenticator = ->(plaintext) { AccessToken.authenticate(plaintext) }

The returned token object must respond to the methods McpToolkit::Auth::Authority#introspection_payload reads (see that module for the contract): kind, account_id, account_ids, expires_at, scopes. A touch_last_used! method, if present, is called.

Returns:

  • (#call, nil)


113
114
115
# File 'lib/mcp_toolkit/configuration.rb', line 113

def token_authenticator
  @token_authenticator
end

#tool_providerObject



379
380
381
# File 'lib/mcp_toolkit/configuration.rb', line 379

def tool_provider
  @tool_provider || composed_tool_provider
end

#upstream_list_ttlInteger

Returns TTL (s) for an upstream's cached, namespaced tool list in McpToolkit::Gateway::Aggregator.

Returns:

  • (Integer)

    TTL (s) for an upstream's cached, namespaced tool list in McpToolkit::Gateway::Aggregator.



308
309
310
# File 'lib/mcp_toolkit/configuration.rb', line 308

def upstream_list_ttl
  @upstream_list_ttl
end

#upstream_timeoutInteger

Returns HTTP open/read timeout (s) for a gateway's calls to an upstream MCP server (McpToolkit::Gateway::Client).

Returns:

  • (Integer)

    HTTP open/read timeout (s) for a gateway's calls to an upstream MCP server (McpToolkit::Gateway::Client).



305
306
307
# File 'lib/mcp_toolkit/configuration.rb', line 305

def upstream_timeout
  @upstream_timeout
end

#upstreamsMcpToolkit::Gateway::UpstreamRegistry (readonly)

The registry of upstream MCP servers this gateway aggregates + proxies to. Each config carries its own (like registry), so it resets with a fresh config. Register via the register_upstream sugar below or directly on this instance. Empty unless the app registers upstreams, so a non-gateway app is unaffected.



317
318
319
# File 'lib/mcp_toolkit/configuration.rb', line 317

def upstreams
  @upstreams
end

#usage_flusher#call?

Persists accumulated usage after the response (an after_action). ->(controller:). MUST never affect the MCP response. nil = no flush.

Returns:

  • (#call, nil)


348
349
350
# File 'lib/mcp_toolkit/configuration.rb', line 348

def usage_flusher
  @usage_flusher
end

#usage_recorder#call?

Records ONE usage event for a single JSON-RPC call (called per batch element). ->(request_data:, account:, principal:, controller:). MUST never affect the MCP response. nil = no metering.

Returns:

  • (#call, nil)


342
343
344
# File 'lib/mcp_toolkit/configuration.rb', line 342

def usage_recorder
  @usage_recorder
end

Instance Method Details

#authority?Boolean

Returns whether this app authenticates tokens locally / answers introspection.

Returns:

  • (Boolean)

    whether this app authenticates tokens locally / answers introspection.



555
556
557
# File 'lib/mcp_toolkit/configuration.rb', line 555

def authority?
  auth_role.to_sym == :authority
end

#initialize_authority_hook_defaultsObject

The authority transport's injection points all default to nil (a no-op): a pure satellite/gateway never touches them. rate_limit_window is the sole non-nil default (the window size only matters once a cap opts in).



494
495
496
497
498
499
500
501
502
503
504
# File 'lib/mcp_toolkit/configuration.rb', line 494

def initialize_authority_hook_defaults
  @rate_limiter = nil
  @usage_recorder = nil
  @usage_flusher = nil
  @session_data_builder = nil
  @tool_provider = nil
  @extra_tool_providers = []
  @rate_limit_max_requests = nil # nil = rate limiting disabled
  @rate_limit_window = 3600 # 1 hour
  @superuser_resolver = nil # nil = duck-type principal.superuser?
end

#initialize_data_path_defaultsObject

Session-TTL and list-executor defaults: the :tokenized / :created_at data-path semantics (a host preserving a pre-gem contract overrides these — see each accessor's docs).



460
461
462
463
464
465
466
467
468
469
# File 'lib/mcp_toolkit/configuration.rb', line 460

def initialize_data_path_defaults
  @session_ttl = 3600 # 1 hour

  @sql_sanitizer = McpToolkit::SqlSanitizer.new
  @bare_filter_value_semantics = :tokenized
  @non_numeric_pk_order = :created_at
  @filter_operator_overrides = {}
  @max_filter_values = 500
  @max_batch_size = 50
end

#introspect_urlObject

Full introspection URL the satellite POSTs to. Raises a clear error if the central URL was never configured.



561
562
563
564
565
# File 'lib/mcp_toolkit/configuration.rb', line 561

def introspect_url
  raise McpToolkit::Errors::ConfigurationError, "central_app_url is not configured" if central_app_url.to_s.empty?

  "#{central_app_url.chomp("/")}#{introspect_path}"
end

#register_upstream(key:, url:, public_tool_list: true) ⇒ Object

Config sugar: register a gateway upstream. Delegates to upstreams.register, so a blank url is ignored (an unconfigured upstream is simply absent). Pass public_tool_list: false for an upstream whose tool list varies by caller privilege, to opt it out of the shared list cache.

c.register_upstream(key: "notifications", url: ENV["NOTIFICATIONS_SERVER_URL"])


512
513
514
# File 'lib/mcp_toolkit/configuration.rb', line 512

def register_upstream(key:, url:, public_tool_list: true)
  upstreams.register(key:, url:, public_tool_list:)
end

#register_upstreams_from_env(mapping, env: ENV) ⇒ Object

Declares the gateway's upstreams from an ENV-var map — { key => env var name } — the shape every authority host repeats: resets the registry first (idempotent across code reloads, where the registration typically re-runs in a to_prepare), and an upstream whose ENV url is blank is never registered, so an unconfigured environment behaves like no-upstreams. env is injectable for tests.

config.register_upstreams_from_env("billing" => "BILLING_MCP_URL")


524
525
526
527
# File 'lib/mcp_toolkit/configuration.rb', line 524

def register_upstreams_from_env(mapping, env: ENV)
  upstreams.reset!
  mapping.each { |key, env_var| register_upstream(key:, url: env[env_var.to_s]) }
end

#satellite?Boolean

Returns whether this app introspects tokens against a central app.

Returns:

  • (Boolean)

    whether this app introspects tokens against a central app.



549
550
551
# File 'lib/mcp_toolkit/configuration.rb', line 549

def satellite?
  auth_role.to_sym == :satellite || central_app_url
end