From de1159104f6bd8e784f35d90c0a1098d485d9963 Mon Sep 17 00:00:00 2001 From: SHEETAL MOHITE Date: Tue, 7 Apr 2026 08:38:55 -0700 Subject: [PATCH 1/2] feat: add support for meta parameter in listTools --- .../client/McpAsyncClient.java | 19 ++- .../client/McpSyncClient.java | 10 ++ .../client/AbstractMcpSyncClientTests.java | 13 ++ .../client/McpAsyncClientTests.java | 139 ++++++++++++++++++ 4 files changed, 177 insertions(+), 4 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 93fcc332a..91083c400 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -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) { @@ -645,16 +645,27 @@ public Mono listTools() { * @return A Mono that emits the list of tools result */ public Mono 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 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 listTools(String cursor, java.util.Map meta) { + return this.initializer.withInitialization("listing tools", init -> this.listToolsInternal(init, cursor, meta)); + } + + private Mono listToolsInternal(Initialization init, String cursor, + java.util.Map 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) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index 7fdaa8941..6cabca162 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -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 meta) { + return withProvidedContext(this.delegate.listTools(cursor, meta)).block(); + } + // -------------------------- // Resources // -------------------------- diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index 26d60568a..327726afb 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -154,6 +154,19 @@ void testListTools() { }); } + @Test + void testListToolsWithMeta() { + withClient(createMcpTransport(), mcpSyncClient -> { + mcpSyncClient.initialize(); + java.util.Map 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 -> { diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java index 48bf1da5b..d0bba8523 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java @@ -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> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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 unmarshalFrom(Object data, TypeRef 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 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> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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 unmarshalFrom(Object data, TypeRef 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 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"); + } + } From 21c8feddd4cad7e1b99611b47668b8821a686361 Mon Sep 17 00:00:00 2001 From: SHEETAL MOHITE Date: Wed, 8 Apr 2026 08:55:42 -0700 Subject: [PATCH 2/2] feat: add support for meta parameter to all paginated requests which are: - resources/list - resources/templates/list - prompts/list - tools/list paginated list operations extended in this review: - listResources(String cursor, Map meta) - listResourceTemplates(String cursor, Map meta) - listPrompts(String cursor, Map meta) Closes #907 --- .../client/McpAsyncClient.java | 64 ++- .../client/McpSyncClient.java | 43 +- .../client/AbstractMcpSyncClientTests.java | 39 ++ .../client/McpAsyncClientTests.java | 418 ++++++++++++++++++ 4 files changed, 558 insertions(+), 6 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 91083c400..8aac5edf9 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -736,12 +736,31 @@ public Mono listResources() { * @see #readResource(McpSchema.Resource) */ public Mono listResources(String cursor) { + return this.listResourcesInternal(cursor, null); + } + + /** + * Retrieves a paginated list of resources provided by the server. Resources represent + * any kind of UTF-8 encoded data that an MCP server makes available to clients, such + * as database records, API responses, log files, and more. + * @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 completes with the list of resources result. + * @see McpSchema.ListResourcesResult + * @see #readResource(McpSchema.Resource) + */ + public Mono listResources(String cursor, java.util.Map meta) { + return this.listResourcesInternal(cursor, meta); + } + + private Mono listResourcesInternal(String cursor, + java.util.Map meta) { return this.initializer.withInitialization("listing resources", init -> { if (init.initializeResult().capabilities().resources() == null) { return Mono.error(new IllegalStateException("Server does not provide the resources capability")); } return init.mcpSession() - .sendRequest(McpSchema.METHOD_RESOURCES_LIST, new McpSchema.PaginatedRequest(cursor), + .sendRequest(McpSchema.METHOD_RESOURCES_LIST, new McpSchema.PaginatedRequest(cursor, meta), LIST_RESOURCES_RESULT_TYPE_REF); }); } @@ -806,12 +825,31 @@ public Mono listResourceTemplates() { * @see McpSchema.ListResourceTemplatesResult */ public Mono listResourceTemplates(String cursor) { + return this.listResourceTemplatesInternal(cursor, null); + } + + /** + * Retrieves a paginated list of resource templates provided by the server. Resource + * templates allow servers to expose parameterized resources using URI templates, + * enabling dynamic resource access based on variable parameters. + * @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 completes with the list of resource templates result. + * @see McpSchema.ListResourceTemplatesResult + */ + public Mono listResourceTemplates(String cursor, + java.util.Map meta) { + return this.listResourceTemplatesInternal(cursor, meta); + } + + private Mono listResourceTemplatesInternal(String cursor, + java.util.Map meta) { return this.initializer.withInitialization("listing resource templates", init -> { if (init.initializeResult().capabilities().resources() == null) { return Mono.error(new IllegalStateException("Server does not provide the resources capability")); } return init.mcpSession() - .sendRequest(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, new McpSchema.PaginatedRequest(cursor), + .sendRequest(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, new McpSchema.PaginatedRequest(cursor, meta), LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF); }); } @@ -906,8 +944,26 @@ public Mono listPrompts() { * @see #getPrompt(GetPromptRequest) */ public Mono listPrompts(String cursor) { - return this.initializer.withInitialization("listing prompts", init -> init.mcpSession() - .sendRequest(McpSchema.METHOD_PROMPT_LIST, new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF)); + return this.listPromptsInternal(cursor, null); + } + + /** + * Retrieves a paginated list of prompts 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 completes with the list of prompts result. + * @see McpSchema.ListPromptsResult + * @see #getPrompt(GetPromptRequest) + */ + public Mono listPrompts(String cursor, java.util.Map meta) { + return this.listPromptsInternal(cursor, meta); + } + + private Mono listPromptsInternal(String cursor, java.util.Map meta) { + return this.initializer.withInitialization("listing prompts", + init -> init.mcpSession() + .sendRequest(McpSchema.METHOD_PROMPT_LIST, new PaginatedRequest(cursor, meta), + LIST_PROMPTS_RESULT_TYPE_REF)); } /** diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index 6cabca162..cd67b7401 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -260,10 +260,12 @@ public McpSchema.ListToolsResult listTools(String cursor) { } /** - * Retrieves a paginated list of tools with optional metadata. + * Retrieves a paginated list of tools provided by the server. * @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 + * @return The list of tools result containing: - tools: List of available tools, each + * with a name, description, and input schema - nextCursor: Optional cursor for + * pagination if more tools are available */ public McpSchema.ListToolsResult listTools(String cursor, java.util.Map meta) { return withProvidedContext(this.delegate.listTools(cursor, meta)).block(); @@ -292,6 +294,17 @@ public McpSchema.ListResourcesResult listResources(String cursor) { } + /** + * Retrieves a paginated list of resources 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 resources result + */ + public McpSchema.ListResourcesResult listResources(String cursor, java.util.Map meta) { + return withProvidedContext(this.delegate.listResources(cursor, meta)).block(); + + } + /** * Send a resources/read request. * @param resource the resource to read @@ -334,6 +347,21 @@ public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor } + /** + * Resource templates allow servers to expose parameterized resources using URI + * templates. Arguments may be auto-completed through the completion API. + * + * Retrieves a paginated list of resource templates provided by the server. + * @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 resource templates result. + */ + public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor, + java.util.Map meta) { + return withProvidedContext(this.delegate.listResourceTemplates(cursor, meta)).block(); + + } + /** * Subscriptions. The protocol supports optional subscriptions to resource changes. * Clients can subscribe to specific resources and receive notifications when they @@ -380,6 +408,17 @@ public ListPromptsResult listPrompts(String cursor) { } + /** + * Retrieves a paginated list of prompts provided by the server. + * @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 prompts result. + */ + public ListPromptsResult listPrompts(String cursor, java.util.Map meta) { + return withProvidedContext(this.delegate.listPrompts(cursor, meta)).block(); + + } + public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) { return withProvidedContext(this.delegate.getPrompt(getPromptRequest)).block(); } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index 327726afb..0c38ddafe 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -691,4 +691,43 @@ void testProgressConsumer() { }); } + @Test + void testListResourcesWithMeta() { + withClient(createMcpTransport(), mcpSyncClient -> { + mcpSyncClient.initialize(); + java.util.Map meta = java.util.Map.of("requestId", "test-123"); + ListResourcesResult resources = mcpSyncClient.listResources(McpSchema.FIRST_PAGE, meta); + + assertThat(resources).isNotNull().satisfies(result -> { + assertThat(result.resources()).isNotNull(); + }); + }); + } + + @Test + void testListResourceTemplatesWithMeta() { + withClient(createMcpTransport(), mcpSyncClient -> { + mcpSyncClient.initialize(); + java.util.Map meta = java.util.Map.of("requestId", "test-123"); + ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(McpSchema.FIRST_PAGE, meta); + + assertThat(result).isNotNull().satisfies(r -> { + assertThat(r.resourceTemplates()).isNotNull(); + }); + }); + } + + @Test + void testListPromptsWithMeta() { + withClient(createMcpTransport(), mcpSyncClient -> { + mcpSyncClient.initialize(); + java.util.Map meta = java.util.Map.of("requestId", "test-123"); + McpSchema.ListPromptsResult result = mcpSyncClient.listPrompts(McpSchema.FIRST_PAGE, meta); + + assertThat(result).isNotNull().satisfies(r -> { + assertThat(r.prompts()).isNotNull(); + }); + }); + } + } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java index d0bba8523..bfe9d5df9 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java @@ -446,4 +446,422 @@ public java.lang.reflect.Type getType() { assertThat(capturedRequest[0].meta()).containsEntry("requestId", "test-123"); } + @Test + void testListResourcesWithCursorAndMeta() { + McpSchema.Resource mockResource = McpSchema.Resource.builder().uri("file:///test.txt").name("test.txt").build(); + McpSchema.ListResourcesResult mockResult = new McpSchema.ListResourcesResult(List.of(mockResource), null); + + McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder().resources(false, false).build(); + McpSchema.InitializeResult initResult = new McpSchema.InitializeResult(ProtocolVersions.MCP_2024_11_05, caps, + MOCK_SERVER_INFO, null); + + McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1]; + + McpClientTransport transport = new McpClientTransport() { + Function, Mono> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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(), initResult, null); + } + else if (McpSchema.METHOD_RESOURCES_LIST.equals(request.method())) { + capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class); + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockResult, null); + } + else { + return Mono.empty(); + } + return handler.apply(Mono.just(response)).then(); + } + + @Override + public T unmarshalFrom(Object data, TypeRef 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 meta = Map.of("customKey", "customValue"); + McpSchema.ListResourcesResult result = client.listResources("cursor-1", meta).block(); + assertThat(result).isNotNull(); + assertThat(result.resources()).hasSize(1); + assertThat(capturedRequest[0]).isNotNull(); + assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1"); + assertThat(capturedRequest[0].meta()).containsEntry("customKey", "customValue"); + } + + @Test + void testSyncListResourcesWithCursorAndMeta() { + McpSchema.Resource mockResource = McpSchema.Resource.builder().uri("file:///test.txt").name("test.txt").build(); + McpSchema.ListResourcesResult mockResult = new McpSchema.ListResourcesResult(List.of(mockResource), null); + + McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder().resources(false, false).build(); + McpSchema.InitializeResult initResult = new McpSchema.InitializeResult(ProtocolVersions.MCP_2024_11_05, caps, + MOCK_SERVER_INFO, null); + + McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1]; + + McpClientTransport transport = new McpClientTransport() { + Function, Mono> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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(), initResult, null); + } + else if (McpSchema.METHOD_RESOURCES_LIST.equals(request.method())) { + capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class); + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockResult, null); + } + else { + return Mono.empty(); + } + return handler.apply(Mono.just(response)).then(); + } + + @Override + public T unmarshalFrom(Object data, TypeRef 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 meta = Map.of("customKey", "customValue"); + McpSchema.ListResourcesResult result = client.listResources("cursor-1", meta); + assertThat(result).isNotNull(); + assertThat(result.resources()).hasSize(1); + assertThat(capturedRequest[0]).isNotNull(); + assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1"); + assertThat(capturedRequest[0].meta()).containsEntry("customKey", "customValue"); + } + + @Test + void testListResourceTemplatesWithCursorAndMeta() { + McpSchema.ResourceTemplate mockTemplate = new McpSchema.ResourceTemplate("file:///{name}", "template", null, + null, null); + McpSchema.ListResourceTemplatesResult mockResult = new McpSchema.ListResourceTemplatesResult( + List.of(mockTemplate), null); + + McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder().resources(false, false).build(); + McpSchema.InitializeResult initResult = new McpSchema.InitializeResult(ProtocolVersions.MCP_2024_11_05, caps, + MOCK_SERVER_INFO, null); + + McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1]; + + McpClientTransport transport = new McpClientTransport() { + Function, Mono> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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(), initResult, null); + } + else if (McpSchema.METHOD_RESOURCES_TEMPLATES_LIST.equals(request.method())) { + capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class); + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockResult, null); + } + else { + return Mono.empty(); + } + return handler.apply(Mono.just(response)).then(); + } + + @Override + public T unmarshalFrom(Object data, TypeRef 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 meta = Map.of("customKey", "customValue"); + McpSchema.ListResourceTemplatesResult result = client.listResourceTemplates("cursor-1", meta).block(); + assertThat(result).isNotNull(); + assertThat(result.resourceTemplates()).hasSize(1); + assertThat(capturedRequest[0]).isNotNull(); + assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1"); + assertThat(capturedRequest[0].meta()).containsEntry("customKey", "customValue"); + } + + @Test + void testSyncListResourceTemplatesWithCursorAndMeta() { + McpSchema.ResourceTemplate mockTemplate = new McpSchema.ResourceTemplate("file:///{name}", "template", null, + null, null); + McpSchema.ListResourceTemplatesResult mockResult = new McpSchema.ListResourceTemplatesResult( + List.of(mockTemplate), null); + + McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder().resources(false, false).build(); + McpSchema.InitializeResult initResult = new McpSchema.InitializeResult(ProtocolVersions.MCP_2024_11_05, caps, + MOCK_SERVER_INFO, null); + + McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1]; + + McpClientTransport transport = new McpClientTransport() { + Function, Mono> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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(), initResult, null); + } + else if (McpSchema.METHOD_RESOURCES_TEMPLATES_LIST.equals(request.method())) { + capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class); + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockResult, null); + } + else { + return Mono.empty(); + } + return handler.apply(Mono.just(response)).then(); + } + + @Override + public T unmarshalFrom(Object data, TypeRef 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 meta = Map.of("customKey", "customValue"); + McpSchema.ListResourceTemplatesResult result = client.listResourceTemplates("cursor-1", meta); + assertThat(result).isNotNull(); + assertThat(result.resourceTemplates()).hasSize(1); + assertThat(capturedRequest[0]).isNotNull(); + assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1"); + assertThat(capturedRequest[0].meta()).containsEntry("customKey", "customValue"); + } + + @Test + void testListPromptsWithCursorAndMeta() { + McpSchema.Prompt mockPrompt = new McpSchema.Prompt("test-prompt", "A test prompt", List.of()); + McpSchema.ListPromptsResult mockResult = new McpSchema.ListPromptsResult(List.of(mockPrompt), null); + + McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder().prompts(false).build(); + McpSchema.InitializeResult initResult = new McpSchema.InitializeResult(ProtocolVersions.MCP_2024_11_05, caps, + MOCK_SERVER_INFO, null); + + McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1]; + + McpClientTransport transport = new McpClientTransport() { + Function, Mono> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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(), initResult, null); + } + else if (McpSchema.METHOD_PROMPT_LIST.equals(request.method())) { + capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class); + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockResult, null); + } + else { + return Mono.empty(); + } + return handler.apply(Mono.just(response)).then(); + } + + @Override + public T unmarshalFrom(Object data, TypeRef 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 meta = Map.of("customKey", "customValue"); + McpSchema.ListPromptsResult result = client.listPrompts("cursor-1", meta).block(); + assertThat(result).isNotNull(); + assertThat(result.prompts()).hasSize(1); + assertThat(capturedRequest[0]).isNotNull(); + assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1"); + assertThat(capturedRequest[0].meta()).containsEntry("customKey", "customValue"); + } + + @Test + void testSyncListPromptsWithCursorAndMeta() { + McpSchema.Prompt mockPrompt = new McpSchema.Prompt("test-prompt", "A test prompt", List.of()); + McpSchema.ListPromptsResult mockResult = new McpSchema.ListPromptsResult(List.of(mockPrompt), null); + + McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder().prompts(false).build(); + McpSchema.InitializeResult initResult = new McpSchema.InitializeResult(ProtocolVersions.MCP_2024_11_05, caps, + MOCK_SERVER_INFO, null); + + McpSchema.PaginatedRequest[] capturedRequest = new McpSchema.PaginatedRequest[1]; + + McpClientTransport transport = new McpClientTransport() { + Function, Mono> handler; + + @Override + public Mono connect( + Function, Mono> handler) { + return Mono.deferContextual(ctx -> { + this.handler = handler; + return Mono.empty(); + }); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public Mono 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(), initResult, null); + } + else if (McpSchema.METHOD_PROMPT_LIST.equals(request.method())) { + capturedRequest[0] = JSON_MAPPER.convertValue(request.params(), McpSchema.PaginatedRequest.class); + response = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), mockResult, null); + } + else { + return Mono.empty(); + } + return handler.apply(Mono.just(response)).then(); + } + + @Override + public T unmarshalFrom(Object data, TypeRef 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 meta = Map.of("customKey", "customValue"); + McpSchema.ListPromptsResult result = client.listPrompts("cursor-1", meta); + assertThat(result).isNotNull(); + assertThat(result.prompts()).hasSize(1); + assertThat(capturedRequest[0]).isNotNull(); + assertThat(capturedRequest[0].cursor()).isEqualTo("cursor-1"); + assertThat(capturedRequest[0].meta()).containsEntry("customKey", "customValue"); + } + }