Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -22,7 +23,6 @@

import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
Expand Down Expand Up @@ -153,6 +153,15 @@ public class McpAsyncClient {
*/
private final LifecycleInitializer initializer;

/**
* MCP provides a standardized way for servers to request tool invocation from
* clients. This flow allows clients to maintain control over tool access, selection,
* and permissions while enabling servers to leverage AI capabilities—with no server
* API keys necessary. Servers can request tool invocation with optional tool
* annotations to provide additional context to the tool.
*/
private Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler;

/**
* Create a new McpAsyncClient with the given transport and session request-response
* timeout.
Expand All @@ -162,8 +171,7 @@ public class McpAsyncClient {
* @param features the MCP Client supported features.
*/
McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout,
McpClientFeatures.Async features) {

McpClientFeatures.Async features, Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler) {
Assert.notNull(transport, "Transport must not be null");
Assert.notNull(requestTimeout, "Request timeout must not be null");
Assert.notNull(initializationTimeout, "Initialization timeout must not be null");
Expand All @@ -172,7 +180,7 @@ public class McpAsyncClient {
this.clientCapabilities = features.clientCapabilities();
this.transport = transport;
this.roots = new ConcurrentHashMap<>(features.roots());

this.toolAnnotationsHandler = toolAnnotationsHandler != null ? toolAnnotationsHandler : tool -> null;
// Request Handlers
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();

Expand Down Expand Up @@ -575,10 +583,48 @@ public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
}
return init.mcpSession()
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
LIST_TOOLS_RESULT_TYPE_REF);
LIST_TOOLS_RESULT_TYPE_REF)
.map(this::mergeToolsAnnotations);
});
}

private McpSchema.ListToolsResult mergeToolsAnnotations(McpSchema.ListToolsResult listToolsResult) {
if (listToolsResult == null || listToolsResult.tools() == null || listToolsResult.tools().isEmpty()) {
return listToolsResult;
}

List<McpSchema.Tool> mergedTools = listToolsResult.tools().stream().map(tool -> {
McpSchema.ToolAnnotations clientAnnoProperties = this.toolAnnotationsHandler.apply(tool.name());
if (clientAnnoProperties == null) {
return tool; // no update needed
}
McpSchema.ToolAnnotations mergedAnno = mergeAnnotations(tool.annotations(), clientAnnoProperties);
return McpSchema.Tool.builder()
.name(tool.name())
.title(tool.title())
.description(tool.description())
.inputSchema(tool.inputSchema())
.outputSchema(tool.outputSchema())
.annotations(mergedAnno)
.meta(tool.meta())
.build();
}).collect(Collectors.toList());
return new McpSchema.ListToolsResult(mergedTools, listToolsResult.nextCursor(), listToolsResult.meta());
}

private McpSchema.ToolAnnotations mergeAnnotations(McpSchema.ToolAnnotations remoteAnnotations,
McpSchema.ToolAnnotations clientAnnotations) {
if (remoteAnnotations == null) {
return clientAnnotations;
}
return new McpSchema.ToolAnnotations(Utils.preferFirst(clientAnnotations.title(), remoteAnnotations.title()),
Utils.mergeBoolean(clientAnnotations.readOnlyHint(), remoteAnnotations.readOnlyHint()),
Utils.mergeBoolean(clientAnnotations.destructiveHint(), remoteAnnotations.destructiveHint()),
Utils.mergeBoolean(clientAnnotations.idempotentHint(), remoteAnnotations.idempotentHint()),
Utils.mergeBoolean(clientAnnotations.openWorldHint(), remoteAnnotations.openWorldHint()),
Utils.mergeBoolean(clientAnnotations.returnDirect(), remoteAnnotations.returnDirect()));
}

private NotificationHandler asyncToolsChangeNotificationHandler(
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers) {
// TODO: params are not used yet
Expand Down
41 changes: 38 additions & 3 deletions mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ class SyncSpec {

private Function<ElicitRequest, ElicitResult> elicitationHandler;

private Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler;

private SyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
Expand Down Expand Up @@ -409,6 +411,21 @@ public SyncSpec progressConsumers(List<Consumer<McpSchema.ProgressNotification>>
return this;
}

/**
* Adds a handler to be invoked when tool annotations are requested. This allows
* the client to provide additional information about tools, such as their
* capabilities and usage instructions.
* @param toolAnnotationsHandler A handler that receives the tool name and returns
* the tool annotations. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolAnnotationsHandler is null
*/
public SyncSpec toolAnnotationsHandler(Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler) {
Assert.notNull(progressConsumers, "tool annotations handler must not be null");
this.toolAnnotationsHandler = toolAnnotationsHandler;
return this;
}

/**
* Create an instance of {@link McpSyncClient} with the provided configurations or
* sensible defaults.
Expand All @@ -422,8 +439,8 @@ public McpSyncClient build() {

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

return new McpSyncClient(
new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout, asyncFeatures));
return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
asyncFeatures, toolAnnotationsHandler));
}

}
Expand Down Expand Up @@ -474,6 +491,8 @@ class AsyncSpec {

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

private Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler;

private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
Expand Down Expand Up @@ -720,6 +739,21 @@ public AsyncSpec progressConsumers(
return this;
}

/**
* Adds a handler to be invoked when tool annotations are requested. This allows
* the client to provide additional information about tools, such as their
* capabilities and usage instructions.
* @param toolAnnotationsHandler A handler that receives the tool name and returns
* the tool annotations. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolAnnotationsHandler is null
*/
public AsyncSpec toolAnnotationsHandler(Function<String, McpSchema.ToolAnnotations> toolAnnotationsHandler) {
Assert.notNull(toolAnnotationsHandler, "tool annotations handler must not be null");
this.toolAnnotationsHandler = toolAnnotationsHandler;
return this;
}

/**
* Create an instance of {@link McpAsyncClient} with the provided configurations
* or sensible defaults.
Expand All @@ -730,7 +764,8 @@ public McpAsyncClient build() {
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
this.samplingHandler, this.elicitationHandler));
this.samplingHandler, this.elicitationHandler),
toolAnnotationsHandler);
}

}
Expand Down
28 changes: 28 additions & 0 deletions mcp/src/main/java/io/modelcontextprotocol/util/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,32 @@ private static boolean isUnderBaseUri(URI baseUri, URI endpointUri) {
return endpointPath.startsWith(basePath);
}

/**
* Returns the first non-null value from the given arguments.
* @param first The first argument
* @param second The second argument
* @return The first non-null value
*/
public static <T> T preferFirst(T first, T second) {
return second != null ? second : first;
}


/**
* Merges two boolean values. If the first value is null, the second value is returned.
* If the second value is null, false is returned.
* @param first The first boolean value
* @param second The second boolean value
* @return The merged boolean value
*/
public static Boolean mergeBoolean(Boolean first, Boolean second) {
if (first == null) {
return second;
}
if (second == null) {
return false;
}
return first && second;
}

}