Skip to content

Commit 4b766f9

Browse files
authored
Clean design and remove chat api (#11)
Signed-off-by: David Kornel <kornys@outlook.com>
1 parent 49e2387 commit 4b766f9

59 files changed

Lines changed: 3207 additions & 5607 deletions

File tree

Some content is hidden

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

CLAUDE.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Project: Strimzi MCP Server
2+
3+
Quarkus application providing Strimzi Kafka management tools via MCP (Model Context Protocol).
4+
Java 21, Quarkus 3.x, Strimzi API 0.50.x, Fabric8 Kubernetes client.
5+
6+
## Build & Test
7+
8+
```bash
9+
mvn compile # compile + checkstyle
10+
mvn test # unit tests (no live cluster needed)
11+
mvn quarkus:dev # dev mode on http://localhost:8080/mcp
12+
```
13+
14+
Checkstyle runs during compile phase. Fix all violations before committing.
15+
16+
## Architecture
17+
18+
```
19+
tool/ → MCP tool definitions (thin wrappers, no logic)
20+
service/domain/ → Business logic (Kafka, Topic, NodePool, Operator)
21+
service/common/ → Generic Kubernetes operations (resource queries, pods, deployments)
22+
dto/ → Response records (JSON output)
23+
config/ → Constants, shared tool descriptions
24+
util/ → Input normalization utilities
25+
```
26+
27+
### Layer rules
28+
29+
- **Tools** call domain services and return typed responses. No try/catch, no business logic.
30+
- **Domain services** contain all business logic. Throw `ToolCallException` for errors.
31+
- **Common services** are generic Kubernetes helpers shared across domain services.
32+
- **DTOs** are immutable `record` types. Use static factory methods (`of()`, `empty()`) not constructors directly.
33+
34+
## MCP Tool Pattern
35+
36+
```java
37+
@Singleton
38+
@WrapBusinessError(Exception.class)
39+
public class XxxTools {
40+
41+
@Inject
42+
XxxService xxxService;
43+
44+
XxxTools() { // package-private no-arg constructor for CDI
45+
}
46+
47+
@Tool(
48+
name = "verb_noun",
49+
description = "Short description of what the tool does."
50+
+ " Additional sentence if needed."
51+
)
52+
public TypedResponse toolMethod(
53+
@ToolArg(description = "...") final String requiredParam,
54+
@ToolArg(description = "...", required = false) final String optionalParam
55+
) {
56+
return xxxService.operation(optionalParam, requiredParam);
57+
}
58+
}
59+
```
60+
61+
### Critical: No @Tool.Annotations
62+
63+
Do NOT use `annotations = @Tool.Annotations(...)` on any `@Tool`. This causes Claude Code
64+
to silently drop all MCP tools during discovery. Not even empty `@Tool.Annotations()` works.
65+
66+
### Tool naming
67+
68+
- Use `snake_case`: `list_kafka_clusters`, `get_kafka_topic`, `get_kafka_bootstrap_servers`
69+
- Pattern: `verb_resource` (list, get)
70+
- Namespace is always optional via `@ToolArg(required = false)`
71+
- Use `StrimziToolsPrompts` constants for shared parameter descriptions (NS_DESC, CLUSTER_DESC, etc.)
72+
73+
### Tool descriptions
74+
75+
- 1-2 sentences max
76+
- No filler phrases ("Perfect for...", "Essential for...", "Use this to...")
77+
- State what it returns, not what it's useful for
78+
- Use string concatenation for multi-line: `"First part." + " Second part."`
79+
80+
## Domain Service Pattern
81+
82+
```java
83+
@ApplicationScoped
84+
public class XxxService {
85+
86+
@Inject
87+
KubernetesResourceService k8sService;
88+
89+
XxxService() { // package-private no-arg constructor for CDI
90+
}
91+
92+
public List<XxxResponse> listItems(final String namespace, final String clusterName) {
93+
String ns = InputUtils.normalizeInput(namespace);
94+
String name = InputUtils.normalizeInput(clusterName);
95+
96+
if (name == null) {
97+
throw new ToolCallException("Cluster name is required");
98+
}
99+
100+
List<Resource> resources;
101+
if (ns != null) {
102+
resources = k8sService.queryResourcesByLabel(Resource.class, ns, LABEL_KEY, name);
103+
} else {
104+
resources = k8sService.queryResourcesByLabelInAnyNamespace(Resource.class, LABEL_KEY, name);
105+
}
106+
107+
return resources.stream()
108+
.map(this::createResponse)
109+
.toList();
110+
}
111+
}
112+
```
113+
114+
### Namespace handling
115+
116+
Every tool accepts an optional namespace. The service method receives it first, normalizes it,
117+
then branches: if non-null query that namespace, if null query all namespaces (auto-discovery).
118+
Use `InputUtils.normalizeInput()` for all user-supplied strings (namespace, cluster name, etc.).
119+
120+
### Error handling
121+
122+
- Validation errors (missing required params): `throw new ToolCallException("message")`
123+
- Resource not found: `throw new ToolCallException("X 'name' not found")`
124+
- Empty list results: return empty list (not an error)
125+
- Infrastructure/API errors: let propagate, `@WrapBusinessError` converts to MCP error
126+
- Never return error objects; always throw or return typed responses
127+
128+
## DTO Pattern
129+
130+
```java
131+
@JsonInclude(JsonInclude.Include.NON_NULL)
132+
public record XxxResponse(
133+
@JsonProperty("name") String name,
134+
@JsonProperty("namespace") String namespace,
135+
@JsonProperty("status") String status
136+
) {
137+
public static XxxResponse of(String name, String namespace, String status) {
138+
return new XxxResponse(name, namespace, status);
139+
}
140+
}
141+
```
142+
143+
- Use `@JsonProperty` with snake_case names on every field
144+
- Use `@JsonInclude(JsonInclude.Include.NON_NULL)` to omit null fields
145+
- Provide static factory methods (`of()`, `empty()`) for construction
146+
- Record javadoc uses `@param` tags for each component
147+
148+
## Constants
149+
150+
`Constants.java` is organized into `Kubernetes` and `Strimzi` inner classes.
151+
152+
### Use Strimzi API constants where available
153+
154+
- Label keys: use `ResourceLabels.STRIMZI_CLUSTER_LABEL`, `ResourceLabels.STRIMZI_KIND_LABEL`,
155+
`ResourceLabels.STRIMZI_COMPONENT_TYPE_LABEL` from `io.strimzi.api.ResourceLabels`
156+
- Node pool roles: use `ProcessRoles.BROKER.toValue()` from `io.strimzi.api.kafka.model.nodepool.ProcessRoles`
157+
- Listener types: use `KafkaListenerType.INTERNAL.toValue()` etc. from
158+
`io.strimzi.api.kafka.model.kafka.listener.KafkaListenerType`
159+
160+
### Custom constants (no API equivalent)
161+
162+
- `Constants.Strimzi.Labels.POOL_NAME` - `"strimzi.io/pool-name"` (not in ResourceLabels)
163+
- `Constants.Strimzi.KindValues.CLUSTER_OPERATOR` - `"cluster-operator"` (label value, no API constant)
164+
- `Constants.Strimzi.ComponentTypes.KAFKA` - `"kafka"` (label value, no API constant)
165+
- `Constants.Strimzi.Operator.APP_LABEL_VALUE` - `"strimzi"` (label value, no API constant)
166+
- `Constants.Kubernetes.*` - standard Kubernetes strings (labels, conditions, phases, container states,
167+
resource status, health status) not provided by Fabric8 as constants
168+
169+
Before adding a new constant, check if it already exists in `ResourceLabels`, `ProcessRoles`,
170+
`KafkaListenerType`, or other Strimzi API classes.
171+
172+
## Code Style
173+
174+
### Enforced by checkstyle (runs at compile)
175+
176+
- License header on every Java file (see `.checkstyle/checkstyle.xml`)
177+
- No star imports
178+
- No tabs
179+
- Import order: third-party, javax, java, static (alphabetical within groups, blank line between)
180+
- All public types, methods, and fields require Javadoc
181+
- Locale-sensitive methods must specify locale: `.toLowerCase(Locale.ROOT)` not `.toLowerCase()`
182+
- Max method length: 150 lines, max parameters: 13, max cyclomatic complexity: 19
183+
184+
### Conventions
185+
186+
- Use `final` on method parameters
187+
- Use `if/else` over multi-line ternary operators. Single-line ternaries for simple expressions are fine.
188+
- Package-private no-arg constructors on CDI beans (not private, not public)
189+
- Use `InputUtils.normalizeInput()` for all user-supplied input
190+
- Prefer `List.of()` for empty immutable lists
191+
- Use `LOG.infof()` / `LOG.debugf()` (JBoss logging with format strings)
192+
- No `assert` statements (checkstyle enforced)
193+
194+
### Javadoc
195+
196+
- Required on all public classes, methods, fields, and record components
197+
- Class-level: brief description of purpose
198+
- Method-level: describe what it does, `@param` for each parameter, `@return` for return value
199+
- Record-level: describe the record, `@param` for each component
200+
- Keep descriptions concise, start with a verb or noun (not "This method...")
201+
- Use `{@code ...}` for inline code references
202+
- Non-empty `@param`/`@return`/`@throws` descriptions (checkstyle enforced)
203+
204+
## Adding a New Tool
205+
206+
1. Create or update the DTO record in `dto/`
207+
2. Add the service method to the appropriate domain service in `service/domain/`
208+
3. Add the `@Tool` method to the corresponding tools class in `tool/`
209+
4. Run `mvn compile` to verify checkstyle + compilation
210+
5. Run `mvn test` to verify tests pass
211+
212+
## Testing
213+
214+
Tests use Quarkus test framework with Mockito for Kubernetes client mocking.
215+
Test file: `src/test/java/io/streamshub/mcp/service/StrimziServiceTest.java`.
216+
Tests verify service behavior without a live Kubernetes cluster.

0 commit comments

Comments
 (0)