5
5
package io .modelcontextprotocol .client ;
6
6
7
7
import java .time .Duration ;
8
+ import java .util .HashMap ;
9
+ import java .util .Set ;
8
10
11
+ import org .slf4j .Logger ;
12
+ import org .slf4j .LoggerFactory ;
13
+
14
+ import com .fasterxml .jackson .core .JsonProcessingException ;
15
+ import com .fasterxml .jackson .databind .JsonNode ;
16
+ import com .fasterxml .jackson .databind .ObjectMapper ;
17
+ import com .fasterxml .jackson .databind .node .ObjectNode ;
18
+ import com .networknt .schema .JsonSchema ;
19
+ import com .networknt .schema .JsonSchemaFactory ;
20
+ import com .networknt .schema .SpecVersion ;
21
+ import com .networknt .schema .ValidationMessage ;
22
+
23
+ import io .modelcontextprotocol .spec .McpError ;
9
24
import io .modelcontextprotocol .spec .McpSchema ;
10
25
import io .modelcontextprotocol .spec .McpSchema .ClientCapabilities ;
11
26
import io .modelcontextprotocol .spec .McpSchema .GetPromptRequest ;
12
27
import io .modelcontextprotocol .spec .McpSchema .GetPromptResult ;
13
28
import io .modelcontextprotocol .spec .McpSchema .ListPromptsResult ;
14
29
import io .modelcontextprotocol .util .Assert ;
15
- import org .slf4j .Logger ;
16
- import org .slf4j .LoggerFactory ;
17
30
18
31
/**
19
32
* A synchronous client implementation for the Model Context Protocol (MCP) that wraps an
@@ -55,6 +68,8 @@ public class McpSyncClient implements AutoCloseable {
55
68
56
69
private static final Logger logger = LoggerFactory .getLogger (McpSyncClient .class );
57
70
71
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper ();
72
+
58
73
// TODO: Consider providing a client config to set this properly
59
74
// this is currently a concern only because AutoCloseable is used - perhaps it
60
75
// is not a requirement?
@@ -206,7 +221,8 @@ public Object ping() {
206
221
/**
207
222
* Calls a tool provided by the server. Tools enable servers to expose executable
208
223
* functionality that can interact with external systems, perform computations, and
209
- * take actions in the real world.
224
+ * take actions in the real world. If tool contains an output schema, validates the
225
+ * tool result structured content against the output schema.
210
226
* @param callToolRequest The request containing: - name: The name of the tool to call
211
227
* (must match a tool name from tools/list) - arguments: Arguments that conform to the
212
228
* tool's input schema
@@ -215,7 +231,53 @@ public Object ping() {
215
231
* Boolean indicating if the execution failed (true) or succeeded (false/absent)
216
232
*/
217
233
public McpSchema .CallToolResult callTool (McpSchema .CallToolRequest callToolRequest ) {
218
- return this .delegate .callTool (callToolRequest ).block ();
234
+ McpSchema .CallToolResult result = this .delegate .callTool (callToolRequest ).block ();
235
+ HashMap <String , McpSchema .JsonSchema > toolsOutputSchemaCache = this .delegate .getToolsOutputSchemaCache ();
236
+ // Should not be triggered but added for completeness
237
+ if (!toolsOutputSchemaCache .containsKey (callToolRequest .name ())) {
238
+ throw new McpError ("Tool with name '" + callToolRequest .name () + "' not found" );
239
+ }
240
+ if (result != null && toolsOutputSchemaCache .get (callToolRequest .name ()) != null ) {
241
+ if (result .structuredContent () == null ) {
242
+ throw new McpError ("CallToolResult validation failed: structuredContent is null and "
243
+ + "does not match tool outputSchema." );
244
+ }
245
+
246
+ McpSchema .JsonSchema outputSchema = toolsOutputSchemaCache .get (callToolRequest .name ());
247
+
248
+ try {
249
+ // Convert outputSchema to string
250
+ String outputSchemaString = OBJECT_MAPPER .writeValueAsString (outputSchema );
251
+
252
+ // Create JsonSchema validator
253
+ ObjectNode schemaNode = (ObjectNode ) OBJECT_MAPPER .readTree (outputSchemaString );
254
+ // Set additional properties to false if not specified in output schema
255
+ if (!schemaNode .has ("additionalProperties" )) {
256
+ schemaNode .put ("additionalProperties" , false );
257
+ }
258
+ JsonSchema schema = JsonSchemaFactory .getInstance (SpecVersion .VersionFlag .V202012 )
259
+ .getSchema (schemaNode );
260
+
261
+ // Convert structured content in reult to JsonNode
262
+ JsonNode jsonNode = OBJECT_MAPPER .valueToTree (result .structuredContent ());
263
+
264
+ // Validate outputSchema against structuredContent
265
+ Set <ValidationMessage > validationResult = schema .validate (jsonNode );
266
+
267
+ // Check if validation passed
268
+ if (!validationResult .isEmpty ()) {
269
+ // Handle validation errors
270
+ throw new McpError (
271
+ "CallToolResult validation failed: structuredContent does not match tool outputSchema." );
272
+ }
273
+ }
274
+ catch (JsonProcessingException e ) {
275
+ // Log error if output schema can't be parsed to prevent erroring out for
276
+ // successful call tool request
277
+ logger .error ("Encountered exception when parsing outputSchema: {}" , e );
278
+ }
279
+ }
280
+ return result ;
219
281
}
220
282
221
283
/**
@@ -353,4 +415,8 @@ public McpSchema.CompleteResult completeCompletion(McpSchema.CompleteRequest com
353
415
return this .delegate .completeCompletion (completeRequest ).block ();
354
416
}
355
417
418
+ private void isStrict (boolean b ) {
419
+ throw new UnsupportedOperationException ("Not supported yet." );
420
+ }
421
+
356
422
}
0 commit comments