Skip to content

Commit 66dd9ee

Browse files
committed
feat: client support for setting ToolAnnotations
- Added a function in Client to set tool annotations received from server - Updated Client builder to support configuring ToolAnnotations - Enables downstream clients to handle server-provided tool metadata
1 parent 713ee1a commit 66dd9ee

File tree

3 files changed

+117
-8
lines changed

3 files changed

+117
-8
lines changed

mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Map;
1515
import java.util.concurrent.ConcurrentHashMap;
1616
import java.util.function.Function;
17+
import java.util.stream.Collectors;
1718

1819
import org.slf4j.Logger;
1920
import org.slf4j.LoggerFactory;
@@ -22,7 +23,6 @@
2223

2324
import io.modelcontextprotocol.spec.McpClientSession;
2425
import io.modelcontextprotocol.spec.McpClientTransport;
25-
import io.modelcontextprotocol.spec.McpError;
2626
import io.modelcontextprotocol.spec.McpSchema;
2727
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
2828
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
@@ -153,6 +153,15 @@ public class McpAsyncClient {
153153
*/
154154
private final LifecycleInitializer initializer;
155155

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+
156165
/**
157166
* Create a new McpAsyncClient with the given transport and session request-response
158167
* timeout.
@@ -162,8 +171,7 @@ public class McpAsyncClient {
162171
* @param features the MCP Client supported features.
163172
*/
164173
McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout,
165-
McpClientFeatures.Async features) {
166-
174+
McpClientFeatures.Async features, Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler) {
167175
Assert.notNull(transport, "Transport must not be null");
168176
Assert.notNull(requestTimeout, "Request timeout must not be null");
169177
Assert.notNull(initializationTimeout, "Initialization timeout must not be null");
@@ -172,7 +180,7 @@ public class McpAsyncClient {
172180
this.clientCapabilities = features.clientCapabilities();
173181
this.transport = transport;
174182
this.roots = new ConcurrentHashMap<>(features.roots());
175-
183+
this.toolAnnotationsHandler = toolAnnotationsHandler != null ? toolAnnotationsHandler : tool -> null;
176184
// Request Handlers
177185
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
178186

@@ -575,10 +583,48 @@ public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
575583
}
576584
return init.mcpSession()
577585
.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);
579588
});
580589
}
581590

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+
582628
private NotificationHandler asyncToolsChangeNotificationHandler(
583629
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers) {
584630
// TODO: params are not used yet

mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ class SyncSpec {
183183

184184
private Function<ElicitRequest, ElicitResult> elicitationHandler;
185185

186+
private Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler;
187+
186188
private SyncSpec(McpClientTransport transport) {
187189
Assert.notNull(transport, "Transport must not be null");
188190
this.transport = transport;
@@ -409,6 +411,21 @@ public SyncSpec progressConsumers(List<Consumer<McpSchema.ProgressNotification>>
409411
return this;
410412
}
411413

414+
/**
415+
* Adds a handler to be invoked when tool annotations are requested. This allows
416+
* the client to provide additional information about tools, such as their
417+
* capabilities and usage instructions.
418+
* @param toolAnnotationsHandler A handler that receives the tool name and returns
419+
* the tool annotations. Must not be null.
420+
* @return This builder instance for method chaining
421+
* @throws IllegalArgumentException if toolAnnotationsHandler is null
422+
*/
423+
public SyncSpec toolAnnotationsHandler(Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler) {
424+
Assert.notNull(progressConsumers, "tool annotations handler must not be null");
425+
this.toolAnnotationsHandler = toolAnnotationsHandler;
426+
return this;
427+
}
428+
412429
/**
413430
* Create an instance of {@link McpSyncClient} with the provided configurations or
414431
* sensible defaults.
@@ -422,8 +439,8 @@ public McpSyncClient build() {
422439

423440
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
424441

425-
return new McpSyncClient(
426-
new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout, asyncFeatures));
442+
return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
443+
asyncFeatures, toolAnnotationsHandler));
427444
}
428445

429446
}
@@ -474,6 +491,8 @@ class AsyncSpec {
474491

475492
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
476493

494+
private Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler;
495+
477496
private AsyncSpec(McpClientTransport transport) {
478497
Assert.notNull(transport, "Transport must not be null");
479498
this.transport = transport;
@@ -720,6 +739,21 @@ public AsyncSpec progressConsumers(
720739
return this;
721740
}
722741

742+
/**
743+
* Adds a handler to be invoked when tool annotations are requested. This allows
744+
* the client to provide additional information about tools, such as their
745+
* capabilities and usage instructions.
746+
* @param toolAnnotationsHandler A handler that receives the tool name and returns
747+
* the tool annotations. Must not be null.
748+
* @return This builder instance for method chaining
749+
* @throws IllegalArgumentException if toolAnnotationsHandler is null
750+
*/
751+
public AsyncSpec toolAnnotationsHandler(Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler) {
752+
Assert.notNull(toolAnnotationsHandler, "tool annotations handler must not be null");
753+
this.toolAnnotationsHandler = toolAnnotationsHandler;
754+
return this;
755+
}
756+
723757
/**
724758
* Create an instance of {@link McpAsyncClient} with the provided configurations
725759
* or sensible defaults.
@@ -730,7 +764,8 @@ public McpAsyncClient build() {
730764
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
731765
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
732766
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
733-
this.samplingHandler, this.elicitationHandler));
767+
this.samplingHandler, this.elicitationHandler),
768+
toolAnnotationsHandler);
734769
}
735770

736771
}

mcp/src/main/java/io/modelcontextprotocol/util/Utils.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,32 @@ private static boolean isUnderBaseUri(URI baseUri, URI endpointUri) {
107107
return endpointPath.startsWith(basePath);
108108
}
109109

110+
/**
111+
* Returns the first non-null value from the given arguments.
112+
* @param first The first argument
113+
* @param second The second argument
114+
* @return The first non-null value
115+
*/
116+
public static <T> T preferFirst(T first, T second) {
117+
return second != null ? second : first;
118+
}
119+
120+
121+
/**
122+
* Merges two boolean values. If the first value is null, the second value is returned.
123+
* If the second value is null, false is returned.
124+
* @param first The first boolean value
125+
* @param second The second boolean value
126+
* @return The merged boolean value
127+
*/
128+
public static Boolean mergeBoolean(Boolean first, Boolean second) {
129+
if (first == null) {
130+
return second;
131+
}
132+
if (second == null) {
133+
return false;
134+
}
135+
return first && second;
136+
}
137+
110138
}

0 commit comments

Comments
 (0)