feat: Add roots change notification support to MCP server
- Implement roots change notification handler in McpAsyncServer - Add rootsChangeConsumer builder methods to support single and multiple consumers - Rename info() builder method to serverInfo() for clarity - Add comprehensive tests for roots change notification functionality - Add default server info implementation in Builder Resolves #39
This commit is contained in:
parent
0b0d5086ce
commit
0fd93a9f82
6 changed files with 266 additions and 58 deletions
|
|
@ -23,6 +23,7 @@ import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -39,6 +40,7 @@ import org.springframework.ai.mcp.spec.McpError;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
|
||||||
|
import org.springframework.ai.mcp.spec.McpSchema.ListRootsResult;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.LoggingLevel;
|
import org.springframework.ai.mcp.spec.McpSchema.LoggingLevel;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
|
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
|
||||||
import org.springframework.ai.mcp.spec.McpSchema.Tool;
|
import org.springframework.ai.mcp.spec.McpSchema.Tool;
|
||||||
|
|
@ -84,7 +86,6 @@ public class McpAsyncServer {
|
||||||
|
|
||||||
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
|
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
|
||||||
|
|
||||||
// TODO: Add support for roots list changed notification
|
|
||||||
/**
|
/**
|
||||||
* Create a new McpAsyncServer with the given transport and capabilities.
|
* Create a new McpAsyncServer with the given transport and capabilities.
|
||||||
* @param mcpTransport The transport layer implementation for MCP communication
|
* @param mcpTransport The transport layer implementation for MCP communication
|
||||||
|
|
@ -94,11 +95,13 @@ public class McpAsyncServer {
|
||||||
* @param resources The map of resource registrations
|
* @param resources The map of resource registrations
|
||||||
* @param resourceTemplates The list of resource templates
|
* @param resourceTemplates The list of resource templates
|
||||||
* @param prompts The map of prompt registrations
|
* @param prompts The map of prompt registrations
|
||||||
|
* @param rootsChangeConsumers The list of consumers that will be notified when the
|
||||||
|
* roots list changes
|
||||||
*/
|
*/
|
||||||
public McpAsyncServer(McpTransport mcpTransport, McpSchema.Implementation serverInfo,
|
public McpAsyncServer(McpTransport mcpTransport, McpSchema.Implementation serverInfo,
|
||||||
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
|
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
|
||||||
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
|
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||||
Map<String, PromptRegistration> prompts) {
|
Map<String, PromptRegistration> prompts, List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
this.serverInfo = serverInfo;
|
this.serverInfo = serverInfo;
|
||||||
this.tools = new CopyOnWriteArrayList<>(tools != null ? tools : List.of());
|
this.tools = new CopyOnWriteArrayList<>(tools != null ? tools : List.of());
|
||||||
|
|
@ -156,6 +159,13 @@ public class McpAsyncServer {
|
||||||
|
|
||||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
|
||||||
|
|
||||||
|
if (Utils.isEmpty(rootsChangeConsumers)) {
|
||||||
|
rootsChangeConsumers = List.of((roots) -> logger
|
||||||
|
.warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots));
|
||||||
|
}
|
||||||
|
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
|
||||||
|
rootsListChnagedNotificationHandler(rootsChangeConsumers));
|
||||||
|
|
||||||
this.transport = mcpTransport;
|
this.transport = mcpTransport;
|
||||||
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
|
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
|
||||||
notificationHandlers);
|
notificationHandlers);
|
||||||
|
|
@ -238,6 +248,33 @@ public class McpAsyncServer {
|
||||||
this.mcpSession.close();
|
this.mcpSession.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
|
||||||
|
};
|
||||||
|
|
||||||
|
private NotificationHandler rootsListChnagedNotificationHandler(
|
||||||
|
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||||
|
|
||||||
|
if (this.clientCapabilities != null && this.clientCapabilities.roots() != null) {
|
||||||
|
|
||||||
|
Mono<ListRootsResult> updatedRootsList = this.mcpSession.sendRequest(McpSchema.METHOD_ROOTS_LIST, null,
|
||||||
|
LIST_ROOTS_RESULT_TYPE_REF);
|
||||||
|
|
||||||
|
return params -> updatedRootsList // @formatter:off
|
||||||
|
.flatMap(listRootsResult ->
|
||||||
|
Mono.fromRunnable(() ->
|
||||||
|
rootsChangeConsumers.stream().forEach(consumer -> consumer.accept(listRootsResult.roots())))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())) // TODO: Check if this is needed
|
||||||
|
.onErrorResume(error -> {
|
||||||
|
logger.error("Error handling roots list change notification", error);
|
||||||
|
return Mono.empty();
|
||||||
|
})
|
||||||
|
.then(); // Convert to Mono<Void>
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
return params -> Mono.empty();
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Tool Management
|
// Tool Management
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.springframework.ai.mcp.spec.McpSchema;
|
import org.springframework.ai.mcp.spec.McpSchema;
|
||||||
|
|
@ -206,9 +207,12 @@ public interface McpServer {
|
||||||
*/
|
*/
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
|
private final static McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
|
||||||
|
"1.0.0");
|
||||||
|
|
||||||
private final McpTransport transport;
|
private final McpTransport transport;
|
||||||
|
|
||||||
private McpSchema.Implementation serverInfo;
|
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
|
||||||
|
|
||||||
private McpSchema.ServerCapabilities serverCapabilities;
|
private McpSchema.ServerCapabilities serverCapabilities;
|
||||||
|
|
||||||
|
|
@ -241,6 +245,8 @@ public interface McpServer {
|
||||||
*/
|
*/
|
||||||
private Map<String, PromptRegistration> prompts = new HashMap<>();
|
private Map<String, PromptRegistration> prompts = new HashMap<>();
|
||||||
|
|
||||||
|
private List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers = new ArrayList<>();
|
||||||
|
|
||||||
private Builder(McpTransport transport) {
|
private Builder(McpTransport transport) {
|
||||||
Assert.notNull(transport, "Transport must not be null");
|
Assert.notNull(transport, "Transport must not be null");
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
@ -255,7 +261,7 @@ public interface McpServer {
|
||||||
* @return This builder instance for method chaining
|
* @return This builder instance for method chaining
|
||||||
* @throws IllegalArgumentException if serverInfo is null
|
* @throws IllegalArgumentException if serverInfo is null
|
||||||
*/
|
*/
|
||||||
public Builder info(McpSchema.Implementation serverInfo) {
|
public Builder serverInfo(McpSchema.Implementation serverInfo) {
|
||||||
Assert.notNull(serverInfo, "Server info must not be null");
|
Assert.notNull(serverInfo, "Server info must not be null");
|
||||||
this.serverInfo = serverInfo;
|
this.serverInfo = serverInfo;
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -263,14 +269,15 @@ public interface McpServer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the server implementation information using name and version strings. This
|
* Sets the server implementation information using name and version strings. This
|
||||||
* is a convenience method alternative to {@link #info(McpSchema.Implementation)}.
|
* is a convenience method alternative to
|
||||||
|
* {@link #serverInfo(McpSchema.Implementation)}.
|
||||||
* @param name The server name. Must not be null or empty.
|
* @param name The server name. Must not be null or empty.
|
||||||
* @param version The server version. Must not be null or empty.
|
* @param version The server version. Must not be null or empty.
|
||||||
* @return This builder instance for method chaining
|
* @return This builder instance for method chaining
|
||||||
* @throws IllegalArgumentException if name or version is null or empty
|
* @throws IllegalArgumentException if name or version is null or empty
|
||||||
* @see #info(McpSchema.Implementation)
|
* @see #serverInfo(McpSchema.Implementation)
|
||||||
*/
|
*/
|
||||||
public Builder info(String name, String version) {
|
public Builder serverInfo(String name, String version) {
|
||||||
Assert.hasText(name, "Name must not be null or empty");
|
Assert.hasText(name, "Name must not be null or empty");
|
||||||
Assert.hasText(version, "Version must not be null or empty");
|
Assert.hasText(version, "Version must not be null or empty");
|
||||||
this.serverInfo = new McpSchema.Implementation(name, version);
|
this.serverInfo = new McpSchema.Implementation(name, version);
|
||||||
|
|
@ -518,6 +525,49 @@ public interface McpServer {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a consumer that will be notified when the list of roots changes. This
|
||||||
|
* is useful for updating resource availability dynamically, such as when new
|
||||||
|
* files are added or removed.
|
||||||
|
* @param consumer The consumer to register. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if consumer is null
|
||||||
|
*/
|
||||||
|
public Builder rootsChangeConsumer(Consumer<List<McpSchema.Root>> consumer) {
|
||||||
|
Assert.notNull(consumer, "Consumer must not be null");
|
||||||
|
this.rootsChangeConsumers.add(consumer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers multiple consumers that will be notified when the list of roots
|
||||||
|
* changes. This method is useful when multiple consumers need to be registered at
|
||||||
|
* once.
|
||||||
|
* @param consumers The list of consumers to register. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if consumers is null
|
||||||
|
*/
|
||||||
|
public Builder rootsChangeConsumers(List<Consumer<List<McpSchema.Root>>> consumers) {
|
||||||
|
Assert.notNull(consumers, "Consumers list must not be null");
|
||||||
|
this.rootsChangeConsumers.addAll(consumers);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers multiple consumers that will be notified when the list of roots
|
||||||
|
* changes using varargs. This method provides a convenient way to register
|
||||||
|
* multiple consumers inline.
|
||||||
|
* @param consumers The consumers to register. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if consumers is null
|
||||||
|
*/
|
||||||
|
public Builder rootsChangeConsumers(Consumer<List<McpSchema.Root>>... consumers) {
|
||||||
|
for (Consumer<List<McpSchema.Root>> consumer : consumers) {
|
||||||
|
this.rootsChangeConsumers.add(consumer);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a synchronous MCP server that provides blocking operations. Synchronous
|
* Builds a synchronous MCP server that provides blocking operations. Synchronous
|
||||||
* servers process each request to completion before handling the next one, making
|
* servers process each request to completion before handling the next one, making
|
||||||
|
|
@ -539,12 +589,9 @@ public interface McpServer {
|
||||||
* settings
|
* settings
|
||||||
*/
|
*/
|
||||||
public McpAsyncServer async() {
|
public McpAsyncServer async() {
|
||||||
if (serverInfo == null) {
|
|
||||||
serverInfo = new McpSchema.Implementation("mcp-server", "1.0.0");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new McpAsyncServer(transport, serverInfo, serverCapabilities, tools, resources, resourceTemplates,
|
return new McpAsyncServer(transport, serverInfo, serverCapabilities, tools, resources, resourceTemplates,
|
||||||
prompts);
|
prompts, rootsChangeConsumers);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ public class DemoServer {
|
||||||
SseServerTransport transport = new SseServerTransport(new ObjectMapper(), "/mcp/message");
|
SseServerTransport transport = new SseServerTransport(new ObjectMapper(), "/mcp/message");
|
||||||
|
|
||||||
var mcpServer = McpServer.using(transport)
|
var mcpServer = McpServer.using(transport)
|
||||||
.info("Weather Forecast", "1.0.0")
|
.serverInfo("Weather Forecast", "1.0.0")
|
||||||
.tool(new McpSchema.Tool("weather", "Weather forecast tool by location", Map.of("city", "String")),
|
.tool(new McpSchema.Tool("weather", "Weather forecast tool by location", Map.of("city", "String")),
|
||||||
(arguments) -> {
|
(arguments) -> {
|
||||||
String city = (String) arguments.get("city");
|
String city = (String) arguments.get("city");
|
||||||
|
|
|
||||||
|
|
@ -84,21 +84,21 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpServer.using(createMcpTransport()).info((McpSchema.Implementation) null))
|
assertThatThrownBy(() -> McpServer.using(createMcpTransport()).serverInfo((McpSchema.Implementation) null))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Server info must not be null");
|
.hasMessage("Server info must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGracefulShutdown() {
|
void testGracefulShutdown() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").async();
|
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete();
|
StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testImmediateClose() {
|
void testImmediateClose() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").async();
|
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
|
||||||
|
|
||||||
assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +111,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
void testAddTool() {
|
void testAddTool() {
|
||||||
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", Map.of("input", "string"));
|
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", Map.of("input", "string"));
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -127,7 +127,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
||||||
|
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
|
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
|
||||||
.async();
|
.async();
|
||||||
|
|
@ -148,7 +148,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
||||||
|
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(too, args -> new CallToolResult(List.of(), false))
|
.tool(too, args -> new CallToolResult(List.of(), false))
|
||||||
.async();
|
.async();
|
||||||
|
|
@ -161,7 +161,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentTool() {
|
void testRemoveNonexistentTool() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -177,7 +177,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
||||||
|
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(too, args -> new CallToolResult(List.of(), false))
|
.tool(too, args -> new CallToolResult(List.of(), false))
|
||||||
.async();
|
.async();
|
||||||
|
|
@ -193,7 +193,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyResourcesListChanged() {
|
void testNotifyResourcesListChanged() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").async();
|
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete();
|
StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete();
|
||||||
|
|
||||||
|
|
@ -203,7 +203,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddResource() {
|
void testAddResource() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -220,7 +220,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddResourceWithNullRegistration() {
|
void testAddResourceWithNullRegistration() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -235,7 +235,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
void testAddResourceWithoutCapability() {
|
void testAddResourceWithoutCapability() {
|
||||||
// Create a server without resource capabilities
|
// Create a server without resource capabilities
|
||||||
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
||||||
|
|
@ -253,7 +253,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
void testRemoveResourceWithoutCapability() {
|
void testRemoveResourceWithoutCapability() {
|
||||||
// Create a server without resource capabilities
|
// Create a server without resource capabilities
|
||||||
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutResources = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> {
|
||||||
|
|
@ -268,7 +268,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyPromptsListChanged() {
|
void testNotifyPromptsListChanged() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").async();
|
var mcpAsyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete();
|
StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete();
|
||||||
|
|
||||||
|
|
@ -278,7 +278,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddPromptWithNullRegistration() {
|
void testAddPromptWithNullRegistration() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(false).build())
|
.capabilities(ServerCapabilities.builder().prompts(false).build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -291,7 +291,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
void testAddPromptWithoutCapability() {
|
void testAddPromptWithoutCapability() {
|
||||||
// Create a server without prompt capabilities
|
// Create a server without prompt capabilities
|
||||||
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
||||||
|
|
@ -309,7 +309,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
void testRemovePromptWithoutCapability() {
|
void testRemovePromptWithoutCapability() {
|
||||||
// Create a server without prompt capabilities
|
// Create a server without prompt capabilities
|
||||||
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
|
McpAsyncServer serverWithoutPrompts = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
|
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
|
||||||
|
|
@ -328,7 +328,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
||||||
|
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.prompts(registration)
|
.prompts(registration)
|
||||||
.async();
|
.async();
|
||||||
|
|
@ -341,7 +341,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentPrompt() {
|
void testRemoveNonexistentPrompt() {
|
||||||
var mcpAsyncServer2 = McpServer.using(createMcpTransport())
|
var mcpAsyncServer2 = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -354,6 +354,70 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// Roots Tests
|
||||||
|
// ---------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRootsChangeConsumers() {
|
||||||
|
// Test with single consumer
|
||||||
|
var rootsReceived = new McpSchema.Root[1];
|
||||||
|
var consumerCalled = new boolean[1];
|
||||||
|
|
||||||
|
var singleConsumerServer = McpServer.using(createMcpTransport())
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
|
consumerCalled[0] = true;
|
||||||
|
if (!roots.isEmpty()) {
|
||||||
|
rootsReceived[0] = roots.get(0);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.async();
|
||||||
|
|
||||||
|
assertThat(singleConsumerServer).isNotNull();
|
||||||
|
assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
.doesNotThrowAnyException();
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Test with multiple consumers
|
||||||
|
var consumer1Called = new boolean[1];
|
||||||
|
var consumer2Called = new boolean[1];
|
||||||
|
var rootsContent = new List[1];
|
||||||
|
|
||||||
|
var multipleConsumersServer = McpServer.using(createMcpTransport())
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
|
consumer1Called[0] = true;
|
||||||
|
rootsContent[0] = roots;
|
||||||
|
}, roots -> consumer2Called[0] = true))
|
||||||
|
.async();
|
||||||
|
|
||||||
|
assertThat(multipleConsumersServer).isNotNull();
|
||||||
|
assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
.doesNotThrowAnyException();
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Test error handling
|
||||||
|
var errorHandlingServer = McpServer.using(createMcpTransport())
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
|
throw new RuntimeException("Test error");
|
||||||
|
}))
|
||||||
|
.async();
|
||||||
|
|
||||||
|
assertThat(errorHandlingServer).isNotNull();
|
||||||
|
assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
.doesNotThrowAnyException();
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Test without consumers
|
||||||
|
var noConsumersServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").async();
|
||||||
|
|
||||||
|
assertThat(noConsumersServer).isNotNull();
|
||||||
|
assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
|
||||||
|
.doesNotThrowAnyException();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Logging Tests
|
// Logging Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -361,7 +425,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -380,7 +444,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithoutCapability() {
|
void testLoggingWithoutCapability() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
@ -396,7 +460,7 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
var mcpAsyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.async();
|
.async();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,28 +83,28 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpServer.using(null)).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpServer.using(createMcpTransport()).info((McpSchema.Implementation) null))
|
assertThatThrownBy(() -> McpServer.using(createMcpTransport()).serverInfo((McpSchema.Implementation) null))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Server info must not be null");
|
.hasMessage("Server info must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGracefulShutdown() {
|
void testGracefulShutdown() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testImmediateClose() {
|
void testImmediateClose() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetAsyncServer() {
|
void testGetAsyncServer() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThat(mcpSyncServer.getAsyncServer()).isNotNull();
|
assertThat(mcpSyncServer.getAsyncServer()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddTool() {
|
void testAddTool() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", Map.of("input", "string"));
|
||||||
|
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
|
.tool(duplicateTool, args -> new CallToolResult(List.of(), false))
|
||||||
.sync();
|
.sync();
|
||||||
|
|
@ -153,7 +153,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", Map.of("input", "string"));
|
Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", Map.of("input", "string"));
|
||||||
|
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tool(tool, args -> new CallToolResult(List.of(), false))
|
.tool(tool, args -> new CallToolResult(List.of(), false))
|
||||||
.sync();
|
.sync();
|
||||||
|
|
@ -166,7 +166,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentTool() {
|
void testRemoveNonexistentTool() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -178,7 +178,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyToolsListChanged() {
|
void testNotifyToolsListChanged() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -191,7 +191,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyResourcesListChanged() {
|
void testNotifyResourcesListChanged() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddResource() {
|
void testAddResource() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -218,7 +218,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddResourceWithNullRegistration() {
|
void testAddResourceWithNullRegistration() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
.capabilities(ServerCapabilities.builder().resources(true, false).build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -230,7 +230,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddResourceWithoutCapability() {
|
void testAddResourceWithoutCapability() {
|
||||||
var serverWithoutResources = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var serverWithoutResources = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
|
||||||
null);
|
null);
|
||||||
|
|
@ -243,7 +243,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveResourceWithoutCapability() {
|
void testRemoveResourceWithoutCapability() {
|
||||||
var serverWithoutResources = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var serverWithoutResources = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class)
|
assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Server must be configured with resource capabilities");
|
.hasMessage("Server must be configured with resource capabilities");
|
||||||
|
|
@ -255,7 +255,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNotifyPromptsListChanged() {
|
void testNotifyPromptsListChanged() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var mcpSyncServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException();
|
||||||
|
|
||||||
|
|
@ -265,7 +265,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testAddPromptWithNullRegistration() {
|
void testAddPromptWithNullRegistration() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(false).build())
|
.capabilities(ServerCapabilities.builder().prompts(false).build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -275,7 +275,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddPromptWithoutCapability() {
|
void testAddPromptWithoutCapability() {
|
||||||
var serverWithoutPrompts = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var serverWithoutPrompts = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
|
||||||
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
|
PromptRegistration registration = new PromptRegistration(prompt, req -> new GetPromptResult(
|
||||||
|
|
@ -288,7 +288,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemovePromptWithoutCapability() {
|
void testRemovePromptWithoutCapability() {
|
||||||
var serverWithoutPrompts = McpServer.using(createMcpTransport()).info("test-server", "1.0.0").sync();
|
var serverWithoutPrompts = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class)
|
assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class)
|
||||||
.hasMessage("Server must be configured with prompt capabilities");
|
.hasMessage("Server must be configured with prompt capabilities");
|
||||||
|
|
@ -302,7 +302,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
List.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
|
||||||
|
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.prompts(registration)
|
.prompts(registration)
|
||||||
.sync();
|
.sync();
|
||||||
|
|
@ -315,7 +315,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonexistentPrompt() {
|
void testRemoveNonexistentPrompt() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
.capabilities(ServerCapabilities.builder().prompts(true).build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -325,6 +325,66 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// Roots Tests
|
||||||
|
// ---------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRootsChangeConsumers() {
|
||||||
|
// Test with single consumer
|
||||||
|
var rootsReceived = new McpSchema.Root[1];
|
||||||
|
var consumerCalled = new boolean[1];
|
||||||
|
|
||||||
|
var singleConsumerServer = McpServer.using(createMcpTransport())
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
|
consumerCalled[0] = true;
|
||||||
|
if (!roots.isEmpty()) {
|
||||||
|
rootsReceived[0] = roots.get(0);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.sync();
|
||||||
|
|
||||||
|
assertThat(singleConsumerServer).isNotNull();
|
||||||
|
assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Test with multiple consumers
|
||||||
|
var consumer1Called = new boolean[1];
|
||||||
|
var consumer2Called = new boolean[1];
|
||||||
|
var rootsContent = new List[1];
|
||||||
|
|
||||||
|
var multipleConsumersServer = McpServer.using(createMcpTransport())
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
|
consumer1Called[0] = true;
|
||||||
|
rootsContent[0] = roots;
|
||||||
|
}, roots -> consumer2Called[0] = true))
|
||||||
|
.sync();
|
||||||
|
|
||||||
|
assertThat(multipleConsumersServer).isNotNull();
|
||||||
|
assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Test error handling
|
||||||
|
var errorHandlingServer = McpServer.using(createMcpTransport())
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.rootsChangeConsumers(List.of(roots -> {
|
||||||
|
throw new RuntimeException("Test error");
|
||||||
|
}))
|
||||||
|
.sync();
|
||||||
|
|
||||||
|
assertThat(errorHandlingServer).isNotNull();
|
||||||
|
assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// Test without consumers
|
||||||
|
var noConsumersServer = McpServer.using(createMcpTransport()).serverInfo("test-server", "1.0.0").sync();
|
||||||
|
|
||||||
|
assertThat(noConsumersServer).isNotNull();
|
||||||
|
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Logging Tests
|
// Logging Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -332,7 +392,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -351,7 +411,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithoutCapability() {
|
void testLoggingWithoutCapability() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
.capabilities(ServerCapabilities.builder().build()) // No logging capability
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
@ -367,7 +427,7 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
var mcpSyncServer = McpServer.using(createMcpTransport())
|
var mcpSyncServer = McpServer.using(createMcpTransport())
|
||||||
.info("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.capabilities(ServerCapabilities.builder().logging().build())
|
.capabilities(ServerCapabilities.builder().logging().build())
|
||||||
.sync();
|
.sync();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ public class McpServerConfig {
|
||||||
|
|
||||||
// Create the server with both tool and resource capabilities
|
// Create the server with both tool and resource capabilities
|
||||||
var server = McpServer.using(transport)
|
var server = McpServer.using(transport)
|
||||||
.info("MCP Demo Server", "1.0.0")
|
.serverInfo("MCP Demo Server", "1.0.0")
|
||||||
.capabilities(capabilities)
|
.capabilities(capabilities)
|
||||||
.resources(systemInfoResourceRegistration())
|
.resources(systemInfoResourceRegistration())
|
||||||
.prompts(greetingPromptRegistration())
|
.prompts(greetingPromptRegistration())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue