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
58 changes: 58 additions & 0 deletions spring-ai-modules/spring-ai-mcp-logging/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?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-logging</artifactId>
<version>0.0.1</version>
<name>spring-ai-mcp-logging</name>

<dependencies>
<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</artifactId>
</dependency>
</dependencies>

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

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</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,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);
}
}
Original file line number Diff line number Diff line change
@@ -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<McpSchema.LoggingMessageNotification> 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<McpSchema.LoggingMessageNotification> getReceivedLogs() {
return receivedLogs;
}

public void reset() {
receivedLogs.clear();
}
}
Original file line number Diff line number Diff line change
@@ -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<McpSyncClient> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.baeldung.mcp.logging.server;

import java.util.List;

public record PasswordStrengthResult(int score, List<String> issues) {
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
logging.level.root=INFO
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}