From bfdce49c2ce788619602d98e8c7295e738d5f6c9 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Sun, 31 May 2026 03:13:53 -0700 Subject: [PATCH 1/3] java: map session.mcp.apps.callTool result to JsonNode The RPC method session.mcp.apps.callTool returns a free-form JSON object (schema: type=object, additionalProperties with x-opaque-json). Previously the Java codegen fell through to Void for this pattern. Fix wrapperResultClassName() to recognize free-form object schemas (type=object + additionalProperties, no properties) and map them to com.fasterxml.jackson.databind.JsonNode. Update import generation in both generateNamespaceApiFile() and generateRpcRootFile() to handle the JsonNode special case. The generated wrapper is now: CompletableFuture callTool(SessionMcpAppsCallToolParams) Add 3 unit tests in RpcWrappersTest verifying: - Correct RPC method name dispatch - SessionId injection into params - JsonNode payload returned from the future Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- java/scripts/codegen/java.ts | 28 ++++++++++- .../generated/rpc/SessionMcpAppsApi.java | 5 +- .../com/github/copilot/RpcWrappersTest.java | 50 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 2d6c9496b..535af044f 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -1433,6 +1433,18 @@ function wrapperResultClassName(method: RpcMethodNode): string { ) { return rpcMethodToClassName(method.rpcMethod) + "Result"; } + + // Free-form object with additionalProperties (e.g., x-opaque-json) → JsonNode + if ( + result && + typeof result === "object" && + result.type === "object" && + result.additionalProperties && + !result.properties + ) { + return "JsonNode"; + } + return "Void"; } @@ -1571,7 +1583,13 @@ async function generateNamespaceApiFile( for (const [key, method] of tree.methods) { const resultClass = wrapperResultClassName(method); const paramsClass = wrapperParamsClassName(method); - if (resultClass !== "Void") allImports.add(`${packageName}.${resultClass}`); + if (resultClass !== "Void") { + if (resultClass === "JsonNode") { + allImports.add("com.fasterxml.jackson.databind.JsonNode"); + } else { + allImports.add(`${packageName}.${resultClass}`); + } + } if (paramsClass) allImports.add(`${packageName}.${paramsClass}`); const { lines, needsMapper: nm } = generateApiMethod(key, method, isSession, sessionIdExpr); @@ -1690,7 +1708,13 @@ async function generateRpcRootFile( for (const [key, method] of tree.methods) { const resultClass = wrapperResultClassName(method); const paramsClass = wrapperParamsClassName(method); - if (resultClass !== "Void") allImports.add(`${packageName}.${resultClass}`); + if (resultClass !== "Void") { + if (resultClass === "JsonNode") { + allImports.add("com.fasterxml.jackson.databind.JsonNode"); + } else { + allImports.add(`${packageName}.${resultClass}`); + } + } if (paramsClass) allImports.add(`${packageName}.${paramsClass}`); const { lines, needsMapper: nm } = generateApiMethod(key, method, isSession, sessionIdExpr); diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java index b6c131a6a..48f4ed2c7 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java @@ -7,6 +7,7 @@ package com.github.copilot.generated.rpc; +import com.fasterxml.jackson.databind.JsonNode; import java.util.concurrent.CompletableFuture; import javax.annotation.processing.Generated; @@ -68,10 +69,10 @@ public CompletableFuture listTools(SessionMcpApps * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ - public CompletableFuture callTool(SessionMcpAppsCallToolParams params) { + public CompletableFuture callTool(SessionMcpAppsCallToolParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.mcp.apps.callTool", _p, Void.class); + return caller.invoke("session.mcp.apps.callTool", _p, JsonNode.class); } /** diff --git a/java/src/test/java/com/github/copilot/RpcWrappersTest.java b/java/src/test/java/com/github/copilot/RpcWrappersTest.java index 7b01e1d38..f19d3db01 100644 --- a/java/src/test/java/com/github/copilot/RpcWrappersTest.java +++ b/java/src/test/java/com/github/copilot/RpcWrappersTest.java @@ -389,6 +389,56 @@ void copilotClient_getRpc_throws_before_start() { "getRpc() must throw IllegalStateException if called before start()"); } + // ── session.mcp.apps.callTool tests ─────────────────────────────────────── + + @Test + void sessionRpc_mcp_apps_callTool_invokes_correct_rpc_method() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-mcp"); + + var params = new com.github.copilot.generated.rpc.SessionMcpAppsCallToolParams(null, "my-server", "my-tool", + null, null); + session.mcp.apps.callTool(params); + + assertEquals(1, stub.calls.size()); + assertEquals("session.mcp.apps.callTool", stub.calls.get(0).method()); + } + + @Test + void sessionRpc_mcp_apps_callTool_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ct-inject"); + + var params = new com.github.copilot.generated.rpc.SessionMcpAppsCallToolParams(null, "server1", "tool1", null, + null); + session.mcp.apps.callTool(params); + + var sentParams = stub.calls.get(0).params(); + assertInstanceOf(com.fasterxml.jackson.databind.node.ObjectNode.class, sentParams); + var node = (com.fasterxml.jackson.databind.node.ObjectNode) sentParams; + assertEquals("sess-ct-inject", node.get("sessionId").asText()); + } + + @Test + void sessionRpc_mcp_apps_callTool_returns_jsonNode_payload() throws Exception { + var stub = new StubCaller(); + var mapper = new ObjectMapper(); + var expectedResult = mapper.createObjectNode(); + expectedResult.put("content", "hello world"); + expectedResult.put("isError", false); + stub.nextResult = expectedResult; + + var session = new SessionRpc(stub, "sess-payload"); + var params = new com.github.copilot.generated.rpc.SessionMcpAppsCallToolParams(null, "echo-server", "echo", + null, null); + var future = session.mcp.apps.callTool(params); + + var result = future.get(); + assertInstanceOf(com.fasterxml.jackson.databind.JsonNode.class, result); + assertEquals("hello world", result.get("content").asText()); + assertEquals(false, result.get("isError").asBoolean()); + } + /** * Helper that creates a loopback socket pair. The client side is used by * {@link JsonRpcClient}; the server side can be read to inspect outbound From 7206e842cc7606d89bd0fa178f5bbc74d3ec329a Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Sun, 31 May 2026 11:03:38 -0700 Subject: [PATCH 2/3] Ensure lingering content from test harness approach does not remain after mvn clean --- java/pom.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/java/pom.xml b/java/pom.xml index 65bb80556..36cf858d9 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -182,6 +182,24 @@ --> true + + + + post-clean-sweep + post-clean + + clean + + + org.apache.maven.plugins From e391d6aff4f2d57224cffac6af45a47782411dcb Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Sun, 31 May 2026 11:07:02 -0700 Subject: [PATCH 3/3] Ensure lingering content from test harness approach does not remain after mvn clean --- java/pom.xml | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/java/pom.xml b/java/pom.xml index 36cf858d9..ae2af3d68 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -167,37 +167,33 @@ org.apache.maven.plugins maven-clean-plugin 3.4.1 - - - true - + default-clean + + true + false + + + post-clean-sweep post-clean clean + + true +