Skip to content

Commit 2afd529

Browse files
edburnsCopilot
andauthored
Map session.mcp.apps.callTool result to JsonNode and harden mvn clean (#1523)
* 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<JsonNode> 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> * Ensure lingering content from test harness approach does not remain after mvn clean * Ensure lingering content from test harness approach does not remain after mvn clean --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4018d92 commit 2afd529

4 files changed

Lines changed: 108 additions & 19 deletions

File tree

java/pom.xml

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,35 @@
167167
<groupId>org.apache.maven.plugins</groupId>
168168
<artifactId>maven-clean-plugin</artifactId>
169169
<version>3.4.1</version>
170-
<configuration>
171-
<!--
172-
The E2E test harness spawns Node.js child processes
173-
(npx tsx server.ts) whose CWD is inside
174-
target/copilot-sdk/test/harness/. On macOS, orphaned
175-
node processes can briefly hold file descriptors on
176-
the directory tree after the test JVM exits, causing
177-
the first 'mvn clean' to fail with
178-
"Failed to delete target/copilot-sdk".
179-
180-
retryOnError + retryCount give the OS time to reap
181-
those processes before the clean phase gives up.
182-
-->
183-
<retryOnError>true</retryOnError>
184-
</configuration>
170+
<executions>
171+
<execution>
172+
<!--
173+
The default-clean execution uses failOnError=false
174+
because external processes (VS Code language servers,
175+
orphaned Node.js test-harness processes) can hold
176+
file descriptors on target/copilot-sdk/ just long
177+
enough to prevent deletion. The post-clean-sweep
178+
retries immediately afterward (by which time the
179+
transient locks have cleared) to ensure target/ is
180+
fully removed.
181+
-->
182+
<id>default-clean</id>
183+
<configuration>
184+
<retryOnError>true</retryOnError>
185+
<failOnError>false</failOnError>
186+
</configuration>
187+
</execution>
188+
<execution>
189+
<id>post-clean-sweep</id>
190+
<phase>post-clean</phase>
191+
<goals>
192+
<goal>clean</goal>
193+
</goals>
194+
<configuration>
195+
<retryOnError>true</retryOnError>
196+
</configuration>
197+
</execution>
198+
</executions>
185199
</plugin>
186200
<plugin>
187201
<groupId>org.apache.maven.plugins</groupId>

java/scripts/codegen/java.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,18 @@ function wrapperResultClassName(method: RpcMethodNode): string {
14331433
) {
14341434
return rpcMethodToClassName(method.rpcMethod) + "Result";
14351435
}
1436+
1437+
// Free-form object with additionalProperties (e.g., x-opaque-json) → JsonNode
1438+
if (
1439+
result &&
1440+
typeof result === "object" &&
1441+
result.type === "object" &&
1442+
result.additionalProperties &&
1443+
!result.properties
1444+
) {
1445+
return "JsonNode";
1446+
}
1447+
14361448
return "Void";
14371449
}
14381450

@@ -1571,7 +1583,13 @@ async function generateNamespaceApiFile(
15711583
for (const [key, method] of tree.methods) {
15721584
const resultClass = wrapperResultClassName(method);
15731585
const paramsClass = wrapperParamsClassName(method);
1574-
if (resultClass !== "Void") allImports.add(`${packageName}.${resultClass}`);
1586+
if (resultClass !== "Void") {
1587+
if (resultClass === "JsonNode") {
1588+
allImports.add("com.fasterxml.jackson.databind.JsonNode");
1589+
} else {
1590+
allImports.add(`${packageName}.${resultClass}`);
1591+
}
1592+
}
15751593
if (paramsClass) allImports.add(`${packageName}.${paramsClass}`);
15761594

15771595
const { lines, needsMapper: nm } = generateApiMethod(key, method, isSession, sessionIdExpr);
@@ -1690,7 +1708,13 @@ async function generateRpcRootFile(
16901708
for (const [key, method] of tree.methods) {
16911709
const resultClass = wrapperResultClassName(method);
16921710
const paramsClass = wrapperParamsClassName(method);
1693-
if (resultClass !== "Void") allImports.add(`${packageName}.${resultClass}`);
1711+
if (resultClass !== "Void") {
1712+
if (resultClass === "JsonNode") {
1713+
allImports.add("com.fasterxml.jackson.databind.JsonNode");
1714+
} else {
1715+
allImports.add(`${packageName}.${resultClass}`);
1716+
}
1717+
}
16941718
if (paramsClass) allImports.add(`${packageName}.${paramsClass}`);
16951719

16961720
const { lines, needsMapper: nm } = generateApiMethod(key, method, isSession, sessionIdExpr);

java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package com.github.copilot.generated.rpc;
99

10+
import com.fasterxml.jackson.databind.JsonNode;
1011
import java.util.concurrent.CompletableFuture;
1112
import javax.annotation.processing.Generated;
1213

@@ -68,10 +69,10 @@ public CompletableFuture<SessionMcpAppsListToolsResult> listTools(SessionMcpApps
6869
* @apiNote This method is experimental and may change in a future version.
6970
* @since 1.0.0
7071
*/
71-
public CompletableFuture<Void> callTool(SessionMcpAppsCallToolParams params) {
72+
public CompletableFuture<JsonNode> callTool(SessionMcpAppsCallToolParams params) {
7273
com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params);
7374
_p.put("sessionId", this.sessionId);
74-
return caller.invoke("session.mcp.apps.callTool", _p, Void.class);
75+
return caller.invoke("session.mcp.apps.callTool", _p, JsonNode.class);
7576
}
7677

7778
/**

java/src/test/java/com/github/copilot/RpcWrappersTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,56 @@ void copilotClient_getRpc_throws_before_start() {
389389
"getRpc() must throw IllegalStateException if called before start()");
390390
}
391391

392+
// ── session.mcp.apps.callTool tests ───────────────────────────────────────
393+
394+
@Test
395+
void sessionRpc_mcp_apps_callTool_invokes_correct_rpc_method() {
396+
var stub = new StubCaller();
397+
var session = new SessionRpc(stub, "sess-mcp");
398+
399+
var params = new com.github.copilot.generated.rpc.SessionMcpAppsCallToolParams(null, "my-server", "my-tool",
400+
null, null);
401+
session.mcp.apps.callTool(params);
402+
403+
assertEquals(1, stub.calls.size());
404+
assertEquals("session.mcp.apps.callTool", stub.calls.get(0).method());
405+
}
406+
407+
@Test
408+
void sessionRpc_mcp_apps_callTool_injects_sessionId() {
409+
var stub = new StubCaller();
410+
var session = new SessionRpc(stub, "sess-ct-inject");
411+
412+
var params = new com.github.copilot.generated.rpc.SessionMcpAppsCallToolParams(null, "server1", "tool1", null,
413+
null);
414+
session.mcp.apps.callTool(params);
415+
416+
var sentParams = stub.calls.get(0).params();
417+
assertInstanceOf(com.fasterxml.jackson.databind.node.ObjectNode.class, sentParams);
418+
var node = (com.fasterxml.jackson.databind.node.ObjectNode) sentParams;
419+
assertEquals("sess-ct-inject", node.get("sessionId").asText());
420+
}
421+
422+
@Test
423+
void sessionRpc_mcp_apps_callTool_returns_jsonNode_payload() throws Exception {
424+
var stub = new StubCaller();
425+
var mapper = new ObjectMapper();
426+
var expectedResult = mapper.createObjectNode();
427+
expectedResult.put("content", "hello world");
428+
expectedResult.put("isError", false);
429+
stub.nextResult = expectedResult;
430+
431+
var session = new SessionRpc(stub, "sess-payload");
432+
var params = new com.github.copilot.generated.rpc.SessionMcpAppsCallToolParams(null, "echo-server", "echo",
433+
null, null);
434+
var future = session.mcp.apps.callTool(params);
435+
436+
var result = future.get();
437+
assertInstanceOf(com.fasterxml.jackson.databind.JsonNode.class, result);
438+
assertEquals("hello world", result.get("content").asText());
439+
assertEquals(false, result.get("isError").asBoolean());
440+
}
441+
392442
/**
393443
* Helper that creates a loopback socket pair. The client side is used by
394444
* {@link JsonRpcClient}; the server side can be read to inspect outbound

0 commit comments

Comments
 (0)