c64bridge 0.9.0 → 0.9.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 (49) hide show
  1. package/README.md +158 -164
  2. package/data/vice/vice-binary-monitor-spec.md +21 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/c64Client.js +76 -2
  5. package/dist/device.js +43 -3
  6. package/dist/rag/indexer.js +18 -8
  7. package/dist/sidwaveCompiler.js +1 -1
  8. package/dist/{assemblyConverter.js → tools/assember.js} +10 -1
  9. package/dist/{basicConverter.js → tools/basicTokenizer.js} +1 -1
  10. package/dist/tools/debug.js +158 -5
  11. package/dist/tools/disassembler.js +478 -0
  12. package/dist/tools/input.js +338 -0
  13. package/dist/tools/memory.js +317 -1
  14. package/dist/tools/programRunners.js +45 -6
  15. package/dist/tools/registry/batch.js +167 -0
  16. package/dist/tools/registry/graphics.js +101 -1
  17. package/dist/tools/registry/index.js +4 -0
  18. package/dist/tools/registry/input.js +2 -0
  19. package/dist/tools/registry/memory.js +61 -1
  20. package/dist/tools/schema.js +4 -5
  21. package/dist/tools/symbolRegistry.js +49 -0
  22. package/dist/tools/types.js +19 -4
  23. package/dist/vice/process.js +43 -2
  24. package/dist/vice/viceClient.js +5 -1
  25. package/mcp/schemas/c64_batch.schema.json +53 -0
  26. package/mcp/schemas/c64_config.schema.json +461 -2
  27. package/mcp/schemas/c64_debug.schema.json +641 -3
  28. package/mcp/schemas/c64_disk.schema.json +340 -2
  29. package/mcp/schemas/c64_drive.schema.json +157 -2
  30. package/mcp/schemas/c64_extract.schema.json +359 -2
  31. package/mcp/schemas/c64_graphics.schema.json +415 -3
  32. package/mcp/schemas/c64_input.schema.json +136 -0
  33. package/mcp/schemas/c64_memory.schema.json +562 -3
  34. package/mcp/schemas/c64_printer.schema.json +253 -2
  35. package/mcp/schemas/c64_program.schema.json +499 -2
  36. package/mcp/schemas/c64_rag.schema.json +82 -2
  37. package/mcp/schemas/c64_select_backend.schema.json +35 -2
  38. package/mcp/schemas/c64_sound.schema.json +793 -2
  39. package/mcp/schemas/c64_stream.schema.json +73 -2
  40. package/mcp/schemas/c64_system.schema.json +301 -2
  41. package/mcp/schemas/c64_vice.schema.json +71 -2
  42. package/mcp/server.json +1 -1
  43. package/mcp/tools.json +6718 -1257
  44. package/mcp.json +1 -1
  45. package/package.json +1 -1
  46. package/scripts/run-coverage.mjs +7 -0
  47. package/scripts/run-tests.ts +2 -0
  48. package/scripts/update-readme.ts +79 -2
  49. package/server.json +2 -2
@@ -1,10 +1,12 @@
1
- import { assemblyToPrg, AssemblyError } from "../assemblyConverter.js";
2
- import { basicToPrg } from "../basicConverter.js";
1
+ import fs from "node:fs";
2
+ import { assemblyToPrgDetailed, AssemblyError } from "./assember.js";
3
+ import { basicToPrg } from "./basicTokenizer.js";
3
4
  import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
4
- import { booleanSchema, objectSchema, stringSchema } from "./schema.js";
5
+ import { booleanSchema, objectSchema, optionalSchema, stringSchema } from "./schema.js";
5
6
  import { textResult } from "./responses.js";
6
7
  import { ToolExecutionError, ToolError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
7
8
  import { pollForProgramOutcome } from "./pollValidator.js";
9
+ import { clearViceSymbols, setViceSymbols, parseViceSymbolFile } from "./symbolRegistry.js";
8
10
  function extractFailureDetails(details) {
9
11
  if (details === undefined || details === null) {
10
12
  return undefined;
@@ -163,6 +165,14 @@ function normalizeRuntimeErrors(errors) {
163
165
  text: error.raw,
164
166
  }));
165
167
  }
168
+ export const __programRunnerHelpersForTests = {
169
+ extractFailureDetails,
170
+ toRecord,
171
+ stripOperationDiscriminator,
172
+ parseBasicRuntimeErrors,
173
+ attemptAutoFixBasicProgram,
174
+ normalizeRuntimeErrors,
175
+ };
166
176
  function structuredExecutionError(message, data) {
167
177
  return {
168
178
  content: [
@@ -221,6 +231,10 @@ const prgFileArgsSchema = objectSchema({
221
231
  description: prgPathDescription,
222
232
  minLength: 1,
223
233
  }),
234
+ symbolsFile: optionalSchema(stringSchema({
235
+ description: "Path to a VICE symbol file (.vs) to load alongside the PRG. Symbols are used by the disassemble tool when annotating addresses.",
236
+ minLength: 1,
237
+ })),
224
238
  },
225
239
  required: ["path"],
226
240
  additionalProperties: false,
@@ -424,9 +438,13 @@ async function executeUploadRunAsm(rawArgs, ctx) {
424
438
  sourceLength: parsed.program.length,
425
439
  ...(shouldVerify ? { verify: true } : {}),
426
440
  });
427
- // Assemble locally to expose structured metadata
428
- const prg = assemblyToPrg(parsed.program);
441
+ // Assemble locally to expose structured metadata and capture symbols
442
+ const asmResult = assemblyToPrgDetailed(parsed.program);
443
+ const prg = asmResult.prg;
429
444
  const entryAddress = prg.readUInt16LE(0);
445
+ if (ctx.platform.id === "vice") {
446
+ setViceSymbols(asmResult.symbols.entries());
447
+ }
430
448
  const result = await ctx.client.uploadAndRunAsm(parsed.program);
431
449
  if (!result.success) {
432
450
  return toolErrorResult(new ToolExecutionError("C64 firmware reported failure while running assembly program", {
@@ -527,6 +545,24 @@ async function executeRunPrg(rawArgs, ctx) {
527
545
  try {
528
546
  const parsed = prgFileArgsSchema.parse(rawArgs ?? {});
529
547
  ctx.logger.info("Running PRG file", { path: parsed.path });
548
+ let symbolsLoaded = 0;
549
+ if (ctx.platform.id === "vice") {
550
+ clearViceSymbols();
551
+ if (parsed.symbolsFile) {
552
+ try {
553
+ const content = await fs.promises.readFile(parsed.symbolsFile, "utf8");
554
+ const syms = parseViceSymbolFile(content);
555
+ setViceSymbols(syms.entries());
556
+ symbolsLoaded = syms.size;
557
+ ctx.logger.info("Loaded VICE symbols from file", { path: parsed.symbolsFile, count: symbolsLoaded });
558
+ }
559
+ catch (fileError) {
560
+ throw new ToolExecutionError(`Failed to read symbols file: ${parsed.symbolsFile}`, {
561
+ details: { message: fileError instanceof Error ? fileError.message : String(fileError) },
562
+ });
563
+ }
564
+ }
565
+ }
530
566
  const result = await ctx.client.runPrgFile(parsed.path);
531
567
  if (!result.success) {
532
568
  throw new ToolExecutionError("C64 firmware reported failure while running PRG", {
@@ -539,12 +575,15 @@ async function executeRunPrg(rawArgs, ctx) {
539
575
  path: parsed.path,
540
576
  entryAddress: null,
541
577
  resources: ["c64://guide/bootstrap"],
578
+ ...(symbolsLoaded > 0 ? { symbolsLoaded } : {}),
542
579
  };
543
- const base = textResult(`PRG ${parsed.path} loaded and executed.`, {
580
+ const symbolNote = symbolsLoaded > 0 ? ` Loaded ${symbolsLoaded} debug symbol(s).` : "";
581
+ const base = textResult(`PRG ${parsed.path} loaded and executed.${symbolNote}`, {
544
582
  success: true,
545
583
  path: parsed.path,
546
584
  entryAddress: null,
547
585
  details: toRecord(result.details) ?? null,
586
+ ...(symbolsLoaded > 0 ? { symbolsLoaded } : {}),
548
587
  });
549
588
  return { ...base, structuredContent: { type: "json", data } };
550
589
  }
@@ -0,0 +1,167 @@
1
+ import { defineToolModule, } from "../types.js";
2
+ import { getPlatformStatus } from "../../platform.js";
3
+ import { arraySchema, booleanSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
4
+ import { jsonResult } from "../responses.js";
5
+ import { ToolValidationError, unknownErrorResult } from "../errors.js";
6
+ const commandSchema = objectSchema({
7
+ description: "A single batch command.",
8
+ properties: {
9
+ tool: stringSchema({ description: "MCP tool name (e.g. c64_memory, c64_debug).", minLength: 1 }),
10
+ args: objectSchema({
11
+ description: "Arguments to pass to the tool (must include 'op' for grouped tools).",
12
+ properties: {},
13
+ additionalProperties: true,
14
+ }),
15
+ description: optionalSchema(stringSchema({ description: "Optional label for this command in the result.", maxLength: 128 })),
16
+ },
17
+ required: ["tool", "args"],
18
+ additionalProperties: false,
19
+ });
20
+ const executeBatchArgsSchema = objectSchema({
21
+ description: "Execute multiple tool calls in one request.",
22
+ properties: {
23
+ commands: arraySchema(commandSchema, {
24
+ description: "Ordered list of commands to execute.",
25
+ minItems: 1,
26
+ }),
27
+ stopOnError: optionalSchema(booleanSchema({
28
+ description: "Halt the batch after the first failure. Default: true.",
29
+ default: true,
30
+ })),
31
+ },
32
+ required: ["commands"],
33
+ additionalProperties: false,
34
+ });
35
+ export const batchModuleGroup = defineToolModule({
36
+ domain: "batch",
37
+ summary: "Execute multiple tool calls in a single MCP request for reduced round-trip overhead.",
38
+ supportedPlatforms: ["c64u", "vice"],
39
+ resources: [],
40
+ prompts: [],
41
+ defaultTags: ["batch", "orchestration"],
42
+ workflowHints: [
43
+ "Use execute_batch when you need to run 3 or more sequential operations — it eliminates per-call network overhead.",
44
+ "Each command must include 'tool' and 'args'; grouped tools require 'op' inside args.",
45
+ ],
46
+ tools: [
47
+ {
48
+ name: "c64_batch",
49
+ description: "Execute multiple c64bridge tool calls in a single request. Reduces latency for multi-step workflows.",
50
+ summary: "Runs an ordered list of tool commands and returns all results with per-command timing.",
51
+ inputSchema: executeBatchArgsSchema.jsonSchema,
52
+ tags: ["batch", "orchestration", "grouped"],
53
+ examples: [
54
+ {
55
+ name: "Read registers then step",
56
+ description: "Get registers, step one instruction, get registers again",
57
+ arguments: {
58
+ commands: [
59
+ { tool: "c64_debug", args: { op: "get_registers" }, description: "before" },
60
+ { tool: "c64_debug", args: { op: "step", count: 1 }, description: "step" },
61
+ { tool: "c64_debug", args: { op: "get_registers" }, description: "after" },
62
+ ],
63
+ },
64
+ },
65
+ {
66
+ name: "Clear screen and type text",
67
+ description: "Fill screen RAM then feed keyboard",
68
+ arguments: {
69
+ commands: [
70
+ { tool: "c64_memory", args: { op: "fill_memory", address: "$0400", length: 1000, pattern: "20" } },
71
+ { tool: "c64_input", args: { op: "write_text", text: "HELLO{RETURN}" } },
72
+ ],
73
+ },
74
+ },
75
+ ],
76
+ execute: async (rawArgs, ctx) => {
77
+ try {
78
+ const parsed = executeBatchArgsSchema.parse(rawArgs);
79
+ const stopOnError = parsed.stopOnError !== false;
80
+ // Lazy import to avoid circular dependency at module-init time.
81
+ const { toolRegistry } = await import("./index.js");
82
+ const results = [];
83
+ let succeeded = 0;
84
+ let failed = 0;
85
+ const batchStart = Date.now();
86
+ for (let i = 0; i < parsed.commands.length; i++) {
87
+ const cmd = parsed.commands[i];
88
+ const toolName = typeof cmd.tool === "string" ? cmd.tool : "";
89
+ const args = (typeof cmd.args === "object" && cmd.args !== null) ? cmd.args : {};
90
+ const label = typeof cmd.description === "string" ? cmd.description : undefined;
91
+ if (!toolName) {
92
+ const err = { index: i, tool: String(cmd.tool), description: label, success: false, error: "tool name must be a non-empty string", elapsedMs: 0 };
93
+ results.push(err);
94
+ failed++;
95
+ if (stopOnError)
96
+ break;
97
+ continue;
98
+ }
99
+ if (toolName === "c64_batch") {
100
+ results.push({
101
+ index: i,
102
+ tool: toolName,
103
+ description: label,
104
+ success: false,
105
+ error: "Nested c64_batch execution is not supported.",
106
+ elapsedMs: 0,
107
+ });
108
+ failed++;
109
+ if (stopOnError)
110
+ break;
111
+ continue;
112
+ }
113
+ const cmdStart = Date.now();
114
+ try {
115
+ const toolResult = await toolRegistry.invoke(toolName, args, {
116
+ ...ctx,
117
+ platform: getPlatformStatus(),
118
+ });
119
+ const elapsedMs = Date.now() - cmdStart;
120
+ const isError = toolResult.isError === true;
121
+ const text = toolResult.content.map((c) => c.text).join("\n");
122
+ const resultPayload = toolResult.structuredContent?.data ?? text;
123
+ if (isError) {
124
+ results.push({ index: i, tool: toolName, description: label, success: false, error: text, elapsedMs });
125
+ failed++;
126
+ if (stopOnError)
127
+ break;
128
+ }
129
+ else {
130
+ results.push({ index: i, tool: toolName, description: label, success: true, result: resultPayload, elapsedMs });
131
+ succeeded++;
132
+ }
133
+ }
134
+ catch (err) {
135
+ const elapsedMs = Date.now() - cmdStart;
136
+ const message = err instanceof Error ? err.message : String(err);
137
+ results.push({ index: i, tool: toolName, description: label, success: false, error: message, elapsedMs });
138
+ failed++;
139
+ if (stopOnError)
140
+ break;
141
+ }
142
+ }
143
+ const totalElapsedMs = Date.now() - batchStart;
144
+ const summary = {
145
+ total: parsed.commands.length,
146
+ executed: results.length,
147
+ succeeded,
148
+ failed,
149
+ elapsedMs: totalElapsedMs,
150
+ results,
151
+ };
152
+ ctx.logger.info("Batch complete", { total: parsed.commands.length, succeeded, failed, elapsedMs: totalElapsedMs });
153
+ return jsonResult(summary, { success: failed === 0, succeeded, failed, total: parsed.commands.length });
154
+ }
155
+ catch (error) {
156
+ if (error instanceof ToolValidationError) {
157
+ return {
158
+ content: [{ type: "text", text: error.message }],
159
+ isError: true,
160
+ };
161
+ }
162
+ return unknownErrorResult(error);
163
+ }
164
+ },
165
+ },
166
+ ],
167
+ });
@@ -3,8 +3,8 @@ import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema,
3
3
  import { graphicsModule, graphicsOperationHandlers as groupedGraphicsHandlers } from "../graphics.js";
4
4
  import { booleanSchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
5
5
  import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, } from "./utils.js";
6
- import { ToolError, toolErrorResult, unknownErrorResult, } from "../errors.js";
7
6
  import { jsonResult } from "../responses.js";
7
+ import { ToolError, toolErrorResult, unknownErrorResult, } from "../errors.js";
8
8
  const graphicsDescriptorIndex = buildDescriptorIndex(graphicsModule);
9
9
  const captureFrameArgsSchema = objectSchema({
10
10
  description: "Capture one or more complete video frames from the active backend.",
@@ -30,6 +30,72 @@ const captureFrameArgsSchema = objectSchema({
30
30
  required: ["op"],
31
31
  additionalProperties: false,
32
32
  });
33
+ // ---------------------------------------------------------------------------
34
+ // get_display_state helpers — VIC-II register decoding
35
+ // ---------------------------------------------------------------------------
36
+ function parseVicState(d011, d016, d018, dd00, d020, d021) {
37
+ const bank = (~dd00) & 0x03; // CIA2 Port A bits 0-1 select VIC bank (inverted)
38
+ const bankBase = bank * 0x4000;
39
+ const bitmapMode = (d011 & 0x20) !== 0;
40
+ const extendedColor = (d011 & 0x40) !== 0;
41
+ const multicolor = (d016 & 0x10) !== 0;
42
+ const screenVisible = (d011 & 0x10) !== 0;
43
+ const raster8 = (d011 & 0x80) !== 0;
44
+ const vertScroll = d011 & 0x07;
45
+ const horizScroll = d016 & 0x07;
46
+ const columns40 = (d016 & 0x08) !== 0;
47
+ const rows25 = (d011 & 0x08) !== 0;
48
+ // D018 bits 4-7 = screen RAM offset (× $0400); bits 1-3 = char/bitmap pointer (× $0800)
49
+ const screenRamOffset = ((d018 >> 4) & 0x0f) * 0x0400;
50
+ const screenRamAddress = bankBase + screenRamOffset;
51
+ const charOrBitmapPointer = ((d018 >> 1) & 0x07) * 0x0800;
52
+ const charOrBitmapAddress = bankBase + charOrBitmapPointer;
53
+ let mode;
54
+ if (bitmapMode && multicolor)
55
+ mode = "multicolor_bitmap";
56
+ else if (bitmapMode)
57
+ mode = "hires_bitmap";
58
+ else if (extendedColor)
59
+ mode = "extended_color_text";
60
+ else if (multicolor)
61
+ mode = "multicolor_text";
62
+ else
63
+ mode = "standard_text";
64
+ return {
65
+ mode,
66
+ bank,
67
+ bankBase: `$${bankBase.toString(16).toUpperCase().padStart(4, "0")}`,
68
+ screenRamAddress: `$${screenRamAddress.toString(16).toUpperCase().padStart(4, "0")}`,
69
+ charOrBitmapAddress: `$${charOrBitmapAddress.toString(16).toUpperCase().padStart(4, "0")}`,
70
+ screenVisible,
71
+ bitmapMode,
72
+ extendedColor,
73
+ multicolor,
74
+ rows25,
75
+ columns40,
76
+ vertScroll,
77
+ horizScroll,
78
+ raster8,
79
+ borderColor: d020 & 0x0f,
80
+ backgroundColor: d021 & 0x0f,
81
+ registers: {
82
+ d011: `$${d011.toString(16).toUpperCase().padStart(2, "0")}`,
83
+ d016: `$${d016.toString(16).toUpperCase().padStart(2, "0")}`,
84
+ d018: `$${d018.toString(16).toUpperCase().padStart(2, "0")}`,
85
+ d020: `$${d020.toString(16).toUpperCase().padStart(2, "0")}`,
86
+ d021: `$${d021.toString(16).toUpperCase().padStart(2, "0")}`,
87
+ dd00: `$${dd00.toString(16).toUpperCase().padStart(2, "0")}`,
88
+ },
89
+ };
90
+ }
91
+ const getDisplayStateArgsSchema = objectSchema({
92
+ description: "Read VIC-II and CIA2 registers to determine the current graphics mode and memory layout. Works on C64U and VICE; both backends are read through shared memory primitives so the response shape is identical.",
93
+ properties: {
94
+ op: literalSchema("get_display_state"),
95
+ },
96
+ required: ["op"],
97
+ additionalProperties: false,
98
+ });
33
99
  const graphicsOperations = [
34
100
  {
35
101
  op: "capture_frame",
@@ -105,6 +171,35 @@ const graphicsOperations = [
105
171
  schema: extendSchemaWithOp("render_bitmap", ensureDescriptor(graphicsDescriptorIndex, "render_bitmap").inputSchema, { description: "Import an image file, convert it to VIC-II bitmap memory, write it into RAM, and display it." }),
106
172
  handler: groupedGraphicsHandlers.render_bitmap,
107
173
  },
174
+ {
175
+ op: "get_display_state",
176
+ schema: getDisplayStateArgsSchema.jsonSchema,
177
+ handler: async (_rawArgs, ctx) => {
178
+ try {
179
+ // Use cross-platform memory reads so the same handler works on both
180
+ // C64U and VICE. The VIC-II and CIA2 registers live in machine memory
181
+ // either way; the underlying facade hides the transport difference.
182
+ const vicRegs = await ctx.client.readMemoryRaw(0xD011, 8);
183
+ const colorRegs = await ctx.client.readMemoryRaw(0xD020, 2);
184
+ const cia2 = await ctx.client.readMemoryRaw(0xDD00, 1);
185
+ const d011 = vicRegs[0] ?? 0;
186
+ const d016 = vicRegs[5] ?? 0; // $D016 = $D011 + 5
187
+ const d018 = vicRegs[7] ?? 0; // $D018 = $D011 + 7
188
+ const d020 = colorRegs[0] ?? 0;
189
+ const d021 = colorRegs[1] ?? 0;
190
+ const dd00 = cia2[0] ?? 0x03;
191
+ const backend = await ctx.client.getActiveBackendType();
192
+ const state = { ...parseVicState(d011, d016, d018, dd00, d020, d021), backend };
193
+ ctx.logger.info("Read VIC-II display state", { mode: state.mode, bank: state.bank, backend });
194
+ return jsonResult(state, { success: true, mode: state.mode, backend });
195
+ }
196
+ catch (error) {
197
+ if (error instanceof ToolError)
198
+ return toolErrorResult(error);
199
+ return unknownErrorResult(error);
200
+ }
201
+ },
202
+ },
108
203
  ];
109
204
  const graphicsOperationHandlers = createOperationHandlers(graphicsOperations);
110
205
  export const graphicsModuleGroup = defineToolModule({
@@ -154,6 +249,11 @@ export const graphicsModuleGroup = defineToolModule({
154
249
  description: "Convert a PNG into VIC bitmap memory and display it",
155
250
  arguments: { op: "render_bitmap", imagePath: "./artifacts/sample.png", format: "hires" },
156
251
  },
252
+ {
253
+ name: "Read VIC-II state",
254
+ description: "Decode graphics mode, memory layout, and colours from VIC-II registers (cross-platform)",
255
+ arguments: { op: "get_display_state" },
256
+ },
157
257
  ],
158
258
  execute: createOperationDispatcher("c64_graphics", graphicsOperationHandlers),
159
259
  },
@@ -14,6 +14,8 @@ import { extractModule } from "./extract.js";
14
14
  import { streamModule } from "./stream.js";
15
15
  import { debugModuleGroup as debugModule } from "../debug.js";
16
16
  import { viceModuleGroup as viceModule } from "../vice.js";
17
+ import { inputModuleGroup as inputModule } from "./input.js";
18
+ import { batchModuleGroup as batchModule } from "./batch.js";
17
19
  const modules = [
18
20
  programModule,
19
21
  memoryModule,
@@ -30,6 +32,8 @@ const modules = [
30
32
  streamModule,
31
33
  debugModule,
32
34
  viceModule,
35
+ inputModule,
36
+ batchModule,
33
37
  ];
34
38
  const toolMap = new Map();
35
39
  for (const module of modules) {
@@ -0,0 +1,2 @@
1
+ import { inputModule } from "../input.js";
2
+ export { inputModule as inputModuleGroup };
@@ -1,5 +1,5 @@
1
1
  import { createOperationDispatcher, defineToolModule, discriminatedUnionSchema, } from "../types.js";
2
- import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers } from "../memory.js";
2
+ import { memoryModule, memoryOperationHandlers as groupedMemoryHandlers, disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, } from "../memory.js";
3
3
  import { metaModule } from "../meta/index.js";
4
4
  import { buildDescriptorIndex, ensureDescriptor, extendSchemaWithOp, createOperationHandlers, invokeModuleTool, } from "./utils.js";
5
5
  const memoryDescriptorIndex = buildDescriptorIndex(memoryModule);
@@ -25,6 +25,36 @@ const memoryOperations = [
25
25
  schema: extendSchemaWithOp("wait_for_text", ensureDescriptor(metaDescriptorIndex, "wait_for_screen_text").inputSchema, { description: "Poll the screen until a substring or regex appears, or timeout elapses." }),
26
26
  handler: async (rawArgs, ctx) => invokeModuleTool(metaModule, "wait_for_screen_text", rawArgs, ctx),
27
27
  },
28
+ {
29
+ op: "disassemble",
30
+ schema: extendSchemaWithOp("disassemble", disassembleArgsSchema.jsonSchema, { description: "Disassemble a memory region into annotated 6502/6510 instructions, including undocumented opcodes with canonical names. Symbol annotations from `.vs` files are applied when available. Works on both C64U and VICE." }),
31
+ handler: groupedMemoryHandlers.disassemble,
32
+ },
33
+ {
34
+ op: "copy_memory",
35
+ schema: extendSchemaWithOp("copy_memory", copyMemoryArgsSchema.jsonSchema, { description: "Copy a RAM region to another address." }),
36
+ handler: groupedMemoryHandlers.copy_memory,
37
+ },
38
+ {
39
+ op: "fill_memory",
40
+ schema: extendSchemaWithOp("fill_memory", fillMemoryArgsSchema.jsonSchema, { description: "Fill a memory range with a repeating byte pattern." }),
41
+ handler: groupedMemoryHandlers.fill_memory,
42
+ },
43
+ {
44
+ op: "search_memory",
45
+ schema: extendSchemaWithOp("search_memory", searchMemoryArgsSchema.jsonSchema, { description: "Search for a byte pattern within a memory range and return matching addresses." }),
46
+ handler: groupedMemoryHandlers.search_memory,
47
+ },
48
+ {
49
+ op: "compare_memory",
50
+ schema: extendSchemaWithOp("compare_memory", compareMemoryArgsSchema.jsonSchema, { description: "Compare two memory regions byte-by-byte and report differences." }),
51
+ handler: groupedMemoryHandlers.compare_memory,
52
+ },
53
+ {
54
+ op: "save_memory",
55
+ schema: extendSchemaWithOp("save_memory", saveMemoryArgsSchema.jsonSchema, { description: "Dump a memory range to a local file, with an optional PRG load-address header." }),
56
+ handler: groupedMemoryHandlers.save_memory,
57
+ },
28
58
  ];
29
59
  const memoryOperationHandlers = createOperationHandlers(memoryOperations);
30
60
  export const memoryModuleGroup = defineToolModule({
@@ -59,6 +89,36 @@ export const memoryModuleGroup = defineToolModule({
59
89
  description: "Poll until the READY. prompt appears",
60
90
  arguments: { op: "wait_for_text", pattern: "READY." },
61
91
  },
92
+ {
93
+ name: "Disassemble entry point",
94
+ description: "Disassemble 20 instructions starting at $0810",
95
+ arguments: { op: "disassemble", address: "$0810", instructionCount: 20 },
96
+ },
97
+ {
98
+ name: "Copy screen RAM",
99
+ description: "Copy 1000 bytes of screen RAM to a scratch area",
100
+ arguments: { op: "copy_memory", source: "$0400", dest: "$C000", length: 1000 },
101
+ },
102
+ {
103
+ name: "Clear screen RAM",
104
+ description: "Fill screen RAM with space character ($20)",
105
+ arguments: { op: "fill_memory", address: "$0400", length: 1000, pattern: "20" },
106
+ },
107
+ {
108
+ name: "Search for RTS",
109
+ description: "Find RTS ($60) instructions in BASIC ROM",
110
+ arguments: { op: "search_memory", startAddress: "$A000", endAddress: "$BFFF", pattern: "60" },
111
+ },
112
+ {
113
+ name: "Compare two buffers",
114
+ description: "Check whether two 256-byte regions differ",
115
+ arguments: { op: "compare_memory", address1: "$C000", address2: "$C100", length: 256 },
116
+ },
117
+ {
118
+ name: "Save PRG dump",
119
+ description: "Save $0801-$CFFF as a PRG file",
120
+ arguments: { op: "save_memory", startAddress: "$0801", endAddress: "$CFFF", filePath: "/tmp/dump.prg" },
121
+ },
62
122
  ],
63
123
  execute: createOperationDispatcher("c64_memory", memoryOperationHandlers),
64
124
  },
@@ -195,15 +195,14 @@ function isPlainObject(value) {
195
195
  }
196
196
  export function objectSchema(options) {
197
197
  const required = options.required ?? [];
198
+ const additionalProperties = options.additionalProperties ?? false;
198
199
  const propertyEntries = Object.entries(options.properties).map(([key, schema]) => [key, schema.jsonSchema]);
199
200
  const jsonSchema = {
200
201
  type: "object",
201
202
  ...(options.description ? { description: options.description } : {}),
202
203
  properties: Object.fromEntries(propertyEntries),
203
204
  ...(required.length > 0 ? { required: required.map((key) => key) } : {}),
204
- ...(options.additionalProperties !== undefined
205
- ? { additionalProperties: options.additionalProperties }
206
- : { additionalProperties: false }),
205
+ additionalProperties,
207
206
  };
208
207
  return createSchema(jsonSchema, (value, path) => {
209
208
  ensureType(isPlainObject(value), "Expected an object", path, {
@@ -234,7 +233,7 @@ export function objectSchema(options) {
234
233
  }
235
234
  }
236
235
  }
237
- if (options.additionalProperties === false) {
236
+ if (!additionalProperties) {
238
237
  for (const key of Object.keys(input)) {
239
238
  if (!hasOwn.call(options.properties, key)) {
240
239
  throw new ToolValidationError("Unexpected property", {
@@ -243,7 +242,7 @@ export function objectSchema(options) {
243
242
  }
244
243
  }
245
244
  }
246
- else if (options.additionalProperties === true) {
245
+ else {
247
246
  for (const key of Object.keys(input)) {
248
247
  if (!hasOwn.call(options.properties, key)) {
249
248
  result[key] = input[key];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * In-process symbol registry for VICE debug sessions.
3
+ * Maps addresses to label names so the disassembler can annotate output.
4
+ * Cleared and repopulated each time a new program is loaded.
5
+ */
6
+ const viceAddressToLabel = new Map();
7
+ export function setViceSymbols(nameToAddress) {
8
+ viceAddressToLabel.clear();
9
+ for (const [name, address] of nameToAddress) {
10
+ if (address >= 0 && address <= 0xffff) {
11
+ viceAddressToLabel.set(address, name);
12
+ }
13
+ }
14
+ }
15
+ export function getViceSymbol(address) {
16
+ return viceAddressToLabel.get(address);
17
+ }
18
+ export function clearViceSymbols() {
19
+ viceAddressToLabel.clear();
20
+ }
21
+ export function getViceSymbols() {
22
+ return viceAddressToLabel;
23
+ }
24
+ /**
25
+ * Parse a VICE symbol file (.vs) into a name->address map.
26
+ *
27
+ * Supported line formats:
28
+ * add_label 0x0810 .main
29
+ * add_label C:0810 .main
30
+ * al C:0810 .main
31
+ * al 0810 .main
32
+ */
33
+ export function parseViceSymbolFile(content) {
34
+ const symbols = new Map();
35
+ for (const rawLine of content.split("\n")) {
36
+ const line = rawLine.trim();
37
+ if (!line || line.startsWith(";") || line.startsWith("#"))
38
+ continue;
39
+ const match = /^(?:add_label|al)\s+(?:[A-Za-z]+:)?(?:0x|\$)?([0-9A-Fa-f]+)\s+\.?(\S+)/i.exec(line);
40
+ if (match) {
41
+ const addr = parseInt(match[1], 16);
42
+ const name = match[2];
43
+ if (!isNaN(addr) && addr >= 0 && addr <= 0xffff && name) {
44
+ symbols.set(name, addr);
45
+ }
46
+ }
47
+ }
48
+ return symbols;
49
+ }
@@ -36,16 +36,30 @@ export function discriminatedUnionSchema(options) {
36
36
  const opValues = [];
37
37
  const mergedProperties = {};
38
38
  const opDescriptions = [];
39
+ const operationMetadata = [];
39
40
  for (const variant of variants) {
40
41
  const variantProps = variant.properties ?? {};
41
42
  const discProp = variantProps[discriminator];
42
43
  const opValue = discProp?.const;
44
+ const variantDesc = variant.description ?? discProp?.description ?? "";
45
+ const requiredInputs = (variant.required ?? []).filter((name) => name !== discriminator);
46
+ const optionalInputs = Object.keys(variantProps).filter((name) => name !== discriminator && !requiredInputs.includes(name));
43
47
  if (opValue) {
44
48
  opValues.push(opValue);
45
- const variantDesc = variant.description ?? discProp?.description ?? "";
46
- if (variantDesc) {
47
- opDescriptions.push(`${opValue}: ${variantDesc}`);
48
- }
49
+ const requiredSummary = requiredInputs.length > 0
50
+ ? ` Required inputs: ${requiredInputs.join(", ")}.`
51
+ : " Required inputs: none.";
52
+ const optionalSummary = optionalInputs.length > 0
53
+ ? ` Optional inputs: ${optionalInputs.join(", ")}.`
54
+ : "";
55
+ opDescriptions.push(`${opValue}: ${variantDesc || `Operation ${opValue}`}${requiredSummary}${optionalSummary}`);
56
+ operationMetadata.push({
57
+ op: opValue,
58
+ ...(variantDesc ? { description: variantDesc } : {}),
59
+ required: requiredInputs,
60
+ optional: optionalInputs,
61
+ inputSchema: variant,
62
+ });
49
63
  }
50
64
  for (const [key, propSchema] of Object.entries(variantProps)) {
51
65
  if (key === discriminator)
@@ -74,6 +88,7 @@ export function discriminatedUnionSchema(options) {
74
88
  },
75
89
  required: [discriminator],
76
90
  additionalProperties: false,
91
+ ...(operationMetadata.length > 0 ? { "x-c64bridge-operations": operationMetadata } : {}),
77
92
  };
78
93
  return schema;
79
94
  }