Skip to content

Commit d86408d

Browse files
✨ feat(mcp): BuildDwelling write slice | add MCP server tool
1 parent 737fa9a commit d86408d

6 files changed

Lines changed: 186 additions & 23 deletions

File tree

.claude/tasks/0001_introduce_mcp_server.md

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ The MCP server will expose the rich domain model through standardized MCP resour
4747

4848
### 🚀 Phase 1: Proof of Concept (Start Here)
4949
**Target: `creaturerecruitment/write/builddwelling` slice**
50-
- [ ] **creaturerecruitment/write/builddwelling**: Build dwelling tools/resources
51-
- [ ] Test and validate the first slice implementation
52-
- [ ] Establish patterns and conventions
50+
- [x] **creaturerecruitment/write/builddwelling**: Build dwelling tools/resources
51+
- [x] Test and validate the first slice implementation
52+
- [x] Establish patterns and conventions
5353

5454
### 📈 Phase 2: Expand Creature Recruitment Context
5555
- [ ] **creaturerecruitment/write/recruitcreature**: Recruit creature tools/resources
@@ -181,4 +181,90 @@ Each phase completion should result in:
181181
- **Documentation** of patterns and conventions
182182
- **Performance validation** under realistic load
183183

184-
This incremental approach ensures the MCP server integrates seamlessly with the existing domain architecture while providing rich, domain-aware capabilities for external consumers. Starting with a single slice allows us to establish solid foundations and patterns that can be confidently replicated across the entire application.
184+
This incremental approach ensures the MCP server integrates seamlessly with the existing domain architecture while providing rich, domain-aware capabilities for external consumers. Starting with a single slice allows us to establish solid foundations and patterns that can be confidently replicated across the entire application.
185+
186+
## 📚 Implementation Patterns & Documentation
187+
188+
### MCP Tool Implementation Pattern
189+
190+
Based on the successful implementation of `build_dwelling` tool, here's the established pattern for creating MCP tools:
191+
192+
#### 1. **File Location**
193+
- Each slice gets its own `ModelContextProtocol.java` file
194+
- Location: `{context}/write/{slice}/ModelContextProtocol.java`
195+
- Example: `creaturerecruitment/write/builddwelling/ModelContextProtocol.java`
196+
197+
#### 2. **Class Structure**
198+
```java
199+
@Component
200+
public class ModelContextProtocol {
201+
202+
private final CommandGateway commandGateway;
203+
204+
public ModelContextProtocol(CommandGateway commandGateway) {
205+
this.commandGateway = commandGateway;
206+
}
207+
208+
@Tool(
209+
name = "tool_name",
210+
description = "Detailed description of what the tool does"
211+
)
212+
public CompletableFuture<Map<String, Object>> methodName(
213+
@ToolParam(description = "Parameter description") String param1,
214+
// ... more parameters
215+
) {
216+
// Implementation
217+
}
218+
}
219+
```
220+
221+
#### 3. **Key Design Decisions**
222+
- **Use Spring AI `@Tool`** annotation (not `@McpTool`)
223+
- **Return `CompletableFuture<Map<String, Object>>`** for async operation results
224+
- **Include `@ToolParam` descriptions** for all parameters to help AI understand usage
225+
- **Reuse existing Command/CommandGateway** infrastructure
226+
- **Follow GameMetaData pattern** for context (gameId, playerId)
227+
- **Provide structured success/error responses** with consistent format
228+
229+
#### 4. **Parameter Validation Pattern**
230+
```java
231+
@ToolParam(description = "Unique identifier for the game instance") String gameId,
232+
@ToolParam(description = "Unique identifier for the player") String playerId,
233+
@ToolParam(description = "Unique identifier for the dwelling to build") String dwellingId,
234+
@ToolParam(description = "Type of creature this dwelling will recruit (e.g., 'ANGEL', 'DRAGON', 'GRIFFIN')") String creatureId,
235+
@ToolParam(description = "Resource cost per troop recruitment. Map of resource types to amounts (e.g., {'GOLD': 1000, 'WOOD': 10})") Map<String, Integer> costPerTroop
236+
```
237+
238+
#### 5. **Response Pattern**
239+
```java
240+
// Success Response
241+
return commandGateway.send(command, GameMetaData.with(gameId, playerId))
242+
.thenApply(result -> Map.of(
243+
"success", true,
244+
"dwellingId", dwellingId,
245+
"creatureId", creatureId,
246+
"costPerTroop", costPerTroop,
247+
"message", "Dwelling built successfully"
248+
))
249+
.exceptionally(throwable -> Map.of(
250+
"success", false,
251+
"error", throwable.getMessage(),
252+
"dwellingId", dwellingId,
253+
"message", "Failed to build dwelling: " + throwable.getMessage()
254+
));
255+
```
256+
257+
### Next Implementation Steps
258+
259+
With the proven pattern established, implement remaining tools following the same structure:
260+
261+
1. **creaturerecruitment/write/recruitcreature/ModelContextProtocol.java**
262+
2. **creaturerecruitment/write/changeavailablecreatures/ModelContextProtocol.java**
263+
3. **creaturerecruitment/read/getdwellingbyid/ModelContextProtocol.java** (Resources)
264+
4. **creaturerecruitment/read/getalldwellings/ModelContextProtocol.java** (Resources)
265+
266+
Each implementation should:
267+
- Follow the established class structure
268+
- Use appropriate parameter validation
269+
- Maintain consistent response formats
270+
- Leverage existing domain infrastructure
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.dddheroes.heroesofddd;
2+
3+
import com.dddheroes.heroesofddd.shared.mcp.configuration.HealthCheckMcpTool;
4+
import org.springframework.ai.tool.ToolCallbackProvider;
5+
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
public class ModelContextProtocolConfiguration {
11+
12+
@Bean
13+
ToolCallbackProvider tools(HealthCheckMcpTool healthCheckMcpTool) {
14+
return MethodToolCallbackProvider.builder().toolObjects(healthCheckMcpTool).build();
15+
}
16+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.dddheroes.heroesofddd.creaturerecruitment.write.builddwelling;
2+
3+
import com.dddheroes.heroesofddd.shared.application.GameMetaData;
4+
import org.axonframework.commandhandling.gateway.CommandGateway;
5+
import org.springframework.ai.chat.model.ToolContext;
6+
import org.springframework.ai.tool.ToolCallbackProvider;
7+
import org.springframework.ai.tool.annotation.Tool;
8+
import org.springframework.ai.tool.annotation.ToolParam;
9+
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.stereotype.Component;
13+
14+
import java.util.Map;
15+
import java.util.concurrent.CompletableFuture;
16+
17+
public class ModelContextProtocol {
18+
19+
@Component
20+
static class Tools {
21+
22+
private final CommandGateway commandGateway;
23+
24+
public Tools(CommandGateway commandGateway) {
25+
this.commandGateway = commandGateway;
26+
}
27+
28+
@Tool(
29+
name = "build_dwelling",
30+
description = "Build a creature dwelling for recruiting specific creatures. Establishes a new dwelling with associated creature type and recruitment cost. The playerId should be provided via MCP client context (similar to REST API headers)."
31+
)
32+
public CompletableFuture<Map<String, Object>> buildDwelling(
33+
@ToolParam(description = "Unique identifier for the game instance") String gameId,
34+
@ToolParam(description = "Unique identifier for the dwelling to build") String dwellingId,
35+
@ToolParam(description = "Type of creature this dwelling will recruit (e.g., 'ANGEL', 'DRAGON', 'GRIFFIN')") String creatureId,
36+
@ToolParam(description = "Resource cost per troop recruitment. Map of resource types to amounts (e.g., {'GOLD': 1000, 'WOOD': 10})") Map<String, Integer> costPerTroop,
37+
ToolContext toolContext
38+
) {
39+
var playerId = playerId(toolContext);
40+
41+
var command = BuildDwelling.command(dwellingId, creatureId, costPerTroop);
42+
43+
return commandGateway.send(command, GameMetaData.with(gameId, playerId))
44+
.thenApply(_ -> Map.of(
45+
"success", true,
46+
"dwellingId", dwellingId,
47+
"creatureId", creatureId,
48+
"costPerTroop", costPerTroop,
49+
"playerId", playerId,
50+
"message", "Dwelling built successfully"
51+
))
52+
.exceptionally(throwable -> Map.of(
53+
"success", false,
54+
"error", throwable != null ? throwable.getMessage() : "Unknown error",
55+
"dwellingId", dwellingId,
56+
"message", "Failed to build dwelling: " + (throwable != null ? throwable.getMessage() : "Unknown error")
57+
));
58+
}
59+
60+
private String playerId(ToolContext toolContext) {
61+
return (String) toolContext.getContext().getOrDefault("playerId", "default-player-id");
62+
}
63+
64+
}
65+
66+
@Configuration
67+
static class Config {
68+
69+
@Bean
70+
ToolCallbackProvider buildDwellingTool(ModelContextProtocol.Tools sliceTools) {
71+
return MethodToolCallbackProvider.builder().toolObjects(sliceTools).build();
72+
}
73+
74+
}
75+
}

src/main/java/com/dddheroes/heroesofddd/shared/mcp/configuration/McpHealthCheck.java renamed to src/main/java/com/dddheroes/heroesofddd/shared/mcp/configuration/HealthCheckMcpTool.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package com.dddheroes.heroesofddd.shared.mcp.configuration;
22

3-
import org.springframework.ai.mcp.server.annotation.McpTool;
3+
import org.springframework.ai.tool.annotation.Tool;
44
import org.springframework.stereotype.Component;
55

66
import java.time.LocalDateTime;
77
import java.util.Map;
88

99
@Component
10-
public class McpHealthCheck {
10+
public class HealthCheckMcpTool {
1111

12-
@McpTool(name = "health_check", description = "Verifies that the MCP server is running and operational")
12+
@Tool(name = "health_check", description = "Verifies that the MCP server is running and operational")
1313
public Map<String, Object> healthCheck() {
1414
return Map.of(
1515
"status", "healthy",

src/main/java/com/dddheroes/heroesofddd/shared/mcp/configuration/McpServerConfiguration.java

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/main/resources/application.yaml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
server:
2+
port: 3773
13
spring:
24
application:
35
name: heroesofddd
@@ -10,14 +12,7 @@ spring:
1012
enabled: true
1113
name: "Heroes of Might & Magic III - DDD/Event Sourcing Server"
1214
version: "1.0.0"
13-
type: SYNC
1415
instructions: "MCP server for Heroes of Might & Magic III domain built with DDD, Event Sourcing, and Axon Framework. Provides domain operations, game management tools, and educational DDD resources."
15-
sse-message-endpoint: /mcp/messages
16-
capabilities:
17-
tool: true
18-
resource: true
19-
prompt: true
20-
completion: true
2116
datasource:
2217
url: jdbc:postgresql://localhost:6446/heroes_of_ddd_development
2318
username: heroes_of_ddd_db_user

0 commit comments

Comments
 (0)