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

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

Raises:



41
42
43
44
45
46
47
48
49
50
# File 'lib/pgbus/mcp/runner.rb', line 41

def authorize!(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.

Parameters:

  • env (Hash) (defaults to: ENV)

    environment to read flags from (injectable for tests).



29
30
31
32
33
34
35
36
37
# File 'lib/pgbus/mcp/runner.rb', line 29

def run(env: ENV)
  authorize!(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.

Returns:

  • (Boolean)


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

Returns:

  • (Boolean)


52
53
54
# File 'lib/pgbus/mcp/runner.rb', line 52

def truthy?(value)
  TRUTHY.include?(value.to_s.strip.downcase)
end