From 3bd09b945272343ef1bbc79e9fcf5eef788aaad3 Mon Sep 17 00:00:00 2001
From: Denislav Prinov
Date: Mon, 12 Jan 2026 16:47:35 +0200
Subject: [PATCH 1/5] hawkBit MCP server
Signed-off-by: Denislav Prinov
---
hawkbit-mcp/Dockerfile | 25 +
hawkbit-mcp/pom.xml | 130 ++++
.../server/HawkBitMcpServerApplication.java | 32 +
.../HawkBitAuthenticationValidator.java | 157 +++++
.../client/McpAuthenticationException.java | 17 +
.../config/HawkBitClientConfiguration.java | 89 +++
.../server/config/HawkBitMcpProperties.java | 200 +++++++
.../config/McpSecurityConfiguration.java | 122 ++++
.../server/config/McpToolConfiguration.java | 83 +++
.../mcp/server/dto/ActionOperation.java | 18 +
.../hawkbit/mcp/server/dto/ListRequest.java | 42 ++
.../mcp/server/dto/ManageActionRequest.java | 28 +
.../dto/ManageDistributionSetRequest.java | 30 +
.../mcp/server/dto/ManageRolloutRequest.java | 32 +
.../dto/ManageSoftwareModuleRequest.java | 30 +
.../server/dto/ManageTargetFilterRequest.java | 26 +
.../mcp/server/dto/ManageTargetRequest.java | 26 +
.../hawkbit/mcp/server/dto/Operation.java | 19 +
.../mcp/server/dto/OperationResponse.java | 55 ++
.../hawkbit/mcp/server/dto/PagedResponse.java | 29 +
.../mcp/server/dto/RolloutOperation.java | 27 +
.../server/prompts/HawkBitPromptProvider.java | 67 +++
.../HawkBitDocumentationResource.java | 201 +++++++
.../server/tools/HawkBitMcpToolProvider.java | 553 ++++++++++++++++++
.../src/main/resources/application.properties | 51 ++
.../resources/hawkbit-entity-definitions.md | 351 +++++++++++
.../main/resources/prompts/hawkbit-context.md | 48 ++
.../src/main/resources/prompts/rsql-help.md | 51 ++
.../src/main/resources/application.properties | 1 +
pom.xml | 11 +
30 files changed, 2551 insertions(+)
create mode 100644 hawkbit-mcp/Dockerfile
create mode 100644 hawkbit-mcp/pom.xml
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkBitMcpServerApplication.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkBitAuthenticationValidator.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitClientConfiguration.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitMcpProperties.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkBitPromptProvider.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkBitDocumentationResource.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkBitMcpToolProvider.java
create mode 100644 hawkbit-mcp/src/main/resources/application.properties
create mode 100644 hawkbit-mcp/src/main/resources/hawkbit-entity-definitions.md
create mode 100644 hawkbit-mcp/src/main/resources/prompts/hawkbit-context.md
create mode 100644 hawkbit-mcp/src/main/resources/prompts/rsql-help.md
diff --git a/hawkbit-mcp/Dockerfile b/hawkbit-mcp/Dockerfile
new file mode 100644
index 0000000000..b25b5672a0
--- /dev/null
+++ b/hawkbit-mcp/Dockerfile
@@ -0,0 +1,25 @@
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# This program and the accompanying materials are made
+# available under the terms of the Eclipse Public License 2.0
+# which is available at https://www.eclipse.org/legal/epl-2.0/
+#
+# SPDX-License-Identifier: EPL-2.0
+#
+
+FROM eclipse-temurin:17-jre-alpine
+
+LABEL maintainer="Eclipse hawkBit Project"
+LABEL org.opencontainers.image.source="https://github.com/eclipse-hawkbit/hawkbit"
+LABEL org.opencontainers.image.description="Standalone MCP Server for hawkBit"
+
+ARG JAR_FILE=target/*.jar
+
+COPY ${JAR_FILE} app.jar
+
+EXPOSE 8081
+
+ENV HAWKBIT_URL=http://localhost:8080
+
+ENTRYPOINT ["java", "-jar", "/app.jar"]
diff --git a/hawkbit-mcp/pom.xml b/hawkbit-mcp/pom.xml
new file mode 100644
index 0000000000..db8048aa65
--- /dev/null
+++ b/hawkbit-mcp/pom.xml
@@ -0,0 +1,130 @@
+
+
+ 4.0.0
+
+ org.eclipse.hawkbit
+ hawkbit-parent
+ ${revision}
+
+
+ hawkbit-mcp-server
+ hawkBit :: MCP Server (Standalone)
+ Standalone MCP server that connects to hawkBit via REST API
+
+
+
+ org.eclipse.hawkbit
+ hawkbit-sdk-mgmt
+ ${project.version}
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.ai
+ spring-ai-starter-mcp-server-webmvc
+
+
+ org.springframework.ai
+ spring-ai-mcp-annotations
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+
+
+ copy-hawkbit-docs
+ generate-resources
+
+ copy-resources
+
+
+ ${project.build.outputDirectory}/hawkbit-docs
+
+
+ ${project.basedir}/../docs
+
+ README.md
+ what-is-hawkbit.md
+ quick-start.md
+ features.md
+ architecture.md
+ base-setup.md
+ hawkbit-sdk.md
+ feign-client.md
+ clustering.md
+ authentication.md
+ authorization.md
+ datamodel.md
+ rollout-management.md
+ targetstate.md
+ management-api.md
+ direct-device-integration-api.md
+ device-management-federation-api.md
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ org.eclipse.hawkbit.mcp.server.HawkBitMcpServerApplication
+
+
+
+
+ repackage
+
+
+
+
+
+
+
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkBitMcpServerApplication.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkBitMcpServerApplication.java
new file mode 100644
index 0000000000..f7d4073635
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkBitMcpServerApplication.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server;
+
+import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
+
+/**
+ * Standalone MCP Server application that connects to hawkBit via REST API.
+ *
+ * This server acts as a proxy between MCP clients and hawkBit,
+ * passing through authentication credentials to the hawkBit REST API.
+ *
+ */
+@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
+@EnableConfigurationProperties(HawkBitMcpProperties.class)
+public class HawkBitMcpServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(HawkBitMcpServerApplication.class, args);
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkBitAuthenticationValidator.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkBitAuthenticationValidator.java
new file mode 100644
index 0000000000..f73d129c7b
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkBitAuthenticationValidator.java
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.client;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import feign.FeignException;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
+
+/**
+ * Validates authentication credentials against hawkBit REST API using the SDK.
+ */
+@Slf4j
+@Component
+public class HawkBitAuthenticationValidator {
+
+ private final HawkbitClient hawkbitClient;
+ private final Tenant dummyTenant;
+ private final Cache validationCache;
+ private final boolean enabled;
+
+ public HawkBitAuthenticationValidator(HawkbitClient hawkbitClient,
+ Tenant dummyTenant,
+ HawkBitMcpProperties properties) {
+ this.hawkbitClient = hawkbitClient;
+ this.dummyTenant = dummyTenant;
+ this.enabled = properties.getValidation().isEnabled();
+
+ this.validationCache = Caffeine.newBuilder()
+ .expireAfterWrite(properties.getValidation().getCacheTtl())
+ .maximumSize(properties.getValidation().getCacheMaxSize())
+ .build();
+
+ log.info("Authentication validation {} with cache TTL={}, maxSize={}",
+ enabled ? "enabled" : "disabled",
+ properties.getValidation().getCacheTtl(),
+ properties.getValidation().getCacheMaxSize());
+ }
+
+ /**
+ * Validates the given authorization header against hawkBit.
+ * @param authHeader the Authorization header value
+ * @return validation result
+ */
+ public ValidationResult validate(String authHeader) {
+ if (!enabled) {
+ return ValidationResult.VALID;
+ }
+
+ if (authHeader == null || authHeader.isBlank()) {
+ return ValidationResult.MISSING_CREDENTIALS;
+ }
+
+ String cacheKey = hashAuthHeader(authHeader);
+ Boolean cachedResult = validationCache.getIfPresent(cacheKey);
+
+ if (cachedResult != null) {
+ log.debug("Authentication validation cache hit: valid={}", cachedResult);
+ return cachedResult ? ValidationResult.VALID : ValidationResult.INVALID_CREDENTIALS;
+ }
+
+ return validateWithHawkBit(cacheKey);
+ }
+
+ private ValidationResult validateWithHawkBit(String cacheKey) {
+ log.debug("Validating authentication against hawkBit using SDK");
+
+ try {
+ MgmtTenantManagementRestApi tenantApi = hawkbitClient.mgmtService(
+ MgmtTenantManagementRestApi.class, dummyTenant);
+
+ ResponseEntity> response = tenantApi.getTenantConfiguration();
+ int statusCode = response.getStatusCode().value();
+
+ if (statusCode >= 200 && statusCode < 300) {
+ log.debug("Authentication valid (status={})", statusCode);
+ validationCache.put(cacheKey, true);
+ return ValidationResult.VALID;
+ } else {
+ log.warn("Unexpected status from hawkBit during auth validation: {}", statusCode);
+ return ValidationResult.HAWKBIT_ERROR;
+ }
+ } catch (FeignException.Unauthorized e) {
+ log.debug("Authentication invalid (status=401)");
+ validationCache.put(cacheKey, false);
+ return ValidationResult.INVALID_CREDENTIALS;
+ } catch (FeignException.Forbidden e) {
+ // 403 = Valid credentials but lacks READ_TENANT_CONFIGURATION permission
+ // User is authenticated in hawkBit but doesn't have this specific permission
+ log.debug("Authentication valid but lacks permission (status=403)");
+ validationCache.put(cacheKey, true);
+ return ValidationResult.VALID;
+ } catch (FeignException e) {
+ log.warn("Error validating authentication against hawkBit: {} - {}",
+ e.getClass().getSimpleName(), e.getMessage());
+ return ValidationResult.HAWKBIT_ERROR;
+ } catch (Exception e) {
+ // Unexpected errors, don't cache, fail closed
+ log.warn("Unexpected error validating authentication against hawkBit: {}", e.getMessage());
+ return ValidationResult.HAWKBIT_ERROR;
+ }
+ }
+
+ private String hashAuthHeader(String authHeader) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(authHeader.getBytes(StandardCharsets.UTF_8));
+ return HexFormat.of().formatHex(hash);
+ } catch (NoSuchAlgorithmException e) {
+ // SHA-256 is always available
+ throw new McpAuthenticationException("SHA-256 not available." + e.getMessage());
+ }
+ }
+
+ /**
+ * Result of authentication validation.
+ */
+ public enum ValidationResult {
+ /**
+ * Credentials are valid (authenticated user).
+ */
+ VALID,
+
+ /**
+ * No credentials provided.
+ */
+ MISSING_CREDENTIALS,
+
+ /**
+ * Credentials are invalid (401 from hawkBit).
+ */
+ INVALID_CREDENTIALS,
+
+ /**
+ * hawkBit is unavailable or returned unexpected error.
+ */
+ HAWKBIT_ERROR
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java
new file mode 100644
index 0000000000..8fd631bfd7
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.client;
+
+public class McpAuthenticationException extends RuntimeException {
+
+ public McpAuthenticationException(String message) {
+ super(message);
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitClientConfiguration.java
new file mode 100644
index 0000000000..b334743fff
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitClientConfiguration.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.config;
+
+import feign.Contract;
+import feign.RequestInterceptor;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.hawkbit.sdk.Controller;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
+import org.eclipse.hawkbit.sdk.HawkbitServer;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import java.util.function.BiFunction;
+
+/**
+ * Configuration for the hawkBit SDK client.
+ *
+ * Uses a custom request interceptor to inject authentication from the
+ * current HTTP request context, enabling per-request credentials.
+ *
+ */
+@Slf4j
+@Configuration
+public class HawkBitClientConfiguration {
+
+ @Bean
+ @Primary
+ public HawkbitServer hawkbitServer(final HawkBitMcpProperties properties) {
+ HawkbitServer server = new HawkbitServer();
+ server.setMgmtUrl(properties.getMgmtUrl());
+ log.info("Configured hawkBit server URL: {}", properties.getMgmtUrl());
+ return server;
+ }
+
+ @Bean
+ public HawkbitClient hawkbitClient(final HawkbitServer server,
+ final Encoder encoder,
+ final Decoder decoder,
+ final Contract contract) {
+ return HawkbitClient.builder()
+ .hawkBitServer(server)
+ .encoder(encoder)
+ .decoder(decoder)
+ .contract(contract)
+ .requestInterceptorFn(requestContextInterceptor())
+ .build();
+ }
+
+ private BiFunction requestContextInterceptor() {
+ return (tenant, controller) -> template -> {
+ RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
+ if (attrs != null) {
+ String authHeader = (String) attrs.getAttribute(
+ McpSecurityConfiguration.AUTH_HEADER_ATTRIBUTE,
+ RequestAttributes.SCOPE_REQUEST);
+ if (authHeader != null) {
+ template.header(HttpHeaders.AUTHORIZATION, authHeader);
+ log.trace("Injected auth header from request context");
+ }
+ }
+ };
+ }
+
+ /**
+ * Dummy tenant bean - actual authentication comes from request context via interceptor.
+ */
+ @Bean
+ public Tenant dummyTenant() {
+ Tenant tenant = new Tenant();
+ tenant.setUsername(null);
+ tenant.setPassword(null);
+ return tenant;
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitMcpProperties.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitMcpProperties.java
new file mode 100644
index 0000000000..5b99065175
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitMcpProperties.java
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.config;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.Duration;
+
+/**
+ * Configuration properties for the standalone hawkBit MCP server.
+ */
+@Data
+@Validated
+@ConfigurationProperties(prefix = "hawkbit.mcp")
+public class HawkBitMcpProperties {
+
+ /**
+ * Base URL of the hawkBit Management API (e.g., ... ).
+ */
+ @NotBlank(message = "hawkbit.mcp.mgmt-url must be configured")
+ private String mgmtUrl;
+
+ /**
+ * Whether to enable the built-in hawkBit tools.
+ * Set to false to provide custom tool implementations.
+ */
+ private boolean toolsEnabled = true;
+
+ /**
+ * Whether to enable the built-in hawkBit documentation resources.
+ * Set to false to provide custom resource implementations.
+ */
+ private boolean resourcesEnabled = true;
+
+ /**
+ * Whether to enable the built-in hawkBit prompts.
+ * Set to false to provide custom prompt implementations.
+ */
+ private boolean promptsEnabled = true;
+
+ /**
+ * Authentication validation configuration.
+ */
+ private Validation validation = new Validation();
+
+ /**
+ * Operations configuration for enabling/disabling specific operations.
+ */
+ private Operations operations = new Operations();
+
+ /**
+ * Configuration for pre-authentication validation against hawkBit.
+ */
+ @Data
+ public static class Validation {
+
+ /**
+ * Whether to validate authentication against hawkBit before processing MCP requests.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Duration to cache authentication validation results.
+ * Shorter values are more secure but increase load on hawkBit.
+ */
+ private Duration cacheTtl = Duration.ofSeconds(60);
+
+ /**
+ * Maximum number of entries in the authentication validation cache.
+ */
+ private int cacheMaxSize = 1000;
+ }
+
+ /**
+ * Configuration for enabling/disabling operations at global and per-entity levels.
+ */
+ @Data
+ public static class Operations {
+
+ // Global defaults
+ private boolean listEnabled = true;
+ private boolean createEnabled = true;
+ private boolean updateEnabled = true;
+ private boolean deleteEnabled = true;
+
+ // Per-entity overrides (null = use global)
+ private EntityConfig targets = new EntityConfig();
+ private RolloutConfig rollouts = new RolloutConfig();
+ private EntityConfig distributionSets = new EntityConfig();
+ private ActionConfig actions = new ActionConfig();
+ private EntityConfig softwareModules = new EntityConfig();
+ private EntityConfig targetFilters = new EntityConfig();
+
+ /**
+ * Check if an operation is enabled globally.
+ */
+ public boolean isGlobalOperationEnabled(final String operation) {
+ return switch (operation.toLowerCase()) {
+ case "list" -> listEnabled;
+ case "create" -> createEnabled;
+ case "update" -> updateEnabled;
+ case "delete" -> deleteEnabled;
+ default -> true;
+ };
+ }
+ }
+
+ /**
+ * Per-entity operation configuration.
+ */
+ @Data
+ public static class EntityConfig {
+
+ private Boolean listEnabled;
+ private Boolean createEnabled;
+ private Boolean updateEnabled;
+ private Boolean deleteEnabled;
+
+ /**
+ * Get the enabled state for an operation, or null if not set (use global).
+ */
+ public Boolean getOperationEnabled(final String operation) {
+ return switch (operation.toLowerCase()) {
+ case "list" -> listEnabled;
+ case "create" -> createEnabled;
+ case "update" -> updateEnabled;
+ case "delete" -> deleteEnabled;
+ default -> null;
+ };
+ }
+ }
+
+ /**
+ * Rollout-specific operation configuration including lifecycle operations.
+ */
+ @Data
+ public static class RolloutConfig extends EntityConfig {
+
+ private Boolean startEnabled = true;
+ private Boolean pauseEnabled = true;
+ private Boolean stopEnabled = true;
+ private Boolean resumeEnabled = true;
+ private Boolean approveEnabled = true;
+ private Boolean denyEnabled = true;
+ private Boolean retryEnabled = true;
+ private Boolean triggerNextGroupEnabled = true;
+
+ @Override
+ public Boolean getOperationEnabled(final String operation) {
+ final Boolean baseResult = super.getOperationEnabled(operation);
+ if (baseResult != null) {
+ return baseResult;
+ }
+ return switch (operation.toLowerCase().replace("_", "-")) {
+ case "start" -> startEnabled;
+ case "pause" -> pauseEnabled;
+ case "stop" -> stopEnabled;
+ case "resume" -> resumeEnabled;
+ case "approve" -> approveEnabled;
+ case "deny" -> denyEnabled;
+ case "retry" -> retryEnabled;
+ case "trigger-next-group" -> triggerNextGroupEnabled;
+ default -> null;
+ };
+ }
+ }
+
+ /**
+ * Action-specific operation configuration.
+ */
+ @Data
+ public static class ActionConfig {
+
+ private Boolean listEnabled;
+ private Boolean deleteEnabled;
+ private Boolean deleteBatchEnabled = true;
+
+ /**
+ * Get the enabled state for an operation, or null if not set (use global).
+ */
+ public Boolean getOperationEnabled(final String operation) {
+ return switch (operation.toLowerCase().replace("_", "-")) {
+ case "list" -> listEnabled;
+ case "delete" -> deleteEnabled;
+ case "delete-batch" -> deleteBatchEnabled;
+ default -> null;
+ };
+ }
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
new file mode 100644
index 0000000000..7af2bf583a
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.hawkbit.mcp.server.client.HawkBitAuthenticationValidator;
+import org.eclipse.hawkbit.mcp.server.client.HawkBitAuthenticationValidator.ValidationResult;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+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.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * Security configuration for the MCP server.
+ */
+@Slf4j
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class McpSecurityConfiguration {
+
+ /**
+ * Request attribute key for storing the Authorization header.
+ */
+ public static final String AUTH_HEADER_ATTRIBUTE = "hawkbit.mcp.auth.header";
+
+ private final HawkBitAuthenticationValidator authenticationValidator;
+
+ @Bean
+ @SuppressWarnings("java:S4502") // CSRF protection is not needed for stateless REST APIs using Authorization header
+ public SecurityFilterChain mcpSecurityFilterChain(final HttpSecurity http) throws Exception {
+ http
+ .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .addFilterBefore(new HawkBitAuthenticationFilter(authenticationValidator),
+ UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+ /**
+ * Filter that validates authentication.
+ */
+ @Slf4j
+ @RequiredArgsConstructor
+ public static class HawkBitAuthenticationFilter extends OncePerRequestFilter {
+
+ private final HawkBitAuthenticationValidator validator;
+
+ @Override
+ protected void doFilterInternal(final HttpServletRequest request, final @NonNull HttpServletResponse response,
+ final @NonNull FilterChain filterChain) throws ServletException, IOException {
+ String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
+
+ if (authHeader != null) {
+ request.setAttribute(AUTH_HEADER_ATTRIBUTE, authHeader);
+ }
+
+ ValidationResult result = validator.validate(authHeader);
+
+ switch (result) {
+ case VALID:
+ filterChain.doFilter(request, response);
+ break;
+
+ case MISSING_CREDENTIALS:
+ log.debug("Rejecting request: missing credentials");
+ sendErrorResponse(response, HttpStatus.UNAUTHORIZED,
+ "Authentication required. Please provide hawkBit credentials.");
+ break;
+
+ case INVALID_CREDENTIALS:
+ log.debug("Rejecting request: invalid credentials");
+ request.removeAttribute(AUTH_HEADER_ATTRIBUTE);
+ sendErrorResponse(response, HttpStatus.UNAUTHORIZED,
+ "Invalid hawkBit credentials.");
+ break;
+
+ case HAWKBIT_ERROR:
+ log.warn("Rejecting request: hawkBit unavailable");
+ request.removeAttribute(AUTH_HEADER_ATTRIBUTE);
+ sendErrorResponse(response, HttpStatus.SERVICE_UNAVAILABLE,
+ "Unable to validate credentials. hawkBit may be unavailable.");
+ break;
+ }
+ }
+
+ private void sendErrorResponse(final HttpServletResponse response, final HttpStatus status, final String message)
+ throws IOException {
+ response.setStatus(status.value());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.getWriter().write(String.format(
+ "{\"error\":\"%s\",\"message\":\"%s\"}",
+ status.getReasonPhrase(),
+ message));
+ }
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java
new file mode 100644
index 0000000000..a9bf1b70b4
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.config;
+
+import org.eclipse.hawkbit.mcp.server.prompts.HawkBitPromptProvider;
+import org.eclipse.hawkbit.mcp.server.resources.HawkBitDocumentationResource;
+import org.eclipse.hawkbit.mcp.server.tools.HawkBitMcpToolProvider;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configuration for MCP tools, resources, and prompts.
+ *
+ *
+ * All beans use {@code @ConditionalOnMissingBean} - override by defining your own bean
+ * Properties allow disabling built-in tools/resources/prompts
+ * Spring AI MCP auto-discovers {@code @Tool}, {@code @McpResource}, and {@code @McpPrompt} annotations
+ *
+ *
+ */
+@Configuration
+public class McpToolConfiguration {
+
+ /**
+ * Creates the hawkBit tool provider.
+ *
+ * Spring AI MCP auto-discovers {@code @McpTool} annotated methods on this bean.
+ * Override by defining your own {@code HawkBitMcpToolProvider} bean.
+ * Disable by setting {@code hawkbit.mcp.tools-enabled=false}.
+ * Individual operations can be enabled/disabled via {@code hawkbit.mcp.operations.*} properties.
+ *
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(name = "hawkbit.mcp.tools-enabled", havingValue = "true", matchIfMissing = true)
+ public HawkBitMcpToolProvider hawkBitMcpToolProvider(
+ final HawkbitClient hawkbitClient,
+ final Tenant dummyTenant,
+ final HawkBitMcpProperties properties) {
+ return new HawkBitMcpToolProvider(hawkbitClient, dummyTenant, properties);
+ }
+
+ /**
+ * Creates the hawkBit documentation resource provider.
+ *
+ * Spring AI MCP auto-discovers {@code @McpResource} annotated methods on this bean.
+ * Override by defining your own {@code HawkBitDocumentationResource} bean.
+ * Disable by setting {@code hawkbit.mcp.resources-enabled=false}.
+ *
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(name = "hawkbit.mcp.resources-enabled", havingValue = "true", matchIfMissing = true)
+ public HawkBitDocumentationResource hawkBitDocumentationResource() {
+ return new HawkBitDocumentationResource();
+ }
+
+ /**
+ * Creates the hawkBit prompt provider.
+ *
+ * Spring AI MCP auto-discovers {@code @McpPrompt} annotated methods on this bean.
+ * Override by defining your own {@code HawkBitPromptProvider} bean.
+ * Disable by setting {@code hawkbit.mcp.prompts-enabled=false}.
+ *
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(name = "hawkbit.mcp.prompts-enabled", havingValue = "true", matchIfMissing = true)
+ public HawkBitPromptProvider hawkBitPromptProvider() {
+ return new HawkBitPromptProvider();
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
new file mode 100644
index 0000000000..2a5bce899e
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+/**
+ * Operations for action management (actions are created indirectly via DS assignment).
+ */
+public enum ActionOperation {
+ DELETE,
+ DELETE_BATCH
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java
new file mode 100644
index 0000000000..c165df4678
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Common request parameters for list operations.
+ */
+public record ListRequest(
+ @JsonPropertyDescription("RSQL filter query (e.g., 'name==test*')")
+ String rsql,
+
+ @JsonPropertyDescription("Number of items to skip (default: 0)")
+ Integer offset,
+
+ @JsonPropertyDescription("Maximum number of items to return (default: 50)")
+ Integer limit
+) {
+
+ public static final int DEFAULT_OFFSET = 0;
+ public static final int DEFAULT_LIMIT = 50;
+
+ public int getOffsetOrDefault() {
+ return offset != null ? offset : DEFAULT_OFFSET;
+ }
+
+ public int getLimitOrDefault() {
+ return limit != null ? limit : DEFAULT_LIMIT;
+ }
+
+ public String getRsqlOrNull() {
+ return rsql != null && !rsql.isBlank() ? rsql : null;
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
new file mode 100644
index 0000000000..af373a0a24
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import java.util.List;
+
+/**
+ * Request wrapper for action management operations.
+ * Actions are created indirectly via distribution set assignment, so only DELETE operations are supported.
+ *
+ * @param operation the operation to perform (DELETE, DELETE_BATCH)
+ * @param actionId the action ID (for DELETE single action)
+ * @param actionIds list of action IDs (for DELETE_BATCH)
+ * @param rsql RSQL filter query (alternative for DELETE_BATCH)
+ */
+public record ManageActionRequest(
+ ActionOperation operation,
+ Long actionId,
+ List actionIds,
+ String rsql
+) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
new file mode 100644
index 0000000000..33612efa4f
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut;
+
+/**
+ * Request wrapper for distribution set management operations.
+ * Reuses existing {@link MgmtDistributionSetRequestBodyPost} for CREATE and
+ * {@link MgmtDistributionSetRequestBodyPut} for UPDATE.
+ *
+ * @param operation the operation to perform (CREATE, UPDATE, DELETE)
+ * @param distributionSetId the distribution set ID (required for UPDATE/DELETE)
+ * @param createBody the request body for CREATE operation
+ * @param updateBody the request body for UPDATE operation
+ */
+public record ManageDistributionSetRequest(
+ Operation operation,
+ Long distributionSetId,
+ MgmtDistributionSetRequestBodyPost createBody,
+ MgmtDistributionSetRequestBodyPut updateBody
+) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
new file mode 100644
index 0000000000..21ff963511
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut;
+
+/**
+ * Request wrapper for rollout management operations including CRUD and lifecycle.
+ * Reuses existing {@link MgmtRolloutRestRequestBodyPost} for CREATE and
+ * {@link MgmtRolloutRestRequestBodyPut} for UPDATE.
+ *
+ * @param operation the operation to perform (CREATE, UPDATE, DELETE, START, PAUSE, STOP, RESUME, APPROVE, DENY, RETRY, TRIGGER_NEXT_GROUP)
+ * @param rolloutId the rollout ID (required for UPDATE/DELETE/lifecycle operations)
+ * @param createBody the request body for CREATE operation
+ * @param updateBody the request body for UPDATE operation
+ * @param remark optional remark for APPROVE/DENY operations
+ */
+public record ManageRolloutRequest(
+ RolloutOperation operation,
+ Long rolloutId,
+ MgmtRolloutRestRequestBodyPost createBody,
+ MgmtRolloutRestRequestBodyPut updateBody,
+ String remark
+) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
new file mode 100644
index 0000000000..ae3b4dbaa1
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPut;
+
+/**
+ * Request wrapper for software module management operations.
+ * Reuses existing {@link MgmtSoftwareModuleRequestBodyPost} for CREATE and
+ * {@link MgmtSoftwareModuleRequestBodyPut} for UPDATE.
+ *
+ * @param operation the operation to perform (CREATE, UPDATE, DELETE)
+ * @param softwareModuleId the software module ID (required for UPDATE/DELETE)
+ * @param createBody the request body for CREATE operation
+ * @param updateBody the request body for UPDATE operation
+ */
+public record ManageSoftwareModuleRequest(
+ Operation operation,
+ Long softwareModuleId,
+ MgmtSoftwareModuleRequestBodyPost createBody,
+ MgmtSoftwareModuleRequestBodyPut updateBody
+) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
new file mode 100644
index 0000000000..b1537563df
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody;
+
+/**
+ * Request wrapper for target filter query management operations.
+ * Reuses existing {@link MgmtTargetFilterQueryRequestBody} for CREATE/UPDATE data.
+ *
+ * @param operation the operation to perform (CREATE, UPDATE, DELETE)
+ * @param filterId the target filter query ID (required for UPDATE/DELETE)
+ * @param body the request body containing filter data (for CREATE/UPDATE)
+ */
+public record ManageTargetFilterRequest(
+ Operation operation,
+ Long filterId,
+ MgmtTargetFilterQueryRequestBody body
+) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
new file mode 100644
index 0000000000..2ace1c616b
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
+
+/**
+ * Request wrapper for target management operations.
+ * Reuses existing {@link MgmtTargetRequestBody} for CREATE/UPDATE data.
+ *
+ * @param operation the operation to perform (CREATE, UPDATE, DELETE)
+ * @param controllerId the target controller ID (required for UPDATE/DELETE, used as identifier for CREATE)
+ * @param body the request body containing target data (for CREATE/UPDATE)
+ */
+public record ManageTargetRequest(
+ Operation operation,
+ String controllerId,
+ MgmtTargetRequestBody body
+) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
new file mode 100644
index 0000000000..5987bcd376
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+/**
+ * Standard CRUD operations for entity management tools.
+ */
+public enum Operation {
+ CREATE,
+ UPDATE,
+ DELETE
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java
new file mode 100644
index 0000000000..910eda378b
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+/**
+ * Unified response wrapper for management operations.
+ *
+ * @param the type of the data payload
+ * @param operation the operation that was performed
+ * @param success whether the operation was successful
+ * @param message optional message (typically for success confirmations or error details)
+ * @param data the operation result data (e.g., created/updated entity)
+ */
+public record OperationResponse(
+ String operation,
+ boolean success,
+ String message,
+ T data
+) {
+
+ /**
+ * Creates a successful response with data.
+ */
+ public static OperationResponse success(final String operation, final T data) {
+ return new OperationResponse<>(operation, true, null, data);
+ }
+
+ /**
+ * Creates a successful response with a message (no data).
+ */
+ public static OperationResponse success(final String operation, final String message) {
+ return new OperationResponse<>(operation, true, message, null);
+ }
+
+ /**
+ * Creates a successful response with both message and data.
+ */
+ public static OperationResponse success(final String operation, final String message, final T data) {
+ return new OperationResponse<>(operation, true, message, data);
+ }
+
+ /**
+ * Creates a failure response with an error message.
+ */
+ public static OperationResponse failure(final String operation, final String message) {
+ return new OperationResponse<>(operation, false, message, null);
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java
new file mode 100644
index 0000000000..62e121d89d
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import java.util.List;
+
+/**
+ * Generic paged response for MCP tool results.
+ *
+ * @param the type of items in the response
+ */
+public record PagedResponse(
+ List content,
+ long total,
+ int offset,
+ int limit
+) {
+
+ public static PagedResponse of(final List content, final long total, final int offset, final int limit) {
+ return new PagedResponse<>(content, total, offset, limit);
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
new file mode 100644
index 0000000000..f48113c253
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+/**
+ * Operations for rollout management including CRUD and lifecycle operations.
+ */
+public enum RolloutOperation {
+ CREATE,
+ UPDATE,
+ DELETE,
+ START,
+ PAUSE,
+ STOP,
+ RESUME,
+ APPROVE,
+ DENY,
+ RETRY,
+ TRIGGER_NEXT_GROUP
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkBitPromptProvider.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkBitPromptProvider.java
new file mode 100644
index 0000000000..7b4dbf9db4
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkBitPromptProvider.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.prompts;
+
+import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import io.modelcontextprotocol.spec.McpSchema.Role;
+import io.modelcontextprotocol.spec.McpSchema.TextContent;
+import lombok.extern.slf4j.Slf4j;
+import org.springaicommunity.mcp.annotation.McpPrompt;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * MCP prompts for hawkBit that provide initial context to LLMs.
+ *
+ * These prompts help LLMs understand what hawkBit is and what documentation
+ * resources are available at the start of a session.
+ *
+ */
+@Slf4j
+public class HawkBitPromptProvider {
+
+ private static final String PROMPTS_PATH = "prompts/";
+
+ @McpPrompt(
+ name = "hawkbit-context",
+ description = "Provides initial context about hawkBit, available tools, and documentation resources. " +
+ "Use this prompt at the start of a session to understand what you can do with hawkBit MCP.")
+ public GetPromptResult getHawkBitContext() {
+ return new GetPromptResult(
+ "hawkBit MCP Server Context",
+ List.of(new PromptMessage(Role.ASSISTANT, new TextContent(loadPrompt("hawkbit-context.md"))))
+ );
+ }
+
+ @McpPrompt(
+ name = "rsql-help",
+ description = "Explains RSQL query syntax for filtering hawkBit entities. " +
+ "Use this when you need help constructing filter queries for targets, rollouts, etc.")
+ public GetPromptResult getRsqlHelp() {
+ return new GetPromptResult(
+ "RSQL Query Help",
+ List.of(new PromptMessage(Role.ASSISTANT, new TextContent(loadPrompt("rsql-help.md"))))
+ );
+ }
+
+ private String loadPrompt(final String filename) {
+ try {
+ ClassPathResource resource = new ClassPathResource(PROMPTS_PATH + filename);
+ return resource.getContentAsString(StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ log.error("Failed to load prompt: {}", filename, e);
+ return "Prompt content not available.";
+ }
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkBitDocumentationResource.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkBitDocumentationResource.java
new file mode 100644
index 0000000000..2f05a5fe55
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkBitDocumentationResource.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.resources;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springaicommunity.mcp.annotation.McpResource;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * MCP resources providing hawkBit documentation for LLMs.
+ */
+@Slf4j
+public class HawkBitDocumentationResource {
+
+ private static final String DOCS_PATH = "hawkbit-docs/";
+
+ @McpResource(
+ uri = "hawkbit://docs/overview",
+ name = "hawkBit Overview",
+ description = "High-level introduction to hawkBit: interfaces (DDI, DMF, Management API), " +
+ "rollout management, and package model for IoT software updates")
+ public String getOverview() {
+ return loadDoc("README.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/what-is-hawkbit",
+ name = "What is hawkBit",
+ description = "Explains what hawkBit is, why IoT software updates matter, " +
+ "and scalability features for cloud deployments")
+ public String getWhatIsHawkbit() {
+ return loadDoc("what-is-hawkbit.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/quick-start",
+ name = "Quick Start Guide",
+ description = "Docker-based setup guides for monolith and microservices deployments, " +
+ "building from sources, and credential configuration")
+ public String getQuickStart() {
+ return loadDoc("quick-start.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/features",
+ name = "Feature Overview",
+ description = "Comprehensive feature list: device repository, software management, " +
+ "artifact delivery, rollout management, and API interfaces")
+ public String getFeatures() {
+ return loadDoc("features.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/architecture",
+ name = "System Architecture",
+ description = "Architecture overview with module diagram and third-party technology stack")
+ public String getArchitecture() {
+ return loadDoc("architecture.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/base-setup",
+ name = "Production Setup",
+ description = "Configuring production infrastructure with MariaDB/MySQL database " +
+ "and RabbitMQ for DMF (Device Management Federation) communication")
+ public String getBaseSetup() {
+ return loadDoc("base-setup.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/sdk",
+ name = "SDK Guide",
+ description = "hawkBit SDK for device and gateway integration: configuration properties, " +
+ "Maven dependencies, and usage examples with DdiTenant and MgmtAPI clients")
+ public String getSdkGuide() {
+ return loadDoc("hawkbit-sdk.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/feign-client",
+ name = "Feign Client Guide",
+ description = "Creating Feign-based REST clients for Management API and DDI API " +
+ "with Spring Boot integration examples")
+ public String getFeignClientGuide() {
+ return loadDoc("feign-client.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/clustering",
+ name = "Clustering Guide",
+ description = "Running hawkBit in clustered environments: Spring Cloud Stream event distribution, " +
+ "caching with TTL, scheduler behavior, and DoS filter constraints")
+ public String getClusteringGuide() {
+ return loadDoc("clustering.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/authentication",
+ name = "Authentication",
+ description = "Security token authentication (target and gateway tokens), certificate-based auth " +
+ "via reverse proxy, TLS/mTLS setup, and Nginx configuration examples")
+ public String getAuthentication() {
+ return loadDoc("authentication.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/authorization",
+ name = "Authorization",
+ description = "Fine-grained permission system for Management API/UI, DDI API authorization, " +
+ "permission groups, OpenID Connect support, and role-based access control")
+ public String getAuthorization() {
+ return loadDoc("authorization.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/datamodel",
+ name = "Data Model",
+ description = "Entity definitions: provisioning targets, distribution sets, software modules, " +
+ "artifacts, entity relationships, and soft/hard delete strategies")
+ public String getDataModel() {
+ return loadDoc("datamodel.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/rollout-management",
+ name = "Rollout Management",
+ description = "Rollout campaigns: cascading deployment groups, success/error thresholds, " +
+ "approval workflow, multi-assignments (beta), action weight prioritization, and state machines")
+ public String getRolloutManagement() {
+ return loadDoc("rollout-management.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/target-state",
+ name = "Target State",
+ description = "Target state definitions (UNKNOWN, IN_SYNC, PENDING, ERROR, REGISTERED) " +
+ "and state transition diagrams")
+ public String getTargetState() {
+ return loadDoc("targetstate.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/management-api",
+ name = "Management API",
+ description = "RESTful API for CRUD operations on targets and software: API versioning, " +
+ "HTTP methods, headers, error handling, and embedded Swagger UI reference")
+ public String getManagementApi() {
+ return loadDoc("management-api.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/ddi-api",
+ name = "DDI API (Direct Device Integration)",
+ description = "HTTP polling-based device integration API: state machine mapping, " +
+ "status feedback mechanisms, update retrieval, and embedded Swagger UI reference")
+ public String getDdiApi() {
+ return loadDoc("direct-device-integration-api.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/dmf-api",
+ name = "DMF API (Device Management Federation)",
+ description = "AMQP-based indirect device integration: message formats (THING_CREATED, etc.), " +
+ "exchanges, queues, bindings, and high-throughput service-to-service communication")
+ public String getDmfApi() {
+ return loadDoc("device-management-federation-api.md");
+ }
+
+ @McpResource(
+ uri = "hawkbit://docs/entity-definitions",
+ name = "hawkBit Entity Definitions",
+ description = "RSQL filtering syntax for querying targets, rollouts, distribution sets, " +
+ "actions, software modules, and target filter queries with examples")
+ public String getEntityDefinitions() {
+ return loadResource("hawkbit-entity-definitions.md");
+ }
+
+ private String loadDoc(final String filename) {
+ return loadResource(DOCS_PATH + filename);
+ }
+
+ private String loadResource(final String path) {
+ try {
+ ClassPathResource resource = new ClassPathResource(path);
+ return resource.getContentAsString(StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ log.error("Failed to load documentation: {}", path, e);
+ return "Documentation not available. Please refer to the hawkBit documentation at https://eclipse.dev/hawkbit/";
+ }
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkBitMcpToolProvider.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkBitMcpToolProvider.java
new file mode 100644
index 0000000000..ec6a14d51f
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkBitMcpToolProvider.java
@@ -0,0 +1,553 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.tools;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
+import org.eclipse.hawkbit.mcp.server.dto.ActionOperation;
+import org.eclipse.hawkbit.mcp.server.dto.ListRequest;
+import org.eclipse.hawkbit.mcp.server.dto.ManageActionRequest;
+import org.eclipse.hawkbit.mcp.server.dto.ManageDistributionSetRequest;
+import org.eclipse.hawkbit.mcp.server.dto.ManageRolloutRequest;
+import org.eclipse.hawkbit.mcp.server.dto.ManageSoftwareModuleRequest;
+import org.eclipse.hawkbit.mcp.server.dto.ManageTargetFilterRequest;
+import org.eclipse.hawkbit.mcp.server.dto.ManageTargetRequest;
+import org.eclipse.hawkbit.mcp.server.dto.Operation;
+import org.eclipse.hawkbit.mcp.server.dto.OperationResponse;
+import org.eclipse.hawkbit.mcp.server.dto.PagedResponse;
+import org.eclipse.hawkbit.mcp.server.dto.RolloutOperation;
+import org.eclipse.hawkbit.mgmt.json.model.PagedList;
+import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
+import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
+import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtActionRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetFilterQueryRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springaicommunity.mcp.annotation.McpTool;
+import org.springframework.http.ResponseEntity;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * MCP tools for hawkBit using the SDK.
+ *
+ * Provides tools for managing targets, rollouts, distribution sets, actions,
+ * software modules, and target filter queries via the hawkBit REST API.
+ *
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class HawkBitMcpToolProvider {
+
+ private static final String OP_CREATE = "CREATE";
+ private static final String OP_UPDATE = "UPDATE";
+ private static final String OP_DELETE = "DELETE";
+ private static final String OP_DELETE_BATCH = "DELETE_BATCH";
+ private static final String OP_START = "START";
+ private static final String OP_PAUSE = "PAUSE";
+ private static final String OP_STOP = "STOP";
+ private static final String OP_RESUME = "RESUME";
+ private static final String OP_APPROVE = "APPROVE";
+ private static final String OP_DENY = "DENY";
+ private static final String OP_RETRY = "RETRY";
+ private static final String OP_TRIGGER_NEXT_GROUP = "TRIGGER_NEXT_GROUP";
+
+ private final HawkbitClient hawkbitClient;
+ private final Tenant dummyTenant;
+ private final HawkBitMcpProperties properties;
+
+ private PagedResponse toPagedResponse(final PagedList pagedList, final ListRequest request) {
+ if (pagedList == null) {
+ return PagedResponse.of(
+ Collections.emptyList(),
+ 0L,
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault());
+ }
+ return PagedResponse.of(
+ pagedList.getContent(),
+ pagedList.getTotal(),
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault());
+ }
+
+ @McpTool(name = "list_targets",
+ description = "Retrieves a paged list of targets (devices) with optional RSQL filtering. " +
+ "Targets represent devices that can receive software updates.")
+ public PagedResponse listTargets(final ListRequest request) {
+ log.debug("Listing targets with rsql={}, offset={}, limit={}",
+ request.rsql(), request.getOffsetOrDefault(), request.getLimitOrDefault());
+
+ MgmtTargetRestApi targetApi = hawkbitClient.mgmtService(MgmtTargetRestApi.class, dummyTenant);
+ ResponseEntity> response = targetApi.getTargets(
+ request.getRsqlOrNull(),
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault(),
+ null);
+
+ return toPagedResponse(response.getBody(), request);
+ }
+
+ @McpTool(name = "list_rollouts",
+ description = "Retrieves a paged list of rollouts with optional RSQL filtering. " +
+ "Rollouts are used to deploy software to groups of targets.")
+ public PagedResponse listRollouts(final ListRequest request) {
+ log.debug("Listing rollouts with rsql={}, offset={}, limit={}",
+ request.rsql(), request.getOffsetOrDefault(), request.getLimitOrDefault());
+
+ MgmtRolloutRestApi rolloutApi = hawkbitClient.mgmtService(MgmtRolloutRestApi.class, dummyTenant);
+ ResponseEntity> response = rolloutApi.getRollouts(
+ request.getRsqlOrNull(),
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault(),
+ null,
+ null);
+
+ return toPagedResponse(response.getBody(), request);
+ }
+
+ @McpTool(name = "list_distribution_sets",
+ description = "Retrieves a paged list of distribution sets with optional RSQL filtering. " +
+ "Distribution sets are software packages that can be deployed to targets.")
+ public PagedResponse listDistributionSets(final ListRequest request) {
+ log.debug("Listing distribution sets with rsql={}, offset={}, limit={}",
+ request.rsql(), request.getOffsetOrDefault(), request.getLimitOrDefault());
+
+ MgmtDistributionSetRestApi dsApi = hawkbitClient.mgmtService(MgmtDistributionSetRestApi.class, dummyTenant);
+ ResponseEntity> response = dsApi.getDistributionSets(
+ request.getRsqlOrNull(),
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault(),
+ null);
+
+ return toPagedResponse(response.getBody(), request);
+ }
+
+ @McpTool(name = "list_actions",
+ description = "Retrieves a paged list of actions with optional RSQL filtering. " +
+ "Actions represent deployment operations assigned to targets.")
+ public PagedResponse listActions(final ListRequest request) {
+ log.debug("Listing actions with rsql={}, offset={}, limit={}",
+ request.rsql(), request.getOffsetOrDefault(), request.getLimitOrDefault());
+
+ MgmtActionRestApi actionApi = hawkbitClient.mgmtService(MgmtActionRestApi.class, dummyTenant);
+ ResponseEntity> response = actionApi.getActions(
+ request.getRsqlOrNull(),
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault(),
+ null,
+ null);
+
+ return toPagedResponse(response.getBody(), request);
+ }
+
+ @McpTool(name = "list_software_modules",
+ description = "Retrieves a paged list of software modules with optional RSQL filtering. " +
+ "Software modules are individual software components within distribution sets.")
+ public PagedResponse listSoftwareModules(final ListRequest request) {
+ log.debug("Listing software modules with rsql={}, offset={}, limit={}",
+ request.rsql(), request.getOffsetOrDefault(), request.getLimitOrDefault());
+
+ MgmtSoftwareModuleRestApi smApi = hawkbitClient.mgmtService(MgmtSoftwareModuleRestApi.class, dummyTenant);
+ ResponseEntity> response = smApi.getSoftwareModules(
+ request.getRsqlOrNull(),
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault(),
+ null);
+
+ return toPagedResponse(response.getBody(), request);
+ }
+
+ @McpTool(name = "list_target_filters",
+ description = "Retrieves a paged list of target filter queries with optional RSQL filtering. " +
+ "Target filters define RSQL queries to group targets for rollouts or auto-assignment.")
+ public PagedResponse listTargetFilters(final ListRequest request) {
+ log.debug("Listing target filters with rsql={}, offset={}, limit={}",
+ request.rsql(), request.getOffsetOrDefault(), request.getLimitOrDefault());
+
+ MgmtTargetFilterQueryRestApi filterApi = hawkbitClient.mgmtService(MgmtTargetFilterQueryRestApi.class, dummyTenant);
+ ResponseEntity> response = filterApi.getFilters(
+ request.getRsqlOrNull(),
+ request.getOffsetOrDefault(),
+ request.getLimitOrDefault(),
+ null,
+ null);
+
+ return toPagedResponse(response.getBody(), request);
+ }
+
+ @McpTool(name = "manage_target",
+ description = "Create, update, or delete targets (devices). " +
+ "Operations: CREATE (new target with controllerId, name, description), " +
+ "UPDATE (modify existing target by controllerId), " +
+ "DELETE (remove target by controllerId)")
+ public OperationResponse manageTarget(final ManageTargetRequest request) {
+ validateOperation(request.operation(), "targets");
+ log.debug("Managing target: operation={}, controllerId={}", request.operation(), request.controllerId());
+
+ final MgmtTargetRestApi api = hawkbitClient.mgmtService(MgmtTargetRestApi.class, dummyTenant);
+
+ return switch (request.operation()) {
+ case CREATE -> {
+ if (request.body() == null) {
+ yield OperationResponse.failure(OP_CREATE, "Request body is required for CREATE operation");
+ }
+ final ResponseEntity> response = api.createTargets(List.of(request.body()));
+ final List created = response.getBody();
+ yield OperationResponse.success(OP_CREATE, "Target created successfully",
+ created != null && !created.isEmpty() ? created.get(0) : null);
+ }
+ case UPDATE -> {
+ if (request.controllerId() == null || request.controllerId().isBlank()) {
+ yield OperationResponse.failure(OP_UPDATE, "controllerId is required for UPDATE operation");
+ }
+ if (request.body() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "Request body is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateTarget(request.controllerId(), request.body());
+ yield OperationResponse.success(OP_UPDATE, "Target updated successfully", response.getBody());
+ }
+ case DELETE -> {
+ if (request.controllerId() == null || request.controllerId().isBlank()) {
+ yield OperationResponse.failure(OP_DELETE, "controllerId is required for DELETE operation");
+ }
+ api.deleteTarget(request.controllerId());
+ yield OperationResponse.success(OP_DELETE, "Target deleted successfully");
+ }
+ };
+ }
+
+ @McpTool(name = "manage_rollout",
+ description = "Create, update, delete, and control rollouts for software deployment. " +
+ "Operations: CREATE (new rollout), UPDATE (modify rollout), DELETE (remove rollout), " +
+ "START (begin rollout), PAUSE, STOP, RESUME, APPROVE, DENY, RETRY, TRIGGER_NEXT_GROUP")
+ public OperationResponse manageRollout(final ManageRolloutRequest request) {
+ validateRolloutOperation(request.operation());
+ log.debug("Managing rollout: operation={}, rolloutId={}", request.operation(), request.rolloutId());
+
+ final MgmtRolloutRestApi api = hawkbitClient.mgmtService(MgmtRolloutRestApi.class, dummyTenant);
+
+ return switch (request.operation()) {
+ case CREATE -> {
+ if (request.createBody() == null) {
+ yield OperationResponse.failure(OP_CREATE, "createBody is required for CREATE operation");
+ }
+ final ResponseEntity response = api.create(request.createBody());
+ yield OperationResponse.success(OP_CREATE, "Rollout created successfully", response.getBody());
+ }
+ case UPDATE -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "rolloutId is required for UPDATE operation");
+ }
+ if (request.updateBody() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "updateBody is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.update(request.rolloutId(), request.updateBody());
+ yield OperationResponse.success(OP_UPDATE, "Rollout updated successfully", response.getBody());
+ }
+ case DELETE -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_DELETE, "rolloutId is required for DELETE operation");
+ }
+ api.delete(request.rolloutId());
+ yield OperationResponse.success(OP_DELETE, "Rollout deleted successfully");
+ }
+ case START -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_START, "rolloutId is required for START operation");
+ }
+ api.start(request.rolloutId());
+ yield OperationResponse.success(OP_START, "Rollout started successfully");
+ }
+ case PAUSE -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_PAUSE, "rolloutId is required for PAUSE operation");
+ }
+ api.pause(request.rolloutId());
+ yield OperationResponse.success(OP_PAUSE, "Rollout paused successfully");
+ }
+ case STOP -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_STOP, "rolloutId is required for STOP operation");
+ }
+ api.stop(request.rolloutId());
+ yield OperationResponse.success(OP_STOP, "Rollout stopped successfully");
+ }
+ case RESUME -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_RESUME, "rolloutId is required for RESUME operation");
+ }
+ api.resume(request.rolloutId());
+ yield OperationResponse.success(OP_RESUME, "Rollout resumed successfully");
+ }
+ case APPROVE -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_APPROVE, "rolloutId is required for APPROVE operation");
+ }
+ api.approve(request.rolloutId(), request.remark());
+ yield OperationResponse.success(OP_APPROVE, "Rollout approved successfully");
+ }
+ case DENY -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_DENY, "rolloutId is required for DENY operation");
+ }
+ api.deny(request.rolloutId(), request.remark());
+ yield OperationResponse.success(OP_DENY, "Rollout denied successfully");
+ }
+ case RETRY -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_RETRY, "rolloutId is required for RETRY operation");
+ }
+ final ResponseEntity response = api.retryRollout(request.rolloutId());
+ yield OperationResponse.success(OP_RETRY, "Rollout retry created successfully", response.getBody());
+ }
+ case TRIGGER_NEXT_GROUP -> {
+ if (request.rolloutId() == null) {
+ yield OperationResponse.failure(OP_TRIGGER_NEXT_GROUP, "rolloutId is required for TRIGGER_NEXT_GROUP operation");
+ }
+ api.triggerNextGroup(request.rolloutId());
+ yield OperationResponse.success(OP_TRIGGER_NEXT_GROUP, "Next rollout group triggered successfully");
+ }
+ };
+ }
+
+ @McpTool(name = "manage_distribution_set",
+ description = "Create, update, or delete distribution sets (software packages). " +
+ "Operations: CREATE (new distribution set with name, version, type), " +
+ "UPDATE (modify existing distribution set), DELETE (remove distribution set)")
+ public OperationResponse manageDistributionSet(final ManageDistributionSetRequest request) {
+ validateOperation(request.operation(), "distributionSets");
+ log.debug("Managing distribution set: operation={}, distributionSetId={}", request.operation(), request.distributionSetId());
+
+ final MgmtDistributionSetRestApi api = hawkbitClient.mgmtService(MgmtDistributionSetRestApi.class, dummyTenant);
+
+ return switch (request.operation()) {
+ case CREATE -> {
+ if (request.createBody() == null) {
+ yield OperationResponse.failure(OP_CREATE, "createBody is required for CREATE operation");
+ }
+ final ResponseEntity> response = api.createDistributionSets(List.of(request.createBody()));
+ final List created = response.getBody();
+ yield OperationResponse.success(OP_CREATE, "Distribution set created successfully",
+ created != null && !created.isEmpty() ? created.get(0) : null);
+ }
+ case UPDATE -> {
+ if (request.distributionSetId() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "distributionSetId is required for UPDATE operation");
+ }
+ if (request.updateBody() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "updateBody is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateDistributionSet(request.distributionSetId(), request.updateBody());
+ yield OperationResponse.success(OP_UPDATE, "Distribution set updated successfully", response.getBody());
+ }
+ case DELETE -> {
+ if (request.distributionSetId() == null) {
+ yield OperationResponse.failure(OP_DELETE, "distributionSetId is required for DELETE operation");
+ }
+ api.deleteDistributionSet(request.distributionSetId());
+ yield OperationResponse.success(OP_DELETE, "Distribution set deleted successfully");
+ }
+ };
+ }
+
+ @McpTool(name = "manage_action",
+ description = "Delete deployment actions. Actions are created indirectly via distribution set assignment. " +
+ "Operations: DELETE (single action by ID), DELETE_BATCH (multiple actions by RSQL filter or list of IDs)")
+ public OperationResponse manageAction(final ManageActionRequest request) {
+ validateActionOperation(request.operation());
+ log.debug("Managing action: operation={}, actionId={}", request.operation(), request.actionId());
+
+ final MgmtActionRestApi api = hawkbitClient.mgmtService(MgmtActionRestApi.class, dummyTenant);
+
+ return switch (request.operation()) {
+ case DELETE -> {
+ if (request.actionId() == null) {
+ yield OperationResponse.failure(OP_DELETE, "actionId is required for DELETE operation");
+ }
+ api.deleteAction(request.actionId());
+ yield OperationResponse.success(OP_DELETE, "Action deleted successfully");
+ }
+ case DELETE_BATCH -> {
+ if ((request.actionIds() == null || request.actionIds().isEmpty()) &&
+ (request.rsql() == null || request.rsql().isBlank())) {
+ yield OperationResponse.failure(OP_DELETE_BATCH, "Either actionIds or rsql is required for DELETE_BATCH operation");
+ }
+ api.deleteActions(request.rsql(), request.actionIds());
+ yield OperationResponse.success(OP_DELETE_BATCH, "Actions deleted successfully");
+ }
+ };
+ }
+
+ @McpTool(name = "manage_software_module",
+ description = "Create, update, or delete software modules. " +
+ "Operations: CREATE (new software module with name, version, type), " +
+ "UPDATE (modify existing software module), DELETE (remove software module)")
+ public OperationResponse manageSoftwareModule(final ManageSoftwareModuleRequest request) {
+ validateOperation(request.operation(), "softwareModules");
+ log.debug("Managing software module: operation={}, softwareModuleId={}", request.operation(), request.softwareModuleId());
+
+ final MgmtSoftwareModuleRestApi api = hawkbitClient.mgmtService(MgmtSoftwareModuleRestApi.class, dummyTenant);
+
+ return switch (request.operation()) {
+ case CREATE -> {
+ if (request.createBody() == null) {
+ yield OperationResponse.failure(OP_CREATE, "createBody is required for CREATE operation");
+ }
+ final ResponseEntity> response = api.createSoftwareModules(List.of(request.createBody()));
+ final List created = response.getBody();
+ yield OperationResponse.success(OP_CREATE, "Software module created successfully",
+ created != null && !created.isEmpty() ? created.get(0) : null);
+ }
+ case UPDATE -> {
+ if (request.softwareModuleId() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "softwareModuleId is required for UPDATE operation");
+ }
+ if (request.updateBody() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "updateBody is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateSoftwareModule(request.softwareModuleId(), request.updateBody());
+ yield OperationResponse.success(OP_UPDATE, "Software module updated successfully", response.getBody());
+ }
+ case DELETE -> {
+ if (request.softwareModuleId() == null) {
+ yield OperationResponse.failure(OP_DELETE, "softwareModuleId is required for DELETE operation");
+ }
+ api.deleteSoftwareModule(request.softwareModuleId());
+ yield OperationResponse.success(OP_DELETE, "Software module deleted successfully");
+ }
+ };
+ }
+
+ @McpTool(name = "manage_target_filter",
+ description = "Create, update, or delete target filter queries. " +
+ "Operations: CREATE (new target filter with name and RSQL query), " +
+ "UPDATE (modify existing target filter), DELETE (remove target filter)")
+ public OperationResponse manageTargetFilter(final ManageTargetFilterRequest request) {
+ validateOperation(request.operation(), "targetFilters");
+ log.debug("Managing target filter: operation={}, filterId={}", request.operation(), request.filterId());
+
+ final MgmtTargetFilterQueryRestApi api = hawkbitClient.mgmtService(MgmtTargetFilterQueryRestApi.class, dummyTenant);
+
+ return switch (request.operation()) {
+ case CREATE -> {
+ if (request.body() == null) {
+ yield OperationResponse.failure(OP_CREATE, "Request body is required for CREATE operation");
+ }
+ final ResponseEntity response = api.createFilter(request.body());
+ yield OperationResponse.success(OP_CREATE, "Target filter created successfully", response.getBody());
+ }
+ case UPDATE -> {
+ if (request.filterId() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "filterId is required for UPDATE operation");
+ }
+ if (request.body() == null) {
+ yield OperationResponse.failure(OP_UPDATE, "Request body is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateFilter(request.filterId(), request.body());
+ yield OperationResponse.success(OP_UPDATE, "Target filter updated successfully", response.getBody());
+ }
+ case DELETE -> {
+ if (request.filterId() == null) {
+ yield OperationResponse.failure(OP_DELETE, "filterId is required for DELETE operation");
+ }
+ api.deleteFilter(request.filterId());
+ yield OperationResponse.success(OP_DELETE, "Target filter deleted successfully");
+ }
+ };
+ }
+
+
+ private void validateOperation(final Operation operation, final String entity) {
+ final String opName = operation.name().toLowerCase();
+ if (!isOperationEnabled(opName, entity)) {
+ throw new IllegalArgumentException(
+ "Operation " + operation + " is not enabled for " + entity +
+ ". Check hawkbit.mcp.operations configuration.");
+ }
+ }
+
+ private void validateRolloutOperation(final RolloutOperation operation) {
+ final String opName = operation.name().toLowerCase().replace("_", "-");
+ final HawkBitMcpProperties.RolloutConfig config = properties.getOperations().getRollouts();
+ final Boolean entitySetting = config.getOperationEnabled(opName);
+
+ // For standard CRUD ops, check global fallback
+ if (entitySetting == null) {
+ if (!properties.getOperations().isGlobalOperationEnabled(opName)) {
+ throw new IllegalArgumentException(
+ "Operation " + operation + " is not enabled for rollouts. " +
+ "Check hawkbit.mcp.operations configuration.");
+ }
+ return;
+ }
+
+ if (!entitySetting) {
+ throw new IllegalArgumentException(
+ "Operation " + operation + " is not enabled for rollouts. " +
+ "Check hawkbit.mcp.operations configuration.");
+ }
+ }
+
+ private void validateActionOperation(final ActionOperation operation) {
+ final String opName = operation.name().toLowerCase().replace("_", "-");
+ final HawkBitMcpProperties.ActionConfig config = properties.getOperations().getActions();
+ final Boolean entitySetting = config.getOperationEnabled(opName);
+
+ if (entitySetting == null) {
+ if (opName.equals("delete") && !properties.getOperations().isGlobalOperationEnabled("delete")) {
+ throw new IllegalArgumentException(
+ "Operation " + operation + " is not enabled for actions. " +
+ "Check hawkbit.mcp.operations configuration.");
+ }
+ return;
+ }
+
+ if (!entitySetting) {
+ throw new IllegalArgumentException(
+ "Operation " + operation + " is not enabled for actions. " +
+ "Check hawkbit.mcp.operations configuration.");
+ }
+ }
+
+ private boolean isOperationEnabled(final String operation, final String entity) {
+ final HawkBitMcpProperties.Operations ops = properties.getOperations();
+ final HawkBitMcpProperties.EntityConfig entityConfig = getEntityConfig(entity);
+
+ final Boolean entitySetting = entityConfig != null ? entityConfig.getOperationEnabled(operation) : null;
+ if (entitySetting != null) {
+ return entitySetting;
+ }
+
+ return ops.isGlobalOperationEnabled(operation);
+ }
+
+ private HawkBitMcpProperties.EntityConfig getEntityConfig(final String entity) {
+ final HawkBitMcpProperties.Operations ops = properties.getOperations();
+ return switch (entity.toLowerCase()) {
+ case "targets" -> ops.getTargets();
+ case "rollouts" -> ops.getRollouts();
+ case "distributionsets" -> ops.getDistributionSets();
+ case "softwaremodules" -> ops.getSoftwareModules();
+ case "targetfilters" -> ops.getTargetFilters();
+ default -> null;
+ };
+ }
+}
diff --git a/hawkbit-mcp/src/main/resources/application.properties b/hawkbit-mcp/src/main/resources/application.properties
new file mode 100644
index 0000000000..e5472e16fe
--- /dev/null
+++ b/hawkbit-mcp/src/main/resources/application.properties
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# This program and the accompanying materials are made
+# available under the terms of the Eclipse Public License 2.0
+# which is available at https://www.eclipse.org/legal/epl-2.0/
+#
+# SPDX-License-Identifier: EPL-2.0
+#
+
+# Server configuration
+server.port=8081
+
+# Spring application name
+spring.application.name=hawkbit-mcp-server
+
+# Spring AI MCP Server configuration
+spring.ai.mcp.server.enabled=true
+spring.ai.mcp.server.name=hawkbit-mcp-server
+spring.ai.mcp.server.version=1.0.0
+spring.ai.mcp.server.type=SYNC
+spring.ai.mcp.server.protocol=STREAMABLE
+# Change from HTTP to STDIO
+#spring.ai.mcp.server.stdio=true
+#spring.ai.mcp.server.protocol=STDIO
+spring.ai.mcp.server.capabilities.prompt=true
+
+# hawkBit connection configuration
+hawkbit.mcp.mgmt-url=${HAWKBIT_URL:http://localhost:8080}
+
+# Authentication validation configuration
+hawkbit.mcp.validation.enabled=true
+hawkbit.mcp.validation.cache-ttl=600s
+hawkbit.mcp.validation.cache-max-size=1000
+
+# Logging configuration
+logging.level.org.eclipse.hawkbit.mcp=DEBUG
+logging.level.org.springframework.ai.mcp=DEBUG
+
+
+# Global: disable all deletes by default
+#hawkbit.mcp.operations.delete-enabled=false
+# But allow delete for targets specifically
+#hawkbit.mcp.operations.targets.delete-enabled=true
+
+# Disable rollout lifecycle operations
+#hawkbit.mcp.operations.rollouts.start-enabled=false
+#hawkbit.mcp.operations.rollouts.approve-enabled=false
+
+# Disable software modules delete operations
+#hawkbit.mcp.operations.software-modules.delete-enabled=false
diff --git a/hawkbit-mcp/src/main/resources/hawkbit-entity-definitions.md b/hawkbit-mcp/src/main/resources/hawkbit-entity-definitions.md
new file mode 100644
index 0000000000..a5768c4580
--- /dev/null
+++ b/hawkbit-mcp/src/main/resources/hawkbit-entity-definitions.md
@@ -0,0 +1,351 @@
+# hawkBit Entity Definitions and RSQL Filtering Guide
+
+This document describes the entities available in hawkBit and how to filter and sort them using RSQL queries through the MCP tools.
+
+## RSQL Query Syntax
+
+RSQL (RESTful Service Query Language) is a query language for filtering and searching entities. It uses a simple, URL-friendly syntax.
+
+### Comparison Operators
+
+| Operator | Description | Example |
+|----------|-------------|---------|
+| `==` | Equal to | `name==MyTarget` |
+| `!=` | Not equal to | `status!=ERROR` |
+| `=lt=` or `<` | Less than | `createdAt=lt=1609459200000` |
+| `=le=` or `<=` | Less than or equal | `weight=le=500` |
+| `=gt=` or `>` | Greater than | `lastTargetQuery=gt=1609459200000` |
+| `=ge=` or `>=` | Greater than or equal | `id=ge=100` |
+| `=in=` | In list | `status=in=(RUNNING,FINISHED)` |
+| `=out=` | Not in list | `updateStatus=out=(ERROR,UNKNOWN)` |
+
+### Logical Operators
+
+| Operator | Description | Example |
+|----------|-------------|---------|
+| `;` or `and` | Logical AND | `name==Test*;status==RUNNING` |
+| `,` or `or` | Logical OR | `status==ERROR,status==CANCELED` |
+
+Always use "and" or "or" for operators when grouping conditions - since this is the human-readable format.
+
+### Wildcard Support
+
+Use `*` as a wildcard character for pattern matching:
+- `name==Device*` - Names starting with "Device"
+- `name==*Controller` - Names ending with "Controller"
+- `name==*test*` - Names containing "test"
+
+### Sub-Entity Filtering
+
+Access nested entity fields using dot notation:
+- `assignedDistributionSet.name==MyDS`
+- `target.controllerId==device123`
+- `type.key==os`
+
+### Map/Metadata Filtering
+
+For metadata and attributes, use dot notation with the key:
+- `metadata.environment==production`
+- `controllerAttributes.revision==1.5`
+
+---
+
+## Entity Definitions
+
+### Target
+
+Targets represent devices or software instances that can receive software updates.
+
+**Filterable/Sortable Fields:**
+
+| Field | Description | Type |
+|------------------------------------|----------------------------------------------------------------------|------|
+| `controllerId` | Unique identifier of the target | String |
+| `name` | Display name | String |
+| `description` | Description text | String |
+| `updateStatus` | Current update status (UNKNOWN, IN_SYNC, PENDING, ERROR, REGISTERED) | Enum |
+| `address` | IP address or URI | String |
+| `lastTargetQuery` | Last time the target polled (timestamp in ms) | Long |
+| `createdAt` | Creation timestamp | Long |
+| `createdBy` | Creator username | String |
+| `lastModifiedAt` | Last modification timestamp | Long |
+| `lastModifiedBy` | Last modifier username | String |
+| `assignedDistributionSet.name` | Name of assigned distribution set | String |
+| `assignedDistributionSet.version` | Version of assigned distribution set | String |
+| `installedDistributionSet.name` | Name of installed distribution set | String |
+| `installedDistributionSet.version` | Version of installed distribution set | String |
+| `targetType.key` | Target type key | String |
+| `targetType.name` | Target type name | String |
+| `tags.name` | Tag name | String |
+| `group` | Group name | String |
+| `metadata.` | Metadata value by key | String |
+| `controllerAttributes.` | Controller attribute by key | String |
+
+**Example Queries:**
+```
+# Find targets with update errors
+updateStatus==ERROR
+
+# Find targets by name pattern
+name==device-*
+
+# Find targets with specific distribution set assigned
+assignedDistributionSet.name==Firmware;assignedDistributionSet.version==2.0.0
+
+# Find targets that haven't polled in 24 hours (timestamp example)
+lastTargetQuery=lt=1704067200000
+
+# Find targets by tag
+tags.name==production
+
+# Find targets by metadata
+metadata.location==factory-A
+
+# Find targets by controller attribute
+controllerAttributes.firmware_version==1.2.3
+```
+
+---
+
+### Distribution Set
+
+Distribution Sets are collections of software modules that can be deployed to targets.
+
+**Filterable/Sortable Fields:**
+
+| Field | Description | Type |
+|-------|-------------|------|
+| `id` | Unique identifier | Long |
+| `name` | Distribution set name | String |
+| `version` | Version string | String |
+| `description` | Description text | String |
+| `type.key` | Distribution set type key | String |
+| `type.name` | Distribution set type name | String |
+| `valid` | Whether the DS is valid for deployment | Boolean |
+| `createdAt` | Creation timestamp | Long |
+| `createdBy` | Creator username | String |
+| `lastModifiedAt` | Last modification timestamp | Long |
+| `lastModifiedBy` | Last modifier username | String |
+| `tags.name` | Tag name | String |
+| `modules.name` | Software module name | String |
+| `metadata.` | Metadata value by key | String |
+
+**Example Queries:**
+```
+# Find distribution sets by name
+name==Firmware*
+
+# Find valid distribution sets only
+valid==true
+
+# Find by type
+type.key==os_app
+
+# Find by tag
+tags.name==release-candidate
+
+# Find distribution sets containing a specific module
+modules.name==bootloader
+```
+
+---
+
+### Rollout
+
+Rollouts are used to deploy software to groups of targets in a controlled manner.
+
+**Filterable/Sortable Fields:**
+
+| Field | Description | Type |
+|-------|-------------|------|
+| `id` | Unique identifier | Long |
+| `name` | Rollout name | String |
+| `description` | Description text | String |
+| `status` | Rollout status (CREATING, READY, PAUSED, STARTING, RUNNING, FINISHED, etc.) | Enum |
+| `distributionSet.id` | Distribution set ID | Long |
+| `distributionSet.name` | Distribution set name | String |
+| `distributionSet.version` | Distribution set version | String |
+| `distributionSet.type` | Distribution set type | String |
+| `createdAt` | Creation timestamp | Long |
+| `createdBy` | Creator username | String |
+| `lastModifiedAt` | Last modification timestamp | Long |
+| `lastModifiedBy` | Last modifier username | String |
+
+**Example Queries:**
+```
+# Find running rollouts
+status==RUNNING
+
+# Find rollouts by name
+name==Campaign*
+
+# Find rollouts for a specific distribution set
+distributionSet.name==Firmware;distributionSet.version==2.0.0
+
+# Find finished or paused rollouts
+status=in=(FINISHED,PAUSED)
+```
+
+---
+
+### Action
+
+Actions represent deployment operations assigned to targets.
+
+**Filterable/Sortable Fields:**
+
+| Field | Description | Type |
+|-------|-------------|------|
+| `id` | Unique identifier | Long |
+| `status` | Action status (SCHEDULED, RUNNING, FINISHED, ERROR, CANCELED, etc.) | Enum |
+| `active` | Whether the action is currently active | Boolean |
+| `weight` | Priority weight (0-1000) | Integer |
+| `lastActionStatusCode` | Last status code reported | Integer |
+| `externalRef` | External reference string | String |
+| `target.controllerId` | Target controller ID | String |
+| `target.name` | Target name | String |
+| `target.updateStatus` | Target update status | Enum |
+| `distributionSet.id` | Distribution set ID | Long |
+| `distributionSet.name` | Distribution set name | String |
+| `distributionSet.version` | Distribution set version | String |
+| `rollout.id` | Rollout ID | Long |
+| `rollout.name` | Rollout name | String |
+| `rolloutGroup.id` | Rollout group ID | Long |
+| `rolloutGroup.name` | Rollout group name | String |
+| `createdAt` | Creation timestamp | Long |
+| `createdBy` | Creator username | String |
+| `lastModifiedAt` | Last modification timestamp | Long |
+| `lastModifiedBy` | Last modifier username | String |
+
+**Example Queries:**
+```
+# Find active actions
+active==true
+
+# Find actions by status
+status==RUNNING
+
+# Find failed actions
+status==ERROR
+
+# Find actions for a specific target
+target.controllerId==device-001
+
+# Find actions for a specific rollout
+rollout.name==Campaign2024
+
+# Find high-priority actions
+weight=gt=800
+
+# Find actions with specific status code
+lastActionStatusCode==200
+```
+
+---
+
+### Software Module
+
+Software Modules are individual software components that make up distribution sets.
+
+**Filterable/Sortable Fields:**
+
+| Field | Description | Type |
+|-------|-------------|------|
+| `id` | Unique identifier | Long |
+| `name` | Module name | String |
+| `version` | Version string | String |
+| `description` | Description text | String |
+| `type.key` | Software module type key | String |
+| `type.name` | Software module type name | String |
+| `createdAt` | Creation timestamp | Long |
+| `createdBy` | Creator username | String |
+| `lastModifiedAt` | Last modification timestamp | Long |
+| `lastModifiedBy` | Last modifier username | String |
+| `metadata.` | Metadata value by key | String |
+
+**Example Queries:**
+```
+# Find modules by name
+name==bootloader*
+
+# Find modules by type
+type.key==os
+
+# Find modules by version
+version==2.0.*
+
+# Find modules with specific metadata
+metadata.checksum==abc123
+```
+
+---
+
+### Target Filter Query
+
+Target Filter Queries define RSQL filters for grouping targets, used for rollouts and auto-assignment.
+
+**Filterable/Sortable Fields:**
+
+| Field | Description | Type |
+|-------|-------------|------|
+| `id` | Unique identifier | Long |
+| `name` | Filter name | String |
+| `autoAssignDistributionSet.name` | Auto-assign DS name | String |
+| `autoAssignDistributionSet.version` | Auto-assign DS version | String |
+| `createdAt` | Creation timestamp | Long |
+| `createdBy` | Creator username | String |
+| `lastModifiedAt` | Last modification timestamp | Long |
+| `lastModifiedBy` | Last modifier username | String |
+
+**Example Queries:**
+```
+# Find filters by name
+name==Production*
+
+# Find filters with auto-assignment configured
+autoAssignDistributionSet.name==*
+
+# Find filters for a specific auto-assign distribution set
+autoAssignDistributionSet.name==Firmware;autoAssignDistributionSet.version==2.0.0
+```
+
+---
+
+## Common Query Patterns
+
+### Combining Multiple Conditions (AND)
+```
+status==RUNNING;createdAt=gt=1704067200000
+```
+
+### Alternative Conditions (OR)
+```
+status==ERROR,status==CANCELED
+```
+
+### Complex Queries with Grouping
+```
+(status==RUNNING,status==SCHEDULED);target.updateStatus!=ERROR
+```
+
+### Timestamp Filtering
+Timestamps are in milliseconds since Unix epoch:
+```
+# Created after January 1, 2024
+createdAt=gt=1704067200000
+
+# Modified in the last 24 hours (example timestamp)
+lastModifiedAt=gt=1704153600000
+```
+
+### Wildcard Patterns
+```
+# Starts with
+name==prefix*
+
+# Ends with
+name==*suffix
+
+# Contains
+name==*substring*
+```
diff --git a/hawkbit-mcp/src/main/resources/prompts/hawkbit-context.md b/hawkbit-mcp/src/main/resources/prompts/hawkbit-context.md
new file mode 100644
index 0000000000..39599f3a67
--- /dev/null
+++ b/hawkbit-mcp/src/main/resources/prompts/hawkbit-context.md
@@ -0,0 +1,48 @@
+# hawkBit MCP Server - Getting Started
+
+You are connected to the **Eclipse hawkBit MCP Server**. hawkBit is a domain-independent
+back-end framework for rolling out software updates to IoT devices.
+
+## What You Can Do
+
+### Tools Available
+You have access to tools for querying the hawkBit Management API:
+- `list_targets` - Query devices that can receive software updates
+- `list_distribution_sets` - Query software packages for deployment
+- `list_rollouts` - Query rollout campaigns for mass deployments
+- `list_actions` - Query deployment operations assigned to targets
+- `list_software_modules` - Query individual software components
+- `list_target_filters` - Query RSQL filters for grouping targets
+
+All tools support RSQL filtering. Read the "hawkBit Entity Definitions" resource for query syntax.
+
+### Documentation Resources
+The following documentation is available (read with MCP resources):
+
+**Getting Started:**
+- `hawkbit://docs/overview` - High-level introduction
+- `hawkbit://docs/what-is-hawkbit` - Why hawkBit exists
+- `hawkbit://docs/features` - Feature overview
+- `hawkbit://docs/architecture` - System architecture
+
+**Core Concepts:**
+- `hawkbit://docs/datamodel` - Entity relationships (targets, distribution sets, modules)
+- `hawkbit://docs/rollout-management` - How rollouts work
+- `hawkbit://docs/target-state` - Target state machine
+- `hawkbit://docs/authentication` - Security and authentication
+- `hawkbit://docs/authorization` - Permissions and access control
+
+**APIs:**
+- `hawkbit://docs/management-api` - REST API for management
+- `hawkbit://docs/ddi-api` - Device polling API
+- `hawkbit://docs/dmf-api` - AMQP-based device federation
+
+**Reference:**
+- `hawkbit://docs/entity-definitions` - RSQL filtering syntax and examples
+
+## Recommended First Steps
+
+1. **For general questions about hawkBit**: Read `hawkbit://docs/overview` or `hawkbit://docs/features`
+2. **For data model questions**: Read `hawkbit://docs/datamodel`
+3. **For RSQL query help**: Read `hawkbit://docs/entity-definitions`
+4. **For rollout/deployment questions**: Read `hawkbit://docs/rollout-management`
diff --git a/hawkbit-mcp/src/main/resources/prompts/rsql-help.md b/hawkbit-mcp/src/main/resources/prompts/rsql-help.md
new file mode 100644
index 0000000000..39430c6923
--- /dev/null
+++ b/hawkbit-mcp/src/main/resources/prompts/rsql-help.md
@@ -0,0 +1,51 @@
+# RSQL Query Syntax for hawkBit
+
+RSQL is a query language for filtering entities. Use it with the `rsql` parameter in list tools.
+
+## Operators
+
+| Operator | Meaning | Example |
+|----------|---------|---------|
+| `==` | Equal | `name==MyTarget` |
+| `!=` | Not equal | `status!=ERROR` |
+| `=lt=` | Less than | `createdAt=lt=1704067200000` |
+| `=gt=` | Greater than | `lastTargetQuery=gt=1704067200000` |
+| `=in=` | In list | `status=in=(RUNNING,FINISHED)` |
+| `=out=` | Not in list | `updateStatus=out=(ERROR,UNKNOWN)` |
+
+## Combining Conditions
+
+- **AND**: Use `;` → `status==RUNNING;name==Device*`
+- **OR**: Use `,` → `status==ERROR,status==CANCELED`
+
+## Wildcards
+
+Use `*` for pattern matching:
+- `name==Device*` - Starts with "Device"
+- `name==*Controller` - Ends with "Controller"
+- `name==*test*` - Contains "test"
+
+## Nested Fields
+
+Access related entities with dot notation:
+- `assignedDistributionSet.name==Firmware`
+- `target.controllerId==device-001`
+- `metadata.environment==production`
+
+## Common Queries
+
+```
+# Targets with errors
+updateStatus==ERROR
+
+# Running rollouts
+status==RUNNING
+
+# Actions for a specific target
+target.controllerId==device-001
+
+# Distribution sets by type
+type.key==os_app
+```
+
+For complete field reference, read `hawkbit://docs/entity-definitions`.
diff --git a/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties b/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties
index b53cfc9d7d..f8616403c2 100644
--- a/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties
+++ b/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties
@@ -68,4 +68,5 @@ springdoc.packages-to-scan=org.eclipse.hawkbit.mgmt,org.eclipse.hawkbit.ddi
springdoc.paths-to-exclude=/system/**
springdoc.swagger-ui.enabled=true
springdoc.swagger-ui.csrf.enabled=true
+
springdoc.swagger-ui.doc-expansion=none
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index a4ef62fba3..a045a44125 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,8 @@
2.21.0
4.5.0
1.8.0
+
+ 1.1.2
5.4.0
4.8.184
@@ -241,6 +243,14 @@
pom
import
+
+
+ org.springframework.ai
+ spring-ai-bom
+ ${spring-ai.version}
+ pom
+ import
+
org.springframework.boot
spring-boot-starter-web
@@ -728,6 +738,7 @@
hawkbit-mgmt
hawkbit-ddi
hawkbit-dmf
+ hawkbit-mcp
hawkbit-monolith
hawkbit-ui
From 5946a0934b785eaafc0ab852b27a63eedd30974f Mon Sep 17 00:00:00 2001
From: Denislav Prinov
Date: Tue, 20 Jan 2026 11:47:45 +0200
Subject: [PATCH 2/5] Fix STDIO authentication support. Change license headers.
Inline Docker build
Signed-off-by: Denislav Prinov
---
docker/build/build_dev.sh | 2 +
hawkbit-mcp/Dockerfile | 25 ----
hawkbit-mcp/README.md | 109 ++++++++++++++++++
hawkbit-mcp/pom.xml | 2 +-
....java => HawkbitMcpServerApplication.java} | 10 +-
...va => HawkbitAuthenticationValidator.java} | 20 ++--
.../client/McpAuthenticationException.java | 2 +-
...n.java => HawkbitClientConfiguration.java} | 51 ++++++--
...perties.java => HawkbitMcpProperties.java} | 25 +++-
.../config/McpSecurityConfiguration.java | 16 ++-
.../server/config/McpToolConfiguration.java | 22 ++--
.../mcp/server/dto/ActionOperation.java | 2 +-
.../hawkbit/mcp/server/dto/ListRequest.java | 2 +-
.../mcp/server/dto/ManageActionRequest.java | 2 +-
.../dto/ManageDistributionSetRequest.java | 2 +-
.../mcp/server/dto/ManageRolloutRequest.java | 2 +-
.../dto/ManageSoftwareModuleRequest.java | 2 +-
.../server/dto/ManageTargetFilterRequest.java | 2 +-
.../mcp/server/dto/ManageTargetRequest.java | 2 +-
.../hawkbit/mcp/server/dto/Operation.java | 2 +-
.../mcp/server/dto/OperationResponse.java | 2 +-
.../hawkbit/mcp/server/dto/PagedResponse.java | 2 +-
.../mcp/server/dto/RolloutOperation.java | 2 +-
...ovider.java => HawkbitPromptProvider.java} | 4 +-
...java => HawkbitDocumentationResource.java} | 4 +-
...vider.java => HawkbitMcpToolProvider.java} | 20 ++--
.../src/main/resources/application.properties | 8 +-
.../src/main/resources/application.properties | 1 -
28 files changed, 247 insertions(+), 98 deletions(-)
delete mode 100644 hawkbit-mcp/Dockerfile
create mode 100644 hawkbit-mcp/README.md
rename hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/{HawkBitMcpServerApplication.java => HawkbitMcpServerApplication.java} (76%)
rename hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/{HawkBitAuthenticationValidator.java => HawkbitAuthenticationValidator.java} (89%)
rename hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/{HawkBitClientConfiguration.java => HawkbitClientConfiguration.java} (55%)
rename hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/{HawkBitMcpProperties.java => HawkbitMcpProperties.java} (89%)
rename hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/{HawkBitPromptProvider.java => HawkbitPromptProvider.java} (96%)
rename hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/{HawkBitDocumentationResource.java => HawkbitDocumentationResource.java} (98%)
rename hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/{HawkBitMcpToolProvider.java => HawkbitMcpToolProvider.java} (97%)
diff --git a/docker/build/build_dev.sh b/docker/build/build_dev.sh
index 2da820bf7d..98efacfd57 100755
--- a/docker/build/build_dev.sh
+++ b/docker/build/build_dev.sh
@@ -62,6 +62,8 @@ if [ -z "$1" ]; then
build "hawkbit-update-server"
# db init
build "hawkbit-repository-jpa-init"
+ # mcp server
+ build "hawkbit-mcp-server"
else
echo "Build $1"
build $1
diff --git a/hawkbit-mcp/Dockerfile b/hawkbit-mcp/Dockerfile
deleted file mode 100644
index b25b5672a0..0000000000
--- a/hawkbit-mcp/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-#
-# Copyright (c) 2025 Contributors to the Eclipse Foundation
-#
-# This program and the accompanying materials are made
-# available under the terms of the Eclipse Public License 2.0
-# which is available at https://www.eclipse.org/legal/epl-2.0/
-#
-# SPDX-License-Identifier: EPL-2.0
-#
-
-FROM eclipse-temurin:17-jre-alpine
-
-LABEL maintainer="Eclipse hawkBit Project"
-LABEL org.opencontainers.image.source="https://github.com/eclipse-hawkbit/hawkbit"
-LABEL org.opencontainers.image.description="Standalone MCP Server for hawkBit"
-
-ARG JAR_FILE=target/*.jar
-
-COPY ${JAR_FILE} app.jar
-
-EXPOSE 8081
-
-ENV HAWKBIT_URL=http://localhost:8080
-
-ENTRYPOINT ["java", "-jar", "/app.jar"]
diff --git a/hawkbit-mcp/README.md b/hawkbit-mcp/README.md
new file mode 100644
index 0000000000..54e78db61f
--- /dev/null
+++ b/hawkbit-mcp/README.md
@@ -0,0 +1,109 @@
+# hawkBit MCP Server
+
+A standalone [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that provides AI assistants with tools to interact with [Eclipse hawkBit](https://www.eclipse.org/hawkbit/) for IoT device software update management.
+
+## Building
+
+From the project root directory:
+
+```bash
+mvn clean package -pl hawkbit-mcp -am -DskipTests
+```
+
+The JAR will be created at: `hawkbit-mcp/target/hawkbit-mcp-server-0-SNAPSHOT.jar`
+
+## Configuration
+
+The MCP server supports two transport modes:
+
+| Mode | Use Case | Authentication |
+|------|----------|----------------|
+| **HTTP/SSE** | Remote access, multi-user | Per-request via `Authorization` header |
+| **STDIO** | Local CLI tools (e.g., Claude Code) | Environment variables |
+
+
+### HTTP Transport
+
+Use HTTP transport when running the server as a standalone service:
+
+```json
+{
+ "mcpServers": {
+ "hawkbit-mcp": {
+ "type": "http",
+ "url": "http://localhost:8081/mcp",
+ "headers": {
+ "Authorization": "Basic "
+ }
+ }
+ }
+}
+```
+
+Start the server separately:
+
+```bash
+java -jar hawkbit-mcp-server-0-SNAPSHOT.jar \
+ --hawkbit.mcp.mgmt-url=
+```
+
+**Generating Base64 credentials:**
+
+```bash
+# Linux/Mac
+echo -n "\\:" | base64
+
+# PowerShell
+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("\:"))
+```
+
+### STDIO Transport
+
+Use STDIO transport for direct integration:
+
+```json
+{
+ "mcpServers": {
+ "hawkbit-mcp": {
+ "command": "java",
+ "args": [
+ "-Dspring.ai.mcp.server.stdio=true",
+ "-Dspring.main.web-application-type=none",
+ "-jar",
+ "/path/to/hawkbit-mcp-server-0-SNAPSHOT.jar"
+ ],
+ "env": {
+ "HAWKBIT_URL": "",
+ "HAWKBIT_USERNAME": "\\",
+ "HAWKBIT_PASSWORD": ""
+ }
+ }
+ }
+}
+```
+
+## Configuration Properties
+
+| Property | Environment Variable | Description | Default |
+|----------|---------------------|-------------|---------|
+| `hawkbit.mcp.mgmt-url` | `HAWKBIT_URL` | hawkBit Management API URL | `http://localhost:8080` |
+| `hawkbit.mcp.username` | `HAWKBIT_USERNAME` | Username for STDIO mode | - |
+| `hawkbit.mcp.password` | `HAWKBIT_PASSWORD` | Password for STDIO mode | - |
+| `hawkbit.mcp.validation.enabled` | - | Validate credentials against hawkBit | `true` |
+| `hawkbit.mcp.validation.cache-ttl` | - | Cache TTL for auth validation | `600s` |
+
+### Operation Controls
+
+You can enable/disable specific operations globally or per-entity:
+
+```properties
+# Global: disable all deletes
+hawkbit.mcp.operations.delete-enabled=false
+
+# Per-entity: allow delete for targets only
+hawkbit.mcp.operations.targets.delete-enabled=true
+
+# Disable rollout lifecycle operations
+hawkbit.mcp.operations.rollouts.start-enabled=false
+hawkbit.mcp.operations.rollouts.approve-enabled=false
+```
diff --git a/hawkbit-mcp/pom.xml b/hawkbit-mcp/pom.xml
index db8048aa65..089263b3e3 100644
--- a/hawkbit-mcp/pom.xml
+++ b/hawkbit-mcp/pom.xml
@@ -115,7 +115,7 @@
org.springframework.boot
spring-boot-maven-plugin
- org.eclipse.hawkbit.mcp.server.HawkBitMcpServerApplication
+ org.eclipse.hawkbit.mcp.server.HawkbitMcpServerApplication
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkBitMcpServerApplication.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkbitMcpServerApplication.java
similarity index 76%
rename from hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkBitMcpServerApplication.java
rename to hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkbitMcpServerApplication.java
index f7d4073635..eb3c7c6746 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkBitMcpServerApplication.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/HawkbitMcpServerApplication.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -9,7 +9,7 @@
*/
package org.eclipse.hawkbit.mcp.server;
-import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
+import org.eclipse.hawkbit.mcp.server.config.HawkbitMcpProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -23,10 +23,10 @@
*
*/
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
-@EnableConfigurationProperties(HawkBitMcpProperties.class)
-public class HawkBitMcpServerApplication {
+@EnableConfigurationProperties(HawkbitMcpProperties.class)
+public class HawkbitMcpServerApplication {
public static void main(String[] args) {
- SpringApplication.run(HawkBitMcpServerApplication.class, args);
+ SpringApplication.run(HawkbitMcpServerApplication.class, args);
}
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkBitAuthenticationValidator.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
similarity index 89%
rename from hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkBitAuthenticationValidator.java
rename to hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
index f73d129c7b..83c69dc510 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkBitAuthenticationValidator.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -13,7 +13,7 @@
import com.github.benmanes.caffeine.cache.Caffeine;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
-import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
+import org.eclipse.hawkbit.mcp.server.config.HawkbitMcpProperties;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
@@ -30,16 +30,16 @@
*/
@Slf4j
@Component
-public class HawkBitAuthenticationValidator {
+public class HawkbitAuthenticationValidator {
private final HawkbitClient hawkbitClient;
private final Tenant dummyTenant;
private final Cache validationCache;
private final boolean enabled;
- public HawkBitAuthenticationValidator(HawkbitClient hawkbitClient,
- Tenant dummyTenant,
- HawkBitMcpProperties properties) {
+ public HawkbitAuthenticationValidator(final HawkbitClient hawkbitClient,
+ final Tenant dummyTenant,
+ final HawkbitMcpProperties properties) {
this.hawkbitClient = hawkbitClient;
this.dummyTenant = dummyTenant;
this.enabled = properties.getValidation().isEnabled();
@@ -60,7 +60,7 @@ public HawkBitAuthenticationValidator(HawkbitClient hawkbitClient,
* @param authHeader the Authorization header value
* @return validation result
*/
- public ValidationResult validate(String authHeader) {
+ public ValidationResult validate(final String authHeader) {
if (!enabled) {
return ValidationResult.VALID;
}
@@ -77,10 +77,10 @@ public ValidationResult validate(String authHeader) {
return cachedResult ? ValidationResult.VALID : ValidationResult.INVALID_CREDENTIALS;
}
- return validateWithHawkBit(cacheKey);
+ return validateWithHawkbit(cacheKey);
}
- private ValidationResult validateWithHawkBit(String cacheKey) {
+ private ValidationResult validateWithHawkbit(final String cacheKey) {
log.debug("Validating authentication against hawkBit using SDK");
try {
@@ -119,7 +119,7 @@ private ValidationResult validateWithHawkBit(String cacheKey) {
}
}
- private String hashAuthHeader(String authHeader) {
+ private String hashAuthHeader(final String authHeader) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(authHeader.getBytes(StandardCharsets.UTF_8));
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java
index 8fd631bfd7..9f07ceb06b 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/McpAuthenticationException.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
similarity index 55%
rename from hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitClientConfiguration.java
rename to hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
index b334743fff..4809331a3d 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -13,6 +13,7 @@
import feign.RequestInterceptor;
import feign.codec.Decoder;
import feign.codec.Encoder;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.sdk.Controller;
import org.eclipse.hawkbit.sdk.HawkbitClient;
@@ -25,22 +26,27 @@
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
import java.util.function.BiFunction;
/**
* Configuration for the hawkBit SDK client.
*
* Uses a custom request interceptor to inject authentication from the
- * current HTTP request context, enabling per-request credentials.
+ * current HTTP request context (HTTP mode) or from static credentials (STDIO mode).
*
*/
@Slf4j
@Configuration
-public class HawkBitClientConfiguration {
+@RequiredArgsConstructor
+public class HawkbitClientConfiguration {
+
+ private final HawkbitMcpProperties properties;
@Bean
@Primary
- public HawkbitServer hawkbitServer(final HawkBitMcpProperties properties) {
+ public HawkbitServer hawkbitServer() {
HawkbitServer server = new HawkbitServer();
server.setMgmtUrl(properties.getMgmtUrl());
log.info("Configured hawkBit server URL: {}", properties.getMgmtUrl());
@@ -63,27 +69,52 @@ public HawkbitClient hawkbitClient(final HawkbitServer server,
private BiFunction requestContextInterceptor() {
return (tenant, controller) -> template -> {
+ String authHeader = null;
+
+ // Try request context first (HTTP mode)
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (attrs != null) {
- String authHeader = (String) attrs.getAttribute(
+ authHeader = (String) attrs.getAttribute(
McpSecurityConfiguration.AUTH_HEADER_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST);
if (authHeader != null) {
- template.header(HttpHeaders.AUTHORIZATION, authHeader);
- log.trace("Injected auth header from request context");
+ log.trace("Using auth header from HTTP request context");
}
}
+
+ // Fall back to static credentials (STDIO mode)
+ if (authHeader == null && properties.hasStaticCredentials()) {
+ String credentials = properties.getUsername() + ":" + properties.getPassword();
+ authHeader = "Basic " + Base64.getEncoder().encodeToString(
+ credentials.getBytes(StandardCharsets.UTF_8));
+ log.trace("Using static credentials from properties (STDIO mode)");
+ }
+
+ // Apply header if available
+ if (authHeader != null) {
+ template.header(HttpHeaders.AUTHORIZATION, authHeader);
+ } else {
+ log.warn("No authentication available - request will likely fail");
+ }
};
}
/**
- * Dummy tenant bean - actual authentication comes from request context via interceptor.
+ * Tenant bean - uses static credentials if configured (STDIO mode),
+ * otherwise actual authentication comes from request context via interceptor (HTTP mode).
*/
@Bean
public Tenant dummyTenant() {
Tenant tenant = new Tenant();
- tenant.setUsername(null);
- tenant.setPassword(null);
+ if (properties.hasStaticCredentials()) {
+ tenant.setUsername(properties.getUsername());
+ tenant.setPassword(properties.getPassword());
+ log.info("Configured tenant with static credentials (STDIO mode)");
+ } else {
+ tenant.setUsername(null);
+ tenant.setPassword(null);
+ log.info("Configured tenant without static credentials (HTTP mode - per-request auth)");
+ }
return tenant;
}
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitMcpProperties.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
similarity index 89%
rename from hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitMcpProperties.java
rename to hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
index 5b99065175..1b7ae875fe 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkBitMcpProperties.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -22,7 +22,7 @@
@Data
@Validated
@ConfigurationProperties(prefix = "hawkbit.mcp")
-public class HawkBitMcpProperties {
+public class HawkbitMcpProperties {
/**
* Base URL of the hawkBit Management API (e.g., ... ).
@@ -30,6 +30,27 @@ public class HawkBitMcpProperties {
@NotBlank(message = "hawkbit.mcp.mgmt-url must be configured")
private String mgmtUrl;
+ /**
+ * Username for hawkBit authentication (used in STDIO mode).
+ * Format: {@code tenant\\username} or just username depending on hawkBit configuration.
+ * Can be set via HAWKBIT_USERNAME environment variable.
+ */
+ private String username;
+
+ /**
+ * Password for hawkBit authentication (used in STDIO mode).
+ * Can be set via HAWKBIT_PASSWORD environment variable.
+ */
+ private String password;
+
+ /**
+ * Check if static credentials are configured.
+ */
+ public boolean hasStaticCredentials() {
+ return username != null && !username.isBlank()
+ && password != null && !password.isBlank();
+ }
+
/**
* Whether to enable the built-in hawkBit tools.
* Set to false to provide custom tool implementations.
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
index 7af2bf583a..b900fba9af 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -16,8 +16,9 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.eclipse.hawkbit.mcp.server.client.HawkBitAuthenticationValidator;
-import org.eclipse.hawkbit.mcp.server.client.HawkBitAuthenticationValidator.ValidationResult;
+import org.eclipse.hawkbit.mcp.server.client.HawkbitAuthenticationValidator;
+import org.eclipse.hawkbit.mcp.server.client.HawkbitAuthenticationValidator.ValidationResult;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
@@ -35,11 +36,16 @@
/**
* Security configuration for the MCP server.
+ *
+ * This configuration is only active in HTTP/servlet mode. In STDIO mode,
+ * authentication is handled via static credentials from properties.
+ *
*/
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class McpSecurityConfiguration {
/**
@@ -47,7 +53,7 @@ public class McpSecurityConfiguration {
*/
public static final String AUTH_HEADER_ATTRIBUTE = "hawkbit.mcp.auth.header";
- private final HawkBitAuthenticationValidator authenticationValidator;
+ private final HawkbitAuthenticationValidator authenticationValidator;
@Bean
@SuppressWarnings("java:S4502") // CSRF protection is not needed for stateless REST APIs using Authorization header
@@ -69,7 +75,7 @@ public SecurityFilterChain mcpSecurityFilterChain(final HttpSecurity http) throw
@RequiredArgsConstructor
public static class HawkBitAuthenticationFilter extends OncePerRequestFilter {
- private final HawkBitAuthenticationValidator validator;
+ private final HawkbitAuthenticationValidator validator;
@Override
protected void doFilterInternal(final HttpServletRequest request, final @NonNull HttpServletResponse response,
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java
index a9bf1b70b4..6320303e22 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpToolConfiguration.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -9,9 +9,9 @@
*/
package org.eclipse.hawkbit.mcp.server.config;
-import org.eclipse.hawkbit.mcp.server.prompts.HawkBitPromptProvider;
-import org.eclipse.hawkbit.mcp.server.resources.HawkBitDocumentationResource;
-import org.eclipse.hawkbit.mcp.server.tools.HawkBitMcpToolProvider;
+import org.eclipse.hawkbit.mcp.server.prompts.HawkbitPromptProvider;
+import org.eclipse.hawkbit.mcp.server.resources.HawkbitDocumentationResource;
+import org.eclipse.hawkbit.mcp.server.tools.HawkbitMcpToolProvider;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -44,11 +44,11 @@ public class McpToolConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "hawkbit.mcp.tools-enabled", havingValue = "true", matchIfMissing = true)
- public HawkBitMcpToolProvider hawkBitMcpToolProvider(
+ public HawkbitMcpToolProvider hawkBitMcpToolProvider(
final HawkbitClient hawkbitClient,
final Tenant dummyTenant,
- final HawkBitMcpProperties properties) {
- return new HawkBitMcpToolProvider(hawkbitClient, dummyTenant, properties);
+ final HawkbitMcpProperties properties) {
+ return new HawkbitMcpToolProvider(hawkbitClient, dummyTenant, properties);
}
/**
@@ -62,8 +62,8 @@ public HawkBitMcpToolProvider hawkBitMcpToolProvider(
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "hawkbit.mcp.resources-enabled", havingValue = "true", matchIfMissing = true)
- public HawkBitDocumentationResource hawkBitDocumentationResource() {
- return new HawkBitDocumentationResource();
+ public HawkbitDocumentationResource hawkBitDocumentationResource() {
+ return new HawkbitDocumentationResource();
}
/**
@@ -77,7 +77,7 @@ public HawkBitDocumentationResource hawkBitDocumentationResource() {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "hawkbit.mcp.prompts-enabled", havingValue = "true", matchIfMissing = true)
- public HawkBitPromptProvider hawkBitPromptProvider() {
- return new HawkBitPromptProvider();
+ public HawkbitPromptProvider hawkBitPromptProvider() {
+ return new HawkbitPromptProvider();
}
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
index 2a5bce899e..43aa212d1d 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java
index c165df4678..bc425a503f 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ListRequest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
index af373a0a24..c22738e3d8 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
index 33612efa4f..f6b305a49f 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
index 21ff963511..fe7d546851 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
index ae3b4dbaa1..1790ae1834 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
index b1537563df..223ba63208 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
index 2ace1c616b..69faf57799 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
index 5987bcd376..8edac82184 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java
index 910eda378b..40a056e28b 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/OperationResponse.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java
index 62e121d89d..c26107ddce 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/PagedResponse.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
index f48113c253..994166230b 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkBitPromptProvider.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkbitPromptProvider.java
similarity index 96%
rename from hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkBitPromptProvider.java
rename to hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkbitPromptProvider.java
index 7b4dbf9db4..a41176c2ec 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkBitPromptProvider.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/prompts/HawkbitPromptProvider.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -29,7 +29,7 @@
*
*/
@Slf4j
-public class HawkBitPromptProvider {
+public class HawkbitPromptProvider {
private static final String PROMPTS_PATH = "prompts/";
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkBitDocumentationResource.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkbitDocumentationResource.java
similarity index 98%
rename from hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkBitDocumentationResource.java
rename to hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkbitDocumentationResource.java
index 2f05a5fe55..e53b1f5030 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkBitDocumentationResource.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/resources/HawkbitDocumentationResource.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -20,7 +20,7 @@
* MCP resources providing hawkBit documentation for LLMs.
*/
@Slf4j
-public class HawkBitDocumentationResource {
+public class HawkbitDocumentationResource {
private static final String DOCS_PATH = "hawkbit-docs/";
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkBitMcpToolProvider.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
similarity index 97%
rename from hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkBitMcpToolProvider.java
rename to hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
index ec6a14d51f..9b9c883b2e 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkBitMcpToolProvider.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -11,7 +11,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
+import org.eclipse.hawkbit.mcp.server.config.HawkbitMcpProperties;
import org.eclipse.hawkbit.mcp.server.dto.ActionOperation;
import org.eclipse.hawkbit.mcp.server.dto.ListRequest;
import org.eclipse.hawkbit.mcp.server.dto.ManageActionRequest;
@@ -54,7 +54,7 @@
*/
@Slf4j
@RequiredArgsConstructor
-public class HawkBitMcpToolProvider {
+public class HawkbitMcpToolProvider {
private static final String OP_CREATE = "CREATE";
private static final String OP_UPDATE = "UPDATE";
@@ -71,7 +71,7 @@ public class HawkBitMcpToolProvider {
private final HawkbitClient hawkbitClient;
private final Tenant dummyTenant;
- private final HawkBitMcpProperties properties;
+ private final HawkbitMcpProperties properties;
private PagedResponse toPagedResponse(final PagedList pagedList, final ListRequest request) {
if (pagedList == null) {
@@ -486,7 +486,7 @@ private void validateOperation(final Operation operation, final String entity) {
private void validateRolloutOperation(final RolloutOperation operation) {
final String opName = operation.name().toLowerCase().replace("_", "-");
- final HawkBitMcpProperties.RolloutConfig config = properties.getOperations().getRollouts();
+ final HawkbitMcpProperties.RolloutConfig config = properties.getOperations().getRollouts();
final Boolean entitySetting = config.getOperationEnabled(opName);
// For standard CRUD ops, check global fallback
@@ -508,7 +508,7 @@ private void validateRolloutOperation(final RolloutOperation operation) {
private void validateActionOperation(final ActionOperation operation) {
final String opName = operation.name().toLowerCase().replace("_", "-");
- final HawkBitMcpProperties.ActionConfig config = properties.getOperations().getActions();
+ final HawkbitMcpProperties.ActionConfig config = properties.getOperations().getActions();
final Boolean entitySetting = config.getOperationEnabled(opName);
if (entitySetting == null) {
@@ -528,8 +528,8 @@ private void validateActionOperation(final ActionOperation operation) {
}
private boolean isOperationEnabled(final String operation, final String entity) {
- final HawkBitMcpProperties.Operations ops = properties.getOperations();
- final HawkBitMcpProperties.EntityConfig entityConfig = getEntityConfig(entity);
+ final HawkbitMcpProperties.Operations ops = properties.getOperations();
+ final HawkbitMcpProperties.EntityConfig entityConfig = getEntityConfig(entity);
final Boolean entitySetting = entityConfig != null ? entityConfig.getOperationEnabled(operation) : null;
if (entitySetting != null) {
@@ -539,8 +539,8 @@ private boolean isOperationEnabled(final String operation, final String entity)
return ops.isGlobalOperationEnabled(operation);
}
- private HawkBitMcpProperties.EntityConfig getEntityConfig(final String entity) {
- final HawkBitMcpProperties.Operations ops = properties.getOperations();
+ private HawkbitMcpProperties.EntityConfig getEntityConfig(final String entity) {
+ final HawkbitMcpProperties.Operations ops = properties.getOperations();
return switch (entity.toLowerCase()) {
case "targets" -> ops.getTargets();
case "rollouts" -> ops.getRollouts();
diff --git a/hawkbit-mcp/src/main/resources/application.properties b/hawkbit-mcp/src/main/resources/application.properties
index e5472e16fe..2d64ebab9a 100644
--- a/hawkbit-mcp/src/main/resources/application.properties
+++ b/hawkbit-mcp/src/main/resources/application.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2025 Contributors to the Eclipse Foundation
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
@@ -28,6 +28,12 @@ spring.ai.mcp.server.capabilities.prompt=true
# hawkBit connection configuration
hawkbit.mcp.mgmt-url=${HAWKBIT_URL:http://localhost:8080}
+# Static credentials for STDIO mode (optional, not needed for HTTP mode with per-request auth)
+# Can be set via environment variables: HAWKBIT_USERNAME, HAWKBIT_PASSWORD
+# Format for username: tenant\username or just username depending on hawkBit configuration
+hawkbit.mcp.username=${HAWKBIT_USERNAME:}
+hawkbit.mcp.password=${HAWKBIT_PASSWORD:}
+
# Authentication validation configuration
hawkbit.mcp.validation.enabled=true
hawkbit.mcp.validation.cache-ttl=600s
diff --git a/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties b/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties
index f8616403c2..b53cfc9d7d 100644
--- a/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties
+++ b/hawkbit-monolith/hawkbit-update-server/src/main/resources/application.properties
@@ -68,5 +68,4 @@ springdoc.packages-to-scan=org.eclipse.hawkbit.mgmt,org.eclipse.hawkbit.ddi
springdoc.paths-to-exclude=/system/**
springdoc.swagger-ui.enabled=true
springdoc.swagger-ui.csrf.enabled=true
-
springdoc.swagger-ui.doc-expansion=none
\ No newline at end of file
From 08473d36eac35974608a605fd9c3584d748a522a Mon Sep 17 00:00:00 2001
From: Denislav Prinov
Date: Tue, 3 Feb 2026 12:27:51 +0200
Subject: [PATCH 3/5] Address PR review: refactor operation DTOs to sealed
interfaces, make authentication validator conditional, and separate
HTTP/STDIO client configurations
Signed-off-by: Denislav Prinov
---
hawkbit-mcp/README.md | 6 +-
hawkbit-mcp/pom.xml | 5 +
.../client/AuthenticationValidator.java | 50 ++
.../HawkbitAuthenticationValidator.java | 41 +-
.../client/NoOpAuthenticationValidator.java | 29 +
.../config/HawkbitClientConfiguration.java | 89 +--
.../server/config/HawkbitMcpProperties.java | 2 +
.../config/McpHttpClientConfiguration.java | 89 +++
.../config/McpSecurityConfiguration.java | 10 +-
.../config/McpStdioClientConfiguration.java | 92 ++++
.../config/McpValidationConfiguration.java | 50 ++
.../mcp/server/dto/ActionOperation.java | 18 -
.../hawkbit/mcp/server/dto/ActionRequest.java | 42 ++
.../server/dto/DistributionSetRequest.java | 50 ++
.../mcp/server/dto/ManageActionRequest.java | 28 -
.../dto/ManageDistributionSetRequest.java | 30 -
.../mcp/server/dto/ManageRolloutRequest.java | 32 --
.../dto/ManageSoftwareModuleRequest.java | 30 -
.../server/dto/ManageTargetFilterRequest.java | 26 -
.../mcp/server/dto/ManageTargetRequest.java | 26 -
.../hawkbit/mcp/server/dto/Operation.java | 19 -
.../mcp/server/dto/RolloutOperation.java | 27 -
.../mcp/server/dto/RolloutRequest.java | 119 ++++
.../mcp/server/dto/SoftwareModuleRequest.java | 50 ++
.../mcp/server/dto/TargetFilterRequest.java | 49 ++
.../hawkbit/mcp/server/dto/TargetRequest.java | 49 ++
.../server/tools/HawkbitMcpToolProvider.java | 518 +++++++++---------
.../src/main/resources/application.properties | 4 +
28 files changed, 963 insertions(+), 617 deletions(-)
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/AuthenticationValidator.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/DistributionSetRequest.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/SoftwareModuleRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetFilterRequest.java
create mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetRequest.java
diff --git a/hawkbit-mcp/README.md b/hawkbit-mcp/README.md
index 54e78db61f..7c92f04c06 100644
--- a/hawkbit-mcp/README.md
+++ b/hawkbit-mcp/README.md
@@ -51,10 +51,10 @@ java -jar hawkbit-mcp-server-0-SNAPSHOT.jar \
```bash
# Linux/Mac
-echo -n "\\:" | base64
+echo -n ":" | base64
# PowerShell
-[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("\:"))
+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(":"))
```
### STDIO Transport
@@ -74,7 +74,7 @@ Use STDIO transport for direct integration:
],
"env": {
"HAWKBIT_URL": "",
- "HAWKBIT_USERNAME": "\\",
+ "HAWKBIT_USERNAME": "",
"HAWKBIT_PASSWORD": ""
}
}
diff --git a/hawkbit-mcp/pom.xml b/hawkbit-mcp/pom.xml
index 089263b3e3..c3ad91250f 100644
--- a/hawkbit-mcp/pom.xml
+++ b/hawkbit-mcp/pom.xml
@@ -116,12 +116,17 @@
spring-boot-maven-plugin
org.eclipse.hawkbit.mcp.server.HawkbitMcpServerApplication
+ false
+ repackage
repackage
+
+ exec
+
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/AuthenticationValidator.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/AuthenticationValidator.java
new file mode 100644
index 0000000000..870be1f88c
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/AuthenticationValidator.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.client;
+
+/**
+ * Interface for authentication validation.
+ * Implementations can validate credentials against hawkBit or provide no-op validation.
+ */
+public interface AuthenticationValidator {
+
+ /**
+ * Validates the given authorization header.
+ *
+ * @param authHeader the Authorization header value
+ * @return validation result
+ */
+ ValidationResult validate(String authHeader);
+
+ /**
+ * Result of authentication validation.
+ */
+ enum ValidationResult {
+ /**
+ * Credentials are valid (authenticated user).
+ */
+ VALID,
+
+ /**
+ * No credentials provided.
+ */
+ MISSING_CREDENTIALS,
+
+ /**
+ * Credentials are invalid (401 from hawkBit).
+ */
+ INVALID_CREDENTIALS,
+
+ /**
+ * hawkBit is unavailable or returned unexpected error.
+ */
+ HAWKBIT_ERROR
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
index 83c69dc510..ca34a85954 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
@@ -18,7 +18,6 @@
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -27,44 +26,39 @@
/**
* Validates authentication credentials against hawkBit REST API using the SDK.
+ * This validator is conditionally created when {@code hawkbit.mcp.validation.enabled=true}.
*/
@Slf4j
-@Component
-public class HawkbitAuthenticationValidator {
+public class HawkbitAuthenticationValidator implements AuthenticationValidator {
private final HawkbitClient hawkbitClient;
private final Tenant dummyTenant;
private final Cache validationCache;
- private final boolean enabled;
public HawkbitAuthenticationValidator(final HawkbitClient hawkbitClient,
final Tenant dummyTenant,
final HawkbitMcpProperties properties) {
this.hawkbitClient = hawkbitClient;
this.dummyTenant = dummyTenant;
- this.enabled = properties.getValidation().isEnabled();
this.validationCache = Caffeine.newBuilder()
.expireAfterWrite(properties.getValidation().getCacheTtl())
.maximumSize(properties.getValidation().getCacheMaxSize())
.build();
- log.info("Authentication validation {} with cache TTL={}, maxSize={}",
- enabled ? "enabled" : "disabled",
+ log.info("Authentication validation enabled with cache TTL={}, maxSize={}",
properties.getValidation().getCacheTtl(),
properties.getValidation().getCacheMaxSize());
}
/**
* Validates the given authorization header against hawkBit.
+ *
* @param authHeader the Authorization header value
* @return validation result
*/
+ @Override
public ValidationResult validate(final String authHeader) {
- if (!enabled) {
- return ValidationResult.VALID;
- }
-
if (authHeader == null || authHeader.isBlank()) {
return ValidationResult.MISSING_CREDENTIALS;
}
@@ -129,29 +123,4 @@ private String hashAuthHeader(final String authHeader) {
throw new McpAuthenticationException("SHA-256 not available." + e.getMessage());
}
}
-
- /**
- * Result of authentication validation.
- */
- public enum ValidationResult {
- /**
- * Credentials are valid (authenticated user).
- */
- VALID,
-
- /**
- * No credentials provided.
- */
- MISSING_CREDENTIALS,
-
- /**
- * Credentials are invalid (401 from hawkBit).
- */
- INVALID_CREDENTIALS,
-
- /**
- * hawkBit is unavailable or returned unexpected error.
- */
- HAWKBIT_ERROR
- }
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java
new file mode 100644
index 0000000000..8bd89133b2
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.client;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * No-operation authentication validator that always returns VALID.
+ * Used when authentication validation is disabled via configuration.
+ */
+@Slf4j
+public class NoOpAuthenticationValidator implements AuthenticationValidator {
+
+ public NoOpAuthenticationValidator() {
+ log.info("Authentication validation disabled - using no-op validator");
+ }
+
+ @Override
+ public ValidationResult validate(final String authHeader) {
+ return ValidationResult.VALID;
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
index 4809331a3d..ae9ead241f 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
@@ -9,32 +9,22 @@
*/
package org.eclipse.hawkbit.mcp.server.config;
-import feign.Contract;
-import feign.RequestInterceptor;
-import feign.codec.Decoder;
-import feign.codec.Encoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.eclipse.hawkbit.sdk.Controller;
-import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.HawkbitServer;
-import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
-import org.springframework.http.HttpHeaders;
-import org.springframework.web.context.request.RequestAttributes;
-import org.springframework.web.context.request.RequestContextHolder;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.function.BiFunction;
/**
- * Configuration for the hawkBit SDK client.
+ * Common configuration for the hawkBit SDK client.
*
- * Uses a custom request interceptor to inject authentication from the
- * current HTTP request context (HTTP mode) or from static credentials (STDIO mode).
+ * Provides the {@link HawkbitServer} bean which is used by both HTTP and STDIO mode configurations.
+ * Mode-specific beans (HawkbitClient, Tenant) are provided by:
+ *
+ * {@link McpHttpClientConfiguration} - for HTTP mode
+ * {@link McpStdioClientConfiguration} - for STDIO mode
+ *
*
*/
@Slf4j
@@ -52,69 +42,4 @@ public HawkbitServer hawkbitServer() {
log.info("Configured hawkBit server URL: {}", properties.getMgmtUrl());
return server;
}
-
- @Bean
- public HawkbitClient hawkbitClient(final HawkbitServer server,
- final Encoder encoder,
- final Decoder decoder,
- final Contract contract) {
- return HawkbitClient.builder()
- .hawkBitServer(server)
- .encoder(encoder)
- .decoder(decoder)
- .contract(contract)
- .requestInterceptorFn(requestContextInterceptor())
- .build();
- }
-
- private BiFunction requestContextInterceptor() {
- return (tenant, controller) -> template -> {
- String authHeader = null;
-
- // Try request context first (HTTP mode)
- RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
- if (attrs != null) {
- authHeader = (String) attrs.getAttribute(
- McpSecurityConfiguration.AUTH_HEADER_ATTRIBUTE,
- RequestAttributes.SCOPE_REQUEST);
- if (authHeader != null) {
- log.trace("Using auth header from HTTP request context");
- }
- }
-
- // Fall back to static credentials (STDIO mode)
- if (authHeader == null && properties.hasStaticCredentials()) {
- String credentials = properties.getUsername() + ":" + properties.getPassword();
- authHeader = "Basic " + Base64.getEncoder().encodeToString(
- credentials.getBytes(StandardCharsets.UTF_8));
- log.trace("Using static credentials from properties (STDIO mode)");
- }
-
- // Apply header if available
- if (authHeader != null) {
- template.header(HttpHeaders.AUTHORIZATION, authHeader);
- } else {
- log.warn("No authentication available - request will likely fail");
- }
- };
- }
-
- /**
- * Tenant bean - uses static credentials if configured (STDIO mode),
- * otherwise actual authentication comes from request context via interceptor (HTTP mode).
- */
- @Bean
- public Tenant dummyTenant() {
- Tenant tenant = new Tenant();
- if (properties.hasStaticCredentials()) {
- tenant.setUsername(properties.getUsername());
- tenant.setPassword(properties.getPassword());
- log.info("Configured tenant with static credentials (STDIO mode)");
- } else {
- tenant.setUsername(null);
- tenant.setPassword(null);
- log.info("Configured tenant without static credentials (HTTP mode - per-request auth)");
- }
- return tenant;
- }
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
index 1b7ae875fe..a47f0b326b 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
@@ -11,6 +11,7 @@
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
+import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@@ -165,6 +166,7 @@ public Boolean getOperationEnabled(final String operation) {
* Rollout-specific operation configuration including lifecycle operations.
*/
@Data
+ @EqualsAndHashCode(callSuper = true)
public static class RolloutConfig extends EntityConfig {
private Boolean startEnabled = true;
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
new file mode 100644
index 0000000000..ee9a0dbb68
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.config;
+
+import feign.Contract;
+import feign.RequestInterceptor;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.hawkbit.sdk.Controller;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
+import org.eclipse.hawkbit.sdk.HawkbitServer;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import java.util.function.BiFunction;
+
+/**
+ * Configuration for HTTP mode.
+ *
+ * In HTTP mode, authentication is extracted from the incoming HTTP request's
+ * Authorization header and forwarded to hawkBit (per-request authentication).
+ *
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "spring.ai.mcp.server.stdio", havingValue = "false", matchIfMissing = true)
+public class McpHttpClientConfiguration {
+
+ @Bean
+ public HawkbitClient hawkbitClient(final HawkbitServer server,
+ final Encoder encoder,
+ final Decoder decoder,
+ final Contract contract) {
+ log.info("Configuring hawkBit client for HTTP mode (per-request authentication)");
+ return HawkbitClient.builder()
+ .hawkBitServer(server)
+ .encoder(encoder)
+ .decoder(decoder)
+ .contract(contract)
+ .requestInterceptorFn(httpRequestInterceptor())
+ .build();
+ }
+
+ private BiFunction httpRequestInterceptor() {
+ return (tenant, controller) -> template -> {
+ RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
+ if (attrs != null) {
+ String authHeader = (String) attrs.getAttribute(
+ McpSecurityConfiguration.AUTH_HEADER_ATTRIBUTE,
+ RequestAttributes.SCOPE_REQUEST);
+ if (authHeader != null) {
+ template.header(HttpHeaders.AUTHORIZATION, authHeader);
+ log.trace("Using auth header from HTTP request context");
+ } else {
+ log.warn("No authentication header in request context - request will likely fail");
+ }
+ } else {
+ log.warn("No request context available - request will likely fail");
+ }
+ };
+ }
+
+ /**
+ * Tenant bean for HTTP mode - credentials come from request context via interceptor.
+ */
+ @Bean
+ public Tenant dummyTenant() {
+ Tenant tenant = new Tenant();
+ tenant.setUsername(null);
+ tenant.setPassword(null);
+ log.info("Configured tenant for HTTP mode (per-request authentication)");
+ return tenant;
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
index b900fba9af..723aa75898 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
@@ -16,8 +16,9 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.eclipse.hawkbit.mcp.server.client.HawkbitAuthenticationValidator;
-import org.eclipse.hawkbit.mcp.server.client.HawkbitAuthenticationValidator.ValidationResult;
+import org.eclipse.hawkbit.mcp.server.client.AuthenticationValidator;
+import org.eclipse.hawkbit.mcp.server.client.AuthenticationValidator.ValidationResult;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -46,6 +47,7 @@
@EnableWebSecurity
@RequiredArgsConstructor
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+@ConditionalOnProperty(name = "spring.ai.mcp.server.stdio", havingValue = "false", matchIfMissing = true)
public class McpSecurityConfiguration {
/**
@@ -53,7 +55,7 @@ public class McpSecurityConfiguration {
*/
public static final String AUTH_HEADER_ATTRIBUTE = "hawkbit.mcp.auth.header";
- private final HawkbitAuthenticationValidator authenticationValidator;
+ private final AuthenticationValidator authenticationValidator;
@Bean
@SuppressWarnings("java:S4502") // CSRF protection is not needed for stateless REST APIs using Authorization header
@@ -75,7 +77,7 @@ public SecurityFilterChain mcpSecurityFilterChain(final HttpSecurity http) throw
@RequiredArgsConstructor
public static class HawkBitAuthenticationFilter extends OncePerRequestFilter {
- private final HawkbitAuthenticationValidator validator;
+ private final AuthenticationValidator validator;
@Override
protected void doFilterInternal(final HttpServletRequest request, final @NonNull HttpServletResponse response,
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
new file mode 100644
index 0000000000..bc4eb5d484
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.config;
+
+import feign.Contract;
+import feign.RequestInterceptor;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.hawkbit.sdk.Controller;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
+import org.eclipse.hawkbit.sdk.HawkbitServer;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.function.BiFunction;
+
+/**
+ * Configuration for STDIO mode.
+ *
+ * In STDIO mode, authentication uses static credentials from configuration properties
+ * (environment variables HAWKBIT_USERNAME and HAWKBIT_PASSWORD).
+ *
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "spring.ai.mcp.server.stdio", havingValue = "true")
+public class McpStdioClientConfiguration {
+
+ private final HawkbitMcpProperties properties;
+
+ @Bean
+ public HawkbitClient hawkbitClient(final HawkbitServer server,
+ final Encoder encoder,
+ final Decoder decoder,
+ final Contract contract) {
+ log.info("Configuring hawkBit client for STDIO mode (static credentials)");
+ return HawkbitClient.builder()
+ .hawkBitServer(server)
+ .encoder(encoder)
+ .decoder(decoder)
+ .contract(contract)
+ .requestInterceptorFn(stdioRequestInterceptor())
+ .build();
+ }
+
+ private BiFunction stdioRequestInterceptor() {
+ return (tenant, controller) -> template -> {
+ if (properties.hasStaticCredentials()) {
+ String credentials = properties.getUsername() + ":" + properties.getPassword();
+ String authHeader = "Basic " + Base64.getEncoder().encodeToString(
+ credentials.getBytes(StandardCharsets.UTF_8));
+ template.header(HttpHeaders.AUTHORIZATION, authHeader);
+ log.trace("Using static credentials from properties (STDIO mode)");
+ } else {
+ log.warn("No static credentials configured for STDIO mode - request will likely fail");
+ }
+ };
+ }
+
+ /**
+ * Tenant bean for STDIO mode - uses static credentials from configuration.
+ */
+ @Bean
+ public Tenant dummyTenant() {
+ Tenant tenant = new Tenant();
+ if (properties.hasStaticCredentials()) {
+ tenant.setUsername(properties.getUsername());
+ tenant.setPassword(properties.getPassword());
+ log.info("Configured tenant with static credentials for STDIO mode");
+ } else {
+ tenant.setUsername(null);
+ tenant.setPassword(null);
+ log.warn("STDIO mode enabled but no static credentials configured");
+ }
+ return tenant;
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
new file mode 100644
index 0000000000..14f0b6c120
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.config;
+
+import org.eclipse.hawkbit.mcp.server.client.AuthenticationValidator;
+import org.eclipse.hawkbit.mcp.server.client.HawkbitAuthenticationValidator;
+import org.eclipse.hawkbit.mcp.server.client.NoOpAuthenticationValidator;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configuration for authentication validation beans.
+ * Creates either a real validator (with caching) or a no-op validator based on configuration.
+ */
+@Configuration
+public class McpValidationConfiguration {
+
+ /**
+ * Creates the hawkBit authentication validator when validation is enabled.
+ * It validates against hawkBit REST API.
+ */
+ @Bean
+ @ConditionalOnProperty(name = "hawkbit.mcp.validation.enabled", havingValue = "true", matchIfMissing = true)
+ public AuthenticationValidator hawkbitAuthenticationValidator(
+ final HawkbitClient hawkbitClient,
+ final Tenant dummyTenant,
+ final HawkbitMcpProperties properties) {
+ return new HawkbitAuthenticationValidator(hawkbitClient, dummyTenant, properties);
+ }
+
+ /**
+ * Creates a no-op authentication validator when validation is disabled.
+ * This validator always returns VALID without any actual validation.
+ */
+ @Bean
+ @ConditionalOnProperty(name = "hawkbit.mcp.validation.enabled", havingValue = "false")
+ public AuthenticationValidator noOpAuthenticationValidator() {
+ return new NoOpAuthenticationValidator();
+ }
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
deleted file mode 100644
index 43aa212d1d..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionOperation.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-/**
- * Operations for action management (actions are created indirectly via DS assignment).
- */
-public enum ActionOperation {
- DELETE,
- DELETE_BATCH
-}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java
new file mode 100644
index 0000000000..884463ab6a
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import java.util.List;
+
+/**
+ * Sealed interface for action management operations.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = ActionRequest.Delete.class, name = "Delete"),
+ @JsonSubTypes.Type(value = ActionRequest.DeleteBatch.class, name = "DeleteBatch")
+})
+public sealed interface ActionRequest
+ permits ActionRequest.Delete, ActionRequest.DeleteBatch {
+
+ /**
+ * Request to delete a single action.
+ *
+ * @param actionIds list containing the single action ID to delete (use single-element list)
+ */
+ record Delete(List actionIds) implements ActionRequest {}
+
+ /**
+ * Request to delete multiple actions.
+ *
+ * @param actionIds list of action IDs to delete (mutually exclusive with rsql)
+ * @param rsql RSQL filter query for selecting actions to delete (mutually exclusive with actionIds)
+ */
+ record DeleteBatch(List actionIds, String rsql) implements ActionRequest {}
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/DistributionSetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/DistributionSetRequest.java
new file mode 100644
index 0000000000..b5d4000521
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/DistributionSetRequest.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut;
+
+/**
+ * Sealed interface for distribution set management operations.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = DistributionSetRequest.Create.class, name = "Create"),
+ @JsonSubTypes.Type(value = DistributionSetRequest.Update.class, name = "Update"),
+ @JsonSubTypes.Type(value = DistributionSetRequest.Delete.class, name = "Delete")
+})
+public sealed interface DistributionSetRequest
+ permits DistributionSetRequest.Create, DistributionSetRequest.Update, DistributionSetRequest.Delete {
+
+ /**
+ * Request to create a new distribution set.
+ *
+ * @param body the request body containing distribution set data (name, version, type)
+ */
+ record Create(MgmtDistributionSetRequestBodyPost body) implements DistributionSetRequest {}
+
+ /**
+ * Request to update an existing distribution set.
+ *
+ * @param distributionSetId the distribution set ID
+ * @param body the request body containing updated distribution set data
+ */
+ record Update(Long distributionSetId, MgmtDistributionSetRequestBodyPut body) implements DistributionSetRequest {}
+
+ /**
+ * Request to delete a distribution set.
+ *
+ * @param distributionSetId the distribution set ID to delete
+ */
+ record Delete(Long distributionSetId) implements DistributionSetRequest {}
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
deleted file mode 100644
index c22738e3d8..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageActionRequest.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-import java.util.List;
-
-/**
- * Request wrapper for action management operations.
- * Actions are created indirectly via distribution set assignment, so only DELETE operations are supported.
- *
- * @param operation the operation to perform (DELETE, DELETE_BATCH)
- * @param actionId the action ID (for DELETE single action)
- * @param actionIds list of action IDs (for DELETE_BATCH)
- * @param rsql RSQL filter query (alternative for DELETE_BATCH)
- */
-public record ManageActionRequest(
- ActionOperation operation,
- Long actionId,
- List actionIds,
- String rsql
-) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
deleted file mode 100644
index f6b305a49f..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageDistributionSetRequest.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
-import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut;
-
-/**
- * Request wrapper for distribution set management operations.
- * Reuses existing {@link MgmtDistributionSetRequestBodyPost} for CREATE and
- * {@link MgmtDistributionSetRequestBodyPut} for UPDATE.
- *
- * @param operation the operation to perform (CREATE, UPDATE, DELETE)
- * @param distributionSetId the distribution set ID (required for UPDATE/DELETE)
- * @param createBody the request body for CREATE operation
- * @param updateBody the request body for UPDATE operation
- */
-public record ManageDistributionSetRequest(
- Operation operation,
- Long distributionSetId,
- MgmtDistributionSetRequestBodyPost createBody,
- MgmtDistributionSetRequestBodyPut updateBody
-) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
deleted file mode 100644
index fe7d546851..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageRolloutRequest.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost;
-import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut;
-
-/**
- * Request wrapper for rollout management operations including CRUD and lifecycle.
- * Reuses existing {@link MgmtRolloutRestRequestBodyPost} for CREATE and
- * {@link MgmtRolloutRestRequestBodyPut} for UPDATE.
- *
- * @param operation the operation to perform (CREATE, UPDATE, DELETE, START, PAUSE, STOP, RESUME, APPROVE, DENY, RETRY, TRIGGER_NEXT_GROUP)
- * @param rolloutId the rollout ID (required for UPDATE/DELETE/lifecycle operations)
- * @param createBody the request body for CREATE operation
- * @param updateBody the request body for UPDATE operation
- * @param remark optional remark for APPROVE/DENY operations
- */
-public record ManageRolloutRequest(
- RolloutOperation operation,
- Long rolloutId,
- MgmtRolloutRestRequestBodyPost createBody,
- MgmtRolloutRestRequestBodyPut updateBody,
- String remark
-) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
deleted file mode 100644
index 1790ae1834..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageSoftwareModuleRequest.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
-import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPut;
-
-/**
- * Request wrapper for software module management operations.
- * Reuses existing {@link MgmtSoftwareModuleRequestBodyPost} for CREATE and
- * {@link MgmtSoftwareModuleRequestBodyPut} for UPDATE.
- *
- * @param operation the operation to perform (CREATE, UPDATE, DELETE)
- * @param softwareModuleId the software module ID (required for UPDATE/DELETE)
- * @param createBody the request body for CREATE operation
- * @param updateBody the request body for UPDATE operation
- */
-public record ManageSoftwareModuleRequest(
- Operation operation,
- Long softwareModuleId,
- MgmtSoftwareModuleRequestBodyPost createBody,
- MgmtSoftwareModuleRequestBodyPut updateBody
-) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
deleted file mode 100644
index 223ba63208..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetFilterRequest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody;
-
-/**
- * Request wrapper for target filter query management operations.
- * Reuses existing {@link MgmtTargetFilterQueryRequestBody} for CREATE/UPDATE data.
- *
- * @param operation the operation to perform (CREATE, UPDATE, DELETE)
- * @param filterId the target filter query ID (required for UPDATE/DELETE)
- * @param body the request body containing filter data (for CREATE/UPDATE)
- */
-public record ManageTargetFilterRequest(
- Operation operation,
- Long filterId,
- MgmtTargetFilterQueryRequestBody body
-) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
deleted file mode 100644
index 69faf57799..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ManageTargetRequest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
-
-/**
- * Request wrapper for target management operations.
- * Reuses existing {@link MgmtTargetRequestBody} for CREATE/UPDATE data.
- *
- * @param operation the operation to perform (CREATE, UPDATE, DELETE)
- * @param controllerId the target controller ID (required for UPDATE/DELETE, used as identifier for CREATE)
- * @param body the request body containing target data (for CREATE/UPDATE)
- */
-public record ManageTargetRequest(
- Operation operation,
- String controllerId,
- MgmtTargetRequestBody body
-) {}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
deleted file mode 100644
index 8edac82184..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/Operation.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-/**
- * Standard CRUD operations for entity management tools.
- */
-public enum Operation {
- CREATE,
- UPDATE,
- DELETE
-}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
deleted file mode 100644
index 994166230b..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutOperation.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.dto;
-
-/**
- * Operations for rollout management including CRUD and lifecycle operations.
- */
-public enum RolloutOperation {
- CREATE,
- UPDATE,
- DELETE,
- START,
- PAUSE,
- STOP,
- RESUME,
- APPROVE,
- DENY,
- RETRY,
- TRIGGER_NEXT_GROUP
-}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutRequest.java
new file mode 100644
index 0000000000..02be86c1fc
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/RolloutRequest.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut;
+
+/**
+ * Sealed interface for rollout management operations including CRUD and lifecycle.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = RolloutRequest.Create.class, name = "Create"),
+ @JsonSubTypes.Type(value = RolloutRequest.Update.class, name = "Update"),
+ @JsonSubTypes.Type(value = RolloutRequest.Delete.class, name = "Delete"),
+ @JsonSubTypes.Type(value = RolloutRequest.Start.class, name = "Start"),
+ @JsonSubTypes.Type(value = RolloutRequest.Pause.class, name = "Pause"),
+ @JsonSubTypes.Type(value = RolloutRequest.Stop.class, name = "Stop"),
+ @JsonSubTypes.Type(value = RolloutRequest.Resume.class, name = "Resume"),
+ @JsonSubTypes.Type(value = RolloutRequest.Approve.class, name = "Approve"),
+ @JsonSubTypes.Type(value = RolloutRequest.Deny.class, name = "Deny"),
+ @JsonSubTypes.Type(value = RolloutRequest.Retry.class, name = "Retry"),
+ @JsonSubTypes.Type(value = RolloutRequest.TriggerNextGroup.class, name = "TriggerNextGroup")
+})
+public sealed interface RolloutRequest
+ permits RolloutRequest.Create, RolloutRequest.Update, RolloutRequest.Delete,
+ RolloutRequest.Start, RolloutRequest.Pause, RolloutRequest.Stop,
+ RolloutRequest.Resume, RolloutRequest.Approve, RolloutRequest.Deny,
+ RolloutRequest.Retry, RolloutRequest.TriggerNextGroup {
+
+ /**
+ * Request to create a new rollout.
+ *
+ * @param body the request body containing rollout data
+ */
+ record Create(MgmtRolloutRestRequestBodyPost body) implements RolloutRequest {}
+
+ /**
+ * Request to update an existing rollout.
+ *
+ * @param rolloutId the rollout ID
+ * @param body the request body containing updated rollout data
+ */
+ record Update(Long rolloutId, MgmtRolloutRestRequestBodyPut body) implements RolloutRequest {}
+
+ /**
+ * Request to delete a rollout.
+ *
+ * @param rolloutId the rollout ID to delete
+ */
+ record Delete(Long rolloutId) implements RolloutRequest {}
+
+ /**
+ * Request to start a rollout.
+ *
+ * @param rolloutId the rollout ID to start
+ */
+ record Start(Long rolloutId) implements RolloutRequest {}
+
+ /**
+ * Request to pause a rollout.
+ *
+ * @param rolloutId the rollout ID to pause
+ */
+ record Pause(Long rolloutId) implements RolloutRequest {}
+
+ /**
+ * Request to stop a rollout.
+ *
+ * @param rolloutId the rollout ID to stop
+ */
+ record Stop(Long rolloutId) implements RolloutRequest {}
+
+ /**
+ * Request to resume a paused rollout.
+ *
+ * @param rolloutId the rollout ID to resume
+ */
+ record Resume(Long rolloutId) implements RolloutRequest {}
+
+ /**
+ * Request to approve a rollout.
+ *
+ * @param rolloutId the rollout ID to approve
+ * @param remark optional remark for the approval
+ */
+ record Approve(Long rolloutId, String remark) implements RolloutRequest {}
+
+ /**
+ * Request to deny a rollout.
+ *
+ * @param rolloutId the rollout ID to deny
+ * @param remark optional remark for the denial
+ */
+ record Deny(Long rolloutId, String remark) implements RolloutRequest {}
+
+ /**
+ * Request to retry a rollout.
+ *
+ * @param rolloutId the rollout ID to retry
+ */
+ record Retry(Long rolloutId) implements RolloutRequest {}
+
+ /**
+ * Request to trigger the next group in a rollout.
+ *
+ * @param rolloutId the rollout ID
+ */
+ record TriggerNextGroup(Long rolloutId) implements RolloutRequest {}
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/SoftwareModuleRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/SoftwareModuleRequest.java
new file mode 100644
index 0000000000..11b5bdf33e
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/SoftwareModuleRequest.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPut;
+
+/**
+ * Sealed interface for software module management operations.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = SoftwareModuleRequest.Create.class, name = "Create"),
+ @JsonSubTypes.Type(value = SoftwareModuleRequest.Update.class, name = "Update"),
+ @JsonSubTypes.Type(value = SoftwareModuleRequest.Delete.class, name = "Delete")
+})
+public sealed interface SoftwareModuleRequest
+ permits SoftwareModuleRequest.Create, SoftwareModuleRequest.Update, SoftwareModuleRequest.Delete {
+
+ /**
+ * Request to create a new software module.
+ *
+ * @param body the request body containing software module data (name, version, type)
+ */
+ record Create(MgmtSoftwareModuleRequestBodyPost body) implements SoftwareModuleRequest {}
+
+ /**
+ * Request to update an existing software module.
+ *
+ * @param softwareModuleId the software module ID
+ * @param body the request body containing updated software module data
+ */
+ record Update(Long softwareModuleId, MgmtSoftwareModuleRequestBodyPut body) implements SoftwareModuleRequest {}
+
+ /**
+ * Request to delete a software module.
+ *
+ * @param softwareModuleId the software module ID to delete
+ */
+ record Delete(Long softwareModuleId) implements SoftwareModuleRequest {}
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetFilterRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetFilterRequest.java
new file mode 100644
index 0000000000..ddab9dc730
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetFilterRequest.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody;
+
+/**
+ * Sealed interface for target filter query management operations.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = TargetFilterRequest.Create.class, name = "Create"),
+ @JsonSubTypes.Type(value = TargetFilterRequest.Update.class, name = "Update"),
+ @JsonSubTypes.Type(value = TargetFilterRequest.Delete.class, name = "Delete")
+})
+public sealed interface TargetFilterRequest
+ permits TargetFilterRequest.Create, TargetFilterRequest.Update, TargetFilterRequest.Delete {
+
+ /**
+ * Request to create a new target filter.
+ *
+ * @param body the request body containing filter data (name, query)
+ */
+ record Create(MgmtTargetFilterQueryRequestBody body) implements TargetFilterRequest {}
+
+ /**
+ * Request to update an existing target filter.
+ *
+ * @param filterId the target filter ID
+ * @param body the request body containing updated filter data
+ */
+ record Update(Long filterId, MgmtTargetFilterQueryRequestBody body) implements TargetFilterRequest {}
+
+ /**
+ * Request to delete a target filter.
+ *
+ * @param filterId the target filter ID to delete
+ */
+ record Delete(Long filterId) implements TargetFilterRequest {}
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetRequest.java
new file mode 100644
index 0000000000..3f5b6dbebd
--- /dev/null
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/TargetRequest.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.hawkbit.mcp.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
+
+/**
+ * Sealed interface for target management operations.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = TargetRequest.Create.class, name = "Create"),
+ @JsonSubTypes.Type(value = TargetRequest.Update.class, name = "Update"),
+ @JsonSubTypes.Type(value = TargetRequest.Delete.class, name = "Delete")
+})
+public sealed interface TargetRequest
+ permits TargetRequest.Create, TargetRequest.Update, TargetRequest.Delete {
+
+ /**
+ * Request to create a new target.
+ *
+ * @param body the request body containing target data (controllerId, name, description)
+ */
+ record Create(MgmtTargetRequestBody body) implements TargetRequest {}
+
+ /**
+ * Request to update an existing target.
+ *
+ * @param controllerId the target controller ID
+ * @param body the request body containing updated target data
+ */
+ record Update(String controllerId, MgmtTargetRequestBody body) implements TargetRequest {}
+
+ /**
+ * Request to delete a target.
+ *
+ * @param controllerId the target controller ID to delete
+ */
+ record Delete(String controllerId) implements TargetRequest {}
+}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
index 9b9c883b2e..b636b4705e 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
@@ -12,18 +12,15 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.mcp.server.config.HawkbitMcpProperties;
-import org.eclipse.hawkbit.mcp.server.dto.ActionOperation;
+import org.eclipse.hawkbit.mcp.server.dto.ActionRequest;
+import org.eclipse.hawkbit.mcp.server.dto.DistributionSetRequest;
import org.eclipse.hawkbit.mcp.server.dto.ListRequest;
-import org.eclipse.hawkbit.mcp.server.dto.ManageActionRequest;
-import org.eclipse.hawkbit.mcp.server.dto.ManageDistributionSetRequest;
-import org.eclipse.hawkbit.mcp.server.dto.ManageRolloutRequest;
-import org.eclipse.hawkbit.mcp.server.dto.ManageSoftwareModuleRequest;
-import org.eclipse.hawkbit.mcp.server.dto.ManageTargetFilterRequest;
-import org.eclipse.hawkbit.mcp.server.dto.ManageTargetRequest;
-import org.eclipse.hawkbit.mcp.server.dto.Operation;
import org.eclipse.hawkbit.mcp.server.dto.OperationResponse;
import org.eclipse.hawkbit.mcp.server.dto.PagedResponse;
-import org.eclipse.hawkbit.mcp.server.dto.RolloutOperation;
+import org.eclipse.hawkbit.mcp.server.dto.RolloutRequest;
+import org.eclipse.hawkbit.mcp.server.dto.SoftwareModuleRequest;
+import org.eclipse.hawkbit.mcp.server.dto.TargetFilterRequest;
+import org.eclipse.hawkbit.mcp.server.dto.TargetRequest;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
@@ -197,303 +194,313 @@ public PagedResponse listTargetFilters(final ListRequest
description = "Create, update, or delete targets (devices). " +
"Operations: CREATE (new target with controllerId, name, description), " +
"UPDATE (modify existing target by controllerId), " +
- "DELETE (remove target by controllerId)")
- public OperationResponse manageTarget(final ManageTargetRequest request) {
- validateOperation(request.operation(), "targets");
- log.debug("Managing target: operation={}, controllerId={}", request.operation(), request.controllerId());
+ "DELETE (remove target by controllerId). " +
+ "Use 'type' field to select operation: " +
+ "{\"type\":\"Create\",\"body\":{\"controllerId\":\"id\",\"name\":\"name\"}}, " +
+ "{\"type\":\"Update\",\"controllerId\":\"id\",\"body\":{...}}, " +
+ "{\"type\":\"Delete\",\"controllerId\":\"id\"}")
+ public OperationResponse manageTarget(final TargetRequest request) {
+ log.debug("Managing target: request={}", request.getClass().getSimpleName());
final MgmtTargetRestApi api = hawkbitClient.mgmtService(MgmtTargetRestApi.class, dummyTenant);
- return switch (request.operation()) {
- case CREATE -> {
- if (request.body() == null) {
- yield OperationResponse.failure(OP_CREATE, "Request body is required for CREATE operation");
- }
- final ResponseEntity> response = api.createTargets(List.of(request.body()));
- final List created = response.getBody();
- yield OperationResponse.success(OP_CREATE, "Target created successfully",
- created != null && !created.isEmpty() ? created.get(0) : null);
- }
- case UPDATE -> {
- if (request.controllerId() == null || request.controllerId().isBlank()) {
- yield OperationResponse.failure(OP_UPDATE, "controllerId is required for UPDATE operation");
- }
- if (request.body() == null) {
- yield OperationResponse.failure(OP_UPDATE, "Request body is required for UPDATE operation");
- }
- final ResponseEntity response = api.updateTarget(request.controllerId(), request.body());
- yield OperationResponse.success(OP_UPDATE, "Target updated successfully", response.getBody());
- }
- case DELETE -> {
- if (request.controllerId() == null || request.controllerId().isBlank()) {
- yield OperationResponse.failure(OP_DELETE, "controllerId is required for DELETE operation");
- }
- api.deleteTarget(request.controllerId());
- yield OperationResponse.success(OP_DELETE, "Target deleted successfully");
+ if (request instanceof TargetRequest.Create r) {
+ validateOperation("create", "targets");
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_CREATE, "Request body is required for CREATE operation");
}
- };
+ final ResponseEntity> response = api.createTargets(List.of(r.body()));
+ final List created = response.getBody();
+ return OperationResponse.success(OP_CREATE, "Target created successfully",
+ created != null && !created.isEmpty() ? created.get(0) : null);
+ } else if (request instanceof TargetRequest.Update r) {
+ validateOperation("update", "targets");
+ if (r.controllerId() == null || r.controllerId().isBlank()) {
+ return OperationResponse.failure(OP_UPDATE, "controllerId is required for UPDATE operation");
+ }
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_UPDATE, "Request body is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateTarget(r.controllerId(), r.body());
+ return OperationResponse.success(OP_UPDATE, "Target updated successfully", response.getBody());
+ } else if (request instanceof TargetRequest.Delete r) {
+ validateOperation("delete", "targets");
+ if (r.controllerId() == null || r.controllerId().isBlank()) {
+ return OperationResponse.failure(OP_DELETE, "controllerId is required for DELETE operation");
+ }
+ api.deleteTarget(r.controllerId());
+ return OperationResponse.success(OP_DELETE, "Target deleted successfully");
+ }
+ throw new IllegalArgumentException("Unknown request type: " + request.getClass().getSimpleName());
}
@McpTool(name = "manage_rollout",
description = "Create, update, delete, and control rollouts for software deployment. " +
- "Operations: CREATE (new rollout), UPDATE (modify rollout), DELETE (remove rollout), " +
- "START (begin rollout), PAUSE, STOP, RESUME, APPROVE, DENY, RETRY, TRIGGER_NEXT_GROUP")
- public OperationResponse manageRollout(final ManageRolloutRequest request) {
- validateRolloutOperation(request.operation());
- log.debug("Managing rollout: operation={}, rolloutId={}", request.operation(), request.rolloutId());
+ "Use 'type' field to select operation. " +
+ "Types: Create, Update, Delete, Start, Pause, Stop, Resume, Approve, Deny, Retry, TriggerNextGroup. " +
+ "Examples: {\"type\":\"Create\",\"body\":{...}}, " +
+ "{\"type\":\"Start\",\"rolloutId\":123}, " +
+ "{\"type\":\"Approve\",\"rolloutId\":123,\"remark\":\"approved\"}")
+ public OperationResponse manageRollout(final RolloutRequest request) {
+ log.debug("Managing rollout: request={}", request.getClass().getSimpleName());
final MgmtRolloutRestApi api = hawkbitClient.mgmtService(MgmtRolloutRestApi.class, dummyTenant);
- return switch (request.operation()) {
- case CREATE -> {
- if (request.createBody() == null) {
- yield OperationResponse.failure(OP_CREATE, "createBody is required for CREATE operation");
- }
- final ResponseEntity response = api.create(request.createBody());
- yield OperationResponse.success(OP_CREATE, "Rollout created successfully", response.getBody());
- }
- case UPDATE -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_UPDATE, "rolloutId is required for UPDATE operation");
- }
- if (request.updateBody() == null) {
- yield OperationResponse.failure(OP_UPDATE, "updateBody is required for UPDATE operation");
- }
- final ResponseEntity response = api.update(request.rolloutId(), request.updateBody());
- yield OperationResponse.success(OP_UPDATE, "Rollout updated successfully", response.getBody());
- }
- case DELETE -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_DELETE, "rolloutId is required for DELETE operation");
- }
- api.delete(request.rolloutId());
- yield OperationResponse.success(OP_DELETE, "Rollout deleted successfully");
- }
- case START -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_START, "rolloutId is required for START operation");
- }
- api.start(request.rolloutId());
- yield OperationResponse.success(OP_START, "Rollout started successfully");
- }
- case PAUSE -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_PAUSE, "rolloutId is required for PAUSE operation");
- }
- api.pause(request.rolloutId());
- yield OperationResponse.success(OP_PAUSE, "Rollout paused successfully");
- }
- case STOP -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_STOP, "rolloutId is required for STOP operation");
- }
- api.stop(request.rolloutId());
- yield OperationResponse.success(OP_STOP, "Rollout stopped successfully");
- }
- case RESUME -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_RESUME, "rolloutId is required for RESUME operation");
- }
- api.resume(request.rolloutId());
- yield OperationResponse.success(OP_RESUME, "Rollout resumed successfully");
- }
- case APPROVE -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_APPROVE, "rolloutId is required for APPROVE operation");
- }
- api.approve(request.rolloutId(), request.remark());
- yield OperationResponse.success(OP_APPROVE, "Rollout approved successfully");
- }
- case DENY -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_DENY, "rolloutId is required for DENY operation");
- }
- api.deny(request.rolloutId(), request.remark());
- yield OperationResponse.success(OP_DENY, "Rollout denied successfully");
- }
- case RETRY -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_RETRY, "rolloutId is required for RETRY operation");
- }
- final ResponseEntity response = api.retryRollout(request.rolloutId());
- yield OperationResponse.success(OP_RETRY, "Rollout retry created successfully", response.getBody());
- }
- case TRIGGER_NEXT_GROUP -> {
- if (request.rolloutId() == null) {
- yield OperationResponse.failure(OP_TRIGGER_NEXT_GROUP, "rolloutId is required for TRIGGER_NEXT_GROUP operation");
- }
- api.triggerNextGroup(request.rolloutId());
- yield OperationResponse.success(OP_TRIGGER_NEXT_GROUP, "Next rollout group triggered successfully");
+ if (request instanceof RolloutRequest.Create r) {
+ validateRolloutOperation("create");
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_CREATE, "body is required for CREATE operation");
}
- };
+ final ResponseEntity response = api.create(r.body());
+ return OperationResponse.success(OP_CREATE, "Rollout created successfully", response.getBody());
+ } else if (request instanceof RolloutRequest.Update r) {
+ validateRolloutOperation("update");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_UPDATE, "rolloutId is required for UPDATE operation");
+ }
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_UPDATE, "body is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.update(r.rolloutId(), r.body());
+ return OperationResponse.success(OP_UPDATE, "Rollout updated successfully", response.getBody());
+ } else if (request instanceof RolloutRequest.Delete r) {
+ validateRolloutOperation("delete");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_DELETE, "rolloutId is required for DELETE operation");
+ }
+ api.delete(r.rolloutId());
+ return OperationResponse.success(OP_DELETE, "Rollout deleted successfully");
+ } else if (request instanceof RolloutRequest.Start r) {
+ validateRolloutOperation("start");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_START, "rolloutId is required for START operation");
+ }
+ api.start(r.rolloutId());
+ return OperationResponse.success(OP_START, "Rollout started successfully");
+ } else if (request instanceof RolloutRequest.Pause r) {
+ validateRolloutOperation("pause");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_PAUSE, "rolloutId is required for PAUSE operation");
+ }
+ api.pause(r.rolloutId());
+ return OperationResponse.success(OP_PAUSE, "Rollout paused successfully");
+ } else if (request instanceof RolloutRequest.Stop r) {
+ validateRolloutOperation("stop");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_STOP, "rolloutId is required for STOP operation");
+ }
+ api.stop(r.rolloutId());
+ return OperationResponse.success(OP_STOP, "Rollout stopped successfully");
+ } else if (request instanceof RolloutRequest.Resume r) {
+ validateRolloutOperation("resume");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_RESUME, "rolloutId is required for RESUME operation");
+ }
+ api.resume(r.rolloutId());
+ return OperationResponse.success(OP_RESUME, "Rollout resumed successfully");
+ } else if (request instanceof RolloutRequest.Approve r) {
+ validateRolloutOperation("approve");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_APPROVE, "rolloutId is required for APPROVE operation");
+ }
+ api.approve(r.rolloutId(), r.remark());
+ return OperationResponse.success(OP_APPROVE, "Rollout approved successfully");
+ } else if (request instanceof RolloutRequest.Deny r) {
+ validateRolloutOperation("deny");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_DENY, "rolloutId is required for DENY operation");
+ }
+ api.deny(r.rolloutId(), r.remark());
+ return OperationResponse.success(OP_DENY, "Rollout denied successfully");
+ } else if (request instanceof RolloutRequest.Retry r) {
+ validateRolloutOperation("retry");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_RETRY, "rolloutId is required for RETRY operation");
+ }
+ final ResponseEntity response = api.retryRollout(r.rolloutId());
+ return OperationResponse.success(OP_RETRY, "Rollout retry created successfully", response.getBody());
+ } else if (request instanceof RolloutRequest.TriggerNextGroup r) {
+ validateRolloutOperation("trigger-next-group");
+ if (r.rolloutId() == null) {
+ return OperationResponse.failure(OP_TRIGGER_NEXT_GROUP, "rolloutId is required for TRIGGER_NEXT_GROUP operation");
+ }
+ api.triggerNextGroup(r.rolloutId());
+ return OperationResponse.success(OP_TRIGGER_NEXT_GROUP, "Next rollout group triggered successfully");
+ }
+ throw new IllegalArgumentException("Unknown request type: " + request.getClass().getSimpleName());
}
@McpTool(name = "manage_distribution_set",
description = "Create, update, or delete distribution sets (software packages). " +
- "Operations: CREATE (new distribution set with name, version, type), " +
- "UPDATE (modify existing distribution set), DELETE (remove distribution set)")
- public OperationResponse manageDistributionSet(final ManageDistributionSetRequest request) {
- validateOperation(request.operation(), "distributionSets");
- log.debug("Managing distribution set: operation={}, distributionSetId={}", request.operation(), request.distributionSetId());
+ "Use 'type' field to select operation: " +
+ "{\"type\":\"Create\",\"body\":{\"name\":\"n\",\"version\":\"v\",\"type\":\"t\"}}, " +
+ "{\"type\":\"Update\",\"distributionSetId\":123,\"body\":{...}}, " +
+ "{\"type\":\"Delete\",\"distributionSetId\":123}")
+ public OperationResponse manageDistributionSet(final DistributionSetRequest request) {
+ log.debug("Managing distribution set: request={}", request.getClass().getSimpleName());
final MgmtDistributionSetRestApi api = hawkbitClient.mgmtService(MgmtDistributionSetRestApi.class, dummyTenant);
- return switch (request.operation()) {
- case CREATE -> {
- if (request.createBody() == null) {
- yield OperationResponse.failure(OP_CREATE, "createBody is required for CREATE operation");
- }
- final ResponseEntity> response = api.createDistributionSets(List.of(request.createBody()));
- final List created = response.getBody();
- yield OperationResponse.success(OP_CREATE, "Distribution set created successfully",
- created != null && !created.isEmpty() ? created.get(0) : null);
- }
- case UPDATE -> {
- if (request.distributionSetId() == null) {
- yield OperationResponse.failure(OP_UPDATE, "distributionSetId is required for UPDATE operation");
- }
- if (request.updateBody() == null) {
- yield OperationResponse.failure(OP_UPDATE, "updateBody is required for UPDATE operation");
- }
- final ResponseEntity response = api.updateDistributionSet(request.distributionSetId(), request.updateBody());
- yield OperationResponse.success(OP_UPDATE, "Distribution set updated successfully", response.getBody());
- }
- case DELETE -> {
- if (request.distributionSetId() == null) {
- yield OperationResponse.failure(OP_DELETE, "distributionSetId is required for DELETE operation");
- }
- api.deleteDistributionSet(request.distributionSetId());
- yield OperationResponse.success(OP_DELETE, "Distribution set deleted successfully");
+ if (request instanceof DistributionSetRequest.Create r) {
+ validateOperation("create", "distributionSets");
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_CREATE, "body is required for CREATE operation");
}
- };
+ final ResponseEntity> response = api.createDistributionSets(List.of(r.body()));
+ final List created = response.getBody();
+ return OperationResponse.success(OP_CREATE, "Distribution set created successfully",
+ created != null && !created.isEmpty() ? created.get(0) : null);
+ } else if (request instanceof DistributionSetRequest.Update r) {
+ validateOperation("update", "distributionSets");
+ if (r.distributionSetId() == null) {
+ return OperationResponse.failure(OP_UPDATE, "distributionSetId is required for UPDATE operation");
+ }
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_UPDATE, "body is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateDistributionSet(r.distributionSetId(), r.body());
+ return OperationResponse.success(OP_UPDATE, "Distribution set updated successfully", response.getBody());
+ } else if (request instanceof DistributionSetRequest.Delete r) {
+ validateOperation("delete", "distributionSets");
+ if (r.distributionSetId() == null) {
+ return OperationResponse.failure(OP_DELETE, "distributionSetId is required for DELETE operation");
+ }
+ api.deleteDistributionSet(r.distributionSetId());
+ return OperationResponse.success(OP_DELETE, "Distribution set deleted successfully");
+ }
+ throw new IllegalArgumentException("Unknown request type: " + request.getClass().getSimpleName());
}
@McpTool(name = "manage_action",
description = "Delete deployment actions. Actions are created indirectly via distribution set assignment. " +
- "Operations: DELETE (single action by ID), DELETE_BATCH (multiple actions by RSQL filter or list of IDs)")
- public OperationResponse manageAction(final ManageActionRequest request) {
- validateActionOperation(request.operation());
- log.debug("Managing action: operation={}, actionId={}", request.operation(), request.actionId());
+ "Use 'type' field to select operation: " +
+ "{\"type\":\"Delete\",\"actionIds\":[123]}, " +
+ "{\"type\":\"DeleteBatch\",\"actionIds\":[1,2,3],\"rsql\":\"\"}")
+ public OperationResponse manageAction(final ActionRequest request) {
+ log.debug("Managing action: request={}", request.getClass().getSimpleName());
final MgmtActionRestApi api = hawkbitClient.mgmtService(MgmtActionRestApi.class, dummyTenant);
- return switch (request.operation()) {
- case DELETE -> {
- if (request.actionId() == null) {
- yield OperationResponse.failure(OP_DELETE, "actionId is required for DELETE operation");
- }
- api.deleteAction(request.actionId());
- yield OperationResponse.success(OP_DELETE, "Action deleted successfully");
- }
- case DELETE_BATCH -> {
- if ((request.actionIds() == null || request.actionIds().isEmpty()) &&
- (request.rsql() == null || request.rsql().isBlank())) {
- yield OperationResponse.failure(OP_DELETE_BATCH, "Either actionIds or rsql is required for DELETE_BATCH operation");
- }
- api.deleteActions(request.rsql(), request.actionIds());
- yield OperationResponse.success(OP_DELETE_BATCH, "Actions deleted successfully");
+ if (request instanceof ActionRequest.Delete r) {
+ validateActionOperation("delete");
+ if (r.actionIds() == null || r.actionIds().isEmpty()) {
+ return OperationResponse.failure(OP_DELETE, "actionIds with single element is required for DELETE operation");
}
- };
+ if (r.actionIds().size() != 1) {
+ return OperationResponse.failure(OP_DELETE, "actionIds must contain exactly one element for DELETE operation. Use DELETE_BATCH for multiple.");
+ }
+ api.deleteAction(r.actionIds().get(0));
+ return OperationResponse.success(OP_DELETE, "Action deleted successfully");
+ } else if (request instanceof ActionRequest.DeleteBatch r) {
+ validateActionOperation("delete-batch");
+ if ((r.actionIds() == null || r.actionIds().isEmpty()) &&
+ (r.rsql() == null || r.rsql().isBlank())) {
+ return OperationResponse.failure(OP_DELETE_BATCH, "Either actionIds or rsql is required for DELETE_BATCH operation");
+ }
+ api.deleteActions(r.rsql(), r.actionIds());
+ return OperationResponse.success(OP_DELETE_BATCH, "Actions deleted successfully");
+ }
+ throw new IllegalArgumentException("Unknown request type: " + request.getClass().getSimpleName());
}
@McpTool(name = "manage_software_module",
description = "Create, update, or delete software modules. " +
- "Operations: CREATE (new software module with name, version, type), " +
- "UPDATE (modify existing software module), DELETE (remove software module)")
- public OperationResponse manageSoftwareModule(final ManageSoftwareModuleRequest request) {
- validateOperation(request.operation(), "softwareModules");
- log.debug("Managing software module: operation={}, softwareModuleId={}", request.operation(), request.softwareModuleId());
+ "Use 'type' field to select operation: " +
+ "{\"type\":\"Create\",\"body\":{\"name\":\"n\",\"version\":\"v\",\"type\":\"t\"}}, " +
+ "{\"type\":\"Update\",\"softwareModuleId\":123,\"body\":{...}}, " +
+ "{\"type\":\"Delete\",\"softwareModuleId\":123}")
+ public OperationResponse manageSoftwareModule(final SoftwareModuleRequest request) {
+ log.debug("Managing software module: request={}", request.getClass().getSimpleName());
final MgmtSoftwareModuleRestApi api = hawkbitClient.mgmtService(MgmtSoftwareModuleRestApi.class, dummyTenant);
- return switch (request.operation()) {
- case CREATE -> {
- if (request.createBody() == null) {
- yield OperationResponse.failure(OP_CREATE, "createBody is required for CREATE operation");
- }
- final ResponseEntity> response = api.createSoftwareModules(List.of(request.createBody()));
- final List created = response.getBody();
- yield OperationResponse.success(OP_CREATE, "Software module created successfully",
- created != null && !created.isEmpty() ? created.get(0) : null);
- }
- case UPDATE -> {
- if (request.softwareModuleId() == null) {
- yield OperationResponse.failure(OP_UPDATE, "softwareModuleId is required for UPDATE operation");
- }
- if (request.updateBody() == null) {
- yield OperationResponse.failure(OP_UPDATE, "updateBody is required for UPDATE operation");
- }
- final ResponseEntity response = api.updateSoftwareModule(request.softwareModuleId(), request.updateBody());
- yield OperationResponse.success(OP_UPDATE, "Software module updated successfully", response.getBody());
- }
- case DELETE -> {
- if (request.softwareModuleId() == null) {
- yield OperationResponse.failure(OP_DELETE, "softwareModuleId is required for DELETE operation");
- }
- api.deleteSoftwareModule(request.softwareModuleId());
- yield OperationResponse.success(OP_DELETE, "Software module deleted successfully");
+ if (request instanceof SoftwareModuleRequest.Create r) {
+ validateOperation("create", "softwareModules");
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_CREATE, "body is required for CREATE operation");
}
- };
+ final ResponseEntity> response = api.createSoftwareModules(List.of(r.body()));
+ final List created = response.getBody();
+ return OperationResponse.success(OP_CREATE, "Software module created successfully",
+ created != null && !created.isEmpty() ? created.get(0) : null);
+ } else if (request instanceof SoftwareModuleRequest.Update r) {
+ validateOperation("update", "softwareModules");
+ if (r.softwareModuleId() == null) {
+ return OperationResponse.failure(OP_UPDATE, "softwareModuleId is required for UPDATE operation");
+ }
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_UPDATE, "body is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateSoftwareModule(r.softwareModuleId(), r.body());
+ return OperationResponse.success(OP_UPDATE, "Software module updated successfully", response.getBody());
+ } else if (request instanceof SoftwareModuleRequest.Delete r) {
+ validateOperation("delete", "softwareModules");
+ if (r.softwareModuleId() == null) {
+ return OperationResponse.failure(OP_DELETE, "softwareModuleId is required for DELETE operation");
+ }
+ api.deleteSoftwareModule(r.softwareModuleId());
+ return OperationResponse.success(OP_DELETE, "Software module deleted successfully");
+ }
+ throw new IllegalArgumentException("Unknown request type: " + request.getClass().getSimpleName());
}
@McpTool(name = "manage_target_filter",
description = "Create, update, or delete target filter queries. " +
- "Operations: CREATE (new target filter with name and RSQL query), " +
- "UPDATE (modify existing target filter), DELETE (remove target filter)")
- public OperationResponse manageTargetFilter(final ManageTargetFilterRequest request) {
- validateOperation(request.operation(), "targetFilters");
- log.debug("Managing target filter: operation={}, filterId={}", request.operation(), request.filterId());
+ "Use 'type' field to select operation: " +
+ "{\"type\":\"Create\",\"body\":{\"name\":\"n\",\"query\":\"name==*\"}}, " +
+ "{\"type\":\"Update\",\"filterId\":123,\"body\":{...}}, " +
+ "{\"type\":\"Delete\",\"filterId\":123}")
+ public OperationResponse manageTargetFilter(final TargetFilterRequest request) {
+ log.debug("Managing target filter: request={}", request.getClass().getSimpleName());
final MgmtTargetFilterQueryRestApi api = hawkbitClient.mgmtService(MgmtTargetFilterQueryRestApi.class, dummyTenant);
- return switch (request.operation()) {
- case CREATE -> {
- if (request.body() == null) {
- yield OperationResponse.failure(OP_CREATE, "Request body is required for CREATE operation");
- }
- final ResponseEntity response = api.createFilter(request.body());
- yield OperationResponse.success(OP_CREATE, "Target filter created successfully", response.getBody());
- }
- case UPDATE -> {
- if (request.filterId() == null) {
- yield OperationResponse.failure(OP_UPDATE, "filterId is required for UPDATE operation");
- }
- if (request.body() == null) {
- yield OperationResponse.failure(OP_UPDATE, "Request body is required for UPDATE operation");
- }
- final ResponseEntity response = api.updateFilter(request.filterId(), request.body());
- yield OperationResponse.success(OP_UPDATE, "Target filter updated successfully", response.getBody());
- }
- case DELETE -> {
- if (request.filterId() == null) {
- yield OperationResponse.failure(OP_DELETE, "filterId is required for DELETE operation");
- }
- api.deleteFilter(request.filterId());
- yield OperationResponse.success(OP_DELETE, "Target filter deleted successfully");
+ if (request instanceof TargetFilterRequest.Create r) {
+ validateOperation("create", "targetFilters");
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_CREATE, "body is required for CREATE operation");
}
- };
+ final ResponseEntity response = api.createFilter(r.body());
+ return OperationResponse.success(OP_CREATE, "Target filter created successfully", response.getBody());
+ } else if (request instanceof TargetFilterRequest.Update r) {
+ validateOperation("update", "targetFilters");
+ if (r.filterId() == null) {
+ return OperationResponse.failure(OP_UPDATE, "filterId is required for UPDATE operation");
+ }
+ if (r.body() == null) {
+ return OperationResponse.failure(OP_UPDATE, "body is required for UPDATE operation");
+ }
+ final ResponseEntity response = api.updateFilter(r.filterId(), r.body());
+ return OperationResponse.success(OP_UPDATE, "Target filter updated successfully", response.getBody());
+ } else if (request instanceof TargetFilterRequest.Delete r) {
+ validateOperation("delete", "targetFilters");
+ if (r.filterId() == null) {
+ return OperationResponse.failure(OP_DELETE, "filterId is required for DELETE operation");
+ }
+ api.deleteFilter(r.filterId());
+ return OperationResponse.success(OP_DELETE, "Target filter deleted successfully");
+ }
+ throw new IllegalArgumentException("Unknown request type: " + request.getClass().getSimpleName());
}
- private void validateOperation(final Operation operation, final String entity) {
- final String opName = operation.name().toLowerCase();
- if (!isOperationEnabled(opName, entity)) {
+ private void validateOperation(final String operation, final String entity) {
+ if (!isOperationEnabled(operation, entity)) {
throw new IllegalArgumentException(
- "Operation " + operation + " is not enabled for " + entity +
+ "Operation " + operation.toUpperCase() + " is not enabled for " + entity +
". Check hawkbit.mcp.operations configuration.");
}
}
- private void validateRolloutOperation(final RolloutOperation operation) {
- final String opName = operation.name().toLowerCase().replace("_", "-");
+ private void validateRolloutOperation(final String operation) {
final HawkbitMcpProperties.RolloutConfig config = properties.getOperations().getRollouts();
- final Boolean entitySetting = config.getOperationEnabled(opName);
+ final Boolean entitySetting = config.getOperationEnabled(operation);
// For standard CRUD ops, check global fallback
if (entitySetting == null) {
- if (!properties.getOperations().isGlobalOperationEnabled(opName)) {
+ if (!properties.getOperations().isGlobalOperationEnabled(operation)) {
throw new IllegalArgumentException(
- "Operation " + operation + " is not enabled for rollouts. " +
+ "Operation " + operation.toUpperCase() + " is not enabled for rollouts. " +
"Check hawkbit.mcp.operations configuration.");
}
return;
@@ -501,20 +508,19 @@ private void validateRolloutOperation(final RolloutOperation operation) {
if (!entitySetting) {
throw new IllegalArgumentException(
- "Operation " + operation + " is not enabled for rollouts. " +
+ "Operation " + operation.toUpperCase() + " is not enabled for rollouts. " +
"Check hawkbit.mcp.operations configuration.");
}
}
- private void validateActionOperation(final ActionOperation operation) {
- final String opName = operation.name().toLowerCase().replace("_", "-");
+ private void validateActionOperation(final String operation) {
final HawkbitMcpProperties.ActionConfig config = properties.getOperations().getActions();
- final Boolean entitySetting = config.getOperationEnabled(opName);
+ final Boolean entitySetting = config.getOperationEnabled(operation);
if (entitySetting == null) {
- if (opName.equals("delete") && !properties.getOperations().isGlobalOperationEnabled("delete")) {
+ if (operation.equals("delete") && !properties.getOperations().isGlobalOperationEnabled("delete")) {
throw new IllegalArgumentException(
- "Operation " + operation + " is not enabled for actions. " +
+ "Operation " + operation.toUpperCase() + " is not enabled for actions. " +
"Check hawkbit.mcp.operations configuration.");
}
return;
@@ -522,7 +528,7 @@ private void validateActionOperation(final ActionOperation operation) {
if (!entitySetting) {
throw new IllegalArgumentException(
- "Operation " + operation + " is not enabled for actions. " +
+ "Operation " + operation.toUpperCase() + " is not enabled for actions. " +
"Check hawkbit.mcp.operations configuration.");
}
}
diff --git a/hawkbit-mcp/src/main/resources/application.properties b/hawkbit-mcp/src/main/resources/application.properties
index 2d64ebab9a..14f92c95e1 100644
--- a/hawkbit-mcp/src/main/resources/application.properties
+++ b/hawkbit-mcp/src/main/resources/application.properties
@@ -11,6 +11,10 @@
# Server configuration
server.port=8081
+# Jackson configuration - accept both uppercase and lowercase enum values
+# This allows LLMs to use either "FORCED" or "forced" for enum fields like MgmtActionType
+spring.jackson.mapper.accept-case-insensitive-enums=true
+
# Spring application name
spring.application.name=hawkbit-mcp-server
From 7ef03996ba26ff864304a561462c87f3f4579597 Mon Sep 17 00:00:00 2001
From: Denislav Prinov
Date: Wed, 4 Feb 2026 21:49:33 +0200
Subject: [PATCH 4/5] Address PR review. Provide More context in tools
description.
Signed-off-by: Denislav Prinov
---
.../client/NoOpAuthenticationValidator.java | 29 ---------
.../config/HawkbitClientConfiguration.java | 61 ++++++++++++++++--
.../server/config/HawkbitMcpProperties.java | 12 ++--
.../config/McpHttpClientConfiguration.java | 53 ++++-----------
.../config/McpSecurityConfiguration.java | 64 ++++++++++---------
.../config/McpStdioClientConfiguration.java | 42 ++----------
.../config/McpValidationConfiguration.java | 19 ++----
.../hawkbit/mcp/server/dto/ActionRequest.java | 4 +-
.../server/tools/HawkbitMcpToolProvider.java | 13 ++--
.../src/main/resources/application.properties | 7 --
10 files changed, 129 insertions(+), 175 deletions(-)
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java
deleted file mode 100644
index 8bd89133b2..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/NoOpAuthenticationValidator.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.client;
-
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * No-operation authentication validator that always returns VALID.
- * Used when authentication validation is disabled via configuration.
- */
-@Slf4j
-public class NoOpAuthenticationValidator implements AuthenticationValidator {
-
- public NoOpAuthenticationValidator() {
- log.info("Authentication validation disabled - using no-op validator");
- }
-
- @Override
- public ValidationResult validate(final String authHeader) {
- return ValidationResult.VALID;
- }
-}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
index ae9ead241f..121d982b2c 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
@@ -9,21 +9,31 @@
*/
package org.eclipse.hawkbit.mcp.server.config;
+import feign.Contract;
+import feign.RequestInterceptor;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.eclipse.hawkbit.sdk.Controller;
+import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.HawkbitServer;
+import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
+import java.util.function.BiFunction;
+
/**
* Common configuration for the hawkBit SDK client.
*
- * Provides the {@link HawkbitServer} bean which is used by both HTTP and STDIO mode configurations.
- * Mode-specific beans (HawkbitClient, Tenant) are provided by:
+ * Provides the {@link HawkbitServer} and {@link HawkbitClient} beans.
+ * Mode-specific authentication is provided via the request interceptor bean from:
*
- * {@link McpHttpClientConfiguration} - for HTTP mode
- * {@link McpStdioClientConfiguration} - for STDIO mode
+ * {@link McpHttpClientConfiguration} - for HTTP mode (per-request authentication)
+ * {@link McpStdioClientConfiguration} - for STDIO mode (static credentials)
*
*
*/
@@ -42,4 +52,47 @@ public HawkbitServer hawkbitServer() {
log.info("Configured hawkBit server URL: {}", properties.getMgmtUrl());
return server;
}
+
+ @Bean
+ public HawkbitClient hawkbitClient(
+ final HawkbitServer server,
+ final Encoder encoder,
+ final Decoder decoder,
+ final Contract contract,
+ final BiFunction hawkbitRequestInterceptor) {
+ log.info("Configuring hawkBit client");
+ return HawkbitClient.builder()
+ .hawkBitServer(server)
+ .encoder(encoder)
+ .decoder(decoder)
+ .contract(contract)
+ .requestInterceptorFn(hawkbitRequestInterceptor)
+ .build();
+ }
+
+ /**
+ * Tenant bean - credentials depend on the mode:
+ *
+ * STDIO mode with credentials: uses static credentials from configuration
+ * STDIO mode without credentials: null credentials (will fail)
+ * HTTP mode: null credentials (auth comes from request context via interceptor)
+ *
+ */
+ @Bean
+ public Tenant dummyTenant(@Value("${spring.ai.mcp.server.stdio:false}") final boolean stdioMode) {
+ final Tenant tenant = new Tenant();
+ // Tenant fields are null by default - only set credentials in STDIO mode when configured
+ if (stdioMode) {
+ if (properties.hasStaticCredentials()) {
+ tenant.setUsername(properties.getUsername());
+ tenant.setPassword(properties.getPassword());
+ log.info("Configured tenant with static credentials for STDIO mode");
+ } else {
+ log.warn("STDIO mode enabled but no static credentials configured");
+ }
+ } else {
+ log.info("Configured tenant for HTTP mode (per-request authentication)");
+ }
+ return tenant;
+ }
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
index a47f0b326b..c2ebec782c 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitMcpProperties.java
@@ -12,6 +12,7 @@
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@@ -33,23 +34,24 @@ public class HawkbitMcpProperties {
/**
* Username for hawkBit authentication (used in STDIO mode).
- * Format: {@code tenant\\username} or just username depending on hawkBit configuration.
- * Can be set via HAWKBIT_USERNAME environment variable.
+ * Read directly from HAWKBIT_USERNAME environment variable.
*/
+ @Value("${HAWKBIT_USERNAME:#{null}}")
private String username;
/**
* Password for hawkBit authentication (used in STDIO mode).
- * Can be set via HAWKBIT_PASSWORD environment variable.
+ * Read directly from HAWKBIT_PASSWORD environment variable.
*/
+ @Value("${HAWKBIT_PASSWORD:#{null}}")
private String password;
/**
* Check if static credentials are configured.
+ * Allows empty strings as valid values (for users who intentionally set empty password).
*/
public boolean hasStaticCredentials() {
- return username != null && !username.isBlank()
- && password != null && !password.isBlank();
+ return username != null && password != null;
}
/**
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
index ee9a0dbb68..60940fa879 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
@@ -9,22 +9,17 @@
*/
package org.eclipse.hawkbit.mcp.server.config;
-import feign.Contract;
import feign.RequestInterceptor;
-import feign.codec.Decoder;
-import feign.codec.Encoder;
-import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.sdk.Controller;
-import org.eclipse.hawkbit.sdk.HawkbitClient;
-import org.eclipse.hawkbit.sdk.HawkbitServer;
import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
-import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.function.BiFunction;
@@ -37,37 +32,26 @@
*/
@Slf4j
@Configuration
-@RequiredArgsConstructor
@ConditionalOnProperty(name = "spring.ai.mcp.server.stdio", havingValue = "false", matchIfMissing = true)
public class McpHttpClientConfiguration {
+ /**
+ * Request interceptor for HTTP mode - extracts authentication from incoming HTTP request.
+ */
@Bean
- public HawkbitClient hawkbitClient(final HawkbitServer server,
- final Encoder encoder,
- final Decoder decoder,
- final Contract contract) {
- log.info("Configuring hawkBit client for HTTP mode (per-request authentication)");
- return HawkbitClient.builder()
- .hawkBitServer(server)
- .encoder(encoder)
- .decoder(decoder)
- .contract(contract)
- .requestInterceptorFn(httpRequestInterceptor())
- .build();
- }
-
- private BiFunction httpRequestInterceptor() {
+ public BiFunction hawkbitRequestInterceptor() {
+ log.info("Configuring HTTP mode request interceptor (per-request authentication)");
return (tenant, controller) -> template -> {
- RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
+ final ServletRequestAttributes attrs =
+ (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
- String authHeader = (String) attrs.getAttribute(
- McpSecurityConfiguration.AUTH_HEADER_ATTRIBUTE,
- RequestAttributes.SCOPE_REQUEST);
+ final HttpServletRequest request = attrs.getRequest();
+ final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null) {
template.header(HttpHeaders.AUTHORIZATION, authHeader);
- log.trace("Using auth header from HTTP request context");
+ log.trace("Using auth header from HTTP request");
} else {
- log.warn("No authentication header in request context - request will likely fail");
+ log.warn("No authentication header in request - request will likely fail");
}
} else {
log.warn("No request context available - request will likely fail");
@@ -75,15 +59,4 @@ private BiFunction httpRequestIntercepto
};
}
- /**
- * Tenant bean for HTTP mode - credentials come from request context via interceptor.
- */
- @Bean
- public Tenant dummyTenant() {
- Tenant tenant = new Tenant();
- tenant.setUsername(null);
- tenant.setPassword(null);
- log.info("Configured tenant for HTTP mode (per-request authentication)");
- return tenant;
- }
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
index 723aa75898..dd803cba68 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
@@ -34,6 +34,7 @@
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
+import java.util.Optional;
/**
* Security configuration for the MCP server.
@@ -41,21 +42,27 @@
* This configuration is only active in HTTP/servlet mode. In STDIO mode,
* authentication is handled via static credentials from properties.
*
+ *
+ * When authentication validation is enabled ({@code hawkbit.mcp.validation.enabled=true}),
+ * a filter validates credentials against hawkBit before forwarding requests.
+ * When disabled, no validation filter is added and requests pass through directly.
+ *
*/
@Slf4j
@Configuration
@EnableWebSecurity
-@RequiredArgsConstructor
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(name = "spring.ai.mcp.server.stdio", havingValue = "false", matchIfMissing = true)
public class McpSecurityConfiguration {
- /**
- * Request attribute key for storing the Authorization header.
- */
- public static final String AUTH_HEADER_ATTRIBUTE = "hawkbit.mcp.auth.header";
+ private final Optional authenticationValidator;
- private final AuthenticationValidator authenticationValidator;
+ public McpSecurityConfiguration(final Optional authenticationValidator) {
+ this.authenticationValidator = authenticationValidator;
+ if (authenticationValidator.isEmpty()) {
+ log.info("Authentication validation disabled - requests will be forwarded without validation");
+ }
+ }
@Bean
@SuppressWarnings("java:S4502") // CSRF protection is not needed for stateless REST APIs using Authorization header
@@ -63,15 +70,22 @@ public SecurityFilterChain mcpSecurityFilterChain(final HttpSecurity http) throw
http
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.csrf(AbstractHttpConfigurer::disable)
- .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .addFilterBefore(new HawkBitAuthenticationFilter(authenticationValidator),
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+ authenticationValidator.ifPresent(validator -> {
+ log.info("Authentication validation enabled - adding validation filter");
+ http.addFilterBefore(new HawkBitAuthenticationFilter(validator),
UsernamePasswordAuthenticationFilter.class);
+ });
return http.build();
}
/**
- * Filter that validates authentication.
+ * Filter that validates authentication against hawkBit.
+ *
+ * Only added to the filter chain when authentication validation is enabled.
+ *
*/
@Slf4j
@RequiredArgsConstructor
@@ -81,39 +95,27 @@ public static class HawkBitAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(final HttpServletRequest request, final @NonNull HttpServletResponse response,
- final @NonNull FilterChain filterChain) throws ServletException, IOException {
- String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
-
- if (authHeader != null) {
- request.setAttribute(AUTH_HEADER_ATTRIBUTE, authHeader);
- }
-
- ValidationResult result = validator.validate(authHeader);
+ final @NonNull FilterChain filterChain) throws ServletException, IOException {
+ final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
+ final ValidationResult result = validator.validate(authHeader);
switch (result) {
- case VALID:
- filterChain.doFilter(request, response);
- break;
-
- case MISSING_CREDENTIALS:
+ case VALID -> filterChain.doFilter(request, response);
+ case MISSING_CREDENTIALS -> {
log.debug("Rejecting request: missing credentials");
sendErrorResponse(response, HttpStatus.UNAUTHORIZED,
"Authentication required. Please provide hawkBit credentials.");
- break;
-
- case INVALID_CREDENTIALS:
+ }
+ case INVALID_CREDENTIALS -> {
log.debug("Rejecting request: invalid credentials");
- request.removeAttribute(AUTH_HEADER_ATTRIBUTE);
sendErrorResponse(response, HttpStatus.UNAUTHORIZED,
"Invalid hawkBit credentials.");
- break;
-
- case HAWKBIT_ERROR:
+ }
+ case HAWKBIT_ERROR -> {
log.warn("Rejecting request: hawkBit unavailable");
- request.removeAttribute(AUTH_HEADER_ATTRIBUTE);
sendErrorResponse(response, HttpStatus.SERVICE_UNAVAILABLE,
"Unable to validate credentials. hawkBit may be unavailable.");
- break;
+ }
}
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
index bc4eb5d484..af05c6ddfc 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
@@ -9,15 +9,10 @@
*/
package org.eclipse.hawkbit.mcp.server.config;
-import feign.Contract;
import feign.RequestInterceptor;
-import feign.codec.Decoder;
-import feign.codec.Encoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.sdk.Controller;
-import org.eclipse.hawkbit.sdk.HawkbitClient;
-import org.eclipse.hawkbit.sdk.HawkbitServer;
import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@@ -43,22 +38,12 @@ public class McpStdioClientConfiguration {
private final HawkbitMcpProperties properties;
+ /**
+ * Request interceptor for STDIO mode - uses static credentials from configuration.
+ */
@Bean
- public HawkbitClient hawkbitClient(final HawkbitServer server,
- final Encoder encoder,
- final Decoder decoder,
- final Contract contract) {
- log.info("Configuring hawkBit client for STDIO mode (static credentials)");
- return HawkbitClient.builder()
- .hawkBitServer(server)
- .encoder(encoder)
- .decoder(decoder)
- .contract(contract)
- .requestInterceptorFn(stdioRequestInterceptor())
- .build();
- }
-
- private BiFunction stdioRequestInterceptor() {
+ public BiFunction hawkbitRequestInterceptor() {
+ log.info("Configuring STDIO mode request interceptor (static credentials)");
return (tenant, controller) -> template -> {
if (properties.hasStaticCredentials()) {
String credentials = properties.getUsername() + ":" + properties.getPassword();
@@ -72,21 +57,4 @@ private BiFunction stdioRequestIntercept
};
}
- /**
- * Tenant bean for STDIO mode - uses static credentials from configuration.
- */
- @Bean
- public Tenant dummyTenant() {
- Tenant tenant = new Tenant();
- if (properties.hasStaticCredentials()) {
- tenant.setUsername(properties.getUsername());
- tenant.setPassword(properties.getPassword());
- log.info("Configured tenant with static credentials for STDIO mode");
- } else {
- tenant.setUsername(null);
- tenant.setPassword(null);
- log.warn("STDIO mode enabled but no static credentials configured");
- }
- return tenant;
- }
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
index 14f0b6c120..1d718eeda7 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
@@ -11,7 +11,6 @@
import org.eclipse.hawkbit.mcp.server.client.AuthenticationValidator;
import org.eclipse.hawkbit.mcp.server.client.HawkbitAuthenticationValidator;
-import org.eclipse.hawkbit.mcp.server.client.NoOpAuthenticationValidator;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -20,14 +19,18 @@
/**
* Configuration for authentication validation beans.
- * Creates either a real validator (with caching) or a no-op validator based on configuration.
*/
@Configuration
public class McpValidationConfiguration {
/**
* Creates the hawkBit authentication validator when validation is enabled.
- * It validates against hawkBit REST API.
+ * Validates credentials against hawkBit REST API with result caching.
+ *
+ * @param hawkbitClient the hawkBit client for API calls
+ * @param dummyTenant the tenant configuration
+ * @param properties the MCP properties with cache configuration
+ * @return the authentication validator
*/
@Bean
@ConditionalOnProperty(name = "hawkbit.mcp.validation.enabled", havingValue = "true", matchIfMissing = true)
@@ -37,14 +40,4 @@ public AuthenticationValidator hawkbitAuthenticationValidator(
final HawkbitMcpProperties properties) {
return new HawkbitAuthenticationValidator(hawkbitClient, dummyTenant, properties);
}
-
- /**
- * Creates a no-op authentication validator when validation is disabled.
- * This validator always returns VALID without any actual validation.
- */
- @Bean
- @ConditionalOnProperty(name = "hawkbit.mcp.validation.enabled", havingValue = "false")
- public AuthenticationValidator noOpAuthenticationValidator() {
- return new NoOpAuthenticationValidator();
- }
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java
index 884463ab6a..3475002c2c 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/dto/ActionRequest.java
@@ -28,9 +28,9 @@ public sealed interface ActionRequest
/**
* Request to delete a single action.
*
- * @param actionIds list containing the single action ID to delete (use single-element list)
+ * @param actionId to delete
*/
- record Delete(List actionIds) implements ActionRequest {}
+ record Delete(Long actionId) implements ActionRequest {}
/**
* Request to delete multiple actions.
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
index b636b4705e..afc24e221b 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java
@@ -192,7 +192,7 @@ public PagedResponse listTargetFilters(final ListRequest
@McpTool(name = "manage_target",
description = "Create, update, or delete targets (devices). " +
- "Operations: CREATE (new target with controllerId, name, description), " +
+ "Operations: CREATE (new target with controllerId, name, description. When creating a target without a specific target type, set \"targetType\": null), " +
"UPDATE (modify existing target by controllerId), " +
"DELETE (remove target by controllerId). " +
"Use 'type' field to select operation: " +
@@ -238,6 +238,8 @@ public OperationResponse manageTarget(final TargetRequest request) {
description = "Create, update, delete, and control rollouts for software deployment. " +
"Use 'type' field to select operation. " +
"Types: Create, Update, Delete, Start, Pause, Stop, Resume, Approve, Deny, Retry, TriggerNextGroup. " +
+ "For Create: use rollout 'type' values: 'soft', 'forced', 'timeforced', 'downloadonly' (lowercase). " +
+ "If 'groups' list is provided, omit 'amountGroups' (they are mutually exclusive). " +
"Examples: {\"type\":\"Create\",\"body\":{...}}, " +
"{\"type\":\"Start\",\"rolloutId\":123}, " +
"{\"type\":\"Approve\",\"rolloutId\":123,\"remark\":\"approved\"}")
@@ -383,13 +385,10 @@ public OperationResponse manageAction(final ActionRequest request) {
if (request instanceof ActionRequest.Delete r) {
validateActionOperation("delete");
- if (r.actionIds() == null || r.actionIds().isEmpty()) {
- return OperationResponse.failure(OP_DELETE, "actionIds with single element is required for DELETE operation");
+ if (r.actionId() == null) {
+ return OperationResponse.failure(OP_DELETE, "actionId is required for DELETE operation");
}
- if (r.actionIds().size() != 1) {
- return OperationResponse.failure(OP_DELETE, "actionIds must contain exactly one element for DELETE operation. Use DELETE_BATCH for multiple.");
- }
- api.deleteAction(r.actionIds().get(0));
+ api.deleteAction(r.actionId());
return OperationResponse.success(OP_DELETE, "Action deleted successfully");
} else if (request instanceof ActionRequest.DeleteBatch r) {
validateActionOperation("delete-batch");
diff --git a/hawkbit-mcp/src/main/resources/application.properties b/hawkbit-mcp/src/main/resources/application.properties
index 14f92c95e1..a6647f26f3 100644
--- a/hawkbit-mcp/src/main/resources/application.properties
+++ b/hawkbit-mcp/src/main/resources/application.properties
@@ -32,14 +32,7 @@ spring.ai.mcp.server.capabilities.prompt=true
# hawkBit connection configuration
hawkbit.mcp.mgmt-url=${HAWKBIT_URL:http://localhost:8080}
-# Static credentials for STDIO mode (optional, not needed for HTTP mode with per-request auth)
-# Can be set via environment variables: HAWKBIT_USERNAME, HAWKBIT_PASSWORD
-# Format for username: tenant\username or just username depending on hawkBit configuration
-hawkbit.mcp.username=${HAWKBIT_USERNAME:}
-hawkbit.mcp.password=${HAWKBIT_PASSWORD:}
-
# Authentication validation configuration
-hawkbit.mcp.validation.enabled=true
hawkbit.mcp.validation.cache-ttl=600s
hawkbit.mcp.validation.cache-max-size=1000
From 3237b2682f6f38712447ba95888bddc17fbcff9f Mon Sep 17 00:00:00 2001
From: Denislav Prinov
Date: Thu, 5 Feb 2026 14:21:39 +0200
Subject: [PATCH 5/5] Address PR Review
Signed-off-by: Denislav Prinov
---
.../HawkbitAuthenticationValidator.java | 4 ++
.../config/HawkbitClientConfiguration.java | 28 +-----------
.../config/McpHttpClientConfiguration.java | 10 +++++
.../config/McpSecurityConfiguration.java | 21 ++++-----
.../config/McpStdioClientConfiguration.java | 16 +++++++
.../config/McpValidationConfiguration.java | 43 -------------------
6 files changed, 40 insertions(+), 82 deletions(-)
delete mode 100644 hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
index ca34a85954..274936c63e 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/client/HawkbitAuthenticationValidator.java
@@ -17,7 +17,9 @@
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -29,6 +31,8 @@
* This validator is conditionally created when {@code hawkbit.mcp.validation.enabled=true}.
*/
@Slf4j
+@Component
+@ConditionalOnProperty(name = "hawkbit.mcp.validation.enabled", havingValue = "true", matchIfMissing = true)
public class HawkbitAuthenticationValidator implements AuthenticationValidator {
private final HawkbitClient hawkbitClient;
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
index 121d982b2c..78caefe5e9 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/HawkbitClientConfiguration.java
@@ -19,7 +19,6 @@
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.HawkbitServer;
import org.eclipse.hawkbit.sdk.Tenant;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -30,7 +29,7 @@
* Common configuration for the hawkBit SDK client.
*
* Provides the {@link HawkbitServer} and {@link HawkbitClient} beans.
- * Mode-specific authentication is provided via the request interceptor bean from:
+ * Mode-specific beans (Tenant, request interceptor) are provided by:
*
* {@link McpHttpClientConfiguration} - for HTTP mode (per-request authentication)
* {@link McpStdioClientConfiguration} - for STDIO mode (static credentials)
@@ -70,29 +69,4 @@ public HawkbitClient hawkbitClient(
.build();
}
- /**
- * Tenant bean - credentials depend on the mode:
- *
- * STDIO mode with credentials: uses static credentials from configuration
- * STDIO mode without credentials: null credentials (will fail)
- * HTTP mode: null credentials (auth comes from request context via interceptor)
- *
- */
- @Bean
- public Tenant dummyTenant(@Value("${spring.ai.mcp.server.stdio:false}") final boolean stdioMode) {
- final Tenant tenant = new Tenant();
- // Tenant fields are null by default - only set credentials in STDIO mode when configured
- if (stdioMode) {
- if (properties.hasStaticCredentials()) {
- tenant.setUsername(properties.getUsername());
- tenant.setPassword(properties.getPassword());
- log.info("Configured tenant with static credentials for STDIO mode");
- } else {
- log.warn("STDIO mode enabled but no static credentials configured");
- }
- } else {
- log.info("Configured tenant for HTTP mode (per-request authentication)");
- }
- return tenant;
- }
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
index 60940fa879..bf41a641fd 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpHttpClientConfiguration.java
@@ -59,4 +59,14 @@ public BiFunction hawkbitRequestIntercep
};
}
+ /**
+ * Tenant bean for HTTP mode - credentials are null as authentication
+ * comes from the incoming HTTP request context via the interceptor.
+ */
+ @Bean
+ public Tenant dummyTenant() {
+ log.info("Configured tenant for HTTP mode (per-request authentication)");
+ return new Tenant();
+ }
+
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
index dd803cba68..1c5886ae29 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpSecurityConfiguration.java
@@ -51,19 +51,13 @@
@Slf4j
@Configuration
@EnableWebSecurity
+@RequiredArgsConstructor
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(name = "spring.ai.mcp.server.stdio", havingValue = "false", matchIfMissing = true)
public class McpSecurityConfiguration {
private final Optional authenticationValidator;
- public McpSecurityConfiguration(final Optional authenticationValidator) {
- this.authenticationValidator = authenticationValidator;
- if (authenticationValidator.isEmpty()) {
- log.info("Authentication validation disabled - requests will be forwarded without validation");
- }
- }
-
@Bean
@SuppressWarnings("java:S4502") // CSRF protection is not needed for stateless REST APIs using Authorization header
public SecurityFilterChain mcpSecurityFilterChain(final HttpSecurity http) throws Exception {
@@ -72,11 +66,14 @@ public SecurityFilterChain mcpSecurityFilterChain(final HttpSecurity http) throw
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
- authenticationValidator.ifPresent(validator -> {
- log.info("Authentication validation enabled - adding validation filter");
- http.addFilterBefore(new HawkBitAuthenticationFilter(validator),
- UsernamePasswordAuthenticationFilter.class);
- });
+ authenticationValidator.ifPresentOrElse(
+ validator -> {
+ log.info("Authentication validation enabled - adding validation filter");
+ http.addFilterBefore(new HawkBitAuthenticationFilter(validator),
+ UsernamePasswordAuthenticationFilter.class);
+ },
+ () -> log.info("Authentication validation disabled - requests will be forwarded without validation")
+ );
return http.build();
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
index af05c6ddfc..bca2bdfdfa 100644
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
+++ b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpStdioClientConfiguration.java
@@ -57,4 +57,20 @@ public BiFunction hawkbitRequestIntercep
};
}
+ /**
+ * Tenant bean for STDIO mode - uses static credentials from configuration properties.
+ */
+ @Bean
+ public Tenant dummyTenant() {
+ final Tenant tenant = new Tenant();
+ if (properties.hasStaticCredentials()) {
+ tenant.setUsername(properties.getUsername());
+ tenant.setPassword(properties.getPassword());
+ log.info("Configured tenant with static credentials for STDIO mode");
+ } else {
+ log.warn("STDIO mode enabled but no static credentials configured");
+ }
+ return tenant;
+ }
+
}
diff --git a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java b/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
deleted file mode 100644
index 1d718eeda7..0000000000
--- a/hawkbit-mcp/src/main/java/org/eclipse/hawkbit/mcp/server/config/McpValidationConfiguration.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Copyright (c) 2026 Contributors to the Eclipse Foundation
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipse.hawkbit.mcp.server.config;
-
-import org.eclipse.hawkbit.mcp.server.client.AuthenticationValidator;
-import org.eclipse.hawkbit.mcp.server.client.HawkbitAuthenticationValidator;
-import org.eclipse.hawkbit.sdk.HawkbitClient;
-import org.eclipse.hawkbit.sdk.Tenant;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * Configuration for authentication validation beans.
- */
-@Configuration
-public class McpValidationConfiguration {
-
- /**
- * Creates the hawkBit authentication validator when validation is enabled.
- * Validates credentials against hawkBit REST API with result caching.
- *
- * @param hawkbitClient the hawkBit client for API calls
- * @param dummyTenant the tenant configuration
- * @param properties the MCP properties with cache configuration
- * @return the authentication validator
- */
- @Bean
- @ConditionalOnProperty(name = "hawkbit.mcp.validation.enabled", havingValue = "true", matchIfMissing = true)
- public AuthenticationValidator hawkbitAuthenticationValidator(
- final HawkbitClient hawkbitClient,
- final Tenant dummyTenant,
- final HawkbitMcpProperties properties) {
- return new HawkbitAuthenticationValidator(hawkbitClient, dummyTenant, properties);
- }
-}