Skip to content

Document ideal pattern for use of MCP with CommandLineRunner #2756

Open
@codefromthecrypt

Description

@codefromthecrypt

Expected Behavior

I would like to use CommandLineRunner with an MCP stdio server without thinking about it and without a hang.

Current Behavior

Currently, if I use an MCP stdio server it hangs after the CommandLineRunner completes

If I use the approach of SpringApplication.close(), it errs then hangs

OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended [otel.javaagent 2025-04-16 00:41:36:711 +0000] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 1.3.0 2025-04-16T00:42:05.032Z ERROR 1 --- [ main] [ ] o.s.boot.SpringApplication : Application run failed

reactor.core.Exceptions$ReactiveException: java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 20000ms in 'source(MonoCreate)' (and no fallback has been configured)
at reactor.core.Exceptions.propagate(Exceptions.java:410) ~[reactor-core-3.7.4.jar!/:3.7.4]
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:102) ~[reactor-core-3.7.4.jar!/:3.7.4]
at reactor.core.publisher.Mono.block(Mono.java:1779) ~[reactor-core-3.7.4.jar!/:3.7.4]
at io.modelcontextprotocol.client.McpSyncClient.callTool(McpSyncClient.java:200) ~[mcp-0.9.0.jar!/:0.9.0]
at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:115) ~[spring-ai-mcp-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:125) ~[spring-ai-mcp-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(DefaultToolCallingManager.java:227) ~[spring-ai-model-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:139) ~[spring-ai-model-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.azure.openai.AzureOpenAiChatModel.internalCall(AzureOpenAiChatModel.java:265) ~[spring-ai-azure-openai-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.azure.openai.AzureOpenAiChatModel.call(AzureOpenAiChatModel.java:240) ~[spring-ai-azure-openai-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1.aroundCall(DefaultChatClient.java:680) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextAroundCall$1(DefaultAroundAdvisorChain.java:98) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at io.micrometer.observation.Observation.observe(Observation.java:564) ~[micrometer-observation-1.14.5.jar!/:1.14.5]
at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextAroundCall(DefaultAroundAdvisorChain.java:98) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetChatResponse(DefaultChatClient.java:493) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.lambda$doGetObservableChatResponse$1(DefaultChatClient.java:482) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at io.micrometer.observation.Observation.observe(Observation.java:564) ~[micrometer-observation-1.14.5.jar!/:1.14.5]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetObservableChatResponse(DefaultChatClient.java:482) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetChatResponse(DefaultChatClient.java:466) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.content(DefaultChatClient.java:516) ~[spring-ai-client-chat-1.0.0-M7.jar!/:1.0.0-M7]
at example.VersionAgent.run(VersionAgent.java:34) ~[!/:0.0.1-SNAPSHOT]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Unknown Source) ~[na:na]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89) ~[spring-aop-6.2.5.jar!/:6.2.5]
at io.micrometer.tracing.annotation.SpanAspectMethodInvocation.proceed(SpanAspectMethodInvocation.java:47) ~[micrometer-tracing-1.4.4.jar!/:1.4.4]
at io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor.proceedUnderSynchronousSpan(ImperativeMethodInvocationProcessor.java:91) ~[micrometer-tracing-1.4.4.jar!/:1.4.4]
at io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor.process(ImperativeMethodInvocationProcessor.java:73) ~[micrometer-tracing-1.4.4.jar!/:1.4.4]
at io.micrometer.tracing.annotation.SpanAspect.newSpanMethod(SpanAspect.java:59) ~[micrometer-tracing-1.4.4.jar!/:1.4.4]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Unknown Source) ~[na:na]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.5.jar!/:6.2.5]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:727) ~[spring-aop-6.2.5.jar!/:6.2.5]
at example.VersionAgent$$SpringCGLIB$$0.run() ~[!/:0.0.1-SNAPSHOT]
at org.springframework.boot.SpringApplication.lambda$callRunner$5(SpringApplication.java:788) ~[spring-boot-3.4.4.jar!/:3.4.4]
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:82) ~[spring-core-6.2.5.jar!/:6.2.5]
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60) ~[spring-core-6.2.5.jar!/:6.2.5]
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:86) ~[spring-core-6.2.5.jar!/:6.2.5]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:796) ~[spring-boot-3.4.4.jar!/:3.4.4]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:787) ~[spring-boot-3.4.4.jar!/:3.4.4]
at org.springframework.boot.SpringApplication.lambda$callRunners$3(SpringApplication.java:772) ~[spring-boot-3.4.4.jar!/:3.4.4]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source) ~[na:na]
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(Unknown Source) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source) ~[na:na]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:772) ~[spring-boot-3.4.4.jar!/:3.4.4]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:325) ~[spring-boot-3.4.4.jar!/:3.4.4]
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:149) ~[spring-boot-3.4.4.jar!/:3.4.4]
at example.Main.run(Main.java:66) ~[!/:0.0.1-SNAPSHOT]
at example.McpClientAgent.main(Mcp.java:90) ~[!/:0.0.1-SNAPSHOT]
at example.Main.main(Main.java:44) ~[!/:0.0.1-SNAPSHOT]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Unknown Source) ~[na:na]
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:102) ~[genai-function-calling.jar:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:64) ~[genai-function-calling.jar:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.launch.PropertiesLauncher.main(PropertiesLauncher.java:580) ~[genai-function-calling.jar:0.0.1-SNAPSHOT]
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:104) ~[reactor-core-3.7.4.jar!/:3.7.4]
... 65 common frames omitted
Caused by: java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 20000ms in 'source(MonoCreate)' (and no fallback has been configured)
at reactor.core.publisher.FluxTimeout$TimeoutMainSubscriber.handleTimeout(FluxTimeout.java:296) ~[reactor-core-3.7.4.jar!/:3.7.4]
at reactor.core.publisher.FluxTimeout$TimeoutMainSubscriber.doTimeout(FluxTimeout.java:281) ~[reactor-core-3.7.4.jar!/:3.7.4]
at reactor.core.publisher.FluxTimeout$TimeoutTimeoutSubscriber.onNext(FluxTimeout.java:420) ~[reactor-core-3.7.4.jar!/:3.7.4]
at io.opentelemetry.javaagent.shaded.instrumentation.reactor.v3_1.TracingSubscriber.onNext(TracingSubscriber.java:68) ~[elastic-otel-javaagent.jar:na]
at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onNext(FluxOnErrorReturn.java:162) ~[reactor-core-3.7.4.jar!/:3.7.4]
at io.opentelemetry.javaagent.shaded.instrumentation.reactor.v3_1.TracingSubscriber.onNext(TracingSubscriber.java:68) ~[elastic-otel-javaagent.jar:na]
at reactor.core.publisher.MonoDelay$MonoDelayRunnable.propagateDelay(MonoDelay.java:270) ~[reactor-core-3.7.4.jar!/:3.7.4]
at reactor.core.publisher.MonoDelay$MonoDelayRunnable.run(MonoDelay.java:285) ~[reactor-core-3.7.4.jar!/:3.7.4]
at io.opentelemetry.javaagent.shaded.instrumentation.reactor.v3_1.ContextPropagationOperator$RunnableWrapper.run(ContextPropagationOperator.java:373) ~[elastic-otel-javaagent.jar:na]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.7.4.jar!/:3.7.4]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.7.4.jar!/:3.7.4]
at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) ~[na:na]
at java.base/java.lang.Thread.run(Unknown Source) ~[na:na]

The only way that I can get the process to exit without hanging or erring is to use the same pattern as the examples show: Inject a ConfigurableApplicationContext to your CommandLineRunner and then close it before exiting its function.

Context

I want to showcase Spring AI in simplest form, and it is simple unless I use an MCP server, which introduces a curious workaround. Even if in spring boot there are concerns we can't address for all use cases, possibly Spring AI can have some decoration of a CommandLineRunner that integrates with and closes its MCP servers.

Even if ConfigurableApplicationContext.close() is the only viable way out, I appreciate your attention.

cc @tzolov @philwebb @ThomasVitale

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions