Class: Woods::Console::RackMiddleware

Inherits:
Object
  • Object
show all
Defined in:
lib/woods/console/rack_middleware.rb

Overview

Rack middleware that serves the embedded console MCP server over HTTP.

Lazy-builds the MCP server on first request so Rails has fully booted and all models are loaded. Uses ActiveRecord connection pool for thread safety under Puma.

Basic setup (Tier 1 tools only)

Add to config/application.rb or an initializer:

config.middleware.use Woods::Console::RackMiddleware, path: '/mcp/console'

This mounts 31 console tools at /mcp/console. By default, console_sql and console_query are blocked in embedded mode and return an “unsupported” error pointing users to enable the flag.

Enabling the feature

The Console MCP is disabled by default. Enable it in your Woods initializer:

Woods.configure do |config|
  config.console_mcp_enabled = true
  config.console_blocked_tables = %w[authorizations credentials]
  config.console_redacted_columns = %w[api_token password_digest]
end

With the flag off, requests to the mounted path return 410 Gone so operators can see the endpoint exists but is gated. See docs/CONSOLE_MCP_SETUP.md for the full security posture (blocked tables, credential scanner, column/EAV redaction, SafeContext rollback).

Enabling read tools (console_sql + console_query)

Set embedded_read_tools: true to unlock the sql and query tools:

# config/initializers/woods_console.rb
Rails.application.config.middleware.use \
  Woods::Console::RackMiddleware,
  path: '/mcp/console',
  embedded_read_tools: true

Security posture with embedded_read_tools: true:

  1. SqlValidator denylist — console_sql rejects INSERT/UPDATE/DELETE/DROP/TRUNCATE/ ALTER/CREATE/REPLACE and similar DML/DDL at the string level before any database interaction. Only SELECT and WITH…SELECT are allowed.

  2. SafeContext rollback — every request (including console_query) runs inside a database transaction that is always rolled back on completion. Even if a query somehow mutated state (e.g. a function with side effects), the rollback ensures nothing persists.

  3. Per-request connection pooling — each HTTP request draws a connection from ActiveRecord::Base’s pool and returns it after the response. No shared mutable state leaks between requests.

These three layers make embedded_read_tools: true safe for read-only workloads. If your threat model requires stricter isolation, use the bridge mode instead (docs/CONSOLE_MCP_SETUP.md) which runs the executor in a separate process.

Constant Summary collapse

DISABLED_BODY =
JSON.generate(
  error: 'woods_console_disabled',
  message: 'Woods Console MCP is disabled. Set ' \
           'Woods.configuration.console_mcp_enabled = true to enable. ' \
           'See docs/CONSOLE_MCP_SETUP.md for the full security posture.'
).freeze

Instance Method Summary collapse

Constructor Details

#initialize(app, path: '/mcp/console', embedded_read_tools: false, unsafe_eval_confirmation: nil, unsafe_eval_audit_log_path: nil) ⇒ RackMiddleware

Returns a new instance of RackMiddleware.

Parameters:

  • app (#call)

    The next Rack app in the middleware stack

  • path (String) (defaults to: '/mcp/console')

    URL path to mount the MCP endpoint (default: ‘/mcp/console’)

  • embedded_read_tools (Boolean) (defaults to: false)

    Enable sql/query tools in embedded mode (default: false)

  • unsafe_eval_confirmation (Confirmation, nil) (defaults to: nil)

    Approval callback for the ‘console_eval` opt-in. Required when `WOODS_CONSOLE_UNSAFE_EVAL=true` (or `config.console_unsafe_eval_enabled = true`); the server refuses to boot without it. Takes precedence over `config.console_unsafe_eval_confirmation`.

  • unsafe_eval_audit_log_path (String, Pathname, nil) (defaults to: nil)

    JSONL audit log path for every ‘console_eval` run. Required on the opt-in path. Takes precedence over `config.console_unsafe_eval_audit_log_path`.



79
80
81
82
83
84
85
86
87
88
# File 'lib/woods/console/rack_middleware.rb', line 79

def initialize(app, path: '/mcp/console', embedded_read_tools: false,
               unsafe_eval_confirmation: nil, unsafe_eval_audit_log_path: nil)
  @app = app
  @path = path
  @embedded_read_tools = embedded_read_tools
  @unsafe_eval_confirmation = unsafe_eval_confirmation
  @unsafe_eval_audit_log_path = unsafe_eval_audit_log_path
  @mutex = Mutex.new
  @transport = nil
end

Instance Method Details

#call(env) ⇒ Array

Rack interface — intercepts requests at the configured path.

Returns 410 Gone when Woods.configuration.console_mcp_enabled is false (the default). This keeps the middleware inert on hosts that have mounted it but not yet opted into the feature. All other requests at non-matching paths pass through to the wrapped app unchanged.

Parameters:

  • env (Hash)

    Rack environment

Returns:

  • (Array)

    Rack response triple



106
107
108
109
110
111
# File 'lib/woods/console/rack_middleware.rb', line 106

def call(env)
  return @app.call(env) unless env['PATH_INFO'].start_with?(@path)
  return [410, { 'content-type' => 'application/json' }, [DISABLED_BODY]] unless enabled?

  ensure_transport.handle_request(Rack::Request.new(env))
end