Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

package com.google.cloud.mcp.e2e;

import static com.google.cloud.mcp.e2e.ToolboxE2ESetup.getTextContent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.cloud.mcp.McpToolboxClient;
Expand All @@ -26,10 +29,14 @@
import com.google.cloud.mcp.tool.ToolResult;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.RegisterExtension;

@Timeout(value = 60, unit = TimeUnit.SECONDS)
class McpToolboxClientE2ETest {

@RegisterExtension static ToolboxE2ESetup server = new ToolboxE2ESetup();
Expand All @@ -41,7 +48,7 @@ void setUp() {
client = McpToolboxClient.builder().baseUrl(server.getBaseUrl()).build();
}

// --- TestBasicE2E ---
// --- Toolset Loading & Error Tests ---

@Test
void testLoadToolsetSpecific() {
Expand All @@ -68,15 +75,43 @@ void testLoadToolsetDefault() {
assertTrue(tools.containsKey("process-data"));
}

@Test
void testLoadNonExistentToolset() {
CompletionException ex =
assertThrows(
CompletionException.class,
() -> {
client.loadToolset("non-existent-toolset").join();
});
assertNotNull(ex.getCause());
assertTrue(
ex.getCause().getMessage().contains("toolset does not exist")
|| ex.getCause().getMessage().contains("non-existent-toolset")
|| ex.getCause().getMessage().contains("Toolset not found"),
"Unexpected cause: " + ex.getCause().getMessage());
}

@Test
void testLoadNonExistentTool() {
CompletionException ex =
assertThrows(
CompletionException.class,
() -> {
client.loadTool("non-existent-tool").join();
});
assertNotNull(ex.getCause());
assertTrue(
ex.getCause().getMessage().contains("Tool not found: non-existent-tool"),
"Unexpected cause: " + ex.getCause().getMessage());
}

// --- Tool Invocation & Argument Validations ---

@Test
void testRunTool() {
Tool tool = client.loadTool("get-n-rows").join();
ToolResult result = tool.execute(Map.of("num_rows", "2")).join();

if (result.isError()) {
System.out.println("ERROR OUTPUT: " + getTextContent(result));
}

assertFalse(
result.isError(), "Expected successful result, but got error: " + getTextContent(result));
String output = getTextContent(result);
Expand All @@ -85,7 +120,43 @@ void testRunTool() {
assertFalse(output.contains("row3"));
}

// --- TestBindParams ---
@Test
void testRunToolMissingRequiredParams() {
Tool tool = client.loadTool("get-n-rows").join();
CompletionException ex =
assertThrows(
CompletionException.class,
() -> {
tool.execute(Map.of()).join();
});
assertNotNull(ex.getCause());
assertTrue(
ex.getCause() instanceof IllegalArgumentException,
"Expected IllegalArgumentException but got: " + ex.getCause().getClass().getName());
assertTrue(
ex.getCause().getMessage().contains("Missing required parameter 'num_rows'"),
"Unexpected message: " + ex.getCause().getMessage());
}

@Test
void testRunToolWrongParamType() {
Tool tool = client.loadTool("get-n-rows").join();
CompletionException ex =
assertThrows(
CompletionException.class,
() -> {
tool.execute(Map.of("num_rows", 2)).join();
});
assertNotNull(ex.getCause());
assertTrue(
ex.getCause() instanceof IllegalArgumentException,
"Expected IllegalArgumentException but got: " + ex.getCause().getClass().getName());
assertTrue(
ex.getCause().getMessage().contains("expected type 'string'"),
"Unexpected message: " + ex.getCause().getMessage());
}

// --- Parameter Binding & Schema Pruning ---

@Test
void testBindParams() {
Expand Down Expand Up @@ -115,7 +186,30 @@ void testBindParamsCallable() {
assertFalse(output.contains("row4"));
}

// --- TestAuth ---
@Test
void testBoundParamPruningSchema() {
Tool tool = client.loadTool("get-n-rows").join();
boolean hadParam =
tool.definition().parameters() != null
&& tool.definition().parameters().stream().anyMatch(p -> "num_rows".equals(p.name()));
assertTrue(hadParam, "Original tool definition should have 'num_rows' parameter");

Tool boundTool = tool.bindParam("num_rows", "3");
boolean hasParamAfter =
boundTool.definition().parameters() != null
&& boundTool.definition().parameters().stream()
.anyMatch(p -> "num_rows".equals(p.name()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also check if tool.definition().parameters() still contains num_rows to ensure immutability?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added! We now assert that the original tool.definition().parameters() still contains num_rows after calling tool.bindParam(...) to verify instance immutability.

assertFalse(hasParamAfter, "Bound parameter 'num_rows' must be pruned from definition schema");

boolean originalStillHasParam =
tool.definition().parameters() != null
&& tool.definition().parameters().stream().anyMatch(p -> "num_rows".equals(p.name()));
assertTrue(
originalStillHasParam,
"Original tool definition must still contain 'num_rows' to ensure immutability");
}

// --- Authentication & Claim Injections ---

@Test
void testRunToolAuth() {
Expand Down Expand Up @@ -150,6 +244,20 @@ void testRunToolWrongAuth() {
"Actual output: " + getTextContent(result));
}

@Test
void testRunToolAuthWithoutProvidingAuth() {
Tool tool = client.loadTool("get-row-by-id-auth").join();
// Running authenticated tool without adding auth token getter
ToolResult result = tool.execute(Map.of("id", "2")).join();
assertTrue(
result.isError(),
"Expected error when invoking tool without auth token. Output: " + getTextContent(result));
assertTrue(
getTextContent(result).toLowerCase().contains("unauthorized")
|| getTextContent(result).contains("401"),
"Expected unauthorized/401 error message. Actual output: " + getTextContent(result));
}

@Test
void testRunToolParamAuth() {
Tool tool =
Expand Down Expand Up @@ -181,11 +289,25 @@ void testRunToolParamAuthNoField() {
assertTrue(getTextContent(result).contains("no field named row_data"));
}

private String getTextContent(ToolResult result) {
if (result.content() == null) return "";
return result.content().stream()
.filter(c -> "text".equals(c.type()) && c.text() != null)
.map(c -> c.text())
.collect(java.util.stream.Collectors.joining("\n"));
@Test
void testRunToolWithFailingTokenSupplier() {
Tool tool =
client
.loadTool("get-row-by-id-auth")
.join()
.addAuthTokenGetter(
"my-test-auth",
() -> CompletableFuture.failedFuture(new RuntimeException("Token unavailable")));

CompletionException ex =
assertThrows(
CompletionException.class,
() -> {
tool.execute(Map.of("id", "2")).join();
});
assertNotNull(ex.getCause());
assertTrue(
ex.getCause().getMessage().contains("Token unavailable"),
"Unexpected cause: " + ex.getCause().getMessage());
}
}
Loading
Loading