@brass-lantern/mcp 0.0.1-stage → 2.3.1
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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +19 -0
- package/dist/docs/llms.txt +35 -0
- package/dist/docs/pages/guide/building-worlds/index.md +218 -0
- package/dist/docs/pages/guide/building-worlds/recipes.md +1406 -0
- package/dist/docs/pages/guide/building-worlds/two-rooms.md +339 -0
- package/dist/docs/pages/guide/coding-agents.md +124 -0
- package/dist/docs/pages/guide/deploying.md +56 -0
- package/dist/docs/pages/guide/getting-started.md +72 -0
- package/dist/docs/pages/guide/how-it-works.md +152 -0
- package/dist/docs/pages/guide/intent-server.md +92 -0
- package/dist/docs/pages/guide/porting-zork.md +115 -0
- package/dist/docs/pages/guide/testing.md +100 -0
- package/dist/docs/pages/guide/using-the-library.md +272 -0
- package/dist/docs/pages/guide/your-first-world.md +206 -0
- package/dist/docs/pages/guide/z-machine.md +96 -0
- package/dist/docs/pages/index.md +58 -0
- package/dist/docs/pages/reference/cartridges.md +47 -0
- package/dist/docs/pages/reference/commands.md +79 -0
- package/dist/docs/pages/reference/conditions-and-events.md +129 -0
- package/dist/docs/pages/reference/world-schema.md +592 -0
- package/dist/docs/pages.json +74 -0
- package/dist/docs.d.ts +21 -0
- package/dist/docs.js +89 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +42 -0
- package/dist/job.d.ts +22 -0
- package/dist/job.js +29 -0
- package/dist/load.d.ts +22 -0
- package/dist/load.js +106 -0
- package/dist/run.d.ts +16 -0
- package/dist/run.js +57 -0
- package/dist/server.d.ts +47 -0
- package/dist/server.js +97 -0
- package/dist/stdio.d.ts +25 -0
- package/dist/stdio.js +75 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +82 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +6 -0
- package/package.json +47 -1
package/dist/stdio.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdout carries the MCP protocol, so a world's console output (at load or while a script runs)
|
|
3
|
+
* would corrupt it. Swaps the global console for one whose stdout and stderr are both
|
|
4
|
+
* process.stderr, so every method (log, dir, table, trace, …) lands there; the SDK transport
|
|
5
|
+
* writes through process.stdout.write directly, so it is unaffected. Returns a function that
|
|
6
|
+
* puts the original console back.
|
|
7
|
+
*/
|
|
8
|
+
export declare function quietStdout(): () => void;
|
|
9
|
+
/**
|
|
10
|
+
* The directories worlds may load from: `cwd`, plus each `--root <dir>` (repeatable), resolved
|
|
11
|
+
* against `cwd`. A trailing `--root` with no directory is ignored, with a warning through `warn`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseRoots(args: readonly string[], cwd: string, warn?: (message: string) => void): string[];
|
|
14
|
+
/**
|
|
15
|
+
* How long one audit_world or play call may run, in milliseconds: `--timeout <seconds>` (the last
|
|
16
|
+
* one wins), else 10 s. A missing, non-numeric or non-positive value is ignored with a warning;
|
|
17
|
+
* one past setTimeout's limit (2147483 s, about 24.8 days) is capped at it, with a warning.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseTimeout(args: readonly string[], warn?: (message: string) => void): number;
|
|
20
|
+
/**
|
|
21
|
+
* A world can throw after its tool call has returned (a timer, a stray promise). Node's default
|
|
22
|
+
* would end the server mid-session; instead, log the error to stderr and keep serving. Returns a
|
|
23
|
+
* function that removes the handlers.
|
|
24
|
+
*/
|
|
25
|
+
export declare function keepRunningOnCrash(log?: (...args: unknown[]) => void): () => void;
|
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Console } from 'node:console';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS } from "./run.js";
|
|
4
|
+
/**
|
|
5
|
+
* Stdout carries the MCP protocol, so a world's console output (at load or while a script runs)
|
|
6
|
+
* would corrupt it. Swaps the global console for one whose stdout and stderr are both
|
|
7
|
+
* process.stderr, so every method (log, dir, table, trace, …) lands there; the SDK transport
|
|
8
|
+
* writes through process.stdout.write directly, so it is unaffected. Returns a function that
|
|
9
|
+
* puts the original console back.
|
|
10
|
+
*/
|
|
11
|
+
export function quietStdout() {
|
|
12
|
+
const saved = globalThis.console;
|
|
13
|
+
globalThis.console = new Console({ stdout: process.stderr, stderr: process.stderr });
|
|
14
|
+
return () => {
|
|
15
|
+
globalThis.console = saved;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The directories worlds may load from: `cwd`, plus each `--root <dir>` (repeatable), resolved
|
|
20
|
+
* against `cwd`. A trailing `--root` with no directory is ignored, with a warning through `warn`.
|
|
21
|
+
*/
|
|
22
|
+
export function parseRoots(args, cwd, warn = (m) => process.stderr.write(`${m}\n`)) {
|
|
23
|
+
const roots = [cwd];
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
if (args[i] !== '--root')
|
|
26
|
+
continue;
|
|
27
|
+
const dir = args[i + 1];
|
|
28
|
+
if (dir === undefined)
|
|
29
|
+
warn('brass-lantern-mcp: --root needs a directory; ignored');
|
|
30
|
+
else
|
|
31
|
+
roots.push(resolve(cwd, dir));
|
|
32
|
+
i += 1;
|
|
33
|
+
}
|
|
34
|
+
return roots;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* How long one audit_world or play call may run, in milliseconds: `--timeout <seconds>` (the last
|
|
38
|
+
* one wins), else 10 s. A missing, non-numeric or non-positive value is ignored with a warning;
|
|
39
|
+
* one past setTimeout's limit (2147483 s, about 24.8 days) is capped at it, with a warning.
|
|
40
|
+
*/
|
|
41
|
+
export function parseTimeout(args, warn = (m) => process.stderr.write(`${m}\n`)) {
|
|
42
|
+
const DEFAULT = DEFAULT_TIMEOUT_MS;
|
|
43
|
+
const MAX_MS = MAX_TIMEOUT_MS;
|
|
44
|
+
let ms = DEFAULT;
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
if (args[i] !== '--timeout')
|
|
47
|
+
continue;
|
|
48
|
+
const value = args[i + 1];
|
|
49
|
+
const seconds = value === undefined || value.trim() === '' ? NaN : Number(value);
|
|
50
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
51
|
+
ms = Math.min(seconds * 1000, MAX_MS);
|
|
52
|
+
if (seconds * 1000 > MAX_MS)
|
|
53
|
+
warn(`brass-lantern-mcp: --timeout is capped at ${Math.floor(MAX_MS / 1000)} seconds`);
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
warn(`brass-lantern-mcp: --timeout needs a positive number of seconds; using ${DEFAULT / 1000}`);
|
|
57
|
+
i += 1;
|
|
58
|
+
}
|
|
59
|
+
return ms;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A world can throw after its tool call has returned (a timer, a stray promise). Node's default
|
|
63
|
+
* would end the server mid-session; instead, log the error to stderr and keep serving. Returns a
|
|
64
|
+
* function that removes the handlers.
|
|
65
|
+
*/
|
|
66
|
+
export function keepRunningOnCrash(log = (...args) => console.error(...args)) {
|
|
67
|
+
const onException = (error) => log('brass-lantern-mcp: uncaught exception:', error);
|
|
68
|
+
const onRejection = (reason) => log('brass-lantern-mcp: unhandled rejection:', reason);
|
|
69
|
+
process.on('uncaughtException', onException);
|
|
70
|
+
process.on('unhandledRejection', onRejection);
|
|
71
|
+
return () => {
|
|
72
|
+
process.off('uncaughtException', onException);
|
|
73
|
+
process.off('unhandledRejection', onRejection);
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedEngine } from './engine.ts';
|
|
2
|
+
import type { LoadedWorld } from './load.ts';
|
|
3
|
+
export declare const MAX_COMMANDS = 500;
|
|
4
|
+
export declare const MAX_LINES = 2000;
|
|
5
|
+
/** The lines every engine tool opens with: which world, which engine. */
|
|
6
|
+
export declare function header(loaded: LoadedWorld, resolved: ResolvedEngine): string[];
|
|
7
|
+
export declare function auditReport(loaded: LoadedWorld, resolved: ResolvedEngine): string;
|
|
8
|
+
export declare function playReport(loaded: LoadedWorld, resolved: ResolvedEngine, commands: string[], seed?: number): string;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const MAX_COMMANDS = 500;
|
|
2
|
+
export const MAX_LINES = 2000;
|
|
3
|
+
/** How many of a long transcript’s last lines survive the cut. */
|
|
4
|
+
const TAIL_LINES = 200;
|
|
5
|
+
const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
6
|
+
/** The lines every engine tool opens with: which world, which engine. */
|
|
7
|
+
export function header(loaded, resolved) {
|
|
8
|
+
const whose = resolved.source === 'project' ? 'the project’s' : 'bundled with the MCP server';
|
|
9
|
+
return [
|
|
10
|
+
`World: ${loaded.exportName} in ${loaded.file}`,
|
|
11
|
+
`Engine: @brass-lantern/engine ${resolved.version} (${whose})`,
|
|
12
|
+
...(resolved.note ? [`Note: ${resolved.note}`] : []),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
export function auditReport(loaded, resolved) {
|
|
16
|
+
const w = loaded.world;
|
|
17
|
+
const problems = resolved.engine.auditWorld(w);
|
|
18
|
+
const size = [
|
|
19
|
+
count(Object.keys(w.rooms).length, 'room', 'rooms'),
|
|
20
|
+
count(Object.keys(w.items).length, 'item', 'items'),
|
|
21
|
+
count(Object.keys(w.npcs).length, 'character', 'characters'),
|
|
22
|
+
count(Object.keys(w.events ?? {}).length, 'event', 'events'),
|
|
23
|
+
].join(', ');
|
|
24
|
+
return [
|
|
25
|
+
...header(loaded, resolved),
|
|
26
|
+
`Size: ${size}`,
|
|
27
|
+
'',
|
|
28
|
+
problems.length === 0 ? 'No problems.' : `${count(problems.length, 'problem', 'problems')}:`,
|
|
29
|
+
...problems.map((p) => `- ${p}`),
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|
|
32
|
+
/** The pieces of a typed line the regex parser can’t read (what the intent server would be asked about). */
|
|
33
|
+
function unread(resolved, world, line) {
|
|
34
|
+
const { splitCommands, fallbackParse, andNames } = resolved.engine;
|
|
35
|
+
if (!splitCommands || !fallbackParse)
|
|
36
|
+
return [];
|
|
37
|
+
return splitCommands(line, world.verbs, andNames?.(world)).filter((piece) => !fallbackParse(piece, world.verbs));
|
|
38
|
+
}
|
|
39
|
+
export function playReport(loaded, resolved, commands, seed = 1) {
|
|
40
|
+
const world = loaded.world;
|
|
41
|
+
const game = resolved.engine.createGame(world, { seed });
|
|
42
|
+
const run = commands.slice(0, MAX_COMMANDS);
|
|
43
|
+
const transcript = [...game.opening];
|
|
44
|
+
let awaiting = false;
|
|
45
|
+
let ran = 0;
|
|
46
|
+
let over = game.state.gameOver;
|
|
47
|
+
for (const command of run) {
|
|
48
|
+
if (over)
|
|
49
|
+
break;
|
|
50
|
+
transcript.push(`> ${command}`);
|
|
51
|
+
// An answer to “Which do you mean?” isn’t a command, so it isn’t checked as one.
|
|
52
|
+
const missed = awaiting ? [] : unread(resolved, world, command);
|
|
53
|
+
if (missed.length > 0) {
|
|
54
|
+
transcript.push(`[not understood by the parser: ${missed.map((p) => `“${p}”`).join(', ')}. With the intent server running, an LLM would get a chance to read it.]`);
|
|
55
|
+
}
|
|
56
|
+
const reply = game.send(command);
|
|
57
|
+
transcript.push(...reply.lines);
|
|
58
|
+
awaiting = reply.awaiting;
|
|
59
|
+
over = reply.gameOver;
|
|
60
|
+
ran += 1;
|
|
61
|
+
}
|
|
62
|
+
// Over the cap, keep the opening and the ending (where the run finished), and cut the middle.
|
|
63
|
+
const shown = transcript.length > MAX_LINES
|
|
64
|
+
? [...transcript.slice(0, MAX_LINES - TAIL_LINES), `[… ${transcript.length - MAX_LINES} lines cut (over ${MAX_LINES}) …]`, ...transcript.slice(-TAIL_LINES)]
|
|
65
|
+
: transcript;
|
|
66
|
+
const s = game.state;
|
|
67
|
+
const state = {
|
|
68
|
+
room: `${world.rooms[s.currentRoom]?.name ?? s.currentRoom} (${s.currentRoom})`,
|
|
69
|
+
inventory: Object.entries(s.locations ?? {})
|
|
70
|
+
.filter(([, place]) => place === 'player')
|
|
71
|
+
.map(([id]) => world.items[id]?.name ?? id),
|
|
72
|
+
flags: Object.keys(s.flags).filter((f) => s.flags[f]).sort(),
|
|
73
|
+
score: resolved.engine.currentScore?.(world, s) ?? s.vars?.score ?? null,
|
|
74
|
+
moves: s.moveCount,
|
|
75
|
+
gameOver: s.gameOver,
|
|
76
|
+
};
|
|
77
|
+
const notes = [
|
|
78
|
+
...(commands.length > MAX_COMMANDS ? [`Only the first ${MAX_COMMANDS} commands ran.`] : []),
|
|
79
|
+
...(over && ran < run.length ? [`Game over after ${ran} of ${run.length} commands.`] : []),
|
|
80
|
+
];
|
|
81
|
+
return [...header(loaded, resolved), `Seed: ${seed}`, '', ...shown, '', ...notes, 'Final state:', '```json', JSON.stringify(state, null, 2), '```'].join('\n');
|
|
82
|
+
}
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The worker entry: runs one audit_world or play job (workerData) and posts the result back.
|
|
2
|
+
// Its stdout and stderr are piped to the server's stderr, and run.ts terminates it on the deadline.
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
4
|
+
import * as bundled from '@brass-lantern/engine';
|
|
5
|
+
import { runJob } from "./job.js";
|
|
6
|
+
parentPort?.postMessage(await runJob(workerData, bundled));
|
package/package.json
CHANGED
|
@@ -1 +1,47 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@brass-lantern/mcp",
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"author": "Todd Greco <mrballistic@gmail.com>",
|
|
5
|
+
"description": "A Model Context Protocol server for brass-lantern: lets a coding agent audit, play and read the docs for the text-adventure world it is writing.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": { "type": "git", "url": "git+https://github.com/mrballistic/brass-lantern.git", "directory": "packages/mcp" },
|
|
8
|
+
"homepage": "https://mrballistic.github.io/brass-lantern/",
|
|
9
|
+
"bugs": "https://github.com/mrballistic/brass-lantern/issues",
|
|
10
|
+
"keywords": ["text-adventure", "interactive-fiction", "mcp", "model-context-protocol", "coding-agent"],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"bin": { "brass-lantern-mcp": "./dist/cli.js" },
|
|
14
|
+
"exports": {
|
|
15
|
+
".": { "@brass-lantern/source": "./src/server.ts", "types": "./dist/server.d.ts", "default": "./dist/server.js" },
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/server.d.ts",
|
|
19
|
+
"files": ["dist", "!dist/**/*.map", "README.md", "LICENSE"],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prebuild": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
|
22
|
+
"build": "tsc -p tsconfig.build.json && tsx scripts/bundle-docs.ts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"type-check": "tsc --noEmit",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"test:coverage": "vitest run --coverage"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@brass-lantern/engine": "^2.3.0",
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
32
|
+
"jiti": "^2.7.0",
|
|
33
|
+
"zod": "^4.6.7"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^10.0.1",
|
|
37
|
+
"@types/node": "^24.12.0",
|
|
38
|
+
"eslint": "^10.12.0",
|
|
39
|
+
"globals": "^17.0.0",
|
|
40
|
+
"tsx": "^4.23.15",
|
|
41
|
+
"typescript": "~6.0.3",
|
|
42
|
+
"typescript-eslint": "^8.71.0",
|
|
43
|
+
"vitest": "^5.0.3"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": { "access": "public", "provenance": true },
|
|
46
|
+
"engines": { "node": ">=24" }
|
|
47
|
+
}
|