diff --git a/spring-ai-modules/spring-ai-mcp/pom.xml b/spring-ai-modules/spring-ai-mcp/pom.xml index 01ad18dc2a94..a2be3919aea7 100644 --- a/spring-ai-modules/spring-ai-mcp/pom.xml +++ b/spring-ai-modules/spring-ai-mcp/pom.xml @@ -40,11 +40,21 @@ spring-boot-starter-test test + + org.junit.platform + junit-platform-launcher + test + 21 - 1.1.2 + 4.0.0 + 1.5.18 + 6.0.1 + 2.0.0-M6 + 3.1.2 + 2.21 @@ -63,6 +73,12 @@ com.baeldung.springai.mcp.client.ClientApplication + + mcp-ui + + com.baeldung.springai.mcp.ui.McpUiApplication + + @@ -74,11 +90,35 @@ ${spring.boot.mainclass} + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + 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/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/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..73017d9cf74d --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp/src/main/java/com/baeldung/springai/mcp/ui/SportSpinnerUI.java @@ -0,0 +1,58 @@ +package com.baeldung.springai.mcp.ui; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +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(StandardCharsets.UTF_8); + } + + 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..25f4e9a45269 --- /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!
+ + + + + + 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..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; @@ -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..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 @@ -64,7 +64,7 @@ void whenMcpClientCallTool_thenTheToolReturnsMockedResponse() { .findFirst() .orElseThrow(); - String argumentName = exchangeRateTool.inputSchema().properties().keySet().stream() + String argumentName = exchangeRateTool.inputSchema().keySet().stream() .findFirst() .orElseThrow();