feat(mcp): Add request timeout configuration for MCP server
Add support for configuring request timeout in MCP server with a default of 20 seconds. This timeout applies to all requests made through the client, including tool calls, resource access, and prompt operations. - Add requestTimeout property to McpServerProperties with default of 20s - Configure server builder with the timeout value - Add tests for default and custom timeout configurations - Update documentation with the new property Resolves #3205 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
@@ -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<AsyncToolSpecification> toolSpecifications = new ArrayList<>(
|
||||
tools.stream().flatMap(List::stream).toList());
|
||||
List<ToolCallback> 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<AsyncToolSpecification> toolSpecifications = new ArrayList<>(
|
||||
tools.stream().flatMap(List::stream).toList());
|
||||
List<ToolCallback> 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<AsyncResourceSpecification> resourceSpecifications = resources.stream().flatMap(List::stream).toList();
|
||||
if (!CollectionUtils.isEmpty(resourceSpecifications)) {
|
||||
serverBuilder.resources(resourceSpecifications);
|
||||
logger.info("Registered resources: " + resourceSpecifications.size());
|
||||
List<AsyncResourceSpecification> 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<AsyncPromptSpecification> promptSpecifications = prompts.stream().flatMap(List::stream).toList();
|
||||
if (!CollectionUtils.isEmpty(promptSpecifications)) {
|
||||
serverBuilder.prompts(promptSpecifications);
|
||||
logger.info("Registered prompts: " + promptSpecifications.size());
|
||||
List<AsyncPromptSpecification> 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<AsyncCompletionSpecification> completionSpecifications = completions.stream()
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
if (!CollectionUtils.isEmpty(completionSpecifications)) {
|
||||
serverBuilder.completions(completionSpecifications);
|
||||
logger.info("Registered completions: " + completionSpecifications.size());
|
||||
List<AsyncCompletionSpecification> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user