Repository: langchain4j/langchain4j (language: java)

Issue:
[BUG] MCP Tool execution error handling is  not correct
Tool execution errors are not properly returned.

In the class ToolExecutionHelper, there is 2 issues:
* If the reply contains an `error` element, the message and code are swallowed and not returned.
* If the reply contains a `content` element, there is no check done for `isError`. This should be exposed in the reply.

**Describe the bug**
Tool execution errors are not properly returned.

**Log and Stack trace**

**To Reproduce**
Call a tool that reply with the 2 kind of errors.

**Expected behavior**

* If there is an `error` element to have the following returned value: `There was an error executing the tool. Message: actual message, code: actual code`

* If there is an `isError` field to have the following reurned value: `There was an error executing the tool. content text`

**Please complete the following information:**
- LangChain4j version: 1.0.0.beta1
- LLM(s) used:  Any
- Java version: JDK21
- Spring Boot version (if applicable): 

**Additional context**



Discussion / hints:

Fix patch:
diff --git a/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolExecutionHelper.java b/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolExecutionHelper.java
index c09449f1320..04f973c10b1 100644
--- a/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolExecutionHelper.java
+++ b/langchain4j-mcp/src/main/java/dev/langchain4j/mcp/client/ToolExecutionHelper.java
@@ -20,7 +20,15 @@ static String extractResult(JsonNode result) {
         if (result.has("result")) {
             JsonNode resultNode = result.get("result");
             if (resultNode.has("content")) {
-                return extractSuccessfulResult((ArrayNode) resultNode.get("content"));
+                String content = extractSuccessfulResult((ArrayNode) resultNode.get("content"));
+                boolean isError = false;
+                if (resultNode.has("isError")) {
+                    isError = resultNode.get("isError").asBoolean();
+                }
+                if (isError) {
+                    content = String.format(EXECUTION_ERROR_MESSAGE + ". The tool returned: %s", content);
+                }
+                return content;
             } else {
                 log.warn("Result does not contain 'content' element: {}", result);
                 return EXECUTION_ERROR_MESSAGE;
@@ -56,6 +64,6 @@ private static String extractError(JsonNode errorNode) {
             errorCode = errorNode.get("code").asInt();
         }
         log.warn("Result contains an error: {}, code: {}", errorMessage, errorCode);
-        return EXECUTION_ERROR_MESSAGE;
+        return String.format(EXECUTION_ERROR_MESSAGE + ". Message: %s. Code: %s", errorMessage, errorCode);
     }
 }


Test patch:
diff --git a/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpToolsTestBase.java b/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpToolsTestBase.java
index a06f330d721..ae47e6ae7df 100644
--- a/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpToolsTestBase.java
+++ b/langchain4j-mcp/src/test/java/dev/langchain4j/mcp/client/integration/McpToolsTestBase.java
@@ -29,7 +29,7 @@ public void verifyToolSpecifications() {
         ToolProviderResult toolProviderResult = obtainTools();
 
         Map<ToolSpecification, ToolExecutor> tools = toolProviderResult.tools();
-        assertThat(tools).hasSize(5);
+        assertThat(tools).hasSize(6);
 
         ToolSpecification echoString = findToolSpecificationByName(toolProviderResult, "echoString");
         assertThat(echoString.description()).isEqualTo("Echoes a string");
@@ -56,6 +56,10 @@ public void verifyToolSpecifications() {
         ToolSpecification error = findToolSpecificationByName(toolProviderResult, "error");
         assertThat(error.description()).isEqualTo("Throws a business error");
         assertThat(error.parameters().properties()).isEmpty();
+
+        ToolSpecification errorResponse = findToolSpecificationByName(toolProviderResult, "errorResponse");
+        assertThat(errorResponse.description()).isEqualTo("Returns a response as an error");
+        assertThat(errorResponse.parameters().properties()).isEmpty();
     }
 
     @Test
@@ -79,7 +83,8 @@ public void executeToolWithWrongArgumentType() {
                 .arguments("{\"input\": 1}") // wrong argument type
                 .build();
         String toolExecutionResultString = executor.execute(toolExecutionRequest, null);
-        assertThat(toolExecutionResultString).isEqualTo("There was an error executing the tool");
+        assertThat(toolExecutionResultString)
+                .isEqualTo("There was an error executing the tool. Message: Internal error. Code: -32603");
     }
 
     @Test
@@ -91,7 +96,9 @@ public void executeNonExistentTool() {
                 .arguments("{\"input\": 1}")
                 .build();
         String toolExecutionResultString = executor.execute(toolExecutionRequest, null);
-        assertThat(toolExecutionResultString).isEqualTo("There was an error executing the tool");
+        assertThat(toolExecutionResultString)
+                .isEqualTo("There was an error executing the tool. "
+                        + "Message: Invalid tool name: THIS-TOOL-DOES-NOT-EXIST. Code: -32602");
     }
 
     @Test
@@ -101,7 +108,21 @@ public void executeToolThatThrowsBusinessError() {
         ToolExecutionRequest toolExecutionRequest =
                 ToolExecutionRequest.builder().name("error").arguments("{}").build();
         String toolExecutionResultString = executor.execute(toolExecutionRequest, null);
-        assertThat(toolExecutionResultString).isEqualTo("There was an error executing the tool");
+        assertThat(toolExecutionResultString)
+                .isEqualTo("There was an error executing the tool. Message: Internal error. Code: -32603");
+    }
+
+    @Test
+    public void executeToolThatReturnsError() {
+        ToolProviderResult toolProviderResult = obtainTools();
+        ToolExecutor executor = findToolExecutorByName(toolProviderResult, "errorResponse");
+        ToolExecutionRequest toolExecutionRequest = ToolExecutionRequest.builder()
+                .name("errorResponse")
+                .arguments("{}")
+                .build();
+        String toolExecutionResultString = executor.execute(toolExecutionRequest, null);
+        assertThat(toolExecutionResultString)
+                .isEqualTo("There was an error executing the tool. The tool returned: This is an actual error");
     }
 
     @Test
diff --git a/langchain4j-mcp/src/test/resources/tools_mcp_server.java b/langchain4j-mcp/src/test/resources/tools_mcp_server.java
index a2a32ce8875..2b0eac0b8c3 100644
--- a/langchain4j-mcp/src/test/resources/tools_mcp_server.java
+++ b/langchain4j-mcp/src/test/resources/tools_mcp_server.java
@@ -3,12 +3,14 @@
 //DEPS io.quarkiverse.mcp:quarkus-mcp-server-stdio:1.0.0.Alpha6
 //DEPS io.quarkiverse.mcp:quarkus-mcp-server-sse:1.0.0.Alpha6
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import io.quarkiverse.mcp.server.TextContent;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolResponse;
 
 public class tools_mcp_server {
 
@@ -37,5 +39,12 @@ public String longOperation() throws Exception {
     public String error() throws Exception {
         throw new RuntimeException("business error");
     }
+    
+    @Tool(description = "Returns a response as an error")
+    public ToolResponse errorResponse() throws Exception {
+        List<TextContent> lst = new ArrayList<>();
+        lst.add(new TextContent("This is an actual error"));
+        return new ToolResponse(true, lst);
+    }
 
 }