← Back to Home

MCP as an AI Control Plane: Context Routing, Governance, and Policy

ai-infrastructuregovernancecontrol-plane
#model-context-protocol#ai-governance#context-routing#policy-enforcement#control-plane#organizational-memory#access-control#compliance#enterprise-ai

The Problem: Context Access Is Ungoverned and Invisible

Your organization has deployed LLMs across fifteen different teams. Sales uses Claude to draft proposals with access to CRM data. Engineering uses GPT-4 to review code with access to repositories. Finance uses internal models to generate reports with access to transaction databases. HR uses chatbots to answer policy questions with access to employee handbooks.

Six months later, you discover that a sales agent accidentally accessed engineering salary data. A finance bot quoted outdated compliance policies. An HR chatbot leaked PII in conversation logs. Nobody noticed until an audit revealed the problems because there was no central visibility into what context any agent was accessing.

This is the governance crisis in AI systems today. Every team builds their own context access patterns. Some use RAG with vector databases. Some use tool calling with custom APIs. Some use fine-tuned models with baked-in data. There's no consistent policy enforcement, no unified audit trail, and no way to answer basic questions like "which agents can access customer data?" or "what context did this agent use to generate that response?"

Traditional approaches fail because they treat context access as an implementation detail, not as infrastructure that requires governance. Teams build direct connections between agents and data sources. Authorization happens ad-hoc in application code. Audit logging is fragmented across systems. Policy changes require coordinating updates across dozens of independent deployments.

The fundamental issue: you can't govern what you can't see, and you can't see context access when it's distributed across every agent implementation. You need a control plane—a centralized layer that mediates all context access, enforces policy uniformly, and provides complete visibility into what context flows to which agents.

Model Context Protocol becomes this control plane when you architect it correctly. Not just as a protocol for fetching context, but as the governance layer for your entire AI infrastructure. Every context request routes through MCP. Every policy applies uniformly. Every access gets audited. This isn't about making MCP "more powerful"—it's about recognizing that any serious AI deployment needs centralized context governance, and MCP provides the right architectural pattern.

The Mental Model: Control Plane vs. Data Plane Separation

Stop thinking of MCP as "a way for agents to get context." Start thinking of it as the control plane for AI context access, sitting above the data plane of actual context sources.

In networking, the control plane makes routing decisions and enforces policy. The data plane moves packets. These are separate concerns with different requirements. Control plane operations are centralized, policy-driven, and audited. Data plane operations are distributed, performance-critical, and governed by control plane decisions.

AI systems need the same separation. The data plane is your databases, vector stores, APIs, and file systems—the actual sources of context. The control plane is the layer that decides which agents can access which context sources, enforces access policies, routes context requests efficiently, and maintains complete audit trails.

MCP as control plane provides three core capabilities:

Context routing: Agents don't connect directly to context sources. They declare context needs to the MCP control plane, which routes requests to appropriate sources based on routing rules, agent identity, and current policy. This is service mesh for AI context.

Policy enforcement: Every context request evaluates against centralized policy before reaching data sources. Access control, data classification, usage limits, compliance requirements—all enforced uniformly regardless of which agent makes the request or which team deployed it.

Organizational memory gateway: MCP mediates access to institutional knowledge. It's not just technical governance—it's ensuring the right context reaches the right agents based on organizational structure, security boundaries, and business logic.

The key invariant: no context reaches any agent without transiting the MCP control plane. This creates a single enforcement point and complete visibility.

Compare this to traditional architectures where each agent team builds direct database connections, custom vector store clients, and ad-hoc API integrations. Every connection is a governance bypass. Every custom implementation is a policy blind spot. MCP as control plane eliminates these bypasses by making context access infrastructure, not application code.

The routing insight: context routing is a policy decision, not a performance optimization.

When an agent requests "customer data," the MCP control plane doesn't just fetch it—it decides based on policy whether that agent should access that context, which version or subset to provide, how to log the access, and whether to apply redactions. These are governance decisions that must happen centrally, not distributed across agent implementations.

The enforcement insight: policy changes propagate instantly through control plane updates.

When compliance requires customer data redaction, you update MCP policy once. Every agent accessing customer data through MCP gets redacted context immediately, regardless of which team owns the agent or how they implemented context access. Without a control plane, you're coordinating updates across every team—a compliance nightmare.

Architecture: MCP Control Plane for Enterprise AI

The architecture separates control decisions from data movement.

Figure: Architecture: MCP Control Plane for Enterprise AI
Figure: Architecture: MCP Control Plane for Enterprise AI

Control Plane Components

Context Router receives all context requests from agents. It doesn't fetch context—it makes routing decisions based on agent identity, requested resources, and current policy. This is where governance happens.

Responsibilities: authenticate agents, classify context requests, select appropriate MCP servers, apply routing policies, enforce rate limits, trigger audits.

Policy Engine evaluates every context request against centralized policy. Policies define who can access what context under which conditions. This is your enforcement layer.

Responsibilities: evaluate access control rules, apply data classification policies, enforce compliance requirements, trigger redactions or filtering, approve or deny requests.

Audit Logger records every context access decision and outcome. This isn't optional—it's core to governance. You need complete trails for compliance, debugging, and security.

Responsibilities: log requests with agent identity and context requested, log policy decisions with reasons, log actual context accessed, enable audit queries and compliance reporting.

Governance Dashboard provides visibility into context access patterns across your organization. This is how you answer "who accessed what" questions and detect anomalies.

Responsibilities: visualize context access by team/agent/source, show policy violations and access denials, enable drill-down into specific access patterns, support compliance reporting.

Data Plane Components

MCP Servers are the data plane. They fetch context from actual sources and return it to the control plane. They don't make policy decisions—they're governed by control plane routing.

Responsibilities: implement MCP protocol, fetch context from data sources, handle source-specific errors and retries, provide resource discovery, cache frequently accessed context.

Data Sources are databases, APIs, file systems—the actual context. MCP servers abstract these behind uniform protocol.

Control Flow

  1. Agent requests context through MCP client
  2. Request hits MCP control plane (context router)
  3. Router authenticates agent and extracts identity
  4. Policy engine evaluates request against policy
  5. If approved, router selects appropriate MCP server
  6. Audit logger records decision
  7. MCP server fetches from data source
  8. Response flows back through control plane
  9. Control plane applies post-access policy (redactions, etc.)
  10. Audit logger records actual context delivered
  11. Agent receives governed context

Every step is policy-enforced and audited. No agent can bypass this flow to access context directly.

Implementation: Building the MCP Control Plane

Layer 1: Context Router with Agent Authentication

The control plane starts with authenticated routing.

code
from typing import Dict, List, Any, Optionalfrom dataclasses import dataclassfrom datetime import datetimeimport jwt@dataclassclass AgentIdentity:    """    Authenticated agent making context request.    This drives all policy decisions.    """    agent_id: str    team: str    environment: str  # production, staging, development    roles: List[str]    scopes: List[str]    authenticated_at: datetime@dataclassclass ContextRequest:    """    Request for context from an agent.    This is what hits the control plane.    """    agent_identity: AgentIdentity    resource_uris: List[str]    purpose: str  # Why context is needed    metadata: Dict[str, Any]class MCPContextRouter:    """    Control plane component that routes context requests.    This is the gateway—everything flows through here.    """        def __init__(        self,        policy_engine,        audit_logger,        mcp_servers: Dict[str, Any]    ):        self.policy = policy_engine        self.audit = audit_logger        self.servers = mcp_servers        async def route_request(        self,        request: ContextRequest    ) -> Dict[str, Any]:        """        Route context request through governance layers.        This is the core control plane operation.        """        # Step 1: Log incoming request        request_id = self.audit.log_request(request)                try:            # Step 2: Evaluate policy for each resource            policy_decisions = await self._evaluate_policies(request)                        # Step 3: Filter approved resources            approved_uris = [                uri for uri, decision in policy_decisions.items()                if decision["approved"]            ]                        denied_uris = [                uri for uri, decision in policy_decisions.items()                if not decision["approved"]            ]                        # Step 4: Log denials            if denied_uris:                self.audit.log_access_denied(                    request_id=request_id,                    agent=request.agent_identity,                    denied_resources=denied_uris,                    reasons=[policy_decisions[uri]["reason"] for uri in denied_uris]                )                        # Step 5: Route approved requests to appropriate servers            context_results = await self._fetch_from_servers(                approved_uris,                request.agent_identity            )                        # Step 6: Apply post-access policy (redactions, filtering)            governed_results = await self._apply_post_access_policy(                context_results,                request.agent_identity            )                        # Step 7: Log successful access            self.audit.log_access_granted(                request_id=request_id,                agent=request.agent_identity,                accessed_resources=list(governed_results.keys())            )                        return {                "request_id": request_id,                "context": governed_results,                "denied": denied_uris,                "governance_applied": True            }                    except Exception as e:            # Log failure            self.audit.log_error(request_id, str(e))            raise        async def _evaluate_policies(        self,        request: ContextRequest    ) -> Dict[str, Dict[str, Any]]:        """        Evaluate policy for each requested resource.        Returns per-resource approval decisions.        """        decisions = {}                for uri in request.resource_uris:            decision = await self.policy.evaluate(                agent=request.agent_identity,                resource=uri,                purpose=request.purpose            )                        decisions[uri] = decision                return decisions        async def _fetch_from_servers(        self,        uris: List[str],        agent: AgentIdentity    ) -> Dict[str, Any]:        """        Route approved requests to appropriate MCP servers.        This is data plane interaction governed by control plane decisions.        """        import asyncio                # Group URIs by server        server_requests = {}        for uri in uris:            server_name = self._route_to_server(uri)            if server_name not in server_requests:                server_requests[server_name] = []            server_requests[server_name].append(uri)                # Fetch from each server in parallel        tasks = []        for server_name, server_uris in server_requests.items():            server = self.servers[server_name]            task = server.fetch_resources(server_uris, agent)            tasks.append(task)                results = await asyncio.gather(*tasks, return_exceptions=True)                # Merge results        merged = {}        for result in results:            if isinstance(result, Exception):                # Log server failure but don't block other results                self.audit.log_server_failure(str(result))                continue            merged.update(result)                return merged        def _route_to_server(self, uri: str) -> str:        """        Determine which MCP server handles this URI.        This is routing logic—keep it simple and explicit.        """        if uri.startswith("crm://"):            return "crm_server"        elif uri.startswith("code://"):            return "code_server"        elif uri.startswith("finance://"):            return "finance_server"        elif uri.startswith("hr://"):            return "hr_server"        else:            raise ValueError(f"Unknown resource scheme: {uri}")

Production considerations:

Every request gets a unique ID. This enables end-to-end tracing through the control plane. When debugging access issues, you need to correlate requests across all components.

Policy evaluation happens before data plane access. Never fetch context and then check policy. That leaks data. Check first, fetch second.

Server failures are isolated. If one MCP server is down, others continue working. Agents get partial context rather than complete failure.

Layer 2: Policy Engine with Declarative Rules

Policy must be declarative and centralized.

code
from typing import Dict, Any, Callablefrom enum import Enumclass PolicyDecision(Enum):    ALLOW = "allow"    DENY = "deny"    REDACT = "redact"@dataclassclass PolicyRule:    """    Declarative policy rule.    These are your governance rules.    """    name: str    description: str    condition: Callable[[AgentIdentity, str], bool]    action: PolicyDecision    reason: str    priority: int = 0class PolicyEngine:    """    Centralized policy enforcement.    This is where governance rules live.    """        def __init__(self):        self.rules: List[PolicyRule] = []        self._load_default_policies()        def add_rule(self, rule: PolicyRule):        """Add policy rule. Higher priority rules evaluated first."""        self.rules.append(rule)        self.rules.sort(key=lambda r: r.priority, reverse=True)        async def evaluate(        self,        agent: AgentIdentity,        resource: str,        purpose: str    ) -> Dict[str, Any]:        """        Evaluate all policies against request.        Returns first matching rule's decision.        """        for rule in self.rules:            if rule.condition(agent, resource):                return {                    "approved": rule.action == PolicyDecision.ALLOW,                    "action": rule.action.value,                    "reason": rule.reason,                    "rule": rule.name                }                # Default deny if no rules match        return {            "approved": False,            "action": "deny",            "reason": "No matching policy rule",            "rule": "default_deny"        }        def _load_default_policies(self):        """        Load standard governance policies.        Customize these for your organization.        """        # Production agents can't access HR data        self.add_rule(PolicyRule(            name="prod_no_hr",            description="Production agents cannot access HR systems",            condition=lambda agent, resource: (                agent.environment == "production" and                 resource.startswith("hr://")            ),            action=PolicyDecision.DENY,            reason="HR data not available in production",            priority=100        ))                # Only finance team can access financial data        self.add_rule(PolicyRule(            name="finance_team_only",            description="Financial data restricted to finance team",            condition=lambda agent, resource: (                resource.startswith("finance://") and                 agent.team != "finance"            ),            action=PolicyDecision.DENY,            reason="Financial data restricted to finance team",            priority=90        ))                # Development environments have broader access        self.add_rule(PolicyRule(            name="dev_allow_all",            description="Development environment has broad access",            condition=lambda agent, resource: (                agent.environment == "development"            ),            action=PolicyDecision.ALLOW,            reason="Development environment",            priority=10        ))                # PII requires explicit scope        self.add_rule(PolicyRule(            name="pii_requires_scope",            description="PII access requires pii_read scope",            condition=lambda agent, resource: (                "pii" in resource.lower() and                 "pii_read" not in agent.scopes            ),            action=PolicyDecision.DENY,            reason="Missing pii_read scope",            priority=95        ))class RedactionPolicy:    """    Post-access policy for redacting sensitive fields.    """        @staticmethod    def redact_pii(        context: Dict[str, Any],        agent: AgentIdentity    ) -> Dict[str, Any]:        """        Redact PII fields unless agent has proper scope.        This is governance at the data level.        """        if "pii_read" in agent.scopes:            return context                # Define PII fields to redact        pii_fields = ["ssn", "email", "phone", "address", "dob"]                redacted = context.copy()        for field in pii_fields:            if field in redacted:                redacted[field] = "***REDACTED***"                return redacted

Production considerations:

Policies are prioritized. High-priority denial rules (production can't access HR) evaluate before permissive rules (dev can access everything). This prevents accidental bypasses.

Rules are declarative functions. Easy to test, easy to audit, easy to explain to compliance teams. Avoid complex imperative logic.

Default deny is critical. If no rule explicitly allows access, deny. Never default to permissive.

Redaction happens post-access. Even if access is granted, sensitive fields can be redacted based on agent scopes. This is defense in depth.

Layer 3: Audit Logger for Compliance

Complete audit trails are non-negotiable.

code
from datetime import datetimeimport jsonclass AuditLogger:    """    Comprehensive audit logging for compliance.    Every context access must be traceable.    """        def __init__(self, storage_backend):        self.storage = storage_backend        def log_request(self, request: ContextRequest) -> str:        """        Log incoming context request.        Returns request ID for correlation.        """        request_id = self._generate_id()                self.storage.write({            "event_type": "context_request",            "request_id": request_id,            "timestamp": datetime.utcnow().isoformat(),            "agent_id": request.agent_identity.agent_id,            "team": request.agent_identity.team,            "environment": request.agent_identity.environment,            "requested_resources": request.resource_uris,            "purpose": request.purpose        })                return request_id        def log_access_denied(        self,        request_id: str,        agent: AgentIdentity,        denied_resources: List[str],        reasons: List[str]    ):        """        Log denied access attempts.        Critical for security monitoring.        """        self.storage.write({            "event_type": "access_denied",            "request_id": request_id,            "timestamp": datetime.utcnow().isoformat(),            "agent_id": agent.agent_id,            "team": agent.team,            "denied_resources": denied_resources,            "denial_reasons": reasons,            "severity": "warning"        })        def log_access_granted(        self,        request_id: str,        agent: AgentIdentity,        accessed_resources: List[str]    ):        """        Log successful context access.        Needed for usage tracking and compliance.        """        self.storage.write({            "event_type": "access_granted",            "request_id": request_id,            "timestamp": datetime.utcnow().isoformat(),            "agent_id": agent.agent_id,            "team": agent.team,            "accessed_resources": accessed_resources,            "resource_count": len(accessed_resources)        })        def query_access_patterns(        self,        agent_id: Optional[str] = None,        resource_pattern: Optional[str] = None,        start_time: Optional[datetime] = None    ) -> List[Dict]:        """        Query audit logs for compliance reporting.        """        filters = {}        if agent_id:            filters["agent_id"] = agent_id        if resource_pattern:            filters["resource_pattern"] = resource_pattern        if start_time:            filters["start_time"] = start_time                return self.storage.query(filters)

Production considerations:

Every event gets a timestamp and request ID. This enables temporal queries and request correlation.

Denied access is logged with severity. Security teams can alert on unusual denial patterns indicating potential attacks or misconfigurations.

Logs are structured JSON. This enables programmatic querying for compliance reports, security analysis, and debugging.

Storage backend is abstracted. Use whatever your organization requires: S3, database, SIEM system.

Pitfalls & Failure Modes

Bypassing the Control Plane

Teams build "shortcuts" that access context sources directly, bypassing MCP governance.

Symptom: Audit logs show only some context access. Teams report accessing data that isn't logged. Policy violations occur without being caught.

Why it happens: Performance optimization. Teams see control plane as overhead and route around it for "critical" use cases.

Detection: Compare actual data source access logs with MCP audit logs. Gaps indicate bypasses.

Prevention: Make direct access impossible. Network isolation, firewall rules, access controls—enforce that context sources only accept connections from MCP servers.

Policy Sprawl Without Versioning

Teams add policies organically, creating hundreds of rules with unclear precedence and conflicting logic.

Symptom: Policy evaluation becomes slow. Unexpected denials occur. Nobody can explain why a request was denied.

Why it happens: No policy management discipline. Each compliance requirement adds rules without reviewing existing ones.

Detection: Monitor policy evaluation latency. Count total rules. If >50 rules or evaluation takes >100ms, you have sprawl.

Prevention: Policy versioning and review. Treat policies as code with version control, testing, and regular audits. Consolidate overlapping rules.

Insufficient Audit Retention

Audit logs are deleted too aggressively, eliminating compliance evidence.

Symptom: Can't answer "who accessed this data three months ago?" Compliance audits fail due to missing logs.

Why it happens: Cost optimization. Teams delete old logs to save storage costs.

Detection: Compliance audit failures. Inability to investigate historical access patterns.

Prevention: Regulatory compliance defines retention. Financial services: 7 years minimum. Healthcare: check HIPAA. Set retention based on requirements, not cost.

Treating MCP as Optional

Teams position MCP as "preferred" but allow direct context access for "legacy" systems.

Symptom: Partial governance. Some agents governed, others not. Incomplete audit trails. Policy violations in ungoverned agents.

Why it happens: Gradual migration strategy. Teams don't want to block existing agents while rolling out MCP.

Detection: Usage metrics showing both MCP and non-MCP context access.

Prevention: Set hard deadline for migration. After deadline, ungoverned access is disabled. No exceptions. Partial governance is no governance.

Policy as Code Without Testing

Teams write complex policy rules without systematic testing, leading to accidental denials or bypasses.

Symptom: Production incidents from policy bugs. Legitimate access denied. Unauthorized access allowed.

Why it happens: Policy treated as configuration, not code. No test suite, no CI validation.

Detection: High policy-related incident rate. Frequent emergency policy rollbacks.

Prevention: Policy testing framework. Unit tests for each rule. Integration tests for rule interactions. CI/CD for policy deployment.

Summary & Next Steps

MCP as control plane transforms context access from unmanaged implementation details into governed infrastructure. Every context request routes through centralized policy enforcement and audit logging. This isn't about adding features to MCP—it's recognizing that production AI systems require the same control plane patterns that production infrastructure has always needed.

The key insights: separation of control plane (routing, policy, audit) from data plane (actual context fetching). Declarative policy that's testable and versioned. Complete audit trails for compliance and debugging. No bypasses—every context access transits the control plane.

Start building your MCP control plane:

This week: Inventory all context access in your organization. Map which agents access which data sources. Identify governance gaps where access is unaudited or policy-free.

Next sprint: Implement basic MCP context router with authentication and audit logging. Route one high-value use case through it. Measure before/after governance coverage.

Within month: Build policy engine with rules for your top compliance requirements. Add redaction policies for PII. Create governance dashboard showing access patterns and policy violations.

Test with compliance scenarios. Simulate access requests that should be denied. Verify denials are logged. Attempt to bypass control plane. Ensure bypasses fail. Run compliance report showing complete audit trail for specific resources.

The goal is organizational readiness for AI governance, not just technical implementation. MCP control plane gives you the infrastructure. You still need organizational processes: policy review cadence, incident response for violations, compliance reporting workflows.

But without the control plane, those processes have no enforcement mechanism. Build the infrastructure first. The processes become possible.