Skip to content
Open
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 @@ -303,7 +303,7 @@ public class McpAsyncClient {
return Mono.empty();
}

return this.listToolsInternal(init, McpSchema.FIRST_PAGE).doOnNext(listToolsResult -> {
return this.listToolsInternal(init, McpSchema.FIRST_PAGE, null).doOnNext(listToolsResult -> {
listToolsResult.tools()
.forEach(tool -> logger.debug("Tool {} schema: {}", tool.name(), tool.outputSchema()));
if (enableCallToolSchemaCaching && listToolsResult.tools() != null) {
Expand Down Expand Up @@ -645,16 +645,27 @@ public Mono<McpSchema.ListToolsResult> listTools() {
* @return A Mono that emits the list of tools result
*/
public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
return this.initializer.withInitialization("listing tools", init -> this.listToolsInternal(init, cursor));
return this.initializer.withInitialization("listing tools", init -> this.listToolsInternal(init, cursor, null));
}

private Mono<McpSchema.ListToolsResult> listToolsInternal(Initialization init, String cursor) {
/**
* Retrieves a paginated list of tools with optional metadata.
* @param cursor Optional pagination cursor from a previous list request
* @param meta Optional metadata to include in the request (_meta field)
* @return A Mono that emits the list of tools result
*/
public Mono<McpSchema.ListToolsResult> listTools(String cursor, java.util.Map<String, Object> meta) {
return this.initializer.withInitialization("listing tools", init -> this.listToolsInternal(init, cursor, meta));
}

private Mono<McpSchema.ListToolsResult> listToolsInternal(Initialization init, String cursor,
java.util.Map<String, Object> meta) {

if (init.initializeResult().capabilities().tools() == null) {
return Mono.error(new IllegalStateException("Server does not provide tools capability"));
}
return init.mcpSession()
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor, meta),
LIST_TOOLS_RESULT_TYPE_REF)
.doOnNext(result -> {
// Validate tool names (warn only)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ public McpSchema.ListToolsResult listTools(String cursor) {

}

/**
* Retrieves a paginated list of tools with optional metadata.
* @param cursor Optional pagination cursor from a previous list request
* @param meta Optional metadata to include in the request (_meta field)
* @return The list of tools result
*/
public McpSchema.ListToolsResult listTools(String cursor, java.util.Map<String, Object> meta) {
return withProvidedContext(this.delegate.listTools(cursor, meta)).block();
}

// --------------------------
// Resources
// --------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ void testListTools() {
});
}

@Test
void testListToolsWithMeta() {
withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize();
java.util.Map<String, Object> meta = java.util.Map.of("requestId", "test-123");
ListToolsResult tools = mcpSyncClient.listTools(McpSchema.FIRST_PAGE, meta);

assertThat(tools).isNotNull().satisfies(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty();
});
});
}

@Test
void testListAllTools() {
withClient(createMcpTransport(), mcpSyncClient -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,143 @@ public java.lang.reflect.Type getType() {
assertThat(names).containsExactlyInAnyOrder("subtract", "add");
}

@Test
void testListToolsWithCursorAndMeta() {
McpSchema.Tool addTool = McpSchema.Tool.builder().name("add").description("calculate add").build();
McpSchema.ListToolsResult mockToolsResult = new McpSchema.ListToolsResult(List.of(addTool), null);

// Use array to capture from anonymous class
McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1];

McpClientTransport transport = new McpClientTransport() {
Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler;

@Override
public Mono<Void> connect(
Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
return Mono.deferContextual(ctx -> {
this.handler = handler;
return Mono.empty();
});
}

@Override
public Mono<Void> closeGracefully() {
return Mono.empty();
}

@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
if (!(message instanceof McpSchema.JSONRPCRequest request)) {
return Mono.empty();
}

McpSchema.JSONRPCResponse response;
if (McpSchema.METHOD_INITIALIZE.equals(request.method())) {
response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), MOCK_INIT_RESULT,
null);
}
else if (McpSchema.METHOD_TOOLS_LIST.equals(request.method())) {
capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class);
response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockToolsResult,
null);
}
else {
return Mono.empty();
}

return handler.apply(Mono.just(response)).then();
}

@Override
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return JSON_MAPPER.convertValue(data, new TypeRef<>() {
@Override
public java.lang.reflect.Type getType() {
return typeRef.getType();
}
});
}
};

McpAsyncClient client = McpClient.async(transport).build();

Map<String, Object> meta = Map.of("customKey", "customValue");
McpSchema.ListToolsResult toolsResult = client.listTools("cursor-1", meta).block();
assertThat(toolsResult).isNotNull();
assertThat(toolsResult.tools()).hasSize(1);
assertThat(capturedRequest[0]).isNotNull();
assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1");
assertThat(capturedRequest[0].meta()).containsEntry("customKey", "customValue");
}

@Test
void testSyncListToolsWithCursorAndMeta() {
McpSchema.Tool addTool = McpSchema.Tool.builder().name("add").description("calculate add").build();
McpSchema.ListToolsResult mockToolsResult = new McpSchema.ListToolsResult(List.of(addTool), null);

McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1];

McpClientTransport transport = new McpClientTransport() {
Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler;

@Override
public Mono<Void> connect(
Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
return Mono.deferContextual(ctx -> {
this.handler = handler;
return Mono.empty();
});
}

@Override
public Mono<Void> closeGracefully() {
return Mono.empty();
}

@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
if (!(message instanceof McpSchema.JSONRPCRequest request)) {
return Mono.empty();
}

McpSchema.JSONRPCResponse response;
if (McpSchema.METHOD_INITIALIZE.equals(request.method())) {
response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), MOCK_INIT_RESULT,
null);
}
else if (McpSchema.METHOD_TOOLS_LIST.equals(request.method())) {
capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class);
response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockToolsResult,
null);
}
else {
return Mono.empty();
}

return handler.apply(Mono.just(response)).then();
}

@Override
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return JSON_MAPPER.convertValue(data, new TypeRef<>() {
@Override
public java.lang.reflect.Type getType() {
return typeRef.getType();
}
});
}
};

McpSyncClient client = McpClient.sync(transport).build();

Map<String, Object> meta = Map.of("requestId", "test-123");
McpSchema.ListToolsResult toolsResult = client.listTools("cursor-1", meta);
assertThat(toolsResult).isNotNull();
assertThat(toolsResult.tools()).hasSize(1);
assertThat(capturedRequest[0]).isNotNull();
assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1");
assertThat(capturedRequest[0].meta()).containsEntry("requestId", "test-123");
}

}