vitaminmcp 1.5.0 → 2.0.0
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 +8 -3
- package/bin/vitaminmcp.mjs +27 -9
- package/checksums.json +5 -3
- package/lib/jars.mjs +91 -3
- package/lib/node.mjs +43 -0
- package/package.json +10 -3
- package/runner/runner.mjs +68 -0
- package/runner/src/actions.mjs +359 -0
- package/runner/src/bots.mjs +238 -0
- package/runner/src/clientview.mjs +247 -0
- package/runner/src/dispatch.mjs +289 -0
- package/runner/src/identity.mjs +62 -0
- package/runner/src/movement.mjs +214 -0
- package/runner/src/protocol.mjs +155 -0
- package/runner/src/text.mjs +90 -0
- package/runner/src/version.mjs +64 -0
- package/runner/src/viewer.mjs +216 -0
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ agent leaves its host, ports and token where this server reads them.
|
|
|
45
45
|
|
|
46
46
|
- **Java 21 or later** on this machine — the jars run on the JVM. Point `JAVA_HOME` at it, or have
|
|
47
47
|
`java` on `PATH`
|
|
48
|
+
- **Node 18.17 or later** when using the source runner fallback. If Node is absent, the launcher
|
|
49
|
+
selects a pinned platform runner asset instead.
|
|
48
50
|
- **Paper 1.21 or later** on the Minecraft server, with `VitaminMCP.jar` in its `plugins/`
|
|
49
51
|
|
|
50
52
|
## Environment
|
|
@@ -55,7 +57,9 @@ agent leaves its host, ports and token where this server reads them.
|
|
|
55
57
|
| `VITAMINMCP_HOME` | where jars and agent handshakes are kept. Default `~/.vitaminmcp` |
|
|
56
58
|
| `VITAMINMCP_TOKEN` | an agent token, for a server that leaves no local handshake |
|
|
57
59
|
| `VITAMINMCP_SERVER_JAR` | run this `mcp-server.jar` instead of a downloaded one |
|
|
58
|
-
| `VITAMINMCP_RUNNER_JAR` | use this
|
|
60
|
+
| `VITAMINMCP_RUNNER_JAR` | use this runner path instead of automatic selection |
|
|
61
|
+
| `VITAMINMCP_NODE` | Node executable for the source runner fallback |
|
|
62
|
+
| `VITAMINMCP_NODE_RUNNER` | bundled `runner.mjs` path when Node is available |
|
|
59
63
|
|
|
60
64
|
## What it downloads
|
|
61
65
|
|
|
@@ -63,8 +67,9 @@ On first run, from [the GitHub release](https://github.com/Backas03/VitaminMCP-m
|
|
|
63
67
|
matching this package's version, into `~/.vitaminmcp/jars/<version>/`:
|
|
64
68
|
|
|
65
69
|
- `mcp-server.jar` (~2 MB) — waited for, since nothing works without it
|
|
66
|
-
-
|
|
67
|
-
|
|
70
|
+
- a source Node runner when Node and the bundled runner are available — no runner asset download
|
|
71
|
+
- otherwise one platform runner asset (`win-x64`, `linux-x64`, `linux-arm64`, `darwin-x64` or
|
|
72
|
+
`darwin-arm64`), checked against a SHA-256 pinned into this package
|
|
68
73
|
|
|
69
74
|
Both are checked against a SHA-256 pinned into this package at publish time. A file that does not
|
|
70
75
|
match is deleted rather than run.
|
package/bin/vitaminmcp.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
4
5
|
import fs from 'node:fs/promises';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
MCP_SERVER_JAR, assetPath, ensureAsset, ensureJar,
|
|
11
|
+
} from '../lib/jars.mjs';
|
|
9
12
|
import { checkJava, findJava } from '../lib/java.mjs';
|
|
13
|
+
import { checkNode, findNode, runnerAssetName } from '../lib/node.mjs';
|
|
10
14
|
|
|
11
15
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
16
|
|
|
@@ -47,7 +51,9 @@ Environment
|
|
|
47
51
|
VITAMINMCP_HOME where jars and agent handshakes are kept (default ~/.vitaminmcp)
|
|
48
52
|
VITAMINMCP_TOKEN an agent token, for a server that leaves no local handshake
|
|
49
53
|
VITAMINMCP_SERVER_JAR run this mcp-server jar instead of a downloaded one
|
|
50
|
-
VITAMINMCP_RUNNER_JAR use this
|
|
54
|
+
VITAMINMCP_RUNNER_JAR use this runner path instead of selecting one automatically
|
|
55
|
+
VITAMINMCP_NODE Node executable for the source runner fallback
|
|
56
|
+
VITAMINMCP_NODE_RUNNER path to a bundled runner.mjs when Node is available
|
|
51
57
|
`;
|
|
52
58
|
|
|
53
59
|
async function main() {
|
|
@@ -82,13 +88,24 @@ async function main() {
|
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
const configuredRunner = process.env.VITAMINMCP_RUNNER_JAR;
|
|
92
|
+
const node = findNode();
|
|
93
|
+
const nodeCheck = checkNode(node);
|
|
94
|
+
const sourceRunner = process.env.VITAMINMCP_NODE_RUNNER
|
|
95
|
+
?? path.join(HERE, '..', 'runner', 'runner.mjs');
|
|
96
|
+
let runner;
|
|
97
|
+
let runnerReady;
|
|
98
|
+
if (configuredRunner) {
|
|
99
|
+
runner = configuredRunner;
|
|
100
|
+
runnerReady = Promise.resolve(runner);
|
|
101
|
+
} else if (nodeCheck.ok && existsSync(sourceRunner)) {
|
|
102
|
+
runner = sourceRunner;
|
|
103
|
+
runnerReady = Promise.resolve(runner);
|
|
104
|
+
} else {
|
|
105
|
+
const asset = runnerAssetName();
|
|
106
|
+
runner = assetPath(release, asset);
|
|
107
|
+
runnerReady = ensureAsset(release, asset, { log: say });
|
|
108
|
+
}
|
|
92
109
|
|
|
93
110
|
if (argv.includes('--jars')) {
|
|
94
111
|
await runnerReady;
|
|
@@ -96,6 +113,7 @@ async function main() {
|
|
|
96
113
|
return 0;
|
|
97
114
|
}
|
|
98
115
|
|
|
116
|
+
await runnerReady;
|
|
99
117
|
return await run(java, server, runner);
|
|
100
118
|
}
|
|
101
119
|
|
package/checksums.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "2.0.0",
|
|
3
3
|
"jars": {
|
|
4
|
-
"mcp-server.jar": "
|
|
5
|
-
|
|
4
|
+
"mcp-server.jar": "557ad3ad919244e31fcbc8f7ecb05c8c7744c2922a0220e0fec9eb531af77759"
|
|
5
|
+
},
|
|
6
|
+
"assets": {
|
|
7
|
+
"bot-runner-win-x64.exe": "230ea83146c9f3ade0999764e8b9dd5ca790f13fec055484237ba8e6b7d8b3bd"
|
|
6
8
|
}
|
|
7
9
|
}
|
package/lib/jars.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
@@ -13,9 +13,9 @@ const RELEASES = 'https://github.com/Backas03/VitaminMCP-minecraft/releases/down
|
|
|
13
13
|
|
|
14
14
|
/** The jar this package launches, and the one it launches in turn. */
|
|
15
15
|
export const MCP_SERVER_JAR = 'mcp-server.jar';
|
|
16
|
-
export const BOT_RUNNER_JAR = 'bot-runner.jar';
|
|
17
16
|
|
|
18
17
|
let checksumsPromise;
|
|
18
|
+
let manifestPromise;
|
|
19
19
|
|
|
20
20
|
const REPORT = 'https://github.com/Backas03/VitaminMCP-minecraft/issues';
|
|
21
21
|
|
|
@@ -53,6 +53,17 @@ async function checksums(version) {
|
|
|
53
53
|
return stamped.jars ?? {};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
async function assetChecksums(version) {
|
|
57
|
+
manifestPromise ??= fs
|
|
58
|
+
.readFile(path.join(HERE, '..', 'checksums.json'), 'utf8')
|
|
59
|
+
.then(JSON.parse);
|
|
60
|
+
const stamped = await manifestPromise;
|
|
61
|
+
if (stamped.version !== version) {
|
|
62
|
+
throw new Error(`This package is version ${version} but its checksums were stamped for ${stamped.version}.`);
|
|
63
|
+
}
|
|
64
|
+
return stamped.assets ?? {};
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
/**
|
|
57
68
|
* Where downloaded jars live: one directory per version, so switching versions never mixes them.
|
|
58
69
|
*
|
|
@@ -69,10 +80,14 @@ export async function cachedJar(version, name) {
|
|
|
69
80
|
const file = path.join(cacheDirectory(version), name);
|
|
70
81
|
try {
|
|
71
82
|
await fs.access(file);
|
|
72
|
-
return file;
|
|
73
83
|
} catch {
|
|
74
84
|
return null;
|
|
75
85
|
}
|
|
86
|
+
const expected = (await checksums(version))[name];
|
|
87
|
+
if (!expected) return null;
|
|
88
|
+
if (await verifyFileHash(file, expected)) return file;
|
|
89
|
+
await fs.rm(file, { force: true });
|
|
90
|
+
return null;
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
/**
|
|
@@ -146,3 +161,76 @@ export async function ensureJar(version, name, { log = () => {} } = {}) {
|
|
|
146
161
|
export function jarPath(version, name) {
|
|
147
162
|
return path.join(cacheDirectory(version), name);
|
|
148
163
|
}
|
|
164
|
+
|
|
165
|
+
export function assetCacheDirectory(version) {
|
|
166
|
+
const root = process.env.VITAMINMCP_HOME || path.join(os.homedir(), '.vitaminmcp');
|
|
167
|
+
return path.join(root, 'assets', version);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function cachedAsset(version, name) {
|
|
171
|
+
const file = path.join(assetCacheDirectory(version), name);
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(file);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const expected = (await assetChecksums(version))[name];
|
|
178
|
+
if (!expected) return null;
|
|
179
|
+
if (await verifyFileHash(file, expected)) return file;
|
|
180
|
+
await fs.rm(file, { force: true });
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Fetches a pinned platform asset only after Node fallback selection has failed. */
|
|
185
|
+
export async function ensureAsset(version, name, { log = () => {} } = {}) {
|
|
186
|
+
const directory = assetCacheDirectory(version);
|
|
187
|
+
const file = path.join(directory, name);
|
|
188
|
+
const existing = await cachedAsset(version, name);
|
|
189
|
+
if (existing) return existing;
|
|
190
|
+
const expected = (await assetChecksums(version))[name];
|
|
191
|
+
if (!expected) throw new Error(`checksums.json does not cover asset ${name}, so it will not be downloaded.`);
|
|
192
|
+
await fs.mkdir(directory, { recursive: true });
|
|
193
|
+
const partial = `${file}.part`;
|
|
194
|
+
await fs.writeFile(partial, '');
|
|
195
|
+
const url = `${RELEASES}/${version}/${name}`;
|
|
196
|
+
log(`downloading ${name} (${version})`);
|
|
197
|
+
const response = await fetch(url, { redirect: 'follow' });
|
|
198
|
+
if (!response.ok || !response.body) {
|
|
199
|
+
await fs.rm(partial, { force: true });
|
|
200
|
+
throw new Error(`Could not download ${name}: ${response.status} ${response.statusText}\n ${url}`);
|
|
201
|
+
}
|
|
202
|
+
const hash = createHash('sha256');
|
|
203
|
+
const source = Readable.fromWeb(response.body);
|
|
204
|
+
source.on('data', (chunk) => hash.update(chunk));
|
|
205
|
+
try {
|
|
206
|
+
await pipeline(source, createWriteStream(partial));
|
|
207
|
+
const actual = hash.digest('hex');
|
|
208
|
+
if (actual !== expected) {
|
|
209
|
+
throw new Error(`${name} does not match its pinned checksum.\n expected ${expected}\n received ${actual}`);
|
|
210
|
+
}
|
|
211
|
+
await fs.rename(partial, file);
|
|
212
|
+
if (process.platform !== 'win32') await fs.chmod(file, 0o755);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
await fs.rm(partial, { force: true });
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
log(`${name} ready`);
|
|
218
|
+
return file;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function assetPath(version, name) {
|
|
222
|
+
return path.join(assetCacheDirectory(version), name);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function sha256File(file) {
|
|
226
|
+
const hash = createHash('sha256');
|
|
227
|
+
for await (const chunk of createReadStream(file)) {
|
|
228
|
+
hash.update(chunk);
|
|
229
|
+
}
|
|
230
|
+
return hash.digest('hex');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Verifies a cached release byte-for-byte without loading the whole file into memory. */
|
|
234
|
+
export async function verifyFileHash(file, expected) {
|
|
235
|
+
return (await sha256File(file)) === expected;
|
|
236
|
+
}
|
package/lib/node.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const REQUIRED_NODE = '18.17';
|
|
6
|
+
|
|
7
|
+
/** Finds an explicitly configured Node first, then the executable on PATH. */
|
|
8
|
+
export function findNode() {
|
|
9
|
+
const configured = process.env.VITAMINMCP_NODE;
|
|
10
|
+
if (configured && (existsSync(configured) || path.basename(configured) === configured)) {
|
|
11
|
+
return configured;
|
|
12
|
+
}
|
|
13
|
+
return process.platform === 'win32' ? 'node.exe' : 'node';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Checks Node before choosing the source runner path. */
|
|
17
|
+
export function checkNode(node) {
|
|
18
|
+
const probe = spawnSync(node, ['--version'], { encoding: 'utf8' });
|
|
19
|
+
if (probe.error) {
|
|
20
|
+
return { ok: false, message: `No Node found. The hybrid runner needs Node ${REQUIRED_NODE} or later.` };
|
|
21
|
+
}
|
|
22
|
+
const match = `${probe.stdout || ''}${probe.stderr || ''}`.match(/v(\d+)(?:\.(\d+))?/);
|
|
23
|
+
if (!match) return { ok: true, version: null };
|
|
24
|
+
const major = Number(match[1]);
|
|
25
|
+
const minor = Number(match[2] ?? 0);
|
|
26
|
+
if (major < 18 || (major === 18 && minor < 17)) {
|
|
27
|
+
return { ok: false, version: `${major}.${minor}`, message:
|
|
28
|
+
`Node ${major}.${minor} is too old — the hybrid runner needs Node ${REQUIRED_NODE} or later.` };
|
|
29
|
+
}
|
|
30
|
+
return { ok: true, version: `${major}.${minor}` };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The release asset name for the current SEA target. */
|
|
34
|
+
export function runnerAssetName(platform = process.platform, arch = process.arch) {
|
|
35
|
+
if (platform === 'win32' && arch === 'x64') return 'bot-runner-win-x64.exe';
|
|
36
|
+
if (platform === 'linux' || platform === 'darwin') {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Native runner assets for ${platform}-${arch} are planned but not released yet. `
|
|
39
|
+
+ 'Install Node 18.17 or later to use the source runner.',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`No VitaminMCP runner asset exists for ${platform}-${arch}.`);
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitaminmcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"mcpName": "io.github.
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"mcpName": "io.github.Backas03/vitaminmcp",
|
|
5
5
|
"description": "MCP server for testing Minecraft plugins: drives a real Paper server and real protocol bots from an AI agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"author": "Backas03",
|
|
25
25
|
"type": "module",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"mineflayer": "^4.37.1",
|
|
28
|
+
"mineflayer-pathfinder": "2.4.5"
|
|
29
|
+
},
|
|
26
30
|
"bin": {
|
|
27
31
|
"vitaminmcp": "bin/vitaminmcp.mjs"
|
|
28
32
|
},
|
|
@@ -32,11 +36,14 @@
|
|
|
32
36
|
"files": [
|
|
33
37
|
"bin",
|
|
34
38
|
"lib",
|
|
39
|
+
"runner",
|
|
35
40
|
"checksums.json",
|
|
36
41
|
"README.md"
|
|
37
42
|
],
|
|
38
43
|
"scripts": {
|
|
44
|
+
"test": "node --test test/*.test.mjs",
|
|
39
45
|
"stamp": "node scripts/stamp-checksums.mjs",
|
|
40
|
-
"
|
|
46
|
+
"stage-runner": "node scripts/stage-node-runner.mjs",
|
|
47
|
+
"prepublishOnly": "node scripts/stage-node-runner.mjs && node scripts/stamp-checksums.mjs --verify"
|
|
41
48
|
}
|
|
42
49
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The bot runner, on mineflayer.
|
|
4
|
+
*
|
|
5
|
+
* Speaks the VitaminMCP stdio line protocol and is launched as the runner child process:
|
|
6
|
+
*
|
|
7
|
+
* node runner.mjs <host> <port> [protocol]
|
|
8
|
+
*
|
|
9
|
+
* Everything it prints on stdout is protocol. Diagnostics go to stderr, which the MCP server
|
|
10
|
+
* inherits — a stray `console.log` here is a desynchronised session, not a stray log line.
|
|
11
|
+
*/
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
|
|
14
|
+
import { BotRegistry } from './src/bots.mjs';
|
|
15
|
+
import { Dispatch } from './src/dispatch.mjs';
|
|
16
|
+
import * as protocol from './src/protocol.mjs';
|
|
17
|
+
import { pingProtocol, versionForProtocol } from './src/version.mjs';
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const argv = process.argv.slice(2);
|
|
21
|
+
if (argv.length < 2) {
|
|
22
|
+
process.stderr.write('usage: runner.mjs <host> <port> [protocol]\n');
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const host = argv[0];
|
|
27
|
+
const port = Number(argv[1]);
|
|
28
|
+
|
|
29
|
+
let bots;
|
|
30
|
+
let negotiated;
|
|
31
|
+
try {
|
|
32
|
+
negotiated = argv.length > 2 ? Number(argv[2]) : await pingProtocol(host, port);
|
|
33
|
+
bots = new BotRegistry(host, port, versionForProtocol(negotiated));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// The Java side reads this line and reports it as the reason the runner did not start, so it
|
|
36
|
+
// has to arrive on stdout in protocol form rather than as a stack trace on stderr.
|
|
37
|
+
write(protocol.encode(protocol.ERROR, 'startup', `${error?.name ?? 'Error'}: ${error?.message ?? error}`));
|
|
38
|
+
process.stderr.write(`${error?.stack ?? error}\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
write(protocol.encode(protocol.READY, String(negotiated)));
|
|
43
|
+
|
|
44
|
+
const dispatch = new Dispatch(bots);
|
|
45
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
46
|
+
|
|
47
|
+
for await (const line of lines) {
|
|
48
|
+
const reply = await dispatch.handle(line);
|
|
49
|
+
if (reply === null) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (reply !== '') {
|
|
53
|
+
write(reply);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
bots.shutdown();
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main().catch((error) => {
|
|
62
|
+
process.stderr.write(`${error?.stack ?? error}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function write(line) {
|
|
67
|
+
process.stdout.write(`${line}\n`);
|
|
68
|
+
}
|