From ff96947707a0d636713b77adab8c2d299dd80e61 Mon Sep 17 00:00:00 2001 From: ultramancode Date: Tue, 13 Jan 2026 22:14:35 +0900 Subject: [PATCH] feat(core): implement dynamic tool management with filtering and cursor pagination - Introduce ToolsRepository for runtime tool management - Support context-aware tool filtering via client exchange - Implement cursor-based pagination for large toolsets - Maintain backward compatibility for existing static registration --- .../server/InMemoryToolsRepository.java | 77 ++++++++++ .../server/McpAsyncServer.java | 88 ++++++----- .../server/McpServer.java | 144 +++++++++++++----- .../server/McpServerFeatures.java | 34 +++-- .../server/ToolsListResult.java | 21 +++ .../server/ToolsRepository.java | 61 ++++++++ .../server/AbstractMcpAsyncServerTests.java | 107 ++++++++++++- 7 files changed, 442 insertions(+), 90 deletions(-) create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/InMemoryToolsRepository.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsListResult.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsRepository.java 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 tools = new ConcurrentHashMap<>(); + + /** + * Create a new empty InMemoryToolsRepository. + */ + public InMemoryToolsRepository() { + } + + /** + * Create a new InMemoryToolsRepository initialized with the given tools. + * @param initialTools Collection of tools to register initially + */ + public InMemoryToolsRepository(List initialTools) { + if (initialTools != null) { + for (McpServerFeatures.AsyncToolSpecification tool : initialTools) { + tools.put(tool.tool().name(), tool); + } + } + } + + @Override + public Mono listTools(McpAsyncServerExchange exchange, String cursor) { + // Ensure stable tool ordering for MCP clients, as ConcurrentHashMap does not + // guarantee iteration order + List toolList = tools.values() + .stream() + .map(McpServerFeatures.AsyncToolSpecification::tool) + .sorted(Comparator.comparing(McpSchema.Tool::name)) + .toList(); + + return Mono.just(new ToolsListResult(toolList, null)); + } + + @Override + public Mono resolveToolForCall(String name, + McpAsyncServerExchange exchange) { + // Default behavior: finding = allowing. + // Use a custom ToolsRepository implementation for context-aware access control. + return Mono.justOrEmpty(tools.get(name)); + } + + @Override + public void addTool(McpServerFeatures.AsyncToolSpecification tool) { + // Last-write-wins policy + tools.put(tool.tool().name(), tool); + } + + @Override + public void removeTool(String name) { + tools.remove(name); + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 23285d514..3501f3444 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2024 the original author or authors. + * Copyright 2024-2026 the original author or authors. */ package io.modelcontextprotocol.server; @@ -11,7 +11,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiFunction; import io.modelcontextprotocol.json.McpJsonMapper; @@ -103,7 +102,7 @@ public class McpAsyncServer { private final String instructions; - private final CopyOnWriteArrayList tools = new CopyOnWriteArrayList<>(); + private final ToolsRepository toolsRepository; private final ConcurrentHashMap resources = new ConcurrentHashMap<>(); @@ -136,7 +135,8 @@ public class McpAsyncServer { this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities().mutate().logging().build(); this.instructions = features.instructions(); - this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools())); + this.toolsRepository = initializeToolsRepository(features.toolsRepository(), jsonSchemaValidator, + features.tools()); this.resources.putAll(features.resources()); this.resourceTemplates.putAll(features.resourceTemplates()); this.prompts.putAll(features.prompts()); @@ -153,6 +153,27 @@ public class McpAsyncServer { requestTimeout, transport, this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers)); } + /** + * Initialize the tools repository, wrapping tools with structured output handling. + */ + private ToolsRepository initializeToolsRepository(ToolsRepository providedRepository, + JsonSchemaValidator jsonSchemaValidator, List initialTools) { + if (providedRepository != null) { + // Add initial tools to the provided repository with structured output + // handling + if (initialTools != null) { + for (McpServerFeatures.AsyncToolSpecification tool : initialTools) { + providedRepository.addTool(withStructuredOutputHandling(jsonSchemaValidator, tool)); + } + } + return providedRepository; + } + // Create default in-memory repository with wrapped tools + List wrappedTools = withStructuredOutputHandling(jsonSchemaValidator, + initialTools); + return new InMemoryToolsRepository(wrappedTools); + } + McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper, McpServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) { @@ -161,7 +182,8 @@ public class McpAsyncServer { this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities().mutate().logging().build(); this.instructions = features.instructions(); - this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools())); + this.toolsRepository = initializeToolsRepository(features.toolsRepository(), jsonSchemaValidator, + features.tools()); this.resources.putAll(features.resources()); this.resourceTemplates.putAll(features.resourceTemplates()); this.prompts.putAll(features.prompts()); @@ -336,12 +358,7 @@ public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification); return Mono.defer(() -> { - // Remove tools with duplicate tool names first - if (this.tools.removeIf(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) { - logger.warn("Replace existing Tool with name '{}'", wrappedToolSpecification.tool().name()); - } - - this.tools.add(wrappedToolSpecification); + this.toolsRepository.addTool(wrappedToolSpecification); logger.debug("Added tool handler: {}", wrappedToolSpecification.tool().name()); if (this.serverCapabilities.tools().listChanged()) { @@ -471,7 +488,11 @@ private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHand * @return A Flux stream of all registered tools */ public Flux listTools() { - return Flux.fromIterable(this.tools).map(McpServerFeatures.AsyncToolSpecification::tool); + // Note: This method returns all tools without exchange context. + // For context-aware listing, use toolsRepository.listTools(exchange, cursor) + // directly. + return Flux.defer( + () -> toolsRepository.listTools(null, null).flatMapMany(result -> Flux.fromIterable(result.tools()))); } /** @@ -488,17 +509,11 @@ public Mono removeTool(String toolName) { } return Mono.defer(() -> { - if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) { - - logger.debug("Removed tool handler: {}", toolName); - if (this.serverCapabilities.tools().listChanged()) { - return notifyToolsListChanged(); - } - } - else { - logger.warn("Ignore as a Tool with name '{}' not found", toolName); + this.toolsRepository.removeTool(toolName); + logger.debug("Requested tool removal: {}", toolName); + if (this.serverCapabilities.tools().listChanged()) { + return notifyToolsListChanged(); } - return Mono.empty(); }); } @@ -513,9 +528,16 @@ public Mono notifyToolsListChanged() { private McpRequestHandler toolsListRequestHandler() { return (exchange, params) -> { - List tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList(); - - return Mono.just(new McpSchema.ListToolsResult(tools, null)); + // Extract cursor from params if present + String cursor = null; + if (params != null) { + McpSchema.PaginatedRequest paginatedRequest = jsonMapper.convertValue(params, + new TypeRef() { + }); + cursor = paginatedRequest.cursor(); + } + return this.toolsRepository.listTools(exchange, cursor) + .map(result -> new McpSchema.ListToolsResult(result.tools(), result.nextCursor())); }; } @@ -525,18 +547,10 @@ private McpRequestHandler toolsCallRequestHandler() { new TypeRef() { }); - Optional toolSpecification = this.tools.stream() - .filter(tr -> callToolRequest.name().equals(tr.tool().name())) - .findAny(); - - if (toolSpecification.isEmpty()) { - return Mono.error(McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS) - .message("Unknown tool: invalid_tool_name") - .data("Tool not found: " + callToolRequest.name()) - .build()); - } - - return toolSpecification.get().callHandler().apply(exchange, callToolRequest); + return this.toolsRepository.resolveToolForCall(callToolRequest.name(), exchange) + .switchIfEmpty(Mono + .error(McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND).message("Tool not found").build())) + .flatMap(spec -> spec.callHandler().apply(exchange, callToolRequest)); }; } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java index fe3125271..bca4e7066 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.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; @@ -63,31 +63,37 @@ * * *

- * 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 // ---------------------------------------