MCP Policy Enforcement
AxonFlow enforces policies at multiple levels during MCP connector operations:
- REQUEST Phase: Evaluates incoming queries before they reach the connector
- RESPONSE Phase: Scans connector responses before returning to the client
- Exfiltration Detection: Prevents large-scale data extraction via row/volume limits
- Dynamic Policies: Orchestrator-evaluated policies for rate limiting, budgets, and access control
This provides comprehensive security for both data input and output.
Phase-Aware Evaluation
Request Phase
The request phase evaluates queries before they are sent to the connector:
- SQL Injection Detection: Blocks malicious SQL patterns (DROP, DELETE without WHERE, UNION injection)
- Dangerous Operation Blocking: Prevents schema modifications, privilege escalation
- Access Control: Enforces tenant-specific permissions
If a policy blocks the request, the connector is never called and a 403 response is returned.
Response Phase
The response phase evaluates data after it's returned from the connector:
- PII Detection: Identifies sensitive data patterns (SSN, credit cards, national IDs)
- Data Redaction: Replaces sensitive values with redacted placeholders
- Audit Logging: Records what data was accessed and redacted
Response Schema
All MCP responses now include policy enforcement metadata:
{
"success": true,
"connector": "postgres-demo",
"data": [
{
"name": "John Doe",
"ssn": "[REDACTED]",
"email": "[email protected]"
}
],
"row_count": 1,
"duration_ms": 45,
"redacted": true,
"redacted_fields": ["data[0].ssn"],
"policy_info": {
"policies_evaluated": 15,
"blocked": false,
"redactions_applied": 1,
"processing_time_ms": 3,
"matched_policies": [
{
"policy_id": "pii-us-ssn",
"policy_name": "US Social Security Number",
"category": "pii-us",
"severity": "critical",
"action": "redact"
}
],
"exfiltration_check": {
"rows_returned": 1,
"row_limit": 10000,
"bytes_returned": 256,
"byte_limit": 10485760,
"within_limits": true
},
"dynamic_policy_info": {
"policies_evaluated": 2,
"matched_policies": [
{
"policy_id": "rate-limit-1",
"policy_name": "API Rate Limit",
"policy_type": "rate-limit",
"action": "allow"
}
],
"orchestrator_reachable": true,
"processing_time_ms": 3
}
}
}
New Response Fields
| Field | Type | Description |
|---|---|---|
redacted | boolean | Whether any fields were redacted |
redacted_fields | string[] | JSON paths of redacted fields |
policy_info | object | Policy evaluation details |
PolicyInfo Object
| Field | Type | Description |
|---|---|---|
policies_evaluated | integer | Number of policies checked |
blocked | boolean | Whether request was blocked |
block_reason | string | Reason if blocked |
redactions_applied | integer | Number of fields redacted |
processing_time_ms | integer | Policy evaluation time |
matched_policies | array | Policies that matched |
PolicyMatchInfo Object
Each entry in matched_policies contains:
| Field | Type | Description |
|---|---|---|
policy_id | string | Unique policy identifier |
policy_name | string | Human-readable policy name |
category | string | Policy category (e.g., pii-us, security-sqli) |
severity | string | Match severity (critical, high, medium, low) |
action | string | Action taken (block, redact, warn, log) |
ExfiltrationCheckInfo Object
Tracks row and volume limits to prevent data exfiltration:
| Field | Type | Description |
|---|---|---|
rows_returned | integer | Number of rows in the response |
row_limit | integer | Configured maximum rows per query |
bytes_returned | integer | Response data size in bytes |
byte_limit | integer | Configured maximum bytes per response |
within_limits | boolean | Whether response is within all limits |
DynamicPolicyInfo Object
Contains results of Orchestrator-evaluated dynamic policies:
| Field | Type | Description |
|---|---|---|
policies_evaluated | integer | Number of dynamic policies checked |
matched_policies | array | Dynamic policies that matched |
orchestrator_reachable | boolean | Whether Orchestrator was reachable |
processing_time_ms | integer | Dynamic policy evaluation time |
DynamicPolicyMatch Object
Each entry in dynamic matched_policies contains:
| Field | Type | Description |
|---|---|---|
policy_id | string | Unique policy identifier |
policy_name | string | Human-readable policy name |
policy_type | string | Policy type (rate-limit, budget, time-access, role-access) |
action | string | Action taken (allow, block, log) |
reason | string | Context for the policy match |
Policy Categories
Security Policies (Request Phase)
| Category | Description | Action |
|---|---|---|
security-sqli | SQL injection patterns | Block |
security-dangerous | DDL, privilege escalation | Block |
PII Policies (Response Phase)
| Category | Description | Action |
|---|---|---|
pii-us | US SSN, Driver's License | Redact |
pii-global | Credit cards, email, phone | Redact |
pii-india | Aadhaar, PAN | Redact |
pii-eu | National IDs, IBAN | Redact |
SDK Integration
Go SDK
resp, err := client.MCPQuery(ctx, axonflow.MCPQueryRequest{
Connector: "postgres-demo",
Statement: "SELECT name, ssn FROM customers",
})
if resp.Redacted {
fmt.Printf("Redacted fields: %v\n", resp.RedactedFields)
}
if resp.PolicyInfo != nil {
fmt.Printf("Evaluated %d policies in %dms\n",
resp.PolicyInfo.PoliciesEvaluated,
resp.PolicyInfo.ProcessingTimeMs)
}
TypeScript SDK
const resp = await client.mcpQuery({
connector: "postgres-demo",
statement: "SELECT name, ssn FROM customers",
});
if (resp.redacted) {
console.log("Redacted fields:", resp.redacted_fields);
}
if (resp.policy_info) {
console.log(`Evaluated ${resp.policy_info.policies_evaluated} policies`);
}
Python SDK
resp = await client.mcp_query(
connector="postgres-demo",
statement="SELECT name, ssn FROM customers"
)
if resp.redacted:
print(f"Redacted fields: {resp.redacted_fields}")
if resp.policy_info:
print(f"Evaluated {resp.policy_info.policies_evaluated} policies")
Java SDK
ConnectorResponse resp = client.mcpQuery(
"postgres-demo",
"SELECT name, ssn FROM customers"
);
if (resp.isRedacted()) {
System.out.println("Redacted fields: " + resp.getRedactedFields());
}
ConnectorPolicyInfo info = resp.getPolicyInfo();
if (info != null) {
System.out.println("Evaluated " + info.getPoliciesEvaluated() + " policies");
}
Handling Blocked Requests
When a request is blocked by policy, the SDK throws an exception or returns an error:
// Go
resp, err := client.MCPQuery(ctx, req)
if err != nil {
// Check if it's a policy violation
if policyErr, ok := err.(*axonflow.PolicyViolationError); ok {
fmt.Printf("Blocked by policy: %s\n", policyErr.Reason)
}
}
// TypeScript
try {
const resp = await client.mcpQuery(req);
} catch (err) {
if (err instanceof PolicyViolationError) {
console.log("Blocked by policy:", err.reason);
}
}
Exfiltration Detection
Exfiltration detection prevents large-scale data extraction via MCP queries by enforcing row count and data volume limits.
Configuration
| Variable | Default | Description |
|---|---|---|
MCP_MAX_ROWS_PER_QUERY | 10000 | Maximum rows per MCP query |
MCP_MAX_BYTES_PER_QUERY | 10485760 | Maximum bytes per response (10MB) |
Behavior
- Responses within limits include
exfiltration_checkwithwithin_limits: true - Responses exceeding limits return 403 with details about which limit was exceeded
- Limits are enforced after PII redaction (redacted data counts toward limits)
Example: Checking Exfiltration Limits
resp, err := client.MCPQuery(ctx, req)
if resp.PolicyInfo != nil && resp.PolicyInfo.ExfiltrationCheck != nil {
info := resp.PolicyInfo.ExfiltrationCheck
fmt.Printf("Rows: %d/%d, Bytes: %d/%d\n",
info.RowsReturned, info.RowLimit,
info.BytesReturned, info.ByteLimit)
if !info.WithinLimits {
fmt.Println("Warning: Response exceeded limits")
}
}
Dynamic Policy Evaluation
Dynamic policies are evaluated by the Orchestrator and support advanced use cases like rate limiting, budget controls, and access restrictions.
Configuration
| Variable | Default | Description |
|---|---|---|
MCP_DYNAMIC_POLICIES_ENABLED | false | Enable dynamic policy evaluation |
MCP_DYNAMIC_POLICIES_TIMEOUT | 5s | Timeout for Orchestrator call |
MCP_DYNAMIC_POLICIES_GRACEFUL | true | Continue if Orchestrator unavailable |
Supported Policy Types
| Type | Description | Example |
|---|---|---|
rate-limit | Limit requests per time window | Max 100 queries per minute |
budget | Track and limit API costs | $1000 monthly budget |
time-access | Restrict to business hours | Allow 9am-5pm only |
role-access | Role-based access control | Admin-only connectors |
Graceful Degradation
When MCP_DYNAMIC_POLICIES_GRACEFUL=true (default), MCP queries continue even if the Orchestrator is unavailable. The response will include orchestrator_reachable: false in the dynamic_policy_info.
Example: Checking Dynamic Policy Results
resp, err := client.MCPQuery(ctx, req)
if resp.PolicyInfo != nil && resp.PolicyInfo.DynamicPolicyInfo != nil {
info := resp.PolicyInfo.DynamicPolicyInfo
fmt.Printf("Evaluated %d dynamic policies\n", info.PoliciesEvaluated)
for _, match := range info.MatchedPolicies {
fmt.Printf(" - %s (%s): %s\n",
match.PolicyName, match.PolicyType, match.Action)
}
}
Performance
The policy engine is designed for minimal latency impact:
- Request Phase:
<5msp99 for policy evaluation - Response Phase:
<10msp99 for redaction - Caching: Compiled regex patterns cached for reuse
- Parallel Evaluation: Policies evaluated concurrently
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
POLICY_CACHE_TTL | 5m | Policy cache refresh interval |
POLICY_BLOCK_ON_ERROR | false | Block if policy engine unavailable |
PII_ACTION | redact | Default PII action (block/redact/warn/log) |
Database Configuration
Policies are stored in the static_policies table with phase-aware columns:
SELECT id, name, category, phase, action_request, action_response
FROM static_policies
WHERE enabled = true AND tenant_id = 'your-tenant';
Audit Logging
All MCP connector operations are logged for compliance and security analysis. The mcp_query_audits table captures:
- Request phase results: Whether the query was blocked, which policies matched
- Response phase results: Whether PII was redacted, which fields were affected
- Exfiltration checks: Row counts and volume limit violations
- Performance metrics: Operation duration, success/failure status
The audit_id field in PolicyInfo correlates with audit log entries for end-to-end traceability:
if resp.PolicyInfo != nil {
// Use audit_id to look up the full audit record
fmt.Printf("Audit ID: %s\n", resp.PolicyInfo.AuditID)
}
For detailed information about MCP audit logging, see Audit Logging.
Examples
See the MCP policy examples in the AxonFlow repository:
- MCP Policy Examples - Basic policy enforcement (SQLi blocking, PII redaction)
- Exfiltration Detection - Row and volume limit enforcement
- Dynamic Policies - Rate limiting, budgets, access control
All examples include implementations in Go, TypeScript, Python, and Java.
Related Documentation
- Audit Logging - Complete audit trail for MCP operations
- Governance Overview
- PII Detection
- SQL Injection Scanning
- Agent API Reference