refactor: improve MCP server resilience and error handling (#579)

- Replace McpError with appropriate standard exceptions (IllegalArgumentException, IllegalStateException) for validation errors
- Change tool/prompt registration to replace existing items instead of rejecting duplicates with warningsi
  - Change addTool behavior to replace existing tools instead of throwing errors
  - Change addPrompt() to allow replacing existing prompts with warning instead of throwing error
- Make removal operations idempotent - log warnings instead of throwing errors for non-existent items
  - Change removePrompt() to log warning instead of throwing error for non-existent prompts
  - Change removeTool() to gracefully handle non-existent tools with warnings instead of errors
- Add listTools() and listPrompts() methods to all server variants (async, sync, stateless)
  - Add listTools() method to all server classes for tool enumeration
  - Add listPrompts() method to all server implementations for retrieving registered prompts
- Improve error construction using McpError.builder() pattern for protocol-specific errors
- Update error handling in session classes to properly propagate McpError JSON-RPC errors
  - Use proper error codes (INVALID_PARAMS) for prompt not found scenarios
- Update tests to reflect new lenient behavior for duplicate registrations and removals
- Add aggregateExceptionMessages() utility method utility to aggregate the exception chains
- Update McpServerSession and McpStreamableServerSession to use the aggregated errors in the json-rpc-error data section

This change makes the MCP server APIs more resilient and user-friendly by using appropriate exception types,
supporting item replacement, and providing listing capabilities while maintaining backward compatibility.

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-09-30 11:10:04 +02:00 • committed by GitHub
commit c1cde20002
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 305 additions and 196 deletions

View file

@ -322,25 +322,24 @@ public class McpAsyncServer {
*/ */
public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) { public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification == null) { 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) { 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) { 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) { 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); var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);
return Mono.defer(() -> { return Mono.defer(() -> {
// Check for duplicate tool names // Remove tools with duplicate tool names first
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) { if (this.tools.removeIf(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) {
return Mono.error( logger.warn("Replace existing Tool with name '{}'", wrappedToolSpecification.tool().name());
new McpError("Tool with name '" + wrappedToolSpecification.tool().name() + "' already exists"));
} }
this.tools.add(wrappedToolSpecification); this.tools.add(wrappedToolSpecification);
@ -464,6 +463,14 @@ public class McpAsyncServer {
.build(); .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. * Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove * @param toolName The name of the tool handler to remove
@ -471,23 +478,25 @@ public class McpAsyncServer {
*/ */
public Mono<Void> removeTool(String toolName) { public Mono<Void> removeTool(String toolName) {
if (toolName == null) { 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) { 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(() -> { return Mono.defer(() -> {
boolean removed = this.tools if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) {
.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName));
if (removed) {
logger.debug("Removed tool handler: {}", toolName); logger.debug("Removed tool handler: {}", toolName);
if (this.serverCapabilities.tools().listChanged()) { if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged(); 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();
}); });
} }
@ -518,8 +527,10 @@ public class McpAsyncServer {
.findAny(); .findAny();
if (toolSpecification.isEmpty()) { if (toolSpecification.isEmpty()) {
return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS, return Mono.error(McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS)
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name()))); .message("Unknown tool: invalid_tool_name")
.data("Tool not found: " + callToolRequest.name())
.build());
} }
return toolSpecification.get().callHandler().apply(exchange, callToolRequest); return toolSpecification.get().callHandler().apply(exchange, callToolRequest);
@ -747,32 +758,36 @@ public class McpAsyncServer {
*/ */
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) { public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
if (promptSpecification == null) { 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) { 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(() -> { return Mono.defer(() -> {
McpServerFeatures.AsyncPromptSpecification specification = this.prompts var previous = this.prompts.put(promptSpecification.prompt().name(), promptSpecification);
.putIfAbsent(promptSpecification.prompt().name(), promptSpecification); if (previous != null) {
if (specification != null) { logger.warn("Replace existing Prompt with name '{}'", promptSpecification.prompt().name());
return Mono.error(
new McpError("Prompt with name '" + promptSpecification.prompt().name() + "' already exists"));
} }
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();
} }
if (this.serverCapabilities.prompts().listChanged()) {
return this.notifyPromptsListChanged();
}
return Mono.empty(); 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. * Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove * @param promptName The name of the prompt handler to remove
@ -780,10 +795,10 @@ public class McpAsyncServer {
*/ */
public Mono<Void> removePrompt(String promptName) { public Mono<Void> removePrompt(String promptName) {
if (promptName == null) { 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) { 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(() -> { return Mono.defer(() -> {
@ -791,14 +806,15 @@ public class McpAsyncServer {
if (removed != null) { if (removed != null) {
logger.debug("Removed prompt handler: {}", promptName); 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()) { if (this.serverCapabilities.prompts().listChanged()) {
return this.notifyPromptsListChanged(); return this.notifyPromptsListChanged();
} }
return Mono.empty(); 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();
}); });
} }
@ -834,8 +850,12 @@ public class McpAsyncServer {
// Implement prompt retrieval logic here // Implement prompt retrieval logic here
McpServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name()); McpServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name());
if (specification == null) { 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)); return Mono.defer(() -> specification.promptHandler().apply(exchange, promptRequest));

View file

@ -319,25 +319,24 @@ public class McpStatelessAsyncServer {
*/ */
public Mono<Void> addTool(McpStatelessServerFeatures.AsyncToolSpecification toolSpecification) { public Mono<Void> addTool(McpStatelessServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification == null) { 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) { 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) { 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) { 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); var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);
return Mono.defer(() -> { return Mono.defer(() -> {
// Check for duplicate tool names // Remove tools with duplicate tool names first
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) { if (this.tools.removeIf(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) {
return Mono.error( logger.warn("Replace existing Tool with name '{}'", wrappedToolSpecification.tool().name());
new McpError("Tool with name '" + wrappedToolSpecification.tool().name() + "' already exists"));
} }
this.tools.add(wrappedToolSpecification); this.tools.add(wrappedToolSpecification);
@ -347,6 +346,14 @@ public class McpStatelessAsyncServer {
}); });
} }
/**
* 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. * Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove * @param toolName The name of the tool handler to remove
@ -354,20 +361,22 @@ public class McpStatelessAsyncServer {
*/ */
public Mono<Void> removeTool(String toolName) { public Mono<Void> removeTool(String toolName) {
if (toolName == null) { 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) { 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(() -> { return Mono.defer(() -> {
boolean removed = this.tools if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) {
.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName));
if (removed) {
logger.debug("Removed tool handler: {}", 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();
}); });
} }
@ -391,8 +400,10 @@ public class McpStatelessAsyncServer {
.findAny(); .findAny();
if (toolSpecification.isEmpty()) { if (toolSpecification.isEmpty()) {
return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS, return Mono.error(McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS)
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name()))); .message("Unknown tool: invalid_tool_name")
.data("Tool not found: " + callToolRequest.name())
.build());
} }
return toolSpecification.get().callHandler().apply(ctx, callToolRequest); return toolSpecification.get().callHandler().apply(ctx, callToolRequest);
@ -593,26 +604,34 @@ public class McpStatelessAsyncServer {
*/ */
public Mono<Void> addPrompt(McpStatelessServerFeatures.AsyncPromptSpecification promptSpecification) { public Mono<Void> addPrompt(McpStatelessServerFeatures.AsyncPromptSpecification promptSpecification) {
if (promptSpecification == null) { 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) { 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(() -> { return Mono.defer(() -> {
McpStatelessServerFeatures.AsyncPromptSpecification specification = this.prompts var previous = this.prompts.put(promptSpecification.prompt().name(), promptSpecification);
.putIfAbsent(promptSpecification.prompt().name(), promptSpecification); if (previous != null) {
if (specification != null) { logger.warn("Replace existing Prompt with name '{}'", promptSpecification.prompt().name());
return Mono.error(
new McpError("Prompt with name '" + promptSpecification.prompt().name() + "' already exists"));
} }
else {
logger.debug("Added prompt handler: {}", promptSpecification.prompt().name()); logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());
}
return Mono.empty(); 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. * Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove * @param promptName The name of the prompt handler to remove
@ -620,10 +639,10 @@ public class McpStatelessAsyncServer {
*/ */
public Mono<Void> removePrompt(String promptName) { public Mono<Void> removePrompt(String promptName) {
if (promptName == null) { 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) { 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(() -> { return Mono.defer(() -> {
@ -633,7 +652,11 @@ public class McpStatelessAsyncServer {
logger.debug("Removed prompt handler: {}", promptName); logger.debug("Removed prompt handler: {}", promptName);
return Mono.empty(); 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();
}); });
} }
@ -662,7 +685,10 @@ public class McpStatelessAsyncServer {
// Implement prompt retrieval logic here // Implement prompt retrieval logic here
McpStatelessServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name()); McpStatelessServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name());
if (specification == null) { 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); return specification.promptHandler().apply(ctx, promptRequest);

View file

@ -74,6 +74,14 @@ public class McpStatelessSyncServer {
.block(); .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. * Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove * @param toolName The name of the tool handler to remove
@ -148,6 +156,14 @@ public class McpStatelessSyncServer {
.block(); .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. * Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove * @param promptName The name of the prompt handler to remove

View file

@ -89,6 +89,14 @@ public class McpSyncServer {
.block(); .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. * Remove a tool handler.
* @param toolName The name of the tool handler to remove * @param toolName The name of the tool handler to remove
@ -162,6 +170,14 @@ public class McpSyncServer {
.block(); .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. * Remove a prompt handler.
* @param promptName The name of the prompt handler to remove * @param promptName The name of the prompt handler to remove

View file

@ -38,11 +38,12 @@ public class McpError extends RuntimeException {
@Override @Override
public String toString() { public String toString() {
var message = super.toString(); var builder = new StringBuilder(super.toString());
if (jsonRpcError != null) { if (jsonRpcError != null) {
return message + jsonRpcError.toString(); builder.append("\n");
builder.append(jsonRpcError.toString());
} }
return message; return builder.toString();
} }
public static Builder builder(int errorCode) { public static Builder builder(int errorCode) {
@ -78,4 +79,38 @@ public class McpError extends RuntimeException {
} }
public static Throwable findRootCause(Throwable throwable) {
Assert.notNull(throwable, "throwable must not be null");
Throwable rootCause = throwable;
while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
rootCause = rootCause.getCause();
}
return rootCause;
}
public static String aggregateExceptionMessages(Throwable throwable) {
Assert.notNull(throwable, "throwable must not be null");
StringBuilder messages = new StringBuilder();
Throwable current = throwable;
while (current != null) {
if (messages.length() > 0) {
messages.append("\n Caused by: ");
}
messages.append(current.getClass().getSimpleName());
if (current.getMessage() != null) {
messages.append(": ").append(current.getMessage());
}
if (current.getCause() == current) {
break;
}
current = current.getCause();
}
return messages.toString();
}
} }

View file

@ -224,9 +224,12 @@ public class McpServerSession implements McpLoggableSession {
else if (message instanceof McpSchema.JSONRPCRequest request) { else if (message instanceof McpSchema.JSONRPCRequest request) {
logger.debug("Received request: {}", request); logger.debug("Received request: {}", request);
return handleIncomingRequest(request, transportContext).onErrorResume(error -> { return handleIncomingRequest(request, transportContext).onErrorResume(error -> {
McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = (error instanceof McpError mcpError
&& mcpError.getJsonRpcError() != null) ? mcpError.getJsonRpcError()
: new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
error.getMessage(), McpError.aggregateExceptionMessages(error));
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, jsonRpcError);
error.getMessage(), null));
// TODO: Should the error go to SSE or back as POST return? // TODO: Should the error go to SSE or back as POST return?
return this.transport.sendMessage(errorResponse).then(Mono.empty()); return this.transport.sendMessage(errorResponse).then(Mono.empty());
}).flatMap(this.transport::sendMessage); }).flatMap(this.transport::sendMessage);
@ -282,10 +285,15 @@ public class McpServerSession implements McpLoggableSession {
} }
return resultMono return resultMono
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null)) .map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
.onErrorResume(error -> Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), .onErrorResume(error -> {
null, new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = (error instanceof McpError mcpError
error.getMessage(), null)))); // TODO: add error message && mcpError.getJsonRpcError() != null) ? mcpError.getJsonRpcError()
// through the data field // TODO: add error message through the data field
: new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
error.getMessage(), McpError.aggregateExceptionMessages(error));
return Mono.just(
new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, jsonRpcError));
});
}); });
} }

View file

@ -179,9 +179,13 @@ public class McpStreamableServerSession implements McpLoggableSession {
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), result, .map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), result,
null)) null))
.onErrorResume(e -> { .onErrorResume(e -> {
McpSchema.JSONRPCResponse.JSONRPCError jsonRpcError = (e instanceof McpError mcpError
&& mcpError.getJsonRpcError() != null) ? mcpError.getJsonRpcError()
: new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
e.getMessage(), McpError.aggregateExceptionMessages(e));
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(),
null, new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, null, jsonRpcError);
e.getMessage(), null));
return Mono.just(errorResponse); return Mono.just(errorResponse);
}) })
.flatMap(transport::sendMessage) .flatMap(transport::sendMessage)

View file

@ -145,13 +145,9 @@ public abstract class AbstractMcpAsyncServerTests {
.tool(duplicateTool, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) .tool(duplicateTool, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))
.build(); .build();
StepVerifier StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(duplicateTool,
.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(duplicateTool,
(exchange, args) -> Mono.just(new CallToolResult(List.of(), false))))) (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyErrorSatisfies(error -> { .verifyComplete();
assertThat(error).isInstanceOf(McpError.class)
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
} }
@ -172,10 +168,7 @@ public abstract class AbstractMcpAsyncServerTests {
StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder()
.tool(duplicateTool) .tool(duplicateTool)
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) .callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
.build())).verifyErrorSatisfies(error -> { .build())).verifyComplete();
assertThat(error).isInstanceOf(McpError.class)
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
} }
@ -269,9 +262,7 @@ public abstract class AbstractMcpAsyncServerTests {
.capabilities(ServerCapabilities.builder().tools(true).build()) .capabilities(ServerCapabilities.builder().tools(true).build())
.build(); .build();
StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> { StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyComplete();
assertThat(error).isInstanceOf(McpError.class).hasMessage("Tool with name 'nonexistent-tool' not found");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
} }
@ -512,9 +503,6 @@ public abstract class AbstractMcpAsyncServerTests {
.capabilities(ServerCapabilities.builder().resources(true, false).build()) .capabilities(ServerCapabilities.builder().resources(true, false).build())
.build(); .build();
// Removing a non-existent resource template should complete successfully (no
// error)
// as per the new implementation that just logs a warning
StepVerifier.create(mcpAsyncServer.removeResourceTemplate("nonexistent://template/{id}")).verifyComplete(); StepVerifier.create(mcpAsyncServer.removeResourceTemplate("nonexistent://template/{id}")).verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
@ -570,7 +558,8 @@ public abstract class AbstractMcpAsyncServerTests {
StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptSpecification) null)) StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptSpecification) null))
.verifyErrorSatisfies(error -> { .verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt specification must not be null"); assertThat(error).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Prompt specification must not be null");
}); });
} }
@ -585,7 +574,7 @@ public abstract class AbstractMcpAsyncServerTests {
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
StepVerifier.create(serverWithoutPrompts.addPrompt(specification)).verifyErrorSatisfies(error -> { StepVerifier.create(serverWithoutPrompts.addPrompt(specification)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class) assertThat(error).isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
}); });
} }
@ -596,7 +585,7 @@ public abstract class AbstractMcpAsyncServerTests {
McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> { StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class) assertThat(error).isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
}); });
} }
@ -626,10 +615,7 @@ public abstract class AbstractMcpAsyncServerTests {
.capabilities(ServerCapabilities.builder().prompts(true).build()) .capabilities(ServerCapabilities.builder().prompts(true).build())
.build(); .build();
StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> { StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyComplete();
assertThat(error).isInstanceOf(McpError.class)
.hasMessage("Prompt with name 'nonexistent-prompt' not found");
});
assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10))) assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10)))
.doesNotThrowAnyException(); .doesNotThrowAnyException();

View file

@ -146,8 +146,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
CreateMessageResult.StopReason.STOP_SEQUENCE); CreateMessageResult.StopReason.STOP_SEQUENCE);
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>(); AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>();
@ -224,8 +225,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
// Server // Server
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>(); AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>();
@ -300,8 +302,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
CreateMessageResult.StopReason.STOP_SEQUENCE); CreateMessageResult.StopReason.STOP_SEQUENCE);
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
@ -393,8 +396,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
Map.of("message", request.message())); Map.of("message", request.message()));
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
@ -448,8 +452,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
AtomicReference<ElicitResult> resultRef = new AtomicReference<>(); AtomicReference<ElicitResult> resultRef = new AtomicReference<>();
@ -520,7 +525,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
}; };
CallToolResult callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); CallToolResult callResponse = CallToolResult.builder().addContent(new TextContent("CALL RESPONSE")).build();
AtomicReference<ElicitResult> resultRef = new AtomicReference<>(); AtomicReference<ElicitResult> resultRef = new AtomicReference<>();
@ -761,7 +766,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
var responseBodyIsNullOrBlank = new AtomicBoolean(false); var responseBodyIsNullOrBlank = new AtomicBoolean(false);
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); var callResponse = McpSchema.CallToolResult.builder()
.addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=importantValue"))
.build();
McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
@ -832,8 +839,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
assertThat(initResult).isNotNull(); assertThat(initResult).isNotNull();
// We expect the tool call to fail immediately with the exception raised by // We expect the tool call to fail immediately with the exception raised by
// the offending tool // the offending tool instead of getting back a timeout.
// instead of getting back a timeout.
assertThatExceptionOfType(McpError.class) assertThatExceptionOfType(McpError.class)
.isThrownBy(() -> mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()))) .isThrownBy(() -> mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())))
.withMessageContaining("Timeout on blocking read"); .withMessageContaining("Timeout on blocking read");
@ -853,8 +859,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var transportContextIsEmpty = new AtomicBoolean(false); var transportContextIsEmpty = new AtomicBoolean(false);
var responseBodyIsNullOrBlank = new AtomicBoolean(false); var responseBodyIsNullOrBlank = new AtomicBoolean(false);
var expectedCallResponse = new McpSchema.CallToolResult( var expectedCallResponse = McpSchema.CallToolResult.builder()
List.of(new McpSchema.TextContent("CALL RESPONSE; ctx=value")), null); .addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=value"))
.build();
McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
@ -872,8 +879,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
e.printStackTrace(); e.printStackTrace();
} }
return new McpSchema.CallToolResult( return McpSchema.CallToolResult.builder()
List.of(new McpSchema.TextContent("CALL RESPONSE; ctx=" + ctxValue)), null); .addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=" + ctxValue))
.build();
}) })
.build(); .build();
@ -906,7 +914,10 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); var callResponse = McpSchema.CallToolResult.builder()
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {

View file

@ -154,10 +154,9 @@ public abstract class AbstractMcpSyncServerTests {
.tool(duplicateTool, (exchange, args) -> new CallToolResult(List.of(), false)) .tool(duplicateTool, (exchange, args) -> new CallToolResult(List.of(), false))
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(duplicateTool, assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(duplicateTool,
(exchange, args) -> new CallToolResult(List.of(), false)))) (exchange, args) -> new CallToolResult(List.of(), false))))
.isInstanceOf(McpError.class) .doesNotThrowAnyException();
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }
@ -175,11 +174,10 @@ public abstract class AbstractMcpSyncServerTests {
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false))
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder()
.tool(duplicateTool) .tool(duplicateTool)
.callHandler((exchange, request) -> new CallToolResult(List.of(), false)) .callHandler((exchange, request) -> new CallToolResult(List.of(), false))
.build())).isInstanceOf(McpError.class) .build())).doesNotThrowAnyException();
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }
@ -272,8 +270,7 @@ public abstract class AbstractMcpSyncServerTests {
.capabilities(ServerCapabilities.builder().tools(true).build()) .capabilities(ServerCapabilities.builder().tools(true).build())
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class) assertThatCode(() -> mcpSyncServer.removeTool("nonexistent-tool")).doesNotThrowAnyException();
.hasMessage("Tool with name 'nonexistent-tool' not found");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }
@ -496,9 +493,6 @@ public abstract class AbstractMcpSyncServerTests {
.capabilities(ServerCapabilities.builder().resources(true, false).build()) .capabilities(ServerCapabilities.builder().resources(true, false).build())
.build(); .build();
// Removing a non-existent resource template should complete successfully (no
// error)
// as per the new implementation that just logs a warning
assertThatCode(() -> mcpSyncServer.removeResourceTemplate("nonexistent://template/{id}")) assertThatCode(() -> mcpSyncServer.removeResourceTemplate("nonexistent://template/{id}"))
.doesNotThrowAnyException(); .doesNotThrowAnyException();
@ -549,7 +543,7 @@ public abstract class AbstractMcpSyncServerTests {
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptSpecification) null)) assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptSpecification) null))
.isInstanceOf(McpError.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Prompt specification must not be null"); .hasMessage("Prompt specification must not be null");
} }
@ -562,7 +556,8 @@ public abstract class AbstractMcpSyncServerTests {
(exchange, req) -> new GetPromptResult("Test prompt description", List (exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specification)).isInstanceOf(McpError.class) assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specification))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
} }
@ -570,7 +565,8 @@ public abstract class AbstractMcpSyncServerTests {
void testRemovePromptWithoutCapability() { void testRemovePromptWithoutCapability() {
var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class) assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
} }
@ -597,8 +593,7 @@ public abstract class AbstractMcpSyncServerTests {
.capabilities(ServerCapabilities.builder().prompts(true).build()) .capabilities(ServerCapabilities.builder().prompts(true).build())
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class) assertThatCode(() -> mcpSyncServer.removePrompt("nonexistent://template/{id}")).doesNotThrowAnyException();
.hasMessage("Prompt with name 'nonexistent-prompt' not found");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }

View file

@ -150,8 +150,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
CreateMessageResult.StopReason.STOP_SEQUENCE); CreateMessageResult.StopReason.STOP_SEQUENCE);
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>(); AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>();
@ -228,8 +229,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
// Server // Server
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>(); AtomicReference<CreateMessageResult> samplingResult = new AtomicReference<>();
@ -304,8 +306,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
CreateMessageResult.StopReason.STOP_SEQUENCE); CreateMessageResult.StopReason.STOP_SEQUENCE);
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
@ -397,8 +400,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
Map.of("message", request.message())); Map.of("message", request.message()));
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
@ -452,8 +456,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
}; };
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), CallToolResult callResponse = McpSchema.CallToolResult.builder()
null); .addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
AtomicReference<ElicitResult> resultRef = new AtomicReference<>(); AtomicReference<ElicitResult> resultRef = new AtomicReference<>();
@ -524,7 +529,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
}; };
CallToolResult callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); CallToolResult callResponse = CallToolResult.builder().addContent(new TextContent("CALL RESPONSE")).build();
AtomicReference<ElicitResult> resultRef = new AtomicReference<>(); AtomicReference<ElicitResult> resultRef = new AtomicReference<>();
@ -765,7 +770,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
var responseBodyIsNullOrBlank = new AtomicBoolean(false); var responseBodyIsNullOrBlank = new AtomicBoolean(false);
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); var callResponse = McpSchema.CallToolResult.builder()
.addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=importantValue"))
.build();
McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
@ -836,8 +843,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
assertThat(initResult).isNotNull(); assertThat(initResult).isNotNull();
// We expect the tool call to fail immediately with the exception raised by // We expect the tool call to fail immediately with the exception raised by
// the offending tool // the offending tool instead of getting back a timeout.
// instead of getting back a timeout.
assertThatExceptionOfType(McpError.class) assertThatExceptionOfType(McpError.class)
.isThrownBy(() -> mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()))) .isThrownBy(() -> mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())))
.withMessageContaining("Timeout on blocking read"); .withMessageContaining("Timeout on blocking read");
@ -857,8 +863,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var transportContextIsEmpty = new AtomicBoolean(false); var transportContextIsEmpty = new AtomicBoolean(false);
var responseBodyIsNullOrBlank = new AtomicBoolean(false); var responseBodyIsNullOrBlank = new AtomicBoolean(false);
var expectedCallResponse = new McpSchema.CallToolResult( var expectedCallResponse = McpSchema.CallToolResult.builder()
List.of(new McpSchema.TextContent("CALL RESPONSE; ctx=value")), null); .addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=value"))
.build();
McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
@ -876,8 +883,9 @@ public abstract class AbstractMcpClientServerIntegrationTests {
e.printStackTrace(); e.printStackTrace();
} }
return new McpSchema.CallToolResult( return McpSchema.CallToolResult.builder()
List.of(new McpSchema.TextContent("CALL RESPONSE; ctx=" + ctxValue)), null); .addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=" + ctxValue))
.build();
}) })
.build(); .build();
@ -910,7 +918,10 @@ public abstract class AbstractMcpClientServerIntegrationTests {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);
var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); var callResponse = McpSchema.CallToolResult.builder()
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
.build();
McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {

View file

@ -149,13 +149,9 @@ public abstract class AbstractMcpAsyncServerTests {
.tool(duplicateTool, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) .tool(duplicateTool, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))
.build(); .build();
StepVerifier StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(duplicateTool,
.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(duplicateTool,
(exchange, args) -> Mono.just(new CallToolResult(List.of(), false))))) (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyErrorSatisfies(error -> { .verifyComplete();
assertThat(error).isInstanceOf(McpError.class)
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
} }
@ -176,10 +172,7 @@ public abstract class AbstractMcpAsyncServerTests {
StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder()
.tool(duplicateTool) .tool(duplicateTool)
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) .callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
.build())).verifyErrorSatisfies(error -> { .build())).verifyComplete();
assertThat(error).isInstanceOf(McpError.class)
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
} }
@ -273,9 +266,7 @@ public abstract class AbstractMcpAsyncServerTests {
.capabilities(ServerCapabilities.builder().tools(true).build()) .capabilities(ServerCapabilities.builder().tools(true).build())
.build(); .build();
StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> { StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyComplete();
assertThat(error).isInstanceOf(McpError.class).hasMessage("Tool with name 'nonexistent-tool' not found");
});
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
} }
@ -516,9 +507,6 @@ public abstract class AbstractMcpAsyncServerTests {
.capabilities(ServerCapabilities.builder().resources(true, false).build()) .capabilities(ServerCapabilities.builder().resources(true, false).build())
.build(); .build();
// Removing a non-existent resource template should complete successfully (no
// error)
// as per the new implementation that just logs a warning
StepVerifier.create(mcpAsyncServer.removeResourceTemplate("nonexistent://template/{id}")).verifyComplete(); StepVerifier.create(mcpAsyncServer.removeResourceTemplate("nonexistent://template/{id}")).verifyComplete();
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
@ -574,7 +562,8 @@ public abstract class AbstractMcpAsyncServerTests {
StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptSpecification) null)) StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptSpecification) null))
.verifyErrorSatisfies(error -> { .verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt specification must not be null"); assertThat(error).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Prompt specification must not be null");
}); });
} }
@ -589,7 +578,7 @@ public abstract class AbstractMcpAsyncServerTests {
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
StepVerifier.create(serverWithoutPrompts.addPrompt(specification)).verifyErrorSatisfies(error -> { StepVerifier.create(serverWithoutPrompts.addPrompt(specification)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class) assertThat(error).isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
}); });
} }
@ -600,7 +589,7 @@ public abstract class AbstractMcpAsyncServerTests {
McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> { StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class) assertThat(error).isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
}); });
} }
@ -630,10 +619,7 @@ public abstract class AbstractMcpAsyncServerTests {
.capabilities(ServerCapabilities.builder().prompts(true).build()) .capabilities(ServerCapabilities.builder().prompts(true).build())
.build(); .build();
StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> { StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyComplete();
assertThat(error).isInstanceOf(McpError.class)
.hasMessage("Prompt with name 'nonexistent-prompt' not found");
});
assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10))) assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10)))
.doesNotThrowAnyException(); .doesNotThrowAnyException();

View file

@ -153,10 +153,9 @@ public abstract class AbstractMcpSyncServerTests {
.tool(duplicateTool, (exchange, args) -> new CallToolResult(List.of(), false)) .tool(duplicateTool, (exchange, args) -> new CallToolResult(List.of(), false))
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(duplicateTool, assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(duplicateTool,
(exchange, args) -> new CallToolResult(List.of(), false)))) (exchange, args) -> new CallToolResult(List.of(), false))))
.isInstanceOf(McpError.class) .doesNotThrowAnyException();
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }
@ -174,11 +173,10 @@ public abstract class AbstractMcpSyncServerTests {
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false))
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder()
.tool(duplicateTool) .tool(duplicateTool)
.callHandler((exchange, request) -> new CallToolResult(List.of(), false)) .callHandler((exchange, request) -> new CallToolResult(List.of(), false))
.build())).isInstanceOf(McpError.class) .build())).doesNotThrowAnyException();
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }
@ -271,8 +269,7 @@ public abstract class AbstractMcpSyncServerTests {
.capabilities(ServerCapabilities.builder().tools(true).build()) .capabilities(ServerCapabilities.builder().tools(true).build())
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class) assertThatCode(() -> mcpSyncServer.removeTool("nonexistent-tool")).doesNotThrowAnyException();
.hasMessage("Tool with name 'nonexistent-tool' not found");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }
@ -495,9 +492,6 @@ public abstract class AbstractMcpSyncServerTests {
.capabilities(ServerCapabilities.builder().resources(true, false).build()) .capabilities(ServerCapabilities.builder().resources(true, false).build())
.build(); .build();
// Removing a non-existent resource template should complete successfully (no
// error)
// as per the new implementation that just logs a warning
assertThatCode(() -> mcpSyncServer.removeResourceTemplate("nonexistent://template/{id}")) assertThatCode(() -> mcpSyncServer.removeResourceTemplate("nonexistent://template/{id}"))
.doesNotThrowAnyException(); .doesNotThrowAnyException();
@ -548,7 +542,7 @@ public abstract class AbstractMcpSyncServerTests {
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptSpecification) null)) assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptSpecification) null))
.isInstanceOf(McpError.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Prompt specification must not be null"); .hasMessage("Prompt specification must not be null");
} }
@ -561,7 +555,8 @@ public abstract class AbstractMcpSyncServerTests {
(exchange, req) -> new GetPromptResult("Test prompt description", List (exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specification)).isInstanceOf(McpError.class) assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specification))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
} }
@ -569,7 +564,8 @@ public abstract class AbstractMcpSyncServerTests {
void testRemovePromptWithoutCapability() { void testRemovePromptWithoutCapability() {
var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class) assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Server must be configured with prompt capabilities"); .hasMessage("Server must be configured with prompt capabilities");
} }
@ -596,8 +592,7 @@ public abstract class AbstractMcpSyncServerTests {
.capabilities(ServerCapabilities.builder().prompts(true).build()) .capabilities(ServerCapabilities.builder().prompts(true).build())
.build(); .build();
assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class) assertThatCode(() -> mcpSyncServer.removePrompt("nonexistent://template/{id}")).doesNotThrowAnyException();
.hasMessage("Prompt with name 'nonexistent-prompt' not found");
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
} }