5
5
package io .modelcontextprotocol .client ;
6
6
7
7
import java .time .Duration ;
8
+ import java .util .Map ;
9
+ import java .util .Optional ;
10
+ import java .util .concurrent .ConcurrentHashMap ;
8
11
9
12
import org .slf4j .Logger ;
10
13
import org .slf4j .LoggerFactory ;
11
14
15
+ import io .modelcontextprotocol .spec .JsonSchemaValidator ;
16
+ import io .modelcontextprotocol .spec .McpError ;
12
17
import io .modelcontextprotocol .spec .McpSchema ;
13
18
import io .modelcontextprotocol .spec .McpSchema .ClientCapabilities ;
14
19
import io .modelcontextprotocol .spec .McpSchema .GetPromptRequest ;
48
53
* @author Dariusz Jędrzejczyk
49
54
* @author Christian Tzolov
50
55
* @author Jihoon Kim
56
+ * @author Anurag Pant
51
57
* @see McpClient
52
58
* @see McpAsyncClient
53
59
* @see McpSchema
@@ -63,14 +69,23 @@ public class McpSyncClient implements AutoCloseable {
63
69
64
70
private final McpAsyncClient delegate ;
65
71
72
+ private final JsonSchemaValidator jsonSchemaValidator ;
73
+
74
+ /**
75
+ * Cached tool output schemas.
76
+ */
77
+ private final ConcurrentHashMap <String , Optional <Map <String , Object >>> toolsOutputSchemaCache ;
78
+
66
79
/**
67
80
* Create a new McpSyncClient with the given delegate.
68
81
* @param delegate the asynchronous kernel on top of which this synchronous client
69
82
* provides a blocking API.
70
83
*/
71
- McpSyncClient (McpAsyncClient delegate ) {
84
+ McpSyncClient (McpAsyncClient delegate , JsonSchemaValidator jsonSchemaValidator ) {
72
85
Assert .notNull (delegate , "The delegate can not be null" );
73
86
this .delegate = delegate ;
87
+ this .jsonSchemaValidator = jsonSchemaValidator ;
88
+ this .toolsOutputSchemaCache = new ConcurrentHashMap <>();
74
89
}
75
90
76
91
/**
@@ -216,7 +231,37 @@ public Object ping() {
216
231
* Boolean indicating if the execution failed (true) or succeeded (false/absent)
217
232
*/
218
233
public McpSchema .CallToolResult callTool (McpSchema .CallToolRequest callToolRequest ) {
219
- return this .delegate .callTool (callToolRequest ).block ();
234
+ if (!this .toolsOutputSchemaCache .containsKey (callToolRequest .name ())) {
235
+ listTools (); // Ensure tools are cached before calling
236
+ }
237
+
238
+ McpSchema .CallToolResult result = this .delegate .callTool (callToolRequest ).block ();
239
+ Optional <Map <String , Object >> optOutputSchema = toolsOutputSchemaCache .get (callToolRequest .name ());
240
+
241
+ if (result != null && result .isError () != null && !result .isError ()) {
242
+ if (optOutputSchema == null ) {
243
+ // Should not be triggered but added for completeness
244
+ throw new McpError ("Tool with name '" + callToolRequest .name () + "' not found" );
245
+ }
246
+ else {
247
+ if (optOutputSchema .isPresent ()) {
248
+ // Validate the tool output against the cached output schema
249
+ var validation = this .jsonSchemaValidator .validate (optOutputSchema .get (),
250
+ result .structuredContent ());
251
+ if (!validation .valid ()) {
252
+ throw new McpError ("Tool call result validation failed: " + validation .errorMessage ());
253
+ }
254
+ }
255
+ else if (result .structuredContent () != null ) {
256
+ logger .warn (
257
+ "Calling a tool with no outputSchema is not expected to return result with structured content, but got: {}" ,
258
+ result .structuredContent ());
259
+ }
260
+
261
+ }
262
+ }
263
+
264
+ return result ;
220
265
}
221
266
222
267
/**
@@ -226,7 +271,14 @@ public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest callToolReque
226
271
* pagination if more tools are available
227
272
*/
228
273
public McpSchema .ListToolsResult listTools () {
229
- return this .delegate .listTools ().block ();
274
+ return this .delegate .listTools ().doOnNext (result -> {
275
+ if (result .tools () != null ) {
276
+ // Cache tools output schema
277
+ result .tools ()
278
+ .forEach (tool -> this .toolsOutputSchemaCache .put (tool .name (),
279
+ Optional .ofNullable (tool .outputSchema ())));
280
+ }
281
+ }).block ();
230
282
}
231
283
232
284
/**
@@ -237,7 +289,14 @@ public McpSchema.ListToolsResult listTools() {
237
289
* pagination if more tools are available
238
290
*/
239
291
public McpSchema .ListToolsResult listTools (String cursor ) {
240
- return this .delegate .listTools (cursor ).block ();
292
+ return this .delegate .listTools (cursor ).doOnNext (result -> {
293
+ if (result .tools () != null ) {
294
+ // Cache tools output schema
295
+ result .tools ()
296
+ .forEach (tool -> this .toolsOutputSchemaCache .put (tool .name (),
297
+ Optional .ofNullable (tool .outputSchema ())));
298
+ }
299
+ }).block ();
241
300
}
242
301
243
302
// --------------------------
0 commit comments