diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/InMemoryToolsRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/InMemoryToolsRepository.java new file mode 100644 index 000000000..aa172c728 --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/InMemoryToolsRepository.java @@ -0,0 +1,77 @@ +/* + * Copyright 2024-2026 the original author or authors. + */ + +package io.modelcontextprotocol.server; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import io.modelcontextprotocol.spec.McpSchema; +import reactor.core.publisher.Mono; + +/** + * Default in-memory implementation of {@link ToolsRepository}. + *
+ * This implementation stores tools in a thread-safe {@link ConcurrentHashMap}. It + * provides backward compatibility by exposing all registered tools to all clients without + * filtering. Pagination is not supported in this implementation (always returns full + * list), and the cursor parameter is ignored. + *
+ */ +public class InMemoryToolsRepository implements ToolsRepository { + + private final ConcurrentHashMap- * Example of creating a basic synchronous server:
{@code
+ * Example of creating a basic synchronous server:
+ *
+ * {@code
* McpServer.sync(transportProvider)
- * .serverInfo("my-server", "1.0.0")
- * .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
- * (exchange, args) -> CallToolResult.builder()
- * .content(List.of(new McpSchema.TextContent("Result: " + calculate(args))))
- * .isError(false)
- * .build())
- * .build();
+ * .serverInfo("my-server", "1.0.0")
+ * .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
+ * (exchange, args) -> CallToolResult.builder()
+ * .content(List.of(new McpSchema.TextContent("Result: " + calculate(args))))
+ * .isError(false)
+ * .build())
+ * .build();
* }
*
- * Example of creating a basic asynchronous server: {@code
+ * Example of creating a basic asynchronous server:
+ *
+ * {@code
* McpServer.async(transportProvider)
- * .serverInfo("my-server", "1.0.0")
- * .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
- * (exchange, args) -> Mono.fromSupplier(() -> calculate(args))
- * .map(result -> CallToolResult.builder()
- * .content(List.of(new McpSchema.TextContent("Result: " + result)))
- * .isError(false)
- * .build()))
- * .build();
+ * .serverInfo("my-server", "1.0.0")
+ * .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
+ * (exchange, args) -> Mono.fromSupplier(() -> calculate(args))
+ * .map(result -> CallToolResult.builder()
+ * .content(List.of(new McpSchema.TextContent("Result: " + result)))
+ * .isError(false)
+ * .build()))
+ * .build();
* }
*
*
- * Example with comprehensive asynchronous configuration:
{@code
+ * Example with comprehensive asynchronous configuration:
+ *
+ * {@code
* McpServer.async(transportProvider)
* .serverInfo("advanced-server", "2.0.0")
* .capabilities(new ServerCapabilities(...))
@@ -235,7 +241,7 @@ private SingleSessionAsyncSpecification(McpServerTransportProvider transportProv
*/
@Override
public McpAsyncServer build() {
- var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
+ var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools, null,
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
this.instructions);
@@ -263,7 +269,7 @@ public StreamableServerAsyncSpecification(McpStreamableServerTransportProvider t
*/
@Override
public McpAsyncServer build() {
- var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
+ var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools, null,
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
this.instructions);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
@@ -300,6 +306,12 @@ abstract class AsyncSpecification> {
*/
final List tools = new ArrayList<>();
+ /**
+ * Custom repository for managing tools. If not set, the default
+ * {@link InMemoryToolsRepository} will be used.
+ */
+ ToolsRepository toolsRepository;
+
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose resources to clients. Resources allow servers to share data that
@@ -335,6 +347,24 @@ abstract class AsyncSpecification> {
public abstract McpAsyncServer build();
+ /**
+ * Sets a custom tools repository for managing tool specifications. This allows
+ * for context-aware tool filtering and dynamic tool management.
+ *
+ *
+ * If not set, the server will use the default {@link InMemoryToolsRepository}
+ * which stores all tools in memory without any filtering.
+ * @param toolsRepository The custom tools repository implementation. Must not be
+ * null.
+ * @return This builder instance for method chaining
+ * @throws IllegalArgumentException if toolsRepository is null
+ */
+ public AsyncSpecification toolsRepository(ToolsRepository toolsRepository) {
+ Assert.notNull(toolsRepository, "Tools repository must not be null");
+ this.toolsRepository = toolsRepository;
+ return this;
+ }
+
/**
* Sets the URI template manager factory to use for creating URI templates. This
* allows for custom URI template parsing and variable extraction.
@@ -433,7 +463,9 @@ public AsyncSpecification capabilities(McpSchema.ServerCapabilities serverCap
* {@link McpServerFeatures.AsyncToolSpecification} explicitly.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .tool(
* Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
* (exchange, args) -> Mono.fromSupplier(() -> calculate(args))
@@ -518,7 +550,9 @@ public AsyncSpecification tools(List
- * Example usage: {@code
+ * Example usage:
+ *
+ * {@code
* .tools(
* McpServerFeatures.AsyncToolSpecification.builder().tool(calculatorTool).callTool(calculatorHandler).build(),
* McpServerFeatures.AsyncToolSpecification.builder().tool(weatherTool).callTool(weatherHandler).build(),
@@ -585,7 +619,9 @@ public AsyncSpecification resources(
* provides a convenient way to register multiple resources inline.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .resources(
* new McpServerFeatures.AsyncResourceSpecification(fileResource, fileHandler),
* new McpServerFeatures.AsyncResourceSpecification(dbResource, dbHandler),
@@ -648,7 +684,9 @@ public AsyncSpecification resourceTemplates(
* source.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .prompts(Map.of("analysis", new McpServerFeatures.AsyncPromptSpecification(
* new Prompt("analysis", "Code analysis template"),
* request -> Mono.fromSupplier(() -> generateAnalysisPrompt(request))
@@ -686,7 +724,9 @@ public AsyncSpecification prompts(List
- * Example usage: {@code
+ * Example usage:
+ *
+ * {@code
* .prompts(
* new McpServerFeatures.AsyncPromptSpecification(analysisPrompt, analysisHandler),
* new McpServerFeatures.AsyncPromptSpecification(summaryPrompt, summaryHandler),
@@ -1034,7 +1074,9 @@ public SyncSpecification capabilities(McpSchema.ServerCapabilities serverCapa
* {@link McpServerFeatures.SyncToolSpecification} explicitly.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .tool(
* Tool.builder().name("calculator").title("Performs calculations".inputSchema(schema).build(),
* (exchange, args) -> CallToolResult.builder()
@@ -1117,7 +1159,9 @@ public SyncSpecification tools(List
* method provides a convenient way to register multiple tools inline.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .tools(
* new ToolSpecification(calculatorTool, calculatorHandler),
* new ToolSpecification(weatherTool, weatherHandler),
@@ -1185,7 +1229,9 @@ public SyncSpecification resources(
* provides a convenient way to register multiple resources inline.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .resources(
* new ResourceSpecification(fileResource, fileHandler),
* new ResourceSpecification(dbResource, dbHandler),
@@ -1245,7 +1291,9 @@ public SyncSpecification resourceTemplates(
* source.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* Map prompts = new HashMap<>();
* prompts.put("analysis", new PromptSpecification(
* new Prompt("analysis", "Code analysis template"),
@@ -1284,7 +1332,9 @@ public SyncSpecification prompts(List
- * Example usage: {@code
+ * Example usage:
+ *
+ * {@code
* .prompts(
* new PromptSpecification(analysisPrompt, analysisHandler),
* new PromptSpecification(summaryPrompt, summaryHandler),
@@ -1623,7 +1673,9 @@ public StatelessAsyncSpecification tools(
* method provides a convenient way to register multiple tools inline.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .tools(
* McpServerFeatures.AsyncToolSpecification.builder().tool(calculatorTool).callTool(calculatorHandler).build(),
* McpServerFeatures.AsyncToolSpecification.builder().tool(weatherTool).callTool(weatherHandler).build(),
@@ -1691,7 +1743,9 @@ public StatelessAsyncSpecification resources(
* provides a convenient way to register multiple resources inline.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .resources(
* new McpServerFeatures.AsyncResourceSpecification(fileResource, fileHandler),
* new McpServerFeatures.AsyncResourceSpecification(dbResource, dbHandler),
@@ -1752,7 +1806,9 @@ public StatelessAsyncSpecification resourceTemplates(
* source.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .prompts(Map.of("analysis", new McpServerFeatures.AsyncPromptSpecification(
* new Prompt("analysis", "Code analysis template"),
* request -> Mono.fromSupplier(() -> generateAnalysisPrompt(request))
@@ -1791,7 +1847,9 @@ public StatelessAsyncSpecification prompts(List
- * Example usage: {@code
+ * Example usage:
+ *
+ * {@code
* .prompts(
* new McpServerFeatures.AsyncPromptSpecification(analysisPrompt, analysisHandler),
* new McpServerFeatures.AsyncPromptSpecification(summaryPrompt, summaryHandler),
@@ -2085,7 +2143,9 @@ public StatelessSyncSpecification tools(
* method provides a convenient way to register multiple tools inline.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .tools(
* McpServerFeatures.SyncToolSpecification.builder().tool(calculatorTool).callTool(calculatorHandler).build(),
* McpServerFeatures.SyncToolSpecification.builder().tool(weatherTool).callTool(weatherHandler).build(),
@@ -2153,7 +2213,9 @@ public StatelessSyncSpecification resources(
* provides a convenient way to register multiple resources inline.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .resources(
* new McpServerFeatures.SyncResourceSpecification(fileResource, fileHandler),
* new McpServerFeatures.SyncResourceSpecification(dbResource, dbHandler),
@@ -2214,7 +2276,9 @@ public StatelessSyncSpecification resourceTemplates(
* source.
*
*
- * Example usage:
{@code
+ * Example usage:
+ *
+ * {@code
* .prompts(Map.of("analysis", new McpServerFeatures.SyncPromptSpecification(
* new Prompt("analysis", "Code analysis template"),
* request -> Mono.fromSupplier(() -> generateAnalysisPrompt(request))
@@ -2253,7 +2317,9 @@ public StatelessSyncSpecification prompts(List
- * Example usage: {@code
+ * Example usage:
+ *
+ * {@code
* .prompts(
* new McpServerFeatures.SyncPromptSpecification(analysisPrompt, analysisHandler),
* new McpServerFeatures.SyncPromptSpecification(summaryPrompt, summaryHandler),
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
index fe0608b1c..b188d8ed6 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024-2025 the original author or authors.
+ * Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.server;
@@ -40,7 +40,8 @@ public class McpServerFeatures {
* @param instructions The server instructions text
*/
record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
- List tools, Map resources,
+ List tools, ToolsRepository toolsRepository,
+ Map resources,
Map resourceTemplates,
Map prompts,
Map completions,
@@ -52,6 +53,8 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool specifications
+ * @param toolsRepository The tools repository (optional, defaults to
+ * InMemoryToolsRepository if tools list is provided)
* @param resources The map of resource specifications
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
@@ -60,7 +63,8 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s
* @param instructions The server instructions text
*/
Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
- List tools, Map resources,
+ List tools, ToolsRepository toolsRepository,
+ Map resources,
Map resourceTemplates,
Map prompts,
Map completions,
@@ -80,9 +84,13 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
- !Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
+ (!Utils.isEmpty(tools) || toolsRepository != null)
+ ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
this.tools = (tools != null) ? tools : List.of();
+ // toolsRepository is initialized in McpAsyncServer.initializeToolsRepository
+ // to ensure structured output handling is always applied
+ this.toolsRepository = toolsRepository;
this.resources = (resources != null) ? resources : Map.of();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : Map.of();
this.prompts = (prompts != null) ? prompts : Map.of();
@@ -135,8 +143,8 @@ static Async fromSync(Sync syncSpec, boolean immediateExecution) {
.subscribeOn(Schedulers.boundedElastic()));
}
- return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources, resourceTemplates,
- prompts, completions, rootChangeConsumers, syncSpec.instructions());
+ return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, null, resources,
+ resourceTemplates, prompts, completions, rootChangeConsumers, syncSpec.instructions());
}
}
@@ -613,13 +621,13 @@ public static Builder builder() {
*
* {@code
* new McpServerFeatures.SyncResourceSpecification(
- * Resource.builder()
- * .uri("docs")
- * .name("Documentation files")
- * .title("Documentation files")
- * .mimeType("text/markdown")
- * .description("Markdown documentation files")
- * .build(),
+ * Resource.builder()
+ * .uri("docs")
+ * .name("Documentation files")
+ * .title("Documentation files")
+ * .mimeType("text/markdown")
+ * .description("Markdown documentation files")
+ * .build(),
* (exchange, request) -> {
* String content = readFile(request.getPath());
* return new ReadResourceResult(content);
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsListResult.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsListResult.java
new file mode 100644
index 000000000..a36c20d2b
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsListResult.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.server;
+
+import java.util.List;
+
+import io.modelcontextprotocol.spec.McpSchema;
+
+/**
+ * Result of a tools list operation from a {@link ToolsRepository}. Follows the MCP
+ * {@code tools/list} response structure.
+ *
+ * @param tools The list of tools found in the repository
+ * @param nextCursor An opaque token representing the next page of results, or null if
+ * there are no more results. The meaning and structure of this token is defined by the
+ * repository implementation.
+ */
+public record ToolsListResult(List tools, String nextCursor) {
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsRepository.java
new file mode 100644
index 000000000..30b9cacca
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsRepository.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.server;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * Repository interface for managing MCP tools with context-aware operations.
+ *
+ * This interface allows custom implementations to provide dynamic tool availability and
+ * access control based on the client session context.
+ *
+ */
+public interface ToolsRepository {
+
+ /**
+ * List tools visible to the given exchange context with pagination support.
+ * @param exchange The client exchange context containing transport details (e.g.,
+ * headers); may be {@code null} for context-free listing
+ * @param cursor An opaque pagination token. If null, the first page of results is
+ * returned. The structure and meaning of this token is implementation-defined.
+ * @return A {@link Mono} emitting the {@link ToolsListResult} containing visible
+ * tools and optional next cursor.
+ */
+ Mono listTools(McpAsyncServerExchange exchange, String cursor);
+
+ /**
+ * Resolve a tool specification for execution by name.
+ *
+ * SECURITY NOTE: Implementations capable of access control SHOULD
+ * verify permission here. It is not sufficient to simply check if the tool exists. If
+ * the tool exists but the current context is not allowed to execute it, this method
+ * SHOULD return an empty Mono.
+ *
+ * @param name The name of the tool to execute
+ * @param exchange The client exchange context
+ * @return A {@link Mono} emitting the
+ * {@link McpServerFeatures.AsyncToolSpecification} if found AND allowed, otherwise
+ * empty.
+ */
+ Mono resolveToolForCall(String name, McpAsyncServerExchange exchange);
+
+ /**
+ * Add a tool to the repository at runtime.
+ *
+ * Implementations should ensure thread safety. If a tool with the same name already
+ * exists, the behavior is implementation-defined (typically overwrites).
+ *
+ * @param tool The tool specification to add
+ */
+ void addTool(McpServerFeatures.AsyncToolSpecification tool);
+
+ /**
+ * Remove a tool from the repository at runtime by name.
+ * @param name The name of the tool to remove
+ */
+ void removeTool(String name);
+
+}
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
index d6677ec9a..549dea53f 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java
@@ -1,11 +1,13 @@
/*
- * Copyright 2024-2024 the original author or authors.
+ * Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.server;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
@@ -306,6 +308,109 @@ void testNotifyToolsListChanged() {
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}
+ @Test
+ void testListToolsDelegatesToCustomRepository() {
+ // Create a spy repository to verify wiring
+ var listToolsCalled = new boolean[1];
+ var addToolCalled = new boolean[1];
+
+ Tool testTool = McpSchema.Tool.builder()
+ .name("custom-repo-tool")
+ .title("Custom Repository Tool")
+ .inputSchema(EMPTY_JSON_SCHEMA)
+ .build();
+
+ ToolsRepository spyRepository = new ToolsRepository() {
+ private final ConcurrentHashMap tools = new ConcurrentHashMap<>();
+
+ @Override
+ public Mono listTools(McpAsyncServerExchange exchange, String cursor) {
+ listToolsCalled[0] = true;
+ return Mono.just(new ToolsListResult(
+ tools.values().stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList(), null));
+ }
+
+ @Override
+ public Mono resolveToolForCall(String name,
+ McpAsyncServerExchange exchange) {
+ return Mono.justOrEmpty(tools.get(name));
+ }
+
+ @Override
+ public void addTool(McpServerFeatures.AsyncToolSpecification tool) {
+ addToolCalled[0] = true;
+ tools.put(tool.tool().name(), tool);
+ }
+
+ @Override
+ public void removeTool(String name) {
+ tools.remove(name);
+ }
+ };
+
+ var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
+ .capabilities(ServerCapabilities.builder().tools(true).build())
+ .toolsRepository(spyRepository)
+ .build();
+
+ // Add a tool
+ StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder()
+ .tool(testTool)
+ .callHandler((exchange, request) -> Mono
+ .just(CallToolResult.builder().content(List.of()).isError(false).build()))
+ .build())).verifyComplete();
+
+ assertThat(addToolCalled[0]).isTrue();
+
+ // List tools should call the repository and contain the added tool
+ StepVerifier.create(mcpAsyncServer.listTools().collectList()).assertNext(tools -> {
+ assertThat(listToolsCalled[0]).isTrue();
+ assertThat(tools).anyMatch(t -> "custom-repo-tool".equals(t.name()));
+ }).verifyComplete();
+
+ assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
+ }
+
+ @Test
+ void testListToolsPassesNullCursorOnFirstCall() {
+ // Verify first page request passes null cursor
+ var receivedCursors = new ArrayList();
+
+ ToolsRepository cursorTrackingRepository = new ToolsRepository() {
+ @Override
+ public Mono listTools(McpAsyncServerExchange exchange, String cursor) {
+ receivedCursors.add(cursor);
+ return Mono.just(new ToolsListResult(List.of(), null));
+ }
+
+ @Override
+ public Mono resolveToolForCall(String name,
+ McpAsyncServerExchange exchange) {
+ return Mono.empty();
+ }
+
+ @Override
+ public void addTool(McpServerFeatures.AsyncToolSpecification tool) {
+ }
+
+ @Override
+ public void removeTool(String name) {
+ }
+ };
+
+ var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
+ .capabilities(ServerCapabilities.builder().tools(true).build())
+ .toolsRepository(cursorTrackingRepository)
+ .build();
+
+ // First call should have null cursor (first page)
+ StepVerifier.create(mcpAsyncServer.listTools().collectList()).assertNext(tools -> {
+ assertThat(receivedCursors).containsExactly(null);
+ }).verifyComplete();
+
+ assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
+ }
+
// ---------------------------------------
// Resources Tests
// ---------------------------------------