c64bridge 1.0.0 → 1.0.2

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.
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "port": {
59
59
  "type": "integer",
60
- "description": "Joystick port (1 = $DC01, 2 = $DC00).",
60
+ "description": "Joystick port (1 or 2).",
61
61
  "minimum": 1,
62
62
  "maximum": 2
63
63
  },
@@ -178,7 +178,7 @@
178
178
  "required": [
179
179
  "op"
180
180
  ],
181
- "description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input (a C64U firmware version that provides it, or U64 3.15+); VICE writes CIA1 registers. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
181
+ "description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
182
182
  "additionalProperties": false,
183
183
  "x-c64bridge-operations": [
184
184
  {
@@ -274,7 +274,7 @@
274
274
  },
275
275
  {
276
276
  "op": "joystick",
277
- "description": "Simulate joystick input. On C64U/U64 this uses machine:input (a C64U firmware version that provides it, or U64 3.15+); VICE writes CIA1 registers.",
277
+ "description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
278
278
  "required": [
279
279
  "port",
280
280
  "controls",
@@ -285,14 +285,14 @@
285
285
  ],
286
286
  "inputSchema": {
287
287
  "type": "object",
288
- "description": "Simulate joystick input. On C64U/U64 this uses machine:input (a C64U firmware version that provides it, or U64 3.15+); VICE writes CIA1 registers.",
288
+ "description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
289
289
  "properties": {
290
290
  "op": {
291
291
  "const": "joystick"
292
292
  },
293
293
  "port": {
294
294
  "type": "integer",
295
- "description": "Joystick port (1 = $DC01, 2 = $DC00).",
295
+ "description": "Joystick port (1 or 2).",
296
296
  "minimum": 1,
297
297
  "maximum": 2
298
298
  },
package/mcp/server.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Stdio-based Model Context Protocol (MCP) server that bridges Commodore 64 Ultimate hardware with AI agents.",
5
5
  "transports": [
6
6
  "stdio"
package/mcp/tools.json CHANGED
@@ -5668,7 +5668,8 @@
5668
5668
  "When defining characters, remind the user to send the BASIC program returned in the payload."
5669
5669
  ],
5670
5670
  "platforms": [
5671
- "c64u"
5671
+ "c64u",
5672
+ "u2"
5672
5673
  ]
5673
5674
  }
5674
5675
  },
@@ -8255,7 +8256,7 @@
8255
8256
  },
8256
8257
  "port": {
8257
8258
  "type": "integer",
8258
- "description": "Joystick port (1 = $DC01, 2 = $DC00).",
8259
+ "description": "Joystick port (1 or 2).",
8259
8260
  "minimum": 1,
8260
8261
  "maximum": 2
8261
8262
  },
@@ -8376,7 +8377,7 @@
8376
8377
  "required": [
8377
8378
  "op"
8378
8379
  ],
8379
- "description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input (a C64U firmware version that provides it, or U64 3.15+); VICE writes CIA1 registers. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
8380
+ "description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
8380
8381
  "additionalProperties": false,
8381
8382
  "x-c64bridge-operations": [
8382
8383
  {
@@ -8472,7 +8473,7 @@
8472
8473
  },
8473
8474
  {
8474
8475
  "op": "joystick",
8475
- "description": "Simulate joystick input. On C64U/U64 this uses machine:input (a C64U firmware version that provides it, or U64 3.15+); VICE writes CIA1 registers.",
8476
+ "description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
8476
8477
  "required": [
8477
8478
  "port",
8478
8479
  "controls",
@@ -8483,14 +8484,14 @@
8483
8484
  ],
8484
8485
  "inputSchema": {
8485
8486
  "type": "object",
8486
- "description": "Simulate joystick input. On C64U/U64 this uses machine:input (a C64U firmware version that provides it, or U64 3.15+); VICE writes CIA1 registers.",
8487
+ "description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
8487
8488
  "properties": {
8488
8489
  "op": {
8489
8490
  "const": "joystick"
8490
8491
  },
8491
8492
  "port": {
8492
8493
  "type": "integer",
8493
- "description": "Joystick port (1 = $DC01, 2 = $DC00).",
8494
+ "description": "Joystick port (1 or 2).",
8494
8495
  "minimum": 1,
8495
8496
  "maximum": 2
8496
8497
  },
package/mcp.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Stdio-based Model Context Protocol (MCP) server that bridges Commodore 64 Ultimate hardware with AI agents.",
5
5
  "entry": "dist/index.js",
6
6
  "type": "stdio",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "mcpName": "io.github.chrisgleissner/c64bridge",
5
5
  "description": "C64 Bridge MCP server for Commodore 64 Ultimate hardware and VICE emulator control",
6
6
  "repository": {
@@ -68,10 +68,10 @@
68
68
  "node": ">=24"
69
69
  },
70
70
  "dependencies": {
71
- "@modelcontextprotocol/sdk": "^1.29.0",
72
- "axios": "^1.18.1",
71
+ "@modelcontextprotocol/sdk": "^1.30.0",
72
+ "axios": "^1.19.0",
73
73
  "date-fns": "^4.4.0",
74
- "fastify": "^5.10.0",
74
+ "fastify": "^5.11.2",
75
75
  "jimp": "^1.6.1",
76
76
  "meyda": "^5.6.3",
77
77
  "octokit": "^5.0.5",
@@ -84,7 +84,7 @@
84
84
  },
85
85
  "devDependencies": {
86
86
  "@types/node": "^25.9.5",
87
- "tsx": "^4.23.0",
87
+ "tsx": "^4.23.11",
88
88
  "typescript": "^6.0.3"
89
89
  },
90
90
  "files": [
@@ -83,12 +83,18 @@ function sendJson(res, payload = {}, statusCode = 200) {
83
83
  function createInitialState() {
84
84
  return {
85
85
  networkPassword: null,
86
+ forcedErrors: null,
87
+ forcedErrorsUrlMatch: null,
88
+ hangOnMachineInputProbe: false,
86
89
  lastPrg: null,
90
+ lastLoadedPrg: null,
91
+ loadCount: 0,
87
92
  runCount: 0,
88
93
  resets: 0,
89
94
  reboots: 0,
90
95
  poweroffs: 0,
91
96
  memory: new Uint8Array(0x10000),
97
+ heartbeat: null,
92
98
  lastWrite: null,
93
99
  writeLog: [],
94
100
  lastRequest: null,
@@ -156,6 +162,82 @@ function toScreenCode(char) {
156
162
  return SCREEN_CODE_LOOKUP.get(char) ?? SCREEN_CODE_LOOKUP.get(" ") ?? 0x20;
157
163
  }
158
164
 
165
+ const KERNAL_KEYBOARD_NDX_ADDRESS = 0x00c6;
166
+ const KERNAL_KEYBOARD_BUFFER_ADDRESS = 0x0277;
167
+
168
+ const SYS_COMMAND_PATTERN = /SYS\s*\d+/i;
169
+ const HEARTBEAT_ADDRESS = 0x0400; // Screen-RAM byte the ASM liveness poller watches.
170
+ const HEARTBEAT_GRANULARITY_MS = 10; // Well below the poll interval so consecutive liveness reads always differ.
171
+ const HEARTBEAT_DURATION_MS = 1000; // How long a SYS-triggered program keeps "running".
172
+
173
+ /**
174
+ * Nothing in this mock runs 6502 code to drain the KERNAL keyboard buffer
175
+ * itself, so a client polling NDX ($00C6) waiting for it to return to zero
176
+ * would otherwise hang forever. Simulate the KERNAL's IRQ-driven drain with
177
+ * a short delay so injectKeyboardQueue's poll loop behaves realistically.
178
+ *
179
+ * When the queued text looks like a typed `SYSnnnnn` command (the trigger
180
+ * upload_run_asm uses to enter an assembled program), also record a
181
+ * time-computed "alive" window (consumed by readMemoryWithHeartbeat). This
182
+ * mock never executes 6502 code, so a liveness poller watching screen RAM
183
+ * would otherwise see no progression. Recording a descriptor rather than
184
+ * mutating memory from a background timer is deliberate: nothing advances
185
+ * asynchronously, so an unrelated later test that merely shares this server
186
+ * instance can never observe a stray screen-RAM mutation.
187
+ */
188
+ function simulateKeyboardDrain(state, address) {
189
+ if (address !== KERNAL_KEYBOARD_NDX_ADDRESS) return;
190
+ const pending = state.memory[KERNAL_KEYBOARD_NDX_ADDRESS] ?? 0;
191
+ const queued = pending > 0
192
+ ? Uint8Array.from(state.memory.subarray(KERNAL_KEYBOARD_BUFFER_ADDRESS, KERNAL_KEYBOARD_BUFFER_ADDRESS + pending))
193
+ : null;
194
+ setTimeout(() => {
195
+ state.memory[KERNAL_KEYBOARD_NDX_ADDRESS] = 0;
196
+ if (!queued) return;
197
+ const text = Buffer.from(queued).toString("ascii");
198
+ if (!SYS_COMMAND_PATTERN.test(text)) return;
199
+ state.heartbeat = {
200
+ address: HEARTBEAT_ADDRESS,
201
+ startedAt: Date.now(),
202
+ durationMs: HEARTBEAT_DURATION_MS,
203
+ baseValue: state.memory[HEARTBEAT_ADDRESS] ?? 0,
204
+ };
205
+ }, 5);
206
+ }
207
+
208
+ /**
209
+ * Serve a screen-RAM read, overlaying a synthetic, monotonically advancing
210
+ * byte at the heartbeat address while a SYS-triggered "alive" window is open.
211
+ * The overlay is a pure function of elapsed time applied only to the returned
212
+ * copy, so it never mutates stored memory and never advances on its own.
213
+ */
214
+ function readMemoryWithHeartbeat(state, address, length) {
215
+ const bytes = state.memory.slice(address, address + length);
216
+ const heartbeat = state.heartbeat;
217
+ if (!heartbeat) return bytes;
218
+ const elapsed = Date.now() - heartbeat.startedAt;
219
+ if (elapsed < 0 || elapsed >= heartbeat.durationMs) return bytes;
220
+ const offset = heartbeat.address - address;
221
+ if (offset < 0 || offset >= bytes.length) return bytes;
222
+ const ticks = Math.floor(elapsed / HEARTBEAT_GRANULARITY_MS);
223
+ bytes[offset] = (heartbeat.baseValue + ticks) & 0xff;
224
+ return bytes;
225
+ }
226
+
227
+ /**
228
+ * Any explicit write covering the heartbeat address means a client has taken
229
+ * over screen RAM, so end the simulated "alive" window and let the real stored
230
+ * bytes show through again. This keeps a lingering heartbeat from corrupting a
231
+ * later writeMemory/readMemory round-trip in a shared-server test.
232
+ */
233
+ function cancelHeartbeatOnWrite(state, address, length) {
234
+ const heartbeat = state.heartbeat;
235
+ if (!heartbeat) return;
236
+ if (address <= heartbeat.address && heartbeat.address < address + length) {
237
+ state.heartbeat = null;
238
+ }
239
+ }
240
+
159
241
  function seedReadyPrompt(state) {
160
242
  const text = "READY.";
161
243
  const buffer = new Uint8Array(text.length + 1);
@@ -275,6 +357,20 @@ export async function startMockC64Server(options = {}) {
275
357
  return;
276
358
  }
277
359
 
360
+ // Test hook: simulate real firmware soft-failures, which answer HTTP 200
361
+ // with a non-empty `errors` array rather than a non-2xx status. Consumed
362
+ // once so a single forced failure does not leak into later requests.
363
+ if (state.forcedErrors && (!state.forcedErrorsUrlMatch || url.includes(state.forcedErrorsUrlMatch))) {
364
+ const errors = state.forcedErrors;
365
+ state.forcedErrors = null;
366
+ state.forcedErrorsUrlMatch = null;
367
+ // Drain any request body so an unread upload cannot corrupt a reused
368
+ // keep-alive connection's framing.
369
+ for await (const _chunk of req) { /* discard */ }
370
+ sendJson(res, { result: "error", errors });
371
+ return;
372
+ }
373
+
278
374
  if (method === "GET" && (url === "/" || url.startsWith("/?"))) {
279
375
  sendJson(res, { status: "ok", host: "mock" });
280
376
  return;
@@ -297,7 +393,10 @@ export async function startMockC64Server(options = {}) {
297
393
  enabled: driveState.power !== "off",
298
394
  power: driveState.power,
299
395
  mode: driveState.mode,
300
- image: driveState.mountedImage,
396
+ // Mirror the real firmware's DriveInfo shape (image_path is a
397
+ // plain string), not the internal mount-params object.
398
+ image_path: driveState.mountedImage?.image ?? null,
399
+ type: driveState.mountedImage?.type ?? driveState.mode ?? null,
301
400
  };
302
401
  }
303
402
  sendJson(res, { drives });
@@ -340,6 +439,11 @@ export async function startMockC64Server(options = {}) {
340
439
  }
341
440
 
342
441
  if (method === "GET" && url === "/v1/machine:input") {
442
+ if (state.hangOnMachineInputProbe) {
443
+ // Test hook: never respond, to prove a caller cannot be blocked on
444
+ // this probe (HARD01-031). Deliberately does not call res.end().
445
+ return;
446
+ }
343
447
  sendJson(res, state.inputState);
344
448
  return;
345
449
  }
@@ -398,6 +502,27 @@ export async function startMockC64Server(options = {}) {
398
502
  return;
399
503
  }
400
504
 
505
+ if (method === "POST" && url === "/v1/runners:load_prg") {
506
+ const chunks = [];
507
+ for await (const chunk of req) {
508
+ chunks.push(chunk);
509
+ }
510
+
511
+ const prg = Buffer.concat(chunks);
512
+ state.lastLoadedPrg = prg;
513
+ state.loadCount += 1;
514
+
515
+ // Mirror real firmware: place the PRG body at its embedded load
516
+ // address without starting it (no RUN, no BASIC pointer changes).
517
+ if (prg.length >= 2) {
518
+ const loadAddress = prg.readUInt16LE(0);
519
+ state.memory.set(prg.subarray(2), loadAddress);
520
+ }
521
+
522
+ sendJson(res, { result: "ok", bytes: prg.length });
523
+ return;
524
+ }
525
+
401
526
  if (url.startsWith("/v1/runners:sidplay")) {
402
527
  let routeUrl;
403
528
  try {
@@ -471,7 +596,7 @@ export async function startMockC64Server(options = {}) {
471
596
  const lengthValue = routeUrl.searchParams.get("length") ?? "256";
472
597
  const address = parseNumeric(addressValue);
473
598
  const length = Math.max(0, parseNumeric(lengthValue, 10));
474
- const bytes = state.memory.slice(address, address + length);
599
+ const bytes = readMemoryWithHeartbeat(state, address, length);
475
600
 
476
601
  const accept = String(req.headers["accept"] || "");
477
602
  if (accept.includes("application/octet-stream")) {
@@ -506,6 +631,8 @@ export async function startMockC64Server(options = {}) {
506
631
  state.memory.set(bytes, address);
507
632
  state.lastWrite = { address, bytes };
508
633
  state.writeLog.push({ address, bytes: Buffer.from(bytes) });
634
+ cancelHeartbeatOnWrite(state, address, bytes.length);
635
+ simulateKeyboardDrain(state, address);
509
636
 
510
637
  sendJson(res, { result: "wrote", address, length: bytes.length });
511
638
  return;
@@ -525,6 +652,8 @@ export async function startMockC64Server(options = {}) {
525
652
  state.memory.set(bytes, address);
526
653
  state.lastWrite = { address, bytes };
527
654
  state.writeLog.push({ address, bytes: Buffer.from(bytes) });
655
+ cancelHeartbeatOnWrite(state, address, bytes.length);
656
+ simulateKeyboardDrain(state, address);
528
657
 
529
658
  sendJson(res, { result: "wrote", address, length: bytes.length });
530
659
  return;
package/server.json CHANGED
@@ -8,12 +8,12 @@
8
8
  "source": "github"
9
9
  },
10
10
  "websiteUrl": "https://github.com/chrisgleissner/c64bridge",
11
- "version": "1.0.0",
11
+ "version": "1.0.2",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "c64bridge",
16
- "version": "1.0.0",
16
+ "version": "1.0.2",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },