From 95cfad6c6656686c40311fa9fb0f79a01af4020d Mon Sep 17 00:00:00 2001 From: Gregory Zak Date: Thu, 16 Jul 2026 15:11:14 -0700 Subject: [PATCH 1/4] fix(langgraph): stop concatenating getServerName()+getName() into connectorType LangGraphAdapter.mcpToolInterceptor() derived a single connectorType as "{serverName}.{toolName}" because the MCP check-input/check-output wire contract only had one identity field. The platform now accepts a separate `tool` field alongside `connector_type` (epic #2905, #2904), so thread server and tool identity through as two distinct fields instead of collapsing them into one opaque string. - MCPCheckInputRequest / MCPCheckOutputRequest gain an optional `tool` field (sdk_only pending platform-spec sync, tracked in the wire-shape baseline like `content_type` before it). - AxonFlow.mcpCheckInput/mcpCheckOutput read "tool" out of the options map and forward it on the wire. - MCPInterceptorOptions gains a toolFn alongside connectorTypeFn. - LangGraphAdapter.mcpToolInterceptor() now defaults connectorType to MCPToolRequest::getServerName and tool to MCPToolRequest::getName, in both the custom-options and default branches. - Add runtime-e2e/mcp_server_tool_split/, run against a live agent: proves the interceptor's split server+tool identity round-trips through check-input/check-output, a direct mcpCheckInput call with an explicit "tool" option is accepted, and the pre-#2909 two-argument mcpCheckInput(connectorType, statement) overload still works unchanged. Refs #2909, epic #2905. Signed-off-by: Gregory Zak --- CHANGELOG.md | 21 ++- .../McpServerToolSplitTest.java | 120 ++++++++++++++++++ runtime-e2e/mcp_server_tool_split/README.md | 31 +++++ .../java/com/getaxonflow/sdk/AxonFlow.java | 18 ++- .../sdk/adapters/LangGraphAdapter.java | 9 +- .../sdk/adapters/MCPInterceptorOptions.java | 36 +++++- .../sdk/adapters/MCPToolInterceptor.java | 12 +- .../sdk/types/MCPCheckInputRequest.java | 49 ++++++- .../sdk/types/MCPCheckOutputRequest.java | 47 ++++++- .../com/getaxonflow/sdk/AxonFlowTest.java | 53 ++++++++ .../sdk/adapters/LangGraphAdapterTest.java | 39 +++++- .../getaxonflow/sdk/types/MoreTypesTest.java | 68 ++++++++++ tests/fixtures/wire-shape-baseline.json | 7 +- 13 files changed, 485 insertions(+), 25 deletions(-) create mode 100644 runtime-e2e/mcp_server_tool_split/McpServerToolSplitTest.java create mode 100644 runtime-e2e/mcp_server_tool_split/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c59ab1..599087a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] Hostile-testing sweep ahead of the BukuWarung integration -(getaxonflow/axonflow-enterprise#2861). Examples + runtime-e2e only — no -library changes. +(getaxonflow/axonflow-enterprise#2861). ### Fixed @@ -19,6 +18,15 @@ library changes. the generic `AxonFlowException` catch with exit 0. The example now reads `AXONFLOW_USER_TOKEN` and exits non-zero on invalid-user-token rejections. +- **`LangGraphAdapter.mcpToolInterceptor()` no longer concatenates MCP server + and tool name into a single `connectorType` string** (epic #2905, #2909). + `MCPToolRequest#getServerName()` and `MCPToolRequest#getName()` are now + threaded through as two separate identity fields — `connector_type` and the + new `tool` field — on `mcpCheckInput`/`mcpCheckOutput`, matching the + platform's two-field (server, tool) MCP identity contract (#2904). + `MCPInterceptorOptions` gains a `toolFn` alongside the existing + `connectorTypeFn` for callers who need custom derivation logic. + ### Added - `runtime-e2e/async_verdict_parity/` — live-agent assertion that @@ -27,6 +35,15 @@ library changes. SQLi → `deny` on `/api/v1/decide`, `allowed=false` on check-input, sync == async on both planes. +- `runtime-e2e/mcp_server_tool_split/` — live-agent assertion for the + `connector_type`/`tool` split (#2909, epic #2905/#2904): + `LangGraphAdapter.mcpToolInterceptor()` round-trips a clean tool call + through check-input/check-output with the server and tool names as two + distinct wire fields, a direct `mcpCheckInput(..., options)` call with an + explicit `tool` option is accepted, and the pre-#2909 two-argument + `mcpCheckInput(connectorType, statement)` overload (no `tool` field) still + works unchanged. + ## [8.5.1] - 2026-06-16: TLS security hardening (production guard) Patch release. No public API changes. diff --git a/runtime-e2e/mcp_server_tool_split/McpServerToolSplitTest.java b/runtime-e2e/mcp_server_tool_split/McpServerToolSplitTest.java new file mode 100644 index 0000000..a1c6f4b --- /dev/null +++ b/runtime-e2e/mcp_server_tool_split/McpServerToolSplitTest.java @@ -0,0 +1,120 @@ +/* + * runtime-e2e/mcp_server_tool_split/McpServerToolSplitTest.java + * + * Real-stack proof that the platform's two-field MCP identity contract + * (connector_type + tool, epic getaxonflow/axonflow-enterprise#2905, #2904) + * is accepted end-to-end through the Java SDK's real public surface, for + * getaxonflow/axonflow-sdk-java#2909: + * + * LangGraphAdapter.mcpToolInterceptor() used to derive a single + * connectorType string as "{serverName}.{toolName}" because the wire + * contract only carried one identity field. It now sends + * MCPToolRequest#getServerName() as connectorType and + * MCPToolRequest#getName() as a separate "tool" field. + * + * This test runs a real JVM with the built SDK jar on the classpath and + * issues real HTTP requests to a real running AxonFlow agent — no mocks, + * no WireMock, no stubs. It asserts: + * + * 1. Interceptor path: MCPToolInterceptor#intercept() (reached only via + * LangGraphAdapter#mcpToolInterceptor(), the actual consumer-facing + * surface) round-trips a clean tool call through check-input -> + * handler -> check-output against the live agent, proving the agent + * accepts and processes the split server+tool identity on both the + * input and output check calls without erroring. + * 2. Direct low-level parity: AxonFlow#mcpCheckInput(connectorType, + * statement, options) with an explicit "tool" option is accepted + * (connector_type and tool arrive as two distinct wire fields). + * 3. Backward compatibility: the original two-argument + * mcpCheckInput(connectorType, statement) overload — which carries + * NO tool field at all — still works unchanged against the same + * live agent, proving the new field is additive, not breaking. + * + * Run (after `mvn -DskipTests package` to produce the SDK jar): + * + * mvn -q -DskipTests dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt + * SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1) + * AXONFLOW_AGENT_URL=http://localhost:8080 \ + * java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \ + * runtime-e2e/mcp_server_tool_split/McpServerToolSplitTest.java + * + * Defaults to http://localhost:8080 (community mode, no auth) if + * AXONFLOW_AGENT_URL is unset. Uses a dedicated tenant/clientId + * ("java-sdk-2909-runtime-e2e") to avoid colliding with other tests + * sharing the same agent. + */ +import com.getaxonflow.sdk.AxonFlow; +import com.getaxonflow.sdk.AxonFlowConfig; +import com.getaxonflow.sdk.adapters.LangGraphAdapter; +import com.getaxonflow.sdk.adapters.MCPToolInterceptor; +import com.getaxonflow.sdk.adapters.MCPToolRequest; +import com.getaxonflow.sdk.types.MCPCheckInputResponse; +import java.util.Map; + +public class McpServerToolSplitTest { + + static void fail(String msg) { + System.err.println("FAIL: " + msg); + System.exit(1); + } + + static void check(boolean cond, String msg) { + if (!cond) { + fail(msg); + } + } + + public static void main(String[] args) throws Exception { + String endpoint = + System.getenv().getOrDefault("AXONFLOW_AGENT_URL", "http://localhost:8080"); + String tenantId = "java-sdk-2909-runtime-e2e"; + + try (AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder().endpoint(endpoint).clientId(tenantId).build())) { + + // 1. Interceptor path: the real consumer-facing surface for #2909. + // MCPToolRequest#getServerName() -> connectorType, #getName() -> tool, + // threaded through as two distinct fields instead of being + // concatenated into one opaque connectorType string. + LangGraphAdapter adapter = + LangGraphAdapter.builder(client, "mcp-server-tool-split-e2e").build(); + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + + MCPToolRequest request = + new MCPToolRequest("orders-server", "list_orders", Map.of("status", "open")); + + Object result = interceptor.intercept(request, req -> Map.of("orders", java.util.List.of())); + check(result != null, "interceptor.intercept() returned null for a clean tool call"); + System.out.println( + "PASS [interceptor-server-tool-split] server=orders-server tool=list_orders " + + "-> result=" + result); + + // 2. Direct low-level call: explicit "tool" option is accepted and + // processed by the live agent as a distinct field from connectorType. + MCPCheckInputResponse withTool = + client.mcpCheckInput( + "orders-server", + "orders-server.list_orders({\"status\":\"open\"})", + Map.of("operation", "execute", "tool", "list_orders")); + check( + withTool.isAllowed(), + "expected clean statement with explicit tool option to be allowed, got blocked: " + + withTool.getBlockReason()); + System.out.println("PASS [direct-check-input-with-tool] allowed=" + withTool.isAllowed()); + + // 3. Backward compatibility: the pre-#2909 two-arg overload has no + // tool field on the wire at all. It must still work unchanged. + MCPCheckInputResponse withoutTool = + client.mcpCheckInput("orders-server", "SELECT 1"); + check( + withoutTool.isAllowed(), + "expected clean statement with no tool field (old shape) to be allowed, got blocked: " + + withoutTool.getBlockReason()); + System.out.println( + "PASS [backward-compat-no-tool] allowed=" + withoutTool.isAllowed()); + } + + System.out.println("RESULT: PASS (3/3)"); + } +} diff --git a/runtime-e2e/mcp_server_tool_split/README.md b/runtime-e2e/mcp_server_tool_split/README.md new file mode 100644 index 0000000..43459f1 --- /dev/null +++ b/runtime-e2e/mcp_server_tool_split/README.md @@ -0,0 +1,31 @@ +# mcp_server_tool_split (#2909 — MCP server/tool identity split, epic #2905/#2904) + +Real-stack proof, against a live AxonFlow agent, that: + +1. `LangGraphAdapter.mcpToolInterceptor()` (the real consumer-facing MCP + interceptor) sends `MCPToolRequest#getServerName()` as `connector_type` + and `MCPToolRequest#getName()` as a separate `tool` field — no longer + concatenated into one opaque `"{serverName}.{toolName}"` string — and a + clean tool call round-trips through check-input -> handler -> check-output + successfully. +2. `AxonFlow#mcpCheckInput(connectorType, statement, options)` with an + explicit `"tool"` option is accepted by the live agent. +3. The pre-#2909 two-argument `mcpCheckInput(connectorType, statement)` + overload — which carries no `tool` field at all — still works unchanged + (backward compatibility). + +## Run + +``` +mvn -q -DskipTests package +mvn -q -DskipTests dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt +SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1) +AXONFLOW_AGENT_URL=http://localhost:8080 \ + java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \ + runtime-e2e/mcp_server_tool_split/McpServerToolSplitTest.java +``` + +Defaults to `http://localhost:8080` (community mode, no auth) if +`AXONFLOW_AGENT_URL` is unset. Uses a dedicated `clientId`/tenant +(`java-sdk-2909-runtime-e2e`) to avoid collisions with other tests sharing +the same agent. Exits non-zero (and prints `FAIL: ...`) if any step fails. diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index f642fc6..bd91fe9 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -2468,7 +2468,7 @@ public MCPCheckInputResponse mcpCheckInput(String connectorType, String statemen * * @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) + * @param options optional parameters: "operation" (String), "parameters" (Map), "tool" (String) * @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) */ @@ -2487,9 +2487,13 @@ public MCPCheckInputResponse mcpCheckInput( // content_type selects the request-redaction detector (ADR-056 / #2563); null // defaults to text/plain server-side. String contentType = (String) options.get("content_type"); + // 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 = (String) options.get("tool"); request = new MCPCheckInputRequest( - connectorType, statement, parameters, operation, contentType); + connectorType, statement, parameters, operation, contentType, tool); } else { request = new MCPCheckInputRequest(connectorType, statement); } @@ -2828,7 +2832,8 @@ public MCPCheckOutputResponse mcpCheckOutput( * * @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) + * @param options optional parameters: "message" (String), "metadata" (Map), "row_count" (int), + * "tool" (String) * @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) */ @@ -2844,9 +2849,14 @@ public MCPCheckOutputResponse mcpCheckOutput( Map metadata = options != null ? (Map) 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..fc4b097 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java @@ -443,20 +443,23 @@ public MCPToolInterceptor mcpToolInterceptor() { */ public MCPToolInterceptor mcpToolInterceptor(MCPInterceptorOptions options) { Function connectorTypeFn; + Function toolFn; String operation; if (options != null) { connectorTypeFn = options.getConnectorTypeFn() != null ? options.getConnectorTypeFn() - : req -> req.getServerName() + "." + req.getName(); + : MCPToolRequest::getServerName; + toolFn = options.getToolFn() != null ? options.getToolFn() : MCPToolRequest::getName; operation = options.getOperation(); } else { - connectorTypeFn = req -> req.getServerName() + "." + req.getName(); + connectorTypeFn = MCPToolRequest::getServerName; + toolFn = MCPToolRequest::getName; operation = "execute"; } - return new MCPToolInterceptor(client, connectorTypeFn, operation); + return new MCPToolInterceptor(client, connectorTypeFn, toolFn, 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..3eadfe4 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java @@ -26,16 +26,22 @@ public final class MCPInterceptorOptions { private final Function connectorTypeFn; + private final Function toolFn; private final String operation; private MCPInterceptorOptions(Builder builder) { this.connectorTypeFn = builder.connectorTypeFn; + this.toolFn = builder.toolFn; this.operation = builder.operation; } /** - * 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 (see {@link #getToolFn()}) so policies can match on server identity, tool identity, + * or both. * * @return the connector type function, or null */ @@ -43,6 +49,16 @@ public Function getConnectorTypeFn() { return connectorTypeFn; } + /** + * Returns the function that maps an MCP request to a tool name string. May be null, in which + * case the default {@link MCPToolRequest#getName()} is used. + * + * @return the tool name function, or null + */ + public Function getToolFn() { + return toolFn; + } + /** * Returns the operation type passed to {@code mcpCheckInput}. Defaults to "execute". * @@ -58,12 +74,14 @@ public static Builder builder() { public static final class Builder { private Function connectorTypeFn; + private Function toolFn; private String operation = "execute"; 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()}. * * @param connectorTypeFn mapping function * @return this builder @@ -73,6 +91,18 @@ public Builder connectorTypeFn(Function connectorTypeFn) return this; } + /** + * Sets a custom function to derive the tool name from an MCP request. Defaults to {@link + * MCPToolRequest#getName()}. + * + * @param toolFn mapping function + * @return this builder + */ + public Builder toolFn(Function toolFn) { + this.toolFn = toolFn; + return this; + } + /** * Sets the operation type. Defaults to "execute". Use "query" for known read-only tool calls. * diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java index b79fcf3..196799f 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java @@ -47,12 +47,17 @@ public final class MCPToolInterceptor { private final AxonFlow client; private final Function connectorTypeFn; + private final Function toolFn; private final String operation; MCPToolInterceptor( - AxonFlow client, Function connectorTypeFn, String operation) { + AxonFlow client, + Function connectorTypeFn, + Function toolFn, + String operation) { this.client = client; this.connectorTypeFn = connectorTypeFn; + this.toolFn = toolFn; this.operation = operation; } @@ -79,12 +84,14 @@ public final class MCPToolInterceptor { */ public Object intercept(MCPToolRequest request, MCPToolHandler handler) throws Exception { String connectorType = connectorTypeFn.apply(request); + String tool = toolFn.apply(request); 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 +118,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..e78858a 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java @@ -51,6 +51,16 @@ 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. + * 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 +86,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 +101,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 +153,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 +170,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..9380cdc 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java @@ -45,6 +45,16 @@ 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. + * Source of truth: {@code platform/agent} {@code MCPCheckInputRequest.Tool} (epic #2905 / + * #2904), mirrored on check-output. + */ + @JsonProperty("tool") + private final String tool; + /** * Creates a request with connector type and response data only. * @@ -56,7 +66,7 @@ public MCPCheckOutputRequest(String connectorType, List> res } /** - * Creates a request with all fields. + * Creates a request with all fields. Tool is left null. * * @param connectorType the MCP connector type (e.g., "postgres") * @param responseData the response data rows to validate @@ -70,11 +80,33 @@ public MCPCheckOutputRequest( String message, Map metadata, int rowCount) { + this(connectorType, responseData, message, metadata, rowCount, null); + } + + /** + * Creates a request with all fields, including the specific tool name. + * + * @param connectorType the MCP connector type/server (e.g., "postgres") + * @param responseData the response data rows to validate + * @param message optional message context + * @param metadata optional metadata + * @param rowCount the number of rows in the response + * @param tool the specific tool/action name (e.g., "query"); null when not distinguished from + * {@code connectorType} + */ + public MCPCheckOutputRequest( + String connectorType, + List> responseData, + String message, + Map metadata, + int rowCount, + String tool) { this.connectorType = connectorType; this.responseData = responseData; this.message = message; this.metadata = metadata; this.rowCount = rowCount; + this.tool = tool; } public String getConnectorType() { @@ -97,6 +129,14 @@ public int getRowCount() { return rowCount; } + /** + * 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; @@ -106,12 +146,13 @@ public boolean equals(Object o) { && Objects.equals(connectorType, that.connectorType) && Objects.equals(responseData, that.responseData) && Objects.equals(message, that.message) - && Objects.equals(metadata, that.metadata); + && Objects.equals(metadata, that.metadata) + && Objects.equals(tool, that.tool); } @Override public int hashCode() { - return Objects.hash(connectorType, responseData, message, metadata, rowCount); + return Objects.hash(connectorType, responseData, message, metadata, rowCount, tool); } @Override diff --git a/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java b/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java index 226b241..2c23113 100644 --- a/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java +++ b/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java @@ -2175,6 +2175,33 @@ void mcpCheckInputWithOptionsShouldSendOperationAndParameters() { .withRequestBody(containing("\"operation\":\"execute\""))); } + @Test + @DisplayName("mcpCheckInput with a \"tool\" option should send connector_type and tool as " + + "separate fields") + void mcpCheckInputWithToolOptionShouldSendConnectorTypeAndToolSeparately() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-input")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 5, " + + "\"policy_info\": {\"policies_evaluated\": 5, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); + + Map options = Map.of("operation", "execute", "tool", "mytool"); + MCPCheckInputResponse response = + axonflow.mcpCheckInput("myserver", "myserver.mytool({})", options); + + assertThat(response.isAllowed()).isTrue(); + + verify( + postRequestedFor(urlEqualTo("/api/v1/mcp/check-input")) + .withRequestBody(containing("\"connector_type\":\"myserver\"")) + .withRequestBody(containing("\"tool\":\"mytool\""))); + } + @Test @DisplayName("mcpCheckInput should handle 403 as blocked result") void mcpCheckInputShouldHandle403AsBlockedResult() { @@ -2311,6 +2338,32 @@ void mcpCheckOutputWithOptionsShouldSendOptions() { .withRequestBody(containing("\"row_count\":1"))); } + @Test + @DisplayName("mcpCheckOutput with a \"tool\" option should send connector_type and tool as " + + "separate fields") + void mcpCheckOutputWithToolOptionShouldSendConnectorTypeAndToolSeparately() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 6, " + + "\"policy_info\": {\"policies_evaluated\": 6, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); + + Map options = Map.of("message", "ok", "tool", "mytool"); + MCPCheckOutputResponse response = axonflow.mcpCheckOutput("myserver", null, options); + + assertThat(response.isAllowed()).isTrue(); + + verify( + postRequestedFor(urlEqualTo("/api/v1/mcp/check-output")) + .withRequestBody(containing("\"connector_type\":\"myserver\"")) + .withRequestBody(containing("\"tool\":\"mytool\""))); + } + @Test @DisplayName("mcpCheckOutput should handle 403 as blocked result") void mcpCheckOutputShouldHandle403AsBlockedResult() { diff --git a/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java b/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java index 1963ad4..41f1339 100644 --- a/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java +++ b/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java @@ -874,7 +874,7 @@ void shouldReturnRedactedData() throws Exception { } @Test - @DisplayName("should use default connector type serverName.toolName") + @DisplayName("should use serverName as connectorType and name as tool, not concatenated") void shouldUseDefaultConnectorType() throws Exception { MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); @@ -887,8 +887,16 @@ void shouldUseDefaultConnectorType() throws Exception { interceptor.intercept(request, req -> "ok"); - verify(client).mcpCheckInput(eq("myserver.mytool"), anyString(), any()); - verify(client).mcpCheckOutput(eq("myserver.mytool"), isNull(), any()); + // connectorType is the bare MCP server name -- NOT "myserver.mytool" -- and the tool + // name is threaded separately via the "tool" option, so policies can match on server + // identity, tool identity, or both instead of a single opaque concatenated string. + ArgumentCaptor> inputOptsCaptor = ArgumentCaptor.forClass(Map.class); + verify(client).mcpCheckInput(eq("myserver"), anyString(), inputOptsCaptor.capture()); + assertThat(inputOptsCaptor.getValue()).containsEntry("tool", "mytool"); + + ArgumentCaptor> outputOptsCaptor = ArgumentCaptor.forClass(Map.class); + verify(client).mcpCheckOutput(eq("myserver"), isNull(), outputOptsCaptor.capture()); + assertThat(outputOptsCaptor.getValue()).containsEntry("tool", "mytool"); } @Test @@ -914,6 +922,31 @@ void shouldUseCustomConnectorTypeFn() throws Exception { ArgumentCaptor> inputOptsCaptor = ArgumentCaptor.forClass(Map.class); verify(client).mcpCheckInput(eq("custom-pg"), anyString(), inputOptsCaptor.capture()); assertThat(inputOptsCaptor.getValue()).containsEntry("operation", "query"); + // Even with a custom connectorTypeFn, the tool name still defaults to + // MCPToolRequest#getName() and is threaded separately. + assertThat(inputOptsCaptor.getValue()).containsEntry("tool", "read"); + } + + @Test + @DisplayName("should use custom tool function independent of connector type function") + void shouldUseCustomToolFn() throws Exception { + MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); + MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); + when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + + MCPInterceptorOptions opts = + MCPInterceptorOptions.builder().toolFn(req -> "renamed-" + req.getName()).build(); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(opts); + MCPToolRequest request = new MCPToolRequest("pg", "read", null); + + interceptor.intercept(request, req -> "data"); + + ArgumentCaptor> inputOptsCaptor = ArgumentCaptor.forClass(Map.class); + verify(client).mcpCheckInput(eq("pg"), anyString(), inputOptsCaptor.capture()); + assertThat(inputOptsCaptor.getValue()).containsEntry("tool", "renamed-read"); } @Test diff --git a/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java index 1a6af0f..8117667 100644 --- a/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java @@ -1640,6 +1640,39 @@ void shouldHaveToString() { assertThat(request.toString()).contains("MCPCheckInputRequest"); assertThat(request.toString()).contains("postgres"); } + + @Test + @DisplayName("should carry connector type and tool as separate fields, not concatenated") + void shouldCarryConnectorTypeAndToolSeparately() { + MCPCheckInputRequest request = + new MCPCheckInputRequest( + "myserver", "myserver.mytool({})", Map.of("timeout", 30), "execute", null, "mytool"); + + assertThat(request.getConnectorType()).isEqualTo("myserver"); + assertThat(request.getTool()).isEqualTo("mytool"); + } + + @Test + @DisplayName("should default tool to null and omit it from JSON when not supplied") + void shouldDefaultToolToNullAndOmitFromJson() throws Exception { + MCPCheckInputRequest request = new MCPCheckInputRequest("postgres", "SELECT 1"); + + assertThat(request.getTool()).isNull(); + String json = objectMapper.writeValueAsString(request); + assertThat(json).doesNotContain("\"tool\""); + } + + @Test + @DisplayName("should serialize tool field to JSON when present") + void shouldSerializeToolToJson() throws Exception { + MCPCheckInputRequest request = + new MCPCheckInputRequest("myserver", "SELECT 1", null, "execute", null, "mytool"); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"connector_type\":\"myserver\""); + assertThat(json).contains("\"tool\":\"mytool\""); + } } @Nested @@ -1810,6 +1843,41 @@ void shouldHaveToString() { assertThat(request.toString()).contains("MCPCheckOutputRequest"); assertThat(request.toString()).contains("postgres"); } + + @Test + @DisplayName("should carry connector type and tool as separate fields, not concatenated") + void shouldCarryConnectorTypeAndToolSeparately() { + List> data = List.of(Map.of("id", 1)); + MCPCheckOutputRequest request = + new MCPCheckOutputRequest("myserver", data, "done", null, 1, "mytool"); + + assertThat(request.getConnectorType()).isEqualTo("myserver"); + assertThat(request.getTool()).isEqualTo("mytool"); + } + + @Test + @DisplayName("should default tool to null and omit it from JSON when not supplied") + void shouldDefaultToolToNullAndOmitFromJson() throws Exception { + List> data = List.of(Map.of("id", 1)); + MCPCheckOutputRequest request = new MCPCheckOutputRequest("postgres", data); + + assertThat(request.getTool()).isNull(); + String json = objectMapper.writeValueAsString(request); + assertThat(json).doesNotContain("\"tool\""); + } + + @Test + @DisplayName("should serialize tool field to JSON when present") + void shouldSerializeToolToJson() throws Exception { + List> data = List.of(Map.of("id", 1)); + MCPCheckOutputRequest request = + new MCPCheckOutputRequest("myserver", data, null, null, 0, "mytool"); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"connector_type\":\"myserver\""); + assertThat(json).contains("\"tool\":\"mytool\""); + } } @Nested diff --git a/tests/fixtures/wire-shape-baseline.json b/tests/fixtures/wire-shape-baseline.json index c55bf83..247ffb8 100644 --- a/tests/fixtures/wire-shape-baseline.json +++ b/tests/fixtures/wire-shape-baseline.json @@ -367,7 +367,8 @@ }, "MCPCheckInputRequest": { "sdk_only": [ - "content_type" + "content_type", + "tool" ], "spec_only": [ "client_id", @@ -386,7 +387,9 @@ "spec_only": [] }, "MCPCheckOutputRequest": { - "sdk_only": [], + "sdk_only": [ + "tool" + ], "spec_only": [ "client_id", "tenant_id", From 3e46d91da5beaa3556a9c32fabf31cce776017a0 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Fri, 17 Jul 2026 23:15:11 +0200 Subject: [PATCH 2/4] fix(langgraph)!: drop toolFn + breaking major (epic #2905, RULING 3) Apply epic #2905 RULING 3 and mark the de-concatenation as a BREAKING change: - RULING 3: DROP the Java-only MCPInterceptorOptions.toolFn. The tool identity is always MCPToolRequest#getName(); connectorTypeFn remains the escape hatch for the server/connector dimension only. A separate tool-identity override let a caller write an arbitrary identity into the audit trail (integrity foot-gun) and had no analog in the py/ts/go adapters. Removed the field, getter, builder method, and constructor param; the shouldUseCustomToolFn test is replaced with one asserting the tool name is always getName(). - Fix the MCPCheckOutputRequest.tool javadoc overclaim ("mirrored on check-output" implied server-side consumption): check-output has NO tool field on any released version (tracked by #2955). Also pin check-input Tool consumption to v9.10.0+. - Add the missing-server edge test: empty serverName -> connector_type="" -> platform 400 -> fail-closed; wire body pinned. - Changelog: move the entry to a new "Changed (BREAKING)" section, require a major SDK bump, add the policy re-scope note, the missing-server edge, and the platform-version pins; drop the stale "gains a toolFn" line. Refs #2909, epic #2905. Signed-off-by: Saurabh Jain --- CHANGELOG.md | 56 +++++++++++++++--- .../__pycache__/lib.cpython-314.pyc | Bin 0 -> 18059 bytes .../sdk/adapters/LangGraphAdapter.java | 7 +-- .../sdk/adapters/MCPInterceptorOptions.java | 33 ++--------- .../sdk/adapters/MCPToolInterceptor.java | 13 ++-- .../sdk/types/MCPCheckInputRequest.java | 5 +- .../sdk/types/MCPCheckOutputRequest.java | 8 ++- .../sdk/adapters/LangGraphAdapterTest.java | 41 +++++++++++-- 8 files changed, 107 insertions(+), 56 deletions(-) create mode 100644 scripts/wire_shape/__pycache__/lib.cpython-314.pyc diff --git a/CHANGELOG.md b/CHANGELOG.md index 599087a..479d79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Hostile-testing sweep ahead of the BukuWarung integration (getaxonflow/axonflow-enterprise#2861). +> **This release contains a breaking change and MUST be published as a major +> version bump.** The `connector_type` wire value emitted by the LangGraph +> adapter changes from `"{server}.{tool}"` to the bare server name; policies +> matching the old concatenated value stop matching until re-scoped (see the +> migration note below). + +### Changed (BREAKING) + +- **The LangGraph adapter now reports the (server, tool) identity as two + separate wire fields instead of concatenating them into `connectorType`** + (epic #2905, #2909). `MCPToolRequest#getServerName()` and + `MCPToolRequest#getName()` are threaded through as `connector_type` and the + new `tool` field on `mcpCheckInput`/`mcpCheckOutput`, matching the platform's + two-field (server, tool) MCP identity contract (#2904). + `mcpToolInterceptor()` sends `connectorType = serverName` and + `tool = getName()`; the default `connectorTypeFn` returns the bare + `serverName`. **`MCPInterceptorOptions` has no `toolFn`:** the tool identity + is always `MCPToolRequest#getName()` so a caller cannot write an arbitrary + tool identity into the audit trail (epic #2905, RULING 3). `connectorTypeFn` + remains the escape hatch for the server/connector dimension only. + + **Migration.** Policies or per-connector settings matching the old + concatenated value — e.g. `connector_type == "filesystem.read_file"` — stop + matching after upgrade. Re-scope them to match `connector_type == + "filesystem"` together with the `tool` field (e.g. `tool == "read_file"`). + The `connectorTypeFn` option is the compatibility lever: a caller can restore + any prior `connector_type` value (including the old concatenated form, + `req -> req.getServerName() + "." + req.getName()`) without losing the + separate `tool` field. + + **Missing-server edge.** With the default resolver, a tool whose + `getServerName()` is empty now sends `connector_type=""`, which the platform + rejects with HTTP 400 → the call throws and the tool is blocked (fail-closed), + never run ungoverned. Previously the concatenated value was `".tool"` (a + non-empty string the platform accepted). Supply a `connectorTypeFn` for + server-less MCP tools. + + **Minimum platform.** The `tool` field is consumed on `POST + /api/v1/mcp/check-input` by platform **v9.10.0+** (enterprise `c8df2006b`, + epic #2905 / #2904). On platforms below v9.10.0 the `tool` field is silently + dropped and identity degrades to the bare server name — coarser than the old + concatenated value — so **upgrade the platform to v9.10.0+ before adopting + this SDK major.** The response plane (`check-output`) does **not** consume + `tool` on any released platform version yet (tracked by #2955, targeted for + v9.11.0); the SDK sends it forward-compatibly and current platforms ignore + it. + ### Fixed - **`examples/basic` passes on enterprise (JWT-validating) stacks.** It @@ -18,15 +65,6 @@ Hostile-testing sweep ahead of the BukuWarung integration the generic `AxonFlowException` catch with exit 0. The example now reads `AXONFLOW_USER_TOKEN` and exits non-zero on invalid-user-token rejections. -- **`LangGraphAdapter.mcpToolInterceptor()` no longer concatenates MCP server - and tool name into a single `connectorType` string** (epic #2905, #2909). - `MCPToolRequest#getServerName()` and `MCPToolRequest#getName()` are now - threaded through as two separate identity fields — `connector_type` and the - new `tool` field — on `mcpCheckInput`/`mcpCheckOutput`, matching the - platform's two-field (server, tool) MCP identity contract (#2904). - `MCPInterceptorOptions` gains a `toolFn` alongside the existing - `connectorTypeFn` for callers who need custom derivation logic. - ### Added - `runtime-e2e/async_verdict_parity/` — live-agent assertion that diff --git a/scripts/wire_shape/__pycache__/lib.cpython-314.pyc b/scripts/wire_shape/__pycache__/lib.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..219c96ab66d366a24f96e1162689b9a355072e50 GIT binary patch literal 18059 zcmdsfdsH0PnP*kMpdUatZ%`wOm!NqxdRnq1OGXIvKp@n@jctKi4P8K6O?RuRk%Vj~ zBs*tJp%%Xc177Zlbkar=girIEcqyQ z{+RuJxB3NY%bwZ)woq5yx^*Al{qA?a`@OE3a&t@^u77#`#g{%l#Bsl&m&B!26a0h! z!*kpe=i(yVNzTO|;ty&fno3G*4rwD=_S8jm?5U6F@zfqN9L$Pju`;?t*$0ggW4yd6 zVj9e<;s(uI(I#EJW$H8izBT^RTb`b)roV1pBzG{^rEfN;T5uUwy=A4pMezjxSQ2B86ntr!oMf58lV3!NT)8EjE3bqLSR=(Q zYY7)AaoKP$b>$2g;w(j_dt&Q*$Z8B5z;^@VL~fV3ZwYq$XrnpM!h zKJ6V9n+0#sx4H*YWHKzO14h;Kk*jEH4fuzgFX2l_@CE|F>x3j6-`^$Ivq2+2;+-@B zwZmdKGJz74DCQpxiawxpzbGRq_{3qq><_oe^`iw814z#`~C88=&UGBqH{0G zusM;@&9KO3@zij@E6YNUcU%-+7MxC}@aidQGT;T~s8y4|Ms@1h!*VFtD}^xsk;#T? zhNd8DRXa|hKrj?hi#vrjA?Wp=Rq=lwRN{@GiBJ%r4|_2POcx`a&;-yp9+D%13IW;` zM*IO$mHbed+ak1*DkBh;G>|k@3LI4OJ{Y6l22>tnJE{LA}LC_)%7mcU`J~tMUMPVr917fgt zya1I-jyo}HZD&J%jEO3q8b2GP&ceXWDM1PWJuL(fTBM~$6h}lN6U-t|7aX2!!I}`! z&yYA7!sp(}7uvdyIE6zaajdkkg#kLjDo#yZi;Aljb%=uK8>Kdev78tgN|-3`giPyk z#2WzQ;gIZ)V4~$_K|be?43E)JPtm*tyur~^Gz^zG9Flx3z%vMiP?_pY78*`T?6c4r z3N@^uuJhFnT4%y}$0-MZ$c)l>^&yeq5*f1MGa8>u!iPF-M9`R9F>+26gH+NF{uMfn zG~rKuXESl?Wd?X#Fch2|4^7Z&$GoXC20S#J3X10#zz(dI3_z<3$RYSb0K4aCpD^ad zQt%3=*qomdPK%T0fD)jLkp;j=M@c~Ro{kd?s$(hxz%dX`Buymimx(!`ae#sqCxj;= z!s>;23IG$u6Vg_XLEa_!M=(@xL=eFb1Wb|-Q^!bbH@<`H=||Mh zvgi$nKFY&bT2gOe$$u962Ym=jp@=w)MTr+jBN}Iu@`_4FuQ0wof?>WQ1}DZHDAO=- zuE}v>^)wGO49Jbu4JS@k4>mcfksfed_+|szB3RX~QvJN5LLY}x9l$FWz<7&t-Q4LbHFVZ-~0UC#^-lLQr) zqBtGWkk|MJ0uoc4n>(r5@kbozL!O;G#*gtX-lcJAUAk7<-SxlIpqP?PTZhMoJ(5H{ z#UP28o#2K3$V50Gp78mHBPW3KW+C90S!!^wSxDukQmOA>QUFLE92|U)1A!PIM2veQ zG67z#bhko6kmOd`J0W?8#$E!+wC)F?20~$Cv#sYMEm*|Z2)&V3qNJ8hPw!|=-%sO` z1nsBcNyXyv#OYG>cqALTaN{S};WouR;@WxsS^mEIh(=b_#0sZ#XyTa

^GIr377B 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 TLg+A`HA7Z-v;ixR&F=pN=^Fkt literal 0 HcmV?d00001 diff --git a/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java b/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java index fc4b097..695693d 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java @@ -443,7 +443,6 @@ public MCPToolInterceptor mcpToolInterceptor() { */ public MCPToolInterceptor mcpToolInterceptor(MCPInterceptorOptions options) { Function connectorTypeFn; - Function toolFn; String operation; if (options != null) { @@ -451,15 +450,15 @@ public MCPToolInterceptor mcpToolInterceptor(MCPInterceptorOptions options) { options.getConnectorTypeFn() != null ? options.getConnectorTypeFn() : MCPToolRequest::getServerName; - toolFn = options.getToolFn() != null ? options.getToolFn() : MCPToolRequest::getName; operation = options.getOperation(); } else { connectorTypeFn = MCPToolRequest::getServerName; - toolFn = MCPToolRequest::getName; operation = "execute"; } - return new MCPToolInterceptor(client, connectorTypeFn, toolFn, operation); + // 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 3eadfe4..36248e9 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java @@ -26,12 +26,10 @@ public final class MCPInterceptorOptions { private final Function connectorTypeFn; - private final Function toolFn; private final String operation; private MCPInterceptorOptions(Builder builder) { this.connectorTypeFn = builder.connectorTypeFn; - this.toolFn = builder.toolFn; this.operation = builder.operation; } @@ -40,8 +38,8 @@ private MCPInterceptorOptions(Builder builder) { * 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 (see {@link #getToolFn()}) so policies can match on server identity, tool identity, - * or both. + * 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 */ @@ -49,16 +47,6 @@ public Function getConnectorTypeFn() { return connectorTypeFn; } - /** - * Returns the function that maps an MCP request to a tool name string. May be null, in which - * case the default {@link MCPToolRequest#getName()} is used. - * - * @return the tool name function, or null - */ - public Function getToolFn() { - return toolFn; - } - /** * Returns the operation type passed to {@code mcpCheckInput}. Defaults to "execute". * @@ -74,7 +62,6 @@ public static Builder builder() { public static final class Builder { private Function connectorTypeFn; - private Function toolFn; private String operation = "execute"; private Builder() {} @@ -83,6 +70,10 @@ private Builder() {} * 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 */ @@ -91,18 +82,6 @@ public Builder connectorTypeFn(Function connectorTypeFn) return this; } - /** - * Sets a custom function to derive the tool name from an MCP request. Defaults to {@link - * MCPToolRequest#getName()}. - * - * @param toolFn mapping function - * @return this builder - */ - public Builder toolFn(Function toolFn) { - this.toolFn = toolFn; - return this; - } - /** * Sets the operation type. Defaults to "execute". Use "query" for known read-only tool calls. * diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java index 196799f..5f406b9 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java @@ -47,17 +47,12 @@ public final class MCPToolInterceptor { private final AxonFlow client; private final Function connectorTypeFn; - private final Function toolFn; private final String operation; MCPToolInterceptor( - AxonFlow client, - Function connectorTypeFn, - Function toolFn, - String operation) { + AxonFlow client, Function connectorTypeFn, String operation) { this.client = client; this.connectorTypeFn = connectorTypeFn; - this.toolFn = toolFn; this.operation = operation; } @@ -84,7 +79,11 @@ public final class MCPToolInterceptor { */ public Object intercept(MCPToolRequest request, MCPToolHandler handler) throws Exception { String connectorType = connectorTypeFn.apply(request); - String tool = toolFn.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 + "." + tool + "(" + argsStr + ")"; diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java index e78858a..579d188 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java @@ -55,8 +55,9 @@ public final class MCPCheckInputRequest { * 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. - * Source of truth: {@code platform/agent} {@code MCPCheckInputRequest.Tool} (epic #2905 / - * #2904). + * 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; diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java index 9380cdc..50191e7 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java @@ -49,8 +49,12 @@ public final class MCPCheckOutputRequest { * 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. - * Source of truth: {@code platform/agent} {@code MCPCheckInputRequest.Tool} (epic #2905 / - * #2904), mirrored on check-output. + * + *

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; diff --git a/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java b/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java index 41f1339..fcd1de0 100644 --- a/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java +++ b/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java @@ -928,16 +928,20 @@ void shouldUseCustomConnectorTypeFn() throws Exception { } @Test - @DisplayName("should use custom tool function independent of connector type function") - void shouldUseCustomToolFn() throws Exception { + @DisplayName("tool name is always getName(), never a caller override (RULING 3)") + void toolNameIsAlwaysRequestName() throws Exception { MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + // A custom connectorTypeFn overrides only the connector/server dimension; + // the tool identity is always MCPToolRequest#getName() — there is no + // toolFn escape hatch that could write an arbitrary tool identity into + // the audit trail (epic #2905, RULING 3). MCPInterceptorOptions opts = - MCPInterceptorOptions.builder().toolFn(req -> "renamed-" + req.getName()).build(); + MCPInterceptorOptions.builder().connectorTypeFn(req -> "renamed-server").build(); MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(opts); MCPToolRequest request = new MCPToolRequest("pg", "read", null); @@ -945,8 +949,35 @@ void shouldUseCustomToolFn() throws Exception { interceptor.intercept(request, req -> "data"); ArgumentCaptor> inputOptsCaptor = ArgumentCaptor.forClass(Map.class); - verify(client).mcpCheckInput(eq("pg"), anyString(), inputOptsCaptor.capture()); - assertThat(inputOptsCaptor.getValue()).containsEntry("tool", "renamed-read"); + verify(client).mcpCheckInput(eq("renamed-server"), anyString(), inputOptsCaptor.capture()); + assertThat(inputOptsCaptor.getValue()).containsEntry("tool", "read"); + } + + @Test + @DisplayName("empty server name sends empty connector_type with the tool name (missing-server)") + void emptyServerNameSendsEmptyConnectorTypeAndTool() throws Exception { + // Missing-server edge (epic #2905): with the default resolver, + // connector_type is the server name, so an empty serverName sends + // connector_type="" while the tool name still travels in the "tool" + // option. A real platform rejects an empty connector_type with HTTP 400, + // which surfaces as an exception — the tool call is blocked (fail-closed) + // and the handler never runs. Before the de-concatenation the value was + // ".tool" (a non-empty string the platform accepted), so this is a + // deliberate, surfaced change for server-less tools. + MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); + MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); + when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + MCPToolRequest request = new MCPToolRequest("", "tool", null); + + interceptor.intercept(request, req -> "data"); + + ArgumentCaptor> inputOptsCaptor = ArgumentCaptor.forClass(Map.class); + verify(client).mcpCheckInput(eq(""), anyString(), inputOptsCaptor.capture()); + assertThat(inputOptsCaptor.getValue()).containsEntry("tool", "tool"); } @Test From 39fd2ae967514fa6da53f3d9d263de3d25214c68 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Fri, 17 Jul 2026 23:15:32 +0200 Subject: [PATCH 3/4] chore: remove accidentally-committed .pyc and gitignore __pycache__ The prior commit's wire-shape validate run left a scripts/wire_shape/ __pycache__/*.pyc build artifact staged. Remove it and add __pycache__/ + *.pyc to .gitignore so it cannot recur. Signed-off-by: Saurabh Jain --- .gitignore | 4 ++++ .../wire_shape/__pycache__/lib.cpython-314.pyc | Bin 18059 -> 0 bytes 2 files changed, 4 insertions(+) delete mode 100644 scripts/wire_shape/__pycache__/lib.cpython-314.pyc diff --git a/.gitignore b/.gitignore index c9e6d37..f5bf946 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,7 @@ local.properties # Heartbeat real-stack smoke build dir tests/heartbeat-real-stack/build/ tests/heartbeat-real-stack/cp.txt + +# Python bytecode (from scripts/wire_shape tooling) +__pycache__/ +*.pyc diff --git a/scripts/wire_shape/__pycache__/lib.cpython-314.pyc b/scripts/wire_shape/__pycache__/lib.cpython-314.pyc deleted file mode 100644 index 219c96ab66d366a24f96e1162689b9a355072e50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18059 zcmdsfdsH0PnP*kMpdUatZ%`wOm!NqxdRnq1OGXIvKp@n@jctKi4P8K6O?RuRk%Vj~ zBs*tJp%%Xc177Zlbkar=girIEcqyQ z{+RuJxB3NY%bwZ)woq5yx^*Al{qA?a`@OE3a&t@^u77#`#g{%l#Bsl&m&B!26a0h! z!*kpe=i(yVNzTO|;ty&fno3G*4rwD=_S8jm?5U6F@zfqN9L$Pju`;?t*$0ggW4yd6 zVj9e<;s(uI(I#EJW$H8izBT^RTb`b)roV1pBzG{^rEfN;T5uUwy=A4pMezjxSQ2B86ntr!oMf58lV3!NT)8EjE3bqLSR=(Q zYY7)AaoKP$b>$2g;w(j_dt&Q*$Z8B5z;^@VL~fV3ZwYq$XrnpM!h zKJ6V9n+0#sx4H*YWHKzO14h;Kk*jEH4fuzgFX2l_@CE|F>x3j6-`^$Ivq2+2;+-@B zwZmdKGJz74DCQpxiawxpzbGRq_{3qq><_oe^`iw814z#`~C88=&UGBqH{0G zusM;@&9KO3@zij@E6YNUcU%-+7MxC}@aidQGT;T~s8y4|Ms@1h!*VFtD}^xsk;#T? zhNd8DRXa|hKrj?hi#vrjA?Wp=Rq=lwRN{@GiBJ%r4|_2POcx`a&;-yp9+D%13IW;` zM*IO$mHbed+ak1*DkBh;G>|k@3LI4OJ{Y6l22>tnJE{LA}LC_)%7mcU`J~tMUMPVr917fgt zya1I-jyo}HZD&J%jEO3q8b2GP&ceXWDM1PWJuL(fTBM~$6h}lN6U-t|7aX2!!I}`! z&yYA7!sp(}7uvdyIE6zaajdkkg#kLjDo#yZi;Aljb%=uK8>Kdev78tgN|-3`giPyk z#2WzQ;gIZ)V4~$_K|be?43E)JPtm*tyur~^Gz^zG9Flx3z%vMiP?_pY78*`T?6c4r z3N@^uuJhFnT4%y}$0-MZ$c)l>^&yeq5*f1MGa8>u!iPF-M9`R9F>+26gH+NF{uMfn zG~rKuXESl?Wd?X#Fch2|4^7Z&$GoXC20S#J3X10#zz(dI3_z<3$RYSb0K4aCpD^ad zQt%3=*qomdPK%T0fD)jLkp;j=M@c~Ro{kd?s$(hxz%dX`Buymimx(!`ae#sqCxj;= z!s>;23IG$u6Vg_XLEa_!M=(@xL=eFb1Wb|-Q^!bbH@<`H=||Mh zvgi$nKFY&bT2gOe$$u962Ym=jp@=w)MTr+jBN}Iu@`_4FuQ0wof?>WQ1}DZHDAO=- zuE}v>^)wGO49Jbu4JS@k4>mcfksfed_+|szB3RX~QvJN5LLY}x9l$FWz<7&t-Q4LbHFVZ-~0UC#^-lLQr) zqBtGWkk|MJ0uoc4n>(r5@kbozL!O;G#*gtX-lcJAUAk7<-SxlIpqP?PTZhMoJ(5H{ z#UP28o#2K3$V50Gp78mHBPW3KW+C90S!!^wSxDukQmOA>QUFLE92|U)1A!PIM2veQ zG67z#bhko6kmOd`J0W?8#$E!+wC)F?20~$Cv#sYMEm*|Z2)&V3qNJ8hPw!|=-%sO` z1nsBcNyXyv#OYG>cqALTaN{S};WouR;@WxsS^mEIh(=b_#0sZ#XyTa

^GIr377B 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 TLg+A`HA7Z-v;ixR&F=pN=^Fkt From 91c43ff2200cafb61a45158f86f4608af76459a9 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 18 Jul 2026 00:25:36 +0200 Subject: [PATCH 4/4] =?UTF-8?q?release:=20finalize=20v9.0.0=20=E2=80=94=20?= =?UTF-8?q?version=20bump=20+=20clean=20[9.0.0]=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump pom.xml to 9.0.0 and finalize the tool-identity content under ## [9.0.0] - 2026-07-18 (banner + internal sweep intro dropped). Changelog scrubbed of internal issue/PR numbers and internal review terminology; minimum platform stated as versions (check-input tool = platform v9.10.0+, response-plane tool = platform v9.11.0+). Signed-off-by: Saurabh Jain --- CHANGELOG.md | 68 ++++++++++++++++++++++------------------------------ pom.xml | 2 +- 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9a483..32ee5ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,30 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -Hostile-testing sweep ahead of the BukuWarung integration -(getaxonflow/axonflow-enterprise#2861), plus the `caller_name` audit field -below. - -> **This release contains a breaking change and MUST be published as a major -> version bump.** The `connector_type` wire value emitted by the LangGraph -> adapter changes from `"{server}.{tool}"` to the bare server name; policies -> matching the old concatenated value stop matching until re-scoped (see the -> migration note below). +## [9.0.0] - 2026-07-18 ### Changed (BREAKING) - **The LangGraph adapter now reports the (server, tool) identity as two - separate wire fields instead of concatenating them into `connectorType`** - (epic #2905, #2909). `MCPToolRequest#getServerName()` and - `MCPToolRequest#getName()` are threaded through as `connector_type` and the - new `tool` field on `mcpCheckInput`/`mcpCheckOutput`, matching the platform's - two-field (server, tool) MCP identity contract (#2904). - `mcpToolInterceptor()` sends `connectorType = serverName` and - `tool = getName()`; the default `connectorTypeFn` returns the bare - `serverName`. **`MCPInterceptorOptions` has no `toolFn`:** the tool identity - is always `MCPToolRequest#getName()` so a caller cannot write an arbitrary - tool identity into the audit trail (epic #2905, RULING 3). `connectorTypeFn` - remains the escape hatch for the server/connector dimension only. + separate wire fields instead of concatenating them into `connectorType`.** + `MCPToolRequest#getServerName()` and `MCPToolRequest#getName()` are threaded + through as `connector_type` and the new `tool` field on + `mcpCheckInput`/`mcpCheckOutput`, matching the platform's two-field + (server, tool) MCP identity contract. `mcpToolInterceptor()` sends + `connectorType = serverName` and `tool = getName()`; the default + `connectorTypeFn` returns the bare `serverName`. **`MCPInterceptorOptions` + has no `toolFn`:** the tool identity is always `MCPToolRequest#getName()`, so + a caller cannot write an arbitrary tool identity into the audit trail. + `connectorTypeFn` remains the escape hatch for the server/connector + dimension only. **Migration.** Policies or per-connector settings matching the old concatenated value — e.g. `connector_type == "filesystem.read_file"` — stop @@ -49,14 +41,13 @@ below. server-less MCP tools. **Minimum platform.** The `tool` field is consumed on `POST - /api/v1/mcp/check-input` by platform **v9.10.0+** (enterprise `c8df2006b`, - epic #2905 / #2904). On platforms below v9.10.0 the `tool` field is silently - dropped and identity degrades to the bare server name — coarser than the old - concatenated value — so **upgrade the platform to v9.10.0+ before adopting - this SDK major.** The response plane (`check-output`) does **not** consume - `tool` on any released platform version yet (tracked by #2955, targeted for - v9.11.0); the SDK sends it forward-compatibly and current platforms ignore - it. + /api/v1/mcp/check-input` by **AxonFlow platform v9.10.0+**. On platforms + below v9.10.0 the `tool` field is silently dropped and identity degrades to + the bare server name — coarser than the old concatenated value — so + **upgrade the platform to v9.10.0+ before adopting this SDK major.** + Response-plane (`check-output`) `tool` scoping requires **AxonFlow platform + v9.11.0+**; until then the SDK sends it forward-compatibly and older + platforms ignore it. ### Fixed @@ -75,21 +66,18 @@ below. async on both planes. - `runtime-e2e/mcp_server_tool_split/` — live-agent assertion for the - `connector_type`/`tool` split (#2909, epic #2905/#2904): - `LangGraphAdapter.mcpToolInterceptor()` round-trips a clean tool call - through check-input/check-output with the server and tool names as two - distinct wire fields, a direct `mcpCheckInput(..., options)` call with an - explicit `tool` option is accepted, and the pre-#2909 two-argument - `mcpCheckInput(connectorType, statement)` overload (no `tool` field) still - works unchanged. + `connector_type`/`tool` split: `LangGraphAdapter.mcpToolInterceptor()` + round-trips a clean tool call through check-input/check-output with the + server and tool names as two distinct wire fields, a direct + `mcpCheckInput(..., options)` call with an explicit `tool` option is + accepted, and the two-argument `mcpCheckInput(connectorType, statement)` + overload (no `tool` field) still works unchanged. - **`AuditToolCallRequest.callerName` (wire: `caller_name`)** — identifies WHICH CLIENT made a tool call (e.g. `claude_code`, `codex`, `cursor`, `openclaw`), replacing the misleadingly-named `toolType` field for that - purpose (getaxonflow/axonflow-enterprise#2912, epic #2905). `toolType` is - kept as a **deprecated** input fallback — not removed, not renamed; the - server resolves `caller_name` if supplied, else the legacy `tool_type`, - else a default. `runtime-e2e/caller_name_audit/` proves `callerName` - reaches `policy_details.caller_name` on a live agent + orchestrator. + purpose. `toolType` is kept as a **deprecated** input fallback — not + removed, not renamed; the server resolves `caller_name` if supplied, else + the legacy `tool_type`, else a default. ## [8.5.1] - 2026-06-16: TLS security hardening (production guard) diff --git a/pom.xml b/pom.xml index 55ab3a8..6eaa825 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.getaxonflow axonflow-sdk - 8.5.1 + 9.0.0 jar AxonFlow Java SDK