@brass-lantern/mcp 0.0.1-stage → 2.3.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +16 -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 +120 -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 +40 -0
- package/dist/load.d.ts +22 -0
- package/dist/load.js +105 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.js +96 -0
- package/dist/stdio.d.ts +19 -0
- package/dist/stdio.js +49 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +82 -0
- package/package.json +47 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# A two-room game
|
|
2
|
+
|
|
3
|
+
This page builds the smallest complete game, [`packages/engine/src/worlds/examples/two-rooms.ts`](https://github.com/mrballistic/brass-lantern/blob/main/packages/engine/src/worlds/examples/two-rooms.ts), from nothing. You’re home after a long time away; the door is locked, and you left the key somewhere obvious. [A test](https://github.com/mrballistic/brass-lantern/blob/main/packages/engine/tests/worlds/examples/two-rooms.test.ts) plays it to the end.
|
|
4
|
+
|
|
5
|
+
The whole game is one file, `two-rooms.ts`, holding one object. Its outline:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import type { World } from '@brass-lantern/engine';
|
|
9
|
+
|
|
10
|
+
export const twoRooms: World = {
|
|
11
|
+
// Two rooms: startRoom and rooms
|
|
12
|
+
// Things: items
|
|
13
|
+
// What happens: npcs, dialogue, flagLabels, events and endings
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Each section below shows one part of that object, in order; put them inside the braces one after another and you have the file. It’s also [shown whole](#the-whole-file) at the end. The game ships with the engine, so you can play it straight away without typing any of it: `import { twoRooms } from '@brass-lantern/engine/worlds'`.
|
|
18
|
+
|
|
19
|
+
## Two rooms
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
startRoom: 'porch',
|
|
23
|
+
rooms: {
|
|
24
|
+
porch: {
|
|
25
|
+
name: 'Front Porch',
|
|
26
|
+
description: 'A creaky porch in front of an old house. A doormat lies at your feet.',
|
|
27
|
+
exits: {
|
|
28
|
+
north: { to: 'hall', door: 'front_door' },
|
|
29
|
+
in: { to: 'hall', door: 'front_door' },
|
|
30
|
+
},
|
|
31
|
+
listExits: ['north'],
|
|
32
|
+
items: ['doormat'],
|
|
33
|
+
npcs: [],
|
|
34
|
+
onEnter: [],
|
|
35
|
+
scenery: ['front_door'],
|
|
36
|
+
},
|
|
37
|
+
hall: {
|
|
38
|
+
name: 'Hall',
|
|
39
|
+
description: 'A dusty hall that smells of old books. The front door is south.',
|
|
40
|
+
exits: {
|
|
41
|
+
south: { to: 'porch', door: 'front_door' },
|
|
42
|
+
out: { to: 'porch', door: 'front_door' },
|
|
43
|
+
},
|
|
44
|
+
listExits: ['south'],
|
|
45
|
+
items: ['side_table'],
|
|
46
|
+
npcs: [],
|
|
47
|
+
onEnter: [],
|
|
48
|
+
scenery: ['front_door'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- **`startRoom`** is where a new game begins.
|
|
54
|
+
- **`exits`** map what the player types to where it goes. A plain string (`north: 'hall'`) is enough for an open way through; here each exit is an object naming a **`door`**, an item that has to be open before anyone passes. Several labels for the same way (`north`, `in`) are normal.
|
|
55
|
+
- **`listExits`** is what the exit line shows. Without it, every label is listed.
|
|
56
|
+
- **`scenery`** lists items that are present without being *in* the room: the door belongs to both rooms, so both name it, and it’s never listed or taken.
|
|
57
|
+
- **`items`**, **`npcs`** and **`onEnter`** are required on every room, even when empty.
|
|
58
|
+
|
|
59
|
+
## Things
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
items: {
|
|
63
|
+
doormat: {
|
|
64
|
+
name: 'doormat',
|
|
65
|
+
aliases: ['mat'],
|
|
66
|
+
description: 'WELCOME, it says, unconvincingly.',
|
|
67
|
+
portable: false,
|
|
68
|
+
refusal: 'It’s nailed down. Somebody really wanted you welcome.',
|
|
69
|
+
tags: [],
|
|
70
|
+
scenery: true,
|
|
71
|
+
instead: { examine: [{ if: '!flag:found_key', then: 'find_key' }] },
|
|
72
|
+
},
|
|
73
|
+
key: { name: 'brass key', aliases: ['key'], description: 'Small, brass, a little green.', portable: true, tags: [] },
|
|
74
|
+
front_door: {
|
|
75
|
+
name: 'front door',
|
|
76
|
+
aliases: ['door'],
|
|
77
|
+
description: 'Solid oak, painted red a long time ago.',
|
|
78
|
+
portable: false,
|
|
79
|
+
tags: [],
|
|
80
|
+
door: true,
|
|
81
|
+
container: { openable: true, locked: true, key: 'key' },
|
|
82
|
+
},
|
|
83
|
+
side_table: {
|
|
84
|
+
name: 'side table',
|
|
85
|
+
aliases: ['table'],
|
|
86
|
+
description: 'A spindly side table.',
|
|
87
|
+
portable: false,
|
|
88
|
+
tags: [],
|
|
89
|
+
scenery: true,
|
|
90
|
+
surface: true,
|
|
91
|
+
contains: ['letter'],
|
|
92
|
+
},
|
|
93
|
+
letter: {
|
|
94
|
+
name: 'letter',
|
|
95
|
+
aliases: ['envelope', 'note'],
|
|
96
|
+
description: 'An envelope with your name on it, in your own handwriting.',
|
|
97
|
+
portable: true,
|
|
98
|
+
tags: [],
|
|
99
|
+
instead: { read: [{ then: 'read_letter' }] },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- **Every item has a `name`** (shown to players), a **`description`** (EXAMINE), says whether it’s **`portable`**, and has **`tags`** (labels for your own rules; `[]` is fine). **`aliases`** are other words for it. The world’s `items` is required too.
|
|
105
|
+
- **`scenery: true`** on an item keeps it out of the room’s list of things (the description already mentions the mat). It’s still in the room’s `items`, so it can be examined. **`refusal`** is what TAKE says to something that isn’t portable.
|
|
106
|
+
- **The door** is an item with `door: true` and a `container` block: it can be opened, it starts locked, and the `key` item unlocks it. UNLOCK DOOR WITH KEY and OPEN DOOR work with no rules at all.
|
|
107
|
+
- **The mat hides the key.** An **`instead`** rule replaces what a verb normally does. A rule is `{ if, then }`: `if` is a [condition](https://mrballistic.github.io/brass-lantern/reference/conditions-and-events#conditions) (`!flag:found_key`, “the flag `found_key` isn’t set”), and `then` names an event to run. EXAMINE MAT runs the `find_key` event the first time; once `found_key` is set the rule’s `if` fails and EXAMINE prints the description as usual.
|
|
108
|
+
- **The key starts nowhere.** It isn’t in any room’s `items`, so it’s offstage until an event moves it.
|
|
109
|
+
- **The side table is a `surface`**: what’s on it is always visible and in reach, which is where the hall’s “Sitting on the side table is:” comes from. `contains` puts the letter there at the start.
|
|
110
|
+
- **The letter ends the game** with an `instead` rule on READ.
|
|
111
|
+
|
|
112
|
+
## What happens
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
npcs: {},
|
|
116
|
+
dialogue: {},
|
|
117
|
+
flagLabels: {},
|
|
118
|
+
events: {
|
|
119
|
+
intro: ['You’re back at the old house at last. You’re sure you left the key somewhere obvious.'],
|
|
120
|
+
find_key: ['You lift a corner of the mat. Underneath: a brass key.', { set: 'found_key' }, { move: 'key', to: 'porch' }],
|
|
121
|
+
read_letter: ['“Dear me,” it begins. “If you’re reading this, you remembered the mat. Welcome home.”', { end: 'home' }],
|
|
122
|
+
},
|
|
123
|
+
endings: {
|
|
124
|
+
home: { lines: ['✨ You’re home.'], footer: ['Type RESTART to play again.'] },
|
|
125
|
+
},
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- **`events`** are named lists of steps. A string prints; an object is an [effect](https://mrballistic.github.io/brass-lantern/reference/conditions-and-events#events-and-effects). `find_key` prints a line, sets a flag and moves the key onto the porch.
|
|
129
|
+
- **`intro`** plays when a new game starts.
|
|
130
|
+
- **`end`** plays one of the world’s **`endings`**: its lines, then the footer, and the game is over. A line starting with an emoji (✨) is shown as an event; [How lines are styled](https://mrballistic.github.io/brass-lantern/reference/conditions-and-events#how-lines-are-styled) has the rules.
|
|
131
|
+
- **`npcs`, `dialogue` and `flagLabels`** are required. This game has no people, and its flags are set by effects rather than [bracket lines](https://mrballistic.github.io/brass-lantern/reference/conditions-and-events#bracket-lines-the-older-form), so they’re empty.
|
|
132
|
+
|
|
133
|
+
## The whole file
|
|
134
|
+
|
|
135
|
+
The three parts, assembled. The sections above leave out only what goes around them: the import, a comment, and `export const twoRooms: World = {` with its closing `};`. (The file in the repo also has `// #region` comments marking the three parts for this page. They change nothing, and they’re left out here.)
|
|
136
|
+
|
|
137
|
+
**two-rooms.ts**
|
|
138
|
+
```ts
|
|
139
|
+
import type { World } from '@brass-lantern/engine';
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The smallest complete game: two rooms, a locked door, a hidden key and a
|
|
143
|
+
* way to win. Built step by step in docs/guide/building-worlds/two-rooms.md;
|
|
144
|
+
* tests/worlds/examples/two-rooms.test.ts plays it.
|
|
145
|
+
*/
|
|
146
|
+
export const twoRooms: World = {
|
|
147
|
+
startRoom: 'porch',
|
|
148
|
+
rooms: {
|
|
149
|
+
porch: {
|
|
150
|
+
name: 'Front Porch',
|
|
151
|
+
description: 'A creaky porch in front of an old house. A doormat lies at your feet.',
|
|
152
|
+
exits: {
|
|
153
|
+
north: { to: 'hall', door: 'front_door' },
|
|
154
|
+
in: { to: 'hall', door: 'front_door' },
|
|
155
|
+
},
|
|
156
|
+
listExits: ['north'],
|
|
157
|
+
items: ['doormat'],
|
|
158
|
+
npcs: [],
|
|
159
|
+
onEnter: [],
|
|
160
|
+
scenery: ['front_door'],
|
|
161
|
+
},
|
|
162
|
+
hall: {
|
|
163
|
+
name: 'Hall',
|
|
164
|
+
description: 'A dusty hall that smells of old books. The front door is south.',
|
|
165
|
+
exits: {
|
|
166
|
+
south: { to: 'porch', door: 'front_door' },
|
|
167
|
+
out: { to: 'porch', door: 'front_door' },
|
|
168
|
+
},
|
|
169
|
+
listExits: ['south'],
|
|
170
|
+
items: ['side_table'],
|
|
171
|
+
npcs: [],
|
|
172
|
+
onEnter: [],
|
|
173
|
+
scenery: ['front_door'],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
items: {
|
|
178
|
+
doormat: {
|
|
179
|
+
name: 'doormat',
|
|
180
|
+
aliases: ['mat'],
|
|
181
|
+
description: 'WELCOME, it says, unconvincingly.',
|
|
182
|
+
portable: false,
|
|
183
|
+
refusal: 'It’s nailed down. Somebody really wanted you welcome.',
|
|
184
|
+
tags: [],
|
|
185
|
+
scenery: true,
|
|
186
|
+
instead: { examine: [{ if: '!flag:found_key', then: 'find_key' }] },
|
|
187
|
+
},
|
|
188
|
+
key: { name: 'brass key', aliases: ['key'], description: 'Small, brass, a little green.', portable: true, tags: [] },
|
|
189
|
+
front_door: {
|
|
190
|
+
name: 'front door',
|
|
191
|
+
aliases: ['door'],
|
|
192
|
+
description: 'Solid oak, painted red a long time ago.',
|
|
193
|
+
portable: false,
|
|
194
|
+
tags: [],
|
|
195
|
+
door: true,
|
|
196
|
+
container: { openable: true, locked: true, key: 'key' },
|
|
197
|
+
},
|
|
198
|
+
side_table: {
|
|
199
|
+
name: 'side table',
|
|
200
|
+
aliases: ['table'],
|
|
201
|
+
description: 'A spindly side table.',
|
|
202
|
+
portable: false,
|
|
203
|
+
tags: [],
|
|
204
|
+
scenery: true,
|
|
205
|
+
surface: true,
|
|
206
|
+
contains: ['letter'],
|
|
207
|
+
},
|
|
208
|
+
letter: {
|
|
209
|
+
name: 'letter',
|
|
210
|
+
aliases: ['envelope', 'note'],
|
|
211
|
+
description: 'An envelope with your name on it, in your own handwriting.',
|
|
212
|
+
portable: true,
|
|
213
|
+
tags: [],
|
|
214
|
+
instead: { read: [{ then: 'read_letter' }] },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
npcs: {},
|
|
219
|
+
dialogue: {},
|
|
220
|
+
flagLabels: {},
|
|
221
|
+
events: {
|
|
222
|
+
intro: ['You’re back at the old house at last. You’re sure you left the key somewhere obvious.'],
|
|
223
|
+
find_key: ['You lift a corner of the mat. Underneath: a brass key.', { set: 'found_key' }, { move: 'key', to: 'porch' }],
|
|
224
|
+
read_letter: ['“Dear me,” it begins. “If you’re reading this, you remembered the mat. Welcome home.”', { end: 'home' }],
|
|
225
|
+
},
|
|
226
|
+
endings: {
|
|
227
|
+
home: { lines: ['✨ You’re home.'], footer: ['Type RESTART to play again.'] },
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Play it
|
|
233
|
+
|
|
234
|
+
You need Node 24 or later. In a new folder:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npm init -y
|
|
238
|
+
npm pkg set type=module
|
|
239
|
+
npm i @brass-lantern/engine
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Save the file above as `two-rooms.ts`, and this beside it as `play.ts`:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { createGame } from '@brass-lantern/engine';
|
|
246
|
+
import { twoRooms } from './two-rooms.ts';
|
|
247
|
+
|
|
248
|
+
const game = createGame(twoRooms);
|
|
249
|
+
console.log(game.opening.join('\n'));
|
|
250
|
+
for (const command of ['north', 'examine mat', 'take key', 'unlock door with key', 'open door', 'north', 'take letter', 'read letter']) {
|
|
251
|
+
console.log(`> ${command}`);
|
|
252
|
+
console.log(game.send(command).lines.join('\n'));
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
node play.ts
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Node 24 and later run TypeScript files as they are, no compiler needed (`npx tsx play.ts` works too). `createGame` runs a world with no browser: `game.opening` is what the game says before the first command, and `game.send(line)` takes whatever a player would type and answers with its `lines`. No server or API key is needed. To skip the typing, import the shipped copy instead: `import { twoRooms } from '@brass-lantern/engine/worlds'`.
|
|
261
|
+
|
|
262
|
+
It prints this, word for word (the test checks the same transcript):
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
You’re back at the old house at last. You’re sure you left the key somewhere obvious.
|
|
266
|
+
📍 Front Porch
|
|
267
|
+
A creaky porch in front of an old house. A doormat lies at your feet.
|
|
268
|
+
Exits: north.
|
|
269
|
+
> north
|
|
270
|
+
The front door is closed.
|
|
271
|
+
> examine mat
|
|
272
|
+
You lift a corner of the mat. Underneath: a brass key.
|
|
273
|
+
> take key
|
|
274
|
+
Taken: brass key.
|
|
275
|
+
> unlock door with key
|
|
276
|
+
Unlocked.
|
|
277
|
+
> open door
|
|
278
|
+
Opened.
|
|
279
|
+
> north
|
|
280
|
+
📍 Hall
|
|
281
|
+
A dusty hall that smells of old books. The front door is south.
|
|
282
|
+
Sitting on the side table is:
|
|
283
|
+
A letter
|
|
284
|
+
Exits: south.
|
|
285
|
+
> take letter
|
|
286
|
+
Taken: letter.
|
|
287
|
+
> read letter
|
|
288
|
+
“Dear me,” it begins. “If you’re reading this, you remembered the mat. Welcome home.”
|
|
289
|
+
✨ You’re home.
|
|
290
|
+
Type RESTART to play again.
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**In the browser**, the same world plays in the terminal from `@brass-lantern/vue`: a styled window, like an old CRT screen, with the typing prompt, saves and the rest of what a player sees. The shortest way there is a new [Vite](https://vite.dev) app:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
npm create vite@latest my-game -- --template vanilla-ts
|
|
297
|
+
cd my-game
|
|
298
|
+
npm i @brass-lantern/engine @brass-lantern/vue vue pinia
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The template already has an `index.html` with `<div id="app">` that loads `src/main.ts`. Copy `two-rooms.ts` into `src/`, then replace everything in `src/main.ts` with:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { mountGame } from '@brass-lantern/vue';
|
|
305
|
+
import '@brass-lantern/vue/style.css';
|
|
306
|
+
import './style.css';
|
|
307
|
+
import { twoRooms } from './two-rooms.ts';
|
|
308
|
+
|
|
309
|
+
mountGame('#app', {
|
|
310
|
+
cartridges: [{ kind: 'world', id: 'two-rooms', title: 'TWO ROOMS', world: twoRooms }],
|
|
311
|
+
storagePrefix: 'two-rooms',
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
and everything in `src/style.css` with:
|
|
316
|
+
|
|
317
|
+
```css
|
|
318
|
+
html, body, #app { height: 100%; margin: 0; overflow: hidden; }
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
npm run dev
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Open the address it prints and the game boots. What the options mean:
|
|
326
|
+
|
|
327
|
+
- **`cartridges`** are the games on offer. A cartridge with `kind: 'world'` is a world like this one; `id` names it (in its saves, for one), and `title` is what the menu shows. With one cartridge there’s no menu: the terminal boots straight into it.
|
|
328
|
+
- **`storagePrefix`** names everything the game keeps in the browser (saves, the player’s theme), so two games on one site don’t share saves.
|
|
329
|
+
- **The game fills `#app`**, which is why `style.css` gives it the whole window.
|
|
330
|
+
|
|
331
|
+
Vite tells TypeScript what a `.css` import is (the template’s `vite/client` types), and it accepts the `.ts` ending on `./two-rooms.ts`, as Node does. Outside Vite, [Using the library](https://mrballistic.github.io/brass-lantern/guide/using-the-library#put-a-game-on-a-page) says what to declare for the stylesheet, and covers themes and the rest of the options.
|
|
332
|
+
|
|
333
|
+
Players won’t type exactly the commands above. “Look under the mat” or “use the key on the door” aren’t commands the engine can act on by itself, so with the [intent server](https://mrballistic.github.io/brass-lantern/guide/intent-server) running they can be mapped onto EXAMINE MAT and UNLOCK DOOR WITH KEY. Without it (as with `createGame`, which never uses it) the player gets a nudge to rephrase.
|
|
334
|
+
|
|
335
|
+
## Where next
|
|
336
|
+
|
|
337
|
+
- Give the game a person to talk to, hints and a score: [Snack Attack](https://mrballistic.github.io/brass-lantern/guide/your-first-world) shows all three.
|
|
338
|
+
- Make the hall dark until you find a lamp: [darkness](https://mrballistic.github.io/brass-lantern/guide/building-worlds/recipes#darkness-and-death).
|
|
339
|
+
- Every field, with its default: the [world schema](https://mrballistic.github.io/brass-lantern/reference/world-schema).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Coding agents
|
|
2
|
+
|
|
3
|
+
Two ways to give a coding agent (Claude Code, Cursor, Codex and the like) what it needs to build a Brass Lantern world: the docs as one file, and a local MCP server that can also audit and play the world it writes.
|
|
4
|
+
|
|
5
|
+
## The docs as one file
|
|
6
|
+
|
|
7
|
+
The site publishes its documentation as markdown, built from the same pages you are reading.
|
|
8
|
+
|
|
9
|
+
- [llms.txt](https://mrballistic.github.io/brass-lantern/llms.txt) is the index: a one-line summary and a link to every page. Each page also has a `.md` copy beside it (add `.md` to its address). Use this when the agent should fetch only the pages it needs.
|
|
10
|
+
- [llms-full.txt](https://mrballistic.github.io/brass-lantern/llms-full.txt) is every page in one file, with the example worlds’ code inlined. Use this when the agent can take it all in one read: it then has the schema, the conditions, the effects and the recipes together.
|
|
11
|
+
|
|
12
|
+
## The MCP server
|
|
13
|
+
|
|
14
|
+
`@brass-lantern/mcp` is a local [Model Context Protocol](https://modelcontextprotocol.io) server. It lets an agent close the loop on a world it is writing: **write** the world file, **audit** it, **play** a few commands to see how it actually reads, **fix** what turned up, and repeat. It also serves this version’s docs, so the agent reads the schema that matches the engine.
|
|
15
|
+
|
|
16
|
+
### Install
|
|
17
|
+
|
|
18
|
+
In Claude Code:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
claude mcp add brass-lantern -- npx -y @brass-lantern/mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For any client that takes JSON config:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"brass-lantern": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "@brass-lantern/mcp"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Node 24 or later. World paths in tool calls are relative to the directory the client starts the server in (normally your project), or absolute. Worlds must be inside that directory or one you allow with `--root <dir>`, which you can repeat and which is relative to the same directory:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
claude mcp add brass-lantern -- npx -y @brass-lantern/mcp --root ../shared-worlds
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### The tools
|
|
44
|
+
|
|
45
|
+
**`audit_world`** checks a world file the way the engine’s `auditWorld` does: exits that go nowhere, conditions that don’t parse, effects naming things that don’t exist, unmapped flag labels and more.
|
|
46
|
+
|
|
47
|
+
Inputs: `path` (a `.ts`, `.mts`, `.js` or `.mjs` module) and optionally `export` (which export is the world; by default the default export, else the only world-shaped one).
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{ "path": "src/worlds/office.ts" }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
World: default in /project/src/worlds/office.ts
|
|
55
|
+
Engine: @brass-lantern/engine 2.3.0 (the project’s)
|
|
56
|
+
Size: 1 room, 0 items, 0 characters, 0 events
|
|
57
|
+
|
|
58
|
+
1 problem:
|
|
59
|
+
- room hall: exit north goes nowhere (“nowhere”)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**`play`** starts a fresh game and types each command as a player would, with no LLM. It is stateless: every call begins at the start, and the game is seeded (default seed 1, or pass `seed`) so a run repeats exactly. To continue a session, send the whole command list again. At most 500 commands run per call, and a transcript over 2,000 lines keeps its opening and its last 200 lines, with a note where the middle was cut. The game stops when it ends.
|
|
63
|
+
|
|
64
|
+
Inputs: `path`, optionally `export`, `commands` (the lines to type, in order) and optionally `seed`.
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{ "path": "src/worlds/office.ts", "commands": ["open drawer", "take key", "north"] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
````text
|
|
71
|
+
World: default in /project/src/worlds/office.ts
|
|
72
|
+
Engine: @brass-lantern/engine 2.3.0 (the project’s)
|
|
73
|
+
Seed: 1
|
|
74
|
+
|
|
75
|
+
…the opening…
|
|
76
|
+
> open drawer
|
|
77
|
+
You open the drawer. Inside is a brass key.
|
|
78
|
+
> take key
|
|
79
|
+
Taken.
|
|
80
|
+
> pull the lever with feeling
|
|
81
|
+
[not understood by the parser: “pull the lever with feeling”. With the intent server running, an LLM would get a chance to read it.]
|
|
82
|
+
…
|
|
83
|
+
|
|
84
|
+
Final state:
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"room": "Lobby (lobby)",
|
|
88
|
+
"inventory": ["brass key"],
|
|
89
|
+
"flags": [],
|
|
90
|
+
"score": 0,
|
|
91
|
+
"moves": 2,
|
|
92
|
+
"gameOver": false
|
|
93
|
+
}
|
|
94
|
+
````
|
|
95
|
+
|
|
96
|
+
The “not understood by the parser” note marks input the regex parser can’t read. A real player with the [intent server](./intent-server) would have it sent to the LLM; here nothing is, so treat the note as “this phrasing needs a verb, an alias or a rule” rather than as a bug. The final state includes `score`, from the engine’s `currentScore`. Notes (such as “Game over after 3 of 5 commands.”) come in a blank-line-separated block just before “Final state:”. Two things depend on the engine in use: a project engine older than 2.3.0 has no `currentScore`, so `score` comes from the world’s `score` variable, or is `null` if the world keeps none; and an engine without the parser helpers skips the “not understood” note. Neither is a fault in the world.
|
|
97
|
+
|
|
98
|
+
**`docs`** serves the documentation bundled with the server. With no arguments it returns the index of pages; with `page` (such as `reference/world-schema`) one page’s markdown; with `query` (such as `timers fuses`) the best-matching sections. If you give both, `page` wins.
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{ "query": "timers fuses" }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
…the best-matching sections, each with its page path…
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Which engine
|
|
109
|
+
|
|
110
|
+
The server audits and plays a world with the engine the world’s own project uses, so a project on 2.2 is checked by 2.2’s rules. It looks for `@brass-lantern/engine` from the world file. If the project has none, the server uses the engine bundled with it, and the reply says “bundled with the MCP server” without further comment. If the project’s engine is found but can’t be used (it can’t be resolved or fails to import, or lacks `createGame` or `auditWorld`), the server falls back to the bundled one and adds a note saying why. Every reply states the engine version and whose it is, so you always know what you were checked against.
|
|
111
|
+
|
|
112
|
+
A world file is re-read on every call, so an edit shows on the next one. That covers `.ts` and `.mts` worlds and the project files they import (loaded with no module cache), and `.mjs` and `.js` worlds along with the project files they import. Packages a world imports stay cached and are not reloaded.
|
|
113
|
+
|
|
114
|
+
### Safety and limits
|
|
115
|
+
|
|
116
|
+
The server **runs the world module it is pointed at**, with your permissions, the same way your test runner runs your tests. Point it at code you trust. It speaks only over local stdio and makes no network calls of its own. A world file is only loaded when it is inside the directory the server started in or one you passed with `--root`, compared after resolving symlinks; anything else is refused with a message naming `--root`. That check confines which world file is loaded, not what the module then imports or does: any module inside an allowed directory can be run. It is not a sandbox.
|
|
117
|
+
|
|
118
|
+
A world’s console output (`console.log`, `dir`, `table` and the rest) goes to stderr, so it can’t break the protocol.
|
|
119
|
+
|
|
120
|
+
Two limits to know: a world whose script loops forever hangs the server until the client restarts it, and a world that calls `process.exit()` ends the server the same way. There is no per-call timeout yet.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Deploying
|
|
2
|
+
|
|
3
|
+
A Brass Lantern game is a static site, plus the intent server if you want loose phrasing understood. If you use the packages in your own app ([Using the library](https://mrballistic.github.io/brass-lantern/guide/using-the-library)), deploy your app the way you already do and mount `intentRoute` in your backend; this page is about the repo’s demo site and server.
|
|
4
|
+
|
|
5
|
+
## Static only
|
|
6
|
+
|
|
7
|
+
`npm run build` writes `apps/site/dist/`, which any static host can serve: GitHub Pages, Netlify, an S3 bucket, a web server. Without an intent server the game still plays on the regex parser alone; players just get a “didn’t understand” reply where the server would have helped.
|
|
8
|
+
|
|
9
|
+
To serve from a subpath, set the base at build time:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
VITE_BASE=/my-game/ npm run build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That’s how the [demo](https://mrballistic.github.io/brass-lantern/demo/) is published: `.github/workflows/pages.yml` builds these docs and the demo together and deploys them to GitHub Pages on every push to `main`.
|
|
16
|
+
|
|
17
|
+
## With the intent server
|
|
18
|
+
|
|
19
|
+
The SPA calls `/api/parse-intent` on its own origin, so serve both from one host and proxy `/api/` to the server:
|
|
20
|
+
|
|
21
|
+
```apache
|
|
22
|
+
# Apache (mod_proxy, mod_proxy_http)
|
|
23
|
+
DocumentRoot /var/www/my-game
|
|
24
|
+
<Directory /var/www/my-game>
|
|
25
|
+
FallbackResource /index.html
|
|
26
|
+
</Directory>
|
|
27
|
+
ProxyPass /api/ http://127.0.0.1:3001/api/
|
|
28
|
+
ProxyPassReverse /api/ http://127.0.0.1:3001/api/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```nginx
|
|
32
|
+
# nginx
|
|
33
|
+
root /var/www/my-game;
|
|
34
|
+
location / { try_files $uri /index.html; }
|
|
35
|
+
location /api/ { proxy_pass http://127.0.0.1:3001; }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Build and run the server:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm ci && npm run build -w @brass-lantern/server
|
|
42
|
+
GEMINI_KEY=… NODE_ENV=production node packages/server/dist/index.js
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Install the dev dependencies**
|
|
46
|
+
The repo’s server imports `express`, which this repo keeps as a dev dependency (the published package treats it as an optional peer). Don’t install with `npm ci --omit=dev` before running `node packages/server/dist/index.js`: the server won’t start. Install everything, or write your own small app around `intentRoute` and install `express` yourself.
|
|
47
|
+
|
|
48
|
+
In production, use a process manager (systemd, pm2, a container) and keep the key in an environment file only that service can read. The server drains in-flight requests on SIGTERM, so rolling restarts don’t drop players’ commands.
|
|
49
|
+
|
|
50
|
+
## Suggestions from running one
|
|
51
|
+
|
|
52
|
+
- **Cache hashed assets forever and `index.html` never**, and upload the assets before `index.html`. Then a deploy never serves a page whose scripts haven’t arrived yet.
|
|
53
|
+
- **Deploy as a user that can only write the site**, not an admin. A CI deploy key is as powerful as the account it logs into.
|
|
54
|
+
- **Pin your CI actions to commit SHAs**, and keep deploy secrets in a deployment environment that only release tags can use.
|
|
55
|
+
- **Check that the key never reaches the bundle.** A CI step that greps `dist/` for `AIza` fails the build if it ever does.
|
|
56
|
+
- **Analytics (the demo site):** set `VITE_GA_MEASUREMENT_ID` at build time to turn on its GA4 reporting. The consent banner appears only then. In your own app, analytics are whatever you wire to `GameOptions.analytics`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Brass Lantern is a small engine for classic parser text adventures (*go north*, *take lamp*, *give the mug to Gary*) that runs in the browser inside a CRT terminal. You write a game as data; the engine runs it.
|
|
4
|
+
|
|
5
|
+
It also plays **Z-machine story files**, the format Infocom’s games shipped in, in the same terminal.
|
|
6
|
+
|
|
7
|
+
It comes with five cartridges: **Snack Attack**, a three-room world that exists to show the engine off, **Zork I, II and III**, and **Zork I rebuilt natively** on the engine. [Play them](https://mrballistic.github.io/brass-lantern/demo/), then come back. For a full-length game built on the engine, try [Office Space: The Text Adventure](https://initech.mrballistic.com).
|
|
8
|
+
|
|
9
|
+
## Use it in your app
|
|
10
|
+
|
|
11
|
+
Brass Lantern is on npm as three library packages (the engine, a Vue terminal and an optional intent server) plus an MCP server for coding agents. [Using the library](https://mrballistic.github.io/brass-lantern/guide/using-the-library) covers installing them, mounting a game, themes, running headless in Node and wiring the intent server.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @brass-lantern/engine @brass-lantern/vue vue pinia
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Run the demo from the repo
|
|
18
|
+
|
|
19
|
+
You need Node 24 or later.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/mrballistic/brass-lantern.git
|
|
23
|
+
cd brass-lantern
|
|
24
|
+
npm install
|
|
25
|
+
npm run dev # http://localhost:5173
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The terminal boots to the cartridge menu; type `1` for Snack Attack, `2`–`4` for Zork I–III, or `5` for the native Zork I. All are fully playable offline: the regex parser handles every command the engine knows, and Zork has its own parser. Loose phrasing (“make that thing stop beeping”) needs the optional [intent server](https://mrballistic.github.io/brass-lantern/guide/intent-server).
|
|
29
|
+
|
|
30
|
+
## What’s in the box
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
packages/
|
|
34
|
+
engine/ @brass-lantern/engine: the engine, the regex parser, fuzzy matching,
|
|
35
|
+
line styling, createGame, auditWorld, the story-file runtime
|
|
36
|
+
(zmachine/), and the bundled worlds (worlds/)
|
|
37
|
+
vue/ @brass-lantern/vue: the terminal, the boot sequence, the game stores,
|
|
38
|
+
themes, mountGame, and the CRT stylesheet
|
|
39
|
+
server/ @brass-lantern/server: the intent server (parseIntent, intentRoute),
|
|
40
|
+
plus the small Express app the repo runs (src/index.ts)
|
|
41
|
+
apps/
|
|
42
|
+
site/ the demo: app.config.ts lists the cartridges, main.ts mounts the game,
|
|
43
|
+
apps/site/public/stories/ holds the Zork trilogy and its licenses
|
|
44
|
+
docs/ this site (npm run docs:dev)
|
|
45
|
+
scripts/ the consumer smoke test, the glkapi vendoring
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Each package has its own tests; `packages/engine/src/types/world.ts` is the world schema, with a comment on every field.
|
|
49
|
+
|
|
50
|
+
## Make it yours
|
|
51
|
+
|
|
52
|
+
In your own app, the cartridges are just the `cartridges` you pass to `mountGame` ([Using the library](https://mrballistic.github.io/brass-lantern/guide/using-the-library)). In the repo, everything that makes the demo *a particular game collection* is in `apps/site/src/app.config.ts`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export const cartridges: Cartridge[] = [
|
|
56
|
+
{ kind: 'world', id: 'snack-attack', title: 'SNACK ATTACK', world: tutorial },
|
|
57
|
+
{ kind: 'zcode', id: 'zork1', title: 'ZORK I', story: 'stories/zork1.z3', format: 'Z-machine v3' },
|
|
58
|
+
{ kind: 'zcode', id: 'zork2', title: 'ZORK II', story: 'stories/zork2.z3', format: 'Z-machine v3' },
|
|
59
|
+
{ kind: 'zcode', id: 'zork3', title: 'ZORK III', story: 'stories/zork3.z3', format: 'Z-machine v3' },
|
|
60
|
+
{ kind: 'world', id: 'zork1-native', title: 'ZORK I · NATIVE', world: zork1 },
|
|
61
|
+
];
|
|
62
|
+
export const appName = 'BRASS LANTERN';
|
|
63
|
+
export const storagePrefix = 'brass-lantern';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Add your own world as a cartridge (and remove the others if you like: with one cartridge there’s no menu). [Building worlds](https://mrballistic.github.io/brass-lantern/guide/building-worlds/) walks through writing one, from two rooms up; [Playing story files](https://mrballistic.github.io/brass-lantern/guide/z-machine) covers Z-machine cartridges.
|
|
67
|
+
|
|
68
|
+
## For coding agents
|
|
69
|
+
|
|
70
|
+
The docs also come as plain markdown for agents and LLM tools. [llms.txt](https://mrballistic.github.io/brass-lantern/llms.txt) indexes every page, each page has a `.md` copy beside it, and [llms-full.txt](https://mrballistic.github.io/brass-lantern/llms-full.txt) is all of it in one file, with the example worlds’ code inlined. Point an agent at llms-full.txt and it has the schema, the conditions, the effects and the recipes in one read. There is also a local MCP server that lets an agent audit and play the world it is writing; [Coding agents](https://mrballistic.github.io/brass-lantern/guide/coding-agents) covers both.
|
|
71
|
+
|
|
72
|
+
Working on Brass Lantern itself? [CONTRIBUTING.md](https://github.com/mrballistic/brass-lantern/blob/main/CONTRIBUTING.md) has the checks to run before a pull request.
|