44
55package io .modelcontextprotocol .server ;
66
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 ;
167import java .net .URI ;
178import java .net .http .HttpClient ;
189import java .net .http .HttpRequest ;
3021import java .util .function .Function ;
3122import java .util .stream .Collectors ;
3223
33- import org .junit .jupiter .params .ParameterizedTest ;
34- import org .junit .jupiter .params .provider .ValueSource ;
35-
3624import io .modelcontextprotocol .client .McpClient ;
25+ import io .modelcontextprotocol .common .McpTransportContext ;
3726import io .modelcontextprotocol .spec .McpError ;
3827import io .modelcontextprotocol .spec .McpSchema ;
3928import io .modelcontextprotocol .spec .McpSchema .CallToolResult ;
5544import io .modelcontextprotocol .spec .McpSchema .Tool ;
5645import io .modelcontextprotocol .util .Utils ;
5746import net .javacrumbs .jsonunit .core .Option ;
47+ import org .junit .jupiter .params .ParameterizedTest ;
48+ import org .junit .jupiter .params .provider .ValueSource ;
5849import reactor .core .publisher .Mono ;
5950import reactor .test .StepVerifier ;
6051
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+
6159public abstract class AbstractMcpClientServerIntegrationTests {
6260
6361 protected ConcurrentHashMap <String , McpClient .SyncSpec > clientBuilders = new ConcurrentHashMap <>();
@@ -104,8 +102,8 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) {
104102 McpServerFeatures .AsyncToolSpecification tool = McpServerFeatures .AsyncToolSpecification .builder ()
105103 .tool (Tool .builder ().name ("tool1" ).description ("tool1 description" ).inputSchema (emptyJsonSchema ).build ())
106104 .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 ) ));
109107 })
110108 .build ();
111109
@@ -118,13 +116,15 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) {
118116
119117 assertThat (client .initialize ()).isNotNull ();
120118
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" ));
128128 }
129129 finally {
130130 server .closeGracefully ().block ();
@@ -334,9 +334,16 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt
334334 InitializeResult initResult = mcpClient .initialize ();
335335 assertThat (initResult ).isNotNull ();
336336
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)" ));
340347 }
341348 finally {
342349 mcpServer .closeGracefully ().block ();
@@ -552,9 +559,16 @@ void testCreateElicitationWithRequestTimeoutFail(String clientType) {
552559 InitializeResult initResult = mcpClient .initialize ();
553560 assertThat (initResult ).isNotNull ();
554561
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)" ));
558572
559573 ElicitResult elicitResult = resultRef .get ();
560574 assertThat (elicitResult ).isNull ();
@@ -838,12 +852,16 @@ void testThrowingToolCallIsCaughtBeforeTimeout(String clientType) {
838852 InitializeResult initResult = mcpClient .initialize ();
839853 assertThat (initResult ).isNotNull ();
840854
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" ));
847865 }
848866 finally {
849867 mcpServer .closeGracefully ();
0 commit comments