MCP

Operator gateway

Default-off, read-only MCP metadata tools with fail-closed authorization.

services/mcp-operator is a read-only MCP resource server for operators. It exposes four metadata-only tools and cannot register a mutating tool.

Implemented Ledger class: shipped-local
Verified 2026-10-21

Default-off behavior

Until IBEX_MCP_OPERATOR_ENABLED=true, the process serves only /health, /ready, and /metrics. Transport is Streamable HTTP only; stdio is not implemented. While disabled, no database, Redis, or public-URL configuration is required. Enabling it requires IBEX_MCP_OPERATOR_DATABASE_URL; protected profiles also require Redis, and production requires public HTTPS resource/auth-server URLs.

Tools

ToolArgumentsScopePermission bitReturns
session.getsession_idoperator.readOperatorMetadataReadRedacted session metadata
run.getrun_idruns.readTraceReadRedacted evidence-run metadata
evidence.queryoptional trace_id, session_id, request_id, error_code, status, limit 1–100 (default 25)evidence.readOperatorRedactedReadRedacted evidence-run list with count and truncated
budget.getactive_only (default true), limit 1–50 (default 20)budget.readBillingReadBudget periods in cached cents

All tools are marked read_only; the registry rejects a mutating tool. No tool accepts an org_id or agent_id argument; identity comes only from the validated token. Argument schemas are strict (additionalProperties: false).

Descriptor identity

Each tool has a versioned ToolSpec containing its scope, input/output JSON Schema, per-call timeout, and row limit. The canonical descriptor is hashed:

sha256:<hex over sorted, compact JSON of name, version, scope, role,
        description, input_schema, output_schema, timeout_ms, row_limit>

The digest is intended to bind the published surface to the server identity. Changing a schema, timeout, or limit changes the digest; treat that as a compatibility event rather than an incidental edit.

Enforcement order

org rate limit → resolve tool → authorize bitmap → validate args → timed read → redact → audit

Rate limiting runs before tool lookup and authorization. That prevents an unauthorized or malformed call from bypassing the org budget, at the cost of unknown calls consuming budget. Rate-limit audit events retain the requested tool name. The budget is org-wide, defaults to 120 requests per minute, and supports per-org overrides; there is no per-tool or per-agent tier. Redis configured but unavailable fails closed in protected profiles.

Reads run under a 2-second per-tool timeout. The database session is opened only after authorization, so a denied call never reaches PostgreSQL.

Error semantics

CodeMeaning
rate_limitedOrg budget exhausted
rate_limit_unavailableRate limiter unavailable; fail closed
tool_not_foundUnknown tool or resource hidden from this tenant
permission_deniedAuthenticated but missing the required bit
policy_unavailablePolicy decision could not be made
invalid_paramsArgument validation failed
backend_unavailableRead dependency failed

Unknown or cross-tenant resources return tool_not_found or not_found, never permission_denied, to avoid existence leaks.

Redaction

Output is sanitized against the tool's declared output schema:

  • Only schema-declared keys survive; everything else is dropped.
  • A deny-list removes sensitive names even if a schema accidentally declares them: content, prompt, token, secret, password, api_key, authorization, raw, payload, arguments, messages, and similar.
  • Scalar types must match the schema.
  • Missing required fields fail closed with redaction_failed.

The result is metadata only: no memory content, prompts, raw evidence payloads, or tool arguments.

Audit

Audit is metadata-only and never blocks the read:

Field setDestination
request_id, org_id, agent_id, tool_name, latency_ms, success, error_code, requested_atibex.mcp_tool_calls when ClickHouse is configured
Descriptor hash, scope, policy decision, and the same metadataStructured logs

Without a ClickHouse URL, audit is logging-only. A full queue drops events; a sink write is not transactional evidence and does not replace the evidence relay.

Authorization caveat

Authorization is a bitmap check on the AuthService-validated token. The PolicySnapshot resource/action evaluator of mcp-gateway-policy.v1 is not wired to a Python verifier. The PolicyEvaluator seam is the only extension point today. This is tracked in GAP-009.

Configuration

VariablePurpose
IBEX_MCP_OPERATOR_ENABLEDDefault false; enables the MCP surface
IBEX_MCP_OPERATOR_DATABASE_URLTenant-scoped read database; required when enabled
IBEX_MCP_OPERATOR_REDIS_URLOrg rate limiting; required in protected profiles
IBEX_MCP_OPERATOR_RATE_LIMIT_RPMOrg-wide requests per minute; default 120
IBEX_MCP_OPERATOR_RATE_LIMIT_ORG_OVERRIDESPer-org RPM overrides
IBEX_MCP_OPERATOR_CLICKHOUSE_URLOptional audit sink
IBEX_MCP_OPERATOR_RESOURCE_URLAdvertised MCP resource; public HTTPS in production
IBEX_MCP_OPERATOR_AUTH_SERVER_URLAdvertised authorization server; public HTTPS in production
IBEX_AUTH_GRPC_ADDRAuthService target
IBEX_OTEL_EXPORTER_OTLP_ENDPOINTOptional tracing export

The service has no OAuth authorization-server implementation and no write path.

Run locally

uv sync --all-extras
IBEX_ENV=development \
IBEX_MCP_OPERATOR_ENABLED=true \
IBEX_MCP_OPERATOR_DATABASE_URL=postgresql+asyncpg://ibex_app:ibex_app@127.0.0.1:5432/ibex \
IBEX_AUTH_GRPC_ADDR=127.0.0.1:9091 \
uv run uvicorn app.main:app --host 127.0.0.1 --port 8091

Verification

uv run pytest -q
uv run pytest -m integration -q

Integration tests require IBEX_MCP_OPERATOR_TEST_DATABASE_URL (or IBEX_DATABASE_URL / DATABASE_URL) and include RLS and cross-tenant checks. A passing unit suite does not prove hosted authorization.

Limits

  • PolicySnapshot evaluation is not wired; bitmap authorization is the current decision.
  • Audit durability and retention are unproven by default.
  • No stdio transport, mutation tool, or OAuth authorization-server behavior.
  • Streamable-HTTP multi-replica behavior and hosted latencies are not benchmarked here.
  • Descriptor hashes detect surface changes; they do not prove a client pinned the expected hash.