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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* 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.
* </p>
*/
public class InMemoryToolsRepository implements ToolsRepository {

private final ConcurrentHashMap<String, McpServerFeatures.AsyncToolSpecification> 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<McpServerFeatures.AsyncToolSpecification> initialTools) {
if (initialTools != null) {
for (McpServerFeatures.AsyncToolSpecification tool : initialTools) {
tools.put(tool.tool().name(), tool);
}
}
}

@Override
public Mono<ToolsListResult> listTools(McpAsyncServerExchange exchange, String cursor) {
// Ensure stable tool ordering for MCP clients, as ConcurrentHashMap does not
// guarantee iteration order
List<McpSchema.Tool> 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<McpServerFeatures.AsyncToolSpecification> 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);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2024 the original author or authors.
* Copyright 2024-2026 the original author or authors.
*/

package io.modelcontextprotocol.server;
Expand All @@ -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;
Expand Down Expand Up @@ -103,7 +102,7 @@ public class McpAsyncServer {

private final String instructions;

private final CopyOnWriteArrayList<McpServerFeatures.AsyncToolSpecification> tools = new CopyOnWriteArrayList<>();
private final ToolsRepository toolsRepository;

private final ConcurrentHashMap<String, McpServerFeatures.AsyncResourceSpecification> resources = new ConcurrentHashMap<>();

Expand Down Expand Up @@ -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());
Expand All @@ -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<McpServerFeatures.AsyncToolSpecification> 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<McpServerFeatures.AsyncToolSpecification> wrappedTools = withStructuredOutputHandling(jsonSchemaValidator,
initialTools);
return new InMemoryToolsRepository(wrappedTools);
}

McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
Expand All @@ -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());
Expand Down Expand Up @@ -336,12 +358,7 @@ public Mono<Void> 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()) {
Expand Down Expand Up @@ -471,7 +488,11 @@ private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHand
* @return A Flux stream of all registered tools
*/
public Flux<Tool> 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())));
}

/**
Expand All @@ -488,17 +509,11 @@ public Mono<Void> 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();
});
}
Expand All @@ -513,9 +528,16 @@ public Mono<Void> notifyToolsListChanged() {

private McpRequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return (exchange, params) -> {
List<Tool> 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<McpSchema.PaginatedRequest>() {
});
cursor = paginatedRequest.cursor();
}
return this.toolsRepository.listTools(exchange, cursor)
.map(result -> new McpSchema.ListToolsResult(result.tools(), result.nextCursor()));
};
}

Expand All @@ -525,18 +547,10 @@ private McpRequestHandler<CallToolResult> toolsCallRequestHandler() {
new TypeRef<McpSchema.CallToolRequest>() {
});

Optional<McpServerFeatures.AsyncToolSpecification> 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));
};
}

Expand Down
Loading