Class: Pgbus::MCP::RackApp

Inherits:
Object
  • Object
show all
Defined in:
lib/pgbus/mcp/rack_app.rb

Overview

A turnkey, gated Rack app that serves the read-only pgbus diagnostic MCP server over HTTP. Mount it inside your existing Rails app — no second process, same DB credentials, same connection pool:

# config/routes.rb
mount Pgbus::MCP.rack_app(token: ENV["PGBUS_MCP_TOKEN"]) => "/pgbus/mcp"

The underlying transport runs in stateless + JSON-response mode, which:

* makes every request a self-contained POST returning a single JSON
object (no in-memory session, no SSE stream, no reaper thread), so it
is safe behind multiple Puma/Falcon workers — any worker can answer
any request;
* is exactly what a read-only diagnostic server needs (it never pushes
server-initiated messages to the client).

Security: requests are rejected with 401 unless they carry the configured token (or pass the supplied auth callable). Run it on an internal network / behind your VPN, never internet-exposed.

Constant Summary collapse

BEARER_PREFIX =
"Bearer "
UNAUTHORIZED_BODY =

Only the JSON body string is frozen and reused. The outer response triple and its headers hash MUST be built fresh per call (#unauthorized) so downstream Rack middleware can mutate them — Rack::TempfileReaper assigns response and Rack::ETag adds headers. Returning a frozen array/hash raised FrozenError → 500 instead of 401 (issue #304).

{ jsonrpc: "2.0", id: nil, error: { code: -32_001, message: "Unauthorized" } }.to_json.freeze

Instance Method Summary collapse

Constructor Details

#initialize(data_source: Pgbus::Web::DataSource.new, allow_payloads: false, token: nil, auth: nil) ⇒ RackApp

Returns a new instance of RackApp.

Parameters:

  • data_source (Pgbus::Web::DataSource) (defaults to: Pgbus::Web::DataSource.new)

    read layer the tools query. Built once and shared: DataSource acquires Pgbus::BusRecord.connection from the ActiveRecord pool on each call, so a single instance is request-safe across threads/workers.

  • allow_payloads (Boolean) (defaults to: false)

    when true, tools honor a per-call include_payloads flag; otherwise message bodies are always redacted.

  • token (String, nil) (defaults to: nil)

    shared secret. When present, requests must send Authorization: Bearer <token>. nil disables the built-in gate.

  • auth (#call, nil) (defaults to: nil)

    custom authenticator taking a Rack::Request and returning truthy to allow. Mirrors Pgbus.configuration.web_auth. Takes precedence over token when both are given.



43
44
45
46
47
48
49
50
51
# File 'lib/pgbus/mcp/rack_app.rb', line 43

def initialize(data_source: Pgbus::Web::DataSource.new, allow_payloads: false, token: nil, auth: nil)
  @token = token
  @auth = auth
  @server = Server.build(data_source: data_source, allow_payloads: allow_payloads)
  @transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(
    @server, stateless: true, enable_json_response: true
  )
  warn_unauthenticated! if @token.nil? && @auth.nil?
end

Instance Method Details

#call(env) ⇒ Object

Mount THIS object, never the bare transport. The auth gate lives here and runs before handle_request for every HTTP method (POST/GET/ DELETE/...). Mounting @transport directly would skip authorization — the gem's transport has no auth of its own.



57
58
59
60
61
62
# File 'lib/pgbus/mcp/rack_app.rb', line 57

def call(env)
  request = Rack::Request.new(env)
  return unauthorized unless authorized?(request)

  @transport.handle_request(request)
end