) options.get("metadata") : null;
int rowCount = options != null ? (int) options.getOrDefault("row_count", 0) : 0;
+ // tool identifies the specific tool/action invoked on the MCP server, distinct
+ // from connectorType which identifies the server/connector itself (epic #2905 /
+ // #2904).
+ String tool = options != null ? (String) options.get("tool") : null;
MCPCheckOutputRequest request =
- new MCPCheckOutputRequest(connectorType, responseData, message, metadata, rowCount);
+ new MCPCheckOutputRequest(
+ connectorType, responseData, message, metadata, rowCount, tool);
Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-output", request);
try (Response response = executeHttp(httpClient, httpRequest)) {
diff --git a/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java b/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java
index fa7569c..695693d 100644
--- a/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java
+++ b/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java
@@ -449,13 +449,15 @@ public MCPToolInterceptor mcpToolInterceptor(MCPInterceptorOptions options) {
connectorTypeFn =
options.getConnectorTypeFn() != null
? options.getConnectorTypeFn()
- : req -> req.getServerName() + "." + req.getName();
+ : MCPToolRequest::getServerName;
operation = options.getOperation();
} else {
- connectorTypeFn = req -> req.getServerName() + "." + req.getName();
+ connectorTypeFn = MCPToolRequest::getServerName;
operation = "execute";
}
+ // The tool identity is always the request's tool name (epic #2905,
+ // RULING 3); there is no caller-supplied tool override.
return new MCPToolInterceptor(client, connectorTypeFn, operation);
}
diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java
index c419e35..36248e9 100644
--- a/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java
+++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java
@@ -34,8 +34,12 @@ private MCPInterceptorOptions(Builder builder) {
}
/**
- * Returns the function that maps an MCP request to a connector type string. May be null, in which
- * case the default "{serverName}.{toolName}" is used.
+ * Returns the function that maps an MCP request to a connector type string. May be null, in
+ * which case the default {@link MCPToolRequest#getServerName()} is used.
+ *
+ * Connector type identifies the MCP server/connector itself; it is sent separately from the
+ * tool name (which is always {@link MCPToolRequest#getName()}) so policies can match on server
+ * identity, tool identity, or both.
*
* @return the connector type function, or null
*/
@@ -63,7 +67,12 @@ public static final class Builder {
private Builder() {}
/**
- * Sets a custom function to derive the connector type from an MCP request.
+ * Sets a custom function to derive the connector type (MCP server identity) from an MCP
+ * request. Defaults to {@link MCPToolRequest#getServerName()}.
+ *
+ *
There is deliberately no {@code toolFn} override: the tool identity is always {@link
+ * MCPToolRequest#getName()} so a caller cannot write an arbitrary tool identity into the audit
+ * trail (epic #2905, RULING 3).
*
* @param connectorTypeFn mapping function
* @return this builder
diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java
index b79fcf3..5f406b9 100644
--- a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java
+++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java
@@ -79,12 +79,18 @@ public final class MCPToolInterceptor {
*/
public Object intercept(MCPToolRequest request, MCPToolHandler handler) throws Exception {
String connectorType = connectorTypeFn.apply(request);
+ // The tool identity is always the request's tool name (epic #2905,
+ // RULING 3): there is no caller-supplied tool override, so an arbitrary
+ // identity can never be written into the audit trail. connectorTypeFn
+ // remains the escape hatch for the server/connector dimension only.
+ String tool = request.getName();
String argsStr = serializeArgs(request.getArgs());
- String statement = connectorType + "(" + argsStr + ")";
+ String statement = connectorType + "." + tool + "(" + argsStr + ")";
// Pre-check: validate input
Map inputOptions = new HashMap<>();
inputOptions.put("operation", operation);
+ inputOptions.put("tool", tool);
if (request.getArgs() != null && !request.getArgs().isEmpty()) {
inputOptions.put("parameters", request.getArgs());
}
@@ -111,6 +117,7 @@ public Object intercept(MCPToolRequest request, MCPToolHandler handler) throws E
Map outputOptions = new HashMap<>();
outputOptions.put("message", resultStr);
+ outputOptions.put("tool", tool);
MCPCheckOutputResponse outputCheck = client.mcpCheckOutput(connectorType, null, outputOptions);
if (!outputCheck.isAllowed()) {
diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java
index b0bfe8c..579d188 100644
--- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java
@@ -51,6 +51,17 @@ public final class MCPCheckInputRequest {
@JsonProperty("content_type")
private final String contentType;
+ /**
+ * The specific tool/action name being invoked on the MCP server (e.g., "query", "search_docs").
+ * Distinct from {@code connectorType}, which identifies the MCP server/connector itself.
+ * Optional; null when the caller doesn't distinguish per-tool identity from the connector.
+ * Consumed on {@code POST /api/v1/mcp/check-input} by platform v9.10.0+ (enterprise c8df2006b);
+ * silently ignored by platforms below v9.10.0. Source of truth: {@code platform/agent}
+ * {@code MCPCheckInputRequest.Tool} (epic #2905 / #2904).
+ */
+ @JsonProperty("tool")
+ private final String tool;
+
/**
* Creates a request with connector type and statement only. Operation defaults to "execute".
*
@@ -76,7 +87,7 @@ public MCPCheckInputRequest(
}
/**
- * Creates a request with all fields, including the redaction content type.
+ * Creates a request with all fields, including the redaction content type. Tool is left null.
*
* @param connectorType the MCP connector type (e.g., "postgres")
* @param statement the statement to validate
@@ -91,11 +102,35 @@ public MCPCheckInputRequest(
Map parameters,
String operation,
String contentType) {
+ this(connectorType, statement, parameters, operation, contentType, null);
+ }
+
+ /**
+ * Creates a request with all fields, including the redaction content type and the specific tool
+ * name.
+ *
+ * @param connectorType the MCP connector type/server (e.g., "postgres")
+ * @param statement the statement to validate
+ * @param parameters optional query parameters
+ * @param operation the operation type (e.g., "query", "execute")
+ * @param contentType the redaction content type (e.g., {@code text/plain}); null defaults
+ * server-side
+ * @param tool the specific tool/action name (e.g., "query"); null when not distinguished from
+ * {@code connectorType}
+ */
+ public MCPCheckInputRequest(
+ String connectorType,
+ String statement,
+ Map parameters,
+ String operation,
+ String contentType,
+ String tool) {
this.connectorType = connectorType;
this.statement = statement;
this.parameters = parameters;
this.operation = operation;
this.contentType = contentType;
+ this.tool = tool;
}
public String getConnectorType() {
@@ -119,6 +154,14 @@ public String getContentType() {
return contentType;
}
+ /**
+ * Returns the specific tool/action name being invoked, or null when not distinguished from
+ * {@code connectorType}.
+ */
+ public String getTool() {
+ return tool;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -128,12 +171,13 @@ public boolean equals(Object o) {
&& Objects.equals(statement, that.statement)
&& Objects.equals(parameters, that.parameters)
&& Objects.equals(operation, that.operation)
- && Objects.equals(contentType, that.contentType);
+ && Objects.equals(contentType, that.contentType)
+ && Objects.equals(tool, that.tool);
}
@Override
public int hashCode() {
- return Objects.hash(connectorType, statement, parameters, operation, contentType);
+ return Objects.hash(connectorType, statement, parameters, operation, contentType, tool);
}
@Override
diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java
index 160985e..50191e7 100644
--- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java
@@ -45,6 +45,20 @@ public final class MCPCheckOutputRequest {
@JsonProperty("row_count")
private final int rowCount;
+ /**
+ * The specific tool/action name being invoked on the MCP server (e.g., "query", "search_docs").
+ * Distinct from {@code connectorType}, which identifies the MCP server/connector itself.
+ * Optional; null when the caller doesn't distinguish per-tool identity from the connector.
+ *
+ * NOTE: unlike the input plane, the platform's check-output schema has NO {@code tool} field
+ * on any released version yet — #2904 added it input-side only, and check-output support is
+ * tracked by #2955 (targeted for v9.11.0). Sending it here is forward-compatible and harmless
+ * (the agent ignores unknown keys), but it is not consumed server-side on any platform today.
+ * Source of truth: {@code platform/agent} {@code MCPCheckInputRequest.Tool} (epic #2905 / #2904).
+ */
+ @JsonProperty("tool")
+ private final String tool;
+
/**
* Creates a request with connector type and response data only.
*
@@ -56,7 +70,7 @@ public MCPCheckOutputRequest(String connectorType, List