diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java index f57cc18dd..b9111188d 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java @@ -149,11 +149,11 @@ public class McpServerAutoConfiguration { // De-duplicate tools by their name, keeping the first occurrence of each tool // name - return tools.stream() - .collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), // Key: - // tool - // name - tool -> tool, // Value: the tool itself + return tools.stream() // Key: tool name + .collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), tool -> tool, // Value: + // the + // tool + // itself (existing, replacement) -> existing)) // On duplicate key, keep the // existing tool .values() @@ -185,47 +185,66 @@ public class McpServerAutoConfiguration { // Create the server with both tool and resource capabilities SyncSpecification serverBuilder = McpServer.sync(transportProvider).serverInfo(serverInfo); - List toolSpecifications = new ArrayList<>(tools.stream().flatMap(List::stream).toList()); - - List providerToolCallbacks = toolCallbackProvider.stream() - .map(pr -> List.of(pr.getToolCallbacks())) - .flatMap(List::stream) - .filter(fc -> fc instanceof ToolCallback) - .map(fc -> (ToolCallback) fc) - .toList(); - - toolSpecifications.addAll(this.toSyncToolSpecifications(providerToolCallbacks, serverProperties)); - - if (!CollectionUtils.isEmpty(toolSpecifications)) { - serverBuilder.tools(toolSpecifications); + // Tools + if (serverProperties.getCapabilities().isTool()) { + logger.info("Enable tools capabilities, notification: " + serverProperties.isToolChangeNotification()); capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); - logger.info("Registered tools: " + toolSpecifications.size() + ", notification: " - + serverProperties.isToolChangeNotification()); + + List toolSpecifications = new ArrayList<>( + tools.stream().flatMap(List::stream).toList()); + + List providerToolCallbacks = toolCallbackProvider.stream() + .map(pr -> List.of(pr.getToolCallbacks())) + .flatMap(List::stream) + .filter(fc -> fc instanceof ToolCallback) + .map(fc -> (ToolCallback) fc) + .toList(); + + toolSpecifications.addAll(this.toSyncToolSpecifications(providerToolCallbacks, serverProperties)); + + if (!CollectionUtils.isEmpty(toolSpecifications)) { + serverBuilder.tools(toolSpecifications); + logger.info("Registered tools: " + toolSpecifications.size()); + } } - List resourceSpecifications = resources.stream().flatMap(List::stream).toList(); - if (!CollectionUtils.isEmpty(resourceSpecifications)) { - serverBuilder.resources(resourceSpecifications); + // Resources + if (serverProperties.getCapabilities().isResource()) { + logger.info( + "Enable resources capabilities, notification: " + serverProperties.isResourceChangeNotification()); capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification()); - logger.info("Registered resources: " + resourceSpecifications.size() + ", notification: " - + serverProperties.isResourceChangeNotification()); + + List resourceSpecifications = resources.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(resourceSpecifications)) { + serverBuilder.resources(resourceSpecifications); + logger.info("Registered resources: " + resourceSpecifications.size()); + } } - List promptSpecifications = prompts.stream().flatMap(List::stream).toList(); - if (!CollectionUtils.isEmpty(promptSpecifications)) { - serverBuilder.prompts(promptSpecifications); + // Prompts + if (serverProperties.getCapabilities().isPrompt()) { + logger.info("Enable prompts capabilities, notification: " + serverProperties.isPromptChangeNotification()); capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification()); - logger.info("Registered prompts: " + promptSpecifications.size() + ", notification: " - + serverProperties.isPromptChangeNotification()); + + List promptSpecifications = prompts.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(promptSpecifications)) { + serverBuilder.prompts(promptSpecifications); + logger.info("Registered prompts: " + promptSpecifications.size()); + } } - List completionSpecifications = completions.stream() - .flatMap(List::stream) - .toList(); - if (!CollectionUtils.isEmpty(completionSpecifications)) { - serverBuilder.completions(completionSpecifications); + // Completions + if (serverProperties.getCapabilities().isCompletion()) { + logger.info("Enable completions capabilities"); capabilitiesBuilder.completions(); - logger.info("Registered completions: " + completionSpecifications.size()); + + List completionSpecifications = completions.stream() + .flatMap(List::stream) + .toList(); + if (!CollectionUtils.isEmpty(completionSpecifications)) { + serverBuilder.completions(completionSpecifications); + logger.info("Registered completions: " + completionSpecifications.size()); + } } rootsChangeConsumers.ifAvailable(consumer -> { @@ -257,11 +276,11 @@ public class McpServerAutoConfiguration { McpServerProperties serverProperties) { // De-duplicate tools by their name, keeping the first occurrence of each tool // name - return tools.stream() - .collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), // Key: - // tool - // name - tool -> tool, // Value: the tool itself + return tools.stream() // Key: tool name + .collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), tool -> tool, // Value: + // the + // tool + // itself (existing, replacement) -> existing)) // On duplicate key, keep the // existing tool .values() @@ -303,35 +322,51 @@ public class McpServerAutoConfiguration { toolSpecifications.addAll(this.toAsyncToolSpecification(providerToolCallbacks, serverProperties)); + // Tools + if (serverProperties.getCapabilities().isTool()) { + logger.info("Enable tools capabilities, notification: " + serverProperties.isToolChangeNotification()); + capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); + } + if (!CollectionUtils.isEmpty(toolSpecifications)) { serverBuilder.tools(toolSpecifications); - capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); - logger.info("Registered tools: " + toolSpecifications.size() + ", notification: " - + serverProperties.isToolChangeNotification()); + logger.info("Registered tools: " + toolSpecifications.size()); + } + + // Resources + if (serverProperties.getCapabilities().isResource()) { + logger.info( + "Enable resources capabilities, notification: " + serverProperties.isResourceChangeNotification()); + capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification()); } List resourceSpecifications = resources.stream().flatMap(List::stream).toList(); if (!CollectionUtils.isEmpty(resourceSpecifications)) { serverBuilder.resources(resourceSpecifications); - capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification()); - logger.info("Registered resources: " + resourceSpecifications.size() + ", notification: " - + serverProperties.isResourceChangeNotification()); + logger.info("Registered resources: " + resourceSpecifications.size()); } + // Prompts + if (serverProperties.getCapabilities().isPrompt()) { + logger.info("Enable prompts capabilities, notification: " + serverProperties.isPromptChangeNotification()); + capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification()); + } List promptSpecifications = prompts.stream().flatMap(List::stream).toList(); if (!CollectionUtils.isEmpty(promptSpecifications)) { serverBuilder.prompts(promptSpecifications); - capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification()); - logger.info("Registered prompts: " + promptSpecifications.size() + ", notification: " - + serverProperties.isPromptChangeNotification()); + logger.info("Registered prompts: " + promptSpecifications.size()); } + // Completions + if (serverProperties.getCapabilities().isCompletion()) { + logger.info("Enable completions capabilities"); + capabilitiesBuilder.completions(); + } List completionSpecifications = completions.stream() .flatMap(List::stream) .toList(); if (!CollectionUtils.isEmpty(completionSpecifications)) { serverBuilder.completions(completionSpecifications); - capabilitiesBuilder.completions(); logger.info("Registered completions: " + completionSpecifications.size()); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java index f82390a2f..467d68884 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java @@ -132,6 +132,56 @@ public class McpServerProperties { */ private ServerType type = ServerType.SYNC; + private Capabilities capabilities = new Capabilities(); + + public static class Capabilities { + + private boolean resource = true; + + private boolean tool = true; + + private boolean prompt = true; + + private boolean completion = true; + + public boolean isResource() { + return resource; + } + + public void setResource(boolean resource) { + this.resource = resource; + } + + public boolean isTool() { + return tool; + } + + public void setTool(boolean tool) { + this.tool = tool; + } + + public boolean isPrompt() { + return prompt; + } + + public void setPrompt(boolean prompt) { + this.prompt = prompt; + } + + public boolean isCompletion() { + return completion; + } + + public void setCompletion(boolean completion) { + this.completion = completion; + } + + } + + public Capabilities getCapabilities() { + return capabilities; + } + /** * Server types supported by the MCP server. */ diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java index c3919b7f7..32bc699ff 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java @@ -18,11 +18,16 @@ package org.springframework.ai.mcp.server.autoconfigure; import java.util.List; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpAsyncServerExchange; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; +import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification; import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; @@ -38,6 +43,7 @@ import reactor.core.publisher.Mono; import org.springframework.ai.mcp.SyncMcpToolCallback; import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -66,6 +72,12 @@ public class McpServerAutoConfigurationIT { assertThat(properties.isToolChangeNotification()).isTrue(); assertThat(properties.isResourceChangeNotification()).isTrue(); assertThat(properties.isPromptChangeNotification()).isTrue(); + + // Check capabilities + assertThat(properties.getCapabilities().isTool()).isTrue(); + assertThat(properties.getCapabilities().isResource()).isTrue(); + assertThat(properties.getCapabilities().isPrompt()).isTrue(); + assertThat(properties.getCapabilities().isCompletion()).isTrue(); }); } @@ -86,6 +98,18 @@ public class McpServerAutoConfigurationIT { }); } + @Test + void syncServerInstructionsConfiguration() { + this.contextRunner.withPropertyValues("spring.ai.mcp.server.instructions=Sync Server Instructions") + .run(context -> { + McpServerProperties properties = context.getBean(McpServerProperties.class); + assertThat(properties.getInstructions()).isEqualTo("Sync Server Instructions"); + + McpSyncServer server = context.getBean(McpSyncServer.class); + assertThat(server).isNotNull(); + }); + } + @Test void transportConfiguration() { this.contextRunner.withUserConfiguration(CustomTransportConfiguration.class).run(context -> { @@ -207,6 +231,16 @@ public class McpServerAutoConfigurationIT { }); } + @Test + void asyncRootsChangeHandlerConfiguration() { + this.contextRunner.withPropertyValues("spring.ai.mcp.server.type=ASYNC") + .withUserConfiguration(TestAsyncRootsHandlerConfiguration.class) + .run(context -> { + McpAsyncServer server = context.getBean(McpAsyncServer.class); + assertThat(server).isNotNull(); + }); + } + @Configuration static class TestResourceConfiguration { @@ -243,6 +277,63 @@ public class McpServerAutoConfigurationIT { } + @Test + void capabilitiesConfiguration() { + this.contextRunner.withPropertyValues("spring.ai.mcp.server.capabilities.tool=false", + "spring.ai.mcp.server.capabilities.resource=false", "spring.ai.mcp.server.capabilities.prompt=false", + "spring.ai.mcp.server.capabilities.completion=false") + .run(context -> { + McpServerProperties properties = context.getBean(McpServerProperties.class); + assertThat(properties.getCapabilities().isTool()).isFalse(); + assertThat(properties.getCapabilities().isResource()).isFalse(); + assertThat(properties.getCapabilities().isPrompt()).isFalse(); + assertThat(properties.getCapabilities().isCompletion()).isFalse(); + }); + } + + @Test + void toolResponseMimeTypeConfiguration() { + this.contextRunner.withPropertyValues("spring.ai.mcp.server.tool-response-mime-type.test-tool=application/json") + .withUserConfiguration(TestToolConfiguration.class) + .run(context -> { + McpServerProperties properties = context.getBean(McpServerProperties.class); + assertThat(properties.getToolResponseMimeType()).containsEntry("test-tool", "application/json"); + + // Verify the MIME type is applied to the tool specifications + List tools = context.getBean("syncTools", List.class); + assertThat(tools).hasSize(1); + + // The server should be properly configured with the tool + McpSyncServer server = context.getBean(McpSyncServer.class); + assertThat(server).isNotNull(); + }); + } + + @Test + void completionSpecificationConfiguration() { + this.contextRunner.withUserConfiguration(TestCompletionConfiguration.class).run(context -> { + List completions = context.getBean("testCompletions", List.class); + assertThat(completions).hasSize(1); + }); + } + + @Test + void asyncCompletionSpecificationConfiguration() { + this.contextRunner.withPropertyValues("spring.ai.mcp.server.type=ASYNC") + .withUserConfiguration(TestAsyncCompletionConfiguration.class) + .run(context -> { + List completions = context.getBean("testAsyncCompletions", List.class); + assertThat(completions).hasSize(1); + }); + } + + @Test + void toolCallbackProviderConfiguration() { + this.contextRunner.withUserConfiguration(TestToolCallbackProviderConfiguration.class).run(context -> { + assertThat(context).hasSingleBean(ToolCallbackProvider.class); + }); + } + @Configuration static class TestToolConfiguration { @@ -262,6 +353,61 @@ public class McpServerAutoConfigurationIT { } + @Configuration + static class TestToolCallbackProviderConfiguration { + + @Bean + ToolCallbackProvider testToolCallbackProvider() { + return () -> { + McpSyncClient mockClient = Mockito.mock(McpSyncClient.class); + McpSchema.Tool mockTool = Mockito.mock(McpSchema.Tool.class); + + Mockito.when(mockTool.name()).thenReturn("provider-tool"); + Mockito.when(mockTool.description()).thenReturn("Provider Tool"); + when(mockClient.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient", "1.0.0")); + + return new ToolCallback[] { new SyncMcpToolCallback(mockClient, mockTool) }; + }; + } + + } + + @Configuration + static class TestCompletionConfiguration { + + @Bean + List testCompletions() { + + BiFunction completionHandler = ( + exchange, request) -> { + // Test implementation + return new McpSchema.CompleteResult( + new McpSchema.CompleteResult.CompleteCompletion(List.of(), 0, false)); + }; + + return List.of(new McpServerFeatures.SyncCompletionSpecification( + new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler)); + } + + } + + @Configuration + static class TestAsyncCompletionConfiguration { + + @Bean + List testAsyncCompletions() { + BiFunction> completionHandler = ( + exchange, request) -> { + // Test implementation + return Mono.just(new McpSchema.CompleteResult( + new McpSchema.CompleteResult.CompleteCompletion(List.of(), 0, false))); + }; + return List.of(new McpServerFeatures.AsyncCompletionSpecification( + new McpSchema.PromptReference("ref/prompt", "code_review"), completionHandler)); + } + + } + @Configuration static class TestRootsHandlerConfiguration { @@ -274,6 +420,18 @@ public class McpServerAutoConfigurationIT { } + @Configuration + static class TestAsyncRootsHandlerConfiguration { + + @Bean + BiConsumer> rootsChangeHandler() { + return (exchange, roots) -> { + // Test implementation + }; + } + + } + static class CustomServerTransport implements McpServerTransport { @Override diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc index 647de672b..79f1b7001 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc @@ -91,7 +91,12 @@ All properties are prefixed with `spring.ai.mcp.server`: |`stdio` |Enable/disable stdio transport |`false` |`name` |Server name for identification |`mcp-server` |`version` |Server version |`1.0.0` +|`instructions` |Optional instructions to provide guidance to the client on how to interact with this server |`null` |`type` |Server type (SYNC/ASYNC) |`SYNC` +|`capabilities.resource` |Enable/disable resource capabilities |`true` +|`capabilities.tool` |Enable/disable tool capabilities |`true` +|`capabilities.prompt` |Enable/disable prompt capabilities |`true` +|`capabilities.completion` |Enable/disable completion capabilities |`true` |`resource-change-notification` |Enable resource change notifications |`true` |`prompt-change-notification` |Enable prompt change notifications |`true` |`tool-change-notification` |Enable tool change notifications |`true` @@ -112,6 +117,17 @@ When activated, it automatically handles the configuration of synchronous tool s To enable this server type, configure your application with `spring.ai.mcp.server.type=ASYNC`. This server type automatically sets up asynchronous tool specifications with built-in Project Reactor support. +== Server Capabilities + +The MCP Server supports four main capability types that can be individually enabled or disabled: + +* **Tools** - Enable/disable tool capabilities with `spring.ai.mcp.server.capabilities.tool=true|false` +* **Resources** - Enable/disable resource capabilities with `spring.ai.mcp.server.capabilities.resource=true|false` +* **Prompts** - Enable/disable prompt capabilities with `spring.ai.mcp.server.capabilities.prompt=true|false` +* **Completions** - Enable/disable completion capabilities with `spring.ai.mcp.server.capabilities.completion=true|false` + +All capabilities are enabled by default. Disabling a capability will prevent the server from registering and exposing the corresponding features to clients. + == Transport Options The MCP Server supports three transport mechanisms, each with its dedicated starter: @@ -141,6 +157,17 @@ public ToolCallbackProvider myTools(...) { } ---- +or directly as individual tool callbacks: + +[source,java] +---- +@Bean +public ToolCallback myTool(...) { + // Create and return a single tool callback + return new ToolCallback(...); +} +---- + or using the low-level API: [source,java] @@ -152,6 +179,13 @@ public List myTools(...) { } ---- +The auto-configuration will automatically detect and register all tool callbacks from: +* Individual `ToolCallback` beans +* Lists of `ToolCallback` beans +* `ToolCallbackProvider` beans + +Tools are de-duplicated by name, with the first occurrence of each tool name being used. + === link:https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/[Resource Management] Provides a standardized way for servers to expose resources to clients. @@ -210,6 +244,33 @@ public List myPrompts() { } ---- +=== link:https://spec.modelcontextprotocol.io/specification/2024-11-05/server/completions/[Completion Management] + +Provides a standardized way for servers to expose completion capabilities to clients. + +* Support for both sync and async completion specifications +* Automatic registration through Spring beans: + +[source,java] +---- +@Bean +public List myCompletions() { + var completion = new McpServerFeatures.SyncCompletionSpecification( + "code-completion", + "Provides code completion suggestions", + (exchange, request) -> { + // Implementation that returns completion suggestions + return new McpSchema.CompletionResult(List.of( + new McpSchema.Completion("suggestion1", "First suggestion"), + new McpSchema.Completion("suggestion2", "Second suggestion") + )); + } + ); + + return List.of(completion); +} +---- + === link:https://spec.modelcontextprotocol.io/specification/2024-11-05/client/roots/#root-list-changes[Root Change Consumers] When roots change, clients that support `listChanged` send a Root Change notification. @@ -254,7 +315,13 @@ spring: name: webmvc-mcp-server version: 1.0.0 type: SYNC + instructions: "This server provides weather information tools and resources" sse-message-endpoint: /mcp/messages + capabilities: + tool: true + resource: true + prompt: true + completion: true ---- === WebFlux Server Configuration @@ -268,7 +335,13 @@ spring: name: webflux-mcp-server version: 1.0.0 type: ASYNC # Recommended for reactive applications + instructions: "This reactive server provides weather information tools and resources" sse-message-endpoint: /mcp/messages + capabilities: + tool: true + resource: true + prompt: true + completion: true ---- === Creating a Spring Boot Application with MCP Server