hawkBit MCP server (#2871)
* hawkBit MCP server Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> * Fix STDIO authentication support. Change license headers. Inline Docker build Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> * Address PR review: refactor operation DTOs to sealed interfaces, make authentication validator conditional, and separate HTTP/STDIO client configurations Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> * Address PR review. Provide More context in tools description. Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> * Address PR Review Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> --------- Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>
This commit is contained in:
@@ -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
|
||||
|
||||
109
hawkbit-mcp/README.md
Normal file
109
hawkbit-mcp/README.md
Normal file
@@ -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 <BASE64_ENCODED_CREDENTIALS>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Start the server separately:
|
||||
|
||||
```bash
|
||||
java -jar hawkbit-mcp-server-0-SNAPSHOT.jar \
|
||||
--hawkbit.mcp.mgmt-url=<HAWKBIT_URL>
|
||||
```
|
||||
|
||||
**Generating Base64 credentials:**
|
||||
|
||||
```bash
|
||||
# Linux/Mac
|
||||
echo -n "<USERNAME>:<PASSWORD>" | base64
|
||||
|
||||
# PowerShell
|
||||
[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("<USERNAME>:<PASSWORD>"))
|
||||
```
|
||||
|
||||
### 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_URL>",
|
||||
"HAWKBIT_USERNAME": "<USERNAME>",
|
||||
"HAWKBIT_PASSWORD": "<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
|
||||
```
|
||||
135
hawkbit-mcp/pom.xml
Normal file
135
hawkbit-mcp/pom.xml
Normal file
@@ -0,0 +1,135 @@
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-mcp-server</artifactId>
|
||||
<name>hawkBit :: MCP Server (Standalone)</name>
|
||||
<description>Standalone MCP server that connects to hawkBit via REST API</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-sdk-mgmt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-hateoas</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-mcp-annotations</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-hawkbit-docs</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.outputDirectory}/hawkbit-docs</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.basedir}/../docs</directory>
|
||||
<includes>
|
||||
<include>README.md</include>
|
||||
<include>what-is-hawkbit.md</include>
|
||||
<include>quick-start.md</include>
|
||||
<include>features.md</include>
|
||||
<include>architecture.md</include>
|
||||
<include>base-setup.md</include>
|
||||
<include>hawkbit-sdk.md</include>
|
||||
<include>feign-client.md</include>
|
||||
<include>clustering.md</include>
|
||||
<include>authentication.md</include>
|
||||
<include>authorization.md</include>
|
||||
<include>datamodel.md</include>
|
||||
<include>rollout-management.md</include>
|
||||
<include>targetstate.md</include>
|
||||
<include>management-api.md</include>
|
||||
<include>direct-device-integration-api.md</include>
|
||||
<include>device-management-federation-api.md</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.eclipse.hawkbit.mcp.server.HawkbitMcpServerApplication</mainClass>
|
||||
<attach>false</attach>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>repackage</id>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<classifier>exec</classifier>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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.
|
||||
* <p>
|
||||
* This server acts as a proxy between MCP clients and hawkBit,
|
||||
* passing through authentication credentials to the hawkBit REST API.
|
||||
* </p>
|
||||
*/
|
||||
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(HawkbitMcpProperties.class)
|
||||
public class HawkbitMcpServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HawkbitMcpServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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 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.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
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.
|
||||
* 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;
|
||||
private final Tenant dummyTenant;
|
||||
private final Cache<String, Boolean> validationCache;
|
||||
|
||||
public HawkbitAuthenticationValidator(final HawkbitClient hawkbitClient,
|
||||
final Tenant dummyTenant,
|
||||
final HawkbitMcpProperties properties) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
this.dummyTenant = dummyTenant;
|
||||
|
||||
this.validationCache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(properties.getValidation().getCacheTtl())
|
||||
.maximumSize(properties.getValidation().getCacheMaxSize())
|
||||
.build();
|
||||
|
||||
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 (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(final 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(final 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public class McpAuthenticationException extends RuntimeException {
|
||||
|
||||
public McpAuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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.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.
|
||||
* <p>
|
||||
* Provides the {@link HawkbitServer} and {@link HawkbitClient} beans.
|
||||
* Mode-specific beans (Tenant, request interceptor) are provided by:
|
||||
* <ul>
|
||||
* <li>{@link McpHttpClientConfiguration} - for HTTP mode (per-request authentication)</li>
|
||||
* <li>{@link McpStdioClientConfiguration} - for STDIO mode (static credentials)</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class HawkbitClientConfiguration {
|
||||
|
||||
private final HawkbitMcpProperties properties;
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public HawkbitServer hawkbitServer() {
|
||||
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,
|
||||
final BiFunction<Tenant, Controller, RequestInterceptor> hawkbitRequestInterceptor) {
|
||||
log.info("Configuring hawkBit client");
|
||||
return HawkbitClient.builder()
|
||||
.hawkBitServer(server)
|
||||
.encoder(encoder)
|
||||
.decoder(decoder)
|
||||
.contract(contract)
|
||||
.requestInterceptorFn(hawkbitRequestInterceptor)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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 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;
|
||||
|
||||
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., <a href="http://localhost:8080">...</a>).
|
||||
*/
|
||||
@NotBlank(message = "hawkbit.mcp.mgmt-url must be configured")
|
||||
private String mgmtUrl;
|
||||
|
||||
/**
|
||||
* Username for hawkBit authentication (used in STDIO mode).
|
||||
* Read directly from HAWKBIT_USERNAME environment variable.
|
||||
*/
|
||||
@Value("${HAWKBIT_USERNAME:#{null}}")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* Password for hawkBit authentication (used in STDIO mode).
|
||||
* 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 && password != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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.RequestInterceptor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.sdk.Controller;
|
||||
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.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Configuration for HTTP mode.
|
||||
* <p>
|
||||
* In HTTP mode, authentication is extracted from the incoming HTTP request's
|
||||
* Authorization header and forwarded to hawkBit (per-request authentication).
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@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 BiFunction<Tenant, Controller, RequestInterceptor> hawkbitRequestInterceptor() {
|
||||
log.info("Configuring HTTP mode request interceptor (per-request authentication)");
|
||||
return (tenant, controller) -> template -> {
|
||||
final ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs != null) {
|
||||
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");
|
||||
} else {
|
||||
log.warn("No authentication header in request - request will likely fail");
|
||||
}
|
||||
} else {
|
||||
log.warn("No request context available - request will likely fail");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 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.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;
|
||||
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;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Security configuration for the MCP server.
|
||||
* <p>
|
||||
* This configuration is only active in HTTP/servlet mode. In STDIO mode,
|
||||
* authentication is handled via static credentials from properties.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*/
|
||||
@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> 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));
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter that validates authentication against hawkBit.
|
||||
* <p>
|
||||
* Only added to the filter chain when authentication validation is enabled.
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public static class HawkBitAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final AuthenticationValidator validator;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final @NonNull HttpServletResponse response,
|
||||
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);
|
||||
case MISSING_CREDENTIALS -> {
|
||||
log.debug("Rejecting request: missing credentials");
|
||||
sendErrorResponse(response, HttpStatus.UNAUTHORIZED,
|
||||
"Authentication required. Please provide hawkBit credentials.");
|
||||
}
|
||||
case INVALID_CREDENTIALS -> {
|
||||
log.debug("Rejecting request: invalid credentials");
|
||||
sendErrorResponse(response, HttpStatus.UNAUTHORIZED,
|
||||
"Invalid hawkBit credentials.");
|
||||
}
|
||||
case HAWKBIT_ERROR -> {
|
||||
log.warn("Rejecting request: hawkBit unavailable");
|
||||
sendErrorResponse(response, HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"Unable to validate credentials. hawkBit may be unavailable.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.RequestInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.sdk.Controller;
|
||||
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.
|
||||
* <p>
|
||||
* In STDIO mode, authentication uses static credentials from configuration properties
|
||||
* (environment variables HAWKBIT_USERNAME and HAWKBIT_PASSWORD).
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(name = "spring.ai.mcp.server.stdio", havingValue = "true")
|
||||
public class McpStdioClientConfiguration {
|
||||
|
||||
private final HawkbitMcpProperties properties;
|
||||
|
||||
/**
|
||||
* Request interceptor for STDIO mode - uses static credentials from configuration.
|
||||
*/
|
||||
@Bean
|
||||
public BiFunction<Tenant, Controller, RequestInterceptor> hawkbitRequestInterceptor() {
|
||||
log.info("Configuring STDIO mode request interceptor (static credentials)");
|
||||
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 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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.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.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>All beans use {@code @ConditionalOnMissingBean} - override by defining your own bean</li>
|
||||
* <li>Properties allow disabling built-in tools/resources/prompts</li>
|
||||
* <li>Spring AI MCP auto-discovers {@code @Tool}, {@code @McpResource}, and {@code @McpPrompt} annotations</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
@Configuration
|
||||
public class McpToolConfiguration {
|
||||
|
||||
/**
|
||||
* Creates the hawkBit tool provider.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*/
|
||||
@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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* </p>
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "hawkbit.mcp.resources-enabled", havingValue = "true", matchIfMissing = true)
|
||||
public HawkbitDocumentationResource hawkBitDocumentationResource() {
|
||||
return new HawkbitDocumentationResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the hawkBit prompt provider.
|
||||
* <p>
|
||||
* 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}.
|
||||
* </p>
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "hawkbit.mcp.prompts-enabled", havingValue = "true", matchIfMissing = true)
|
||||
public HawkbitPromptProvider hawkBitPromptProvider() {
|
||||
return new HawkbitPromptProvider();
|
||||
}
|
||||
}
|
||||
@@ -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 actionId to delete
|
||||
*/
|
||||
record Delete(Long actionId) 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<Long> actionIds, String rsql) implements ActionRequest {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Unified response wrapper for management operations.
|
||||
*
|
||||
* @param <T> 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<T>(
|
||||
String operation,
|
||||
boolean success,
|
||||
String message,
|
||||
T data
|
||||
) {
|
||||
|
||||
/**
|
||||
* Creates a successful response with data.
|
||||
*/
|
||||
public static <T> OperationResponse<T> 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 <T> OperationResponse<T> 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 <T> OperationResponse<T> 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 <T> OperationResponse<T> failure(final String operation, final String message) {
|
||||
return new OperationResponse<>(operation, false, message, null);
|
||||
}
|
||||
}
|
||||
@@ -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.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Generic paged response for MCP tool results.
|
||||
*
|
||||
* @param <T> the type of items in the response
|
||||
*/
|
||||
public record PagedResponse<T>(
|
||||
List<T> content,
|
||||
long total,
|
||||
int offset,
|
||||
int limit
|
||||
) {
|
||||
|
||||
public static <T> PagedResponse<T> of(final List<T> content, final long total, final int offset, final int limit) {
|
||||
return new PagedResponse<>(content, total, offset, limit);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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.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.
|
||||
* <p>
|
||||
* These prompts help LLMs understand what hawkBit is and what documentation
|
||||
* resources are available at the start of a session.
|
||||
* </p>
|
||||
*/
|
||||
@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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 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.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/";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* 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.tools;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.mcp.server.config.HawkbitMcpProperties;
|
||||
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.OperationResponse;
|
||||
import org.eclipse.hawkbit.mcp.server.dto.PagedResponse;
|
||||
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;
|
||||
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.
|
||||
* <p>
|
||||
* Provides tools for managing targets, rollouts, distribution sets, actions,
|
||||
* software modules, and target filter queries via the hawkBit REST API.
|
||||
* </p>
|
||||
*/
|
||||
@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 <T> PagedResponse<T> toPagedResponse(final PagedList<T> 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<MgmtTarget> 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<PagedList<MgmtTarget>> 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<MgmtRolloutResponseBody> 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<PagedList<MgmtRolloutResponseBody>> 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<MgmtDistributionSet> 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<PagedList<MgmtDistributionSet>> 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<MgmtAction> 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<PagedList<MgmtAction>> 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<MgmtSoftwareModule> 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<PagedList<MgmtSoftwareModule>> 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<MgmtTargetFilterQuery> 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<PagedList<MgmtTargetFilterQuery>> 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. 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: " +
|
||||
"{\"type\":\"Create\",\"body\":{\"controllerId\":\"id\",\"name\":\"name\"}}, " +
|
||||
"{\"type\":\"Update\",\"controllerId\":\"id\",\"body\":{...}}, " +
|
||||
"{\"type\":\"Delete\",\"controllerId\":\"id\"}")
|
||||
public OperationResponse<Object> manageTarget(final TargetRequest request) {
|
||||
log.debug("Managing target: request={}", request.getClass().getSimpleName());
|
||||
|
||||
final MgmtTargetRestApi api = hawkbitClient.mgmtService(MgmtTargetRestApi.class, dummyTenant);
|
||||
|
||||
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<List<MgmtTarget>> response = api.createTargets(List.of(r.body()));
|
||||
final List<MgmtTarget> 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<MgmtTarget> 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. " +
|
||||
"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\"}")
|
||||
public OperationResponse<Object> manageRollout(final RolloutRequest request) {
|
||||
log.debug("Managing rollout: request={}", request.getClass().getSimpleName());
|
||||
|
||||
final MgmtRolloutRestApi api = hawkbitClient.mgmtService(MgmtRolloutRestApi.class, dummyTenant);
|
||||
|
||||
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<MgmtRolloutResponseBody> 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<MgmtRolloutResponseBody> 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<MgmtRolloutResponseBody> 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). " +
|
||||
"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<Object> manageDistributionSet(final DistributionSetRequest request) {
|
||||
log.debug("Managing distribution set: request={}", request.getClass().getSimpleName());
|
||||
|
||||
final MgmtDistributionSetRestApi api = hawkbitClient.mgmtService(MgmtDistributionSetRestApi.class, dummyTenant);
|
||||
|
||||
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<List<MgmtDistributionSet>> response = api.createDistributionSets(List.of(r.body()));
|
||||
final List<MgmtDistributionSet> 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<MgmtDistributionSet> 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. " +
|
||||
"Use 'type' field to select operation: " +
|
||||
"{\"type\":\"Delete\",\"actionIds\":[123]}, " +
|
||||
"{\"type\":\"DeleteBatch\",\"actionIds\":[1,2,3],\"rsql\":\"\"}")
|
||||
public OperationResponse<Object> manageAction(final ActionRequest request) {
|
||||
log.debug("Managing action: request={}", request.getClass().getSimpleName());
|
||||
|
||||
final MgmtActionRestApi api = hawkbitClient.mgmtService(MgmtActionRestApi.class, dummyTenant);
|
||||
|
||||
if (request instanceof ActionRequest.Delete r) {
|
||||
validateActionOperation("delete");
|
||||
if (r.actionId() == null) {
|
||||
return OperationResponse.failure(OP_DELETE, "actionId is required for DELETE operation");
|
||||
}
|
||||
api.deleteAction(r.actionId());
|
||||
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. " +
|
||||
"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<Object> manageSoftwareModule(final SoftwareModuleRequest request) {
|
||||
log.debug("Managing software module: request={}", request.getClass().getSimpleName());
|
||||
|
||||
final MgmtSoftwareModuleRestApi api = hawkbitClient.mgmtService(MgmtSoftwareModuleRestApi.class, dummyTenant);
|
||||
|
||||
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<List<MgmtSoftwareModule>> response = api.createSoftwareModules(List.of(r.body()));
|
||||
final List<MgmtSoftwareModule> 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<MgmtSoftwareModule> 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. " +
|
||||
"Use 'type' field to select operation: " +
|
||||
"{\"type\":\"Create\",\"body\":{\"name\":\"n\",\"query\":\"name==*\"}}, " +
|
||||
"{\"type\":\"Update\",\"filterId\":123,\"body\":{...}}, " +
|
||||
"{\"type\":\"Delete\",\"filterId\":123}")
|
||||
public OperationResponse<Object> manageTargetFilter(final TargetFilterRequest request) {
|
||||
log.debug("Managing target filter: request={}", request.getClass().getSimpleName());
|
||||
|
||||
final MgmtTargetFilterQueryRestApi api = hawkbitClient.mgmtService(MgmtTargetFilterQueryRestApi.class, dummyTenant);
|
||||
|
||||
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<MgmtTargetFilterQuery> 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<MgmtTargetFilterQuery> 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 String operation, final String entity) {
|
||||
if (!isOperationEnabled(operation, entity)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Operation " + operation.toUpperCase() + " is not enabled for " + entity +
|
||||
". Check hawkbit.mcp.operations configuration.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRolloutOperation(final String operation) {
|
||||
final HawkbitMcpProperties.RolloutConfig config = properties.getOperations().getRollouts();
|
||||
final Boolean entitySetting = config.getOperationEnabled(operation);
|
||||
|
||||
// For standard CRUD ops, check global fallback
|
||||
if (entitySetting == null) {
|
||||
if (!properties.getOperations().isGlobalOperationEnabled(operation)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Operation " + operation.toUpperCase() + " is not enabled for rollouts. " +
|
||||
"Check hawkbit.mcp.operations configuration.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entitySetting) {
|
||||
throw new IllegalArgumentException(
|
||||
"Operation " + operation.toUpperCase() + " is not enabled for rollouts. " +
|
||||
"Check hawkbit.mcp.operations configuration.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateActionOperation(final String operation) {
|
||||
final HawkbitMcpProperties.ActionConfig config = properties.getOperations().getActions();
|
||||
final Boolean entitySetting = config.getOperationEnabled(operation);
|
||||
|
||||
if (entitySetting == null) {
|
||||
if (operation.equals("delete") && !properties.getOperations().isGlobalOperationEnabled("delete")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Operation " + operation.toUpperCase() + " is not enabled for actions. " +
|
||||
"Check hawkbit.mcp.operations configuration.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entitySetting) {
|
||||
throw new IllegalArgumentException(
|
||||
"Operation " + operation.toUpperCase() + " 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
54
hawkbit-mcp/src/main/resources/application.properties
Normal file
54
hawkbit-mcp/src/main/resources/application.properties
Normal file
@@ -0,0 +1,54 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
# 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
|
||||
|
||||
# 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.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
|
||||
351
hawkbit-mcp/src/main/resources/hawkbit-entity-definitions.md
Normal file
351
hawkbit-mcp/src/main/resources/hawkbit-entity-definitions.md
Normal file
@@ -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.<key>` | Metadata value by key | String |
|
||||
| `controllerAttributes.<key>` | 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.<key>` | 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.<key>` | 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*
|
||||
```
|
||||
48
hawkbit-mcp/src/main/resources/prompts/hawkbit-context.md
Normal file
48
hawkbit-mcp/src/main/resources/prompts/hawkbit-context.md
Normal file
@@ -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`
|
||||
51
hawkbit-mcp/src/main/resources/prompts/rsql-help.md
Normal file
51
hawkbit-mcp/src/main/resources/prompts/rsql-help.md
Normal file
@@ -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`.
|
||||
11
pom.xml
11
pom.xml
@@ -77,6 +77,8 @@
|
||||
<commons-io.version>2.21.0</commons-io.version>
|
||||
<commons-collections4.version>4.5.0</commons-collections4.version>
|
||||
<io-protostuff.version>1.8.0</io-protostuff.version>
|
||||
<!-- Spring AI for MCP support -->
|
||||
<spring-ai.version>1.1.2</spring-ai.version>
|
||||
<!-- test -->
|
||||
<rabbitmq.http-client.version>5.4.0</rabbitmq.http-client.version>
|
||||
<classgraph.version>4.8.184</classgraph.version>
|
||||
@@ -241,6 +243,14 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Spring AI for MCP Server -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
@@ -728,6 +738,7 @@
|
||||
<module>hawkbit-mgmt</module>
|
||||
<module>hawkbit-ddi</module>
|
||||
<module>hawkbit-dmf</module>
|
||||
<module>hawkbit-mcp</module>
|
||||
<module>hawkbit-monolith</module>
|
||||
|
||||
<module>hawkbit-ui</module>
|
||||
|
||||
Reference in New Issue
Block a user