Skip to content
Merged
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
Expand Up @@ -322,25 +322,24 @@ private McpNotificationHandler asyncRootsListChangedNotificationHandler(
*/
public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification == null) {
return Mono.error(new McpError("Tool specification must not be null"));
return Mono.error(new IllegalArgumentException("Tool specification must not be null"));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that those are runtime server errors that are not propagated over the wire to the client.

}
if (toolSpecification.tool() == null) {
return Mono.error(new McpError("Tool must not be null"));
return Mono.error(new IllegalArgumentException("Tool must not be null"));
}
if (toolSpecification.call() == null && toolSpecification.callHandler() == null) {
return Mono.error(new McpError("Tool call handler must not be null"));
return Mono.error(new IllegalArgumentException("Tool call handler must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}

var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);

return Mono.defer(() -> {
// Check for duplicate tool names
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) {
return Mono.error(
new McpError("Tool with name '" + wrappedToolSpecification.tool().name() + "' already exists"));
// 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);
Expand Down Expand Up @@ -464,30 +463,40 @@ private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHand
.build();
}

/**
* List all registered tools.
* @return A Flux stream of all registered tools
*/
public Flux<Tool> listTools() {
return Flux.fromIterable(this.tools).map(McpServerFeatures.AsyncToolSpecification::tool);
}

/**
* Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removeTool(String toolName) {
if (toolName == null) {
return Mono.error(new McpError("Tool name must not be null"));
return Mono.error(new IllegalArgumentException("Tool name must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}

return Mono.defer(() -> {
boolean removed = this.tools
.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName));
if (removed) {
if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) {

logger.debug("Removed tool handler: {}", toolName);
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
}
return Mono.empty();
}
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
else {
logger.warn("Ignore as a Tool with name '{}' not found", toolName);
}

return Mono.empty();
});
}

Expand Down Expand Up @@ -518,8 +527,10 @@ private McpRequestHandler<CallToolResult> toolsCallRequestHandler() {
.findAny();

if (toolSpecification.isEmpty()) {
return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS,
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name())));
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);
Expand Down Expand Up @@ -747,58 +758,63 @@ private Optional<McpServerFeatures.AsyncResourceTemplateSpecification> findResou
*/
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
if (promptSpecification == null) {
return Mono.error(new McpError("Prompt specification must not be null"));
return Mono.error(new IllegalArgumentException("Prompt specification must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with prompt capabilities"));
}

return Mono.defer(() -> {
McpServerFeatures.AsyncPromptSpecification specification = this.prompts
.putIfAbsent(promptSpecification.prompt().name(), promptSpecification);
if (specification != null) {
return Mono.error(
new McpError("Prompt with name '" + promptSpecification.prompt().name() + "' already exists"));
var previous = this.prompts.put(promptSpecification.prompt().name(), promptSpecification);
if (previous != null) {
logger.warn("Replace existing Prompt with name '{}'", promptSpecification.prompt().name());
}
else {
logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());
}

logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());

// Servers that declared the listChanged capability SHOULD send a
// notification,
// when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return notifyPromptsListChanged();
return this.notifyPromptsListChanged();
}

return Mono.empty();
});
}

/**
* List all registered prompts.
* @return A Flux stream of all registered prompts
*/
public Flux<McpSchema.Prompt> listPrompts() {
return Flux.fromIterable(this.prompts.values()).map(McpServerFeatures.AsyncPromptSpecification::prompt);
}

/**
* Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removePrompt(String promptName) {
if (promptName == null) {
return Mono.error(new McpError("Prompt name must not be null"));
return Mono.error(new IllegalArgumentException("Prompt name must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with prompt capabilities"));
}

return Mono.defer(() -> {
McpServerFeatures.AsyncPromptSpecification removed = this.prompts.remove(promptName);

if (removed != null) {
logger.debug("Removed prompt handler: {}", promptName);
// Servers that declared the listChanged capability SHOULD send a
// notification, when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return this.notifyPromptsListChanged();
}
return Mono.empty();
}
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
else {
logger.warn("Ignore as a Prompt with name '{}' not found", promptName);
}
return Mono.empty();
});
}

Expand Down Expand Up @@ -834,8 +850,12 @@ private McpRequestHandler<McpSchema.GetPromptResult> promptsGetRequestHandler()

// Implement prompt retrieval logic here
McpServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name());

if (specification == null) {
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
.message("Invalid prompt name")
.data("Prompt not found: " + promptRequest.name())
.build());
}

return Mono.defer(() -> specification.promptHandler().apply(exchange, promptRequest));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,25 +319,24 @@ public Mono<CallToolResult> apply(McpTransportContext transportContext, McpSchem
*/
public Mono<Void> addTool(McpStatelessServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification == null) {
return Mono.error(new McpError("Tool specification must not be null"));
return Mono.error(new IllegalArgumentException("Tool specification must not be null"));
}
if (toolSpecification.tool() == null) {
return Mono.error(new McpError("Tool must not be null"));
return Mono.error(new IllegalArgumentException("Tool must not be null"));
}
if (toolSpecification.callHandler() == null) {
return Mono.error(new McpError("Tool call handler must not be null"));
return Mono.error(new IllegalArgumentException("Tool call handler must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}

var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);

return Mono.defer(() -> {
// Check for duplicate tool names
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) {
return Mono.error(
new McpError("Tool with name '" + wrappedToolSpecification.tool().name() + "' already exists"));
// 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);
Expand All @@ -347,27 +346,37 @@ public Mono<Void> addTool(McpStatelessServerFeatures.AsyncToolSpecification tool
});
}

/**
* List all registered tools.
* @return A Flux stream of all registered tools
*/
public Flux<Tool> listTools() {
return Flux.fromIterable(this.tools).map(McpStatelessServerFeatures.AsyncToolSpecification::tool);
}

/**
* Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removeTool(String toolName) {
if (toolName == null) {
return Mono.error(new McpError("Tool name must not be null"));
return Mono.error(new IllegalArgumentException("Tool name must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}

return Mono.defer(() -> {
boolean removed = this.tools
.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName));
if (removed) {
if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) {

logger.debug("Removed tool handler: {}", toolName);
return Mono.empty();
}
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
else {
logger.warn("Ignore as a Tool with name '{}' not found", toolName);
}

return Mono.empty();
});
}

Expand All @@ -391,8 +400,10 @@ private McpStatelessRequestHandler<CallToolResult> toolsCallRequestHandler() {
.findAny();

if (toolSpecification.isEmpty()) {
return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS,
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name())));
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(ctx, callToolRequest);
Expand Down Expand Up @@ -593,37 +604,45 @@ private Optional<McpStatelessServerFeatures.AsyncResourceTemplateSpecification>
*/
public Mono<Void> addPrompt(McpStatelessServerFeatures.AsyncPromptSpecification promptSpecification) {
if (promptSpecification == null) {
return Mono.error(new McpError("Prompt specification must not be null"));
return Mono.error(new IllegalArgumentException("Prompt specification must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with prompt capabilities"));
}

return Mono.defer(() -> {
McpStatelessServerFeatures.AsyncPromptSpecification specification = this.prompts
.putIfAbsent(promptSpecification.prompt().name(), promptSpecification);
if (specification != null) {
return Mono.error(
new McpError("Prompt with name '" + promptSpecification.prompt().name() + "' already exists"));
var previous = this.prompts.put(promptSpecification.prompt().name(), promptSpecification);
if (previous != null) {
logger.warn("Replace existing Prompt with name '{}'", promptSpecification.prompt().name());
}
else {
logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());
}

logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());

return Mono.empty();
});
}

/**
* List all registered prompts.
* @return A Flux stream of all registered prompts
*/
public Flux<McpSchema.Prompt> listPrompts() {
return Flux.fromIterable(this.prompts.values())
.map(McpStatelessServerFeatures.AsyncPromptSpecification::prompt);
}

/**
* Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removePrompt(String promptName) {
if (promptName == null) {
return Mono.error(new McpError("Prompt name must not be null"));
return Mono.error(new IllegalArgumentException("Prompt name must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
return Mono.error(new IllegalStateException("Server must be configured with prompt capabilities"));
}

return Mono.defer(() -> {
Expand All @@ -633,7 +652,11 @@ public Mono<Void> removePrompt(String promptName) {
logger.debug("Removed prompt handler: {}", promptName);
return Mono.empty();
}
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
else {
logger.warn("Ignore as a Prompt with name '{}' not found", promptName);
}

return Mono.empty();
});
}

Expand Down Expand Up @@ -662,7 +685,10 @@ private McpStatelessRequestHandler<McpSchema.GetPromptResult> promptsGetRequestH
// Implement prompt retrieval logic here
McpStatelessServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name());
if (specification == null) {
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
.message("Invalid prompt name")
.data("Prompt not found: " + promptRequest.name())
.build());
}

return specification.promptHandler().apply(ctx, promptRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ public void addTool(McpStatelessServerFeatures.SyncToolSpecification toolSpecifi
.block();
}

/**
* List all registered tools.
* @return A list of all registered tools
*/
public List<McpSchema.Tool> listTools() {
return this.asyncServer.listTools().collectList().block();
}

/**
* Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove
Expand Down Expand Up @@ -148,6 +156,14 @@ public void addPrompt(McpStatelessServerFeatures.SyncPromptSpecification promptS
.block();
}

/**
* List all registered prompts.
* @return A list of all registered prompts
*/
public List<McpSchema.Prompt> listPrompts() {
return this.asyncServer.listPrompts().collectList().block();
}

/**
* Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove
Expand Down
Loading