1212import java .util .HashMap ;
1313import java .util .List ;
1414import java .util .Map ;
15+ import java .util .Optional ;
1516import java .util .concurrent .ConcurrentHashMap ;
1617import java .util .function .Function ;
1718
1819import org .slf4j .Logger ;
1920import org .slf4j .LoggerFactory ;
2021
2122import io .modelcontextprotocol .json .TypeRef ;
22-
23+ import io . modelcontextprotocol . json . schema . JsonSchemaValidator ;
2324import io .modelcontextprotocol .spec .McpClientSession ;
2425import io .modelcontextprotocol .spec .McpClientTransport ;
26+ import io .modelcontextprotocol .spec .McpError ;
2527import io .modelcontextprotocol .spec .McpSchema ;
2628import io .modelcontextprotocol .spec .McpSchema .ClientCapabilities ;
2729import io .modelcontextprotocol .spec .McpSchema .CreateMessageRequest ;
7577 * @author Dariusz Jędrzejczyk
7678 * @author Christian Tzolov
7779 * @author Jihoon Kim
80+ * @author Anurag Pant
7881 * @see McpClient
7982 * @see McpSchema
8083 * @see McpClientSession
@@ -152,16 +155,33 @@ public class McpAsyncClient {
152155 */
153156 private final LifecycleInitializer initializer ;
154157
158+ /**
159+ * JSON schema validator to use for validating tool responses against output schemas.
160+ */
161+ private final JsonSchemaValidator jsonSchemaValidator ;
162+
163+ /**
164+ * Cached tool output schemas.
165+ */
166+ private final ConcurrentHashMap <String , Optional <Map <String , Object >>> toolsOutputSchemaCache ;
167+
168+ /**
169+ * Whether to enable automatic schema caching during callTool operations.
170+ */
171+ private final boolean enableCallToolSchemaCaching ;
172+
155173 /**
156174 * Create a new McpAsyncClient with the given transport and session request-response
157175 * timeout.
158176 * @param transport the transport to use.
159177 * @param requestTimeout the session request-response timeout.
160178 * @param initializationTimeout the max timeout to await for the client-server
161- * @param features the MCP Client supported features.
179+ * @param jsonSchemaValidator the JSON schema validator to use for validating tool
180+ * @param features the MCP Client supported features. responses against output
181+ * schemas.
162182 */
163183 McpAsyncClient (McpClientTransport transport , Duration requestTimeout , Duration initializationTimeout ,
164- McpClientFeatures .Async features ) {
184+ JsonSchemaValidator jsonSchemaValidator , McpClientFeatures .Async features ) {
165185
166186 Assert .notNull (transport , "Transport must not be null" );
167187 Assert .notNull (requestTimeout , "Request timeout must not be null" );
@@ -171,6 +191,9 @@ public class McpAsyncClient {
171191 this .clientCapabilities = features .clientCapabilities ();
172192 this .transport = transport ;
173193 this .roots = new ConcurrentHashMap <>(features .roots ());
194+ this .jsonSchemaValidator = jsonSchemaValidator ;
195+ this .toolsOutputSchemaCache = new ConcurrentHashMap <>();
196+ this .enableCallToolSchemaCaching = features .enableCallToolSchemaCaching ();
174197
175198 // Request Handlers
176199 Map <String , RequestHandler <?>> requestHandlers = new HashMap <>();
@@ -539,15 +562,61 @@ private RequestHandler<ElicitResult> elicitationCreateHandler() {
539562 * @see #listTools()
540563 */
541564 public Mono <McpSchema .CallToolResult > callTool (McpSchema .CallToolRequest callToolRequest ) {
542- return this .initializer .withIntitialization ("calling tools" , init -> {
543- if (init .initializeResult ().capabilities ().tools () == null ) {
544- return Mono .error (new IllegalStateException ("Server does not provide tools capability" ));
545- }
546- return init .mcpSession ()
547- .sendRequest (McpSchema .METHOD_TOOLS_CALL , callToolRequest , CALL_TOOL_RESULT_TYPE_REF );
565+ return Mono .defer (() -> {
566+ // Conditionally cache schemas if needed, otherwise return empty Mono
567+ Mono <Void > cachingStep = (this .enableCallToolSchemaCaching
568+ && !this .toolsOutputSchemaCache .containsKey (callToolRequest .name ())) ? this .listTools ().then ()
569+ : Mono .empty ();
570+
571+ return cachingStep .then (this .initializer .withIntitialization ("calling tool" , init -> {
572+ if (init .initializeResult ().capabilities ().tools () == null ) {
573+ return Mono .error (new IllegalStateException ("Server does not provide tools capability" ));
574+ }
575+
576+ return init .mcpSession ()
577+ .sendRequest (McpSchema .METHOD_TOOLS_CALL , callToolRequest , CALL_TOOL_RESULT_TYPE_REF )
578+ .flatMap (result -> validateToolResult (callToolRequest .name (), result ));
579+ }));
548580 });
549581 }
550582
583+ /**
584+ * Calls a tool provided by the server and validates the result against the cached
585+ * output schema.
586+ * @param toolName The name of the tool to call
587+ * @param result The result of the tool call
588+ * @return A Mono that emits the validated tool result
589+ */
590+ private Mono <McpSchema .CallToolResult > validateToolResult (String toolName , McpSchema .CallToolResult result ) {
591+ Optional <Map <String , Object >> optOutputSchema = toolsOutputSchemaCache .get (toolName );
592+
593+ if (result != null && result .isError () != null && !result .isError ()) {
594+ if (optOutputSchema == null ) {
595+ // Tool not found in cache - skip validation and proceed
596+ logger .debug ("Tool '{}' not found in cache, skipping validation" , toolName );
597+ return Mono .just (result );
598+ }
599+ else {
600+ if (optOutputSchema .isPresent ()) {
601+ // Validate the tool output against the cached output schema
602+ var validation = this .jsonSchemaValidator .validate (optOutputSchema .get (),
603+ result .structuredContent ());
604+ if (!validation .valid ()) {
605+ logger .warn ("Tool call result validation failed: {}" , validation .errorMessage ());
606+ return Mono .just (new McpSchema .CallToolResult (validation .errorMessage (), true ));
607+ }
608+ }
609+ else if (result .structuredContent () != null ) {
610+ logger .warn (
611+ "Calling a tool with no outputSchema is not expected to return result with structured content, but got: {}" ,
612+ result .structuredContent ());
613+ }
614+ }
615+ }
616+
617+ return Mono .just (result );
618+ }
619+
551620 /**
552621 * Retrieves the list of all tools provided by the server.
553622 * @return A Mono that emits the list of all tools result
@@ -574,7 +643,16 @@ public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
574643 }
575644 return init .mcpSession ()
576645 .sendRequest (McpSchema .METHOD_TOOLS_LIST , new McpSchema .PaginatedRequest (cursor ),
577- LIST_TOOLS_RESULT_TYPE_REF );
646+ LIST_TOOLS_RESULT_TYPE_REF )
647+ .map (result -> {
648+ if (result .tools () != null ) {
649+ // Cache tools output schema
650+ result .tools ()
651+ .forEach (tool -> this .toolsOutputSchemaCache .put (tool .name (),
652+ Optional .ofNullable (tool .outputSchema ())));
653+ }
654+ return result ;
655+ });
578656 });
579657 }
580658
0 commit comments