Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spring-ai-modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<module>spring-ai-chat-stream</module>
<module>spring-ai-introduction</module>
<module>spring-ai-mcp</module>
<module>spring-ai-mcp-elicitations</module>
<!-- <module>spring-ai-mcp-oauth</module>--><!-- test failures -->
<module>spring-ai-multiple-llms</module>
<module>spring-ai-semantic-caching</module>
Expand Down
83 changes: 83 additions & 0 deletions spring-ai-modules/spring-ai-mcp-elicitations/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>spring-ai-modules</artifactId>
<version>0.0.1</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>spring-ai-mcp-elicitations</artifactId>
<version>0.0.1</version>
<name>spring-ai-mcp-elicitations</name>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>

<properties>
<java.version>21</java.version>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>

<profiles>
<profile>
<id>mcp-server</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring.boot.mainclass>com.baeldung.springai.mcp.server.ServerApplication</spring.boot.mainclass>
</properties>
</profile>
<profile>
<id>mcp-client</id>
<properties>
<spring.boot.mainclass>com.baeldung.springai.mcp.client.ClientApplication</spring.boot.mainclass>
</properties>
</profile>
</profiles>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>${spring.boot.mainclass}</mainClass>
</configuration>
</plugin>
</plugins>
</build>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.baeldung.springai.mcp.client;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.annotation.McpElicitation;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
import static io.modelcontextprotocol.spec.McpSchema.ElicitResult;

@Configuration
class ChatbotConfiguration {

private static final Logger log = LoggerFactory.getLogger(ChatbotConfiguration.class);

@Bean
ChatClient chatClient(ChatModel chatModel, SyncMcpToolCallbackProvider toolCallbackProvider) {
return ChatClient
.builder(chatModel)
.defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
.build();
}

@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"
)
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.baeldung.springai.mcp.client;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ChatbotController {

private final ChatClient chatClient;

ChatbotController(ChatClient chatClient) {
this.chatClient = chatClient;
}

@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) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.baeldung.springai.mcp.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@PropertySource("classpath:application-mcp-client.properties")
class ClientApplication {

public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.baeldung.springai.mcp.server;

record Author(String name, String email) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.baeldung.springai.mcp.server;

import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.annotation.McpToolParam;
import org.springaicommunity.mcp.context.McpSyncRequestContext;
import org.springaicommunity.mcp.context.StructuredElicitResult;
import org.springframework.stereotype.Component;

@Component
class AuthorRepository {

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", "john.doe@baeldung.com");
}
}
return null;
}

private boolean isPremiumArticle(String articleTitle) {
return true;
}

private boolean isSubscriber(String username) {
return true;
}

private boolean isValidReason(String reason) {
return true;
}

private boolean isBlank(String value) {
return value == null || value.isBlank();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.baeldung.springai.mcp.server;

record PremiumArticleAccessRequest(String username, String reason) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.baeldung.springai.mcp.server;

import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;

/**
* Excluding the below auto-configuration to avoid start up
* failure. Its corresponding starter is present on the classpath but is
* only needed by the MCP client application.
*/
@SpringBootApplication(exclude = {
AnthropicChatAutoConfiguration.class
})
@PropertySource("classpath:application-mcp-server.properties")
class ServerApplication {

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
spring.ai.anthropic.chat.options.model=claude-opus-4-5-20251101

spring.ai.mcp.client.capabilities.elicitation={}
spring.ai.mcp.client.streamable-http.connections.author-server.url=http://localhost:8081/mcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
spring.ai.mcp.server.name=author-server
spring.ai.mcp.server.type=SYNC
spring.ai.mcp.server.protocol=streamable
server.port=8081
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%p] [%c{1}] - %m%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>

<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="CONSOLE" />
</logger>
</configuration>