Skip to content

Commit 50ea637

Browse files
authored
Merge pull request #9 from cuspymd/feature/dedicated-server-support-423232572268798275
Implement dedicated server support
2 parents 4ae13cd + 16d0970 commit 50ea637

27 files changed

+683
-102
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ A Fabric mod that implements a Model Context Protocol (MCP) server, enabling AI
44

55
## Overview
66

7-
This mod creates an HTTP server within the Minecraft client that accepts MCP protocol requests, allowing Large Language Models to execute Minecraft commands safely and efficiently. The mod includes comprehensive safety validation to prevent destructive operations.
7+
This mod creates an HTTP server within the Minecraft client or dedicated server that accepts MCP protocol requests, allowing Large Language Models to execute Minecraft commands safely and efficiently. The mod includes comprehensive safety validation to prevent destructive operations.
8+
9+
It is designed to be fully compatible with both Single-Player (Integrated Server) and Multiplayer Dedicated Servers.
810

911
## Features
1012

13+
- **Server and Client Support**: Works on both single-player and dedicated server environments.
1114
- **MCP Protocol Support**: Full implementation of Model Context Protocol for AI interaction
1215
- **Safety Validation**: Comprehensive command filtering and validation system
1316
- **Asynchronous Execution**: Non-blocking command execution to maintain game performance
@@ -34,9 +37,17 @@ This mod creates an HTTP server within the Minecraft client that accepts MCP pro
3437

3538
The MCP server starts automatically when you launch Minecraft with the mod installed. By default, it runs on `localhost:8080`.
3639

40+
### Server vs Client Modes
41+
42+
The mod detects if it is running in a Client (Single Player) or a Dedicated Server environment:
43+
- **Client Mode**: Full feature support, including the `take_screenshot` tool, which uses the local game window.
44+
- **Dedicated Server Mode**: Has access to tools like `execute_commands`, `get_player_info`, and `get_blocks_in_area`, enabling full AI manipulation of the world without rendering. The `take_screenshot` tool is disabled in server mode since there is no rendering context. Note that `get_player_info` currently selects the first online player on the server to report its location.
45+
46+
If playing Single Player, the integrated server logic runs through the client-side MCP.
47+
3748
### Configuration
3849

39-
The mod creates a configuration file at `config/mcp-client.json`:
50+
The mod creates a configuration file at `config/mcp.json`:
4051

4152
```json
4253
{
@@ -267,7 +278,7 @@ Capture a screenshot of the current Minecraft game screen. Optionally, you can s
267278

268279
For debugging purposes, you can enable local saving of every screenshot captured by the MCP server.
269280

270-
1. Open `config/mcp-client.json`.
281+
1. Open `config/mcp.json`.
271282
2. Set `"save_screenshots_for_debug": true` in the `client` section.
272283
3. Screenshots will be saved to the `mcp_debug_screenshots/` directory in your Minecraft instance folder.
273284
4. Files are named using the pattern: `screenshot_YYYYMMDD_HHMMSS_SSS.png`.

build.gradle

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ loom {
2828
}
2929
}
3030

31+
runs {
32+
server {
33+
source sourceSets.client
34+
}
35+
}
36+
3137
}
3238

3339
dependencies {
@@ -95,4 +101,4 @@ publishing {
95101
// The repositories here will be used for publishing your artifact, not for
96102
// retrieving dependencies.
97103
}
98-
}
104+
}

src/client/java/cuspymd/mcp/mod/MCPServerModClient.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ public void onInitializeClient() {
2727
String transport = config.getServer().getTransport();
2828

2929
if ("http".equals(transport)) {
30-
httpServer = new HTTPMCPServer(config);
30+
httpServer = new HTTPMCPServer(config,
31+
new cuspymd.mcp.mod.command.CommandExecutor(config),
32+
new cuspymd.mcp.mod.utils.PlayerInfoProvider(),
33+
new cuspymd.mcp.mod.utils.BlockScanner(),
34+
new cuspymd.mcp.mod.utils.ScreenshotUtils(),
35+
true
36+
);
3137
httpServer.start();
3238
LOGGER.info("HTTP MCP Server started on port {}", httpServer.getPort());
3339
} else {
3440
// Default to stdio transport
35-
ipcServer = new IPCServer(config);
41+
ipcServer = new IPCServer(config, new cuspymd.mcp.mod.command.CommandExecutor(config));
3642
ipcServer.start();
3743
LOGGER.info("IPC Server started on port {}", ipcServer.getPort());
3844
}
@@ -52,4 +58,4 @@ public void onClientShutdown() {
5258
LOGGER.info("HTTP MCP Server stopped");
5359
}
5460
}
55-
}
61+
}

src/client/java/cuspymd/mcp/mod/command/CommandExecutor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.concurrent.TimeoutException;
2020
import java.util.concurrent.TimeUnit;
2121

22-
public class CommandExecutor {
22+
public class CommandExecutor implements cuspymd.mcp.mod.command.ICommandExecutor {
2323
private static final Logger LOGGER = LoggerFactory.getLogger(CommandExecutor.class);
2424
private static final long COMMAND_MESSAGE_WAIT_MS = 700L;
2525
private static final long COMMAND_MESSAGE_IDLE_MS = 120L;

src/client/java/cuspymd/mcp/mod/utils/BlockScanner.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
import java.util.ArrayList;
1111
import java.util.List;
1212

13-
public class BlockScanner {
13+
public class BlockScanner implements cuspymd.mcp.mod.utils.IBlockScanner {
1414
private static final Logger LOGGER = LoggerFactory.getLogger(BlockScanner.class);
1515

16-
public static JsonObject scanBlocksInArea(JsonObject fromPos, JsonObject toPos, int maxAreaSize) {
16+
@Override
17+
public JsonObject scanBlocksInArea(JsonObject fromPos, JsonObject toPos, int maxAreaSize) {
18+
return scanBlocksInAreaStatic(fromPos, toPos, maxAreaSize);
19+
}
20+
21+
public static JsonObject scanBlocksInAreaStatic(JsonObject fromPos, JsonObject toPos, int maxAreaSize) {
1722
try {
1823
MinecraftClient client = MinecraftClient.getInstance();
1924
if (client.world == null) {
@@ -71,7 +76,7 @@ public static JsonObject scanBlocksInArea(JsonObject fromPos, JsonObject toPos,
7176
result.add("area", areaInfo);
7277

7378
// Scan blocks
74-
List<JsonObject> blockList = new ArrayList<>();
79+
List<BlockCompressor.BlockData> blockList = new ArrayList<>();
7580
int totalBlocks = 0;
7681

7782
for (int x = minX; x <= maxX; x++) {
@@ -85,13 +90,7 @@ public static JsonObject scanBlocksInArea(JsonObject fromPos, JsonObject toPos,
8590
continue;
8691
}
8792

88-
JsonObject blockInfo = new JsonObject();
89-
blockInfo.addProperty("x", x);
90-
blockInfo.addProperty("y", y);
91-
blockInfo.addProperty("z", z);
92-
blockInfo.addProperty("type", blockState.getBlock().toString());
93-
94-
blockList.add(blockInfo);
93+
blockList.add(new BlockCompressor.BlockData(x, y, z, net.minecraft.registry.Registries.BLOCK.getId(blockState.getBlock()).toString()));
9594
totalBlocks++;
9695
}
9796
}

src/client/java/cuspymd/mcp/mod/utils/PlayerInfoProvider.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
import net.minecraft.util.math.Vec3d;
77
import net.minecraft.world.World;
88

9-
public class PlayerInfoProvider {
9+
public class PlayerInfoProvider implements cuspymd.mcp.mod.utils.IPlayerInfoProvider {
1010

11-
public static JsonObject getPlayerInfo() {
11+
@Override
12+
public JsonObject getPlayerInfo() {
13+
return getPlayerInfoStatic();
14+
}
15+
16+
public static JsonObject getPlayerInfoStatic() {
1217
MinecraftClient client = MinecraftClient.getInstance();
1318
ClientPlayerEntity player = client.player;
1419
World world = client.world;

src/client/java/cuspymd/mcp/mod/utils/ScreenshotUtils.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@
2121
import java.util.List;
2222
import java.util.concurrent.CompletableFuture;
2323

24-
public class ScreenshotUtils {
24+
public class ScreenshotUtils implements cuspymd.mcp.mod.utils.IScreenshotUtils {
2525
private static final Logger LOGGER = LoggerFactory.getLogger(ScreenshotUtils.class);
2626
static final List<DeferredTask> pendingDeferredTasks = Collections.synchronizedList(new ArrayList<>());
2727

28+
@Override
29+
public CompletableFuture<String> takeScreenshot(JsonObject params) {
30+
return takeScreenshotStatic(params);
31+
}
32+
2833
static class DeferredTask {
2934
Runnable runnable;
3035
int remainingTicks;
@@ -43,7 +48,7 @@ static class DeferredTask {
4348
* @param params JsonObject containing optional x, y, z, yaw, pitch
4449
* @return A CompletableFuture that completes with the Base64 encoded PNG image data
4550
*/
46-
public static CompletableFuture<String> takeScreenshot(JsonObject params) {
51+
public static CompletableFuture<String> takeScreenshotStatic(JsonObject params) {
4752
MinecraftClient client = MinecraftClient.getInstance();
4853
CompletableFuture<String> future = new CompletableFuture<>();
4954

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package cuspymd.mcp.mod;
2+
3+
import net.fabricmc.api.DedicatedServerModInitializer;
4+
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
5+
import cuspymd.mcp.mod.bridge.HTTPMCPServer;
6+
import cuspymd.mcp.mod.bridge.IPCServer;
7+
import cuspymd.mcp.mod.config.MCPConfig;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
public class MCPServerModServer implements DedicatedServerModInitializer {
12+
public static final Logger LOGGER = LoggerFactory.getLogger("mcp-server-mod");
13+
private HTTPMCPServer httpServer;
14+
private IPCServer ipcServer;
15+
16+
@Override
17+
public void onInitializeServer() {
18+
LOGGER.info("Initializing Minecraft MCP Dedicated Server");
19+
20+
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
21+
try {
22+
MCPConfig config = MCPConfig.load();
23+
if (config.getServer().isAutoStart()) {
24+
String transport = config.getServer().getTransport();
25+
26+
if ("http".equals(transport)) {
27+
httpServer = new HTTPMCPServer(config,
28+
new cuspymd.mcp.mod.server.tools.ServerCommandExecutor(config, server),
29+
new cuspymd.mcp.mod.server.tools.ServerPlayerInfoProvider(server),
30+
new cuspymd.mcp.mod.server.tools.ServerBlockScanner(server),
31+
new cuspymd.mcp.mod.server.tools.ServerScreenshotUtils(),
32+
false
33+
);
34+
httpServer.start();
35+
LOGGER.info("HTTP MCP Server started on port {}", httpServer.getPort());
36+
} else {
37+
// Default to stdio transport
38+
ipcServer = new IPCServer(config, new cuspymd.mcp.mod.server.tools.ServerCommandExecutor(config, server));
39+
ipcServer.start();
40+
LOGGER.info("IPC Server started on port {}", ipcServer.getPort());
41+
}
42+
}
43+
} catch (Exception e) {
44+
LOGGER.error("Failed to start MCP Server", e);
45+
}
46+
});
47+
48+
ServerLifecycleEvents.SERVER_STOPPING.register(server -> {
49+
if (ipcServer != null) {
50+
ipcServer.stop();
51+
LOGGER.info("IPC Server stopped");
52+
}
53+
if (httpServer != null) {
54+
httpServer.stop();
55+
LOGGER.info("HTTP MCP Server stopped");
56+
}
57+
});
58+
}
59+
}

src/client/java/cuspymd/mcp/mod/bridge/HTTPMCPServer.java renamed to src/main/java/cuspymd/mcp/mod/bridge/HTTPMCPServer.java

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import com.sun.net.httpserver.HttpExchange;
77
import com.sun.net.httpserver.HttpHandler;
88
import com.sun.net.httpserver.HttpServer;
9-
import cuspymd.mcp.mod.command.CommandExecutor;
9+
import cuspymd.mcp.mod.command.ICommandExecutor;
1010
import cuspymd.mcp.mod.config.MCPConfig;
1111
import cuspymd.mcp.mod.server.MCPProtocol;
12-
import cuspymd.mcp.mod.utils.PlayerInfoProvider;
13-
import cuspymd.mcp.mod.utils.BlockScanner;
14-
import cuspymd.mcp.mod.utils.ScreenshotUtils;
12+
import cuspymd.mcp.mod.utils.IPlayerInfoProvider;
13+
import cuspymd.mcp.mod.utils.IBlockScanner;
14+
import cuspymd.mcp.mod.utils.IScreenshotUtils;
1515
import org.slf4j.Logger;
1616
import org.slf4j.LoggerFactory;
1717

@@ -31,14 +31,33 @@ public class HTTPMCPServer {
3131
private static final Gson GSON = new Gson();
3232

3333
private final MCPConfig config;
34-
private final CommandExecutor commandExecutor;
34+
private final ICommandExecutor commandExecutor;
35+
private final IPlayerInfoProvider playerInfoProvider;
36+
private final IBlockScanner blockScanner;
37+
private final IScreenshotUtils screenshotUtils;
38+
private final boolean screenshotToolEnabled;
3539
private final AtomicBoolean running = new AtomicBoolean(false);
3640
private HttpServer httpServer;
3741
private ExecutorService executor;
3842

39-
public HTTPMCPServer(MCPConfig config) {
43+
public HTTPMCPServer(MCPConfig config, ICommandExecutor commandExecutor, IPlayerInfoProvider playerInfoProvider, IBlockScanner blockScanner, IScreenshotUtils screenshotUtils) {
44+
this(config, commandExecutor, playerInfoProvider, blockScanner, screenshotUtils, true);
45+
}
46+
47+
public HTTPMCPServer(
48+
MCPConfig config,
49+
ICommandExecutor commandExecutor,
50+
IPlayerInfoProvider playerInfoProvider,
51+
IBlockScanner blockScanner,
52+
IScreenshotUtils screenshotUtils,
53+
boolean screenshotToolEnabled
54+
) {
4055
this.config = config;
41-
this.commandExecutor = new CommandExecutor(config);
56+
this.commandExecutor = commandExecutor;
57+
this.playerInfoProvider = playerInfoProvider;
58+
this.blockScanner = blockScanner;
59+
this.screenshotUtils = screenshotUtils;
60+
this.screenshotToolEnabled = screenshotToolEnabled;
4261
}
4362

4463
public void start() throws IOException {
@@ -275,7 +294,7 @@ private JsonObject handlePing() {
275294

276295
private JsonObject handleToolsList() {
277296
JsonObject response = new JsonObject();
278-
response.add("tools", MCPProtocol.getToolsListResponse(config));
297+
response.add("tools", MCPProtocol.getToolsListResponse(config, screenshotToolEnabled));
279298
return response;
280299
}
281300

@@ -295,6 +314,9 @@ private JsonObject handleToolsCall(JsonObject params) {
295314
return handleGetBlocksInArea(arguments);
296315
}
297316
case "take_screenshot" -> {
317+
if (!screenshotToolEnabled) {
318+
return MCPProtocol.createErrorResponse("Tool not available in dedicated server mode", null);
319+
}
298320
return handleTakeScreenshot(arguments);
299321
}
300322
case null, default -> {
@@ -315,7 +337,7 @@ private JsonObject handleToolsCall(JsonObject params) {
315337

316338
private JsonObject handleGetPlayerInfo() {
317339
try {
318-
JsonObject playerInfo = PlayerInfoProvider.getPlayerInfo();
340+
JsonObject playerInfo = playerInfoProvider.getPlayerInfo();
319341

320342
// Check if there was an error getting player info
321343
if (playerInfo.has("error")) {
@@ -347,7 +369,7 @@ private JsonObject handleGetBlocksInArea(JsonObject arguments) {
347369
}
348370

349371
int maxAreaSize = config.getServer().getMaxAreaSize();
350-
JsonObject result = BlockScanner.scanBlocksInArea(fromPos, toPos, maxAreaSize);
372+
JsonObject result = blockScanner.scanBlocksInArea(fromPos, toPos, maxAreaSize);
351373

352374
// Check if there was an error scanning blocks
353375
if (result.has("error")) {
@@ -405,7 +427,7 @@ JsonObject awaitScreenshotResult(CompletableFuture<String> future) {
405427
}
406428

407429
CompletableFuture<String> takeScreenshotAsync(JsonObject params) {
408-
return ScreenshotUtils.takeScreenshot(params);
430+
return screenshotUtils.takeScreenshot(params);
409431
}
410432

411433
private JsonObject createSuccessResponse(JsonObject result, Integer requestId) {

src/client/java/cuspymd/mcp/mod/bridge/IPCServer.java renamed to src/main/java/cuspymd/mcp/mod/bridge/IPCServer.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import com.google.gson.Gson;
44
import com.google.gson.JsonObject;
55
import com.google.gson.JsonParser;
6-
import cuspymd.mcp.mod.command.CommandExecutor;
6+
import cuspymd.mcp.mod.command.ICommandExecutor;
77
import cuspymd.mcp.mod.config.MCPConfig;
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
@@ -20,13 +20,13 @@ public class IPCServer {
2020
private static final Gson GSON = new Gson();
2121
private static final int IPC_PORT = 25565; // Default port for IPC
2222

23-
private final CommandExecutor commandExecutor;
23+
private final ICommandExecutor commandExecutor;
2424
private final AtomicBoolean running = new AtomicBoolean(false);
2525
private ServerSocket serverSocket;
2626
private ExecutorService executor;
2727

28-
public IPCServer(MCPConfig config) {
29-
this.commandExecutor = new CommandExecutor(config);
28+
public IPCServer(MCPConfig config, ICommandExecutor commandExecutor) {
29+
this.commandExecutor = commandExecutor;
3030
}
3131

3232
public void start() throws IOException {

0 commit comments

Comments
 (0)