diff --git a/core/src/main/java/com/google/adk/models/Claude.java b/core/src/main/java/com/google/adk/models/Claude.java index ebb786e35..01feda1d4 100644 --- a/core/src/main/java/com/google/adk/models/Claude.java +++ b/core/src/main/java/com/google/adk/models/Claude.java @@ -31,8 +31,7 @@ import com.anthropic.models.messages.ToolUnion; import com.anthropic.models.messages.ToolUseBlockParam; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.google.adk.JsonBaseModel; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.genai.types.Content; @@ -170,9 +169,22 @@ private ContentBlockParam partToAnthropicMessageBlock(Part part) { .build()); } else if (part.functionResponse().isPresent()) { String content = ""; - if (part.functionResponse().get().response().isPresent() - && part.functionResponse().get().response().get().getOrDefault("result", null) != null) { - content = part.functionResponse().get().response().get().get("result").toString(); + if (part.functionResponse().get().response().isPresent()) { + Map responseData = part.functionResponse().get().response().get(); + + Object contentObj = responseData.get("content"); + Object resultObj = responseData.get("result"); + + if (contentObj instanceof List list && !list.isEmpty()) { + // Native MCP format: list of content blocks + content = extractMcpContentBlocks(list); + } else if (resultObj != null) { + // ADK tool result object + content = resultObj instanceof String s ? s : serializeToJson(resultObj); + } else if (!responseData.isEmpty()) { + // Fallback: arbitrary JSON structure + content = serializeToJson(responseData); + } } return ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() @@ -184,6 +196,30 @@ private ContentBlockParam partToAnthropicMessageBlock(Part part) { throw new UnsupportedOperationException("Not supported yet."); } + private String extractMcpContentBlocks(List list) { + List textBlocks = new ArrayList<>(); + for (Object item : list) { + if (item instanceof Map m && "text".equals(m.get("type"))) { + Object textObj = m.get("text"); + textBlocks.add(textObj != null ? String.valueOf(textObj) : ""); + } else if (item instanceof String s) { + textBlocks.add(s); + } else { + textBlocks.add(serializeToJson(item)); + } + } + return String.join("\n", textBlocks); + } + + private String serializeToJson(Object obj) { + try { + return JsonBaseModel.getMapper().writeValueAsString(obj); + } catch (Exception e) { + logger.warn("Failed to serialize object to JSON", e); + return String.valueOf(obj); + } + } + private void updateTypeString(Map valueDict) { if (valueDict == null) { return; @@ -221,10 +257,9 @@ private Tool functionDeclarationToAnthropicTool(FunctionDeclaration functionDecl .get() .forEach( (key, schema) -> { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.registerModule(new Jdk8Module()); Map schemaMap = - objectMapper.convertValue(schema, new TypeReference>() {}); + JsonBaseModel.getMapper() + .convertValue(schema, new TypeReference>() {}); updateTypeString(schemaMap); properties.put(key, schemaMap); }); diff --git a/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java b/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java index d9c28e501..3b0c3d70a 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java +++ b/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java @@ -16,7 +16,6 @@ package com.google.adk.tools.mcp; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.adk.tools.BaseTool; @@ -24,13 +23,9 @@ import com.google.common.collect.ImmutableMap; import com.google.genai.types.FunctionDeclaration; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.Content; import io.modelcontextprotocol.spec.McpSchema.JsonSchema; -import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.ToolAnnotations; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Optional; @@ -116,51 +111,6 @@ protected static Map wrapCallResult( return ImmutableMap.of("error", "MCP framework error: CallToolResult was null"); } - List contents = callResult.content(); - Boolean isToolError = callResult.isError(); - - if (isToolError != null && isToolError) { - String errorMessage = "Tool execution failed."; - if (contents != null - && !contents.isEmpty() - && contents.get(0) instanceof TextContent textContent) { - if (textContent.text() != null && !textContent.text().isEmpty()) { - errorMessage += " Details: " + textContent.text(); - } - } - return ImmutableMap.of("error", errorMessage); - } - - if (contents == null || contents.isEmpty()) { - return ImmutableMap.of(); - } - - List textOutputs = new ArrayList<>(); - for (Content content : contents) { - if (content instanceof TextContent textContent) { - if (textContent.text() != null) { - textOutputs.add(textContent.text()); - } - } - } - - if (textOutputs.isEmpty()) { - return ImmutableMap.of( - "error", - "Tool '" + mcpToolName + "' returned content that is not TextContent.", - "content_details", - contents.toString()); - } - - List> resultMaps = new ArrayList<>(); - for (String textOutput : textOutputs) { - try { - resultMaps.add( - objectMapper.readValue(textOutput, new TypeReference>() {})); - } catch (JsonProcessingException e) { - resultMaps.add(ImmutableMap.of("text", textOutput)); - } - } - return ImmutableMap.of("text_output", resultMaps); + return objectMapper.convertValue(callResult, new TypeReference>() {}); } } diff --git a/core/src/test/java/com/google/adk/models/ClaudeTest.java b/core/src/test/java/com/google/adk/models/ClaudeTest.java new file mode 100644 index 000000000..677d40627 --- /dev/null +++ b/core/src/test/java/com/google/adk/models/ClaudeTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.anthropic.client.AnthropicClient; +import com.anthropic.models.messages.ContentBlockParam; +import com.anthropic.models.messages.ToolResultBlockParam; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.genai.types.FunctionResponse; +import com.google.genai.types.Part; +import java.lang.reflect.Method; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +@RunWith(JUnit4.class) +public final class ClaudeTest { + + private Claude claude; + private Method partToAnthropicMessageBlockMethod; + + @Before + public void setUp() throws Exception { + AnthropicClient mockClient = Mockito.mock(AnthropicClient.class); + claude = new Claude("claude-3-opus", mockClient); + + // Access private method for testing the extraction logic + partToAnthropicMessageBlockMethod = + Claude.class.getDeclaredMethod("partToAnthropicMessageBlock", Part.class); + partToAnthropicMessageBlockMethod.setAccessible(true); + } + + @Test + public void testPartToAnthropicMessageBlock_mcpNativeFormat() throws Exception { + Map responseData = + ImmutableMap.of( + "content", + ImmutableList.of(ImmutableMap.of("type", "text", "text", "Extracted native MCP text"))); + FunctionResponse funcParam = + FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build(); + Part part = Part.builder().functionResponse(funcParam).build(); + + ContentBlockParam result = + (ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part); + + ToolResultBlockParam toolResult = result.asToolResult(); + assertThat(toolResult.content().get().asString()).isEqualTo("Extracted native MCP text"); + } + + @Test + public void testPartToAnthropicMessageBlock_legacyResultKey() throws Exception { + Map responseData = ImmutableMap.of("result", "Legacy result text"); + FunctionResponse funcParam = + FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build(); + Part part = Part.builder().functionResponse(funcParam).build(); + + ContentBlockParam result = + (ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part); + + ToolResultBlockParam toolResult = result.asToolResult(); + assertThat(toolResult.content().get().asString()).isEqualTo("Legacy result text"); + } + + @Test + public void testPartToAnthropicMessageBlock_jsonFallback() throws Exception { + Map responseData = ImmutableMap.of("custom_key", "custom_value"); + FunctionResponse funcParam = + FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build(); + Part part = Part.builder().functionResponse(funcParam).build(); + + ContentBlockParam result = + (ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part); + + ToolResultBlockParam toolResult = result.asToolResult(); + assertThat(toolResult.content().get().asString()).contains("\"custom_key\":\"custom_value\""); + } +} diff --git a/core/src/test/java/com/google/adk/tools/mcp/AbstractMcpToolTest.java b/core/src/test/java/com/google/adk/tools/mcp/AbstractMcpToolTest.java new file mode 100644 index 000000000..e8d9ea631 --- /dev/null +++ b/core/src/test/java/com/google/adk/tools/mcp/AbstractMcpToolTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.tools.mcp; + +import static com.google.common.truth.Truth.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.TextContent; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class AbstractMcpToolTest { + + private ObjectMapper objectMapper; + + @Before + public void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + public void testWrapCallResult_success() { + CallToolResult result = + CallToolResult.builder() + .content(ImmutableList.of(new TextContent("success"))) + .isError(false) + .build(); + + Map map = AbstractMcpTool.wrapCallResult(objectMapper, "my_tool", result); + + assertThat(map).containsKey("content"); + List content = (List) map.get("content"); + assertThat(content).hasSize(1); + + Map contentItem = (Map) content.get(0); + assertThat(contentItem).containsEntry("type", "text"); + assertThat(contentItem).containsEntry("text", "success"); + + assertThat(map).containsEntry("isError", false); + } +}