Skip to content

Commit 9ed7535

Browse files
committed
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 <[email protected]>
1 parent 8b8fb4f commit 9ed7535

File tree

4 files changed

+97
-33
lines changed

4 files changed

+97
-33
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java

+38-32
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,
256256

257257
serverBuilder.instructions(serverProperties.getInstructions());
258258

259+
serverBuilder.requestTimeout(serverProperties.getRequestTimeout());
260+
259261
return serverBuilder.build();
260262
}
261263

@@ -311,63 +313,65 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvide
311313
// Create the server with both tool and resource capabilities
312314
AsyncSpecification serverBuilder = McpServer.async(transportProvider).serverInfo(serverInfo);
313315

314-
List<AsyncToolSpecification> toolSpecifications = new ArrayList<>(
315-
tools.stream().flatMap(List::stream).toList());
316-
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
317-
.map(pr -> List.of(pr.getToolCallbacks()))
318-
.flatMap(List::stream)
319-
.filter(fc -> fc instanceof ToolCallback)
320-
.map(fc -> (ToolCallback) fc)
321-
.toList();
322-
323-
toolSpecifications.addAll(this.toAsyncToolSpecification(providerToolCallbacks, serverProperties));
324-
325316
// Tools
326317
if (serverProperties.getCapabilities().isTool()) {
318+
List<AsyncToolSpecification> toolSpecifications = new ArrayList<>(
319+
tools.stream().flatMap(List::stream).toList());
320+
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
321+
.map(pr -> List.of(pr.getToolCallbacks()))
322+
.flatMap(List::stream)
323+
.filter(fc -> fc instanceof ToolCallback)
324+
.map(fc -> (ToolCallback) fc)
325+
.toList();
326+
327+
toolSpecifications.addAll(this.toAsyncToolSpecification(providerToolCallbacks, serverProperties));
328+
327329
logger.info("Enable tools capabilities, notification: " + serverProperties.isToolChangeNotification());
328330
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
329-
}
330331

331-
if (!CollectionUtils.isEmpty(toolSpecifications)) {
332-
serverBuilder.tools(toolSpecifications);
333-
logger.info("Registered tools: " + toolSpecifications.size());
332+
if (!CollectionUtils.isEmpty(toolSpecifications)) {
333+
serverBuilder.tools(toolSpecifications);
334+
logger.info("Registered tools: " + toolSpecifications.size());
335+
}
334336
}
335337

336338
// Resources
337339
if (serverProperties.getCapabilities().isResource()) {
338340
logger.info(
339341
"Enable resources capabilities, notification: " + serverProperties.isResourceChangeNotification());
340342
capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification());
341-
}
342343

343-
List<AsyncResourceSpecification> resourceSpecifications = resources.stream().flatMap(List::stream).toList();
344-
if (!CollectionUtils.isEmpty(resourceSpecifications)) {
345-
serverBuilder.resources(resourceSpecifications);
346-
logger.info("Registered resources: " + resourceSpecifications.size());
344+
List<AsyncResourceSpecification> resourceSpecifications = resources.stream().flatMap(List::stream).toList();
345+
if (!CollectionUtils.isEmpty(resourceSpecifications)) {
346+
serverBuilder.resources(resourceSpecifications);
347+
logger.info("Registered resources: " + resourceSpecifications.size());
348+
}
347349
}
348350

349351
// Prompts
350352
if (serverProperties.getCapabilities().isPrompt()) {
351353
logger.info("Enable prompts capabilities, notification: " + serverProperties.isPromptChangeNotification());
352354
capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification());
353-
}
354-
List<AsyncPromptSpecification> promptSpecifications = prompts.stream().flatMap(List::stream).toList();
355-
if (!CollectionUtils.isEmpty(promptSpecifications)) {
356-
serverBuilder.prompts(promptSpecifications);
357-
logger.info("Registered prompts: " + promptSpecifications.size());
355+
List<AsyncPromptSpecification> promptSpecifications = prompts.stream().flatMap(List::stream).toList();
356+
357+
if (!CollectionUtils.isEmpty(promptSpecifications)) {
358+
serverBuilder.prompts(promptSpecifications);
359+
logger.info("Registered prompts: " + promptSpecifications.size());
360+
}
358361
}
359362

360363
// Completions
361364
if (serverProperties.getCapabilities().isCompletion()) {
362365
logger.info("Enable completions capabilities");
363366
capabilitiesBuilder.completions();
364-
}
365-
List<AsyncCompletionSpecification> completionSpecifications = completions.stream()
366-
.flatMap(List::stream)
367-
.toList();
368-
if (!CollectionUtils.isEmpty(completionSpecifications)) {
369-
serverBuilder.completions(completionSpecifications);
370-
logger.info("Registered completions: " + completionSpecifications.size());
367+
List<AsyncCompletionSpecification> completionSpecifications = completions.stream()
368+
.flatMap(List::stream)
369+
.toList();
370+
371+
if (!CollectionUtils.isEmpty(completionSpecifications)) {
372+
serverBuilder.completions(completionSpecifications);
373+
logger.info("Registered completions: " + completionSpecifications.size());
374+
}
371375
}
372376

373377
rootsChangeConsumer.ifAvailable(consumer -> {
@@ -383,6 +387,8 @@ public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvide
383387

384388
serverBuilder.instructions(serverProperties.getInstructions());
385389

390+
serverBuilder.requestTimeout(serverProperties.getRequestTimeout());
391+
386392
return serverBuilder.build();
387393
}
388394

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java

+17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.ai.mcp.server.autoconfigure;
1818

19+
import java.time.Duration;
1920
import java.util.HashMap;
2021
import java.util.Map;
2122

@@ -134,6 +135,22 @@ public class McpServerProperties {
134135

135136
private Capabilities capabilities = new Capabilities();
136137

138+
/**
139+
* Sets the duration to wait for server responses before timing out requests. This
140+
* timeout applies to all requests made through the client, including tool calls,
141+
* resource access, and prompt operations.
142+
*/
143+
private Duration requestTimeout = Duration.ofSeconds(20);
144+
145+
public Duration getRequestTimeout() {
146+
return this.requestTimeout;
147+
}
148+
149+
public void setRequestTimeout(Duration requestTimeout) {
150+
Assert.notNull(requestTimeout, "Request timeout must not be null");
151+
this.requestTimeout = requestTimeout;
152+
}
153+
137154
public Capabilities getCapabilities() {
138155
return this.capabilities;
139156
}

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java

+41-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ void defaultConfiguration() {
7272
assertThat(properties.isToolChangeNotification()).isTrue();
7373
assertThat(properties.isResourceChangeNotification()).isTrue();
7474
assertThat(properties.isPromptChangeNotification()).isTrue();
75+
assertThat(properties.getRequestTimeout().getSeconds()).isEqualTo(20);
76+
assertThat(properties.getBaseUrl()).isEqualTo("");
77+
assertThat(properties.getSseEndpoint()).isEqualTo("/sse");
78+
assertThat(properties.getSseMessageEndpoint()).isEqualTo("/mcp/message");
7579

7680
// Check capabilities
7781
assertThat(properties.getCapabilities().isTool()).isTrue();
@@ -85,7 +89,8 @@ void defaultConfiguration() {
8589
void asyncConfiguration() {
8690
this.contextRunner
8791
.withPropertyValues("spring.ai.mcp.server.type=ASYNC", "spring.ai.mcp.server.name=test-server",
88-
"spring.ai.mcp.server.version=2.0.0", "spring.ai.mcp.server.instructions=My MCP Server")
92+
"spring.ai.mcp.server.version=2.0.0", "spring.ai.mcp.server.instructions=My MCP Server",
93+
"spring.ai.mcp.server.request-timeout=30s")
8994
.run(context -> {
9095
assertThat(context).hasSingleBean(McpAsyncServer.class);
9196
assertThat(context).doesNotHaveBean(McpSyncServer.class);
@@ -95,6 +100,7 @@ void asyncConfiguration() {
95100
assertThat(properties.getVersion()).isEqualTo("2.0.0");
96101
assertThat(properties.getInstructions()).isEqualTo("My MCP Server");
97102
assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.ASYNC);
103+
assertThat(properties.getRequestTimeout().getSeconds()).isEqualTo(30);
98104
});
99105
}
100106

@@ -252,6 +258,10 @@ void capabilitiesConfiguration() {
252258
assertThat(properties.getCapabilities().isResource()).isFalse();
253259
assertThat(properties.getCapabilities().isPrompt()).isFalse();
254260
assertThat(properties.getCapabilities().isCompletion()).isFalse();
261+
262+
// Verify the server is configured with the disabled capabilities
263+
McpSyncServer server = context.getBean(McpSyncServer.class);
264+
assertThat(server).isNotNull();
255265
});
256266
}
257267

@@ -273,6 +283,36 @@ void toolResponseMimeTypeConfiguration() {
273283
});
274284
}
275285

286+
@Test
287+
void requestTimeoutConfiguration() {
288+
this.contextRunner.withPropertyValues("spring.ai.mcp.server.request-timeout=45s").run(context -> {
289+
McpServerProperties properties = context.getBean(McpServerProperties.class);
290+
assertThat(properties.getRequestTimeout().getSeconds()).isEqualTo(45);
291+
292+
// Verify the server is configured with the timeout
293+
McpSyncServer server = context.getBean(McpSyncServer.class);
294+
assertThat(server).isNotNull();
295+
});
296+
}
297+
298+
@Test
299+
void endpointConfiguration() {
300+
this.contextRunner
301+
.withPropertyValues("spring.ai.mcp.server.base-url=http://localhost:8080",
302+
"spring.ai.mcp.server.sse-endpoint=/events",
303+
"spring.ai.mcp.server.sse-message-endpoint=/api/mcp/message")
304+
.run(context -> {
305+
McpServerProperties properties = context.getBean(McpServerProperties.class);
306+
assertThat(properties.getBaseUrl()).isEqualTo("http://localhost:8080");
307+
assertThat(properties.getSseEndpoint()).isEqualTo("/events");
308+
assertThat(properties.getSseMessageEndpoint()).isEqualTo("/api/mcp/message");
309+
310+
// Verify the server is configured with the endpoints
311+
McpSyncServer server = context.getBean(McpSyncServer.class);
312+
assertThat(server).isNotNull();
313+
});
314+
}
315+
276316
@Test
277317
void completionSpecificationConfiguration() {
278318
this.contextRunner.withUserConfiguration(TestCompletionConfiguration.class).run(context -> {

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ All properties are prefixed with `spring.ai.mcp.server`:
104104
|`sse-message-endpoint` | Custom SSE Message endpoint path for web transport to be used by the client to send messages|`/mcp/message`
105105
|`sse-endpoint` |Custom SSE endpoint path for web transport |`/sse`
106106
|`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` | -
107+
|`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
107108
|===
108109

109110
== Sync/Async Server Types

0 commit comments

Comments
 (0)