jsbeeb-mcp 0.1.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 +109 -0
- package/package.json +33 -0
- package/server.js +407 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# jsbeeb-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that
|
|
4
|
+
exposes a headless [BBC Micro emulator](https://github.com/mattgodbolt/jsbeeb)
|
|
5
|
+
to AI assistants (Claude, Cursor, etc.).
|
|
6
|
+
|
|
7
|
+
Write a BASIC program, run it, get the text output and a screenshot — all
|
|
8
|
+
without opening a browser.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Running
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# via npx (no install needed)
|
|
20
|
+
npx jsbeeb-mcp
|
|
21
|
+
|
|
22
|
+
# or if installed globally
|
|
23
|
+
npm install -g jsbeeb-mcp
|
|
24
|
+
jsbeeb-mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Connecting to Claude Desktop
|
|
28
|
+
|
|
29
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"jsbeeb": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["jsbeeb-mcp"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Tools
|
|
43
|
+
|
|
44
|
+
### `run_basic` _(convenience — no session management needed)_
|
|
45
|
+
|
|
46
|
+
One-shot: boot a BBC Micro, load a BASIC program, run it, return text output
|
|
47
|
+
and an optional screenshot, then clean up.
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"source": "10 PRINT \"HELLO WORLD\"\n20 GOTO 10",
|
|
52
|
+
"model": "B-DFS1.2",
|
|
53
|
+
"timeout_secs": 10,
|
|
54
|
+
"screenshot": true
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Session-based tools
|
|
59
|
+
|
|
60
|
+
For multi-step interaction (debugging, iterative development):
|
|
61
|
+
|
|
62
|
+
| Tool | Description |
|
|
63
|
+
| ------------------ | -------------------------------------------------------------- |
|
|
64
|
+
| `create_machine` | Boot a BBC Micro (B or Master), returns a `session_id` |
|
|
65
|
+
| `destroy_machine` | Free a session |
|
|
66
|
+
| `load_basic` | Tokenise + load BBC BASIC source into PAGE |
|
|
67
|
+
| `type_input` | Type text at the current keyboard prompt (RETURN is automatic) |
|
|
68
|
+
| `run_until_prompt` | Run until BASIC/OS prompt, return captured screen text |
|
|
69
|
+
| `screenshot` | Capture the current screen as a PNG image |
|
|
70
|
+
| `read_memory` | Read bytes from the memory map (with hex dump) |
|
|
71
|
+
| `write_memory` | Poke bytes into memory |
|
|
72
|
+
| `read_registers` | Get 6502 CPU registers (PC, A, X, Y, S, P) |
|
|
73
|
+
| `run_for_cycles` | Run exactly N 2MHz CPU cycles |
|
|
74
|
+
| `load_disc` | Load an `.ssd`/`.dsd` disc image into drive 0 |
|
|
75
|
+
|
|
76
|
+
## What works
|
|
77
|
+
|
|
78
|
+
- ✅ BBC BASIC programs (tokenised and loaded directly into memory)
|
|
79
|
+
- ✅ Text output capture (position, colour, mode)
|
|
80
|
+
- ✅ Screenshots (real Video chip output → PNG via `sharp`)
|
|
81
|
+
- ✅ Memory read/write
|
|
82
|
+
- ✅ CPU register inspection
|
|
83
|
+
- ✅ BBC B and Master models
|
|
84
|
+
- ✅ Multiple concurrent sessions
|
|
85
|
+
- ✅ Disc image loading (`.ssd`/`.dsd`)
|
|
86
|
+
|
|
87
|
+
## Known limitations
|
|
88
|
+
|
|
89
|
+
- **Boot text**: the VDU capture hook is installed after the initial boot
|
|
90
|
+
completes, so the OS startup banner isn't captured. Everything after the
|
|
91
|
+
first `>` prompt is captured.
|
|
92
|
+
- **No assembler built in**: to run machine code, poke it via `write_memory`
|
|
93
|
+
and `CALL` it from BASIC, or use the BBC's own inline assembler in BASIC.
|
|
94
|
+
- **Sound**: the sound chip runs but produces no audio output (headless mode).
|
|
95
|
+
|
|
96
|
+
## Architecture
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
server.js # MCP server — tool definitions, session store
|
|
100
|
+
examples/ # Standalone scripts demonstrating MachineSession directly
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`MachineSession` lives in jsbeeb itself (`src/machine-session.js`) and is
|
|
104
|
+
imported here as `jsbeeb/machine-session`. It wraps jsbeeb's `TestMachine`
|
|
105
|
+
with a real `Video` instance (full video chip into a 1024×625 RGBA
|
|
106
|
+
framebuffer), VDU text capture, and screenshot support via `sharp`.
|
|
107
|
+
|
|
108
|
+
Framebuffer snapshots are taken inside the `paint_ext` vsync callback (before
|
|
109
|
+
the buffer is cleared), ensuring screenshots always show a complete frame.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jsbeeb-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for jsbeeb — lets AI assistants control a headless BBC Micro emulator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jsbeeb-mcp": "./server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"server.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node server.js",
|
|
16
|
+
"test": "node test-mcp-client.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
23
|
+
"jsbeeb": "^1.3.2",
|
|
24
|
+
"sharp": "^0.34.5"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/mattgodbolt/jsbeeb-mcp.git"
|
|
29
|
+
},
|
|
30
|
+
"keywords": ["bbc-micro", "mcp", "emulator", "jsbeeb", "ai"],
|
|
31
|
+
"author": "Matt Godbolt",
|
|
32
|
+
"license": "GPL-3.0-or-later"
|
|
33
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* jsbeeb MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes a headless BBC Micro emulator to AI assistants via the Model
|
|
6
|
+
* Context Protocol. Start it with:
|
|
7
|
+
*
|
|
8
|
+
* node server.js
|
|
9
|
+
*
|
|
10
|
+
* and connect it from Claude Desktop, Cursor, or any MCP-compatible client
|
|
11
|
+
* by adding it to mcp_servers in the client config.
|
|
12
|
+
*
|
|
13
|
+
* Capabilities:
|
|
14
|
+
* - Boot a BBC B or BBC Master
|
|
15
|
+
* - Load and run BBC BASIC programs
|
|
16
|
+
* - Type at the keyboard
|
|
17
|
+
* - Capture text output
|
|
18
|
+
* - Take screenshots (PNG, base64-encoded)
|
|
19
|
+
* - Read/write memory
|
|
20
|
+
* - Inspect CPU registers
|
|
21
|
+
* - Persistent sessions (multiple machines at once)
|
|
22
|
+
* - One-shot `run_basic` convenience tool (no session management needed)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { MachineSession } from "jsbeeb/machine-session";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Session store
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const sessions = new Map(); // sessionId → MachineSession
|
|
35
|
+
|
|
36
|
+
function requireSession(sessionId) {
|
|
37
|
+
const s = sessions.get(sessionId);
|
|
38
|
+
if (!s) throw new Error(`No session with id "${sessionId}". Call create_machine first.`);
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function newSessionId() {
|
|
43
|
+
return crypto.randomUUID();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// MCP server
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const server = new McpServer({
|
|
51
|
+
name: "jsbeeb",
|
|
52
|
+
version: "0.1.0",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Tool: create_machine
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
server.tool(
|
|
60
|
+
"create_machine",
|
|
61
|
+
"Boot a BBC Micro emulator and return a session ID for use with all other tools. " +
|
|
62
|
+
"The machine runs until the BASIC prompt before this call returns.",
|
|
63
|
+
{
|
|
64
|
+
model: z
|
|
65
|
+
.enum(["B-DFS1.2", "B-DFS2.26", "Master", "Master-MOS3.20"])
|
|
66
|
+
.default("B-DFS1.2")
|
|
67
|
+
.describe("BBC Micro model to emulate"),
|
|
68
|
+
boot_timeout_secs: z.number().default(30).describe("Max seconds of emulated time to wait for the boot prompt"),
|
|
69
|
+
},
|
|
70
|
+
async ({ model, boot_timeout_secs }) => {
|
|
71
|
+
const session = new MachineSession(model);
|
|
72
|
+
await session.initialise();
|
|
73
|
+
const bootOutput = await session.boot(boot_timeout_secs);
|
|
74
|
+
const id = newSessionId();
|
|
75
|
+
sessions.set(id, session);
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: JSON.stringify({
|
|
81
|
+
session_id: id,
|
|
82
|
+
model,
|
|
83
|
+
boot_output: bootOutput,
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Tool: destroy_machine
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
server.tool(
|
|
96
|
+
"destroy_machine",
|
|
97
|
+
"Destroy a BBC Micro session and free its resources.",
|
|
98
|
+
{ session_id: z.string().describe("Session ID from create_machine") },
|
|
99
|
+
async ({ session_id }) => {
|
|
100
|
+
const s = sessions.get(session_id);
|
|
101
|
+
if (s) {
|
|
102
|
+
s.destroy();
|
|
103
|
+
sessions.delete(session_id);
|
|
104
|
+
}
|
|
105
|
+
return { content: [{ type: "text", text: "Session destroyed." }] };
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Tool: load_disc
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
server.tool(
|
|
114
|
+
"load_disc",
|
|
115
|
+
"Insert a disc image (.ssd or .dsd file) into drive 0 of the emulator. " +
|
|
116
|
+
"After loading, use type_input to issue DFS commands (e.g. '*RUN hello', '*DIR', 'CHAIN\"\"').",
|
|
117
|
+
{
|
|
118
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
119
|
+
image_path: z.string().describe("Absolute path to an .ssd or .dsd disc image file"),
|
|
120
|
+
},
|
|
121
|
+
async ({ session_id, image_path }) => {
|
|
122
|
+
const session = requireSession(session_id);
|
|
123
|
+
await session.loadDisc(image_path);
|
|
124
|
+
return { content: [{ type: "text", text: `Disc image loaded: ${image_path}` }] };
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Tool: load_basic
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
server.tool(
|
|
133
|
+
"load_basic",
|
|
134
|
+
"Tokenise BBC BASIC source code and load it into the emulator's PAGE memory, " +
|
|
135
|
+
"exactly as if you had typed it in and saved it. Does NOT run the program.",
|
|
136
|
+
{
|
|
137
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
138
|
+
source: z.string().describe("BBC BASIC source code (plain text, BBC dialect)"),
|
|
139
|
+
},
|
|
140
|
+
async ({ session_id, source }) => {
|
|
141
|
+
const session = requireSession(session_id);
|
|
142
|
+
await session.loadBasic(source);
|
|
143
|
+
return { content: [{ type: "text", text: "BASIC program loaded into PAGE." }] };
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Tool: type_input
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
server.tool(
|
|
152
|
+
"type_input",
|
|
153
|
+
"Type text at the current keyboard prompt (simulates key presses). " +
|
|
154
|
+
"A newline/RETURN is automatically sent after the text. " +
|
|
155
|
+
"Use run_until_prompt after this to collect output.",
|
|
156
|
+
{
|
|
157
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
158
|
+
text: z.string().describe("Text to type (e.g. 'RUN' or '10 PRINT\"HELLO\"')"),
|
|
159
|
+
},
|
|
160
|
+
async ({ session_id, text }) => {
|
|
161
|
+
const session = requireSession(session_id);
|
|
162
|
+
await session.type(text);
|
|
163
|
+
return { content: [{ type: "text", text: `Typed: ${text}` }] };
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Tool: run_until_prompt
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
server.tool(
|
|
172
|
+
"run_until_prompt",
|
|
173
|
+
"Run the emulator until it returns to a keyboard input prompt (e.g. the BASIC prompt after RUN completes). " +
|
|
174
|
+
"Returns all text output that was written to the screen since the last call.",
|
|
175
|
+
{
|
|
176
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
177
|
+
timeout_secs: z.number().default(60).describe("Max emulated seconds to run before giving up"),
|
|
178
|
+
clear: z
|
|
179
|
+
.boolean()
|
|
180
|
+
.default(true)
|
|
181
|
+
.describe(
|
|
182
|
+
"If true (default), clear the output buffer after returning it. " +
|
|
183
|
+
"Pass false to peek at accumulated output without consuming it.",
|
|
184
|
+
),
|
|
185
|
+
},
|
|
186
|
+
async ({ session_id, timeout_secs, clear }) => {
|
|
187
|
+
const session = requireSession(session_id);
|
|
188
|
+
const output = await session.runUntilPrompt(timeout_secs, { clear });
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "text",
|
|
193
|
+
text: JSON.stringify(output),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Tool: screenshot
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
server.tool(
|
|
205
|
+
"screenshot",
|
|
206
|
+
"Capture the current BBC Micro screen as a PNG image. " +
|
|
207
|
+
"Returns a base64-encoded PNG of the full 1024×625 emulated display. " +
|
|
208
|
+
"Tip: call run_until_prompt first to let the screen settle.",
|
|
209
|
+
{
|
|
210
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
211
|
+
active_only: z
|
|
212
|
+
.boolean()
|
|
213
|
+
.default(true)
|
|
214
|
+
.describe("If true, crop to the active display area and apply 2× pixel scaling for clarity"),
|
|
215
|
+
},
|
|
216
|
+
async ({ session_id, active_only }) => {
|
|
217
|
+
const session = requireSession(session_id);
|
|
218
|
+
const png = active_only ? await session.screenshotActive() : await session.screenshot();
|
|
219
|
+
return {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: "image",
|
|
223
|
+
data: png.toString("base64"),
|
|
224
|
+
mimeType: "image/png",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Tool: read_memory
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
server.tool(
|
|
236
|
+
"read_memory",
|
|
237
|
+
"Read bytes from the BBC Micro's memory map. " + "Returns an array of decimal byte values plus a hex dump.",
|
|
238
|
+
{
|
|
239
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
240
|
+
address: z.number().int().min(0).max(0xffff).describe("Start address (0–65535)"),
|
|
241
|
+
length: z.number().int().min(1).max(256).default(16).describe("Number of bytes to read (max 256)"),
|
|
242
|
+
},
|
|
243
|
+
async ({ session_id, address, length }) => {
|
|
244
|
+
const session = requireSession(session_id);
|
|
245
|
+
const bytes = session.readMemory(address, length);
|
|
246
|
+
const hexDump = formatHexDump(address, bytes);
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: JSON.stringify({
|
|
252
|
+
address,
|
|
253
|
+
addressHex: `0x${address.toString(16).toUpperCase()}`,
|
|
254
|
+
bytes,
|
|
255
|
+
hexDump,
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Tool: write_memory
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
server.tool(
|
|
268
|
+
"write_memory",
|
|
269
|
+
"Write bytes into the BBC Micro's memory. " +
|
|
270
|
+
"Useful for poking machine code, modifying variables, or patching running programs.",
|
|
271
|
+
{
|
|
272
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
273
|
+
address: z.number().int().min(0).max(0xffff).describe("Start address (0–65535)"),
|
|
274
|
+
bytes: z.array(z.number().int().min(0).max(255)).describe("Array of byte values to write"),
|
|
275
|
+
},
|
|
276
|
+
async ({ session_id, address, bytes }) => {
|
|
277
|
+
const session = requireSession(session_id);
|
|
278
|
+
session.writeMemory(address, bytes);
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: `Wrote ${bytes.length} byte(s) at 0x${address.toString(16).toUpperCase()}.`,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Tool: read_registers
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
server.tool(
|
|
295
|
+
"read_registers",
|
|
296
|
+
"Read the current 6502 CPU register state (PC, A, X, Y, stack pointer, processor status).",
|
|
297
|
+
{ session_id: z.string().describe("Session ID from create_machine") },
|
|
298
|
+
async ({ session_id }) => {
|
|
299
|
+
const session = requireSession(session_id);
|
|
300
|
+
const regs = session.registers();
|
|
301
|
+
return { content: [{ type: "text", text: JSON.stringify(regs) }] };
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Tool: run_for_cycles
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
server.tool(
|
|
310
|
+
"run_for_cycles",
|
|
311
|
+
"Run the emulator for an exact number of 2MHz CPU cycles. " +
|
|
312
|
+
"Useful for precise timing, or just to advance the clock a bit between interactions.",
|
|
313
|
+
{
|
|
314
|
+
session_id: z.string().describe("Session ID from create_machine"),
|
|
315
|
+
cycles: z.number().int().min(1).describe("Number of 2MHz CPU cycles to execute"),
|
|
316
|
+
clear: z
|
|
317
|
+
.boolean()
|
|
318
|
+
.default(true)
|
|
319
|
+
.describe("If true (default), clear the output buffer after returning it. Pass false to peek."),
|
|
320
|
+
},
|
|
321
|
+
async ({ session_id, cycles, clear }) => {
|
|
322
|
+
const session = requireSession(session_id);
|
|
323
|
+
await session.runFor(cycles);
|
|
324
|
+
const output = session.drainOutput({ clear });
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: "text",
|
|
329
|
+
text: JSON.stringify({ cycles_run: cycles, output }),
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Tool: run_basic (convenience: one-shot, no session management needed)
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
server.tool(
|
|
341
|
+
"run_basic",
|
|
342
|
+
"One-shot convenience tool: boot a BBC Micro, load a BASIC program, run it, " +
|
|
343
|
+
"return all text output and a screenshot, then destroy the session. " +
|
|
344
|
+
"Perfect for quickly trying out ideas without managing sessions.",
|
|
345
|
+
{
|
|
346
|
+
source: z.string().describe("BBC BASIC source code to run"),
|
|
347
|
+
model: z.enum(["B-DFS1.2", "Master"]).default("B-DFS1.2").describe("BBC Micro model"),
|
|
348
|
+
timeout_secs: z.number().default(30).describe("Max emulated seconds to allow the program to run"),
|
|
349
|
+
screenshot: z.boolean().default(true).describe("Include a screenshot of the final screen state"),
|
|
350
|
+
},
|
|
351
|
+
async ({ source, model, timeout_secs, screenshot: wantScreenshot }) => {
|
|
352
|
+
const session = new MachineSession(model);
|
|
353
|
+
try {
|
|
354
|
+
await session.initialise();
|
|
355
|
+
await session.boot(30);
|
|
356
|
+
await session.loadBasic(source);
|
|
357
|
+
await session.type("RUN");
|
|
358
|
+
const output = await session.runUntilPrompt(timeout_secs);
|
|
359
|
+
|
|
360
|
+
const result = { output };
|
|
361
|
+
|
|
362
|
+
if (wantScreenshot) {
|
|
363
|
+
const png = await session.screenshotActive();
|
|
364
|
+
return {
|
|
365
|
+
content: [
|
|
366
|
+
{ type: "text", text: JSON.stringify(result) },
|
|
367
|
+
{ type: "image", data: png.toString("base64"), mimeType: "image/png" },
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
373
|
+
} finally {
|
|
374
|
+
session.destroy();
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// Helpers
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
function formatHexDump(startAddr, bytes) {
|
|
384
|
+
const lines = [];
|
|
385
|
+
for (let i = 0; i < bytes.length; i += 16) {
|
|
386
|
+
const chunk = bytes.slice(i, i + 16);
|
|
387
|
+
const addr = (startAddr + i).toString(16).toUpperCase().padStart(4, "0");
|
|
388
|
+
const hex = chunk.map((b) => b.toString(16).toUpperCase().padStart(2, "0")).join(" ");
|
|
389
|
+
const ascii = chunk.map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
|
|
390
|
+
lines.push(`${addr} ${hex.padEnd(47)} |${ascii}|`);
|
|
391
|
+
}
|
|
392
|
+
return lines.join("\n");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Start server
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
async function main() {
|
|
400
|
+
const transport = new StdioServerTransport();
|
|
401
|
+
await server.connect(transport);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
main().catch((err) => {
|
|
405
|
+
console.error("Failed to start jsbeeb MCP server:", err);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
});
|