Class: TalkToYourApp::Configuration
- Inherits:
-
Object
- Object
- TalkToYourApp::Configuration
- Defined in:
- lib/talk_to_your_app/configuration.rb
Overview
The single mutable configuration object. Held as a memoized singleton on
the TalkToYourApp module, so calling TalkToYourApp.configure more than
once merges into the same instance rather than replacing it.
Constant Summary collapse
- FALSEY_STRINGS =
Stringy env values are common here (
config.enabled = ENV["MCP_ENABLED"]), and every non-empty string is truthy in Ruby — so "false"/"0"/"" would otherwise serve. Coerce the common falsey forms to a real boolean; the disabled side is the safe one to bias toward. %w[false 0 no off].freeze
Instance Attribute Summary collapse
-
#allowed_hosts ⇒ Object
Extra
Hostheader values accepted by the transport's DNS-rebinding protection, beyond the always-allowed loopback defaults (127.0.0.1, ::1, localhost). -
#allowed_origins ⇒ Object
Origins permitted for browser-originated cross-origin requests (DNS-rebinding protection).
-
#api_keys ⇒ Object
Named API keys, { "principal-name" => "secret-key" }.
-
#connections ⇒ Object
readonly
Declared named connections, keyed by gem-internal symbol name.
-
#enabled ⇒ Object
Global on/off switch for the whole gem.
-
#enabled_plugins ⇒ Object
readonly
Enabled plugins, keyed by name => options hash.
-
#instructions ⇒ Object
MCP server identity, surfaced to clients in the
initializehandshake (serverInfo + instructions). -
#log_level ⇒ Object
Global audit log level (default :info).
-
#logger ⇒ Object
Audit logger.
-
#mount_at ⇒ Object
Path the MCP endpoint is mounted at in the host app's router.
-
#server_description ⇒ Object
MCP server identity, surfaced to clients in the
initializehandshake (serverInfo + instructions). -
#server_name ⇒ Object
MCP server identity, surfaced to clients in the
initializehandshake (serverInfo + instructions). -
#server_title ⇒ Object
MCP server identity, surfaced to clients in the
initializehandshake (serverInfo + instructions). -
#server_version ⇒ Object
MCP server identity, surfaced to clients in the
initializehandshake (serverInfo + instructions). -
#stateless ⇒ Object
When true, the Streamable HTTP transport runs stateless: every request is self-contained, with no per-session state held in the transport.
Instance Method Summary collapse
-
#auth_configured? ⇒ Boolean
True when at least one authentication mechanism is configured.
-
#authorize(&block) ⇒ Object
Optional per-principal tool authorization.
-
#authorize_configured? ⇒ Boolean
True when a per-principal authorizer has been set.
- #authorized?(principal, tool_name, args = {}) ⇒ Boolean
-
#basic_auth(&block) ⇒ Object
Sets or reads the HTTP Basic auth callable.
-
#connection(name, database:, role: :reading, replica: false, statement_timeout: nil) ⇒ Object
Declares a named connection plugins can reference.
-
#initialize ⇒ Configuration
constructor
A new instance of Configuration.
-
#plugin(name, **options) ⇒ Object
Enables a registered plugin, with optional per-plugin options.
Constructor Details
#initialize ⇒ Configuration
Returns a new instance of Configuration.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/talk_to_your_app/configuration.rb', line 76 def initialize @enabled = true @mount_at = "/mcp" @server_name = "talk_to_your_app" @server_version = TalkToYourApp::VERSION @server_title = nil @server_description = nil @instructions = nil @connections = {} @enabled_plugins = {} @logger = nil @api_keys = {} @allowed_origins = [] @allowed_hosts = [] @stateless = false @basic_auth = nil @log_level = :info @authorizer = nil end |
Instance Attribute Details
#allowed_hosts ⇒ Object
Extra Host header values accepted by the transport's DNS-rebinding
protection, beyond the always-allowed loopback defaults (127.0.0.1, ::1,
localhost). A non-loopback deployment (the endpoint served from a real
domain) MUST list its host here, or every request is rejected with
"Forbidden: Invalid Host header". Each entry matches a bare host name (any
port) or a full host:port. Empty by default (loopback only).
45 46 47 |
# File 'lib/talk_to_your_app/configuration.rb', line 45 def allowed_hosts @allowed_hosts end |
#allowed_origins ⇒ Object
Origins permitted for browser-originated cross-origin requests (DNS-rebinding protection). Forwarded to the SDK transport, which owns the check: no-Origin (non-browser) and same-origin requests are always allowed; a cross-origin request must match this list (case-insensitive).
37 38 39 |
# File 'lib/talk_to_your_app/configuration.rb', line 37 def allowed_origins @allowed_origins end |
#api_keys ⇒ Object
Named API keys, { "principal-name" => "secret-key" }. The name is logged as the principal. Supports multiple keys for rotation.
31 32 33 |
# File 'lib/talk_to_your_app/configuration.rb', line 31 def api_keys @api_keys end |
#connections ⇒ Object (readonly)
Declared named connections, keyed by gem-internal symbol name.
142 143 144 |
# File 'lib/talk_to_your_app/configuration.rb', line 142 def connections @connections end |
#enabled ⇒ Object
Global on/off switch for the whole gem. When false, the mounted endpoint serves nothing (503) and boot validation is skipped, so an operator can ship the initializer and disable it per-environment without the gem refusing to boot on otherwise-incomplete configuration. Default true.
51 52 53 |
# File 'lib/talk_to_your_app/configuration.rb', line 51 def enabled @enabled end |
#enabled_plugins ⇒ Object (readonly)
Enabled plugins, keyed by name => options hash. Plugins are off by default.
145 146 147 |
# File 'lib/talk_to_your_app/configuration.rb', line 145 def enabled_plugins @enabled_plugins end |
#instructions ⇒ Object
MCP server identity, surfaced to clients in the initialize handshake
(serverInfo + instructions). All optional except name/version, which
default sensibly.
19 20 21 |
# File 'lib/talk_to_your_app/configuration.rb', line 19 def instructions @instructions end |
#log_level ⇒ Object
Global audit log level (default :info). Overridable per plugin via the
plugin DSL's log_level.
27 28 29 |
# File 'lib/talk_to_your_app/configuration.rb', line 27 def log_level @log_level end |
#logger ⇒ Object
Audit logger. Defaults to Rails.logger at boot; swappable to any object implementing the Logger interface.
23 24 25 |
# File 'lib/talk_to_your_app/configuration.rb', line 23 def logger @logger end |
#mount_at ⇒ Object
Path the MCP endpoint is mounted at in the host app's router. Default "/mcp".
14 15 16 |
# File 'lib/talk_to_your_app/configuration.rb', line 14 def mount_at @mount_at end |
#server_description ⇒ Object
MCP server identity, surfaced to clients in the initialize handshake
(serverInfo + instructions). All optional except name/version, which
default sensibly.
19 20 21 |
# File 'lib/talk_to_your_app/configuration.rb', line 19 def server_description @server_description end |
#server_name ⇒ Object
MCP server identity, surfaced to clients in the initialize handshake
(serverInfo + instructions). All optional except name/version, which
default sensibly.
19 20 21 |
# File 'lib/talk_to_your_app/configuration.rb', line 19 def server_name @server_name end |
#server_title ⇒ Object
MCP server identity, surfaced to clients in the initialize handshake
(serverInfo + instructions). All optional except name/version, which
default sensibly.
19 20 21 |
# File 'lib/talk_to_your_app/configuration.rb', line 19 def server_title @server_title end |
#server_version ⇒ Object
MCP server identity, surfaced to clients in the initialize handshake
(serverInfo + instructions). All optional except name/version, which
default sensibly.
19 20 21 |
# File 'lib/talk_to_your_app/configuration.rb', line 19 def server_version @server_version end |
#stateless ⇒ Object
When true, the Streamable HTTP transport runs stateless: every request is
self-contained, with no per-session state held in the transport. Required
when the host app runs more than one Puma/Unicorn worker or replica, where
a follow-up request can land on a process that never saw the initialize
handshake and would otherwise fail with "Session not found". Trades away
SSE streaming and server-initiated notifications, neither of which the
bundled read-only tools use. Default false.
74 75 76 |
# File 'lib/talk_to_your_app/configuration.rb', line 74 def stateless @stateless end |
Instance Method Details
#auth_configured? ⇒ Boolean
True when at least one authentication mechanism is configured.
106 107 108 |
# File 'lib/talk_to_your_app/configuration.rb', line 106 def auth_configured? api_keys.any? || !@basic_auth.nil? end |
#authorize(&block) ⇒ Object
Optional per-principal tool authorization. The block receives
(principal, tool_name, args) and returns truthy to allow the call. args
is the tool's argument hash (symbol keys, as received — defaults not yet
applied), enabling per-flag/per-task/per-table decisions. A two-parameter
block keeps working (Ruby blocks ignore extra arguments); an explicit
lambda must accept all three. With no authorizer configured, every
authenticated principal may call every tool.
config. { |principal, tool| principal == "admin" || tool.start_with?("db.") }
config. { |_p, tool, args| tool != "flipper.enable_flag" || args[:name] != "require_2fa" }
126 127 128 129 |
# File 'lib/talk_to_your_app/configuration.rb', line 126 def (&block) @authorizer = block if block @authorizer end |
#authorize_configured? ⇒ Boolean
True when a per-principal authorizer has been set. Without one, every authenticated principal may call every enabled tool.
112 113 114 |
# File 'lib/talk_to_your_app/configuration.rb', line 112 def !@authorizer.nil? end |
#authorized?(principal, tool_name, args = {}) ⇒ Boolean
131 132 133 134 135 136 137 138 139 |
# File 'lib/talk_to_your_app/configuration.rb', line 131 def (principal, tool_name, args = {}) return true if @authorizer.nil? @authorizer.call(principal, tool_name, args) rescue StandardError => e # A raising authorizer denies (fail-closed), mirroring basic_auth handling. warn("talk_to_your_app: authorizer raised: #{e.class}: #{e.}") false end |
#basic_auth(&block) ⇒ Object
Sets or reads the HTTP Basic auth callable. The block receives (username, password) and returns truthy to authenticate.
config.basic_auth { |user, pass| User.authenticate(user, pass) }
100 101 102 103 |
# File 'lib/talk_to_your_app/configuration.rb', line 100 def basic_auth(&block) @basic_auth = block if block @basic_auth end |
#connection(name, database:, role: :reading, replica: false, statement_timeout: nil) ⇒ Object
Declares a named connection plugins can reference.
config.connection :read, database: "primary" # role: :reading (default)
config.connection :write, database: "primary", role: :writing
database is a database.yml config key. role defaults to :reading (the
safe default; a :reading connection prevents writes at the Rails layer) —
pass role: :writing explicitly for a writer. replica: true marks the
connection as pointing at a replica (informational; combining it with
role: :writing is rejected as nonsensical).
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/talk_to_your_app/configuration.rb', line 177 def connection(name, database:, role: :reading, replica: false, statement_timeout: nil) role = role.to_sym unless %i[reading writing].include?(role) raise ConfigurationError, "connection #{name.inspect}: role must be :reading or :writing, got #{role.inspect}." end if replica && role == :writing raise ConfigurationError, "connection #{name.inspect}: `replica: true` with `role: :writing` is nonsensical — a replica cannot accept writes." end @connections[name.to_sym] = ConnectionRegistry::ConnectionSpec.new( name: name.to_sym, database: database.to_sym, role: role, replica: replica, statement_timeout: statement_timeout, ) end |
#plugin(name, **options) ⇒ Object
Enables a registered plugin, with optional per-plugin options.
config.plugin :db, connection: :read
config.plugin :sidekiq, connection: false
Re-declaring a plugin merges options, so a later call can refine an earlier one. But re-wiring it to a different connection is rejected — a silent last-wins overwrite could point :db at a writable connection unnoticed.
155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/talk_to_your_app/configuration.rb', line 155 def plugin(name, **) key = name.to_sym existing = @enabled_plugins[key] if existing && .key?(:connection) && existing.key?(:connection) && existing[:connection] != [:connection] raise ConfigurationError, "plugin #{key.inspect} is already wired to connection #{existing[:connection].inspect}; " \ "refusing to silently re-wire it to #{[:connection].inspect}." end @enabled_plugins[key] = (existing || {}).merge() end |