Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.7.0] - 2026-02-28

### Added

- **MCP Policy-Check Endpoints** (Platform v4.6.0+): Standalone policy validation for external orchestrators (LangGraph, CrewAI) to enforce AxonFlow policies without executing connector queries
- `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()`
- `mcpCheckOutput(connectorType, responseData)`: Validate MCP response data against output policies (PII redaction, exfiltration limits, dynamic policies). Returns original or redacted data with `PolicyInfo`
- New types: `MCPCheckInputRequest`, `MCPCheckInputResponse`, `MCPCheckOutputRequest`, `MCPCheckOutputResponse`
- Sync + async variants with overloads for additional options (`parameters`, `operation`)
- Supports both query-style (`responseData`) and execute-style (`message` + `metadata`) output validation

---

## [3.6.0] - 2026-02-22

### Added
Expand Down
204 changes: 204 additions & 0 deletions src/main/java/com/getaxonflow/sdk/AxonFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,210 @@ public ConnectorResponse mcpExecute(String connector, String statement) {
return mcpQuery(connector, statement);
}

// ========================================================================
// MCP Policy Check (Standalone)
// ========================================================================

/**
* Validates an MCP input statement against configured policies without executing it.
*
* <p>This method calls the agent's {@code /api/v1/mcp/check-input} endpoint to pre-validate
* a statement before sending it to the connector. Useful for checking SQL injection
* patterns, blocked operations, and input policy violations.</p>
*
* <p>Example usage:
* <pre>{@code
* MCPCheckInputResponse result = axonflow.mcpCheckInput("postgres", "SELECT * FROM users");
* if (!result.isAllowed()) {
* System.out.println("Blocked: " + result.getBlockReason());
* }
* }</pre>
*
* @param connectorType name of the MCP connector type (e.g., "postgres")
* @param statement the statement to validate
* @return MCPCheckInputResponse with allowed status, block reason, and policy info
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
*/
public MCPCheckInputResponse mcpCheckInput(String connectorType, String statement) {
return mcpCheckInput(connectorType, statement, null);
}

/**
* Validates an MCP input statement against configured policies with options.
*
* @param connectorType name of the MCP connector type (e.g., "postgres")
* @param statement the statement to validate
* @param options optional parameters: "operation" (String), "parameters" (Map)
* @return MCPCheckInputResponse with allowed status, block reason, and policy info
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
*/
public MCPCheckInputResponse mcpCheckInput(String connectorType, String statement, Map<String, Object> options) {
Objects.requireNonNull(connectorType, "connectorType cannot be null");
Objects.requireNonNull(statement, "statement cannot be null");

return retryExecutor.execute(() -> {
MCPCheckInputRequest request;
if (options != null) {
String operation = (String) options.getOrDefault("operation", "query");
@SuppressWarnings("unchecked")
Map<String, Object> parameters = (Map<String, Object>) options.get("parameters");
request = new MCPCheckInputRequest(connectorType, statement, parameters, operation);
} else {
request = new MCPCheckInputRequest(connectorType, statement);
}

Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-input", request);
try (Response response = httpClient.newCall(httpRequest).execute()) {
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new ConnectorException("Empty response from MCP check-input", connectorType, "mcpCheckInput");
}
String responseJson = responseBody.string();

// 403 means policy blocked — the body is still a valid response
if (!response.isSuccessful() && response.code() != 403) {
try {
Map<String, Object> errorData = objectMapper.readValue(responseJson,
new TypeReference<Map<String, Object>>() {});
String errorMsg = errorData.get("error") != null ?
errorData.get("error").toString() :
"MCP check-input failed: " + response.code();
throw new ConnectorException(errorMsg, connectorType, "mcpCheckInput");
} catch (JsonProcessingException e) {
throw new ConnectorException("MCP check-input failed: " + response.code(), connectorType, "mcpCheckInput");
}
}

return objectMapper.readValue(responseJson, MCPCheckInputResponse.class);
}
}, "mcpCheckInput");
}

/**
* Asynchronously validates an MCP input statement against configured policies.
*
* @param connectorType name of the MCP connector type
* @param statement the statement to validate
* @return a future containing the check result
*/
public CompletableFuture<MCPCheckInputResponse> mcpCheckInputAsync(String connectorType, String statement) {
return CompletableFuture.supplyAsync(() -> mcpCheckInput(connectorType, statement), asyncExecutor);
}

/**
* Asynchronously validates an MCP input statement against configured policies with options.
*
* @param connectorType name of the MCP connector type
* @param statement the statement to validate
* @param options optional parameters
* @return a future containing the check result
*/
public CompletableFuture<MCPCheckInputResponse> mcpCheckInputAsync(String connectorType, String statement, Map<String, Object> options) {
return CompletableFuture.supplyAsync(() -> mcpCheckInput(connectorType, statement, options), asyncExecutor);
}

/**
* Validates MCP response data against configured policies.
*
* <p>This method calls the agent's {@code /api/v1/mcp/check-output} endpoint to check
* response data for PII content, exfiltration limit violations, and other output
* policy violations. If PII redaction is active, {@code redactedData} contains the
* sanitized version.</p>
*
* <p>Example usage:
* <pre>{@code
* List<Map<String, Object>> rows = List.of(
* Map.of("name", "John", "ssn", "123-45-6789")
* );
* MCPCheckOutputResponse result = axonflow.mcpCheckOutput("postgres", rows);
* if (!result.isAllowed()) {
* System.out.println("Blocked: " + result.getBlockReason());
* }
* if (result.getRedactedData() != null) {
* System.out.println("Redacted: " + result.getRedactedData());
* }
* }</pre>
*
* @param connectorType name of the MCP connector type (e.g., "postgres")
* @param responseData the response data rows to validate
* @return MCPCheckOutputResponse with allowed status, redacted data, and policy info
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
*/
public MCPCheckOutputResponse mcpCheckOutput(String connectorType, List<Map<String, Object>> responseData) {
return mcpCheckOutput(connectorType, responseData, null);
}

/**
* Validates MCP response data against configured policies with options.
*
* @param connectorType name of the MCP connector type (e.g., "postgres")
* @param responseData the response data rows to validate
* @param options optional parameters: "message" (String), "metadata" (Map), "row_count" (int)
* @return MCPCheckOutputResponse with allowed status, redacted data, and policy info
* @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked)
*/
public MCPCheckOutputResponse mcpCheckOutput(String connectorType, List<Map<String, Object>> responseData, Map<String, Object> options) {
Objects.requireNonNull(connectorType, "connectorType cannot be null");
// responseData can be null for execute-style requests that use message instead

return retryExecutor.execute(() -> {
String message = options != null ? (String) options.get("message") : null;
@SuppressWarnings("unchecked")
Map<String, Object> metadata = options != null ? (Map<String, Object>) options.get("metadata") : null;
int rowCount = options != null ? (int) options.getOrDefault("row_count", 0) : 0;

MCPCheckOutputRequest request = new MCPCheckOutputRequest(connectorType, responseData, message, metadata, rowCount);

Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-output", request);
try (Response response = httpClient.newCall(httpRequest).execute()) {
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new ConnectorException("Empty response from MCP check-output", connectorType, "mcpCheckOutput");
}
String responseJson = responseBody.string();

// 403 means policy blocked — the body is still a valid response
if (!response.isSuccessful() && response.code() != 403) {
try {
Map<String, Object> errorData = objectMapper.readValue(responseJson,
new TypeReference<Map<String, Object>>() {});
String errorMsg = errorData.get("error") != null ?
errorData.get("error").toString() :
"MCP check-output failed: " + response.code();
throw new ConnectorException(errorMsg, connectorType, "mcpCheckOutput");
} catch (JsonProcessingException e) {
throw new ConnectorException("MCP check-output failed: " + response.code(), connectorType, "mcpCheckOutput");
}
}

return objectMapper.readValue(responseJson, MCPCheckOutputResponse.class);
}
}, "mcpCheckOutput");
}

/**
* Asynchronously validates MCP response data against configured policies.
*
* @param connectorType name of the MCP connector type
* @param responseData the response data rows to validate
* @return a future containing the check result
*/
public CompletableFuture<MCPCheckOutputResponse> mcpCheckOutputAsync(String connectorType, List<Map<String, Object>> responseData) {
return CompletableFuture.supplyAsync(() -> mcpCheckOutput(connectorType, responseData), asyncExecutor);
}

/**
* Asynchronously validates MCP response data against configured policies with options.
*
* @param connectorType name of the MCP connector type
* @param responseData the response data rows to validate
* @param options optional parameters
* @return a future containing the check result
*/
public CompletableFuture<MCPCheckOutputResponse> mcpCheckOutputAsync(String connectorType, List<Map<String, Object>> responseData, Map<String, Object> options) {
return CompletableFuture.supplyAsync(() -> mcpCheckOutput(connectorType, responseData, options), asyncExecutor);
}

// ========================================================================
// Policy CRUD - Static Policies
// ========================================================================
Expand Down
112 changes: 112 additions & 0 deletions src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2026 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.types;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Map;
import java.util.Objects;

/**
* Request to validate an MCP input against configured policies without executing it.
*
* <p>Used with the {@code POST /api/v1/mcp/check-input} endpoint to pre-validate
* a statement before sending it to the connector.</p>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class MCPCheckInputRequest {

@JsonProperty("connector_type")
private final String connectorType;

@JsonProperty("statement")
private final String statement;

@JsonProperty("parameters")
private final Map<String, Object> parameters;

@JsonProperty("operation")
private final String operation;

/**
* Creates a request with connector type and statement only.
* Operation defaults to "query".
*
* @param connectorType the MCP connector type (e.g., "postgres")
* @param statement the statement to validate
*/
public MCPCheckInputRequest(String connectorType, String statement) {
this(connectorType, statement, null, "query");
}

/**
* Creates a request with all fields.
*
* @param connectorType the MCP connector type (e.g., "postgres")
* @param statement the statement to validate
* @param parameters optional query parameters
* @param operation the operation type (e.g., "query", "execute")
*/
public MCPCheckInputRequest(String connectorType, String statement,
Map<String, Object> parameters, String operation) {
this.connectorType = connectorType;
this.statement = statement;
this.parameters = parameters;
this.operation = operation;
}

public String getConnectorType() {
return connectorType;
}

public String getStatement() {
return statement;
}

public Map<String, Object> getParameters() {
return parameters;
}

public String getOperation() {
return operation;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MCPCheckInputRequest that = (MCPCheckInputRequest) o;
return Objects.equals(connectorType, that.connectorType) &&
Objects.equals(statement, that.statement) &&
Objects.equals(parameters, that.parameters) &&
Objects.equals(operation, that.operation);
}

@Override
public int hashCode() {
return Objects.hash(connectorType, statement, parameters, operation);
}

@Override
public String toString() {
return "MCPCheckInputRequest{" +
"connectorType='" + connectorType + '\'' +
", statement='" + statement + '\'' +
", operation='" + operation + '\'' +
'}';
}
}
Loading
Loading