Mcp Approval Floor
Demonstrates the openai_mcp_dispatch operator approval floor on a complete persisted agentic Responses pipeline
Versions marked “overview” do not contain this page. Selecting one opens that version’s documentation overview.
Category: Setup-dependent integration
Task: Demonstrates the openai_mcp_dispatch operator approval floor on a complete persisted agentic Responses pipeline
Prerequisites: The external service, credentials, or certificates referenced by this configuration.
This configuration comes from the selected release. The example has not been run here; external services are not bundled.
Download the source file.
Companion resources from the same snapshot:
# MCP Approval Floor
# Requires `--features openai-mcp-tools,store-sqlite` because these filters are opt-in.
#
# Demonstrates the openai_mcp_dispatch operator approval floor on a complete
# persisted agentic Responses pipeline. The floor (minimum_approval: always) is
# combined monotonically with each MCP tool's client require_approval, so a
# client that submits "require_approval": "never" still produces an
# mcp_approval_request and runs zero tool calls until it is approved. Omit
# minimum_approval to keep the default client-driven behavior.
#
# The effective policy is:
#
# approval_required = operator_floor || client_requires_approval
#
# The floor can only raise the requirement: a client "never" (or a tool the
# server annotates readOnlyHint: true) can never suppress an operator-required
# approval. This enforces an approval *exchange*; it does not prove a human
# approved the action, since an authorized API client can submit the
# mcp_approval_response.
#
# The pipeline mirrors agentic-loop.yaml (see it for the full lifecycle): a pre-
# IRR state setup with a Responses store and rehydrate, then an
# iterative_request_router inference step that pairs openai_mcp_dispatch with the
# openai_agentic_loop owner. The store is REQUIRED: a pending approval is
# correlated on the follow-up turn via previous_response_id, so the floor fails
# closed (store=false -> 400, no store -> 500) when it cannot persist.
#
# Pipeline ordering (pre-IRR, runs once):
# ai_operation -> openai_responses_request (x2) -> openai_tool_parse
# -> state_owner -> openai_responses_store -> openai_responses_rehydrate
# -> openai_mcp_tool_resolve
# IRR inference step:
# openai_stream_events -> openai_mcp_dispatch (minimum_approval: always)
# -> openai_agentic_loop -> openai_responses_proxy -> router -> load_balancer
#
# Example (client asks for NO approval; the operator floor forces it):
#
# curl -X POST http://localhost:8080/v1/responses \
# -H "Content-Type: application/json" \
# -d '{
# "model": "gpt-4.1",
# "input": "What is the weather in SF?",
# "tools": [{"type": "mcp", "server_label": "weather",
# "server_url": "http://mcp-weather:8001/mcp",
# "allowed_tools": ["get_weather"],
# "require_approval": "never"}]
# }'
# # -> returns an mcp_approval_request output item (not an executed tool call).
# # Approve it on the next turn by echoing the returned response id and an
# # mcp_approval_response, exactly as in agentic-loop.yaml's round trip.
#
# Build:
# cargo build -p praxis-ai-proxy --features openai-mcp-tools,store-sqlite
listeners:
- name: ai-gateway
address: "127.0.0.1:8080"
filter_chains: [mcp-approval-floor-pipeline]
filter_chains:
- name: mcp-approval-floor-pipeline
filters:
- filter: ai_operation
- filter: openai_responses_request
initialize_state: false
on_invalid: reject
headers:
format: x-praxis-ai-format
model: x-praxis-ai-model
stream: x-praxis-ai-stream
- filter: openai_responses_request
on_invalid: reject
headers:
format: ~
model: ~
stream: ~
mode: ~
- filter: openai_tool_parse
- filter: state_owner
mode: single_tenant
tenant_id: default
# REQUIRED for the floor: the pending approval is persisted here and
# correlated on the follow-up turn via previous_response_id.
- filter: openai_responses_store
backend: sqlite
database_url: "sqlite://responses.db?mode=rwc"
responses_table: openai_responses
conversations_table: openai_conversations
- filter: openai_responses_rehydrate
- filter: openai_mcp_tool_resolve
- filter: iterative_request_router
initial_step: inference
max_iterations: 11
max_stream_response_bytes: 67108864
timeout_ms: 360000
steps:
- name: inference
filters:
- filter: openai_stream_events
- filter: openai_mcp_dispatch
# Operator approval floor: force approval for every MCP call in
# this pipeline, regardless of the client's per-tool
# require_approval. Remove this line to restore client-driven
# approval.
minimum_approval: always
max_calls_per_round: 32
max_parallel_calls: 8
max_result_bytes: 1048576
max_total_result_bytes: 8388608
- filter: openai_agentic_loop
max_infer_iters: 10
- filter: openai_responses_proxy
- filter: router
routes:
- path_prefix: "/"
cluster: "inference-backend"
- filter: load_balancer
clusters:
- name: "inference-backend"
read_timeout_ms: 300000
endpoints:
- "127.0.0.1:3001"
on_result:
- filter: openai_agentic_loop
key: action
value: loop
next: inference
- default: true
done: true
insecure_options:
allow_private_endpoints: true # example proxies to local backends
allow_private_upstreams: true # permits loopback MCP servers in local dev