14
14
import java .util .Map ;
15
15
import java .util .concurrent .ConcurrentHashMap ;
16
16
import java .util .function .Function ;
17
+ import java .util .stream .Collectors ;
17
18
18
19
import org .slf4j .Logger ;
19
20
import org .slf4j .LoggerFactory ;
22
23
23
24
import io .modelcontextprotocol .spec .McpClientSession ;
24
25
import io .modelcontextprotocol .spec .McpClientTransport ;
25
- import io .modelcontextprotocol .spec .McpError ;
26
26
import io .modelcontextprotocol .spec .McpSchema ;
27
27
import io .modelcontextprotocol .spec .McpSchema .ClientCapabilities ;
28
28
import io .modelcontextprotocol .spec .McpSchema .CreateMessageRequest ;
@@ -153,6 +153,15 @@ public class McpAsyncClient {
153
153
*/
154
154
private final LifecycleInitializer initializer ;
155
155
156
+ /**
157
+ * MCP provides a standardized way for servers to request tool invocation from
158
+ * clients. This flow allows clients to maintain control over tool access, selection,
159
+ * and permissions while enabling servers to leverage AI capabilities—with no server
160
+ * API keys necessary. Servers can request tool invocation with optional tool
161
+ * annotations to provide additional context to the tool.
162
+ */
163
+ private Function <String , McpSchema .ToolAnnotations > toolAnnotationsHandler ;
164
+
156
165
/**
157
166
* Create a new McpAsyncClient with the given transport and session request-response
158
167
* timeout.
@@ -162,8 +171,7 @@ public class McpAsyncClient {
162
171
* @param features the MCP Client supported features.
163
172
*/
164
173
McpAsyncClient (McpClientTransport transport , Duration requestTimeout , Duration initializationTimeout ,
165
- McpClientFeatures .Async features ) {
166
-
174
+ McpClientFeatures .Async features , Function <String , McpSchema .ToolAnnotations > toolAnnotationsHandler ) {
167
175
Assert .notNull (transport , "Transport must not be null" );
168
176
Assert .notNull (requestTimeout , "Request timeout must not be null" );
169
177
Assert .notNull (initializationTimeout , "Initialization timeout must not be null" );
@@ -172,7 +180,7 @@ public class McpAsyncClient {
172
180
this .clientCapabilities = features .clientCapabilities ();
173
181
this .transport = transport ;
174
182
this .roots = new ConcurrentHashMap <>(features .roots ());
175
-
183
+ this . toolAnnotationsHandler = toolAnnotationsHandler != null ? toolAnnotationsHandler : tool -> null ;
176
184
// Request Handlers
177
185
Map <String , RequestHandler <?>> requestHandlers = new HashMap <>();
178
186
@@ -575,10 +583,48 @@ public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
575
583
}
576
584
return init .mcpSession ()
577
585
.sendRequest (McpSchema .METHOD_TOOLS_LIST , new McpSchema .PaginatedRequest (cursor ),
578
- LIST_TOOLS_RESULT_TYPE_REF );
586
+ LIST_TOOLS_RESULT_TYPE_REF )
587
+ .map (this ::mergeToolsAnnotations );
579
588
});
580
589
}
581
590
591
+ private McpSchema .ListToolsResult mergeToolsAnnotations (McpSchema .ListToolsResult listToolsResult ) {
592
+ if (listToolsResult == null || listToolsResult .tools () == null || listToolsResult .tools ().isEmpty ()) {
593
+ return listToolsResult ;
594
+ }
595
+
596
+ List <McpSchema .Tool > mergedTools = listToolsResult .tools ().stream ().map (tool -> {
597
+ McpSchema .ToolAnnotations clientAnnoProperties = this .toolAnnotationsHandler .apply (tool .name ());
598
+ if (clientAnnoProperties == null ) {
599
+ return tool ; // no update needed
600
+ }
601
+ McpSchema .ToolAnnotations mergedAnno = mergeAnnotations (tool .annotations (), clientAnnoProperties );
602
+ return McpSchema .Tool .builder ()
603
+ .name (tool .name ())
604
+ .title (tool .title ())
605
+ .description (tool .description ())
606
+ .inputSchema (tool .inputSchema ())
607
+ .outputSchema (tool .outputSchema ())
608
+ .annotations (mergedAnno )
609
+ .meta (tool .meta ())
610
+ .build ();
611
+ }).collect (Collectors .toList ());
612
+ return new McpSchema .ListToolsResult (mergedTools , listToolsResult .nextCursor (), listToolsResult .meta ());
613
+ }
614
+
615
+ private McpSchema .ToolAnnotations mergeAnnotations (McpSchema .ToolAnnotations remoteAnnotations ,
616
+ McpSchema .ToolAnnotations clientAnnotations ) {
617
+ if (remoteAnnotations == null ) {
618
+ return clientAnnotations ;
619
+ }
620
+ return new McpSchema .ToolAnnotations (Utils .preferFirst (clientAnnotations .title (), remoteAnnotations .title ()),
621
+ Utils .mergeBoolean (clientAnnotations .readOnlyHint (), remoteAnnotations .readOnlyHint ()),
622
+ Utils .mergeBoolean (clientAnnotations .destructiveHint (), remoteAnnotations .destructiveHint ()),
623
+ Utils .mergeBoolean (clientAnnotations .idempotentHint (), remoteAnnotations .idempotentHint ()),
624
+ Utils .mergeBoolean (clientAnnotations .openWorldHint (), remoteAnnotations .openWorldHint ()),
625
+ Utils .mergeBoolean (clientAnnotations .returnDirect (), remoteAnnotations .returnDirect ()));
626
+ }
627
+
582
628
private NotificationHandler asyncToolsChangeNotificationHandler (
583
629
List <Function <List <McpSchema .Tool >, Mono <Void >>> toolsChangeConsumers ) {
584
630
// TODO: params are not used yet
0 commit comments