You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This PR introduces the ToolsRepository interface and its default implementation (InMemoryToolsRepository) to enable dynamic, context-aware tool management in McpAsyncServer.
Motivation
Currently, McpAsyncServer only supports static tool registration during initialization. This limits flexibility for scenarios like:
Dynamic plugin loading: Tools loaded from external sources at runtime
Multi-tenant filtering: Different clients see different tool sets based on their context
Permission-based access control: Admin-only tools hidden from regular users
Enterprise Scalability: Efficiently handling large-scale toolsets without overwhelming the client or the network. Spec-aligned cursor-based pagination is implemented to minimize payload overhead
Changes
New Files
File
Description
ToolsRepository.java
Interface for tool management with context-aware operations
InMemoryToolsRepository.java
Thread-safe default implementation using ConcurrentHashMap
ToolsListResult.java
Record encapsulating list response with pagination cursor
Modified Files
File
Changes
McpAsyncServer.java
Delegates tool operations to ToolsRepository
McpServerFeatures.java
Added toolsRepository field to builder
McpServer.java
Builder now supports .toolsRepository() method
Technical Considerations
Cursor-based Pagination: The implementation uses cursor tokens (following the MCP spec). The cursor is opaque to clients; while the examples below use a simple numeric offset for demonstration, production implementations can choose stable tokens (e.g., signed/encoded/timestamp-based) to ensure data stability.
ToolsListResult Encapsulation: Response is encapsulated in ToolsListResult to cleanly separate the list of tools from the metadata (cursor), matching the MCP tools/list structure.
Separation of Concerns: listTools handles visibility ("What tools exist?"), while resolveToolForCall handles executability ("Can I run this?").
Important
Security Note: Hiding a tool in listTools is NOT sufficient for security. Implementations should also verify permissions in resolveToolForCall.
Backward Compatibility: Existing .tools() API continues to work unchanged. If no custom repository is provided, InMemoryToolsRepository is automatically used.
Thread Safety: InMemoryToolsRepository uses ConcurrentHashMap for safe concurrent access.
Extensibility: Developers can implement custom repositories (e.g., Database-backed, Redis-backed) by implementing the ToolsRepository interface.
Example: Filtering by Client Context and Pagination
Example code
publicclassRoleBasedToolsRepositoryimplementsToolsRepository {
privatestaticfinalStringROLE_ADMIN = "ADMIN";
privatestaticfinalStringROLE_USER = "USER";
privatestaticfinalStringADMIN_TOOL_PREFIX = "admin-";
privatefinalList<McpServerFeatures.AsyncToolSpecification> tools = newCopyOnWriteArrayList<>();
@OverridepublicMono<ToolsListResult> listTools(McpAsyncServerExchangeexchange, Stringcursor) {
// 1. Extract role from exchange (e.g., from attributes or transport header)Stringrole = extractRole(exchange);
// 2. Filter tools based on roleList<McpServerFeatures.AsyncToolSpecification> visibleTools = tools.stream()
// Admin tools are visible only to ADMINs
.filter(spec -> !spec.tool().name().startsWith(ADMIN_TOOL_PREFIX) || ROLE_ADMIN.equals(role))
.toList();
// 3. Pagination (With safety guards)intpageSize = 10;
intstart = parseCursor(cursor);
if (start >= visibleTools.size()) {
returnMono.just(newToolsListResult(List.of(), null));
}
intend = Math.min(start + pageSize, visibleTools.size());
List<McpSchema.Tool> pageContent = visibleTools.subList(start, end).stream()
.map(McpServerFeatures.AsyncToolSpecification::tool)
.toList();
StringnextCursor = (end < visibleTools.size()) ? String.valueOf(end) : null;
returnMono.just(newToolsListResult(pageContent, nextCursor));
}
privateintparseCursor(Stringcursor) {
if (cursor == null) return0;
try {
returnInteger.parseInt(cursor);
} catch (NumberFormatExceptione) {
return0;
}
}
@OverridepublicMono<McpServerFeatures.AsyncToolSpecification> resolveToolForCall(Stringname, McpAsyncServerExchangeexchange) {
Stringrole = extractRole(exchange);
// Security Check: Prevent unauthorized accessif (name.startsWith(ADMIN_TOOL_PREFIX) && !ROLE_ADMIN.equals(role)) {
returnMono.empty();
}
returnMono.justOrEmpty(
tools.stream().filter(t -> t.tool().name().equals(name)).findFirst()
);
}
privateStringextractRole(McpAsyncServerExchangeexchange) {
// Example: Extract role from custom attributes or transport headers// In the verification scenario, this matches the 'X-Role' headerObjectrole = exchange.transportContext().get("X-Role");
returnrole != null ? role.toString() : ROLE_USER;
}
}
Pagination Best Practices
Stable sorting: Ensure consistent ordering between page requests
Context isolation: Cursors should be valid only for the same exchange context (tenant/role)
Use null for end: Return null (not empty string) for nextCursor when no more pages exist
Verification Results
The implementation was verified using a sample Spring Boot application to confirm that filtering and pagination work as intended in different role-based scenarios.
Scenario 1: Standard User Access (Filtering & Pagination)
Condition: Request with X-Role: USER header.
Setup: 5 tools total (2 admin tools, 3 public tools).
Behavior:
Filtering: Admin tools are correctly hidden from the list.
Pagination: Successfully returns visible tools with a valid nextCursor.
Result: page1 returned 2 public tools. nextCursor is "2", pointing to the next available tool.
My code follows the repository's style guidelines (spring-javaformat:apply)
New and existing tests pass locally
I have added appropriate error handling
I have added or updated documentation as needed (Javadoc added)
Additional context
Design Decision: The ToolsRepository was introduced to decouple tool storage from the core engine, enabling pluggable and dynamic tool management.
Opaque Cursors: While the examples use numeric offsets, the implementation treats cursors as opaque strings to allow for diverse backend implementations (e.g., Redis, DB, or Encrypted tokens).
Important
Security Note: Hiding a tool in listTools is NOT sufficient for security. Implementation developers SHOULD also verify permissions in resolveToolForCall to prevent unauthorized execution.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implement ToolsRepository for Dynamic Tool Management & Cursor-based Pagination
Closes #746
Summary
This PR introduces the
ToolsRepositoryinterface and its default implementation (InMemoryToolsRepository) to enable dynamic, context-aware tool management inMcpAsyncServer.Motivation
Currently,
McpAsyncServeronly supports static tool registration during initialization. This limits flexibility for scenarios like:Changes
New Files
ToolsRepository.javaInMemoryToolsRepository.javaConcurrentHashMapToolsListResult.javaModified Files
McpAsyncServer.javaToolsRepositoryMcpServerFeatures.javatoolsRepositoryfield to builderMcpServer.java.toolsRepository()methodTechnical Considerations
Cursor-based Pagination: The implementation uses cursor tokens (following the MCP spec). The cursor is opaque to clients; while the examples below use a simple numeric offset for demonstration, production implementations can choose stable tokens (e.g., signed/encoded/timestamp-based) to ensure data stability.
ToolsListResult Encapsulation: Response is encapsulated in
ToolsListResultto cleanly separate the list of tools from the metadata (cursor), matching the MCPtools/liststructure.Separation of Concerns:
listToolshandles visibility ("What tools exist?"), whileresolveToolForCallhandles executability ("Can I run this?").Important
Security Note: Hiding a tool in
listToolsis NOT sufficient for security. Implementations should also verify permissions inresolveToolForCall..tools()API continues to work unchanged. If no custom repository is provided,InMemoryToolsRepositoryis automatically used.InMemoryToolsRepositoryusesConcurrentHashMapfor safe concurrent access.ToolsRepositoryinterface.API Usage
Existing Code (No Changes Required)
Custom Repository
Example: Filtering by Client Context and Pagination
Example code
Verification Results
The implementation was verified using a sample Spring Boot application to confirm that filtering and pagination work as intended in different role-based scenarios.
Scenario 1: Standard User Access (Filtering & Pagination)
- Condition: Request with
- Setup: 5 tools total (2 admin tools, 3 public tools).
- Behavior:
- Filtering: Admin tools are correctly hidden from the list.
- Pagination: Successfully returns visible tools with a valid
- Result:
- View User Request Screenshot (Postman)
mcp_userX-Role: USERheader.nextCursor.page1returned 2 public tools.nextCursoris "2", pointing to the next available tool.Scenario 2: Administrator Access (Full Visibility)
- Condition: Request with
- Behavior: All registered tools, including administrative ones, are visible.
- Result: All 5 tools were returned across pages as expected.
- View Admin Request Screenshot (Postman)
mcp_adminX-Role: ADMINheader.Types of changes
Checklist
spring-javaformat:apply)Additional context
ToolsRepositorywas introduced to decouple tool storage from the core engine, enabling pluggable and dynamic tool management.Important
Security Note: Hiding a tool in
listToolsis NOT sufficient for security. Implementation developers SHOULD also verify permissions inresolveToolForCallto prevent unauthorized execution.