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 b9111188d..2ba5530f5 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 @@ -256,6 +256,8 @@ public class McpServerAutoConfiguration { serverBuilder.instructions(serverProperties.getInstructions()); + serverBuilder.requestTimeout(serverProperties.getRequestTimeout()); + return serverBuilder.build(); } @@ -311,26 +313,26 @@ public class McpServerAutoConfiguration { // Create the server with both tool and resource capabilities AsyncSpecification serverBuilder = McpServer.async(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.toAsyncToolSpecification(providerToolCallbacks, serverProperties)); - // Tools if (serverProperties.getCapabilities().isTool()) { + 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.toAsyncToolSpecification(providerToolCallbacks, serverProperties)); + logger.info("Enable tools capabilities, notification: " + serverProperties.isToolChangeNotification()); capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); - } - if (!CollectionUtils.isEmpty(toolSpecifications)) { - serverBuilder.tools(toolSpecifications); - logger.info("Registered tools: " + toolSpecifications.size()); + if (!CollectionUtils.isEmpty(toolSpecifications)) { + serverBuilder.tools(toolSpecifications); + logger.info("Registered tools: " + toolSpecifications.size()); + } } // Resources @@ -338,36 +340,38 @@ public class McpServerAutoConfiguration { 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); - logger.info("Registered resources: " + resourceSpecifications.size()); + List resourceSpecifications = resources.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(resourceSpecifications)) { + serverBuilder.resources(resourceSpecifications); + 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); - logger.info("Registered prompts: " + promptSpecifications.size()); + List promptSpecifications = prompts.stream().flatMap(List::stream).toList(); + + if (!CollectionUtils.isEmpty(promptSpecifications)) { + serverBuilder.prompts(promptSpecifications); + 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); - 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()); + } } rootsChangeConsumer.ifAvailable(consumer -> { @@ -383,6 +387,8 @@ public class McpServerAutoConfiguration { serverBuilder.instructions(serverProperties.getInstructions()); + serverBuilder.requestTimeout(serverProperties.getRequestTimeout()); + return serverBuilder.build(); } 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 6dcada931..12f82c729 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 @@ -16,6 +16,7 @@ package org.springframework.ai.mcp.server.autoconfigure; +import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -134,6 +135,22 @@ public class McpServerProperties { private Capabilities capabilities = new Capabilities(); + /** + * Sets the duration to wait for server responses before timing out requests. This + * timeout applies to all requests made through the client, including tool calls, + * resource access, and prompt operations. + */ + private Duration requestTimeout = Duration.ofSeconds(20); + + public Duration getRequestTimeout() { + return this.requestTimeout; + } + + public void setRequestTimeout(Duration requestTimeout) { + Assert.notNull(requestTimeout, "Request timeout must not be null"); + this.requestTimeout = requestTimeout; + } + public Capabilities getCapabilities() { return this.capabilities; } 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 c9de2ae7d..b81b1fa85 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 @@ -72,6 +72,10 @@ public class McpServerAutoConfigurationIT { assertThat(properties.isToolChangeNotification()).isTrue(); assertThat(properties.isResourceChangeNotification()).isTrue(); assertThat(properties.isPromptChangeNotification()).isTrue(); + assertThat(properties.getRequestTimeout().getSeconds()).isEqualTo(20); + assertThat(properties.getBaseUrl()).isEqualTo(""); + assertThat(properties.getSseEndpoint()).isEqualTo("/sse"); + assertThat(properties.getSseMessageEndpoint()).isEqualTo("/mcp/message"); // Check capabilities assertThat(properties.getCapabilities().isTool()).isTrue(); @@ -85,7 +89,8 @@ public class McpServerAutoConfigurationIT { void asyncConfiguration() { this.contextRunner .withPropertyValues("spring.ai.mcp.server.type=ASYNC", "spring.ai.mcp.server.name=test-server", - "spring.ai.mcp.server.version=2.0.0", "spring.ai.mcp.server.instructions=My MCP Server") + "spring.ai.mcp.server.version=2.0.0", "spring.ai.mcp.server.instructions=My MCP Server", + "spring.ai.mcp.server.request-timeout=30s") .run(context -> { assertThat(context).hasSingleBean(McpAsyncServer.class); assertThat(context).doesNotHaveBean(McpSyncServer.class); @@ -95,6 +100,7 @@ public class McpServerAutoConfigurationIT { assertThat(properties.getVersion()).isEqualTo("2.0.0"); assertThat(properties.getInstructions()).isEqualTo("My MCP Server"); assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.ASYNC); + assertThat(properties.getRequestTimeout().getSeconds()).isEqualTo(30); }); } @@ -252,6 +258,10 @@ public class McpServerAutoConfigurationIT { assertThat(properties.getCapabilities().isResource()).isFalse(); assertThat(properties.getCapabilities().isPrompt()).isFalse(); assertThat(properties.getCapabilities().isCompletion()).isFalse(); + + // Verify the server is configured with the disabled capabilities + McpSyncServer server = context.getBean(McpSyncServer.class); + assertThat(server).isNotNull(); }); } @@ -273,6 +283,36 @@ public class McpServerAutoConfigurationIT { }); } + @Test + void requestTimeoutConfiguration() { + this.contextRunner.withPropertyValues("spring.ai.mcp.server.request-timeout=45s").run(context -> { + McpServerProperties properties = context.getBean(McpServerProperties.class); + assertThat(properties.getRequestTimeout().getSeconds()).isEqualTo(45); + + // Verify the server is configured with the timeout + McpSyncServer server = context.getBean(McpSyncServer.class); + assertThat(server).isNotNull(); + }); + } + + @Test + void endpointConfiguration() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.server.base-url=http://localhost:8080", + "spring.ai.mcp.server.sse-endpoint=/events", + "spring.ai.mcp.server.sse-message-endpoint=/api/mcp/message") + .run(context -> { + McpServerProperties properties = context.getBean(McpServerProperties.class); + assertThat(properties.getBaseUrl()).isEqualTo("http://localhost:8080"); + assertThat(properties.getSseEndpoint()).isEqualTo("/events"); + assertThat(properties.getSseMessageEndpoint()).isEqualTo("/api/mcp/message"); + + // Verify the server is configured with the endpoints + McpSyncServer server = context.getBean(McpSyncServer.class); + assertThat(server).isNotNull(); + }); + } + @Test void completionSpecificationConfiguration() { this.contextRunner.withUserConfiguration(TestCompletionConfiguration.class).run(context -> { 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 79f1b7001..42493036a 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 @@ -104,6 +104,7 @@ All properties are prefixed with `spring.ai.mcp.server`: |`sse-message-endpoint` | Custom SSE Message endpoint path for web transport to be used by the client to send messages|`/mcp/message` |`sse-endpoint` |Custom SSE endpoint path for web transport |`/sse` |`base-url` | Optional URL prefix. For example `base-url=/api/v1` means that the client should access the sse endpont at `/api/v1` + `sse-endpoint` and the message endpoint is `/api/v1` + `sse-message-endpoint` | - +|`request-timeout` | Duration to wait for server responses before timing out requests. Applies to all requests made through the client, including tool calls, resource access, and prompt operations. | `20` seconds |=== == Sync/Async Server Types