Skip to content

Commit cd4626a

Browse files
feat: add mcp_check_input and mcp_check_output methods (#103)
* feat: add mcp_check_input and mcp_check_output methods Add standalone policy-check methods for external orchestrators to use AxonFlow as a policy gate. Includes async methods, sync wrappers, and Pydantic request/response types. 403 responses are treated as valid policy-blocked results, not errors. Refs: getaxonflow/axonflow-enterprise#1258 * fix: apply ruff formatting to sync wrappers * docs: add MCP policy-check endpoints to changelog * docs: set v3.7.0 release date in changelog * docs: improve v3.7.0 changelog entry
1 parent 60b2150 commit cd4626a

4 files changed

Lines changed: 194 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to the AxonFlow Python SDK will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.7.0] - 2026-02-28
9+
10+
### Added
11+
12+
- **MCP Policy-Check Endpoints** (Platform v4.6.0+): Standalone policy validation for external orchestrators (LangGraph, CrewAI) to enforce AxonFlow policies without executing connector queries
13+
- `mcp_check_input(connector_type, statement)`: Validate SQL/commands against input policies (SQLi detection, dangerous query blocking, PII in queries, dynamic policies). Returns `allowed=True` or raises with `block_reason`
14+
- `mcp_check_output(connector_type, response_data)`: Validate MCP response data against output policies (PII redaction, exfiltration limits, dynamic policies). Returns original or redacted data with `policy_info`
15+
- New types: `MCPCheckInputRequest`, `MCPCheckInputResponse`, `MCPCheckOutputRequest`, `MCPCheckOutputResponse`
16+
- Async methods with sync wrappers (`mcp_check_input_sync`, `mcp_check_output_sync`)
17+
- Supports both query-style (`response_data`) and execute-style (`message` + `metadata`) output validation
18+
19+
---
20+
821
## [3.6.0] - 2026-02-22
922

1023
### Added

axonflow/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@
152152
ListExecutionsResponse,
153153
ListUsageRecordsOptions,
154154
ListWebhooksResponse,
155+
MCPCheckInputRequest,
156+
MCPCheckInputResponse,
157+
MCPCheckOutputRequest,
158+
MCPCheckOutputResponse,
155159
MediaAnalysisResponse,
156160
MediaAnalysisResult,
157161
MediaContent,
@@ -240,6 +244,11 @@
240244
"ConnectorMetadata",
241245
"ConnectorInstallRequest",
242246
"ConnectorResponse",
247+
# MCP Policy Check types
248+
"MCPCheckInputRequest",
249+
"MCPCheckInputResponse",
250+
"MCPCheckOutputRequest",
251+
"MCPCheckOutputResponse",
243252
# Planning types
244253
"PlanStep",
245254
"PlanResponse",

axonflow/client.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@
151151
ListExecutionsResponse,
152152
ListUsageRecordsOptions,
153153
ListWebhooksResponse,
154+
MCPCheckInputResponse,
155+
MCPCheckOutputResponse,
154156
MediaContent,
155157
MediaGovernanceConfig,
156158
MediaGovernanceStatus,
@@ -1076,6 +1078,110 @@ async def mcp_execute(
10761078
"""
10771079
return await self.mcp_query(connector, statement, options)
10781080

1081+
async def mcp_check_input(
1082+
self,
1083+
connector_type: str,
1084+
statement: str,
1085+
operation: str = "query",
1086+
parameters: dict[str, Any] | None = None,
1087+
) -> MCPCheckInputResponse:
1088+
"""Validate an MCP request against configured policies without executing it.
1089+
1090+
Use this when an external orchestrator (e.g., LangGraph, CrewAI) manages MCP
1091+
execution but needs AxonFlow policy enforcement as a pre-execution gate.
1092+
1093+
Args:
1094+
connector_type: Type of MCP connector (e.g., "postgres", "snowflake").
1095+
statement: The SQL query or command to validate.
1096+
operation: Operation type - "query" (default) or "execute".
1097+
parameters: Optional query parameters.
1098+
1099+
Returns:
1100+
MCPCheckInputResponse with allowed status, block reason, and policy info.
1101+
1102+
Raises:
1103+
ConnectorError: If the request fails (non-403 errors only).
1104+
"""
1105+
url = f"{self._config.endpoint}/api/v1/mcp/check-input"
1106+
body: dict[str, Any] = {
1107+
"connector_type": connector_type,
1108+
"statement": statement,
1109+
"operation": operation,
1110+
}
1111+
if parameters:
1112+
body["parameters"] = parameters
1113+
1114+
if self._config.debug:
1115+
self._logger.debug(
1116+
"MCP check-input",
1117+
connector_type=connector_type,
1118+
statement=statement[:50],
1119+
)
1120+
1121+
response = await self._http_client.post(url, json=body)
1122+
data = response.json()
1123+
1124+
if not response.is_success and response.status_code != 403: # noqa: PLR2004
1125+
error_msg = data.get("error", "MCP check-input failed")
1126+
raise ConnectorError(error_msg, connector_type, "check-input")
1127+
1128+
return MCPCheckInputResponse(**data)
1129+
1130+
async def mcp_check_output(
1131+
self,
1132+
connector_type: str,
1133+
response_data: list[dict[str, Any]] | None = None,
1134+
message: str | None = None,
1135+
metadata: dict[str, Any] | None = None,
1136+
row_count: int = 0,
1137+
) -> MCPCheckOutputResponse:
1138+
"""Validate MCP response data against configured policies.
1139+
1140+
Use this when an external orchestrator manages MCP execution but needs AxonFlow
1141+
policy enforcement as a post-execution gate (PII redaction, exfiltration limits).
1142+
1143+
Args:
1144+
connector_type: Type of MCP connector (e.g., "postgres", "snowflake").
1145+
response_data: Array of row objects from a query response.
1146+
message: Execute-style response message (e.g., "5 rows affected").
1147+
metadata: Connector metadata for SQLi scanning.
1148+
row_count: Total number of rows returned.
1149+
1150+
Returns:
1151+
MCPCheckOutputResponse with allowed status, redacted data, and policy info.
1152+
1153+
Raises:
1154+
ConnectorError: If the request fails (non-403 errors only).
1155+
"""
1156+
url = f"{self._config.endpoint}/api/v1/mcp/check-output"
1157+
body: dict[str, Any] = {
1158+
"connector_type": connector_type,
1159+
}
1160+
if response_data is not None:
1161+
body["response_data"] = response_data
1162+
if message is not None:
1163+
body["message"] = message
1164+
if metadata is not None:
1165+
body["metadata"] = metadata
1166+
if row_count > 0:
1167+
body["row_count"] = row_count
1168+
1169+
if self._config.debug:
1170+
self._logger.debug(
1171+
"MCP check-output",
1172+
connector_type=connector_type,
1173+
row_count=row_count,
1174+
)
1175+
1176+
response = await self._http_client.post(url, json=body)
1177+
data = response.json()
1178+
1179+
if not response.is_success and response.status_code != 403: # noqa: PLR2004
1180+
error_msg = data.get("error", "MCP check-output failed")
1181+
raise ConnectorError(error_msg, connector_type, "check-output")
1182+
1183+
return MCPCheckOutputResponse(**data)
1184+
10791185
async def generate_plan(
10801186
self,
10811187
query: str,
@@ -5843,6 +5949,33 @@ def mcp_execute(
58435949
"""Execute a statement against an MCP connector (alias for mcp_query)."""
58445950
return self._run_sync(self._async_client.mcp_execute(connector, statement, options))
58455951

5952+
def mcp_check_input(
5953+
self,
5954+
connector_type: str,
5955+
statement: str,
5956+
operation: str = "query",
5957+
parameters: dict[str, Any] | None = None,
5958+
) -> MCPCheckInputResponse:
5959+
"""Validate an MCP request against configured policies without executing it."""
5960+
return self._run_sync(
5961+
self._async_client.mcp_check_input(connector_type, statement, operation, parameters)
5962+
)
5963+
5964+
def mcp_check_output(
5965+
self,
5966+
connector_type: str,
5967+
response_data: list[dict[str, Any]] | None = None,
5968+
message: str | None = None,
5969+
metadata: dict[str, Any] | None = None,
5970+
row_count: int = 0,
5971+
) -> MCPCheckOutputResponse:
5972+
"""Validate MCP response data against configured policies."""
5973+
return self._run_sync(
5974+
self._async_client.mcp_check_output(
5975+
connector_type, response_data, message, metadata, row_count
5976+
)
5977+
)
5978+
58465979
def generate_plan(
58475980
self,
58485981
query: str,

axonflow/types.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,45 @@ def was_redacted(self) -> bool:
358358
return self.redacted
359359

360360

361+
class MCPCheckInputRequest(BaseModel):
362+
"""Request to validate input against MCP policies."""
363+
364+
connector_type: str
365+
statement: str
366+
parameters: dict[str, Any] | None = Field(default=None)
367+
operation: str = Field(default="query")
368+
369+
370+
class MCPCheckInputResponse(BaseModel):
371+
"""Result of input policy evaluation."""
372+
373+
allowed: bool
374+
block_reason: str | None = Field(default=None)
375+
policies_evaluated: int = Field(default=0, ge=0)
376+
policy_info: ConnectorPolicyInfo | None = Field(default=None)
377+
378+
379+
class MCPCheckOutputRequest(BaseModel):
380+
"""Request to validate output against MCP policies."""
381+
382+
connector_type: str
383+
response_data: list[dict[str, Any]] | None = Field(default=None)
384+
message: str | None = Field(default=None)
385+
metadata: dict[str, Any] | None = Field(default=None)
386+
row_count: int = Field(default=0, ge=0)
387+
388+
389+
class MCPCheckOutputResponse(BaseModel):
390+
"""Result of output policy evaluation."""
391+
392+
allowed: bool
393+
block_reason: str | None = Field(default=None)
394+
redacted_data: Any | None = Field(default=None)
395+
policies_evaluated: int = Field(default=0, ge=0)
396+
exfiltration_info: ExfiltrationCheckInfo | None = Field(default=None)
397+
policy_info: ConnectorPolicyInfo | None = Field(default=None)
398+
399+
361400
class PlanStep(BaseModel):
362401
"""A step in a multi-agent plan."""
363402

0 commit comments

Comments
 (0)