From 564523a19981210dd61543fe94476cd197a89e9e Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 27 Aug 2026 08:27:46 -0400 Subject: [PATCH] BAEL-9710 Add sample MCP server and client with unit tests for MCP logging --- .../spring-ai-mcp-logging/pom.xml | 58 ++++++++++++++++++ .../client/McpLoggingClientApplication.java | 12 ++++ .../PasswordStrengthMcpClientHandlers.java | 33 +++++++++++ .../client/PasswordStrengthToolClient.java | 30 ++++++++++ .../server/McpLoggingServerApplication.java | 12 ++++ .../server/PasswordStrengthResult.java | 6 ++ .../server/PasswordStrengthService.java | 59 +++++++++++++++++++ .../resources/application-client.properties | 17 ++++++ .../resources/application-server.properties | 15 +++++ .../src/main/resources/application.properties | 1 + ...wordStrengthMcpClientHandlersUnitTest.java | 33 +++++++++++ .../PasswordStrengthServiceUnitTest.java | 57 ++++++++++++++++++ 12 files changed, 333 insertions(+) create mode 100644 spring-ai-modules/spring-ai-mcp-logging/pom.xml create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/McpLoggingClientApplication.java create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlers.java create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthToolClient.java create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/McpLoggingServerApplication.java create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthResult.java create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthService.java create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-client.properties create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-server.properties create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application.properties create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlersUnitTest.java create mode 100644 spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/server/PasswordStrengthServiceUnitTest.java diff --git a/spring-ai-modules/spring-ai-mcp-logging/pom.xml b/spring-ai-modules/spring-ai-mcp-logging/pom.xml new file mode 100644 index 000000000000..c6587f148da3 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + com.baeldung + spring-ai-modules + 0.0.1 + ../pom.xml + + + spring-ai-mcp-logging + 0.0.1 + spring-ai-mcp-logging + + + + org.springframework.ai + spring-ai-starter-mcp-client + + + org.springframework.ai + spring-ai-starter-mcp-server + + + + + 21 + 1.1.2 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/McpLoggingClientApplication.java b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/McpLoggingClientApplication.java new file mode 100644 index 000000000000..e63fb6a799d5 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/McpLoggingClientApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.mcp.logging.client; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class McpLoggingClientApplication { + + public static void main(String[] args) { + SpringApplication.run(McpLoggingClientApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlers.java b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlers.java new file mode 100644 index 000000000000..5c65c76aec6b --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlers.java @@ -0,0 +1,33 @@ +package com.baeldung.mcp.logging.client; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.annotation.McpLogging; +import org.springframework.stereotype.Component; + +import io.modelcontextprotocol.spec.McpSchema; + +@Component +public class PasswordStrengthMcpClientHandlers { + + private static final Logger LOGGER = LoggerFactory.getLogger(PasswordStrengthMcpClientHandlers.class); + + private final List receivedLogs = new CopyOnWriteArrayList<>(); + + @McpLogging(clients = "password-strength-logging-server") + public void handleLoggingMessage(McpSchema.LoggingMessageNotification notification) { + LOGGER.info("Received server logging notification [{}]: {}", notification.level(), notification.data()); + receivedLogs.add(notification); + } + + public List getReceivedLogs() { + return receivedLogs; + } + + public void reset() { + receivedLogs.clear(); + } +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthToolClient.java b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthToolClient.java new file mode 100644 index 000000000000..6c250c0333a6 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/client/PasswordStrengthToolClient.java @@ -0,0 +1,30 @@ +package com.baeldung.mcp.logging.client; + +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; + +@Service +public class PasswordStrengthToolClient { + + public static final String CHECK_PASSWORD_STRENGTH_TOOL = "check_password_strength"; + + private final McpSyncClient mcpSyncClient; + + public PasswordStrengthToolClient(List mcpSyncClients) { + if (mcpSyncClients.isEmpty()) { + throw new IllegalStateException("No McpSyncClient beans were configured"); + } + this.mcpSyncClient = mcpSyncClients.get(0); + } + + public McpSchema.CallToolResult checkPasswordStrength(String password) { + McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(CHECK_PASSWORD_STRENGTH_TOOL, + Map.of("password", password)); + return mcpSyncClient.callTool(request); + } +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/McpLoggingServerApplication.java b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/McpLoggingServerApplication.java new file mode 100644 index 000000000000..83c8a28b1bb3 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/McpLoggingServerApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.mcp.logging.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class McpLoggingServerApplication { + + public static void main(String[] args) { + SpringApplication.run(McpLoggingServerApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthResult.java b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthResult.java new file mode 100644 index 000000000000..6162361be9de --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthResult.java @@ -0,0 +1,6 @@ +package com.baeldung.mcp.logging.server; + +import java.util.List; + +public record PasswordStrengthResult(int score, List issues) { +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthService.java b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthService.java new file mode 100644 index 000000000000..fa61a21c9ee4 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/java/com/baeldung/mcp/logging/server/PasswordStrengthService.java @@ -0,0 +1,59 @@ +package com.baeldung.mcp.logging.server; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.springaicommunity.mcp.context.McpSyncRequestContext; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springframework.stereotype.Service; + +@Service +public class PasswordStrengthService { + + private static final int MIN_RECOMMENDED_LENGTH = 12; + + private static final Set COMMON_PASSWORDS = Set.of("password", "123456", "qwerty", "letmein", "password1", + "admin"); + + @McpTool(name = "check_password_strength", description = "Evaluates password strength and returns a score with recommendations.") + public PasswordStrengthResult checkStrength( + @McpToolParam(description = "The password to evaluate", required = true) String password, + McpSyncRequestContext ctx) { + ctx.debug("Evaluating password of length " + password.length()); + + List issues = new ArrayList<>(); + + if (password.length() < MIN_RECOMMENDED_LENGTH) { + ctx.warn("Password shorter than recommended " + MIN_RECOMMENDED_LENGTH + " characters"); + issues.add("too short"); + } else { + ctx.debug("Length check passed"); + } + + if (!password.matches(".*[A-Z].*")) { + ctx.warn("Password missing uppercase letters"); + issues.add("no uppercase"); + } else { + ctx.debug("Uppercase check passed"); + } + + if (!password.matches(".*[0-9].*")) { + ctx.warn("Password missing digits"); + issues.add("no digits"); + } else { + ctx.debug("Digit check passed"); + } + + if (COMMON_PASSWORDS.contains(password.toLowerCase())) { + ctx.error("Password found in common-password list"); + issues.add("commonly used"); + } + + int score = Math.max(0, 100 - issues.size() * 25); + ctx.info("Final score: " + score); + + return new PasswordStrengthResult(score, issues); + } +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-client.properties b/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-client.properties new file mode 100644 index 000000000000..fd1b33195d06 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-client.properties @@ -0,0 +1,17 @@ +spring.application.name=mcp-logging-demo-client + +spring.main.web-application-type=none + +spring.main.banner-mode=off + +logging.file.name=target/mcp-logging-demo-client.log + +spring.ai.mcp.client.type=SYNC + +spring.autoconfigure.exclude=org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration + +spring.ai.mcp.client.stdio.connections.password-strength-logging-server.command=java + +spring.ai.mcp.client.stdio.connections.password-strength-logging-server.args=-Dloader.main=com.baeldung.mcp.logging.server.McpLoggingServerApplication,-jar,${MCP_JAR:target/spring-ai-mcp-logging.jar},--spring.profiles.active=server + +spring.autoconfigure.exclude=org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-server.properties b/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-server.properties new file mode 100644 index 000000000000..14b0330038f3 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application-server.properties @@ -0,0 +1,15 @@ +spring.application.name=mcp-logging-demo-server + +spring.ai.mcp.server.stdio=true + +spring.ai.mcp.server.name=password-strength-logging-server + +spring.ai.mcp.server.version=1.0.0 + +spring.main.web-application-type=none + +spring.main.banner-mode=off + +logging.pattern.console= + +logging.file.name=target/mcp-logging-demo-server.log diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application.properties b/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application.properties new file mode 100644 index 000000000000..4b49e8e8a2cd --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/main/resources/application.properties @@ -0,0 +1 @@ +logging.level.root=INFO \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlersUnitTest.java b/spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlersUnitTest.java new file mode 100644 index 000000000000..ac6c4fd0ed08 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/client/PasswordStrengthMcpClientHandlersUnitTest.java @@ -0,0 +1,33 @@ +package com.baeldung.mcp.logging.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.modelcontextprotocol.spec.McpSchema; + +class PasswordStrengthMcpClientHandlersUnitTest { + + private final PasswordStrengthMcpClientHandlers handlers = new PasswordStrengthMcpClientHandlers(); + + @Test + void whenLoggingMessageReceived_thenStoredInReceivedLogs() { + McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.WARNING).logger("password-strength-logging-server") + .data("Password shorter than recommended 12 characters").build(); + + handlers.handleLoggingMessage(notification); + + assertThat(handlers.getReceivedLogs()).containsExactly(notification); + } + + @Test + void whenLoggingMessageCleared_thenReceivedLogsEmpty() { + handlers.handleLoggingMessage(McpSchema.LoggingMessageNotification.builder().level(McpSchema.LoggingLevel.INFO) + .logger("password-strength-logging-server").data("Final score: 25").build()); + + handlers.reset(); + + assertThat(handlers.getReceivedLogs()).isEmpty(); + } +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/server/PasswordStrengthServiceUnitTest.java b/spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/server/PasswordStrengthServiceUnitTest.java new file mode 100644 index 000000000000..1db361e72119 --- /dev/null +++ b/spring-ai-modules/spring-ai-mcp-logging/src/test/java/com/baeldung/mcp/logging/server/PasswordStrengthServiceUnitTest.java @@ -0,0 +1,57 @@ +package com.baeldung.mcp.logging.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; + +import org.springaicommunity.mcp.context.McpSyncRequestContext; + +class PasswordStrengthServiceUnitTest { + + private final PasswordStrengthService passwordStrengthService = new PasswordStrengthService(); + + @Test + void whenPasswordIsStrong_thenScoreIsPerfectAndNoIssuesReported() { + McpSyncRequestContext ctx = mock(McpSyncRequestContext.class); + + PasswordStrengthResult result = passwordStrengthService.checkStrength("Tr0ubad0urCastle!", ctx); + + assertThat(result.score()).isEqualTo(100); + assertThat(result.issues()).isEmpty(); + } + + @Test + void whenPasswordIsShortWithNoUppercaseOrDigits_thenAllThreeIssuesAreReported() { + McpSyncRequestContext ctx = mock(McpSyncRequestContext.class); + + PasswordStrengthResult result = passwordStrengthService.checkStrength("lowercase", ctx); + + assertThat(result.issues()).containsExactlyInAnyOrder("too short", "no uppercase", "no digits"); + assertThat(result.score()).isEqualTo(25); + } + + @Test + void whenPasswordIsCommonlyUsed_thenErrorLevelNotificationIsSent() { + McpSyncRequestContext ctx = mock(McpSyncRequestContext.class); + + PasswordStrengthResult result = passwordStrengthService.checkStrength("password1", ctx); + + assertThat(result.issues()).contains("commonly used"); + verify(ctx).error("Password found in common-password list"); + } + + @Test + void whenCheckStrengthIsCalled_thenNotificationsAreSentAtLevelsMatchingEachFinding() { + McpSyncRequestContext ctx = mock(McpSyncRequestContext.class); + + passwordStrengthService.checkStrength("weak", ctx); + + verify(ctx).debug("Evaluating password of length 4"); + verify(ctx).warn("Password shorter than recommended 12 characters"); + verify(ctx).warn("Password missing uppercase letters"); + verify(ctx).warn("Password missing digits"); + verify(ctx).info("Final score: 25"); + } +} \ No newline at end of file