4
4
5
5
package io .modelcontextprotocol .server ;
6
6
7
- import static net .javacrumbs .jsonunit .assertj .JsonAssertions .assertThatJson ;
8
- import static net .javacrumbs .jsonunit .assertj .JsonAssertions .json ;
9
- import static org .assertj .core .api .Assertions .assertThat ;
10
- import static org .assertj .core .api .Assertions .assertThatExceptionOfType ;
11
- import static org .assertj .core .api .Assertions .assertWith ;
12
- import static org .awaitility .Awaitility .await ;
13
- import static org .mockito .Mockito .mock ;
14
-
15
- import io .modelcontextprotocol .common .McpTransportContext ;
16
7
import java .net .URI ;
17
8
import java .net .http .HttpClient ;
18
9
import java .net .http .HttpRequest ;
30
21
import java .util .function .Function ;
31
22
import java .util .stream .Collectors ;
32
23
33
- import org .junit .jupiter .params .ParameterizedTest ;
34
- import org .junit .jupiter .params .provider .ValueSource ;
35
-
36
24
import io .modelcontextprotocol .client .McpClient ;
25
+ import io .modelcontextprotocol .common .McpTransportContext ;
37
26
import io .modelcontextprotocol .spec .McpError ;
38
27
import io .modelcontextprotocol .spec .McpSchema ;
39
28
import io .modelcontextprotocol .spec .McpSchema .CallToolResult ;
55
44
import io .modelcontextprotocol .spec .McpSchema .Tool ;
56
45
import io .modelcontextprotocol .util .Utils ;
57
46
import net .javacrumbs .jsonunit .core .Option ;
47
+ import org .junit .jupiter .params .ParameterizedTest ;
48
+ import org .junit .jupiter .params .provider .ValueSource ;
58
49
import reactor .core .publisher .Mono ;
59
50
import reactor .test .StepVerifier ;
60
51
52
+ import static net .javacrumbs .jsonunit .assertj .JsonAssertions .assertThatJson ;
53
+ import static net .javacrumbs .jsonunit .assertj .JsonAssertions .json ;
54
+ import static org .assertj .core .api .Assertions .assertThat ;
55
+ import static org .assertj .core .api .Assertions .assertWith ;
56
+ import static org .awaitility .Awaitility .await ;
57
+ import static org .mockito .Mockito .mock ;
58
+
61
59
public abstract class AbstractMcpClientServerIntegrationTests {
62
60
63
61
protected ConcurrentHashMap <String , McpClient .SyncSpec > clientBuilders = new ConcurrentHashMap <>();
@@ -104,8 +102,8 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) {
104
102
McpServerFeatures .AsyncToolSpecification tool = McpServerFeatures .AsyncToolSpecification .builder ()
105
103
.tool (Tool .builder ().name ("tool1" ).description ("tool1 description" ).inputSchema (emptyJsonSchema ).build ())
106
104
.callHandler ((exchange , request ) -> {
107
- exchange .createMessage (mock (McpSchema .CreateMessageRequest .class )). block ();
108
- return Mono .just (mock (CallToolResult .class ));
105
+ return exchange .createMessage (mock (McpSchema .CreateMessageRequest .class ))
106
+ . then ( Mono .just (mock (CallToolResult .class ) ));
109
107
})
110
108
.build ();
111
109
@@ -118,13 +116,15 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) {
118
116
119
117
assertThat (client .initialize ()).isNotNull ();
120
118
121
- try {
122
- client .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ()));
123
- }
124
- catch (McpError e ) {
125
- assertThat (e ).isInstanceOf (McpError .class )
126
- .hasMessage ("Client must be configured with sampling capabilities" );
127
- }
119
+ McpSchema .CallToolResult callToolResult = client .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ()));
120
+
121
+ // Tool errors should be reported within the result object, not as MCP
122
+ // protocol-level errors. This allows the LLM to see and potentially
123
+ // handle the error.
124
+ assertThat (callToolResult ).isNotNull ();
125
+ assertThat (callToolResult .isError ()).isTrue ();
126
+ assertThat (callToolResult .content ()).containsExactly (new McpSchema .TextContent (
127
+ "Error calling tool: Client must be configured with sampling capabilities" ));
128
128
}
129
129
finally {
130
130
server .closeGracefully ().block ();
@@ -334,9 +334,16 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt
334
334
InitializeResult initResult = mcpClient .initialize ();
335
335
assertThat (initResult ).isNotNull ();
336
336
337
- assertThatExceptionOfType (McpError .class ).isThrownBy (() -> {
338
- mcpClient .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ()));
339
- }).withMessageContaining ("1000ms" );
337
+ McpSchema .CallToolResult callToolResult = mcpClient
338
+ .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ()));
339
+
340
+ // Tool errors should be reported within the result object, not as MCP
341
+ // protocol-level errors. This allows the LLM to see and potentially
342
+ // handle the error.
343
+ assertThat (callToolResult ).isNotNull ();
344
+ assertThat (callToolResult .isError ()).isTrue ();
345
+ assertThat (callToolResult .content ()).containsExactly (new McpSchema .TextContent (
346
+ "Error calling tool: Did not observe any item or terminal signal within 1000ms in 'source(MonoCreate)' (and no fallback has been configured)" ));
340
347
}
341
348
finally {
342
349
mcpServer .closeGracefully ().block ();
@@ -552,9 +559,16 @@ void testCreateElicitationWithRequestTimeoutFail(String clientType) {
552
559
InitializeResult initResult = mcpClient .initialize ();
553
560
assertThat (initResult ).isNotNull ();
554
561
555
- assertThatExceptionOfType (McpError .class ).isThrownBy (() -> {
556
- mcpClient .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ()));
557
- }).withMessageContaining ("within 1000ms" );
562
+ McpSchema .CallToolResult callToolResult = mcpClient
563
+ .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ()));
564
+
565
+ // Tool errors should be reported within the result object, not as MCP
566
+ // protocol-level errors. This allows the LLM to see and potentially
567
+ // handle the error.
568
+ assertThat (callToolResult ).isNotNull ();
569
+ assertThat (callToolResult .isError ()).isTrue ();
570
+ assertThat (callToolResult .content ()).containsExactly (new McpSchema .TextContent (
571
+ "Error calling tool: Did not observe any item or terminal signal within 1000ms in 'source(MonoCreate)' (and no fallback has been configured)" ));
558
572
559
573
ElicitResult elicitResult = resultRef .get ();
560
574
assertThat (elicitResult ).isNull ();
@@ -838,12 +852,16 @@ void testThrowingToolCallIsCaughtBeforeTimeout(String clientType) {
838
852
InitializeResult initResult = mcpClient .initialize ();
839
853
assertThat (initResult ).isNotNull ();
840
854
841
- // We expect the tool call to fail immediately with the exception raised by
842
- // the offending tool
843
- // instead of getting back a timeout.
844
- assertThatExceptionOfType (McpError .class )
845
- .isThrownBy (() -> mcpClient .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ())))
846
- .withMessageContaining ("Timeout on blocking read" );
855
+ McpSchema .CallToolResult callToolResult = mcpClient
856
+ .callTool (new McpSchema .CallToolRequest ("tool1" , Map .of ()));
857
+
858
+ // Tool errors should be reported within the result object, not as MCP
859
+ // protocol-level errors. This allows the LLM to see and potentially
860
+ // handle the error.
861
+ assertThat (callToolResult ).isNotNull ();
862
+ assertThat (callToolResult .isError ()).isTrue ();
863
+ assertThat (callToolResult .content ()).containsExactly (new McpSchema .TextContent (
864
+ "Error calling tool: Timeout on blocking read for 1000000000 NANOSECONDS" ));
847
865
}
848
866
finally {
849
867
mcpServer .closeGracefully ();
0 commit comments