c64bridge 0.9.3 → 1.0.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.
Files changed (65) hide show
  1. package/AGENTS.md +19 -3
  2. package/README.md +190 -148
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/c64Client.js +277 -41
  5. package/dist/config.js +30 -18
  6. package/dist/device.js +191 -125
  7. package/dist/knowledge.js +1 -1
  8. package/dist/loggingHttpClient.js +15 -3
  9. package/dist/mcp-server.js +65 -16
  10. package/dist/platform.js +13 -2
  11. package/dist/rag/indexer.js +11 -8
  12. package/dist/rag/init.js +11 -8
  13. package/dist/sidwaveCompiler.js +2 -2
  14. package/dist/tools/audio.js +27 -6
  15. package/dist/tools/developer.js +6 -6
  16. package/dist/tools/graphics.js +4 -4
  17. package/dist/tools/input.js +136 -24
  18. package/dist/tools/machineControl.js +85 -6
  19. package/dist/tools/memory.js +21 -6
  20. package/dist/tools/meta/artifacts.js +20 -8
  21. package/dist/tools/meta/background.js +11 -1
  22. package/dist/tools/meta/index.js +1 -1
  23. package/dist/tools/pollValidator.js +19 -51
  24. package/dist/tools/printer.js +1 -0
  25. package/dist/tools/programRunners.js +5 -5
  26. package/dist/tools/rag.js +1 -1
  27. package/dist/tools/registry/batch.js +1 -1
  28. package/dist/tools/registry/config.js +5 -5
  29. package/dist/tools/registry/disk.js +2 -2
  30. package/dist/tools/registry/drive.js +2 -2
  31. package/dist/tools/registry/graphics.js +2 -2
  32. package/dist/tools/registry/memory.js +2 -2
  33. package/dist/tools/registry/platform.js +9 -5
  34. package/dist/tools/registry/printer.js +1 -0
  35. package/dist/tools/registry/program.js +5 -5
  36. package/dist/tools/registry/rag.js +1 -1
  37. package/dist/tools/registry/sound.js +4 -4
  38. package/dist/tools/registry/system.js +13 -3
  39. package/dist/tools/storage.js +1 -1
  40. package/dist/vice/mockServer.js +33 -0
  41. package/dist/vice/process.js +11 -1
  42. package/dist/vice/viceClient.js +35 -6
  43. package/doc/c64u/c64-openapi.yaml +1130 -774
  44. package/doc/reviews/hardening/01-fable/prompt-handover.md +125 -0
  45. package/doc/reviews/hardening/01-fable/prompt.md +98 -0
  46. package/doc/reviews/hardening/01-fable/review-prompt.md +468 -0
  47. package/doc/reviews/hardening/01-fable/review.md +1234 -0
  48. package/doc/u2/u2-openapi.yaml +1192 -0
  49. package/generated/c64/index.js +142 -9
  50. package/generated/openapi/c64u/index.js +1117 -0
  51. package/generated/openapi/u2/index.js +929 -0
  52. package/mcp/schemas/c64_graphics.schema.json +3 -3
  53. package/mcp/schemas/c64_input.schema.json +249 -8
  54. package/mcp/schemas/c64_memory.schema.json +3 -3
  55. package/mcp/schemas/c64_program.schema.json +3 -3
  56. package/mcp/schemas/c64_select_backend.schema.json +2 -0
  57. package/mcp/schemas/c64_system.schema.json +41 -1
  58. package/mcp/server.json +1 -1
  59. package/mcp/tools.json +386 -44
  60. package/mcp.json +14 -2
  61. package/package.json +4 -2
  62. package/scripts/mockC64Server.mjs +171 -2
  63. package/scripts/prepare-release.mjs +13 -2
  64. package/scripts/update-readme.ts +18 -30
  65. package/server.json +3 -3
package/mcp.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "0.9.3",
3
+ "version": "1.0.1",
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",
@@ -37,7 +37,7 @@
37
37
  "default": "~/.c64bridge.json"
38
38
  },
39
39
  "C64_MODE": {
40
- "description": "Select active backend (c64u for Ultimate hardware, vice for emulator)",
40
+ "description": "Select active backend (c64u/U64 hardware, u2/U2-family cartridge, or vice emulator)",
41
41
  "default": "c64u"
42
42
  },
43
43
  "C64U_HOST": {
@@ -52,6 +52,18 @@
52
52
  "description": "Override the C64 Ultimate network password sent as X-Password",
53
53
  "default": ""
54
54
  },
55
+ "U2_HOST": {
56
+ "description": "Override the U2/U2+/U2+L host name or IP address when C64_MODE=u2",
57
+ "default": ""
58
+ },
59
+ "U2_PORT": {
60
+ "description": "Override the U2-family REST port when C64_MODE=u2",
61
+ "default": ""
62
+ },
63
+ "U2_PASSWORD": {
64
+ "description": "Override the U2-family network password sent as X-Password when C64_MODE=u2",
65
+ "default": ""
66
+ },
55
67
  "C64BRIDGE_DIAGNOSTICS_DIR": {
56
68
  "description": "Override the directory where persistent MCP diagnostics files are written",
57
69
  "default": "~/.c64bridge/diagnostics"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "0.9.3",
3
+ "version": "1.0.1",
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": {
@@ -38,7 +38,9 @@
38
38
  "check": "npm run build && npm test",
39
39
  "check:node-compat": "node scripts/check-node-compat.mjs",
40
40
  "build:rag": "node scripts/invoke-bun.mjs scripts/rag-rebuild.mjs",
41
- "api:generate": "npx --yes swagger-typescript-api@12.0.4 -p doc/c64u/c64-openapi.yaml -o generated/c64 -n index.ts --axios --single-http-client --module-name-first-tag",
41
+ "api:generate": "npm run api:generate:c64u && npm run api:generate:u2",
42
+ "api:generate:c64u": "npx --yes swagger-typescript-api@12.0.4 -p doc/c64u/c64-openapi.yaml -o generated/openapi/c64u -n index.ts --axios --single-http-client --module-name-index 0",
43
+ "api:generate:u2": "npx --yes swagger-typescript-api@12.0.4 -p doc/u2/u2-openapi.yaml -o generated/openapi/u2 -n index.ts --axios --single-http-client --module-name-index 0",
42
44
  "c64:tool": "bash scripts/c64-tool.sh",
43
45
  "rag:rebuild": "node scripts/invoke-bun.mjs scripts/rag-rebuild.mjs",
44
46
  "rag:fetch": "node scripts/fetch-external-sources.mjs",
@@ -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,
@@ -112,6 +118,8 @@ function createInitialState() {
112
118
  lastConfigAction: null,
113
119
  menuToggleCount: 0,
114
120
  lastMenuTarget: null,
121
+ menuScreen: Buffer.from([0x41, 0x01, 0x42, 0x02]),
122
+ inputState: { keyboard: { inputs: [] }, joysticks: [{ port: 1, inputs: [] }, { port: 2, inputs: [] }] },
115
123
  streams: {
116
124
  video: { active: false, target: null, packetsSent: 0 },
117
125
  audio: { active: false, target: null, packetsSent: 0 },
@@ -154,6 +162,82 @@ function toScreenCode(char) {
154
162
  return SCREEN_CODE_LOOKUP.get(char) ?? SCREEN_CODE_LOOKUP.get(" ") ?? 0x20;
155
163
  }
156
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
+
157
241
  function seedReadyPrompt(state) {
158
242
  const text = "READY.";
159
243
  const buffer = new Uint8Array(text.length + 1);
@@ -273,6 +357,20 @@ export async function startMockC64Server(options = {}) {
273
357
  return;
274
358
  }
275
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
+
276
374
  if (method === "GET" && (url === "/" || url.startsWith("/?"))) {
277
375
  sendJson(res, { status: "ok", host: "mock" });
278
376
  return;
@@ -295,7 +393,10 @@ export async function startMockC64Server(options = {}) {
295
393
  enabled: driveState.power !== "off",
296
394
  power: driveState.power,
297
395
  mode: driveState.mode,
298
- 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,
299
400
  };
300
401
  }
301
402
  sendJson(res, { drives });
@@ -331,6 +432,49 @@ export async function startMockC64Server(options = {}) {
331
432
  return;
332
433
  }
333
434
 
435
+ if (method === "GET" && url === "/v1/machine:menu_screen") {
436
+ res.setHeader("Content-Type", "application/octet-stream");
437
+ res.end(state.menuScreen);
438
+ return;
439
+ }
440
+
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
+ }
447
+ sendJson(res, state.inputState);
448
+ return;
449
+ }
450
+
451
+ if (method === "POST" && url === "/v1/machine:input") {
452
+ const batch = await readJson(req);
453
+ for (const event of batch.events ?? []) {
454
+ if (event.kind === "release_all") {
455
+ state.inputState.keyboard.inputs = [];
456
+ state.inputState.joysticks.forEach((joystick) => { joystick.inputs = []; });
457
+ continue;
458
+ }
459
+ const target = event.kind === "keyboard"
460
+ ? state.inputState.keyboard
461
+ : state.inputState.joysticks.find((joystick) => joystick.port === event.port);
462
+ if (!target) continue;
463
+ const inputs = new Set(target.inputs);
464
+ for (const input of event.inputs ?? []) {
465
+ if (event.transition === "release") inputs.delete(input);
466
+ else if (event.transition === "press") inputs.add(input);
467
+ else if (event.transition === "tap") {
468
+ inputs.add(input);
469
+ inputs.delete(input);
470
+ }
471
+ }
472
+ target.inputs = [...inputs];
473
+ }
474
+ sendJson(res, state.inputState);
475
+ return;
476
+ }
477
+
334
478
  if (method === "GET" && url === "/v1/machine:debugreg") {
335
479
  sendJson(res, { value: state.debugreg ?? "00" });
336
480
  return;
@@ -358,6 +502,27 @@ export async function startMockC64Server(options = {}) {
358
502
  return;
359
503
  }
360
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
+
361
526
  if (url.startsWith("/v1/runners:sidplay")) {
362
527
  let routeUrl;
363
528
  try {
@@ -431,7 +596,7 @@ export async function startMockC64Server(options = {}) {
431
596
  const lengthValue = routeUrl.searchParams.get("length") ?? "256";
432
597
  const address = parseNumeric(addressValue);
433
598
  const length = Math.max(0, parseNumeric(lengthValue, 10));
434
- const bytes = state.memory.slice(address, address + length);
599
+ const bytes = readMemoryWithHeartbeat(state, address, length);
435
600
 
436
601
  const accept = String(req.headers["accept"] || "");
437
602
  if (accept.includes("application/octet-stream")) {
@@ -466,6 +631,8 @@ export async function startMockC64Server(options = {}) {
466
631
  state.memory.set(bytes, address);
467
632
  state.lastWrite = { address, bytes };
468
633
  state.writeLog.push({ address, bytes: Buffer.from(bytes) });
634
+ cancelHeartbeatOnWrite(state, address, bytes.length);
635
+ simulateKeyboardDrain(state, address);
469
636
 
470
637
  sendJson(res, { result: "wrote", address, length: bytes.length });
471
638
  return;
@@ -485,6 +652,8 @@ export async function startMockC64Server(options = {}) {
485
652
  state.memory.set(bytes, address);
486
653
  state.lastWrite = { address, bytes };
487
654
  state.writeLog.push({ address, bytes: Buffer.from(bytes) });
655
+ cancelHeartbeatOnWrite(state, address, bytes.length);
656
+ simulateKeyboardDrain(state, address);
488
657
 
489
658
  sendJson(res, { result: "wrote", address, length: bytes.length });
490
659
  return;
@@ -26,8 +26,19 @@ const fileExists = async (relativePath) => {
26
26
  };
27
27
 
28
28
  const getRepositoryMetadata = (repository) => {
29
- const url = typeof repository === 'string' ? repository : repository?.url;
30
- if (!url) {
29
+ const rawUrl = typeof repository === 'string' ? repository : repository?.url;
30
+ if (!rawUrl) {
31
+ return undefined;
32
+ }
33
+
34
+ let url = rawUrl.trim();
35
+ if (url.startsWith('git+')) {
36
+ url = url.slice(4);
37
+ }
38
+ if (url.endsWith('.git')) {
39
+ url = url.slice(0, -4);
40
+ }
41
+ if (!/^https?:\/\//.test(url)) {
31
42
  return undefined;
32
43
  }
33
44
 
@@ -27,6 +27,7 @@ type ManifestEnvEntry = {
27
27
  type EnvCategory =
28
28
  | "Server Runtime"
29
29
  | "C64 Ultimate"
30
+ | "U2-family"
30
31
  | "VICE Runtime"
31
32
  | "VICE Audio Capture"
32
33
  | "SID Playback"
@@ -55,6 +56,9 @@ function classifyEnvVariable(name: string): EnvCategory {
55
56
  if (name.startsWith("C64U_")) {
56
57
  return "C64 Ultimate";
57
58
  }
59
+ if (name.startsWith("U2_")) {
60
+ return "U2-family";
61
+ }
58
62
  if (name.startsWith("VICE_") || name === "FORCE_XVFB" || name === "DISABLE_XVFB") {
59
63
  if (name === "VICE_MODE" || name === "VICE_LIMIT_CYCLES" || name === "VICE_RUN_TIMEOUT_MS") {
60
64
  return "VICE Audio Capture";
@@ -79,6 +83,9 @@ function resolveJsonConfigKey(name: string): string {
79
83
  C64U_HOST: "c64u.host",
80
84
  C64U_PORT: "c64u.port",
81
85
  C64U_PASSWORD: "c64u.networkPassword",
86
+ U2_HOST: "u2.host",
87
+ U2_PORT: "u2.port",
88
+ U2_PASSWORD: "u2.networkPassword",
82
89
  VICE_BINARY: "vice.exe",
83
90
  VICE_DIRECTORY: "vice.directory",
84
91
  VICE_HOST: "vice.host",
@@ -104,6 +111,7 @@ export function renderEnvironmentSection(envEntries: Readonly<Record<string, Man
104
111
  const orderedCategories: readonly EnvCategory[] = [
105
112
  "Server Runtime",
106
113
  "C64 Ultimate",
114
+ "U2-family",
107
115
  "VICE Runtime",
108
116
  "VICE Audio Capture",
109
117
  "SID Playback",
@@ -145,7 +153,6 @@ type GroupedOperation = {
145
153
  readonly required: readonly string[];
146
154
  readonly optional: readonly string[];
147
155
  readonly properties: Readonly<Record<string, JsonSchema | undefined>>;
148
- readonly notes: readonly string[];
149
156
  };
150
157
 
151
158
  type GroupedToolInfo = {
@@ -168,6 +175,11 @@ function toTableValue(values: readonly string[]): string {
168
175
  return values.join(", ");
169
176
  }
170
177
 
178
+ function withVerificationNote(description: string, properties: Readonly<Record<string, JsonSchema | undefined>>): string {
179
+ const supportsVerification = Object.keys(properties).some((name) => name.toLowerCase().startsWith("verify"));
180
+ return supportsVerification ? `${description} Supports verification.` : description;
181
+ }
182
+
171
183
  function formatInputName(name: string, schema?: JsonSchema): string {
172
184
  const defaultSuffix = schema && Object.prototype.hasOwnProperty.call(schema, "default")
173
185
  ? `=${JSON.stringify(schema.default)}`
@@ -229,21 +241,12 @@ function collectFromOperationMetadata(schema: JsonSchema): readonly GroupedOpera
229
241
  ? entry.optional.filter((name): name is string => typeof name === "string" && name !== "op")
230
242
  : Object.keys(properties).filter((name) => name !== "op" && !requiredProps.includes(name));
231
243
 
232
- const notes: string[] = [];
233
- const hasVerificationProperty = Object.keys(properties).some((name) =>
234
- name.toLowerCase().startsWith("verify"),
235
- );
236
- if (hasVerificationProperty) {
237
- notes.push("supports verify");
238
- }
239
-
240
244
  operations.push({
241
245
  op: opName,
242
- description: getString(entry.description, `Operation ${opName}`),
246
+ description: withVerificationNote(getString(entry.description, `Operation ${opName}`), properties),
243
247
  required: requiredProps,
244
248
  optional: optionalProps,
245
249
  properties,
246
- notes,
247
250
  });
248
251
  }
249
252
 
@@ -282,21 +285,12 @@ function collectFromVariantList(variants: readonly JsonSchema[]): readonly Group
282
285
  const description = getString(typedVariant.description, getString(opSchema?.description, `Operation ${opName}`));
283
286
  const requiredProps = ((typedVariant.required as readonly string[] | undefined) ?? []).filter((name) => name !== "op");
284
287
 
285
- const notes: string[] = [];
286
- const hasVerificationProperty = Object.keys(properties).some((name) =>
287
- name.toLowerCase().startsWith("verify"),
288
- );
289
- if (hasVerificationProperty) {
290
- notes.push("supports verify");
291
- }
292
-
293
288
  operations.push({
294
289
  op: opName,
295
- description,
290
+ description: withVerificationNote(description, properties),
296
291
  required: requiredProps,
297
292
  optional: Object.keys(properties).filter((name) => name !== "op" && !requiredProps.includes(name)),
298
293
  properties,
299
- notes,
300
294
  });
301
295
  }
302
296
 
@@ -321,18 +315,12 @@ function collectFromFlattenedSchema(schema: JsonSchema): readonly GroupedOperati
321
315
  }
322
316
 
323
317
  const descriptionMap = parseOperationDescriptions(getString(schema.description));
324
- const hasVerificationProperty = Object.keys(properties).some((name) =>
325
- name !== "op" && name.toLowerCase().startsWith("verify"),
326
- );
327
- const notes = hasVerificationProperty ? ["supports verify"] : [];
328
-
329
318
  return opNames.map((opName) => ({
330
319
  op: opName,
331
- description: descriptionMap.get(opName) ?? `Operation ${opName}`,
320
+ description: withVerificationNote(descriptionMap.get(opName) ?? `Operation ${opName}`, properties),
332
321
  required: [] as readonly string[],
333
322
  optional: Object.keys(properties).filter((name) => name !== "op"),
334
323
  properties,
335
- notes,
336
324
  }));
337
325
  }
338
326
 
@@ -391,13 +379,13 @@ export function renderToolsSection(modules: readonly ToolModuleDescriptor[] = de
391
379
  escapeCell(operation.description),
392
380
  escapeCell(formatInputs(operation.required, operation.properties)),
393
381
  escapeCell(formatInputs(operation.optional, operation.properties)),
394
- operation.notes.length ? escapeCell(operation.notes.join(", ")) : "—",
395
382
  effectivePlatforms.includes("c64u") ? "✅" : "",
383
+ effectivePlatforms.includes("u2") ? "✅" : "",
396
384
  effectivePlatforms.includes("vice") ? "✅" : "",
397
385
  ];
398
386
  });
399
387
 
400
- lines.push(renderTable(["Operation", "Description", "Required Inputs", "Optional Inputs", "Notes", "C64U", "VICE"], operationRows));
388
+ lines.push(renderTable(["Operation", "Description", "Required Inputs", "Optional Inputs", "C64U", "U2", "VICE"], operationRows));
401
389
  }
402
390
 
403
391
  lines.push("");
package/server.json CHANGED
@@ -4,16 +4,16 @@
4
4
  "title": "C64 Bridge",
5
5
  "description": "MCP server for Commodore 64 Ultimate hardware and VICE emulator control.",
6
6
  "repository": {
7
- "url": "git+https://github.com/chrisgleissner/c64bridge.git",
7
+ "url": "https://github.com/chrisgleissner/c64bridge",
8
8
  "source": "github"
9
9
  },
10
10
  "websiteUrl": "https://github.com/chrisgleissner/c64bridge",
11
- "version": "0.9.3",
11
+ "version": "1.0.1",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "c64bridge",
16
- "version": "0.9.3",
16
+ "version": "1.0.1",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },