From ca14dc65f1481357ad2c0dc6d8d032e77bf78323 Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Mon, 18 May 2026 20:26:55 +0300 Subject: [PATCH 1/5] BAEL-9671: upgrade spring ai --- spring-ai-modules/spring-ai-mcp/pom.xml | 8 +++++++- .../springai/mcp/client/ChatbotConfiguration.java | 14 +++++++------- .../springai/mcp/test/ExchangeRateMcpTool.java | 4 ++-- .../ExchangeRateMcpToolSseIntegrationTest.java | 2 +- ...changeRateMcpToolStreamableIntegrationTest.java | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/spring-ai-modules/spring-ai-mcp/pom.xml b/spring-ai-modules/spring-ai-mcp/pom.xml index 01ad18dc2a94..104af5ee72c4 100644 --- a/spring-ai-modules/spring-ai-mcp/pom.xml +++ b/spring-ai-modules/spring-ai-mcp/pom.xml @@ -44,7 +44,7 @@ 21 - 1.1.2 + 2.0.0-M6 @@ -63,6 +63,12 @@ com.baeldung.springai.mcp.client.ClientApplication + + mcp-UI + + com.baeldung.springai.mcp.ui.McpApplication + + diff --git a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/client/ChatbotConfiguration.java b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/client/ChatbotConfiguration.java index eeca5372b118..b049405f9c86 100644 --- a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/client/ChatbotConfiguration.java +++ b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/client/ChatbotConfiguration.java @@ -1,12 +1,14 @@ package com.baeldung.springai.mcp.client; +import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.spec.McpSchema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; -import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; +import org.springframework.ai.mcp.customizer.McpClientCustomizer; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -24,12 +26,10 @@ ChatClient chatClient(ChatModel chatModel, SyncMcpToolCallbackProvider toolCallb } @Bean - McpSyncClientCustomizer mcpSyncClientCustomizer() { - return (name, mcpClientSpec) -> { - mcpClientSpec.toolsChangeConsumer(tools -> { - logger.info("Detected tools changes."); - }); - }; + McpClientCustomizer mcpSyncClientCustomizer() { + return (name, mcpClientSpec) -> + mcpClientSpec.toolsChangeConsumer( + tools -> logger.info("Detected tools changes.")); } } \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/test/ExchangeRateMcpTool.java b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/test/ExchangeRateMcpTool.java index 966e6528b125..9fbf421e1dd3 100644 --- a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/test/ExchangeRateMcpTool.java +++ b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/test/ExchangeRateMcpTool.java @@ -1,7 +1,7 @@ package com.baeldung.springai.mcp.test; -import org.springaicommunity.mcp.annotation.McpTool; -import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springframework.ai.mcp.annotation.McpTool; +import org.springframework.ai.mcp.annotation.McpToolParam; import org.springframework.stereotype.Component; @Component diff --git a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java index 0efbd458fea4..0fa9806f608c 100644 --- a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java +++ b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java @@ -62,7 +62,7 @@ void whenMcpClientCallTool_thenTheToolReturnsMockedResponse() { .findFirst() .orElseThrow(); - String argumentName = exchangeRateTool.inputSchema().properties().keySet().stream() + String argumentName = exchangeRateTool.inputSchema().keySet().stream() .findFirst() .orElseThrow(); diff --git a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java index 657c17b06fe8..9200f5565783 100644 --- a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java +++ b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java @@ -64,7 +64,7 @@ void whenMcpClientCallTool_thenTheToolReturnsMockedResponse() { .findFirst() .orElseThrow(); - String argumentName = exchangeRateTool.inputSchema().properties().keySet().stream() + String argumentName = exchangeRateTool.inputSchema().keySet().stream() .findFirst() .orElseThrow(); From 0ec84579382fbd2f75b1e32d3bef3e4ecc01284e Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Mon, 18 May 2026 20:26:55 +0300 Subject: [PATCH 2/5] BAEL-9671: upgrade spring ai --- spring-ai-modules/spring-ai-mcp/pom.xml | 28 +++++++++++++++++-- ...ExchangeRateMcpToolSseIntegrationTest.java | 4 +-- ...eRateMcpToolStreamableIntegrationTest.java | 20 ++++++------- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/spring-ai-modules/spring-ai-mcp/pom.xml b/spring-ai-modules/spring-ai-mcp/pom.xml index 104af5ee72c4..1958ef363cf3 100644 --- a/spring-ai-modules/spring-ai-mcp/pom.xml +++ b/spring-ai-modules/spring-ai-mcp/pom.xml @@ -44,7 +44,12 @@ 21 + 4.0.0 + 1.5.18 + 5.13.0 2.0.0-M6 + 3.1.2 + 2.21 @@ -64,9 +69,9 @@ - mcp-UI + mcp-ui - com.baeldung.springai.mcp.ui.McpApplication + com.baeldung.springai.mcp.ui.McpUiApplication @@ -85,6 +90,25 @@ + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson-annotations.version} + + + tools.jackson + jackson-bom + ${jackson-bom.version} + pom + import + org.springframework.ai spring-ai-bom diff --git a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java index 0fa9806f608c..f4e245b575e6 100644 --- a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java +++ b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolSseIntegrationTest.java @@ -14,8 +14,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.bean.override.mockito.MockitoBean; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT @@ -28,7 +28,7 @@ class ExchangeRateMcpToolSseIntegrationTest { @Autowired private TestMcpClientFactory testMcpClientFactory; - @MockBean + @MockitoBean private ExchangeRateService exchangeRateService; private McpSyncClient client; diff --git a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java index 9200f5565783..77058d29c978 100644 --- a/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java +++ b/spring-ai-modules/spring-ai-mcp/src/test/java/com/baeldung/springai/mcp/test/ExchangeRateMcpToolStreamableIntegrationTest.java @@ -1,22 +1,22 @@ package com.baeldung.springai.mcp.test; -import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.spec.McpSchema; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Objects; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.bean.override.mockito.MockitoBean; -import java.util.Map; -import java.util.Objects; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @@ -27,7 +27,7 @@ class ExchangeRateMcpToolStreamableIntegrationTest { @LocalServerPort private int port; - @MockBean + @MockitoBean private ExchangeRateService exchangeRateService; @Autowired From 3dd8724e27efb1cd354105f6347230ee3481c488 Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Fri, 22 May 2026 22:56:47 +0300 Subject: [PATCH 3/5] BAEL-9671: embedded html with spring ai --- .../spring-ai-mcp/.claude/settings.json | 8 + .../springai/mcp/ui/McpUiApplication.java | 30 +++ .../springai/mcp/ui/SportSpinnerUI.java | 57 +++++ .../resources/application-mcp-ui.properties | 3 + .../main/resources/static/sport-spinner.html | 228 ++++++++++++++++++ 5 files changed, 326 insertions(+) create mode 100644 spring-ai-modules/spring-ai-mcp/.claude/settings.json create mode 100644 spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/McpUiApplication.java create mode 100644 spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java create mode 100644 spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties create mode 100644 spring-ai-modules/spring-ai-mcp/src/main/resources/static/sport-spinner.html diff --git a/spring-ai-modules/spring-ai-mcp/.claude/settings.json b/spring-ai-modules/spring-ai-mcp/.claude/settings.json new file mode 100644 index 000000000000..e82b4e15c769 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp/.claude/settings.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "productivity-spinner": { + "type": "http", + "url": "http://localhost:3001/mcp" + } + } +} diff --git a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/McpUiApplication.java b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/McpUiApplication.java new file mode 100644 index 000000000000..06504c43b9d4 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/McpUiApplication.java @@ -0,0 +1,30 @@ +package com.baeldung.springai.mcp.ui; + +import org.springframework.ai.mcp.annotation.McpTool; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +@SpringBootApplication +class McpUiApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder() + .sources(McpUiApplication.class) + .profiles("mcp-ui") + .run(args); + } + + @McpTool( + title = "Say Hello", + name = "say-hello", + description = "A simple tool that returns a greeting message." + ) + String sayHello() { + return "Hello from the MCP UI Application!"; + } + + /* + npx @modelcontextprotocol/inspector + claude mcp add --transport http sport-spinner http://localhost:3001/mcp + */ +} diff --git a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java new file mode 100644 index 000000000000..b2cd1dd01efc --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java @@ -0,0 +1,57 @@ +package com.baeldung.springai.mcp.ui; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; + +import org.springframework.ai.mcp.annotation.McpResource; +import org.springframework.ai.mcp.annotation.McpTool; +import org.springframework.ai.mcp.annotation.context.MetaProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +@Component +class SportSpinnerUI { + + @Value("classpath:/static/sport-spinner.html") + private Resource sportSpinnerResource; + + @McpTool( + title = "Spin Sport Wheel", + name = "spin-sport-wheel", + description = "Opens a fortune wheel that spins and randomly picks today's sport from 12 slices: swim, bike, run, bouldering, tennis, and a special triathlon (each appearing twice).", + metaProvider = SportSpinnerToolMetaProvider.class) + public String spinSportWheel() { + return "Opening the sport spinner wheel."; + } + + @McpResource( + name = "Sport Spinner App Resource", + uri = "ui://sport/sport-spinner.html", + mimeType = "text/html;profile=mcp-app", + metaProvider = CspMetaProvider.class) + public String getSportSpinnerResource() throws IOException { + return sportSpinnerResource.getContentAsString(Charset.defaultCharset()); + } + + public static final class SportSpinnerToolMetaProvider implements MetaProvider { + @Override + public Map getMeta() { + return Map.of("ui", + Map.of("resourceUri", "ui://sport/sport-spinner.html")); + } + } + + public static final class CspMetaProvider implements MetaProvider { + @Override + public Map getMeta() { + return Map.of("ui", + Map.of("csp", + Map.of("resourceDomains", + List.of("https://unpkg.com")))); + } + } + +} diff --git a/spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties b/spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties new file mode 100644 index 000000000000..8502222f1028 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties @@ -0,0 +1,3 @@ + +server.port=3001 +spring.ai.mcp.server.protocol=streamable diff --git a/spring-ai-modules/spring-ai-mcp/src/main/resources/static/sport-spinner.html b/spring-ai-modules/spring-ai-mcp/src/main/resources/static/sport-spinner.html new file mode 100644 index 000000000000..345715d29d5c --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp/src/main/resources/static/sport-spinner.html @@ -0,0 +1,228 @@ + + + + + Sport Spinner + + + + +

🎡 Sport Spinner

+

Spin the wheel to pick today's sport

+ +
+
+ +
+
+ +
Ready to spin!
+ + + + + + From a459b67499914be7394df4c03e3d427569f40fea Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Wed, 27 May 2026 18:15:45 +0300 Subject: [PATCH 4/5] BAEL-9671: fix build --- spring-ai-modules/spring-ai-mcp/pom.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/spring-ai-modules/spring-ai-mcp/pom.xml b/spring-ai-modules/spring-ai-mcp/pom.xml index 1958ef363cf3..a2be3919aea7 100644 --- a/spring-ai-modules/spring-ai-mcp/pom.xml +++ b/spring-ai-modules/spring-ai-mcp/pom.xml @@ -40,13 +40,18 @@ spring-boot-starter-test test
+ + org.junit.platform + junit-platform-launcher + test +
21 4.0.0 1.5.18 - 5.13.0 + 6.0.1 2.0.0-M6 3.1.2 2.21 @@ -85,6 +90,11 @@ ${spring.boot.mainclass} + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 +
From ba55f0d241a80a85477abdc4003560ba75152167 Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Wed, 27 May 2026 18:32:24 +0300 Subject: [PATCH 5/5] BAEL-9671: code review --- spring-ai-modules/spring-ai-mcp/.claude/settings.json | 8 -------- .../java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java | 3 ++- .../src/main/resources/application-mcp-ui.properties | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 spring-ai-modules/spring-ai-mcp/.claude/settings.json diff --git a/spring-ai-modules/spring-ai-mcp/.claude/settings.json b/spring-ai-modules/spring-ai-mcp/.claude/settings.json deleted file mode 100644 index e82b4e15c769..000000000000 --- a/spring-ai-modules/spring-ai-mcp/.claude/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "productivity-spinner": { - "type": "http", - "url": "http://localhost:3001/mcp" - } - } -} diff --git a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java index b2cd1dd01efc..73017d9cf74d 100644 --- a/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java +++ b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -33,7 +34,7 @@ public String spinSportWheel() { mimeType = "text/html;profile=mcp-app", metaProvider = CspMetaProvider.class) public String getSportSpinnerResource() throws IOException { - return sportSpinnerResource.getContentAsString(Charset.defaultCharset()); + return sportSpinnerResource.getContentAsString(StandardCharsets.UTF_8); } public static final class SportSpinnerToolMetaProvider implements MetaProvider { diff --git a/spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties b/spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties index 8502222f1028..25f4e9a45269 100644 --- a/spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties +++ b/spring-ai-modules/spring-ai-mcp/src/main/resources/application-mcp-ui.properties @@ -1,3 +1,3 @@ server.port=3001 -spring.ai.mcp.server.protocol=streamable +spring.ai.mcp.server.protocol=STREAMABLE