@lzimul/minecraft-mcp 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -17
- package/build/app/server.js +6 -1
- package/build/app/tool-events.js +72 -0
- package/build/app/websocket.js +131 -0
- package/build/config/server-info.js +1 -1
- package/build/config/websocket.js +48 -0
- package/build/index.js +17 -1
- package/build/interfaces/websocket.interface.js +1 -0
- package/build/resources/dimension-resources.js +1 -1
- package/lib/app/server.d.ts +5 -1
- package/lib/app/server.d.ts.map +1 -1
- package/lib/app/tool-events.d.ts +5 -0
- package/lib/app/tool-events.d.ts.map +1 -0
- package/lib/app/websocket.d.ts +10 -0
- package/lib/app/websocket.d.ts.map +1 -0
- package/lib/config/websocket.d.ts +8 -0
- package/lib/config/websocket.d.ts.map +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/interfaces/websocket.interface.d.ts +47 -0
- package/lib/interfaces/websocket.interface.d.ts.map +1 -0
- package/lib/resources/dimension-resources.d.ts +3 -0
- package/lib/resources/dimension-resources.d.ts.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Minecraft Model Context Protocol (MCP) server. It exposes the complete Minecraft
|
|
|
10
10
|
- Dimension tools for metadata, paginated block access, entity listings and raw JSON snapshots
|
|
11
11
|
- Entity and player tools with deletion, movement, exact position control, and a 9-slot hotbar plus 27-slot backpack per entity, managed through `getInventory`, `setInventory`, `clearInventory` and `swapInventorySlots`
|
|
12
12
|
- World state (blocks, entities, players and inventories) is persisted per dimension as JSON and survives restarts
|
|
13
|
+
- Parallel WebSocket service that broadcasts every tool call (name, arguments, result, error and timing) to all connected clients, with a full world snapshot sent on connect and on demand (`{"type":"world-request"}`)
|
|
13
14
|
- log4js logging to stderr with chalk colours (error red, warn yellow, info green); stdout stays clean for the MCP transport
|
|
14
15
|
- Structured tool output (`outputSchema` / `structuredContent`) plus human readable text
|
|
15
16
|
- Every failure path returns a readable message instead of an unhandled exception
|
|
@@ -125,11 +126,15 @@ Each snapshot looks like this:
|
|
|
125
126
|
|
|
126
127
|
### Configuration
|
|
127
128
|
|
|
128
|
-
| Variable | Default
|
|
129
|
-
| ------------------- |
|
|
130
|
-
| `MCP_WORLD_STORAGE` | `file`
|
|
131
|
-
| `MCP_WORLD_DIR` | `./app/dimensions` | Directory that holds the per-dimension snapshots
|
|
132
|
-
| `MCP_LOG_LEVEL` | `info`
|
|
129
|
+
| Variable | Default | Description |
|
|
130
|
+
| ------------------- | ------------------ | -------------------------------------------------------- |
|
|
131
|
+
| `MCP_WORLD_STORAGE` | `file` | `file` for JSON snapshots, `memory` for volatile state |
|
|
132
|
+
| `MCP_WORLD_DIR` | `./app/dimensions` | Directory that holds the per-dimension snapshots |
|
|
133
|
+
| `MCP_LOG_LEVEL` | `info` | log4js level: `trace`, `debug`, `info`, `warn`, `error` |
|
|
134
|
+
| `MCP_WS_ENABLED` | `false` | Start the WebSocket fan-out service (`false` to disable) |
|
|
135
|
+
| `MCP_WS_HOST` | `127.0.0.1` | Host/interface the WebSocket service binds to |
|
|
136
|
+
| `MCP_WS_PORT` | `8765` | TCP port for the WebSocket service |
|
|
137
|
+
| `MCP_WS_PATH` | `/` | URL path the WebSocket service listens on |
|
|
133
138
|
|
|
134
139
|
A corrupt or oversized snapshot is moved aside as `<dimension>.json.corrupt-<uuid>` and the dimension starts empty, so a bad file never blocks startup.
|
|
135
140
|
|
|
@@ -148,6 +153,58 @@ State is accessed through `IWorldStore` (`src/interfaces/world-store.interface.t
|
|
|
148
153
|
| `FileWorldStore` | stdio (`npm start`) | Atomic per-dimension JSON files under `MCP_WORLD_DIR` |
|
|
149
154
|
| `MemoryWorldStore` | tests, `memory` | Volatile; useful for throwaway sessions |
|
|
150
155
|
|
|
156
|
+
### WebSocket service
|
|
157
|
+
|
|
158
|
+
The server can run a WebSocket fan-out service alongside the stdio MCP transport, but it should be treated as a privileged telemetry channel. Keep it **disabled by default** unless you explicitly need it, and enable it only with access controls (for example: an origin allow-list and/or a shared token presented during the upgrade request). Bind/listen settings can still be configured with the `MCP_WS_*` variables above. The MCP transport is unaffected: stdout stays reserved for JSON-RPC, and a WebSocket listen failure is logged but never crashes the process.
|
|
159
|
+
|
|
160
|
+
When enabled, authenticated/authorized clients receive a **world snapshot** right after the handshake (pushed on a short delay so clients that attach their message listener after `open` still catch it), then a **tool-call event** for every MCP tool invocation:
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
// On connect (and on {"type":"world-request"}):
|
|
164
|
+
{ "type": "world-snapshot", "timestamp": 1730000000000, "dimensions": [ /* full per-dimension state: counts, placedBlocks, entities */ ] }
|
|
165
|
+
|
|
166
|
+
// Broadcast to every client after each tool call:
|
|
167
|
+
{
|
|
168
|
+
"type": "tool-call",
|
|
169
|
+
"tool": "getBlock",
|
|
170
|
+
"arguments": { "dimension": "overworld", "x": 0, "y": 320, "z": 0 },
|
|
171
|
+
"result": { "isError": true, "content": [ /* ... */ ] },
|
|
172
|
+
"isError": true,
|
|
173
|
+
"error": "Coordinates (0, 320, 0) are out of range in Overworld (Y: -64 to 319).",
|
|
174
|
+
"startedAt": 1730000000000,
|
|
175
|
+
"durationMs": 1
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A client may send `{"type":"world-request"}` (or `{"type":"world"}`) to request a fresh snapshot. The service observes tool calls by wrapping `McpServer.registerTool`, so tools added in the future are broadcast automatically.
|
|
180
|
+
|
|
181
|
+
#### WebSocket TypeScript types
|
|
182
|
+
|
|
183
|
+
Package consumers can import the protocol types (published via `lib/index.d.ts`) instead of hand-writing them:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import type {
|
|
187
|
+
IWorldSnapshotMessage, // server -> client: full world state on connect / world-request
|
|
188
|
+
IDimensionSnapshot, // one dimension: counts, placedBlocks, entities
|
|
189
|
+
ISnapshotPlacedBlock, // { x, y, z, block: BlockInfo, states: BlockStates }
|
|
190
|
+
IToolCallEvent, // server -> client: one tool invocation
|
|
191
|
+
IWebSocketServerMessage, // union of IWorldSnapshotMessage | IToolCallEvent
|
|
192
|
+
IWebSocketClientMessage, // client -> server: { type: 'world-request' | 'world' }
|
|
193
|
+
ToolCallListener,
|
|
194
|
+
} from "@lzimul/minecraft-mcp";
|
|
195
|
+
|
|
196
|
+
// Discriminate incoming messages by `msg.type`:
|
|
197
|
+
function handle(msg: IWebSocketServerMessage): void {
|
|
198
|
+
if (msg.type === "world-snapshot") {
|
|
199
|
+
for (const dimension of msg.dimensions) {
|
|
200
|
+
renderDimension(dimension);
|
|
201
|
+
}
|
|
202
|
+
} else if (msg.type === "tool-call") {
|
|
203
|
+
applyToolCall(msg); // msg.tool, msg.arguments, msg.result, msg.isError, msg.error, msg.startedAt, msg.durationMs
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
151
208
|
## Resources
|
|
152
209
|
|
|
153
210
|
| URI | Description |
|
|
@@ -189,7 +246,7 @@ Measured on Node.js 26 (results vary by hardware):
|
|
|
189
246
|
src/
|
|
190
247
|
index.ts Entry point
|
|
191
248
|
app/ Server assembly
|
|
192
|
-
config/ Server metadata, dimensions, storage, world limits
|
|
249
|
+
config/ Server metadata, dimensions, storage, world limits, WebSocket
|
|
193
250
|
data/ Generated registry data (blocks, items, entities, version)
|
|
194
251
|
enums/ Enumerations (EBoundingBox, EIconTheme, EDimensionType, EEntityKind, EInventorySection, EMoveDirection)
|
|
195
252
|
interfaces/ Interfaces (IBlock, IItem, IEntityType, IEntity, IEntityInventory, IDimension, IPlacedBlock, IWorldSnapshot, IWorldStore)
|
|
@@ -203,6 +260,7 @@ scripts/
|
|
|
203
260
|
logger.mjs Coloured stdout logger for developer scripts
|
|
204
261
|
generate-data.mjs Regenerates src/data from minecraft-data
|
|
205
262
|
smoke-test.mjs End-to-end smoke test (blocks, dimensions, entities, inventories)
|
|
263
|
+
websocket-test.mjs WebSocket fan-out test (snapshots, tool-call broadcast)
|
|
206
264
|
persistence-test.mjs Restart test for blocks, entities and inventories
|
|
207
265
|
error-test.mjs Error fallbacks, sanitizing and storage limit tests
|
|
208
266
|
stress-test.mjs Parallel write, coalescing and restart stress test
|
|
@@ -211,17 +269,18 @@ scripts/
|
|
|
211
269
|
|
|
212
270
|
## Scripts
|
|
213
271
|
|
|
214
|
-
| Command
|
|
215
|
-
|
|
|
216
|
-
| `npm run build`
|
|
217
|
-
| `npm run dev`
|
|
218
|
-
| `npm start`
|
|
219
|
-
| `npm test`
|
|
220
|
-
| `npm run test:
|
|
221
|
-
| `npm run
|
|
222
|
-
| `npm run
|
|
223
|
-
| `npm run
|
|
224
|
-
| `npm run lint
|
|
272
|
+
| Command | Description |
|
|
273
|
+
| ------------------------ | ---------------------------------------------------------------- |
|
|
274
|
+
| `npm run build` | Lint, compile and generate docs |
|
|
275
|
+
| `npm run dev` | Build then start the server |
|
|
276
|
+
| `npm start` | Start the server (stdio) |
|
|
277
|
+
| `npm test` | Run the smoke, WebSocket, persistence, security and stress tests |
|
|
278
|
+
| `npm run test:websocket` | Run only the WebSocket fan-out test |
|
|
279
|
+
| `npm run test:stress` | Run only the parallel write stress test |
|
|
280
|
+
| `npm run generate-data` | Regenerate registry data (see below) |
|
|
281
|
+
| `npm run clean` | Remove build output |
|
|
282
|
+
| `npm run lint` | Lint without modifying files |
|
|
283
|
+
| `npm run lint:fix` | Lint and auto-fix |
|
|
225
284
|
|
|
226
285
|
## Data
|
|
227
286
|
|
package/build/app/server.js
CHANGED
|
@@ -2,8 +2,13 @@ import { McpServer } from '@modelcontextprotocol/server';
|
|
|
2
2
|
import { SERVER_INFO } from '../config/server-info.js';
|
|
3
3
|
import { registerResources } from '../resources/index.js';
|
|
4
4
|
import { registerTools } from '../tools/index.js';
|
|
5
|
-
|
|
5
|
+
import { instrumentToolCalls } from './tool-events.js';
|
|
6
|
+
export function createServer(world, options) {
|
|
6
7
|
const server = new McpServer(SERVER_INFO);
|
|
8
|
+
const onToolCall = options?.onToolCall;
|
|
9
|
+
if (onToolCall) {
|
|
10
|
+
instrumentToolCalls(server, onToolCall);
|
|
11
|
+
}
|
|
7
12
|
registerTools(server, world);
|
|
8
13
|
registerResources(server, world);
|
|
9
14
|
return server;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { toErrorMessage } from '../utils/error-message.js';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
function readIsError(result) {
|
|
4
|
+
if (typeof result !== 'object' || result === null) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return result.isError === true;
|
|
8
|
+
}
|
|
9
|
+
function extractErrorMessage(result) {
|
|
10
|
+
if (typeof result !== 'object' || result === null) {
|
|
11
|
+
return 'Tool call failed';
|
|
12
|
+
}
|
|
13
|
+
const content = result.content;
|
|
14
|
+
if (Array.isArray(content)) {
|
|
15
|
+
const first = content.find((entry) => typeof entry === 'object' &&
|
|
16
|
+
entry !== null &&
|
|
17
|
+
entry.type === 'text' &&
|
|
18
|
+
typeof entry.text === 'string');
|
|
19
|
+
if (first) {
|
|
20
|
+
return first.text;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return 'Tool call failed';
|
|
24
|
+
}
|
|
25
|
+
function safeNotify(listener, event) {
|
|
26
|
+
try {
|
|
27
|
+
listener(event);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logger.warn(`Tool-call listener failed: ${toErrorMessage(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function instrumentToolCalls(server, listener) {
|
|
34
|
+
const host = server;
|
|
35
|
+
const original = host.registerTool.bind(server);
|
|
36
|
+
host.registerTool = (name, config, cb) => {
|
|
37
|
+
const wrapped = async (args, extra) => {
|
|
38
|
+
const startedAt = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const result = await cb(args, extra);
|
|
41
|
+
const durationMs = Date.now() - startedAt;
|
|
42
|
+
const isError = readIsError(result);
|
|
43
|
+
safeNotify(listener, {
|
|
44
|
+
type: 'tool-call',
|
|
45
|
+
tool: name,
|
|
46
|
+
arguments: args,
|
|
47
|
+
result,
|
|
48
|
+
isError,
|
|
49
|
+
error: isError ? extractErrorMessage(result) : null,
|
|
50
|
+
startedAt,
|
|
51
|
+
durationMs
|
|
52
|
+
});
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const durationMs = Date.now() - startedAt;
|
|
57
|
+
safeNotify(listener, {
|
|
58
|
+
type: 'tool-call',
|
|
59
|
+
tool: name,
|
|
60
|
+
arguments: args,
|
|
61
|
+
result: null,
|
|
62
|
+
isError: true,
|
|
63
|
+
error: toErrorMessage(error),
|
|
64
|
+
startedAt,
|
|
65
|
+
durationMs
|
|
66
|
+
});
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return original(name, config, wrapped);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
+
import { toErrorMessage } from '../utils/error-message.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { dimensionState } from '../resources/dimension-resources.js';
|
|
5
|
+
import { World } from '../world/world.js';
|
|
6
|
+
const INITIAL_SNAPSHOT_DELAY_MS = 25;
|
|
7
|
+
function buildWorldSnapshot(world) {
|
|
8
|
+
return {
|
|
9
|
+
type: 'world-snapshot',
|
|
10
|
+
timestamp: Date.now(),
|
|
11
|
+
dimensions: World.listDimensions().map(dimension => dimensionState(world, dimension))
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function sendText(socket, text) {
|
|
15
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
socket.send(text);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
logger.warn(`WebSocket send failed: ${toErrorMessage(error)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sendObject(socket, payload) {
|
|
26
|
+
try {
|
|
27
|
+
sendText(socket, JSON.stringify(payload));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logger.warn(`WebSocket serialize failed: ${toErrorMessage(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isClientMessage(value) {
|
|
34
|
+
if (typeof value !== 'object' || value === null) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const type = value.type;
|
|
38
|
+
return type === 'world-request' || type === 'world';
|
|
39
|
+
}
|
|
40
|
+
function handleClientMessage(world, socket, data) {
|
|
41
|
+
const text = data.toString().trim();
|
|
42
|
+
if (text.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let request;
|
|
46
|
+
try {
|
|
47
|
+
request = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (isClientMessage(request)) {
|
|
53
|
+
sendObject(socket, buildWorldSnapshot(world));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function startWebSocketService(world, config) {
|
|
57
|
+
const clients = new Set();
|
|
58
|
+
const pendingSnapshots = new Set();
|
|
59
|
+
const wss = new WebSocketServer({
|
|
60
|
+
host: config.host,
|
|
61
|
+
port: config.port,
|
|
62
|
+
path: config.path
|
|
63
|
+
});
|
|
64
|
+
wss.on('error', error => {
|
|
65
|
+
logger.error(`WebSocket service error: ${toErrorMessage(error)}`);
|
|
66
|
+
});
|
|
67
|
+
wss.on('connection', socket => {
|
|
68
|
+
clients.add(socket);
|
|
69
|
+
logger.info(`WebSocket client connected (${clients.size} client(s))`);
|
|
70
|
+
const snapshotTimer = setTimeout(() => {
|
|
71
|
+
pendingSnapshots.delete(snapshotTimer);
|
|
72
|
+
try {
|
|
73
|
+
sendObject(socket, buildWorldSnapshot(world));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
logger.warn(`WebSocket initial snapshot failed: ${toErrorMessage(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}, INITIAL_SNAPSHOT_DELAY_MS);
|
|
79
|
+
pendingSnapshots.add(snapshotTimer);
|
|
80
|
+
socket.on('message', data => {
|
|
81
|
+
handleClientMessage(world, socket, data);
|
|
82
|
+
});
|
|
83
|
+
socket.on('close', () => {
|
|
84
|
+
clients.delete(socket);
|
|
85
|
+
logger.info(`WebSocket client disconnected (${clients.size} client(s))`);
|
|
86
|
+
});
|
|
87
|
+
socket.on('error', error => {
|
|
88
|
+
logger.warn(`WebSocket client error: ${toErrorMessage(error)}`);
|
|
89
|
+
clients.delete(socket);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
function broadcast(event) {
|
|
93
|
+
if (clients.size === 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let text;
|
|
97
|
+
try {
|
|
98
|
+
text = JSON.stringify(event);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.warn(`WebSocket tool-call serialize failed: ${toErrorMessage(error)}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const socket of clients) {
|
|
105
|
+
sendText(socket, text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function clientCount() {
|
|
109
|
+
return clients.size;
|
|
110
|
+
}
|
|
111
|
+
async function close() {
|
|
112
|
+
for (const snapshotTimer of pendingSnapshots) {
|
|
113
|
+
clearTimeout(snapshotTimer);
|
|
114
|
+
}
|
|
115
|
+
pendingSnapshots.clear();
|
|
116
|
+
for (const socket of clients) {
|
|
117
|
+
try {
|
|
118
|
+
socket.close(1001, 'Server shutting down');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
clients.clear();
|
|
124
|
+
await new Promise(resolve => {
|
|
125
|
+
wss.close(() => {
|
|
126
|
+
resolve();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { broadcast, clientCount, close };
|
|
131
|
+
}
|
|
@@ -7,7 +7,7 @@ export const SERVER_INFO = {
|
|
|
7
7
|
name: '@lzimul/minecraft-mcp',
|
|
8
8
|
title: 'lZiMUl Minecraft MCP',
|
|
9
9
|
description: `Minecraft Model Context Protocol server (Java Edition ${MINECRAFT_DATA_VERSION}): exposes all ${MINECRAFT_BLOCK_COUNT} blocks, ${MINECRAFT_ITEM_COUNT} items and ${MINECRAFT_ENTITY_COUNT} entity types with their properties as resources, plus tools to read, place, query and fill blocks, inspect dimensions, create, move and delete entities and players, set entity positions, and manage their hotbar and backpack inventories.`,
|
|
10
|
-
version: '1.0.
|
|
10
|
+
version: '1.0.7',
|
|
11
11
|
websiteUrl: 'https://projects.lzimul.com/minecraft-mcp',
|
|
12
12
|
icons: [
|
|
13
13
|
{
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function envString(name) {
|
|
2
|
+
const raw = process.env[name];
|
|
3
|
+
if (typeof raw !== 'string') {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const trimmed = raw.trim();
|
|
7
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
8
|
+
}
|
|
9
|
+
function envFlag(name, fallback) {
|
|
10
|
+
const raw = envString(name);
|
|
11
|
+
if (raw === null) {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
const normalized = raw.toLowerCase();
|
|
15
|
+
if (['false', '0', 'no', 'off'].includes(normalized)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (['true', '1', 'yes', 'on'].includes(normalized)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
function envPort(name, fallback) {
|
|
24
|
+
const raw = envString(name);
|
|
25
|
+
if (raw === null) {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
const parsed = Number.parseInt(raw, 10);
|
|
29
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535
|
|
30
|
+
? parsed
|
|
31
|
+
: fallback;
|
|
32
|
+
}
|
|
33
|
+
function envHost(name, fallback) {
|
|
34
|
+
return envString(name) ?? fallback;
|
|
35
|
+
}
|
|
36
|
+
function envPath(name, fallback) {
|
|
37
|
+
const raw = envString(name);
|
|
38
|
+
if (raw === null) {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
return raw.startsWith('/') ? raw : `/${raw}`;
|
|
42
|
+
}
|
|
43
|
+
export const WEBSOCKET_CONFIG = {
|
|
44
|
+
enabled: envFlag('MCP_WS_ENABLED', true),
|
|
45
|
+
host: envHost('MCP_WS_HOST', '127.0.0.1'),
|
|
46
|
+
port: envPort('MCP_WS_PORT', 8765),
|
|
47
|
+
path: envPath('MCP_WS_PATH', '/')
|
|
48
|
+
};
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
3
3
|
import { createServer } from './app/server.js';
|
|
4
|
+
import { startWebSocketService } from './app/websocket.js';
|
|
5
|
+
import { WEBSOCKET_CONFIG } from './config/websocket.js';
|
|
4
6
|
import { WORLD_STORAGE } from './config/storage.js';
|
|
5
7
|
import { toErrorMessage } from './utils/error-message.js';
|
|
6
8
|
import { logger } from './utils/logger.js';
|
|
@@ -8,7 +10,12 @@ import { createWorldStore } from './world/stores/index.js';
|
|
|
8
10
|
import { World } from './world/world.js';
|
|
9
11
|
async function main() {
|
|
10
12
|
const world = await World.load(createWorldStore(), WORLD_STORAGE.writeDelayMs);
|
|
11
|
-
|
|
13
|
+
let wsService = null;
|
|
14
|
+
if (WEBSOCKET_CONFIG.enabled) {
|
|
15
|
+
wsService = startWebSocketService(world, WEBSOCKET_CONFIG);
|
|
16
|
+
logger.info(`WebSocket service listening on ws://${WEBSOCKET_CONFIG.host}:${WEBSOCKET_CONFIG.port}${WEBSOCKET_CONFIG.path}`);
|
|
17
|
+
}
|
|
18
|
+
const handle = serveStdio(() => createServer(world, wsService ? { onToolCall: wsService.broadcast } : {}), {
|
|
12
19
|
legacy: 'reject'
|
|
13
20
|
});
|
|
14
21
|
let shuttingDown = false;
|
|
@@ -32,6 +39,15 @@ async function main() {
|
|
|
32
39
|
logger.error(`Failed to close the MCP transport: ${toErrorMessage(error)}`);
|
|
33
40
|
code = 1;
|
|
34
41
|
}
|
|
42
|
+
if (wsService) {
|
|
43
|
+
try {
|
|
44
|
+
await wsService.close();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.error(`Failed to close the WebSocket service: ${toErrorMessage(error)}`);
|
|
48
|
+
code = 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
35
51
|
process.exit(code);
|
|
36
52
|
};
|
|
37
53
|
process.once('SIGINT', () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -17,7 +17,7 @@ function dimensionSummary(world, dimension) {
|
|
|
17
17
|
uri: `lzimul://minecraft/dimensions/${dimension.type}`
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
function dimensionState(world, dimension) {
|
|
20
|
+
export function dimensionState(world, dimension) {
|
|
21
21
|
const simulated = world.getDimension(dimension.type);
|
|
22
22
|
return {
|
|
23
23
|
key: dimension.key,
|
package/lib/app/server.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import type { ToolCallListener } from './tool-events.js';
|
|
2
3
|
import type { World } from '../world/world.js';
|
|
3
|
-
export
|
|
4
|
+
export interface ICreateServerOptions {
|
|
5
|
+
readonly onToolCall?: ToolCallListener;
|
|
6
|
+
}
|
|
7
|
+
export declare function createServer(world: World, options?: ICreateServerOptions): McpServer;
|
|
4
8
|
//# sourceMappingURL=server.d.ts.map
|
package/lib/app/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAOzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,MAAM,WAAW,oBAAoB;IAEnC,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACxC;AAGD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,KAAK,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC7B,SAAS,CAaX"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import type { IToolCallEvent, ToolCallListener } from '../interfaces/websocket.interface.js';
|
|
3
|
+
export type { IToolCallEvent, ToolCallListener };
|
|
4
|
+
export declare function instrumentToolCalls(server: McpServer, listener: ToolCallListener): void;
|
|
5
|
+
//# sourceMappingURL=tool-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-events.d.ts","sourceRoot":"","sources":["../../src/app/tool-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAK9D,OAAO,KAAK,EACV,cAAc,EACd,gBAAgB,EACjB,MAAM,sCAAsC,CAAC;AAE9C,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAC;AA4DjD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,gBAAgB,GACzB,IAAI,CAwCN"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { World } from '../world/world.js';
|
|
2
|
+
import type { IWebSocketConfig } from '../config/websocket.js';
|
|
3
|
+
import type { IToolCallEvent } from '../interfaces/websocket.interface.js';
|
|
4
|
+
export interface IWebSocketService {
|
|
5
|
+
broadcast(event: IToolCallEvent): void;
|
|
6
|
+
clientCount(): number;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function startWebSocketService(world: World, config: IWebSocketConfig): IWebSocketService;
|
|
10
|
+
//# sourceMappingURL=websocket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/app/websocket.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,sCAAsC,CAAC;AAc9C,MAAM,WAAW,iBAAiB;IAEhC,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IAEvC,WAAW,IAAI,MAAM,CAAC;IAEtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA0ED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,gBAAgB,GACvB,iBAAiB,CAyFnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/config/websocket.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AA2DD,eAAO,MAAM,gBAAgB,EAAE,gBAK9B,CAAC"}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {};
|
|
2
|
+
export type { IDimensionSnapshot, ISnapshotPlacedBlock, IToolCallEvent, IWebSocketClientMessage, IWebSocketServerMessage, IWorldSnapshotMessage, ToolCallListener } from './interfaces/websocket.interface.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAoGA,YAAY,EACV,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,qCAAqC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EDimensionType } from '../enums/dimension-type.enum.js';
|
|
2
|
+
import type { BlockInfo } from '../schemas/block-schema.js';
|
|
3
|
+
import type { EntityInfo } from '../schemas/entity-schema.js';
|
|
4
|
+
import type { BlockStates } from './block-states.interface.js';
|
|
5
|
+
export interface ISnapshotPlacedBlock {
|
|
6
|
+
readonly x: number;
|
|
7
|
+
readonly y: number;
|
|
8
|
+
readonly z: number;
|
|
9
|
+
readonly block: BlockInfo;
|
|
10
|
+
readonly states: BlockStates;
|
|
11
|
+
}
|
|
12
|
+
export interface IDimensionSnapshot {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly type: EDimensionType;
|
|
15
|
+
readonly displayName: string;
|
|
16
|
+
readonly minY: number;
|
|
17
|
+
readonly maxY: number;
|
|
18
|
+
readonly hasSkylight: boolean;
|
|
19
|
+
readonly modifiedBlockCount: number;
|
|
20
|
+
readonly entityCount: number;
|
|
21
|
+
readonly playerCount: number;
|
|
22
|
+
readonly placedBlocks: readonly ISnapshotPlacedBlock[];
|
|
23
|
+
readonly entities: readonly EntityInfo[];
|
|
24
|
+
}
|
|
25
|
+
export interface IWorldSnapshotMessage {
|
|
26
|
+
readonly type: 'world-snapshot';
|
|
27
|
+
readonly timestamp: number;
|
|
28
|
+
readonly dimensions: readonly IDimensionSnapshot[];
|
|
29
|
+
}
|
|
30
|
+
export interface IToolCallEvent {
|
|
31
|
+
readonly type: 'tool-call';
|
|
32
|
+
readonly tool: string;
|
|
33
|
+
readonly arguments: unknown;
|
|
34
|
+
readonly result: unknown;
|
|
35
|
+
readonly isError: boolean;
|
|
36
|
+
readonly error: string | null;
|
|
37
|
+
readonly startedAt: number;
|
|
38
|
+
readonly durationMs: number;
|
|
39
|
+
}
|
|
40
|
+
export type ToolCallListener = (event: IToolCallEvent) => void;
|
|
41
|
+
export type IWebSocketClientMessage = {
|
|
42
|
+
readonly type: 'world-request';
|
|
43
|
+
} | {
|
|
44
|
+
readonly type: 'world';
|
|
45
|
+
};
|
|
46
|
+
export type IWebSocketServerMessage = IWorldSnapshotMessage | IToolCallEvent;
|
|
47
|
+
//# sourceMappingURL=websocket.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/websocket.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAG/D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAGD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACvD,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AAGD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAEhC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACpD;AAGD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAGD,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;AAG/D,MAAM,MAAM,uBAAuB,GACjC;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAGlE,MAAM,MAAM,uBAAuB,GAAG,qBAAqB,GAAG,cAAc,CAAC"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { World } from '../world/world.js';
|
|
3
|
+
import type { IDimension } from '../interfaces/dimension.interface.js';
|
|
4
|
+
import type { IDimensionSnapshot } from '../interfaces/websocket.interface.js';
|
|
5
|
+
export declare function dimensionState(world: World, dimension: IDimension): IDimensionSnapshot;
|
|
3
6
|
export declare function registerDimensionIndexResource(server: McpServer, world: World): void;
|
|
4
7
|
export declare function registerDimensionResources(server: McpServer, world: World): void;
|
|
5
8
|
//# sourceMappingURL=dimension-resources.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dimension-resources.d.ts","sourceRoot":"","sources":["../../src/resources/dimension-resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"dimension-resources.d.ts","sourceRoot":"","sources":["../../src/resources/dimension-resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAuB/E,wBAAgB,cAAc,CAC5B,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,UAAU,GACpB,kBAAkB,CAqBpB;AAGD,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,KAAK,GACX,IAAI,CAgCN;AAGD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,KAAK,GACX,IAAI,CA2BN"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzimul/minecraft-mcp",
|
|
3
3
|
"description": "Minecraft Model Context Protocol (MCP) Server",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.7",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "lZiMUl",
|
|
7
7
|
"email": "lZiMUl@lzimul.com",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"dev": "npm run build && npm run start",
|
|
45
45
|
"generate-data": "node ./scripts/generate-data.mjs",
|
|
46
46
|
"clean": "node ./scripts/clean.mjs",
|
|
47
|
-
"test": "node ./scripts/smoke-test.mjs && node ./scripts/persistence-test.mjs && node ./scripts/error-test.mjs && node ./scripts/stress-test.mjs",
|
|
47
|
+
"test": "node ./scripts/smoke-test.mjs && node ./scripts/websocket-test.mjs && node ./scripts/persistence-test.mjs && node ./scripts/error-test.mjs && node ./scripts/stress-test.mjs",
|
|
48
|
+
"test:websocket": "node ./scripts/websocket-test.mjs",
|
|
48
49
|
"test:stress": "node ./scripts/stress-test.mjs",
|
|
49
50
|
"prepublishOnly": "npm run build && npm test"
|
|
50
51
|
},
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"@modelcontextprotocol/server": "^2.4.0",
|
|
73
74
|
"chalk": "^6.0.1",
|
|
74
75
|
"log4js": "^6.9.1",
|
|
76
|
+
"ws": "^8.22.1",
|
|
75
77
|
"zod": "^4.6.7"
|
|
76
78
|
},
|
|
77
79
|
"devDependencies": {
|
|
@@ -81,6 +83,7 @@
|
|
|
81
83
|
"@eslint/json": "^2.1.1",
|
|
82
84
|
"@eslint/markdown": "^8.0.3",
|
|
83
85
|
"@types/node": "^26.6.5",
|
|
86
|
+
"@types/ws": "^8.18.2",
|
|
84
87
|
"@typescript-eslint/eslint-plugin": "^8.72.1",
|
|
85
88
|
"@typescript-eslint/parser": "^8.72.1",
|
|
86
89
|
"eslint": "^10.13.0",
|