Skip to content

Commit d97292e

Browse files
authored
Merge branch 'kafbat:main' into main
2 parents 3ad2ea9 + 14d9ec0 commit d97292e

File tree

90 files changed

+956
-280
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+956
-280
lines changed

.github/workflows/backend_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ jobs:
3434

3535
- name: "Tests"
3636
run: |
37-
./gradlew :api:test --info
37+
./gradlew :api:test

api/build.gradle

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ dependencies {
6262
implementation libs.netty.common
6363
implementation libs.netty.handler
6464

65+
implementation libs.modelcontextprotocol.spring.webflux
66+
implementation libs.victools.jsonschema.generator
6567

6668
// Google Managed Service for Kafka IAM support
6769
implementation (libs.google.managed.kafka.login.handler) {
@@ -104,14 +106,20 @@ generateGrammarSource {
104106
arguments += ["-package", "ksql"]
105107
}
106108

109+
tasks.withType(JavaCompile) {
110+
options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked"
111+
}
112+
113+
114+
107115
sourceSets {
108116
main {
109-
antlr {
110-
srcDirs = ["src/main/antlr4"]
111-
}
112117
java {
113118
srcDirs += generateGrammarSource.outputDirectory
114119
}
120+
resources {
121+
srcDirs += project(":frontend").layout.buildDirectory.dir("vite")
122+
}
115123
}
116124
}
117125

@@ -149,15 +157,7 @@ if (buildDockerImages) {
149157
}
150158

151159
if (includeFrontend) {
152-
tasks.named("build") {
153-
dependsOn(":frontend:buildFrontend")
154-
}
155-
156160
tasks.named("processResources") {
157161
dependsOn(":frontend:buildFrontend")
158-
from(project(":frontend").layout.buildDirectory.dir("vite")) {
159-
into("static")
160-
}
161-
into("$buildDir/resources/main")
162162
}
163163
}

api/src/main/java/io/kafbat/ui/config/CustomWebFilter.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ public class CustomWebFilter implements WebFilter {
1717

1818
final String path = exchange.getRequest().getPath().pathWithinApplication().value();
1919

20+
ServerWebExchange filterExchange = exchange;
21+
2022
if (path.startsWith("/ui") || path.isEmpty() || path.equals("/")) {
21-
return chain.filter(
22-
exchange.mutate().request(
23-
exchange.getRequest().mutate()
24-
.path(basePath + "/index.html")
25-
.contextPath(basePath)
26-
.build()
27-
).build()
28-
);
23+
filterExchange = exchange.mutate().request(
24+
exchange.getRequest().mutate()
25+
.path(basePath + "/index.html")
26+
.contextPath(basePath)
27+
.build()
28+
).build();
2929
}
3030

31-
return chain.filter(exchange);
31+
return chain.filter(filterExchange).contextWrite(ctx -> ctx.put(ServerWebExchange.class, exchange));
3232
}
3333
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.kafbat.ui.config;
2+
3+
import com.github.victools.jsonschema.generator.OptionPreset;
4+
import com.github.victools.jsonschema.generator.SchemaGenerator;
5+
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
6+
import com.github.victools.jsonschema.generator.SchemaVersion;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
@Configuration
11+
public class JsonSchemaConfig {
12+
@Bean
13+
public SchemaGenerator schemaGenerator() {
14+
SchemaGeneratorConfigBuilder configBuilder =
15+
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON);
16+
return new SchemaGenerator(configBuilder.build());
17+
}
18+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.kafbat.ui.config;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.kafbat.ui.service.mcp.McpSpecificationGenerator;
5+
import io.kafbat.ui.service.mcp.McpTool;
6+
import io.modelcontextprotocol.server.McpAsyncServer;
7+
import io.modelcontextprotocol.server.McpServer;
8+
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
9+
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
10+
import io.modelcontextprotocol.spec.McpSchema;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.context.annotation.Configuration;
17+
import org.springframework.web.reactive.function.server.RouterFunction;
18+
19+
@Configuration
20+
@RequiredArgsConstructor
21+
@ConditionalOnProperty(value = "mcp.enabled", havingValue = "true")
22+
public class McpConfig {
23+
24+
private final List<McpTool> mcpTools;
25+
private final McpSpecificationGenerator mcpSpecificationGenerator;
26+
27+
// SSE transport
28+
@Bean
29+
public WebFluxSseServerTransportProvider sseServerTransport(ObjectMapper mapper) {
30+
return new WebFluxSseServerTransportProvider(mapper, "/mcp/message", "/mcp/sse");
31+
}
32+
33+
// Router function for SSE transport used by Spring WebFlux to start an HTTP
34+
// server.
35+
36+
@Bean
37+
public RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransportProvider transport) {
38+
return transport.getRouterFunction();
39+
}
40+
41+
@Bean
42+
public McpAsyncServer mcpServer(WebFluxSseServerTransportProvider transport) {
43+
44+
// Configure server capabilities with resource support
45+
var capabilities = McpSchema.ServerCapabilities.builder()
46+
.resources(false, true)
47+
.tools(true) // Tool support with list changes notifications
48+
.prompts(false) // Prompt support with list changes notifications
49+
.logging() // Logging support
50+
.build();
51+
52+
// Create the server with both tool and resource capabilities
53+
return McpServer.async(transport)
54+
.serverInfo("Kafka UI MCP", "0.0.1")
55+
.capabilities(capabilities)
56+
.tools(tools())
57+
.build();
58+
}
59+
60+
private List<AsyncToolSpecification> tools() {
61+
List<AsyncToolSpecification> tools = new ArrayList<>();
62+
for (McpTool mcpTool : mcpTools) {
63+
tools.addAll(mcpSpecificationGenerator.convertTool(mcpTool));
64+
}
65+
return tools;
66+
}
67+
}

api/src/main/java/io/kafbat/ui/config/auth/AbstractAuthSecurityConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ protected AbstractAuthSecurityConfig() {
2121
"/manifest.json",
2222
"/favicon.svg",
2323
"/favicon/**",
24+
"/images/**",
25+
"/fonts/**",
2426

2527
"/static/**",
2628
"/resources/**",

api/src/main/java/io/kafbat/ui/controller/AclsController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.kafbat.ui.model.rbac.AccessContext;
1212
import io.kafbat.ui.model.rbac.permission.AclAction;
1313
import io.kafbat.ui.service.acl.AclsService;
14+
import io.kafbat.ui.service.mcp.McpTool;
1415
import java.util.Optional;
1516
import lombok.RequiredArgsConstructor;
1617
import org.apache.kafka.common.resource.PatternType;
@@ -24,7 +25,7 @@
2425

2526
@RestController
2627
@RequiredArgsConstructor
27-
public class AclsController extends AbstractController implements AclsApi {
28+
public class AclsController extends AbstractController implements AclsApi, McpTool {
2829

2930
private final AclsService aclsService;
3031

api/src/main/java/io/kafbat/ui/controller/ApplicationConfigController.java

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55

66
import io.kafbat.ui.api.ApplicationConfigApi;
77
import io.kafbat.ui.config.ClustersProperties;
8-
import io.kafbat.ui.model.ActionDTO;
8+
import io.kafbat.ui.mapper.DynamicConfigMapper;
99
import io.kafbat.ui.model.AppAuthenticationSettingsDTO;
1010
import io.kafbat.ui.model.ApplicationConfigDTO;
11-
import io.kafbat.ui.model.ApplicationConfigPropertiesDTO;
1211
import io.kafbat.ui.model.ApplicationConfigValidationDTO;
1312
import io.kafbat.ui.model.ApplicationInfoDTO;
1413
import io.kafbat.ui.model.ClusterConfigValidationDTO;
@@ -20,12 +19,9 @@
2019
import io.kafbat.ui.util.ApplicationRestarter;
2120
import io.kafbat.ui.util.DynamicConfigOperations;
2221
import java.util.Map;
23-
import java.util.Optional;
2422
import javax.annotation.Nullable;
2523
import lombok.RequiredArgsConstructor;
2624
import lombok.extern.slf4j.Slf4j;
27-
import org.mapstruct.Mapper;
28-
import org.mapstruct.factory.Mappers;
2925
import org.springframework.http.ResponseEntity;
3026
import org.springframework.http.codec.multipart.FilePart;
3127
import org.springframework.http.codec.multipart.Part;
@@ -41,26 +37,11 @@
4137
@RequiredArgsConstructor
4238
public class ApplicationConfigController extends AbstractController implements ApplicationConfigApi {
4339

44-
private static final PropertiesMapper MAPPER = Mappers.getMapper(PropertiesMapper.class);
45-
46-
@Mapper
47-
interface PropertiesMapper {
48-
49-
DynamicConfigOperations.PropertiesStructure fromDto(ApplicationConfigPropertiesDTO dto);
50-
51-
ApplicationConfigPropertiesDTO toDto(DynamicConfigOperations.PropertiesStructure propertiesStructure);
52-
53-
default ActionDTO stringToActionDto(String str) {
54-
return Optional.ofNullable(str)
55-
.map(s -> Enum.valueOf(ActionDTO.class, s.toUpperCase()))
56-
.orElseThrow();
57-
}
58-
}
59-
6040
private final DynamicConfigOperations dynamicConfigOperations;
6141
private final ApplicationRestarter restarter;
6242
private final KafkaClusterFactory kafkaClusterFactory;
6343
private final ApplicationInfoService applicationInfoService;
44+
private final DynamicConfigMapper configMapper;
6445

6546
@Override
6647
public Mono<ResponseEntity<ApplicationInfoDTO>> getApplicationInfo(ServerWebExchange exchange) {
@@ -83,7 +64,7 @@ public Mono<ResponseEntity<ApplicationConfigDTO>> getCurrentConfig(ServerWebExch
8364
return validateAccess(context)
8465
.then(Mono.fromSupplier(() -> ResponseEntity.ok(
8566
new ApplicationConfigDTO()
86-
.properties(MAPPER.toDto(dynamicConfigOperations.getCurrentProperties()))
67+
.properties(configMapper.toDto(dynamicConfigOperations.getCurrentProperties()))
8768
)))
8869
.doOnEach(sig -> audit(context, sig));
8970
}
@@ -98,7 +79,7 @@ public Mono<ResponseEntity<Void>> restartWithConfig(Mono<RestartRequestDTO> rest
9879
return validateAccess(context)
9980
.then(restartRequestDto)
10081
.doOnNext(restartDto -> {
101-
var newConfig = MAPPER.fromDto(restartDto.getConfig().getProperties());
82+
var newConfig = configMapper.fromDto(restartDto.getConfig().getProperties());
10283
dynamicConfigOperations.persist(newConfig);
10384
})
10485
.doOnEach(sig -> audit(context, sig))
@@ -132,7 +113,7 @@ public Mono<ResponseEntity<ApplicationConfigValidationDTO>> validateConfig(Mono<
132113
return validateAccess(context)
133114
.then(configDto)
134115
.flatMap(config -> {
135-
DynamicConfigOperations.PropertiesStructure newConfig = MAPPER.fromDto(config.getProperties());
116+
DynamicConfigOperations.PropertiesStructure newConfig = configMapper.fromDto(config.getProperties());
136117
ClustersProperties clustersProperties = newConfig.getKafka();
137118
return validateClustersConfig(clustersProperties)
138119
.map(validations -> new ApplicationConfigValidationDTO().clusters(validations));

api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,22 @@ public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExch
4545
.map(SecurityContext::getAuthentication)
4646
.map(Principal::getName);
4747

48-
var builder = AuthenticationInfoDTO.builder()
48+
var builder = new AuthenticationInfoDTO()
4949
.rbacEnabled(accessControlService.isRbacEnabled());
5050

5151
return userName
5252
.zipWith(permissions)
5353
.map(data -> (AuthenticationInfoDTO) builder
5454
.userInfo(new UserInfoDTO(data.getT1(), data.getT2()))
55-
.build()
5655
)
57-
.switchIfEmpty(Mono.just(builder.build()))
56+
.switchIfEmpty(Mono.just(builder))
5857
.map(ResponseEntity::ok);
5958
}
6059

6160
private List<UserPermissionDTO> mapPermissions(List<Permission> permissions, List<String> clusters) {
6261
return permissions
6362
.stream()
64-
.map(permission -> (UserPermissionDTO) UserPermissionDTO.builder()
63+
.map(permission -> new UserPermissionDTO()
6564
.clusters(clusters)
6665
.resource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase()))
6766
.value(permission.getValue())
@@ -71,7 +70,6 @@ private List<UserPermissionDTO> mapPermissions(List<Permission> permissions, Lis
7170
.map(this::mapAction)
7271
.filter(Objects::nonNull)
7372
.toList())
74-
.build()
7573
)
7674
.toList();
7775
}

0 commit comments

Comments
 (0)