|
151 | 151 | ListExecutionsResponse, |
152 | 152 | ListUsageRecordsOptions, |
153 | 153 | ListWebhooksResponse, |
| 154 | + MCPCheckInputResponse, |
| 155 | + MCPCheckOutputResponse, |
154 | 156 | MediaContent, |
155 | 157 | MediaGovernanceConfig, |
156 | 158 | MediaGovernanceStatus, |
@@ -1076,6 +1078,110 @@ async def mcp_execute( |
1076 | 1078 | """ |
1077 | 1079 | return await self.mcp_query(connector, statement, options) |
1078 | 1080 |
|
| 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 | + |
1079 | 1185 | async def generate_plan( |
1080 | 1186 | self, |
1081 | 1187 | query: str, |
@@ -5843,6 +5949,33 @@ def mcp_execute( |
5843 | 5949 | """Execute a statement against an MCP connector (alias for mcp_query).""" |
5844 | 5950 | return self._run_sync(self._async_client.mcp_execute(connector, statement, options)) |
5845 | 5951 |
|
| 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 | + |
5846 | 5979 | def generate_plan( |
5847 | 5980 | self, |
5848 | 5981 | query: str, |
|
0 commit comments