Skip to content

Commit b0d48e0

Browse files
hawkBit MCP server
Signed-off-by: Denislav Prinov <[email protected]>
1 parent c48877e commit b0d48e0

30 files changed

+2551
-0
lines changed

hawkbit-mcp/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#
2+
# Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
#
4+
# This program and the accompanying materials are made
5+
# available under the terms of the Eclipse Public License 2.0
6+
# which is available at https://www.eclipse.org/legal/epl-2.0/
7+
#
8+
# SPDX-License-Identifier: EPL-2.0
9+
#
10+
11+
FROM eclipse-temurin:17-jre-alpine
12+
13+
LABEL maintainer="Eclipse hawkBit Project"
14+
LABEL org.opencontainers.image.source="https://github.com/eclipse-hawkbit/hawkbit"
15+
LABEL org.opencontainers.image.description="Standalone MCP Server for hawkBit"
16+
17+
ARG JAR_FILE=target/*.jar
18+
19+
COPY ${JAR_FILE} app.jar
20+
21+
EXPOSE 8081
22+
23+
ENV HAWKBIT_URL=http://localhost:8080
24+
25+
ENTRYPOINT ["java", "-jar", "/app.jar"]

hawkbit-mcp/pom.xml

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<!--
2+
3+
Copyright (c) 2025 Contributors to the Eclipse Foundation
4+
5+
This program and the accompanying materials are made
6+
available under the terms of the Eclipse Public License 2.0
7+
which is available at https://www.eclipse.org/legal/epl-2.0/
8+
9+
SPDX-License-Identifier: EPL-2.0
10+
11+
-->
12+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
13+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
14+
<modelVersion>4.0.0</modelVersion>
15+
<parent>
16+
<groupId>org.eclipse.hawkbit</groupId>
17+
<artifactId>hawkbit-parent</artifactId>
18+
<version>${revision}</version>
19+
</parent>
20+
21+
<artifactId>hawkbit-mcp-server</artifactId>
22+
<name>hawkBit :: MCP Server (Standalone)</name>
23+
<description>Standalone MCP server that connects to hawkBit via REST API</description>
24+
25+
<dependencies>
26+
<dependency>
27+
<groupId>org.eclipse.hawkbit</groupId>
28+
<artifactId>hawkbit-sdk-mgmt</artifactId>
29+
<version>${project.version}</version>
30+
<exclusions>
31+
<exclusion>
32+
<groupId>org.springdoc</groupId>
33+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
34+
</exclusion>
35+
<exclusion>
36+
<groupId>org.springframework.boot</groupId>
37+
<artifactId>spring-boot-starter-hateoas</artifactId>
38+
</exclusion>
39+
</exclusions>
40+
</dependency>
41+
42+
<dependency>
43+
<groupId>org.springframework.boot</groupId>
44+
<artifactId>spring-boot-starter-validation</artifactId>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.springframework.boot</groupId>
48+
<artifactId>spring-boot-starter-security</artifactId>
49+
</dependency>
50+
51+
<dependency>
52+
<groupId>org.springframework.ai</groupId>
53+
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
54+
</dependency>
55+
<dependency>
56+
<groupId>org.springframework.ai</groupId>
57+
<artifactId>spring-ai-mcp-annotations</artifactId>
58+
</dependency>
59+
60+
<dependency>
61+
<groupId>org.springframework.boot</groupId>
62+
<artifactId>spring-boot-configuration-processor</artifactId>
63+
<optional>true</optional>
64+
</dependency>
65+
66+
<dependency>
67+
<groupId>com.github.ben-manes.caffeine</groupId>
68+
<artifactId>caffeine</artifactId>
69+
</dependency>
70+
</dependencies>
71+
72+
<build>
73+
<plugins>
74+
<plugin>
75+
<groupId>org.apache.maven.plugins</groupId>
76+
<artifactId>maven-resources-plugin</artifactId>
77+
<executions>
78+
<execution>
79+
<id>copy-hawkbit-docs</id>
80+
<phase>generate-resources</phase>
81+
<goals>
82+
<goal>copy-resources</goal>
83+
</goals>
84+
<configuration>
85+
<outputDirectory>${project.build.outputDirectory}/hawkbit-docs</outputDirectory>
86+
<resources>
87+
<resource>
88+
<directory>${project.basedir}/../docs</directory>
89+
<includes>
90+
<include>README.md</include>
91+
<include>what-is-hawkbit.md</include>
92+
<include>quick-start.md</include>
93+
<include>features.md</include>
94+
<include>architecture.md</include>
95+
<include>base-setup.md</include>
96+
<include>hawkbit-sdk.md</include>
97+
<include>feign-client.md</include>
98+
<include>clustering.md</include>
99+
<include>authentication.md</include>
100+
<include>authorization.md</include>
101+
<include>datamodel.md</include>
102+
<include>rollout-management.md</include>
103+
<include>targetstate.md</include>
104+
<include>management-api.md</include>
105+
<include>direct-device-integration-api.md</include>
106+
<include>device-management-federation-api.md</include>
107+
</includes>
108+
</resource>
109+
</resources>
110+
</configuration>
111+
</execution>
112+
</executions>
113+
</plugin>
114+
<plugin>
115+
<groupId>org.springframework.boot</groupId>
116+
<artifactId>spring-boot-maven-plugin</artifactId>
117+
<configuration>
118+
<mainClass>org.eclipse.hawkbit.mcp.server.HawkBitMcpServerApplication</mainClass>
119+
</configuration>
120+
<executions>
121+
<execution>
122+
<goals>
123+
<goal>repackage</goal>
124+
</goals>
125+
</execution>
126+
</executions>
127+
</plugin>
128+
</plugins>
129+
</build>
130+
</project>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*/
10+
package org.eclipse.hawkbit.mcp.server;
11+
12+
import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
13+
import org.springframework.boot.SpringApplication;
14+
import org.springframework.boot.autoconfigure.SpringBootApplication;
15+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
16+
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
17+
18+
/**
19+
* Standalone MCP Server application that connects to hawkBit via REST API.
20+
* <p>
21+
* This server acts as a proxy between MCP clients and hawkBit,
22+
* passing through authentication credentials to the hawkBit REST API.
23+
* </p>
24+
*/
25+
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
26+
@EnableConfigurationProperties(HawkBitMcpProperties.class)
27+
public class HawkBitMcpServerApplication {
28+
29+
public static void main(String[] args) {
30+
SpringApplication.run(HawkBitMcpServerApplication.class, args);
31+
}
32+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*/
10+
package org.eclipse.hawkbit.mcp.server.client;
11+
12+
import com.github.benmanes.caffeine.cache.Cache;
13+
import com.github.benmanes.caffeine.cache.Caffeine;
14+
import feign.FeignException;
15+
import lombok.extern.slf4j.Slf4j;
16+
import org.eclipse.hawkbit.mcp.server.config.HawkBitMcpProperties;
17+
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
18+
import org.eclipse.hawkbit.sdk.HawkbitClient;
19+
import org.eclipse.hawkbit.sdk.Tenant;
20+
import org.springframework.http.ResponseEntity;
21+
import org.springframework.stereotype.Component;
22+
23+
import java.nio.charset.StandardCharsets;
24+
import java.security.MessageDigest;
25+
import java.security.NoSuchAlgorithmException;
26+
import java.util.HexFormat;
27+
28+
/**
29+
* Validates authentication credentials against hawkBit REST API using the SDK.
30+
*/
31+
@Slf4j
32+
@Component
33+
public class HawkBitAuthenticationValidator {
34+
35+
private final HawkbitClient hawkbitClient;
36+
private final Tenant dummyTenant;
37+
private final Cache<String, Boolean> validationCache;
38+
private final boolean enabled;
39+
40+
public HawkBitAuthenticationValidator(HawkbitClient hawkbitClient,
41+
Tenant dummyTenant,
42+
HawkBitMcpProperties properties) {
43+
this.hawkbitClient = hawkbitClient;
44+
this.dummyTenant = dummyTenant;
45+
this.enabled = properties.getValidation().isEnabled();
46+
47+
this.validationCache = Caffeine.newBuilder()
48+
.expireAfterWrite(properties.getValidation().getCacheTtl())
49+
.maximumSize(properties.getValidation().getCacheMaxSize())
50+
.build();
51+
52+
log.info("Authentication validation {} with cache TTL={}, maxSize={}",
53+
enabled ? "enabled" : "disabled",
54+
properties.getValidation().getCacheTtl(),
55+
properties.getValidation().getCacheMaxSize());
56+
}
57+
58+
/**
59+
* Validates the given authorization header against hawkBit.
60+
* @param authHeader the Authorization header value
61+
* @return validation result
62+
*/
63+
public ValidationResult validate(String authHeader) {
64+
if (!enabled) {
65+
return ValidationResult.VALID;
66+
}
67+
68+
if (authHeader == null || authHeader.isBlank()) {
69+
return ValidationResult.MISSING_CREDENTIALS;
70+
}
71+
72+
String cacheKey = hashAuthHeader(authHeader);
73+
Boolean cachedResult = validationCache.getIfPresent(cacheKey);
74+
75+
if (cachedResult != null) {
76+
log.debug("Authentication validation cache hit: valid={}", cachedResult);
77+
return cachedResult ? ValidationResult.VALID : ValidationResult.INVALID_CREDENTIALS;
78+
}
79+
80+
return validateWithHawkBit(cacheKey);
81+
}
82+
83+
private ValidationResult validateWithHawkBit(String cacheKey) {
84+
log.debug("Validating authentication against hawkBit using SDK");
85+
86+
try {
87+
MgmtTenantManagementRestApi tenantApi = hawkbitClient.mgmtService(
88+
MgmtTenantManagementRestApi.class, dummyTenant);
89+
90+
ResponseEntity<?> response = tenantApi.getTenantConfiguration();
91+
int statusCode = response.getStatusCode().value();
92+
93+
if (statusCode >= 200 && statusCode < 300) {
94+
log.debug("Authentication valid (status={})", statusCode);
95+
validationCache.put(cacheKey, true);
96+
return ValidationResult.VALID;
97+
} else {
98+
log.warn("Unexpected status from hawkBit during auth validation: {}", statusCode);
99+
return ValidationResult.HAWKBIT_ERROR;
100+
}
101+
} catch (FeignException.Unauthorized e) {
102+
log.debug("Authentication invalid (status=401)");
103+
validationCache.put(cacheKey, false);
104+
return ValidationResult.INVALID_CREDENTIALS;
105+
} catch (FeignException.Forbidden e) {
106+
// 403 = Valid credentials but lacks READ_TENANT_CONFIGURATION permission
107+
// User is authenticated in hawkBit but doesn't have this specific permission
108+
log.debug("Authentication valid but lacks permission (status=403)");
109+
validationCache.put(cacheKey, true);
110+
return ValidationResult.VALID;
111+
} catch (FeignException e) {
112+
log.warn("Error validating authentication against hawkBit: {} - {}",
113+
e.getClass().getSimpleName(), e.getMessage());
114+
return ValidationResult.HAWKBIT_ERROR;
115+
} catch (Exception e) {
116+
// Unexpected errors, don't cache, fail closed
117+
log.warn("Unexpected error validating authentication against hawkBit: {}", e.getMessage());
118+
return ValidationResult.HAWKBIT_ERROR;
119+
}
120+
}
121+
122+
private String hashAuthHeader(String authHeader) {
123+
try {
124+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
125+
byte[] hash = digest.digest(authHeader.getBytes(StandardCharsets.UTF_8));
126+
return HexFormat.of().formatHex(hash);
127+
} catch (NoSuchAlgorithmException e) {
128+
// SHA-256 is always available
129+
throw new McpAuthenticationException("SHA-256 not available." + e.getMessage());
130+
}
131+
}
132+
133+
/**
134+
* Result of authentication validation.
135+
*/
136+
public enum ValidationResult {
137+
/**
138+
* Credentials are valid (authenticated user).
139+
*/
140+
VALID,
141+
142+
/**
143+
* No credentials provided.
144+
*/
145+
MISSING_CREDENTIALS,
146+
147+
/**
148+
* Credentials are invalid (401 from hawkBit).
149+
*/
150+
INVALID_CREDENTIALS,
151+
152+
/**
153+
* hawkBit is unavailable or returned unexpected error.
154+
*/
155+
HAWKBIT_ERROR
156+
}
157+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*/
10+
package org.eclipse.hawkbit.mcp.server.client;
11+
12+
public class McpAuthenticationException extends RuntimeException {
13+
14+
public McpAuthenticationException(String message) {
15+
super(message);
16+
}
17+
}

0 commit comments

Comments
 (0)