From 2f75885a5694befe370d3c76fa19b04b3f82286d Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Mon, 17 Nov 2025 21:42:51 +0200 Subject: [PATCH 01/21] first approach --- .gitignore | 33 +++++ pom.xml | 75 ++++++++++ .../etsi/osl/mcp/backend/ChatController.java | 64 ++++++++ .../etsi/osl/mcp/backend/MarkdownHelper.java | 15 ++ .../mcp/backend/OslMcpClientApplication.java | 12 ++ src/main/resources/application.yaml | 27 ++++ src/main/resources/public/index.html | 140 ++++++++++++++++++ src/main/resources/static/robot.svg | 8 + src/main/resources/static/styles.css | 96 ++++++++++++ 9 files changed, 470 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/org/etsi/osl/mcp/backend/ChatController.java create mode 100644 src/main/java/org/etsi/osl/mcp/backend/MarkdownHelper.java create mode 100644 src/main/java/org/etsi/osl/mcp/backend/OslMcpClientApplication.java create mode 100644 src/main/resources/application.yaml create mode 100644 src/main/resources/public/index.html create mode 100644 src/main/resources/static/robot.svg create mode 100644 src/main/resources/static/styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0cb4263 --- /dev/null +++ b/pom.xml @@ -0,0 +1,75 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.5 + + + + org.etsi.osl + org.etsi.osl.mcp.backend + 0.0.1-SNAPSHOT + org.etsi.osl.mcp.backend + + + OpenSlice by ETSI + https://osl.etsi.org + + + + 17 + 1.1.0 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.ai + spring-ai-starter-mcp-client-webflux + + + org.springframework.ai + spring-ai-starter-model-ollama + + + org.commonmark + commonmark + 0.24.0 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java new file mode 100644 index 0000000..572cba4 --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java @@ -0,0 +1,64 @@ +package org.etsi.osl.mcp.backend; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.web.bind.annotation.*; + +@RestController +public class ChatController { + private static final Logger log = LoggerFactory.getLogger(ChatController.class); + private final ChatClient chatClient; + + + public ChatController(ChatClient.Builder chatClientBuilder, + ToolCallbackProvider tools) { + this.chatClient = chatClientBuilder + .defaultAdvisors(new SimpleLoggerAdvisor()) + .defaultToolCallbacks(tools) + .build(); + } + + @PostMapping("/ask") + public Answer ask(@RequestBody Question question) { + var response = chatClient.prompt() + .user(question.question()) + .call() + .content(); + 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(htmlResponse); + } + + 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) {} + + public record Answer(String answer) {} +} diff --git a/src/main/java/org/etsi/osl/mcp/backend/MarkdownHelper.java b/src/main/java/org/etsi/osl/mcp/backend/MarkdownHelper.java new file mode 100644 index 0000000..77afa04 --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/MarkdownHelper.java @@ -0,0 +1,15 @@ +package org.etsi.osl.mcp.backend; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + +public class MarkdownHelper { + private static final Parser parser = Parser.builder().build(); + private static final HtmlRenderer renderer = HtmlRenderer.builder().build(); + + public static String toHTML(String markdownText) { + Node document = parser.parse(markdownText); + return renderer.render(document); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/mcp/backend/OslMcpClientApplication.java b/src/main/java/org/etsi/osl/mcp/backend/OslMcpClientApplication.java new file mode 100644 index 0000000..b13c849 --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/OslMcpClientApplication.java @@ -0,0 +1,12 @@ +package org.etsi.osl.mcp.backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class OslMcpClientApplication { + + public static void main(String[] args) { + SpringApplication.run(OslMcpClientApplication.class, args); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..8df08d1 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,27 @@ + +server: + port: 11880 + +spring: + application: + name: osl-mcp-backend + ai: + ollama: + base-url: http://localhost:11434 + chat: + options: + model: gpt-oss:20b + temperature: 0.7 + mcp: + client: + type: SYNC # or ASYNC + streamable-http: + connections: + openslice-server: + url: http://localhost:13015 + +logging: + level: + root: INFO + org.etsi.osl.*: DEBUG + org.springframework.ai.chat.client.advisor: DEBUG \ No newline at end of file diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html new file mode 100644 index 0000000..79a08c2 --- /dev/null +++ b/src/main/resources/public/index.html @@ -0,0 +1,140 @@ + + + + + + AI Chat + + + + + +
+
+
+
+ +
+
+ Robot Avatar +
+
+
ChatBot
+

Online

+
+
+ + +
+ +
+
+ Robot Avatar +
+
+
+ Hi there! How can I help you today? +
+
10:03 AM
+
+
+ +
+ + +
+
+ + +
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/src/main/resources/static/robot.svg b/src/main/resources/static/robot.svg new file mode 100644 index 0000000..b8fa6ba --- /dev/null +++ b/src/main/resources/static/robot.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/styles.css b/src/main/resources/static/styles.css new file mode 100644 index 0000000..50c6b9c --- /dev/null +++ b/src/main/resources/static/styles.css @@ -0,0 +1,96 @@ +body { + background-color: #f4f7f6; + height: 100vh; +} +.chat-container { + height: 85vh; + max-height: 85vh; + border-radius: 15px; + background-color: #fff; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} +.chat-header { + border-radius: 15px 15px 0 0; + background-color: #7269ef; + color: white; + padding: 15px; +} +.chat-messages { + height: calc(100% - 140px); + overflow-y: auto; + padding: 20px; +} +.message { + margin-bottom: 20px; + max-width: 80%; +} +.message-content { + padding: 12px 15px; + border-radius: 15px; + display: inline-block; +} +.incoming .message-content { + background-color: #f5f6fa; + color: #343a40; +} +.outgoing { + margin-left: auto; +} +.outgoing .message-content { + background-color: #7269ef; + color: white; +} +.message-time { + font-size: 0.75rem; + color: #6c757d; + margin-top: 5px; +} +.chat-input { + padding: 15px; + background-color: #fff; + border-top: 1px solid #e9ecef; + border-radius: 0 0 15px 15px; + position: absolute; + bottom: 0; + width: 100%; +} +.online-indicator { + width: 10px; + height: 10px; + background-color: #28a745; + border-radius: 50%; + display: inline-block; + margin-right: 5px; +} +.avatar { + min-width: 40px; + width: 40px; + height: 40px; + border-radius: 50%; + margin-right: 10px; + background-color: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: #495057; + flex-shrink: 0; + overflow: hidden; + padding: 0; +} +.avatar svg { + width: 30px; + height: 30px; +} +.outgoing .avatar { + margin-right: 0; + margin-left: 10px; + background-color: #7269ef; + color: white; +} +.message-textarea { + resize: none; + overflow: auto; + height: 50px; + max-height: 100px; +} \ No newline at end of file -- GitLab From c450151d2da9888adb5ae8e5c64393ecd326b648 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Tue, 18 Nov 2025 01:01:28 +0200 Subject: [PATCH 02/21] enabling oauth for client --- pom.xml | 15 +++++- .../osl/mcp/backend/McpConfiguration.java | 50 +++++++++++++++++++ .../backend/WebSecurityConfigKeycloak.java | 25 ++++++++++ src/main/resources/application.yaml | 15 +++++- 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/etsi/osl/mcp/backend/McpConfiguration.java create mode 100644 src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java diff --git a/pom.xml b/pom.xml index 0cb4263..294c9ad 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ org.springframework.ai - spring-ai-starter-mcp-client-webflux + spring-ai-starter-mcp-client org.springframework.ai @@ -50,6 +50,15 @@ spring-boot-starter-test test + + + + org.springaicommunity + mcp-client-security + 0.0.4 + + + @@ -60,6 +69,10 @@ pom import + + + + diff --git a/src/main/java/org/etsi/osl/mcp/backend/McpConfiguration.java b/src/main/java/org/etsi/osl/mcp/backend/McpConfiguration.java new file mode 100644 index 0000000..7603d06 --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/McpConfiguration.java @@ -0,0 +1,50 @@ +package org.etsi.osl.mcp.backend; + +import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; +import org.springaicommunity.mcp.security.client.sync.AuthenticationMcpTransportContextProvider; +import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2AuthorizationCodeSyncHttpRequestCustomizer; + +import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; + +/** + * @author Daniel Garnier-Moiroux + */ +@Configuration +class McpConfiguration { + + @Bean + McpSyncHttpClientRequestCustomizer requestCustomizer(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, + ClientRegistrationRepository clientRegistrationRepository) { + var registrationId = findUniqueClientRegistration(clientRegistrationRepository); + return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(oAuth2AuthorizedClientManager, registrationId); + } + + @Bean + McpSyncClientCustomizer syncClientCustomizer() { + return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); + } + + /** + * Returns the ID of the {@code spring.security.oauth2.client.registration}, if + * unique. + */ + private static String findUniqueClientRegistration(ClientRegistrationRepository clientRegistrationRepository) { + String registrationId; + if (!(clientRegistrationRepository instanceof InMemoryClientRegistrationRepository repo)) { + throw new IllegalStateException("Expected an InMemoryClientRegistrationRepository"); + } + var iterator = repo.iterator(); + var firstRegistration = iterator.next(); + if (iterator.hasNext()) { + throw new IllegalStateException("Expected a single Client Registration"); + } + registrationId = firstRegistration.getRegistrationId(); + return registrationId; + } + +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java new file mode 100644 index 0000000..eb92b78 --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java @@ -0,0 +1,25 @@ +package org.etsi.osl.mcp.backend; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@Profile("!testing") +public class WebSecurityConfigKeycloak { + + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .oauth2Client(Customizer.withDefaults()) + .csrf(CsrfConfigurer::disable) + .build(); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8df08d1..7e7d8c8 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -10,7 +10,8 @@ spring: base-url: http://localhost:11434 chat: options: - model: gpt-oss:20b + xmodel: gpt-oss:20b + model: qwen3:8b temperature: 0.7 mcp: client: @@ -19,6 +20,18 @@ spring: connections: openslice-server: url: http://localhost:13015 + security: + oauth2: + client: + registration: + authserver: + client-id: osapiWebClientId + client-secret: secret + authorization-grant-type: authorization_code + provider: authserver + provider: + authserver: + issuer-uri: http://keycloak:8080/auth/realms/openslice logging: level: -- GitLab From dca12699bfbf18dc61b13a48f6c269248f61586b Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Tue, 2 Dec 2025 15:16:58 +0200 Subject: [PATCH 03/21] add support to respond via activeMQ agents queue --- pom.xml | 83 +++++++++++++++---- .../etsi/osl/mcp/backend/ChatController.java | 12 +++ .../ActiveMQComponentConfig.java | 41 +++++++++ .../{ => configuration}/McpConfiguration.java | 2 +- .../configuration/PartnerRouteBuillder.java | 53 ++++++++++++ src/main/resources/application.yaml | 17 +++- 6 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java rename src/main/java/org/etsi/osl/mcp/backend/{ => configuration}/McpConfiguration.java (97%) create mode 100644 src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java diff --git a/pom.xml b/pom.xml index 294c9ad..49ba05b 100644 --- a/pom.xml +++ b/pom.xml @@ -25,8 +25,31 @@ 17 1.1.0 + 4.0.0-RC2 + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + org.apache.camel.springboot + camel-spring-boot-dependencies + ${camel.version} + pom + import + + + + + + org.springframework.boot @@ -58,23 +81,51 @@ 0.0.4 - - - - - - org.springframework.ai - spring-ai-bom - ${spring-ai.version} - pom - import - - - + + + org.springframework.boot + spring-boot-starter-activemq + + + org.apache.activemq + activemq-amqp + test + + + org.apache.qpid + proton-j + + + + + org.messaginghub + pooled-jms + - - - + + + org.apache.camel.springboot + camel-spring-boot-starter + + + org.apache.activemq + activemq-pool + + + org.apache.camel + camel-activemq + + + org.apache.activemq + activemq-broker + + + + + org.apache.camel.springboot + camel-service-starter + + diff --git a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java index 572cba4..2d9fbcc 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java +++ b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java @@ -1,5 +1,6 @@ package org.etsi.osl.mcp.backend; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; @@ -37,6 +38,17 @@ public class ChatController { return new Answer(htmlResponse); } + + public String simpleAsk( Map headers, String question) { + var response = chatClient.prompt() + .user( question ) + .call() + .content(); + + log.debug("Response from AI: {}", response); + return response; + } + private String formatResponse(Question question, String answer) { return answer; // var prompt = """ diff --git a/src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java b/src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java new file mode 100644 index 0000000..a0d3fca --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/configuration/ActiveMQComponentConfig.java @@ -0,0 +1,41 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.bugzilla + * %% + * Copyright (C) 2019 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.mcp.backend.configuration; + +import org.apache.camel.component.activemq.ActiveMQComponent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import jakarta.jms.ConnectionFactory; + +/** + * @author ctranoris + * + */ +@Configuration +public class ActiveMQComponentConfig { + + @Bean(name = "activemq") + public ActiveMQComponent createComponent(ConnectionFactory factory) { + ActiveMQComponent activeMQComponent = new ActiveMQComponent(); + activeMQComponent.setConnectionFactory(factory); + return activeMQComponent; + } +} diff --git a/src/main/java/org/etsi/osl/mcp/backend/McpConfiguration.java b/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java similarity index 97% rename from src/main/java/org/etsi/osl/mcp/backend/McpConfiguration.java rename to src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java index 7603d06..d446dc1 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/McpConfiguration.java +++ b/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java @@ -1,4 +1,4 @@ -package org.etsi.osl.mcp.backend; +package org.etsi.osl.mcp.backend.configuration; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; import org.springaicommunity.mcp.security.client.sync.AuthenticationMcpTransportContextProvider; diff --git a/src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java b/src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java new file mode 100644 index 0000000..bd76a0a --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/configuration/PartnerRouteBuillder.java @@ -0,0 +1,53 @@ +package org.etsi.osl.mcp.backend.configuration; + +import java.io.IOException; +import org.apache.camel.LoggingLevel; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.etsi.osl.mcp.backend.ChatController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; + + +@Configuration +@Component +public class PartnerRouteBuillder extends RouteBuilder { + + @Value("${spring.application.name}") + private String compname; + + @Autowired + private ChatController chatController; + + @Override + public void configure() throws Exception { + + + String EVENT_CHAT = "jms:queue:agents/"+compname ; + + + from(EVENT_CHAT) + .log(LoggingLevel.INFO, log, EVENT_CHAT + " message received!") + .to("log:DEBUG?showBody=true&showHeaders=true") + .bean( chatController, "simpleAsk( ${headers}, ${body} )") + .convertBodyTo( String.class ); + } + + static T toJsonObj(String content, Class valueType) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.readValue(content, valueType); + } + + static String toJsonString(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsString(object); + } + + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7e7d8c8..d2d75f4 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,17 +1,17 @@ - + server: port: 11880 spring: application: - name: osl-mcp-backend + name: osl-mcp-backend ai: ollama: base-url: http://localhost:11434 chat: options: - xmodel: gpt-oss:20b - model: qwen3:8b + model: gpt-oss:20b + xmodel: qwen3:8b temperature: 0.7 mcp: client: @@ -33,6 +33,15 @@ spring: authserver: issuer-uri: http://keycloak:8080/auth/realms/openslice + activemq: + brokerUrl: tcp://localhost:61616?jms.watchTopicAdvisories=false + user: artemis + password: artemis + pool: + enabled: true + max-connections: 100 + packages: + trust-all: true logging: level: root: INFO -- GitLab From 5922d2206757895bed5219d56fb69dee89204246 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 4 Dec 2025 17:07:00 +0200 Subject: [PATCH 04/21] adding dockerfile and example dockercompose --- Dockerfile | 32 +++++++ README.md | 223 +++++++++++++++++++++++++++++++++------------ docker-compose.yml | 45 +++++++++ 3 files changed, 244 insertions(+), 56 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aa6f298 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Build stage +FROM maven:3.9-eclipse-temurin-17 AS builder + +WORKDIR /build + +# Copy pom.xml and download dependencies +COPY pom.xml . +RUN mvn dependency:go-offline + +# Copy source code +COPY src ./src + +# Build application +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:17-jre-alpine + +WORKDIR /app + +# Copy built JAR from builder stage +COPY --from=builder /build/target/org.etsi.osl.mcp.backend-0.0.1-SNAPSHOT.jar app.jar + +# Expose port +EXPOSE 11880 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 + +# Run application +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md index 9aeced0..3166e9a 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,204 @@ -# org.etsi.org.mcp.backend +# org.etsi.osl.mcp.backend +A Spring Boot backend service that integrates Large Language Models (LLMs) with the Model Context Protocol (MCP) for the OpenSlice platform. It provides REST and message-driven interfaces for AI-powered question answering with access to external tools and context via MCP servers. +## Features -## Getting started +- **REST API** - `/ask` endpoint for synchronous question processing +- **Message-Driven** - ActiveMQ queue listener for asynchronous question submission +- **MCP Integration** - Connects to MCP servers (e.g., OpenSlice) to provide tools and context to LLMs +- **OAuth2 Authentication** - Keycloak-based security for MCP server access +- **HTML Response Formatting** - Markdown to HTML conversion for web compatibility +- **Local LLM Support** - Ollama integration for running open-source models locally -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +## Requirements -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +- Java 17+ +- Maven 3.9+ +- Ollama (for local LLM inference, default: `http://localhost:11434`) +- ActiveMQ broker (default: `tcp://localhost:61616`) +- Keycloak (for OAuth2, default: `http://keycloak:8080/auth/realms/openslice`) +- MCP Server (e.g., OpenSlice at `http://localhost:13015`) -## Add your files +## Quick Start -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command: +### Build +```bash +mvn clean package +``` +### Run Locally +```bash +mvn spring-boot:run ``` -cd existing_repo -git remote add origin https://labs.etsi.org/rep/osl/code/org.etsi.org.mcp.backend.git -git branch -M main -git push -uf origin main + +The application starts on port **11880**. + +### Docker +```bash +docker build -t org.etsi.osl.mcp.backend:latest . +docker run -p 11880:11880 org.etsi.osl.mcp.backend:latest ``` -## Integrate with your tools +### Docker Compose (Complete Stack) +The easiest way to run the entire application stack with all dependencies: -- [ ] [Set up project integrations](https://labs.etsi.org/rep/osl/code/org.etsi.org.mcp.backend/-/settings/integrations) +```bash +docker-compose up -d +``` -## Collaborate with your team +This starts: +- Ollama LLM server (http://localhost:11434) +- OSL MCP Backend (http://localhost:11880) -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/) +After services are running, pull an LLM model: +```bash +docker-compose exec ollama ollama pull gpt-oss:20b +``` -## Test and Deploy +Check service status: +```bash +docker-compose ps +``` -Use the built-in continuous integration in GitLab. +View logs: +```bash +docker-compose logs -f osl-mcp-backend +``` -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +Stop all services: +```bash +docker-compose down +``` -*** +See `docker-compose.yml` for configuration details and environment variables. -# Editing this README +## Usage -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +### REST API Example +```bash +curl -X POST http://localhost:11880/ask \ + -H "Content-Type: application/json" \ + -d '{"question": "What is the weather today?"}' +``` -## Suggestions for a good README +Response: +```json +{ + "answer": "..." +} +``` -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +### ActiveMQ Integration +Send a message to the queue `agents/osl-mcp-backend` via ActiveMQ, and the response will be processed by the Camel route. -## Name -Choose a self-explaining name for your project. +## Configuration -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +Edit `src/main/resources/application.yaml` to customize: -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +- **Server port** - Change `server.port` +- **LLM model** - Adjust `spring.ai.ollama.chat.options.model` +- **MCP server URL** - Update `spring.ai.mcp.client.streamable-http.connections.openslice-server.url` +- **OAuth2 credentials** - Configure `spring.security.oauth2.client.registration.authserver` +- **ActiveMQ broker** - Modify `spring.activemq.brokerUrl` +- **Logging level** - Adjust `logging.level` -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +## Testing -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +```bash +mvn test +``` -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +Run a single test: +```bash +mvn test -Dtest=ClassName#testMethodName +``` -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +## Architecture -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +### High-Level Design -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +The application follows a layered architecture with three main integration points: -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +1. **REST Layer** - `ChatController` provides HTTP endpoints for synchronous question processing +2. **Message Layer** - Apache Camel routes handle asynchronous message processing from ActiveMQ +3. **AI/Context Layer** - Spring AI ChatClient combines LLM capabilities with MCP tools and context -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +### Core Components + +#### ChatController +- **Endpoint**: `POST /ask` +- **Function**: Accepts questions, invokes ChatClient, converts responses to HTML +- **Dependencies**: Spring AI ChatClient, MarkdownHelper +- **Methods**: + - `ask()` - HTTP endpoint for REST clients + - `simpleAsk()` - Internal method for Camel route integration + +#### MCP Configuration +- Sets up OAuth2-secured synchronous HTTP connections to MCP servers +- Configures authentication with Keycloak for OAuth2 client credentials flow +- Manages MCP client customization with security context providers + +#### Apache Camel Routes +- **Queue Listener**: `jms:queue:agents/[application-name]` +- **Message Flow**: ActiveMQ → Camel Route → ChatController.simpleAsk() → Response +- **Purpose**: Enables external systems to submit questions asynchronously + +#### ActiveMQ Integration +- Provides connection pooling for JMS communication +- Routes incoming messages to the backend for processing +- Allows distributed systems to queue questions for batch processing + +### Data Flow + +``` +REST Client External System + | | + v v +POST /ask ActiveMQ Queue + | | + +-----------> ChatController <--------+ + | + v + Spring AI ChatClient + | + +----------+----------+ + v v + Ollama LLM MCP Server + (Language) (Tools/Context) + | | + +----------+----------+ + v + HTML Response + | + +----------+----------+ + v v + REST Response Queue Response +``` + +### Key Dependencies + +- **Spring Boot 3.5.5** - Application framework +- **Spring AI 1.1.0** - LLM and MCP client integration +- **Apache Camel 4.0.0-RC2** - Message routing and orchestration +- **Spring Security** - OAuth2 authentication +- **CommonMark** - Markdown to HTML conversion + +### External Services + +- **Ollama** - Local LLM inference server +- **Keycloak** - OAuth2 authentication and authorization +- **ActiveMQ** - Message broker for async processing +- **MCP Server** - Provides tools and context to the LLM -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. ## License -For open source projects, say how it is licensed. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +This project is part of OpenSlice by ETSI. See LICENSE file for details. + +## References + +- [OpenSlice](https://osl.etsi.org) +- [Spring AI Documentation](https://spring.io/projects/spring-ai) +- [Model Context Protocol](https://modelcontextprotocol.io) +- [Apache Camel](https://camel.apache.org) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..58bcfa1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +version: '3.8' + +services: + + # Ollama LLM Server + ollama: + image: ollama/ollama:latest + container_name: osl-ollama + ports: + - "11434:11434" + environment: + OLLAMA_HOST: 0.0.0.0:11434 + volumes: + - ollama_data:/root/.ollama + # Note: You may need to pull a model after starting: + # docker exec osl-ollama ollama pull gpt-oss:20b + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"] + interval: 30s + timeout: 10s + retries: 3 + + # OSL MCP Backend + osl-mcp-backend: + build: + context: . + dockerfile: Dockerfile + container_name: osl-mcp-backend + ports: + - "11880:11880" + environment: + SPRING_AI_OLLAMA_BASE_URL: http://ollama:11434 + SPRING_AI_MCP_CLIENT_STREAMABLE_HTTP_CONNECTIONS_OPENSLICE_SERVER_URL: http://mcp-server:13015 + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_AUTHSERVER_ISSUER_URI: http://keycloak:8080/auth/realms/openslice + SPRING_ACTIVEMQ_BROKERURL: tcp://activemq:61616 + SPRING_ACTIVEMQ_USER: artemis + SPRING_ACTIVEMQ_PASSWORD: artemis + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:11880/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + ollama_data: -- GitLab From f550c48099dfb8b851720fb723564fe56b10238d Mon Sep 17 00:00:00 2001 From: denazi Date: Tue, 16 Dec 2025 11:03:37 +0200 Subject: [PATCH 05/21] BE Service #2: Implement authorization BE Service controller and Chat UI --- pom.xml | 8 + .../etsi/osl/mcp/backend/AuthController.java | 115 +++++++ .../backend/WebSecurityConfigKeycloak.java | 55 +++- src/main/resources/application.yaml | 18 +- src/main/resources/public/chat.html | 60 ++++ src/main/resources/public/css/chat.css | 307 ++++++++++++++++++ src/main/resources/public/css/welcome.css | 110 +++++++ .../resources/public/images/logo_clear.png | Bin 0 -> 16566 bytes src/main/resources/public/index.html | 182 +++++++---- src/main/resources/public/js/chat.js | 186 +++++++++++ src/main/resources/public/js/welcome.js | 29 ++ src/main/resources/public/welcome.html | 95 ++++++ 12 files changed, 1094 insertions(+), 71 deletions(-) create mode 100644 src/main/java/org/etsi/osl/mcp/backend/AuthController.java create mode 100644 src/main/resources/public/chat.html create mode 100644 src/main/resources/public/css/chat.css create mode 100644 src/main/resources/public/css/welcome.css create mode 100644 src/main/resources/public/images/logo_clear.png create mode 100644 src/main/resources/public/js/chat.js create mode 100644 src/main/resources/public/js/welcome.js create mode 100644 src/main/resources/public/welcome.html diff --git a/pom.xml b/pom.xml index 49ba05b..dd21939 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,14 @@ + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.boot spring-boot-starter-web diff --git a/src/main/java/org/etsi/osl/mcp/backend/AuthController.java b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java new file mode 100644 index 0000000..1905527 --- /dev/null +++ b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java @@ -0,0 +1,115 @@ +package org.etsi.osl.mcp.backend; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class AuthController { + + @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") + private String issuerUri; + + @GetMapping("/api/user") + public Map getUserInfo(Authentication authentication, + @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { + Map userInfo = new HashMap<>(); + + if (authentication != null && authentication.isAuthenticated()) { + userInfo.put("authenticated", true); + userInfo.put("name", authentication.getName()); + + String username = authentication.getName(); + if (authentication.getPrincipal() instanceof OidcUser oidcUser) { + userInfo.put("email", oidcUser.getEmail()); + if (oidcUser.getPreferredUsername() != null) { + username = oidcUser.getPreferredUsername(); + userInfo.put("preferredUsername", oidcUser.getPreferredUsername()); + } + } + userInfo.put("username", username); + + if (authorizedClient != null && authorizedClient.getAccessToken() != null) { + userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); + userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); + } + } else { + userInfo.put("authenticated", false); + } + + return userInfo; + } + + @PostMapping("/api/logout") + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + performLogout(request, response, authentication); + } + + @GetMapping("/api/logout") + public void logoutGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + performLogout(request, response, authentication); + } + + private void performLogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + if (authentication != null) { + new SecurityContextLogoutHandler().logout(request, null, authentication); + } + + if (request.getSession(false) != null) { + request.getSession().invalidate(); + } + + String logoutUrl = buildKeycloakLogoutUrl(request, authentication); + response.sendRedirect(logoutUrl); + } + + private String buildKeycloakLogoutUrl(HttpServletRequest request, Authentication authentication) { + String keycloakIssuer = issuerUri; + + if (authentication != null && authentication.getPrincipal() instanceof OidcUser oidcUser) { + if (oidcUser.getIssuer() != null) { + keycloakIssuer = oidcUser.getIssuer().toString(); + } + } + + if (keycloakIssuer == null || keycloakIssuer.isEmpty()) { + return "/welcome.html"; + } + + String baseUrl = request.getScheme() + "://" + request.getServerName(); + if ((request.getScheme().equals("http") && request.getServerPort() != 80) || + (request.getScheme().equals("https") && request.getServerPort() != 443)) { + baseUrl += ":" + request.getServerPort(); + } + + String postLogoutRedirectUri = baseUrl + "/welcome.html"; + + String keycloakLogoutUrl = keycloakIssuer + "/protocol/openid-connect/logout"; + + return UriComponentsBuilder + .fromUriString(keycloakLogoutUrl) + .queryParam("post_logout_redirect_uri", postLogoutRedirectUri) + .build() + .toUriString(); + } + + @GetMapping("/api/health") + public Map health() { + Map response = new HashMap<>(); + response.put("status", "UP"); + return response; + } +} diff --git a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java index eb92b78..744cd36 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +++ b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java @@ -7,19 +7,70 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.http.HttpStatus; @Configuration @EnableWebSecurity @Profile("!testing") public class WebSecurityConfigKeycloak { - @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + return http + .authorizeHttpRequests(auth -> auth + // Public static resources + .requestMatchers("/", "/index.html", "/styles.css", "/robot.svg", "/favicon.ico").permitAll() + .requestMatchers("/api/health").permitAll() + .requestMatchers("/api/user").permitAll() + .requestMatchers("/api/logout").permitAll() + .requestMatchers("/logout").permitAll() + .requestMatchers("/ask", "/api/**").authenticated() + .anyRequest().permitAll() + ) + // OAuth2 Login for browser-based authentication + .oauth2Login(oauth2 -> oauth2 + .defaultSuccessUrl("/", true) + ) + // Logout configuration + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/") + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID") + ) .oauth2Client(Customizer.withDefaults()) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + ) + .exceptionHandling(exception -> exception + .defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new AntPathRequestMatcher("/ask") + ) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + ) .csrf(CsrfConfigurer::disable) .build(); } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d2d75f4..8333f7b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -22,16 +22,26 @@ spring: url: http://localhost:13015 security: oauth2: + resourceserver: + jwt: + issuer-uri: http://keycloak:8080/auth/realms/openslice client: registration: - authserver: + keycloak: client-id: osapiWebClientId - client-secret: secret authorization-grant-type: authorization_code - provider: authserver + redirect-uri: "{baseUrl}/login/oauth2/code/keycloak" + scope: + - openid + - profile + provider: keycloak provider: - authserver: + keycloak: issuer-uri: http://keycloak:8080/auth/realms/openslice + authorization-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/auth + token-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/token + user-info-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/userinfo + jwk-set-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/certs activemq: brokerUrl: tcp://localhost:61616?jms.watchTopicAdvisories=false diff --git a/src/main/resources/public/chat.html b/src/main/resources/public/chat.html new file mode 100644 index 0000000..944c0fd --- /dev/null +++ b/src/main/resources/public/chat.html @@ -0,0 +1,60 @@ + + + + + + OpenSlice Chat Assistant + + + + + + + + +
+
+
+

OpenSlice AI Assistant

+ Ask me anything about your OpenSlice platform +
+
+
+
AI
+
+
Hi there! How can I help you today?
+
+
+
+
+
+
+ + +
+
+
+
+ + + diff --git a/src/main/resources/public/css/chat.css b/src/main/resources/public/css/chat.css new file mode 100644 index 0000000..df841f1 --- /dev/null +++ b/src/main/resources/public/css/chat.css @@ -0,0 +1,307 @@ +/* OpenSlice Chat Assistant - Chat Page Styles */ + +body { + font-family: "Open Sans", sans-serif; + padding-top: 60px; + padding-bottom: 20px; + background-color: white; +} + +/* Navbar */ +.navbar-default { + background-color: rgba(245, 245, 245, 0.93); + border-bottom: 1px solid #e7e7e7; +} + +.navbar-brand img { + height: 25px; +} + +/* Chat Container */ +.chat-container { + max-width: 900px; + margin: 20px auto; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + overflow: hidden; +} + +/* Chat Header */ +.chat-header { + background-color: #7EADDB; + color: white; + padding: 20px; + border-bottom: 1px solid #6589AB; +} + +.chat-header h4 { + margin: 0; + font-weight: 600; +} + +.chat-header .online-indicator { + display: inline-block; + width: 8px; + height: 8px; + background-color: #5cb85c; + border-radius: 50%; + margin-right: 5px; +} + +/* Chat Messages */ +.chat-messages { + height: 500px; + overflow-y: auto; + padding: 20px; + background-color: #f9f9f9; +} + +.message { + margin-bottom: 20px; + display: flex; + align-items: flex-start; + clear: both; +} + +.message.incoming { + flex-direction: row; + justify-content: flex-start; + padding-right: 80px; +} + +.message.outgoing { + flex-direction: row-reverse; + justify-content: flex-start; + padding-left: 80px; +} + +.message .avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #7EADDB; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + flex-shrink: 0; +} + +.message.incoming .avatar { + margin-right: 12px; +} + +.message.outgoing .avatar { + margin-left: 12px; + background-color: #6589AB; +} + +.message-wrapper { + display: flex; + flex-direction: column; + max-width: 66%; + min-width: fit-content; +} + +.message.incoming .message-wrapper { + align-items: flex-start; +} + +.message.outgoing .message-wrapper { + align-items: flex-end; +} + +.message-content { + max-width: 100%; + padding: 12px 16px; + border-radius: 8px; + word-wrap: break-word; + white-space: pre-wrap; + line-height: 1.5; +} + +.message-content p { + margin: 0; +} + +.message-content p:last-child { + margin-bottom: 0; +} + +.message-content p + p { + margin-top: 10px; +} + +.message-content code { + background-color: rgba(0, 0, 0, 0.05); + padding: 2px 4px; + border-radius: 3px; + font-family: monospace; + font-size: 13px; +} + +.message-content pre { + background-color: rgba(0, 0, 0, 0.05); + padding: 10px; + border-radius: 4px; + overflow-x: auto; + margin: 10px 0; +} + +.message-content pre code { + background-color: transparent; + padding: 0; +} + +.message-content ul, .message-content ol { + margin: 10px 0; + padding-left: 20px; +} + +.message-content li { + margin: 5px 0; +} + +.message.incoming .message-content { + background-color: white; + border: 1px solid #e0e0e0; + color: #333; +} + +.message.outgoing .message-content { + background-color: #7EADDB; + color: white; +} + +.message.outgoing .message-content code { + background-color: rgba(255, 255, 255, 0.2); +} + +.message.outgoing .message-content pre { + background-color: rgba(255, 255, 255, 0.2); +} + +.message-time { + font-size: 11px; + color: #999; + margin-top: 5px; + text-align: right; +} + +.message.incoming .message-time { + text-align: left; +} + +/* Chat Input */ +.chat-input { + padding: 20px; + background-color: white; + border-top: 1px solid #e0e0e0; +} + +.chat-input .input-group { + display: flex; +} + +.chat-input textarea { + flex: 1; + resize: none; + border: 1px solid #ddd; + border-radius: 4px 0 0 4px; + padding: 10px; + font-size: 14px; + min-height: 45px; +} + +.chat-input button { + background-color: #7EADDB; + border: none; + color: white; + padding: 0 25px; + border-radius: 0 4px 4px 0; + font-weight: 600; + transition: background-color 0.3s; +} + +.chat-input button:hover { + background-color: #6589AB; +} + +/* Tool Execution */ +.tool-execution { + background-color: #f0f8ff; + border-left: 3px solid #7EADDB; + padding: 10px; + margin-top: 8px; + border-radius: 4px; + font-family: monospace; + font-size: 12px; +} + +.tool-name { + font-weight: bold; + color: #6589AB; +} + +/* Typing Indicator */ +.typing-indicator { + display: none; + margin-bottom: 20px; +} + +.typing-indicator.active { + display: flex; +} + +.typing-indicator .avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #7EADDB; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + flex-shrink: 0; + margin-right: 12px; +} + +.typing-indicator .dots { + background-color: white; + border: 1px solid #e0e0e0; + padding: 12px 20px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 6px; +} + +.typing-indicator .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #7EADDB; + animation: typing 1.4s infinite; +} + +.typing-indicator .dot:nth-child(2) { + animation-delay: 0.2s; +} + +.typing-indicator .dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing { + 0%, 60%, 100% { + transform: translateY(0); + opacity: 0.4; + } + 30% { + transform: translateY(-10px); + opacity: 1; + } +} diff --git a/src/main/resources/public/css/welcome.css b/src/main/resources/public/css/welcome.css new file mode 100644 index 0000000..c27aa16 --- /dev/null +++ b/src/main/resources/public/css/welcome.css @@ -0,0 +1,110 @@ +/* OpenSlice Chat Assistant - Welcome Page Styles */ + +body { + font-family: 'Open Sans', sans-serif; + margin: 0; + padding: 60px 0 40px 0; + min-height: 100vh; + background-color: white; +} + +/* Navbar */ +.navbar-default { + background-color: rgba(245, 245, 245, 0.93); + border-bottom: 1px solid #e7e7e7; +} + +.navbar-brand img { + height: 25px; +} + +/* Welcome Section */ +.welcome-section { + background: linear-gradient(135deg, #989DC3 0%, #7EADDB 100%); + padding: 80px 0; + text-align: center; + color: #FCFCFF; + margin-top: 0; + min-height: 500px; +} + +.welcome-section h1 { + font-size: 48px; + font-weight: 300; + margin-bottom: 20px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +.welcome-section .subtitle { + font-size: 22px; + margin-bottom: 30px; + font-weight: 300; +} + +.login-message { + background: rgba(255, 255, 255, 0.95); + color: #d9534f; + padding: 15px 30px; + border-radius: 4px; + display: inline-block; + margin-bottom: 30px; + font-weight: 600; + font-size: 16px; +} + +.btn-login { + background-color: #7EADDB; + color: white; + border: none; + padding: 15px 40px; + font-size: 18px; + font-weight: 600; + border-radius: 4px; + transition: all 0.3s; +} + +.btn-login:hover { + background-color: #6589AB; + color: white; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + transform: translateY(-2px); +} + +/* Features Section */ +.features-section { + background-color: white; + padding: 60px 0; +} + +.features-section h2 { + text-align: center; + font-size: 36px; + font-weight: 300; + margin-bottom: 50px; + color: #333; +} + +.feature-box { + text-align: center; + padding: 30px 20px; + margin-bottom: 30px; +} + +.feature-icon { + font-size: 60px; + margin-bottom: 20px; + color: #7EADDB; +} + +.feature-box h3 { + font-size: 22px; + font-weight: 600; + margin-bottom: 15px; + color: #333; +} + +.feature-box p { + font-size: 15px; + color: #666; + line-height: 1.6; +} diff --git a/src/main/resources/public/images/logo_clear.png b/src/main/resources/public/images/logo_clear.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa5efc135209ce66cee07ea7a76dc0a4cff9244 GIT binary patch literal 16566 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 T_gx)z3S^9@tDnm{r-UW|I%voO literal 0 HcmV?d00001 diff --git a/src/main/resources/public/index.html b/src/main/resources/public/index.html index 79a08c2..688ce83 100644 --- a/src/main/resources/public/index.html +++ b/src/main/resources/public/index.html @@ -3,65 +3,102 @@ - AI Chat - - - + OpenSlice Chat Assistant + + -
-
-
-
- -
-
- Robot Avatar -
-
-
ChatBot
-

Online

-
-
- - -
- -
-
- Robot Avatar -
-
-
- Hi there! How can I help you today? -
-
10:03 AM
-
-
- -
- - -
-
- - -
-
-
-
-
-
+ + + + + + // Login button handler + $('#loginBtn').click(function() { + window.location.href = '/oauth2/authorization/keycloak'; + }); + + // Logout button handler + $('#logoutBtn').click(function() { + $.ajax({ + type: "POST", + url: "/api/logout", + dataType: 'json' + }) + .done(function(response) { + console.log("Logged out:", response); + accessToken = null; + currentUser = null; + updateAuthUI(); + addMessage("You have been logged out.", false); + }) + .fail(function(jqXHR, textStatus, errorThrown) { + console.error("Logout error:", textStatus, errorThrown); + }); + }); + }); - - - + + + + + + + +
+
+

Welcome to OpenSlice Chat Assistant

+

AI-powered assistance for your OpenSlice platform

+ + + +
+ + +
+
+ + +
+
+

What you can do

+
+
+
+
💬
+

Natural Language Queries

+

Ask questions about your services, catalogs, and deployments using natural language. Get instant answers powered by AI.

+
+
+
+
+
🔧
+

Intelligent Tool Execution

+

Let AI help you interact with TMF APIs and manage resources efficiently. Automate complex workflows with simple commands.

+
+
+
+
+
📊
+

Real-time Information

+

Get instant answers about your platform status, resource utilization, and system health in real-time.

+
+
+
+
+
+ + + + + -- GitLab From ce910b543c1527b1b326c75128ebba68f5b786b2 Mon Sep 17 00:00:00 2001 From: denazi Date: Fri, 19 Dec 2025 14:47:48 +0200 Subject: [PATCH 06/21] BE Service #2: Addition of gitlab-ci and Dockerization --- .gitlab-ci.yml | 40 +++++++++++++++++++++++++ Dockerfile | 26 ++++++++-------- docker-compose.yml | 35 ++++++++++++++++++++-- pom.xml | 4 +++ src/main/resources/application.yaml | 46 +++++++++++++---------------- 5 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..40be386 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,40 @@ +include: + - project: osl/code/org.etsi.osl.main + ref: main + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "main"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "develop"' + + - project: osl/code/org.etsi.osl.main + ref: $CI_COMMIT_REF_NAME + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build_unprotected.yml + rules: + - if: '$CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop" && $CI_COMMIT_REF_PROTECTED == "false"' + +maven_build: + extends: .maven_build + +docker_build: + extends: .docker_build + needs: + - maven_build diff --git a/Dockerfile b/Dockerfile index aa6f298..440fed7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,32 @@ -# Build stage +# ---------- Stage 1: Build ---------- FROM maven:3.9-eclipse-temurin-17 AS builder +WORKDIR /app -WORKDIR /build - -# Copy pom.xml and download dependencies +# Copy pom.xml and download dependencies first (cache layer) COPY pom.xml . -RUN mvn dependency:go-offline +RUN mvn dependency:go-offline -B -# Copy source code +# Copy the rest of the source code COPY src ./src -# Build application +# Build the Spring Boot executable JAR RUN mvn clean package -DskipTests -# Runtime stage +# ---------- Stage 2: Runtime ---------- FROM eclipse-temurin:17-jre-alpine -WORKDIR /app +# Create application directory +RUN mkdir -p /opt/openslice/lib/ -# Copy built JAR from builder stage -COPY --from=builder /build/target/org.etsi.osl.mcp.backend-0.0.1-SNAPSHOT.jar app.jar +# Copy the built JAR from the builder stage +COPY --from=builder /app/target/org.etsi.osl.mcp.backend-0.0.1-SNAPSHOT.jar /opt/openslice/lib/app.jar # Expose port EXPOSE 11880 # Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application -ENTRYPOINT ["java", "-jar", "app.jar"] +CMD ["java", "-jar", "/opt/openslice/lib/app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index 58bcfa1..cf9c10a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,17 +29,46 @@ services: ports: - "11880:11880" environment: + # Server Configuration + SERVER_PORT: 11880 + SPRING_APPLICATION_NAME: osl-mcp-backend + + # AI Configuration SPRING_AI_OLLAMA_BASE_URL: http://ollama:11434 - SPRING_AI_MCP_CLIENT_STREAMABLE_HTTP_CONNECTIONS_OPENSLICE_SERVER_URL: http://mcp-server:13015 - SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_AUTHSERVER_ISSUER_URI: http://keycloak:8080/auth/realms/openslice - SPRING_ACTIVEMQ_BROKERURL: tcp://activemq:61616 + SPRING_AI_OLLAMA_CHAT_MODEL: gpt-oss:20b + SPRING_AI_OLLAMA_CHAT_TEMPERATURE: 0.7 + + # MCP Client Configuration + SPRING_AI_MCP_CLIENT_TYPE: SYNC + SPRING_AI_MCP_CLIENT_STREAMABLE_HTTP_CONNECTIONS_OPENSLICE_SERVER_URL: http://openslice-mcp:13015/sse + + # OAuth2/Keycloak Configuration + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/auth/realms/openslice + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KEYCLOAK_ISSUER_URI: http://keycloak:8080/auth/realms/openslice + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENT_ID: osapiWebClientId + + # ActiveMQ Configuration + SPRING_ACTIVEMQ_BROKER_URL: tcp://anartemis:61616?jms.watchTopicAdvisories=false SPRING_ACTIVEMQ_USER: artemis SPRING_ACTIVEMQ_PASSWORD: artemis + + # Logging Configuration + LOGGING_LEVEL_ROOT: INFO + LOGGING_LEVEL_OSL: DEBUG + LOGGING_LEVEL_SPRING_AI: DEBUG + depends_on: + - ollama healthcheck: test: ["CMD", "curl", "-f", "http://localhost:11880/actuator/health"] interval: 30s timeout: 10s retries: 3 + start_period: 60s volumes: ollama_data: + +networks: + default: + name: compose_back + external: true diff --git a/pom.xml b/pom.xml index dd21939..e392e4f 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,10 @@ spring-boot-starter-test test
+ + org.springframework.boot + spring-boot-starter-actuator + diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8333f7b..83819de 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,59 +1,53 @@ server: - port: 11880 + port: ${SERVER_PORT:11880} spring: application: - name: osl-mcp-backend + name: ${SPRING_APPLICATION_NAME:osl-mcp-backend} ai: ollama: - base-url: http://localhost:11434 + base-url: ${SPRING_AI_OLLAMA_BASE_URL:http://localhost:11434} chat: options: - model: gpt-oss:20b - xmodel: qwen3:8b - temperature: 0.7 + model: ${SPRING_AI_OLLAMA_CHAT_MODEL:gpt-oss:20b} + temperature: ${SPRING_AI_OLLAMA_CHAT_TEMPERATURE:0.7} mcp: client: - type: SYNC # or ASYNC + type: ${SPRING_AI_MCP_CLIENT_TYPE:SYNC} # or ASYNC streamable-http: connections: openslice-server: - url: http://localhost:13015 + url: ${SPRING_AI_MCP_CLIENT_STREAMABLE_HTTP_CONNECTIONS_OPENSLICE_SERVER_URL:http://localhost:13015} security: oauth2: resourceserver: jwt: - issuer-uri: http://keycloak:8080/auth/realms/openslice + issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI:http://keycloak:8080/auth/realms/openslice} client: registration: keycloak: - client-id: osapiWebClientId - authorization-grant-type: authorization_code - redirect-uri: "{baseUrl}/login/oauth2/code/keycloak" + client-id: ${SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KEYCLOAK_CLIENT_ID:osapiWebClientId} scope: - openid - profile provider: keycloak provider: keycloak: - issuer-uri: http://keycloak:8080/auth/realms/openslice - authorization-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/auth - token-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/token - user-info-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/userinfo - jwk-set-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/certs + issuer-uri: ${SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KEYCLOAK_ISSUER_URI:http://keycloak:8080/auth/realms/openslice} activemq: - brokerUrl: tcp://localhost:61616?jms.watchTopicAdvisories=false - user: artemis - password: artemis + brokerUrl: ${SPRING_ACTIVEMQ_BROKER_URL:tcp://localhost:61616?jms.watchTopicAdvisories=false} + user: ${SPRING_ACTIVEMQ_USER:artemis} + password: ${SPRING_ACTIVEMQ_PASSWORD:artemis} pool: - enabled: true - max-connections: 100 + enabled: ${SPRING_ACTIVEMQ_POOL_ENABLED:true} + max-connections: ${SPRING_ACTIVEMQ_POOL_MAX_CONNECTIONS:100} packages: - trust-all: true + trust-all: ${SPRING_ACTIVEMQ_PACKAGES_TRUST_ALL:true} + logging: level: - root: INFO - org.etsi.osl.*: DEBUG - org.springframework.ai.chat.client.advisor: DEBUG \ No newline at end of file + root: ${LOGGING_LEVEL_ROOT:INFO} + org.etsi.osl.*: ${LOGGING_LEVEL_OSL:DEBUG} + org.springframework.ai.chat.client.advisor: ${LOGGING_LEVEL_SPRING_AI:DEBUG} \ No newline at end of file -- GitLab From e62b8c08fbde27d056252ddadf1a05544ea51c80 Mon Sep 17 00:00:00 2001 From: denazi Date: Fri, 19 Dec 2025 14:55:41 +0200 Subject: [PATCH 07/21] BE Service #2: Addition of ci_settings --- ci_settings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 ci_settings.xml diff --git a/ci_settings.xml b/ci_settings.xml new file mode 100644 index 0000000..69ad06e --- /dev/null +++ b/ci_settings.xml @@ -0,0 +1,16 @@ + + + + gitlab-maven + + + + Job-Token + ${CI_JOB_TOKEN} + + + + + + -- GitLab From 6b6a38f7ce8292cba256461fe8cc8d1b16f15f13 Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Mon, 22 Dec 2025 13:21:07 +0000 Subject: [PATCH 08/21] adding main pom dependency and distribution management --- pom.xml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index e392e4f..ddfaafc 100644 --- a/pom.xml +++ b/pom.xml @@ -6,15 +6,15 @@ 4.0.0 - org.springframework.boot - spring-boot-starter-parent - 3.5.5 - + org.etsi.osl + org.etsi.osl.main + 2025Q4-SNAPSHOT + ../org.etsi.osl.main org.etsi.osl org.etsi.osl.mcp.backend - 0.0.1-SNAPSHOT + ${org.etsi.osl.mcp.backend.version} org.etsi.osl.mcp.backend @@ -28,6 +28,24 @@ 4.0.0-RC2 + + + gitlab-maven + https://labs.etsi.org/rep/api/v4/groups/260/-/packages/maven + + + + + gitlab-maven + ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/maven + + + gitlab-maven + ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/maven + + + + -- GitLab From 183c5139194f229e8e473492855b58bafae2503c Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Mon, 22 Dec 2025 13:22:41 +0000 Subject: [PATCH 09/21] fixing main pom's relative path --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ddfaafc..a68d30e 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ org.etsi.osl org.etsi.osl.main 2025Q4-SNAPSHOT - ../org.etsi.osl.main + ../org.etsi.osl.main org.etsi.osl -- GitLab From 39ec8d082790634357f92b4d99c123c180b0c11f Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Mon, 22 Dec 2025 13:37:45 +0000 Subject: [PATCH 10/21] Adding spring boot dependencies --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index a68d30e..ab6b14a 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,14 @@ + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot-version} + pom + import + org.springframework.ai spring-ai-bom -- GitLab From a025e87c1432b97ab6be2ee73cc1492f05b996f0 Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Mon, 22 Dec 2025 14:08:16 +0000 Subject: [PATCH 11/21] adding spring boot version in pom --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index ab6b14a..3369563 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ + 3.5.5 17 1.1.0 4.0.0-RC2 -- GitLab From 16f28557ef34d7d962a0fd6e34e799fde4cbf907 Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Mon, 22 Dec 2025 14:17:14 +0000 Subject: [PATCH 12/21] change Dockerfile to accept the jar file from the target dir (previous pipeline stage) --- Dockerfile | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 440fed7..c09bfca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,12 @@ -# ---------- Stage 1: Build ---------- -FROM maven:3.9-eclipse-temurin-17 AS builder -WORKDIR /app - -# Copy pom.xml and download dependencies first (cache layer) -COPY pom.xml . -RUN mvn dependency:go-offline -B - -# Copy the rest of the source code -COPY src ./src - -# Build the Spring Boot executable JAR -RUN mvn clean package -DskipTests - -# ---------- Stage 2: Runtime ---------- FROM eclipse-temurin:17-jre-alpine +MAINTAINER osl.etsi.org + # Create application directory RUN mkdir -p /opt/openslice/lib/ -# Copy the built JAR from the builder stage -COPY --from=builder /app/target/org.etsi.osl.mcp.backend-0.0.1-SNAPSHOT.jar /opt/openslice/lib/app.jar - +# Copy the built JAR from a previous pipeline +COPY /target/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar /opt/openslice/lib # Expose port EXPOSE 11880 @@ -29,4 +15,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application -CMD ["java", "-jar", "/opt/openslice/lib/app.jar"] +CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar"] -- GitLab From 17601a526b989ae9eb10e42bdc7703fb9fcfaebc Mon Sep 17 00:00:00 2001 From: Irene Denazi Date: Tue, 23 Dec 2025 11:29:26 +0000 Subject: [PATCH 13/21] Fix pom.xml build --- pom.xml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 3369563..d68a6d7 100644 --- a/pom.xml +++ b/pom.xml @@ -166,13 +166,21 @@
- - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot-version} + + + + repackage + + + + + + -- GitLab From 7e86aa42d1610797b9b3171c762f8210a30506cd Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Sun, 11 Jan 2026 22:35:49 +0000 Subject: [PATCH 14/21] adding restart always clause to mcp backend container --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index cf9c10a..4c5ef6d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ services: context: . dockerfile: Dockerfile container_name: osl-mcp-backend + restart: always ports: - "11880:11880" environment: -- GitLab From 983f670d3d14490d679e6e68c7bb52d757691313 Mon Sep 17 00:00:00 2001 From: denazi Date: Fri, 16 Jan 2026 12:48:53 +0200 Subject: [PATCH 15/21] Issue #3: Addition of Initial Prompt --- docker-compose.yml | 1 + .../etsi/osl/mcp/backend/ChatController.java | 20 +++++++++++++++---- src/main/resources/application.yaml | 2 ++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4c5ef6d..fd952ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: SPRING_AI_OLLAMA_BASE_URL: http://ollama:11434 SPRING_AI_OLLAMA_CHAT_MODEL: gpt-oss:20b SPRING_AI_OLLAMA_CHAT_TEMPERATURE: 0.7 + SPRING_AI_CHAT_SYSTEM_PROMPT: "You are an OpenSlice AI assistant" # MCP Client Configuration SPRING_AI_MCP_CLIENT_TYPE: SYNC diff --git a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java index 2d9fbcc..ff0bc74 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java +++ b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java @@ -1,27 +1,39 @@ package org.etsi.osl.mcp.backend; import java.util.Map; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.tool.ToolCallbackProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; -import org.springframework.web.bind.annotation.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; @RestController public class ChatController { private static final Logger log = LoggerFactory.getLogger(ChatController.class); private final ChatClient chatClient; + private final String systemPrompt; public ChatController(ChatClient.Builder chatClientBuilder, - ToolCallbackProvider tools) { + ToolCallbackProvider tools, + @Value("${spring.ai.chat.system-prompt}") String systemPrompt) { + // 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; this.chatClient = chatClientBuilder .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultToolCallbacks(tools) + .defaultSystem(systemPrompt) .build(); + log.info("ChatController initialized with system prompt: {}", systemPrompt); } @PostMapping("/ask") diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 83819de..a485f68 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -12,6 +12,8 @@ spring: options: model: ${SPRING_AI_OLLAMA_CHAT_MODEL:gpt-oss:20b} temperature: ${SPRING_AI_OLLAMA_CHAT_TEMPERATURE:0.7} + chat: + system-prompt: ${SPRING_AI_CHAT_SYSTEM_PROMPT:You are an OpenSlice AI assistant.} mcp: client: type: ${SPRING_AI_MCP_CLIENT_TYPE:SYNC} # or ASYNC -- GitLab From e461af67b3cec53fca21b9dfd045b5f7583cf7be Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Fri, 16 Jan 2026 17:31:43 +0200 Subject: [PATCH 16/21] prosektikos maxMessages --- .../etsi/osl/mcp/backend/ChatController.java | 22 +++++++++++++++++-- src/main/resources/application.yaml | 1 + 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java index ff0bc74..bd3e05b 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java +++ b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java @@ -1,27 +1,43 @@ package org.etsi.osl.mcp.backend; import java.util.Map; - +import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; 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.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.annotation.SessionScope; @RestController +@SessionScope public class ChatController { private static final Logger log = LoggerFactory.getLogger(ChatController.class); private final ChatClient chatClient; private final String systemPrompt; + private final String conversationId; + + @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, - @Value("${spring.ai.chat.system-prompt}") String systemPrompt) { + @Value("${spring.ai.chat.system-prompt}") String systemPrompt, + ChatMemory chatMemory) { // 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."); @@ -34,12 +50,14 @@ public class ChatController { .defaultSystem(systemPrompt) .build(); log.info("ChatController initialized with system prompt: {}", systemPrompt); + this.conversationId = UUID.randomUUID().toString(); } @PostMapping("/ask") public Answer ask(@RequestBody Question question) { var response = chatClient.prompt() .user(question.question()) + .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); log.debug("Response from AI: {}", response); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a485f68..cdd6bb0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -14,6 +14,7 @@ spring: temperature: ${SPRING_AI_OLLAMA_CHAT_TEMPERATURE:0.7} chat: system-prompt: ${SPRING_AI_CHAT_SYSTEM_PROMPT:You are an OpenSlice AI assistant.} + maxMessages: ${SPRING_AI_CHAT_MAX_MESSAGES:100} mcp: client: type: ${SPRING_AI_MCP_CLIENT_TYPE:SYNC} # or ASYNC -- GitLab From d4151e37a9fe308790ea3d337c48087af4c9a914 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Sat, 17 Jan 2026 09:42:48 +0200 Subject: [PATCH 17/21] prosektikos strikes back --- src/main/java/org/etsi/osl/mcp/backend/ChatController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java index bd3e05b..dc1e8d6 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java +++ b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java @@ -5,6 +5,7 @@ import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; @@ -45,7 +46,7 @@ public class ChatController { this.systemPrompt = systemPrompt; this.chatClient = chatClientBuilder - .defaultAdvisors(new SimpleLoggerAdvisor()) + .defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build()) .defaultToolCallbacks(tools) .defaultSystem(systemPrompt) .build(); -- GitLab From aeb03f4160b08fadb2fd3b9ad2dd24d3755a083d Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Wed, 21 Jan 2026 18:53:15 +0000 Subject: [PATCH 18/21] Return raw markdown instead of HTML conversion (fix for #5) --- src/main/java/org/etsi/osl/mcp/backend/ChatController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java index dc1e8d6..587f4e4 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/ChatController.java +++ b/src/main/java/org/etsi/osl/mcp/backend/ChatController.java @@ -64,9 +64,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(htmlResponse); + // var htmlResponse = MarkdownHelper.toHTML(markdownAnswer); + // log.debug("HTML formatted response: {}", htmlResponse); + return new Answer(markdownAnswer); } -- GitLab From 0ba902cb89103e3d52313d30b8267a4a83842643 Mon Sep 17 00:00:00 2001 From: denazi Date: Thu, 22 Jan 2026 17:01:42 +0200 Subject: [PATCH 19/21] Issue #6: Fix Authorization for BE Service --- Dockerfile | 26 ++++++-- .../etsi/osl/mcp/backend/AuthController.java | 36 +++++++---- .../backend/WebSecurityConfigKeycloak.java | 64 +++++++++++++------ .../configuration/McpConfiguration.java | 30 ++++----- 4 files changed, 103 insertions(+), 53 deletions(-) diff --git a/Dockerfile b/Dockerfile index c09bfca..dc10fc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,30 @@ -FROM eclipse-temurin:17-jre-alpine +# Build stage +FROM maven:3.9-eclipse-temurin-17 AS build MAINTAINER osl.etsi.org +# Set working directory +WORKDIR /app + +# Copy pom.xml and download dependencies +COPY pom.xml . +RUN mvn dependency:go-offline -B || true + +# Copy source code +COPY src ./src + +# Build the application +RUN mvn clean package -DskipTests -B + +# Runtime stage +FROM eclipse-temurin:17-jre-alpine +MAINTAINER osl.etsi.org # Create application directory RUN mkdir -p /opt/openslice/lib/ -# Copy the built JAR from a previous pipeline -COPY /target/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar /opt/openslice/lib +# Copy the built JAR from build stage +COPY --from=build /app/target/org.etsi.osl.mcp.backend-*.jar /opt/openslice/lib/org.etsi.osl.mcp.backend.jar + # Expose port EXPOSE 11880 @@ -15,4 +33,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application -CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar"] +CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend.jar"] diff --git a/src/main/java/org/etsi/osl/mcp/backend/AuthController.java b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java index 1905527..16a4c10 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/AuthController.java +++ b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java @@ -1,11 +1,14 @@ package org.etsi.osl.mcp.backend; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; @@ -13,19 +16,20 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; @RestController public class AuthController { @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") private String issuerUri; + + @Autowired(required = false) + private OAuth2AuthorizedClientService authorizedClientService; @GetMapping("/api/user") - public Map getUserInfo(Authentication authentication, - @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { + public Map getUserInfo(Authentication authentication) { Map userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { @@ -42,9 +46,19 @@ public class AuthController { } userInfo.put("username", username); - if (authorizedClient != null && authorizedClient.getAccessToken() != null) { - userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); - userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); + // Try to get OAuth2AuthorizedClient if available (from OAuth2 login) + if (authorizedClientService != null && authentication.getName() != null) { + try { + OAuth2AuthorizedClient authorizedClient = + authorizedClientService.loadAuthorizedClient("keycloak", authentication.getName()); + if (authorizedClient != null && authorizedClient.getAccessToken() != null) { + userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); + userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); + } + } catch (Exception e) { + // OAuth2 client not available - user authenticated via JWT Bearer token + // This is normal for API requests with Bearer tokens + } } } else { userInfo.put("authenticated", false); diff --git a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java index 744cd36..51fda1a 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +++ b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java @@ -1,40 +1,66 @@ package org.etsi.osl.mcp.backend; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -import org.springframework.security.config.Customizer; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.http.HttpStatus; @Configuration @EnableWebSecurity @Profile("!testing") public class WebSecurityConfigKeycloak { + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUri; + + // Security filter chain for JWT Bearer token only (API endpoints) + @Bean + @Order(1) + SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { + return http + .securityMatcher("/ask/**") + .authorizeHttpRequests(auth -> auth + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + ) + .csrf(CsrfConfigurer::disable) + .build(); + } + + // Security filter chain for OAuth2 login (UI) @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + @Order(2) + SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // Public static resources .requestMatchers("/", "/index.html", "/styles.css", "/robot.svg", "/favicon.ico").permitAll() .requestMatchers("/api/health").permitAll() - .requestMatchers("/api/user").permitAll() - .requestMatchers("/api/logout").permitAll() - .requestMatchers("/logout").permitAll() - .requestMatchers("/ask", "/api/**").authenticated() + // UI-only endpoints (OAuth2 login) + .requestMatchers("/api/user", "/api/logout", "/logout").permitAll() .anyRequest().permitAll() ) - // OAuth2 Login for browser-based authentication + // OAuth2 Login for browser-based authentication (UI) .oauth2Login(oauth2 -> oauth2 .defaultSuccessUrl("/", true) ) @@ -45,17 +71,6 @@ public class WebSecurityConfigKeycloak { .invalidateHttpSession(true) .deleteCookies("JSESSIONID") ) - .oauth2Client(Customizer.withDefaults()) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) - .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) - ) - .exceptionHandling(exception -> exception - .defaultAuthenticationEntryPointFor( - new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), - new AntPathRequestMatcher("/ask") - ) - ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ) @@ -73,4 +88,11 @@ public class WebSecurityConfigKeycloak { jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } + + @Bean + public JwtDecoder jwtDecoder() { + // Eagerly initialize the JWT decoder on application startup + // This fetches the JWKS from Keycloak immediately + return NimbusJwtDecoder.withJwkSetUri(issuerUri + "/protocol/openid-connect/certs").build(); + } } diff --git a/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java b/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java index d446dc1..87ca06c 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java +++ b/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java @@ -1,13 +1,6 @@ package org.etsi.osl.mcp.backend.configuration; -import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; -import org.springaicommunity.mcp.security.client.sync.AuthenticationMcpTransportContextProvider; -import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2AuthorizationCodeSyncHttpRequestCustomizer; - -import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; @@ -17,17 +10,20 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg @Configuration class McpConfiguration { - @Bean - McpSyncHttpClientRequestCustomizer requestCustomizer(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, - ClientRegistrationRepository clientRegistrationRepository) { - var registrationId = findUniqueClientRegistration(clientRegistrationRepository); - return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(oAuth2AuthorizedClientManager, registrationId); - } + // Disabled OAuth2 customization for MCP client - not needed for JWT-based API + // The MCP server connection doesn't need OAuth2 authentication + + // @Bean + // McpSyncHttpClientRequestCustomizer requestCustomizer(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, + // ClientRegistrationRepository clientRegistrationRepository) { + // var registrationId = findUniqueClientRegistration(clientRegistrationRepository); + // return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(oAuth2AuthorizedClientManager, registrationId); + // } - @Bean - McpSyncClientCustomizer syncClientCustomizer() { - return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); - } + // @Bean + // McpSyncClientCustomizer syncClientCustomizer() { + // return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); + // } /** * Returns the ID of the {@code spring.security.oauth2.client.registration}, if -- GitLab From e222f3b128249ca1a40d945860f1b3a4a7d4f2dc Mon Sep 17 00:00:00 2001 From: denazi Date: Thu, 22 Jan 2026 17:11:13 +0200 Subject: [PATCH 20/21] Revert Dockerfile --- Dockerfile | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index dc10fc3..c09bfca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,12 @@ -# Build stage -FROM maven:3.9-eclipse-temurin-17 AS build -MAINTAINER osl.etsi.org - -# Set working directory -WORKDIR /app - -# Copy pom.xml and download dependencies -COPY pom.xml . -RUN mvn dependency:go-offline -B || true - -# Copy source code -COPY src ./src - -# Build the application -RUN mvn clean package -DskipTests -B - -# Runtime stage FROM eclipse-temurin:17-jre-alpine MAINTAINER osl.etsi.org + # Create application directory RUN mkdir -p /opt/openslice/lib/ -# Copy the built JAR from build stage -COPY --from=build /app/target/org.etsi.osl.mcp.backend-*.jar /opt/openslice/lib/org.etsi.osl.mcp.backend.jar - +# Copy the built JAR from a previous pipeline +COPY /target/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar /opt/openslice/lib # Expose port EXPOSE 11880 @@ -33,4 +15,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application -CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend.jar"] +CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar"] -- GitLab From fd2c4e6f0ea3d0946c6bfe6d53acba85a78e3fe5 Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Sat, 24 Jan 2026 21:41:02 +0000 Subject: [PATCH 21/21] Preparation for Release 2025Q4 --- Dockerfile | 4 ++-- pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c09bfca..c297914 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ MAINTAINER osl.etsi.org RUN mkdir -p /opt/openslice/lib/ # Copy the built JAR from a previous pipeline -COPY /target/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar /opt/openslice/lib +COPY /target/org.etsi.osl.mcp.backend-1.0.0.jar /opt/openslice/lib # Expose port EXPOSE 11880 @@ -15,4 +15,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application -CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar"] +CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend-1.0.0.jar"] diff --git a/pom.xml b/pom.xml index d68a6d7..625eeb5 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ org.etsi.osl org.etsi.osl.main - 2025Q4-SNAPSHOT + 2025Q4 ../org.etsi.osl.main -- GitLab