Operator gateway
services/mcp-operator is a read-only MCP resource server for operators. It exposes four metadata-only tools and cannot register a mutating tool.
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
| Tool | Arguments | Scope | Permission bit | Returns |
|---|---|---|---|---|
session.get | session_id | operator.read | OperatorMetadataRead | Redacted session metadata |
run.get | run_id | runs.read | TraceRead | Redacted evidence-run metadata |
evidence.query | optional trace_id, session_id, request_id, error_code, status, limit 1–100 (default 25) | evidence.read | OperatorRedactedRead | Redacted evidence-run list with count and truncated |
budget.get | active_only (default true), limit 1–50 (default 20) | budget.read | BillingRead | Budget 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
| Code | Meaning |
|---|---|
rate_limited | Org budget exhausted |
rate_limit_unavailable | Rate limiter unavailable; fail closed |
tool_not_found | Unknown tool or resource hidden from this tenant |
permission_denied | Authenticated but missing the required bit |
policy_unavailable | Policy decision could not be made |
invalid_params | Argument validation failed |
backend_unavailable | Read 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 set | Destination |
|---|---|
request_id, org_id, agent_id, tool_name, latency_ms, success, error_code, requested_at | ibex.mcp_tool_calls when ClickHouse is configured |
| Descriptor hash, scope, policy decision, and the same metadata | Structured 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
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
| Variable | Purpose |
|---|---|
IBEX_MCP_OPERATOR_ENABLED | Default false; enables the MCP surface |
IBEX_MCP_OPERATOR_DATABASE_URL | Tenant-scoped read database; required when enabled |
IBEX_MCP_OPERATOR_REDIS_URL | Org rate limiting; required in protected profiles |
IBEX_MCP_OPERATOR_RATE_LIMIT_RPM | Org-wide requests per minute; default 120 |
IBEX_MCP_OPERATOR_RATE_LIMIT_ORG_OVERRIDES | Per-org RPM overrides |
IBEX_MCP_OPERATOR_CLICKHOUSE_URL | Optional audit sink |
IBEX_MCP_OPERATOR_RESOURCE_URL | Advertised MCP resource; public HTTPS in production |
IBEX_MCP_OPERATOR_AUTH_SERVER_URL | Advertised authorization server; public HTTPS in production |
IBEX_AUTH_GRPC_ADDR | AuthService target |
IBEX_OTEL_EXPORTER_OTLP_ENDPOINT | Optional 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:[email protected]: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.

