Skip to main content

Connecting to the Controlplane

GovernanceMiddleware works in two modes: standalone (local governance only) and connected (centralized policy management via the Waxell controlplane). This page explains how to connect your middleware to the controlplane and what happens when the controlplane is unreachable.

Prerequisites

  • Python 3.10+
  • A FastMCP server with GovernanceMiddleware installed (see Middleware Quickstart)
  • A Waxell account with an API key
pip install waxell-observe[mcp-server]

Standalone vs Connected Mode

FeatureStandaloneConnected
PII scanningYesYes
Rate limitingYesYes
OTel span recordingYesYes
Per-tool config (@governance, tool_configs)YesYes
Centralized policy checksNoYes
Remote policy managementNoYes
Audit trail in controlplaneSpans onlySpans + policy decisions

Standalone mode is the default when no api_key is provided. All local governance features (PII scanning, rate limiting, per-tool config) work without a controlplane connection. Policy checks are skipped silently.

Connected mode activates when you provide an api_key. The middleware sends policy check requests to the controlplane before every tool call, using the mcp:{tool_name} pattern.


Setting Up the Connection

Provide your API key and API URL to the GovernanceMiddleware constructor:

from fastmcp import FastMCP
from waxell_observe.mcp import GovernanceMiddleware

mcp = FastMCP("MyServer")

mcp.add_middleware(GovernanceMiddleware(
api_key="wax_sk_...",
api_url="https://acme.waxell.dev",
))

You can also use environment variables by reading them at initialization:

import os

mcp.add_middleware(GovernanceMiddleware(
api_key=os.environ.get("WAXELL_API_KEY", ""),
api_url=os.environ.get("WAXELL_API_URL", ""),
))

Auto-Initialization

On the first tool call, the middleware automatically calls waxell_observe.init() with the provided credentials. This sets up OTel tracing and the controlplane connection. The init call is idempotent -- subsequent tool calls skip initialization.

You do not need to call waxell.init() separately in your server code.


How Policy Checks Work

When a tool call arrives, the middleware sends a policy check to the controlplane using the pattern mcp:{tool_name}:

Tool call: read_file(path="/etc/passwd")
-> Policy check: mcp:read_file
-> Controlplane returns: { action: "block", reason: "Sensitive file access" }
-> ToolError raised, tool does not execute

The controlplane evaluates all matching policy rules and returns an action:

ActionMeaningTool executes?
allowPolicy permits the callYes
blockPolicy forbids the callNo -- ToolError raised
warnPolicy flags the call but permits executionYes (warning logged)
throttlePolicy permits the call after an enforced delayYes (after delay, capped at 30s)

Policy Action Override

Per-tool configuration can override the controlplane's action. If a ToolGovernanceConfig specifies policy_action, that value takes precedence:

from waxell_observe.mcp import GovernanceMiddleware, ToolGovernanceConfig

mcp.add_middleware(GovernanceMiddleware(
api_key="wax_sk_...",
tool_configs={
"delete_user": ToolGovernanceConfig(
policy_action="block", # Always block, regardless of controlplane
),
},
))

Fail-Open Semantics

Fail-open is the default

When the controlplane is unreachable, the middleware allows tool calls to proceed by default. This ensures your MCP server remains functional even during controlplane outages.

The fail_open setting controls behavior when governance processing fails:

Scenariofail_open=True (default)fail_open=False
Controlplane unreachableTool executes, policy_skipped span attribute setGovernance error propagates
Policy check timeoutTool executes with warningError raised
PII scan errorTool executes with warningError raised
Rate limit checkAlways enforced (local)Always enforced (local)
Intentional blocks (ToolError)Always propagatedAlways propagated

Key distinction: Intentional blocks (rate limit exceeded, policy action = "block", PII detected in strict mode) always raise ToolError regardless of the fail_open setting. Fail-open only applies to unexpected errors in the governance pipeline itself.

Configuring Fail-Open

mcp.add_middleware(GovernanceMiddleware(
api_key="wax_sk_...",
policy_config={
"fail_open": True, # Default: governance errors let calls proceed
},
))

To run in fail-closed mode (governance errors block tool calls):

mcp.add_middleware(GovernanceMiddleware(
api_key="wax_sk_...",
policy_config={
"fail_open": False, # Governance errors block tool calls
},
))

What Gets Recorded on the Span

When the controlplane is unreachable and fail-open activates:

Span AttributeValue
waxell.mcp.policy_skippedtrue
waxell.mcp.governance_checkedfalse
waxell.mcp.governance_actionNot set (no policy decision available)

This lets you query for calls that bypassed governance:

{ span.waxell.mcp.policy_skipped = true }

Environment Variable Configuration

For production deployments, pass credentials via environment variables rather than hardcoding them:

export WAXELL_API_KEY="wax_sk_..."
export WAXELL_API_URL="https://acme.waxell.dev"
import os
from fastmcp import FastMCP
from waxell_observe.mcp import GovernanceMiddleware

mcp = FastMCP("ProductionServer")

mcp.add_middleware(GovernanceMiddleware(
api_key=os.environ.get("WAXELL_API_KEY", ""),
api_url=os.environ.get("WAXELL_API_URL", ""),
policy_config={
"default_pii_scan": "strict",
"fail_open": True,
},
))

Full Example

Complete server connected to controlplane with policy scenarios
import os
from fastmcp import FastMCP
from waxell_observe.mcp import GovernanceMiddleware, ToolGovernanceConfig, governance

mcp = FastMCP("GovernedDocServer")

# Connect to controlplane with per-tool overrides
mcp.add_middleware(GovernanceMiddleware(
api_key=os.environ.get("WAXELL_API_KEY", ""),
api_url=os.environ.get("WAXELL_API_URL", ""),
policy_config={
"default_pii_scan": "standard",
"default_policy_action": "allow",
"fail_open": True,
},
tool_configs={
# Always block delete operations, even if controlplane allows
"delete_document": ToolGovernanceConfig(
policy_action="block",
pii_scan="strict",
),
},
))


@mcp.tool
async def read_document(doc_id: str) -> str:
"""Read a document by ID.

Controlplane policy check runs with pattern: mcp:read_document
Uses server defaults (standard PII scan, allow policy action).
"""
return f"Document {doc_id} content..."


@governance(approval_required=True, pii_scan="strict")
@mcp.tool
async def update_document(doc_id: str, content: str) -> dict:
"""Update a document's content.

@governance decorator overrides: strict PII scan, approval required.
Controlplane policy check runs with pattern: mcp:update_document
"""
return {"status": "updated", "doc_id": doc_id}


@mcp.tool
async def delete_document(doc_id: str) -> dict:
"""Delete a document permanently.

Constructor tool_configs override: always blocked.
Even if the controlplane returns action="allow", the local
policy_action="block" takes precedence.
"""
return {"status": "deleted", "doc_id": doc_id}


@governance(rate_limit={"max_per_minute": 5})
@mcp.tool
async def search_documents(query: str) -> list:
"""Search documents by query.

Rate limited to 5 calls per minute via @governance decorator.
Controlplane policy check runs with pattern: mcp:search_documents
"""
return [{"doc_id": "1", "title": "Result", "snippet": "..."}]


if __name__ == "__main__":
mcp.run()

Run the server:

export WAXELL_API_KEY="wax_sk_..."
export WAXELL_API_URL="https://acme.waxell.dev"
python server.py

Next Steps