Loading pom.xml +24 −2 Original line number Diff line number Diff line Loading @@ -23,9 +23,9 @@ </organization> <properties> <spring.boot-version>3.5.5</spring.boot-version> <spring.boot-version>4.0.0</spring.boot-version> <java.version>17</java.version> <spring-ai.version>1.1.0</spring-ai.version> <spring-ai.version>2.0.0-M2</spring-ai.version> <camel.version>4.0.0-RC2</camel.version> </properties> Loading @@ -34,6 +34,19 @@ <id>gitlab-maven</id> <url>https://labs.etsi.org/rep/api/v4/groups/260/-/packages/maven</url> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <distributionManagement> <repository> Loading Loading @@ -97,6 +110,11 @@ <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> <dependency> <groupId>org.springaicommunity</groupId> <artifactId>spring-ai-agent-utils</artifactId> <version>0.4.2</version> </dependency> <dependency> <groupId>org.commonmark</groupId> Loading Loading @@ -164,6 +182,10 @@ <groupId>org.apache.camel.springboot</groupId> <artifactId>camel-service-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-retry</artifactId> </dependency> </dependencies> <build> Loading src/main/java/org/etsi/osl/mcp/backend/ChatController.java +35 −38 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package org.etsi.osl.mcp.backend; import java.util.Map; import java.util.UUID; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; Loading @@ -10,7 +11,11 @@ import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.tool.ToolCallbackProvider; import org.springaicommunity.agent.tools.SkillsTool; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; Loading @@ -28,28 +33,40 @@ public class ChatController { @Value("${spring.ai.chat.maxMessages}") private final int maxMessages = 100; ChatMemoryRepository chatMemoryRepository; ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages( maxMessages ) .build(); public ChatController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ToolCallbackProvider mcpTools, @Value("${spring.ai.chat.system-prompt}") String systemPrompt, ChatMemory chatMemory) { @Value("${agent.skills.dirs}") String agentSkillsDir, ChatMemoryRepository chatMemoryRepository) { // Validate that system prompt is not empty if (systemPrompt == null || systemPrompt.trim().isEmpty()) { throw new IllegalArgumentException("System prompt cannot be null or empty. Please configure SPRING_AI_CHAT_SYSTEM_PROMPT with a valid value."); } this.systemPrompt = systemPrompt; ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages(maxMessages) .build(); // Build SkillsTool - build() returns a ToolCallback, not SkillsTool ToolCallback skillsTool = SkillsTool.builder() // FIXED TYPE HERE .addSkillsDirectory(agentSkillsDir) .build(); this.chatClient = chatClientBuilder .defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build()) .defaultToolCallbacks(tools) .defaultAdvisors( new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build() ) .defaultToolCallbacks(mcpTools) .defaultToolCallbacks(skillsTool) // Now this works! .defaultSystem(systemPrompt) .build(); log.info("ChatController initialized with skills from: {}", agentSkillsDir); log.info("ChatController initialized with system prompt: {}", systemPrompt); this.conversationId = UUID.randomUUID().toString(); } Loading @@ -64,12 +81,9 @@ public class ChatController { log.debug("Response from AI: {}", response); String markdownAnswer = formatResponse(question, response); log.debug("Markdown formatted response: {}", markdownAnswer); // var htmlResponse = MarkdownHelper.toHTML(markdownAnswer); // log.debug("HTML formatted response: {}", htmlResponse); return new Answer(markdownAnswer); } public String simpleAsk(Map<String, Object> headers, String question) { var response = chatClient.prompt() .user(question) Loading @@ -82,23 +96,6 @@ public class ChatController { private String formatResponse(Question question, String answer) { return answer; // var prompt = """ // Following are the question and answer: // // Question: {question} // // Answer: {answer} // // Format the answer into plain human readable text and return only the formatted response. // """; // return chatClient // .prompt() // .user(u -> u.text(prompt) // .param("question", question.question()) // .param("answer", answer) // ) // .call() // .content(); } public record Question(String question) {} Loading src/main/java/org/etsi/osl/mcp/backend/configuration/CustomMcpToolFilter.java 0 → 100644 +45 −0 Original line number Diff line number Diff line package org.etsi.osl.mcp.backend.configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.modelcontextprotocol.spec.McpSchema; import org.springframework.ai.mcp.McpToolFilter; import org.springframework.ai.mcp.McpConnectionInfo; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.regex.Pattern; /** * Configurable MCP Tool Filter * * Filters MCP tools based on a configurable regex pattern. * Only tools matching the pattern will be available to the chat client. * * @author OpenSlice Team */ @Component public class CustomMcpToolFilter implements McpToolFilter { private static final Logger log = LoggerFactory.getLogger(CustomMcpToolFilter.class); private final Pattern allowedPattern; public CustomMcpToolFilter(@Value("${mcp.allowed.tool.pattern:.*}") String patternString) { this.allowedPattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE); log.info("MCP Tool Filter initialized with pattern: {}", patternString); } @Override public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) { boolean matches = allowedPattern.matcher(tool.name()).find(); if (matches) { log.debug("MCP Tool ALLOWED: {}", tool.name()); } else { log.info("MCP Tool FILTERED OUT: {}", tool.name()); } return matches; } } src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java +0 −3 Original line number Diff line number Diff line Loading @@ -35,12 +35,9 @@ class McpConfiguration { // Get the current authentication from SecurityContext Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // Check if the authentication is JWT-based (from Bearer token) if (authentication instanceof JwtAuthenticationToken jwtAuth) { // Extract the JWT token value String tokenValue = jwtAuth.getToken().getTokenValue(); // Add as Bearer token to the outgoing MCP Server request builder.header("Authorization", "Bearer " + tokenValue); } }; Loading src/main/resources/application.yaml +9 −1 Original line number Diff line number Diff line server: port: ${SERVER_PORT:11880} agent: skills: dirs: ${AGENT_SKILLS_DIRS:skills} mcp: allowed: tool: pattern: ${MCP_ALLOWED_TOOL_PATTERN:.*product.*} spring: application: name: ${SPRING_APPLICATION_NAME:osl-mcp-backend} Loading Loading
pom.xml +24 −2 Original line number Diff line number Diff line Loading @@ -23,9 +23,9 @@ </organization> <properties> <spring.boot-version>3.5.5</spring.boot-version> <spring.boot-version>4.0.0</spring.boot-version> <java.version>17</java.version> <spring-ai.version>1.1.0</spring-ai.version> <spring-ai.version>2.0.0-M2</spring-ai.version> <camel.version>4.0.0-RC2</camel.version> </properties> Loading @@ -34,6 +34,19 @@ <id>gitlab-maven</id> <url>https://labs.etsi.org/rep/api/v4/groups/260/-/packages/maven</url> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <distributionManagement> <repository> Loading Loading @@ -97,6 +110,11 @@ <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> <dependency> <groupId>org.springaicommunity</groupId> <artifactId>spring-ai-agent-utils</artifactId> <version>0.4.2</version> </dependency> <dependency> <groupId>org.commonmark</groupId> Loading Loading @@ -164,6 +182,10 @@ <groupId>org.apache.camel.springboot</groupId> <artifactId>camel-service-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-retry</artifactId> </dependency> </dependencies> <build> Loading
src/main/java/org/etsi/osl/mcp/backend/ChatController.java +35 −38 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ package org.etsi.osl.mcp.backend; import java.util.Map; import java.util.UUID; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; Loading @@ -10,7 +11,11 @@ import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.chat.client.advisor.ToolCallAdvisor; import org.springframework.ai.tool.ToolCallbackProvider; import org.springaicommunity.agent.tools.SkillsTool; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; Loading @@ -28,28 +33,40 @@ public class ChatController { @Value("${spring.ai.chat.maxMessages}") private final int maxMessages = 100; ChatMemoryRepository chatMemoryRepository; ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages( maxMessages ) .build(); public ChatController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ToolCallbackProvider mcpTools, @Value("${spring.ai.chat.system-prompt}") String systemPrompt, ChatMemory chatMemory) { @Value("${agent.skills.dirs}") String agentSkillsDir, ChatMemoryRepository chatMemoryRepository) { // Validate that system prompt is not empty if (systemPrompt == null || systemPrompt.trim().isEmpty()) { throw new IllegalArgumentException("System prompt cannot be null or empty. Please configure SPRING_AI_CHAT_SYSTEM_PROMPT with a valid value."); } this.systemPrompt = systemPrompt; ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(chatMemoryRepository) .maxMessages(maxMessages) .build(); // Build SkillsTool - build() returns a ToolCallback, not SkillsTool ToolCallback skillsTool = SkillsTool.builder() // FIXED TYPE HERE .addSkillsDirectory(agentSkillsDir) .build(); this.chatClient = chatClientBuilder .defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build()) .defaultToolCallbacks(tools) .defaultAdvisors( new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build() ) .defaultToolCallbacks(mcpTools) .defaultToolCallbacks(skillsTool) // Now this works! .defaultSystem(systemPrompt) .build(); log.info("ChatController initialized with skills from: {}", agentSkillsDir); log.info("ChatController initialized with system prompt: {}", systemPrompt); this.conversationId = UUID.randomUUID().toString(); } Loading @@ -64,12 +81,9 @@ public class ChatController { log.debug("Response from AI: {}", response); String markdownAnswer = formatResponse(question, response); log.debug("Markdown formatted response: {}", markdownAnswer); // var htmlResponse = MarkdownHelper.toHTML(markdownAnswer); // log.debug("HTML formatted response: {}", htmlResponse); return new Answer(markdownAnswer); } public String simpleAsk(Map<String, Object> headers, String question) { var response = chatClient.prompt() .user(question) Loading @@ -82,23 +96,6 @@ public class ChatController { private String formatResponse(Question question, String answer) { return answer; // var prompt = """ // Following are the question and answer: // // Question: {question} // // Answer: {answer} // // Format the answer into plain human readable text and return only the formatted response. // """; // return chatClient // .prompt() // .user(u -> u.text(prompt) // .param("question", question.question()) // .param("answer", answer) // ) // .call() // .content(); } public record Question(String question) {} Loading
src/main/java/org/etsi/osl/mcp/backend/configuration/CustomMcpToolFilter.java 0 → 100644 +45 −0 Original line number Diff line number Diff line package org.etsi.osl.mcp.backend.configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.modelcontextprotocol.spec.McpSchema; import org.springframework.ai.mcp.McpToolFilter; import org.springframework.ai.mcp.McpConnectionInfo; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.regex.Pattern; /** * Configurable MCP Tool Filter * * Filters MCP tools based on a configurable regex pattern. * Only tools matching the pattern will be available to the chat client. * * @author OpenSlice Team */ @Component public class CustomMcpToolFilter implements McpToolFilter { private static final Logger log = LoggerFactory.getLogger(CustomMcpToolFilter.class); private final Pattern allowedPattern; public CustomMcpToolFilter(@Value("${mcp.allowed.tool.pattern:.*}") String patternString) { this.allowedPattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE); log.info("MCP Tool Filter initialized with pattern: {}", patternString); } @Override public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) { boolean matches = allowedPattern.matcher(tool.name()).find(); if (matches) { log.debug("MCP Tool ALLOWED: {}", tool.name()); } else { log.info("MCP Tool FILTERED OUT: {}", tool.name()); } return matches; } }
src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java +0 −3 Original line number Diff line number Diff line Loading @@ -35,12 +35,9 @@ class McpConfiguration { // Get the current authentication from SecurityContext Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // Check if the authentication is JWT-based (from Bearer token) if (authentication instanceof JwtAuthenticationToken jwtAuth) { // Extract the JWT token value String tokenValue = jwtAuth.getToken().getTokenValue(); // Add as Bearer token to the outgoing MCP Server request builder.header("Authorization", "Bearer " + tokenValue); } }; Loading
src/main/resources/application.yaml +9 −1 Original line number Diff line number Diff line server: port: ${SERVER_PORT:11880} agent: skills: dirs: ${AGENT_SKILLS_DIRS:skills} mcp: allowed: tool: pattern: ${MCP_ALLOWED_TOOL_PATTERN:.*product.*} spring: application: name: ${SPRING_APPLICATION_NAME:osl-mcp-backend} Loading