Skip to content

Commit 5a2abbf

Browse files
Mateusz Krawieccopybara-github
authored andcommitted
fix: resolve MCP tool parsing errors in Claude integration
The Claude model integration parsing logic failed when processing MCP tool responses because it only extracted output from the legacy `result` field. Extended extraction logic to: - Support native MCP `content` arrays. - Support legacy `result` structures natively. - Fallback to generic JSON serialization of the entire map. Additionally, updated AbstractMcpTool.wrapCallResult() format to match Python ADK. PiperOrigin-RevId: 889141233
1 parent 677b6d7 commit 5a2abbf

File tree

4 files changed

+203
-59
lines changed

4 files changed

+203
-59
lines changed

core/src/main/java/com/google/adk/models/Claude.java

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
import com.anthropic.models.messages.ToolUnion;
3232
import com.anthropic.models.messages.ToolUseBlockParam;
3333
import com.fasterxml.jackson.core.type.TypeReference;
34-
import com.fasterxml.jackson.databind.ObjectMapper;
35-
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
34+
import com.google.adk.JsonBaseModel;
3635
import com.google.common.collect.ImmutableList;
3736
import com.google.common.collect.ImmutableMap;
3837
import com.google.genai.types.Content;
@@ -170,9 +169,22 @@ private ContentBlockParam partToAnthropicMessageBlock(Part part) {
170169
.build());
171170
} else if (part.functionResponse().isPresent()) {
172171
String content = "";
173-
if (part.functionResponse().get().response().isPresent()
174-
&& part.functionResponse().get().response().get().getOrDefault("result", null) != null) {
175-
content = part.functionResponse().get().response().get().get("result").toString();
172+
if (part.functionResponse().get().response().isPresent()) {
173+
Map<String, Object> responseData = part.functionResponse().get().response().get();
174+
175+
Object contentObj = responseData.get("content");
176+
Object resultObj = responseData.get("result");
177+
178+
if (contentObj instanceof List<?> list && !list.isEmpty()) {
179+
// Native MCP format: list of content blocks
180+
content = extractMcpContentBlocks(list);
181+
} else if (resultObj != null) {
182+
// ADK tool result object
183+
content = resultObj instanceof String s ? s : serializeToJson(resultObj);
184+
} else if (!responseData.isEmpty()) {
185+
// Fallback: arbitrary JSON structure
186+
content = serializeToJson(responseData);
187+
}
176188
}
177189
return ContentBlockParam.ofToolResult(
178190
ToolResultBlockParam.builder()
@@ -184,6 +196,30 @@ private ContentBlockParam partToAnthropicMessageBlock(Part part) {
184196
throw new UnsupportedOperationException("Not supported yet.");
185197
}
186198

199+
private String extractMcpContentBlocks(List<?> list) {
200+
List<String> textBlocks = new ArrayList<>();
201+
for (Object item : list) {
202+
if (item instanceof Map<?, ?> m && "text".equals(m.get("type"))) {
203+
Object textObj = m.get("text");
204+
textBlocks.add(textObj != null ? String.valueOf(textObj) : "");
205+
} else if (item instanceof String s) {
206+
textBlocks.add(s);
207+
} else {
208+
textBlocks.add(serializeToJson(item));
209+
}
210+
}
211+
return String.join("\n", textBlocks);
212+
}
213+
214+
private String serializeToJson(Object obj) {
215+
try {
216+
return JsonBaseModel.getMapper().writeValueAsString(obj);
217+
} catch (Exception e) {
218+
logger.warn("Failed to serialize object to JSON", e);
219+
return String.valueOf(obj);
220+
}
221+
}
222+
187223
private void updateTypeString(Map<String, Object> valueDict) {
188224
if (valueDict == null) {
189225
return;
@@ -221,10 +257,9 @@ private Tool functionDeclarationToAnthropicTool(FunctionDeclaration functionDecl
221257
.get()
222258
.forEach(
223259
(key, schema) -> {
224-
ObjectMapper objectMapper = new ObjectMapper();
225-
objectMapper.registerModule(new Jdk8Module());
226260
Map<String, Object> schemaMap =
227-
objectMapper.convertValue(schema, new TypeReference<Map<String, Object>>() {});
261+
JsonBaseModel.getMapper()
262+
.convertValue(schema, new TypeReference<Map<String, Object>>() {});
228263
updateTypeString(schemaMap);
229264
properties.put(key, schemaMap);
230265
});

core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,16 @@
1616

1717
package com.google.adk.tools.mcp;
1818

19-
import com.fasterxml.jackson.core.JsonProcessingException;
2019
import com.fasterxml.jackson.core.type.TypeReference;
2120
import com.fasterxml.jackson.databind.ObjectMapper;
2221
import com.google.adk.tools.BaseTool;
2322
import com.google.adk.tools.mcp.McpToolException.McpToolDeclarationException;
2423
import com.google.common.collect.ImmutableMap;
2524
import com.google.genai.types.FunctionDeclaration;
2625
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
27-
import io.modelcontextprotocol.spec.McpSchema.Content;
2826
import io.modelcontextprotocol.spec.McpSchema.JsonSchema;
29-
import io.modelcontextprotocol.spec.McpSchema.TextContent;
3027
import io.modelcontextprotocol.spec.McpSchema.Tool;
3128
import io.modelcontextprotocol.spec.McpSchema.ToolAnnotations;
32-
import java.util.ArrayList;
33-
import java.util.List;
3429
import java.util.Map;
3530
import java.util.Optional;
3631

@@ -116,51 +111,6 @@ protected static Map<String, Object> wrapCallResult(
116111
return ImmutableMap.of("error", "MCP framework error: CallToolResult was null");
117112
}
118113

119-
List<Content> contents = callResult.content();
120-
Boolean isToolError = callResult.isError();
121-
122-
if (isToolError != null && isToolError) {
123-
String errorMessage = "Tool execution failed.";
124-
if (contents != null
125-
&& !contents.isEmpty()
126-
&& contents.get(0) instanceof TextContent textContent) {
127-
if (textContent.text() != null && !textContent.text().isEmpty()) {
128-
errorMessage += " Details: " + textContent.text();
129-
}
130-
}
131-
return ImmutableMap.of("error", errorMessage);
132-
}
133-
134-
if (contents == null || contents.isEmpty()) {
135-
return ImmutableMap.of();
136-
}
137-
138-
List<String> textOutputs = new ArrayList<>();
139-
for (Content content : contents) {
140-
if (content instanceof TextContent textContent) {
141-
if (textContent.text() != null) {
142-
textOutputs.add(textContent.text());
143-
}
144-
}
145-
}
146-
147-
if (textOutputs.isEmpty()) {
148-
return ImmutableMap.of(
149-
"error",
150-
"Tool '" + mcpToolName + "' returned content that is not TextContent.",
151-
"content_details",
152-
contents.toString());
153-
}
154-
155-
List<Map<String, Object>> resultMaps = new ArrayList<>();
156-
for (String textOutput : textOutputs) {
157-
try {
158-
resultMaps.add(
159-
objectMapper.readValue(textOutput, new TypeReference<Map<String, Object>>() {}));
160-
} catch (JsonProcessingException e) {
161-
resultMaps.add(ImmutableMap.of("text", textOutput));
162-
}
163-
}
164-
return ImmutableMap.of("text_output", resultMaps);
114+
return objectMapper.convertValue(callResult, new TypeReference<Map<String, Object>>() {});
165115
}
166116
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.models;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import com.anthropic.client.AnthropicClient;
22+
import com.anthropic.models.messages.ContentBlockParam;
23+
import com.anthropic.models.messages.ToolResultBlockParam;
24+
import com.google.common.collect.ImmutableList;
25+
import com.google.common.collect.ImmutableMap;
26+
import com.google.genai.types.FunctionResponse;
27+
import com.google.genai.types.Part;
28+
import java.lang.reflect.Method;
29+
import java.util.Map;
30+
import org.junit.Before;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
import org.junit.runners.JUnit4;
34+
import org.mockito.Mockito;
35+
36+
@RunWith(JUnit4.class)
37+
public final class ClaudeTest {
38+
39+
private Claude claude;
40+
private Method partToAnthropicMessageBlockMethod;
41+
42+
@Before
43+
public void setUp() throws Exception {
44+
AnthropicClient mockClient = Mockito.mock(AnthropicClient.class);
45+
claude = new Claude("claude-3-opus", mockClient);
46+
47+
// Access private method for testing the extraction logic
48+
partToAnthropicMessageBlockMethod =
49+
Claude.class.getDeclaredMethod("partToAnthropicMessageBlock", Part.class);
50+
partToAnthropicMessageBlockMethod.setAccessible(true);
51+
}
52+
53+
@Test
54+
public void testPartToAnthropicMessageBlock_mcpNativeFormat() throws Exception {
55+
Map<String, Object> responseData =
56+
ImmutableMap.of(
57+
"content",
58+
ImmutableList.of(ImmutableMap.of("type", "text", "text", "Extracted native MCP text")));
59+
FunctionResponse funcParam =
60+
FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build();
61+
Part part = Part.builder().functionResponse(funcParam).build();
62+
63+
ContentBlockParam result =
64+
(ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part);
65+
66+
ToolResultBlockParam toolResult = result.asToolResult();
67+
assertThat(toolResult.content().get().asString()).isEqualTo("Extracted native MCP text");
68+
}
69+
70+
@Test
71+
public void testPartToAnthropicMessageBlock_legacyResultKey() throws Exception {
72+
Map<String, Object> responseData = ImmutableMap.of("result", "Legacy result text");
73+
FunctionResponse funcParam =
74+
FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build();
75+
Part part = Part.builder().functionResponse(funcParam).build();
76+
77+
ContentBlockParam result =
78+
(ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part);
79+
80+
ToolResultBlockParam toolResult = result.asToolResult();
81+
assertThat(toolResult.content().get().asString()).isEqualTo("Legacy result text");
82+
}
83+
84+
@Test
85+
public void testPartToAnthropicMessageBlock_jsonFallback() throws Exception {
86+
Map<String, Object> responseData = ImmutableMap.of("custom_key", "custom_value");
87+
FunctionResponse funcParam =
88+
FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build();
89+
Part part = Part.builder().functionResponse(funcParam).build();
90+
91+
ContentBlockParam result =
92+
(ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part);
93+
94+
ToolResultBlockParam toolResult = result.asToolResult();
95+
assertThat(toolResult.content().get().asString()).contains("\"custom_key\":\"custom_value\"");
96+
}
97+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.tools.mcp;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.google.common.collect.ImmutableList;
23+
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
24+
import io.modelcontextprotocol.spec.McpSchema.TextContent;
25+
import java.util.List;
26+
import java.util.Map;
27+
import org.junit.Before;
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
import org.junit.runners.JUnit4;
31+
32+
@RunWith(JUnit4.class)
33+
public final class AbstractMcpToolTest {
34+
35+
private ObjectMapper objectMapper;
36+
37+
@Before
38+
public void setUp() {
39+
objectMapper = new ObjectMapper();
40+
}
41+
42+
@Test
43+
public void testWrapCallResult_success() {
44+
CallToolResult result =
45+
CallToolResult.builder()
46+
.content(ImmutableList.of(new TextContent("success")))
47+
.isError(false)
48+
.build();
49+
50+
Map<String, Object> map = AbstractMcpTool.wrapCallResult(objectMapper, "my_tool", result);
51+
52+
assertThat(map).containsKey("content");
53+
List<?> content = (List<?>) map.get("content");
54+
assertThat(content).hasSize(1);
55+
56+
Map<?, ?> contentItem = (Map<?, ?>) content.get(0);
57+
assertThat(contentItem).containsEntry("type", "text");
58+
assertThat(contentItem).containsEntry("text", "success");
59+
60+
assertThat(map).containsEntry("isError", false);
61+
}
62+
}

0 commit comments

Comments
 (0)