Skip to content

Commit 641638d

Browse files
feat: add mcpCheckInput and mcpCheckOutput methods (#101)
* feat: add mcpCheckInput and mcpCheckOutput methods Add standalone policy-check methods and request/response POJOs for external orchestrators to use AxonFlow as a policy gate. Includes sync and async (CompletableFuture) variants. 403 responses are treated as valid policy-blocked results, not errors. Refs: getaxonflow/axonflow-enterprise#1258 * test: add tests for MCP check-input/check-output methods Cover mcpCheckInput and mcpCheckOutput SDK methods and their associated request/response types to restore JaCoCo coverage above the 73% threshold. Tests added: - mcpCheckInput: basic call, with options, 403 blocked result, 500 error, null validation, async variant - mcpCheckOutput: basic call, with options, 403 blocked result, exfiltration info, 500 error, null validation, async variant - MCPCheckInputRequest: construction, serialization, equals/hashCode - MCPCheckInputResponse: construction, deserialization, equals/hashCode - MCPCheckOutputRequest: construction, serialization, equals/hashCode - MCPCheckOutputResponse: construction, deserialization, equals/hashCode * docs: add MCP policy-check endpoints to changelog * fix: allow null responseData in mcpCheckOutput for execute-style requests The API contract allows check-output requests with message only (no response_data) for execute-style validation. Removed the Objects.requireNonNull check on responseData and updated the test to verify execute-style requests with null responseData work correctly. * fix: add missing HashMap import in AxonFlowTest * docs: set v3.7.0 release date in changelog * docs: improve v3.7.0 changelog entry
1 parent ca7a268 commit 641638d

8 files changed

Lines changed: 1346 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 Java 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+
- `mcpCheckInput(connectorType, statement)`: Validate SQL/commands against input policies (SQLi detection, dangerous query blocking, PII in queries, dynamic policies). Returns `MCPCheckInputResponse` with `isAllowed()` or `getBlockReason()`
14+
- `mcpCheckOutput(connectorType, responseData)`: Validate MCP response data against output policies (PII redaction, exfiltration limits, dynamic policies). Returns original or redacted data with `PolicyInfo`
15+
- New types: `MCPCheckInputRequest`, `MCPCheckInputResponse`, `MCPCheckOutputRequest`, `MCPCheckOutputResponse`
16+
- Sync + async variants with overloads for additional options (`parameters`, `operation`)
17+
- Supports both query-style (`responseData`) and execute-style (`message` + `metadata`) output validation
18+
19+
---
20+
821
## [3.6.0] - 2026-02-22
922

1023
### Added

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,210 @@ public ConnectorResponse mcpExecute(String connector, String statement) {
14051405
return mcpQuery(connector, statement);
14061406
}
14071407

1408+
// ========================================================================
1409+
// MCP Policy Check (Standalone)
1410+
// ========================================================================
1411+
1412+
/**
1413+
* Validates an MCP input statement against configured policies without executing it.
1414+
*
1415+
* <p>This method calls the agent's {@code /api/v1/mcp/check-input} endpoint to pre-validate
1416+
* a statement before sending it to the connector. Useful for checking SQL injection
1417+
* patterns, blocked operations, and input policy violations.</p>
1418+
*
1419+
* <p>Example usage:
1420+
* <pre>{@code
1421+
* MCPCheckInputResponse result = axonflow.mcpCheckInput("postgres", "SELECT * FROM users");
1422+
* if (!result.isAllowed()) {
1423+
* System.out.println("Blocked: " + result.getBlockReason());
1424+
* }
1425+
* }</pre>
1426+
*
1427+
* @param connectorType name of the MCP connector type (e.g., "postgres")
1428+
* @param statement the statement to validate
1429+
* @return MCPCheckInputResponse with allowed status, block reason, and policy info
1430+
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
1431+
*/
1432+
public MCPCheckInputResponse mcpCheckInput(String connectorType, String statement) {
1433+
return mcpCheckInput(connectorType, statement, null);
1434+
}
1435+
1436+
/**
1437+
* Validates an MCP input statement against configured policies with options.
1438+
*
1439+
* @param connectorType name of the MCP connector type (e.g., "postgres")
1440+
* @param statement the statement to validate
1441+
* @param options optional parameters: "operation" (String), "parameters" (Map)
1442+
* @return MCPCheckInputResponse with allowed status, block reason, and policy info
1443+
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
1444+
*/
1445+
public MCPCheckInputResponse mcpCheckInput(String connectorType, String statement, Map<String, Object> options) {
1446+
Objects.requireNonNull(connectorType, "connectorType cannot be null");
1447+
Objects.requireNonNull(statement, "statement cannot be null");
1448+
1449+
return retryExecutor.execute(() -> {
1450+
MCPCheckInputRequest request;
1451+
if (options != null) {
1452+
String operation = (String) options.getOrDefault("operation", "query");
1453+
@SuppressWarnings("unchecked")
1454+
Map<String, Object> parameters = (Map<String, Object>) options.get("parameters");
1455+
request = new MCPCheckInputRequest(connectorType, statement, parameters, operation);
1456+
} else {
1457+
request = new MCPCheckInputRequest(connectorType, statement);
1458+
}
1459+
1460+
Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-input", request);
1461+
try (Response response = httpClient.newCall(httpRequest).execute()) {
1462+
ResponseBody responseBody = response.body();
1463+
if (responseBody == null) {
1464+
throw new ConnectorException("Empty response from MCP check-input", connectorType, "mcpCheckInput");
1465+
}
1466+
String responseJson = responseBody.string();
1467+
1468+
// 403 means policy blocked — the body is still a valid response
1469+
if (!response.isSuccessful() && response.code() != 403) {
1470+
try {
1471+
Map<String, Object> errorData = objectMapper.readValue(responseJson,
1472+
new TypeReference<Map<String, Object>>() {});
1473+
String errorMsg = errorData.get("error") != null ?
1474+
errorData.get("error").toString() :
1475+
"MCP check-input failed: " + response.code();
1476+
throw new ConnectorException(errorMsg, connectorType, "mcpCheckInput");
1477+
} catch (JsonProcessingException e) {
1478+
throw new ConnectorException("MCP check-input failed: " + response.code(), connectorType, "mcpCheckInput");
1479+
}
1480+
}
1481+
1482+
return objectMapper.readValue(responseJson, MCPCheckInputResponse.class);
1483+
}
1484+
}, "mcpCheckInput");
1485+
}
1486+
1487+
/**
1488+
* Asynchronously validates an MCP input statement against configured policies.
1489+
*
1490+
* @param connectorType name of the MCP connector type
1491+
* @param statement the statement to validate
1492+
* @return a future containing the check result
1493+
*/
1494+
public CompletableFuture<MCPCheckInputResponse> mcpCheckInputAsync(String connectorType, String statement) {
1495+
return CompletableFuture.supplyAsync(() -> mcpCheckInput(connectorType, statement), asyncExecutor);
1496+
}
1497+
1498+
/**
1499+
* Asynchronously validates an MCP input statement against configured policies with options.
1500+
*
1501+
* @param connectorType name of the MCP connector type
1502+
* @param statement the statement to validate
1503+
* @param options optional parameters
1504+
* @return a future containing the check result
1505+
*/
1506+
public CompletableFuture<MCPCheckInputResponse> mcpCheckInputAsync(String connectorType, String statement, Map<String, Object> options) {
1507+
return CompletableFuture.supplyAsync(() -> mcpCheckInput(connectorType, statement, options), asyncExecutor);
1508+
}
1509+
1510+
/**
1511+
* Validates MCP response data against configured policies.
1512+
*
1513+
* <p>This method calls the agent's {@code /api/v1/mcp/check-output} endpoint to check
1514+
* response data for PII content, exfiltration limit violations, and other output
1515+
* policy violations. If PII redaction is active, {@code redactedData} contains the
1516+
* sanitized version.</p>
1517+
*
1518+
* <p>Example usage:
1519+
* <pre>{@code
1520+
* List<Map<String, Object>> rows = List.of(
1521+
* Map.of("name", "John", "ssn", "123-45-6789")
1522+
* );
1523+
* MCPCheckOutputResponse result = axonflow.mcpCheckOutput("postgres", rows);
1524+
* if (!result.isAllowed()) {
1525+
* System.out.println("Blocked: " + result.getBlockReason());
1526+
* }
1527+
* if (result.getRedactedData() != null) {
1528+
* System.out.println("Redacted: " + result.getRedactedData());
1529+
* }
1530+
* }</pre>
1531+
*
1532+
* @param connectorType name of the MCP connector type (e.g., "postgres")
1533+
* @param responseData the response data rows to validate
1534+
* @return MCPCheckOutputResponse with allowed status, redacted data, and policy info
1535+
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
1536+
*/
1537+
public MCPCheckOutputResponse mcpCheckOutput(String connectorType, List<Map<String, Object>> responseData) {
1538+
return mcpCheckOutput(connectorType, responseData, null);
1539+
}
1540+
1541+
/**
1542+
* Validates MCP response data against configured policies with options.
1543+
*
1544+
* @param connectorType name of the MCP connector type (e.g., "postgres")
1545+
* @param responseData the response data rows to validate
1546+
* @param options optional parameters: "message" (String), "metadata" (Map), "row_count" (int)
1547+
* @return MCPCheckOutputResponse with allowed status, redacted data, and policy info
1548+
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
1549+
*/
1550+
public MCPCheckOutputResponse mcpCheckOutput(String connectorType, List<Map<String, Object>> responseData, Map<String, Object> options) {
1551+
Objects.requireNonNull(connectorType, "connectorType cannot be null");
1552+
// responseData can be null for execute-style requests that use message instead
1553+
1554+
return retryExecutor.execute(() -> {
1555+
String message = options != null ? (String) options.get("message") : null;
1556+
@SuppressWarnings("unchecked")
1557+
Map<String, Object> metadata = options != null ? (Map<String, Object>) options.get("metadata") : null;
1558+
int rowCount = options != null ? (int) options.getOrDefault("row_count", 0) : 0;
1559+
1560+
MCPCheckOutputRequest request = new MCPCheckOutputRequest(connectorType, responseData, message, metadata, rowCount);
1561+
1562+
Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-output", request);
1563+
try (Response response = httpClient.newCall(httpRequest).execute()) {
1564+
ResponseBody responseBody = response.body();
1565+
if (responseBody == null) {
1566+
throw new ConnectorException("Empty response from MCP check-output", connectorType, "mcpCheckOutput");
1567+
}
1568+
String responseJson = responseBody.string();
1569+
1570+
// 403 means policy blocked — the body is still a valid response
1571+
if (!response.isSuccessful() && response.code() != 403) {
1572+
try {
1573+
Map<String, Object> errorData = objectMapper.readValue(responseJson,
1574+
new TypeReference<Map<String, Object>>() {});
1575+
String errorMsg = errorData.get("error") != null ?
1576+
errorData.get("error").toString() :
1577+
"MCP check-output failed: " + response.code();
1578+
throw new ConnectorException(errorMsg, connectorType, "mcpCheckOutput");
1579+
} catch (JsonProcessingException e) {
1580+
throw new ConnectorException("MCP check-output failed: " + response.code(), connectorType, "mcpCheckOutput");
1581+
}
1582+
}
1583+
1584+
return objectMapper.readValue(responseJson, MCPCheckOutputResponse.class);
1585+
}
1586+
}, "mcpCheckOutput");
1587+
}
1588+
1589+
/**
1590+
* Asynchronously validates MCP response data against configured policies.
1591+
*
1592+
* @param connectorType name of the MCP connector type
1593+
* @param responseData the response data rows to validate
1594+
* @return a future containing the check result
1595+
*/
1596+
public CompletableFuture<MCPCheckOutputResponse> mcpCheckOutputAsync(String connectorType, List<Map<String, Object>> responseData) {
1597+
return CompletableFuture.supplyAsync(() -> mcpCheckOutput(connectorType, responseData), asyncExecutor);
1598+
}
1599+
1600+
/**
1601+
* Asynchronously validates MCP response data against configured policies with options.
1602+
*
1603+
* @param connectorType name of the MCP connector type
1604+
* @param responseData the response data rows to validate
1605+
* @param options optional parameters
1606+
* @return a future containing the check result
1607+
*/
1608+
public CompletableFuture<MCPCheckOutputResponse> mcpCheckOutputAsync(String connectorType, List<Map<String, Object>> responseData, Map<String, Object> options) {
1609+
return CompletableFuture.supplyAsync(() -> mcpCheckOutput(connectorType, responseData, options), asyncExecutor);
1610+
}
1611+
14081612
// ========================================================================
14091613
// Policy CRUD - Static Policies
14101614
// ========================================================================
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2026 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.getaxonflow.sdk.types;
17+
18+
import com.fasterxml.jackson.annotation.JsonInclude;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
import java.util.Map;
22+
import java.util.Objects;
23+
24+
/**
25+
* Request to validate an MCP input against configured policies without executing it.
26+
*
27+
* <p>Used with the {@code POST /api/v1/mcp/check-input} endpoint to pre-validate
28+
* a statement before sending it to the connector.</p>
29+
*/
30+
@JsonInclude(JsonInclude.Include.NON_NULL)
31+
public final class MCPCheckInputRequest {
32+
33+
@JsonProperty("connector_type")
34+
private final String connectorType;
35+
36+
@JsonProperty("statement")
37+
private final String statement;
38+
39+
@JsonProperty("parameters")
40+
private final Map<String, Object> parameters;
41+
42+
@JsonProperty("operation")
43+
private final String operation;
44+
45+
/**
46+
* Creates a request with connector type and statement only.
47+
* Operation defaults to "query".
48+
*
49+
* @param connectorType the MCP connector type (e.g., "postgres")
50+
* @param statement the statement to validate
51+
*/
52+
public MCPCheckInputRequest(String connectorType, String statement) {
53+
this(connectorType, statement, null, "query");
54+
}
55+
56+
/**
57+
* Creates a request with all fields.
58+
*
59+
* @param connectorType the MCP connector type (e.g., "postgres")
60+
* @param statement the statement to validate
61+
* @param parameters optional query parameters
62+
* @param operation the operation type (e.g., "query", "execute")
63+
*/
64+
public MCPCheckInputRequest(String connectorType, String statement,
65+
Map<String, Object> parameters, String operation) {
66+
this.connectorType = connectorType;
67+
this.statement = statement;
68+
this.parameters = parameters;
69+
this.operation = operation;
70+
}
71+
72+
public String getConnectorType() {
73+
return connectorType;
74+
}
75+
76+
public String getStatement() {
77+
return statement;
78+
}
79+
80+
public Map<String, Object> getParameters() {
81+
return parameters;
82+
}
83+
84+
public String getOperation() {
85+
return operation;
86+
}
87+
88+
@Override
89+
public boolean equals(Object o) {
90+
if (this == o) return true;
91+
if (o == null || getClass() != o.getClass()) return false;
92+
MCPCheckInputRequest that = (MCPCheckInputRequest) o;
93+
return Objects.equals(connectorType, that.connectorType) &&
94+
Objects.equals(statement, that.statement) &&
95+
Objects.equals(parameters, that.parameters) &&
96+
Objects.equals(operation, that.operation);
97+
}
98+
99+
@Override
100+
public int hashCode() {
101+
return Objects.hash(connectorType, statement, parameters, operation);
102+
}
103+
104+
@Override
105+
public String toString() {
106+
return "MCPCheckInputRequest{" +
107+
"connectorType='" + connectorType + '\'' +
108+
", statement='" + statement + '\'' +
109+
", operation='" + operation + '\'' +
110+
'}';
111+
}
112+
}

0 commit comments

Comments
 (0)