Class: Pgbus::MCP::RackApp
- Inherits:
-
Object
- Object
- Pgbus::MCP::RackApp
- 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.
DNS-rebinding protection: since mcp 0.23 the transport validates the Host
header (loopback only, by default) and the Origin header (same-origin
only). That defends a server bound to localhost against a browser page
whose DNS name was re-pointed at 127.0.0.1 — a page that can carry no
bearer token, because the secret never reaches the attacker's origin. So
when this app is gated (+token+ or auth) the check is redundant, and
left on it rejects every request to a real hostname
(https://app.example.com/pgbus/mcp → 403 "Invalid Host header"). The
default therefore follows the gate: off when gated, on when unauthenticated.
Override with dns_rebinding_protection:, and widen the accepted hosts /
origins with allowed_hosts: / allowed_origins: when the check is on.
Constant Summary collapse
- BEARER_PREFIX =
"Bearer "- MIN_MCP_VERSION =
First mcp release with the transport's allowed_hosts / allowed_origins / dns_rebinding_protection options. Older gems would raise ArgumentError on the pass-through; fail with the fix spelled out instead.
Gem::Version.new("0.23.0")
- 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
-
#call(env) ⇒ Object
Mount THIS object, never the bare transport.
-
#initialize(data_source: Pgbus::Web::DataSource.new, allow_payloads: false, token: nil, auth: nil, allowed_hosts: nil, allowed_origins: nil, dns_rebinding_protection: nil) ⇒ RackApp
constructor
A new instance of RackApp.
Constructor Details
#initialize(data_source: Pgbus::Web::DataSource.new, allow_payloads: false, token: nil, auth: nil, allowed_hosts: nil, allowed_origins: nil, dns_rebinding_protection: nil) ⇒ RackApp
Returns a new instance of RackApp.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/pgbus/mcp/rack_app.rb', line 67 def initialize(data_source: Pgbus::Web::DataSource.new, allow_payloads: false, token: nil, auth: nil, allowed_hosts: nil, allowed_origins: nil, dns_rebinding_protection: nil) check_mcp_version! @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, allowed_hosts: allowed_hosts, allowed_origins: allowed_origins, dns_rebinding_protection: dns_rebinding_protection.nil? ? unauthenticated? : dns_rebinding_protection ) warn_unauthenticated! if unauthenticated? 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.
86 87 88 89 90 91 |
# File 'lib/pgbus/mcp/rack_app.rb', line 86 def call(env) request = Rack::Request.new(env) return unless (request) @transport.handle_request(request) end |