Module: Pgbus::MCP::Runner
- Defined in:
- lib/pgbus/mcp/runner.rb
Overview
Boots the pgbus diagnostic MCP server over stdio. This is what ‘pgbus mcp` invokes. Separated from Server so the server assembly stays transport-agnostic and unit-testable without touching $stdin/$stdout.
Security posture (issue #180):
* Never starts unless explicitly invoked (`pgbus mcp`).
* Reuses the app's existing DB connection/config — no new privileged
path is opened.
* Payloads are redacted unless PGBUS_MCP_ALLOW_PAYLOADS is truthy.
* Optional token gate: when PGBUS_MCP_TOKEN is set, the same value must
be present in PGBUS_MCP_AUTH_TOKEN at boot or the server refuses to
start. stdio is a local, parent-spawned channel, so the gate is a
boot-time precondition rather than a per-message header.
Constant Summary collapse
- TRUTHY =
%w[1 true yes on].freeze
Class Method Summary collapse
-
.authorize!(env) ⇒ Object
Raise unless the configured token matches.
- .log_start(allow_payloads) ⇒ Object
-
.run(env: ENV) ⇒ Object
Build the server, enforce the optional token gate, and drive the stdio transport until the client disconnects.
-
.secure_compare?(expected, provided) ⇒ Boolean
Constant-time comparison to avoid leaking the token via timing.
- .truthy?(value) ⇒ Boolean
Class Method Details
.authorize!(env) ⇒ Object
Raise unless the configured token matches. No-op when PGBUS_MCP_TOKEN is unset (token gating is opt-in).
41 42 43 44 45 46 47 48 49 50 |
# File 'lib/pgbus/mcp/runner.rb', line 41 def (env) expected = env["PGBUS_MCP_TOKEN"] return if expected.nil? || expected.empty? provided = env["PGBUS_MCP_AUTH_TOKEN"] return if secure_compare?(expected, provided) raise Pgbus::Error, "pgbus MCP: authentication failed. PGBUS_MCP_TOKEN is set; provide a matching PGBUS_MCP_AUTH_TOKEN." end |
.log_start(allow_payloads) ⇒ Object
67 68 69 70 |
# File 'lib/pgbus/mcp/runner.rb', line 67 def log_start(allow_payloads) mode = allow_payloads ? "payloads ALLOWED" : "payloads redacted" Pgbus.logger.info { "[Pgbus::MCP] starting read-only diagnostic server over stdio (#{mode})" } end |
.run(env: ENV) ⇒ Object
Build the server, enforce the optional token gate, and drive the stdio transport until the client disconnects.
29 30 31 32 33 34 35 36 37 |
# File 'lib/pgbus/mcp/runner.rb', line 29 def run(env: ENV) (env) allow_payloads = truthy?(env["PGBUS_MCP_ALLOW_PAYLOADS"]) server = Server.build(allow_payloads: allow_payloads) transport = ::MCP::Server::Transports::StdioTransport.new(server) log_start(allow_payloads) transport.open end |
.secure_compare?(expected, provided) ⇒ Boolean
Constant-time comparison to avoid leaking the token via timing. Delegates to ActiveSupport’s vetted implementation (railties already pulls in active_support). secure_compare handles unequal lengths safely — it compares fixed-length SHA256 digests, then verifies the raw values match — so it never raises on a length mismatch.
61 62 63 64 65 |
# File 'lib/pgbus/mcp/runner.rb', line 61 def secure_compare?(expected, provided) return false if provided.nil? ActiveSupport::SecurityUtils.secure_compare(expected, provided) end |
.truthy?(value) ⇒ Boolean
52 53 54 |
# File 'lib/pgbus/mcp/runner.rb', line 52 def truthy?(value) TRUTHY.include?(value.to_s.strip.downcase) end |