Let's get started with a Microservice Architecture with Spring Cloud:
MCP Elicitations With Spring AI
Last updated: February 10, 2026
1. Overview
When working with Model Context Protocol (MCP), there are scenarios where MCP servers need additional details from users during tool execution that weren’t included in the original request. Without a standardized way to request this information, the tool has no way to communicate this back to the client and fails to execute.
MCP Elicitations address this issue by allowing the MCP server to pause and explicitly request the missing information from the user. This enables us to build interactive tools capable of dynamically gathering additional context.
In this tutorial, we’ll explore how to implement MCP Elicitations using Spring AI.
2. Creating an MCP Server
Let’s start by building an MCP server that exposes a tool for fetching author details.
We’ll design this tool to conditionally trigger an elicitation request for additional details if they are missing from the original request.
2.1. Dependencies and Configuration
Let’s start by adding the necessary dependencies to our project’s pom.xml file:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>1.1.2</version>
</dependency>
We import Spring AI’s MCP server dependency, which provides the necessary classes for creating a custom HTTP-based MCP server.
Next, let’s edit the application.properties file to configure our application as an MCP server:
spring.ai.mcp.server.name=author-server
spring.ai.mcp.server.type=SYNC
spring.ai.mcp.server.protocol=streamable
Here, we configure a name for our MCP server, set it to be synchronous, and specify the transport type as streamable HTTP.
2.2. Defining a Tool
Next, let’s define a tool that our MCP server will expose.
We’ll create an AuthorRepository class that provides a method to fetch author details using an article title. If the requested article is a premium one, we’ll elicit additional information from the user before returning the author details:
private static final Logger log = LoggerFactory.getLogger(AuthorRepository.class);
@McpTool(description = "Get Baeldung author details using an article title")
Author getAuthorByArticleTitle(
@McpToolParam(description = "Title/name of the article") String articleTitle,
@McpToolParam(required = false, description = "Name of user requesting author information") String username,
@McpToolParam(required = false, description = "Reason for requesting author information") String reason,
McpSyncRequestContext requestContext
) {
log.info("Author requested for article: {}", articleTitle);
if (isPremiumArticle(articleTitle)) {
log.info("Article is premium, further information required");
if ((isBlank(username) || isBlank(reason)) && requestContext.elicitEnabled()) {
log.info("Required details missing, initiating elicitation");
StructuredElicitResult<PremiumArticleAccessRequest> elicitResult = requestContext.elicit(
e -> e.message("Baeldung username and reason required."),
PremiumArticleAccessRequest.class
);
if (McpSchema.ElicitResult.Action.ACCEPT.equals(elicitResult.action())) {
username = elicitResult.structuredContent().username();
reason = elicitResult.structuredContent().reason();
log.info("Elicitation accepted - username: {}, reason: {}", username, reason);
}
}
if (isSubscriber(username) && isValidReason(reason)) {
log.info("Access granted, returning author details");
return new Author("John Doe", "[email protected]");
}
}
return null;
}
record Author(String name, String email) {
}
record PremiumArticleAccessRequest(String username, String reason) {
}
We annotate our getAuthorByArticleTitle() method with the @McpTool annotation to expose it as an MCP tool. The method accepts the articleTitle as a required parameter, along with optional username and reason parameters.
Additionally, we inject the McpSyncRequestContext as a method parameter, which provides access to the current request’s metadata and enables us to initiate elicitation requests back to the MCP client.
If the requested article is premium and any of the optional parameters are missing, we use the elicit() method to trigger an elicitation request. We pass a message explaining what information is needed and a schema defining the expected response structure.
We should also note that before initiating this elicitation request, we call the elicitEnabled() method to check whether the connected MCP client supports elicitation, since attempting to elicit from an unsupported client would result in an error.
If the user accepts the elicitation request and provides valid details, we extract the username and reason from the result and proceed with the authorization checks before returning hardcoded author details.
For our demonstration, the isPremiumArticle(), isSubscriber(), and isValidReason() private methods always return true.
3. Creating an MCP Host
Now that we have our MCP server ready, we need an application to consume it.
We’ll be building a chatbot using Anthropic’s Claude model, which will act as our MCP host. Alternatively, we can use a local LLM via Hugging Face or Ollama, as the specific AI model is irrelevant for this demonstration.
We’ll be creating a new Spring Boot application in this section.
3.1. Dependencies and Configuring an LLM
First, let’s include the necessary dependency in our pom.xml file:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
<version>1.1.2</version>
</dependency>
The Anthropic starter dependency is a wrapper around the Anthropic Message API, and we’ll use it to interact with the Claude model in our application.
Additionally, we import the MCP client starter dependency, which will allow us to configure clients inside our Spring Boot application that maintain 1:1 connections with the MCP servers.
Next, let’s configure our Anthropic API key and chat model in the application.properties file:
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
spring.ai.anthropic.chat.options.model=claude-opus-4-5-20251101
We use the ${} property placeholder to load the value of our API Key from an environment variable.
Additionally, we specify Claude Opus 4.5 by Anthropic, using the claude-opus-4-5-20251101 model ID. Feel free to explore and use a different model based on requirements.
With these two properties set, Spring AI automatically creates a bean of type ChatModel, allowing us to interact with the specified model.
3.2. Configuring an MCP Client and Enabling MCP Elicitation
Finally, to use our custom MCP server in our chatbot application, we need to configure an MCP client against it:
spring.ai.mcp.client.capabilities.elicitation={}
spring.ai.mcp.client.streamable-http.connections.author-server.url=http://localhost:8081/mcp
In our application.properties file, we first enable MCP elicitation, which allows the MCP servers to send back an elicitation request if a tool requires additional information.
Next, we configure a new client against our custom MCP server using the streamable HTTP transport type. Our configuration assumes the MCP server to be running at http://localhost:8081/mcp. We need to make sure to update the url property if it’s running on a different host or port.
During application startup, Spring AI will scan our configuration, create the MCP client, and establish a connection with the corresponding MCP server. Additionally, it creates a bean of type SyncMcpToolCallbackProvider, which provides a list of all the tools exposed by the configured MCP servers.
3.3. Building a Basic Chatbot
With our LLM and MCP client configured, let’s build a simple chatbot.
We’ll start by creating a bean of type ChatClient using the auto-configured ChatModel and SyncMcpToolCallbackProvider beans:
@Bean
ChatClient chatClient(ChatModel chatModel, SyncMcpToolCallbackProvider toolCallbackProvider) {
return ChatClient
.builder(chatModel)
.defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
.build();
}
The ChatClient class will act as our main entry point for interacting with our chat completion model, i.e., Claude Opus 4.5.
Next, let’s inject the ChatClient bean in a controller class and expose a REST API:
@PostMapping("/chat")
ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
String answer = chatClient
.prompt()
.user(chatRequest.question())
.call()
.content();
return ResponseEntity.ok(new ChatResponse(answer));
}
record ChatRequest(String question) {
}
record ChatResponse(String answer) {
}
Here, we simply pass the user’s question to the chatClient bean and return the LLM’s response. We’ll use this API endpoint to interact with our chatbot later in the tutorial.
3.4. Handling Elicitation Requests
When the MCP server initiates an elicitation request, the MCP client must have a mechanism to intercept this request and provide the necessary data.
Let’s define a method in our configuration class to handle these requests:
private static final Logger log = LoggerFactory.getLogger(ChatbotConfiguration.class);
@McpElicitation(clients = "author-server")
ElicitResult handleElicitation(ElicitRequest elicitRequest) {
log.info("Elicitation requested: {}", elicitRequest.message());
log.info("Requested schema: {}", elicitRequest.requestedSchema());
return new ElicitResult(
ElicitResult.Action.ACCEPT,
Map.of(
"username", "john.smith",
"reason", "Contacting author for article feedback"
)
);
}
We annotate our handleElicitation() method with @McpElicitation, specifying the clients attribute as author-server to link it to the client we configured earlier in our application.properties file.
When the MCP server triggers an elicitation request, this handler receives the ElicitRequest containing the message and requested schema.
In our handler, we log the elicitation details and return an ElicitResult with the ACCEPT action along with the requested details. For our demonstration, we’re returning hardcoded values. In a production application, the MCP client would typically prompt the user for this information.
It’s worth noting that MCP Elicitation also supports a URL mode, where the server directs users to an external URL to request sensitive details or perform protected actions. This ensures sensitive data never passes through the MCP client. However, at the time of this writing, Spring AI does not support URL mode elicitation.
4. Interacting With Our Chatbot
Now that we’ve built our MCP server and host application, let’s interact with our chatbot and test the elicitation flow.
We’ll use the HTTPie CLI to invoke the chatbot’s API endpoint:
http POST :8080/chat question="Who wrote the article 'Testing CORS in Spring Boot?' on Baeldung, and how can I contact them?"
Here, we send a simple question asking for details regarding the author who wrote a specific article. We deliberately don’t specify the username or reason in our query to trigger the elicitation flow on the MCP server.
Let’s see what we get as a response:
{
"answer": "The article 'Testing CORS in Spring Boot' on Baeldung was written by John Doe. You can contact him via email at [[email protected]](mailto:[email protected])."
}
As we can see, the chatbot successfully returns the author’s details.
Let’s look at the logs of our chatbot to confirm the elicitation request was received:
[2026-01-28 13:16:25] [INFO] [c.b.s.m.c.ChatbotConfiguration] - Elicitation requested: Baeldung username and reason required.
[2026-01-28 13:16:25] [INFO] [c.b.s.m.c.ChatbotConfiguration] - Requested schema: {type=object, properties={reason={type=string}, username={type=string}}, required=[reason, username]}
The logs confirm that our elicitation handler received the request from the MCP server, including the message and the expected schema for the response.
Now, let’s also examine the logs from our MCP server to confirm the complete flow:
[2026-01-28 15:28:00] [INFO] [c.b.s.m.s.AuthorRepository] - Author requested for article: Testing CORS in Spring Boot
[2026-01-28 15:28:00] [INFO] [c.b.s.m.s.AuthorRepository] - Article is premium, further information required
[2026-01-28 15:28:00] [INFO] [c.b.s.m.s.AuthorRepository] - Required details missing, initiating elicitation
[2026-01-28 15:28:00] [INFO] [c.b.s.m.s.AuthorRepository] - Elicitation accepted - username: john.smith, reason: Contacting author for article feedback
[2026-01-28 15:28:00] [INFO] [c.b.s.m.s.AuthorRepository] - Access granted, returning author details
Here, the tool detected that the article is premium, initiated an elicitation request to gather the missing parameters, received the hardcoded values from our elicitation handler, and finally executed the tool logic to return the author details.
5. Conclusion
In this article, we explored how to implement MCP Elicitations with Spring AI.
We built an MCP server exposing a tool that requests additional information when accessing premium content. Then, we created an MCP host application with an elicitation handler that responds to these requests.
Finally, we tested our implementation and verified the elicitation flow through the application logs. This pattern enables more interactive AI applications where tools can gather additional context from the user when needed.
As always, all the code examples used in this article are available over on GitHub.
















