Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Maven build output
target/
.vscode/
7 changes: 4 additions & 3 deletions springboot-modules/pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.kodesastra</groupId>
Expand Down Expand Up @@ -34,6 +34,7 @@
<modules>
<module>spring-ai-qdrant</module>
<module>spring-ai</module>
<module>spring-ai-mcp</module>
</modules>

<build>
Expand All @@ -58,4 +59,4 @@
<spring-ai.version>1.0.0-M8</spring-ai.version>
</properties>

</project>
</project>
32 changes: 32 additions & 0 deletions springboot-modules/spring-ai-mcp/mcp-client/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.kodesastra</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<groupId>com.kodesastra</groupId>
<artifactId>mcp-client</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>
<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-model-azure-openai</artifactId>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ks.mcp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class McpSpringClientApplication {

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.ks.mcp.config;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;

import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema.Tool;

@Configuration
public class McpClientConfiguration {
final Logger logger = LoggerFactory.getLogger(McpClientConfiguration.class);

@Value("${spring.ai.azure.openai.api-key}")
private String apiKey;
@Value("${spring.ai.azure.openai.endpoint}")
private String endpoint;

@Value("${spring.ai.azure.openai.chat.options.deployment-name}")
private String deploymentName;
@Value("${spring.ai.azure.openai.chat.options.temperature}")
private String temperature;

@Autowired
private ToolCallbackProvider toolCallbackProvider;

private Set<String> getTools(List<McpSyncClient> mcpSyncClients) {
final String MONITORING_TOOL_NAME = "event-resolver-client - event-resolver-mcp";

return mcpSyncClients.stream()
.filter(e -> e.getClientInfo().name().equalsIgnoreCase(MONITORING_TOOL_NAME))
.map(e -> e.listTools().tools())
.flatMap(List::stream)
.map(Tool::name)
.collect(Collectors.toSet());
}

@Bean
public ChatModel chatClient(@Autowired List<McpSyncClient> mcpSyncClients) {
AzureOpenAiChatOptions azureOpenAiChatOptions = AzureOpenAiChatOptions.builder()
.deploymentName(deploymentName)
.toolNames(getTools(mcpSyncClients))
.toolCallbacks(toolCallbackProvider.getToolCallbacks())
.temperature(Double.parseDouble(temperature))
.build();

OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
.credential(new AzureKeyCredential(apiKey))
.endpoint(endpoint);

AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.defaultOptions(azureOpenAiChatOptions)
.build();

return chatModel;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
spring:
ai:
azure:
openai:
api-key: FSP-xxxx
endpoint: https://xxxxx-eastus2.cognitiveservices.azure.com/
chat:
options:
deployment-name: gpt-4.1
temperature: 1

mcp:
client:
name: event-resolver-client
version: 1.0.0
enabled: true
type: SYNC
toolcallback:
enabled: true
sse:
connections:
event-resolver-mcp:
url: http://localhost:8080
sse-endpoint: /mcp/sse
sse-message-endpoint: /mcp/messages

logging:
level:
root: INFO
com.ks.mcp.service: DEBUG
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.ks.mcp;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest

class McpToolsIntegrationTest {

final Logger logger = LoggerFactory.getLogger(McpToolsIntegrationTest.class);

@Autowired
private ChatModel chatClient;

@Test
void whenNginxServiceDownThenRestartService() {
String action = """
Event:
Nginx service on node 'staging-api-01' is unresponsive.

Action:
1. Check if the node is reachable
2. If the node is in Available state Restart the Nginx service on the node
3. Send a notification with the final status of the node to node operators

Constraint:
Restart the service not more than two times.
""";
String systemMessageText = """
**MANDATORY PROTOCOL:**
1. NO recommendations, analysis, or speculation
2. Strictly use the tools available
3. Respond with EXACT tool output + status summary
""";

Message systemMessage = new SystemMessage(systemMessageText);
Message userMessage = new UserMessage(action);

String response = chatClient.call(systemMessage, userMessage);

logger.info("Restart Node request Response: {}", response);

assertThat(response)
.contains("staging-api-01")
.contains("restart");
}

}
31 changes: 31 additions & 0 deletions springboot-modules/spring-ai-mcp/mcp-server/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.kodesastra</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<groupId>com.kodesastra</groupId>
<artifactId>mcp-server</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ks.mcp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class NodeManagementMcpServerApp {

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.ks.mcp.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class NodeManagementService {

final Logger logger = LoggerFactory.getLogger(NodeManagementService.class);

public String getNodeDetails(String nodeId) {
logger.info("getNodeDetails called for nodeId: {}", nodeId);
return String.format("""
Node Details for %s:
- Status: Available
- CPU Usage: 45%%
- Memory: 2.3GB/8GB
- Uptime: 23h 45m
- Last Check: 2026-01-07T20:29:00Z
""", nodeId);
}

public String performBasicCheck(String nodeId, String checkType) {
logger.info("performBasicCheck called for nodeId: {}", nodeId);
return String.format("""
Basic Check Results for %s:
- Ping: SUCCESS (12ms)
- SSH Port 22: OPEN
- Disk Space: 78%% used
- Overall: HEALTHY
""", nodeId);
}

public String fetchServiceStatus(String nodeId, String serviceName) {
logger.info("fetchServiceStatus called for nodeId: {} and serviceName: {}", nodeId, serviceName);
return String.format("Service %s status: STOPPED", serviceName);
}

public String restartNodeService(String nodeId, String serviceName) {
logger.info("restartNodeService called for nodeId: {} and serviceName: {}", nodeId, serviceName);

return String.format("""
Service Restart Initiated:
- Node: %s
- Service: %s
- Action: RESTART
- Status: SUCCESSFULL
""", nodeId, serviceName);
}

public String sendNotification(String nodeId, String message, String priority) {
logger.info("sendNotification called for nodeId: {} and message: {}", nodeId, message);
String level = priority != null ? priority : "MEDIUM";
return String.format("""
Notification Sent:
- Node: %s
- Priority: %s
- Message: %s
- Channels: Slack, Email, PagerDuty
- Status: DELIVERED
""", nodeId, level, message);
}
}
Loading
Loading