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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -167,21 +167,35 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.4.1</version>
<configuration>
<!--
The E2E test harness spawns Node.js child processes
(npx tsx server.ts) whose CWD is inside
target/copilot-sdk/test/harness/. On macOS, orphaned
node processes can briefly hold file descriptors on
the directory tree after the test JVM exits, causing
the first 'mvn clean' to fail with
"Failed to delete target/copilot-sdk".

retryOnError + retryCount give the OS time to reap
those processes before the clean phase gives up.
-->
<retryOnError>true</retryOnError>
</configuration>
<executions>
<execution>
<!--
The default-clean execution uses failOnError=false
because external processes (VS Code language servers,
orphaned Node.js test-harness processes) can hold
file descriptors on target/copilot-sdk/ just long
enough to prevent deletion. The post-clean-sweep
retries immediately afterward (by which time the
transient locks have cleared) to ensure target/ is
fully removed.
-->
<id>default-clean</id>
<configuration>
<retryOnError>true</retryOnError>
<failOnError>false</failOnError>
</configuration>
</execution>
<execution>
<id>post-clean-sweep</id>
<phase>post-clean</phase>
<goals>
<goal>clean</goal>
</goals>
<configuration>
<retryOnError>true</retryOnError>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
28 changes: 26 additions & 2 deletions java/scripts/codegen/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -68,10 +69,10 @@ public CompletableFuture<SessionMcpAppsListToolsResult> listTools(SessionMcpApps
* @apiNote This method is experimental and may change in a future version.
* @since 1.0.0
*/
public CompletableFuture<Void> callTool(SessionMcpAppsCallToolParams params) {
public CompletableFuture<JsonNode> 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);
}

/**
Expand Down
50 changes: 50 additions & 0 deletions java/src/test/java/com/github/copilot/RpcWrappersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading