Class: TalkToYourApp::Configuration

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/talk_to_your_app/configuration.rb', line 74

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_hostsObject

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).



43
44
45
# File 'lib/talk_to_your_app/configuration.rb', line 43

def allowed_hosts
  @allowed_hosts
end

#allowed_originsObject

Origins permitted for browser-originated requests (DNS-rebinding protection). Empty allowlist permits non-browser clients (no Origin).



35
36
37
# File 'lib/talk_to_your_app/configuration.rb', line 35

def allowed_origins
  @allowed_origins
end

#api_keysObject

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

#connectionsObject (readonly)

Declared named connections, keyed by gem-internal symbol name.



129
130
131
# File 'lib/talk_to_your_app/configuration.rb', line 129

def connections
  @connections
end

#enabledObject

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.



49
50
51
# File 'lib/talk_to_your_app/configuration.rb', line 49

def enabled
  @enabled
end

#enabled_pluginsObject (readonly)

Enabled plugins, keyed by name => options hash. Plugins are off by default.



132
133
134
# File 'lib/talk_to_your_app/configuration.rb', line 132

def enabled_plugins
  @enabled_plugins
end

#instructionsObject

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_levelObject

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

#loggerObject

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_atObject

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_descriptionObject

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_nameObject

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_titleObject

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_versionObject

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

#statelessObject

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.



72
73
74
# File 'lib/talk_to_your_app/configuration.rb', line 72

def stateless
  @stateless
end

Instance Method Details

#auth_configured?Boolean

True when at least one authentication mechanism is configured.

Returns:

  • (Boolean)


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

def auth_configured?
  api_keys.any? || !@basic_auth.nil?
end

#authorize(&block) ⇒ Object

Optional per-principal tool authorization. The block receives (principal, tool_name) and returns truthy to allow the call. With no authorizer configured, every authenticated principal may call every tool.

config.authorize { |principal, tool| principal == "admin" || tool.start_with?("db.") }


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

def authorize(&block)
  @authorizer = block if block
  @authorizer
end

#authorized?(principal, tool_name) ⇒ Boolean

Returns:

  • (Boolean)


118
119
120
121
122
123
124
125
126
# File 'lib/talk_to_your_app/configuration.rb', line 118

def authorized?(principal, tool_name)
  return true if @authorizer.nil?

  @authorizer.call(principal, tool_name)
rescue StandardError => e
  # A raising authorizer denies (fail-closed), mirroring basic_auth handling.
  warn("talk_to_your_app: authorizer raised: #{e.class}: #{e.message}")
  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) }


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

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).



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/talk_to_your_app/configuration.rb', line 164

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.



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/talk_to_your_app/configuration.rb', line 142

def plugin(name, **options)
  key = name.to_sym
  existing = @enabled_plugins[key]
  if existing && options.key?(:connection) && existing.key?(:connection) &&
     existing[:connection] != options[:connection]
    raise ConfigurationError,
      "plugin #{key.inspect} is already wired to connection #{existing[:connection].inspect}; " \
      "refusing to silently re-wire it to #{options[:connection].inspect}."
  end
  @enabled_plugins[key] = (existing || {}).merge(options)
end