c64bridge 0.9.3 → 1.0.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.
Files changed (48) 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 +127 -5
  5. package/dist/config.js +12 -8
  6. package/dist/device.js +75 -45
  7. package/dist/mcp-server.js +11 -3
  8. package/dist/platform.js +13 -2
  9. package/dist/tools/audio.js +1 -1
  10. package/dist/tools/developer.js +6 -6
  11. package/dist/tools/graphics.js +4 -4
  12. package/dist/tools/input.js +129 -12
  13. package/dist/tools/machineControl.js +85 -6
  14. package/dist/tools/memory.js +1 -1
  15. package/dist/tools/meta/index.js +1 -1
  16. package/dist/tools/programRunners.js +5 -5
  17. package/dist/tools/rag.js +1 -1
  18. package/dist/tools/registry/batch.js +1 -1
  19. package/dist/tools/registry/config.js +5 -5
  20. package/dist/tools/registry/disk.js +2 -2
  21. package/dist/tools/registry/drive.js +2 -2
  22. package/dist/tools/registry/graphics.js +2 -2
  23. package/dist/tools/registry/memory.js +2 -2
  24. package/dist/tools/registry/platform.js +7 -5
  25. package/dist/tools/registry/program.js +5 -5
  26. package/dist/tools/registry/rag.js +1 -1
  27. package/dist/tools/registry/sound.js +4 -4
  28. package/dist/tools/registry/system.js +13 -3
  29. package/dist/tools/storage.js +1 -1
  30. package/doc/c64u/c64-openapi.yaml +1130 -774
  31. package/doc/u2/u2-openapi.yaml +1192 -0
  32. package/generated/c64/index.js +142 -9
  33. package/generated/openapi/c64u/index.js +1117 -0
  34. package/generated/openapi/u2/index.js +929 -0
  35. package/mcp/schemas/c64_graphics.schema.json +3 -3
  36. package/mcp/schemas/c64_input.schema.json +247 -6
  37. package/mcp/schemas/c64_memory.schema.json +3 -3
  38. package/mcp/schemas/c64_program.schema.json +3 -3
  39. package/mcp/schemas/c64_select_backend.schema.json +2 -0
  40. package/mcp/schemas/c64_system.schema.json +41 -1
  41. package/mcp/server.json +1 -1
  42. package/mcp/tools.json +382 -41
  43. package/mcp.json +14 -2
  44. package/package.json +4 -2
  45. package/scripts/mockC64Server.mjs +40 -0
  46. package/scripts/prepare-release.mjs +13 -2
  47. package/scripts/update-readme.ts +18 -30
  48. package/server.json +3 -3
package/dist/c64Client.js CHANGED
@@ -10,14 +10,26 @@ import { createSocket } from "node:dgram";
10
10
  import axios from "axios";
11
11
  import { basicToPrg } from "./tools/translation/basicTokenizer.js";
12
12
  import { assemblyToPrg } from "./tools/translation/assembler.js";
13
- import { screenCodesToAscii } from "./petscii.js";
13
+ import { petsciiToAscii, screenCodesToAscii } from "./petscii.js";
14
14
  import { resolveAddressSymbol } from "./knowledge.js";
15
- import { createAllFacades, createFacade } from "./device.js";
15
+ import { createAllFacades, createFacade, } from "./device.js";
16
16
  import { Api } from "../generated/c64/index.js";
17
17
  import { createLoggingHttpClient } from "./loggingHttpClient.js";
18
18
  import { withDiagnosticSpan, writeDiagnosticEvent } from "./diagnostics.js";
19
19
  import { C64U_NTSC_AUDIO_SAMPLE_RATE, C64U_PAL_AUDIO_SAMPLE_RATE, collectCompleteVideoFrames, parseAudioPacket, parseVideoPacket, } from "./streamCapture.js";
20
20
  import { buildVicBitmapRegisters, resolveVicBitmapMemoryLayout, } from "./vicBitmap.js";
21
+ function menuMatrixIncludes(matrix, expected) {
22
+ // Firmware supplies the character matrix followed by its colour matrix. The
23
+ // documented wire format is raw bytes, so only inspect the first 40×25
24
+ // character cells when that complete layout is present.
25
+ const characters = matrix.length >= 2000 ? matrix.subarray(0, 1000) : matrix;
26
+ const normalizedExpected = expected.replace(/\s+/g, " ").toUpperCase();
27
+ return [screenCodesToAscii(characters), petsciiToAscii(characters)]
28
+ .some((text) => text.replace(/\s+/g, " ").toUpperCase().includes(normalizedExpected));
29
+ }
30
+ function isMissingEndpoint(error) {
31
+ return axios.isAxiosError(error) && error.response?.status === 404;
32
+ }
21
33
  const TEXT_SCREEN_COLUMNS = 40;
22
34
  const TEXT_SCREEN_ROWS = 25;
23
35
  const TEXT_SCREEN_SIZE = TEXT_SCREEN_COLUMNS * TEXT_SCREEN_ROWS;
@@ -45,6 +57,7 @@ export class C64Client {
45
57
  this.warmupPromises = new Map();
46
58
  this.greetingDd00Cache = new Map();
47
59
  this.greetingDd00Warmups = new Map();
60
+ this.machineInputCapabilityProbes = new Map();
48
61
  this.c64uVideoCaptureSession = null;
49
62
  this.activeType = "c64u";
50
63
  this.baseUrl = baseUrl;
@@ -66,13 +79,15 @@ export class C64Client {
66
79
  preferredC64uNetworkPassword: options.networkPassword,
67
80
  });
68
81
  this.facadePromise = allFacadesPromise.then(({ primary }) => primary.facade);
69
- this.initPromise = allFacadesPromise.then(({ primary, secondary, secondaryType }) => {
82
+ this.initPromise = allFacadesPromise.then(({ primary, facades }) => {
70
83
  const primaryPromise = Promise.resolve(primary.facade);
71
84
  this.activeType = primary.selected;
72
85
  this.facadePromise = primaryPromise;
73
86
  this.allFacades.set(primary.selected, primaryPromise);
74
- if (secondary && secondaryType) {
75
- this.allFacades.set(secondaryType, Promise.resolve(secondary));
87
+ for (const [type, facade] of facades) {
88
+ if (type !== primary.selected) {
89
+ this.allFacades.set(type, Promise.resolve(facade));
90
+ }
76
91
  }
77
92
  });
78
93
  }
@@ -90,6 +105,28 @@ export class C64Client {
90
105
  await this.initPromise;
91
106
  return this.activeType;
92
107
  }
108
+ /**
109
+ * Discover native endpoint availability without changing machine state.
110
+ * `machine:input` has a safe GET probe; `machine:menu_screen` is only
111
+ * distinguishable from an inactive menu once a menu is visible.
112
+ */
113
+ async getNativeEndpointCapabilities() {
114
+ const facade = await this.facadePromise;
115
+ if (facade.type === "vice") {
116
+ return { machineInput: "unavailable", machineMenuScreen: "unavailable" };
117
+ }
118
+ if (facade.type === "u2") {
119
+ return { machineInput: "unavailable", machineMenuScreen: "unknown" };
120
+ }
121
+ let probe = this.machineInputCapabilityProbes.get(facade.type);
122
+ if (!probe) {
123
+ probe = facade.getInputState()
124
+ .then(() => "available")
125
+ .catch((error) => isMissingEndpoint(error) ? "unavailable" : "unknown");
126
+ this.machineInputCapabilityProbes.set(facade.type, probe);
127
+ }
128
+ return { machineInput: await probe, machineMenuScreen: "unknown" };
129
+ }
93
130
  getAvailableBackends() {
94
131
  return Array.from(this.allFacades.keys());
95
132
  }
@@ -685,6 +722,79 @@ export class C64Client {
685
722
  return { success: false, details: this.normaliseError(error) };
686
723
  }
687
724
  }
725
+ /**
726
+ * Start from a clean machine state. C64U/U64 firmware exposes this only in
727
+ * the Tool Menu; U2-family firmware uses its equivalent REST reboot and
728
+ * VICE is restarted when it is managed by c64bridge.
729
+ */
730
+ async powerCycle() {
731
+ try {
732
+ const facade = await this.facadePromise;
733
+ if (facade.type === "u2") {
734
+ const result = await facade.reboot();
735
+ return { ...result, details: { strategy: "reboot", result: result.details } };
736
+ }
737
+ if (facade.type === "vice") {
738
+ const result = await facade.powerCycle();
739
+ return { ...result, details: { strategy: "fresh_start", result: result.details } };
740
+ }
741
+ const endpoints = await this.getNativeEndpointCapabilities();
742
+ if (endpoints.machineInput !== "available") {
743
+ return {
744
+ success: false,
745
+ details: {
746
+ code: "native_input_unavailable",
747
+ message: "C64U/U64 Power Cycle requires machine:input and machine:menu_screen, which this firmware does not currently expose.",
748
+ fallback: "Use c64_system reboot if a firmware reboot is acceptable, or power cycle from the device itself.",
749
+ },
750
+ };
751
+ }
752
+ const menuResult = await facade.menuButton();
753
+ if (!menuResult.success) {
754
+ throw new Error("Unable to open the Ultimate menu for power cycling");
755
+ }
756
+ const initial = await this.readVerifiedMenuScreen(facade, "open Ultimate menu");
757
+ await this.requirePowerCycleInput(facade, ["f1"]);
758
+ await this.readVerifiedMenuScreen(facade, "open Tool Menu", "TOOL MENU", initial);
759
+ await this.requirePowerCycleInput(facade, ["return"]);
760
+ let current = await this.readVerifiedMenuScreen(facade, "enter Tool Menu", "POWER CYCLE");
761
+ for (let index = 0; index < 4; index += 1) {
762
+ await this.requirePowerCycleInput(facade, ["cursor_up_down"]);
763
+ current = await this.readVerifiedMenuScreen(facade, `move to Power Cycle (${index + 1}/4)`, "POWER CYCLE", current);
764
+ }
765
+ await this.requirePowerCycleInput(facade, ["return"]);
766
+ return { success: true, details: { strategy: "tool_menu", verifiedMenu: "Tool Menu", selectedItem: "Power Cycle" } };
767
+ }
768
+ catch (error) {
769
+ if (isMissingEndpoint(error)) {
770
+ return {
771
+ success: false,
772
+ details: {
773
+ code: "native_menu_screen_unavailable",
774
+ message: "C64U/U64 Power Cycle could not verify Tool Menu navigation because machine:menu_screen is unavailable.",
775
+ fallback: "Use c64_system reboot if a firmware reboot is acceptable, or power cycle from the device itself.",
776
+ },
777
+ };
778
+ }
779
+ return { success: false, details: this.normaliseError(error) };
780
+ }
781
+ }
782
+ async requirePowerCycleInput(facade, inputs) {
783
+ await facade.sendInputEvents({ events: [{ kind: "keyboard", inputs, transition: "tap" }] });
784
+ }
785
+ async readVerifiedMenuScreen(facade, phase, requiredText, previous) {
786
+ const matrix = await facade.readMenuScreen();
787
+ if (matrix.length === 0) {
788
+ throw new Error(`Unable to ${phase}: machine:menu_screen returned an empty matrix`);
789
+ }
790
+ if (previous && Buffer.compare(Buffer.from(previous), Buffer.from(matrix)) === 0) {
791
+ throw new Error(`Unable to ${phase}: machine:menu_screen did not change after menu input`);
792
+ }
793
+ if (requiredText && !menuMatrixIncludes(matrix, requiredText)) {
794
+ throw new Error(`Unable to ${phase}: machine:menu_screen did not contain '${requiredText}'`);
795
+ }
796
+ return matrix;
797
+ }
688
798
  async menuButton() {
689
799
  try {
690
800
  if (await this.shouldUseC64uMockBypass()) {
@@ -698,6 +808,18 @@ export class C64Client {
698
808
  return { success: false, details: this.normaliseError(error) };
699
809
  }
700
810
  }
811
+ async readMenuScreen() {
812
+ const facade = await this.facadePromise;
813
+ return facade.readMenuScreen();
814
+ }
815
+ async getInputState() {
816
+ const facade = await this.facadePromise;
817
+ return facade.getInputState();
818
+ }
819
+ async sendInputEvents(batch) {
820
+ const facade = await this.facadePromise;
821
+ return facade.sendInputEvents(batch);
822
+ }
701
823
  async debugregRead() {
702
824
  try {
703
825
  if (await this.shouldUseC64uMockBypass()) {
package/dist/config.js CHANGED
@@ -34,16 +34,20 @@ export function loadConfig() {
34
34
  }
35
35
  }
36
36
  rawConfig ?? (rawConfig = {});
37
- // New schema: prefer c64u.{host,port,baseUrl}; keep legacy fields as fallback
37
+ // New schema: choose the configured Ultimate hardware profile; retain
38
+ // c64u and legacy fields as the default when C64_MODE is not set to u2.
38
39
  const c64u = rawConfig?.c64u;
40
+ const u2 = rawConfig?.u2;
41
+ const useU2 = process.env.C64_MODE?.trim().toLowerCase() === "u2";
42
+ const hardware = useU2 ? (u2 ?? c64u) : (c64u ?? u2);
39
43
  const vice = rawConfig?.vice;
40
- const parsedC64uHost = parseEndpoint(configuredString(c64u?.host));
41
- const parsedC64uHostname = parseEndpoint(configuredString(c64u?.hostname));
44
+ const parsedC64uHost = parseEndpoint(configuredString(hardware?.host));
45
+ const parsedC64uHostname = parseEndpoint(configuredString(hardware?.hostname));
42
46
  const parsedLegacyHost = parseEndpoint(configuredString(rawConfig?.c64_host));
43
47
  const parsedLegacyIp = parseEndpoint(configuredString(rawConfig?.c64_ip));
44
- const parsedEnvHost = parseEndpoint(configuredString(process.env.C64U_HOST));
48
+ const parsedEnvHost = parseEndpoint(configuredString(useU2 ? process.env.U2_HOST ?? process.env.C64U_HOST : process.env.C64U_HOST));
45
49
  const parsedBaseOverrides = [
46
- parseEndpoint(normaliseBaseUrl(c64u?.baseUrl)),
50
+ parseEndpoint(normaliseBaseUrl(hardware?.baseUrl)),
47
51
  parseEndpoint(normaliseBaseUrl(rawConfig?.baseUrl)),
48
52
  ];
49
53
  const hostCandidates = [
@@ -55,9 +59,9 @@ export function loadConfig() {
55
59
  ...parsedBaseOverrides.map((entry) => entry.hostname),
56
60
  ];
57
61
  const portCandidates = [
58
- configuredPort(process.env.C64U_PORT),
62
+ configuredPort(useU2 ? process.env.U2_PORT ?? process.env.C64U_PORT : process.env.C64U_PORT),
59
63
  parsedEnvHost.port,
60
- configuredPort(c64u?.port),
64
+ configuredPort(hardware?.port),
61
65
  parsedC64uHost.port,
62
66
  parsedC64uHostname.port,
63
67
  configuredPort(rawConfig?.c64_port),
@@ -74,7 +78,7 @@ export function loadConfig() {
74
78
  c64_host: hostWithPort,
75
79
  baseUrl,
76
80
  c64_port: port,
77
- networkPassword: firstDefined(configuredString(process.env.C64U_PASSWORD), configuredString(c64u?.networkPassword), configuredString(rawConfig?.networkPassword)),
81
+ networkPassword: firstDefined(configuredString(useU2 ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD), configuredString(hardware?.networkPassword), configuredString(rawConfig?.networkPassword)),
78
82
  vicePrewarm: firstDefined(configuredBoolean(process.env.VICE_PREWARM), configuredBoolean(vice?.prewarm), configuredBoolean(rawConfig?.vicePrewarm)) ?? false,
79
83
  };
80
84
  cachedConfig = config;
package/dist/device.js CHANGED
@@ -77,10 +77,13 @@ function readConfigFile() {
77
77
  if (!merged.c64u && json?.c64u && typeof json.c64u === "object" && !Array.isArray(json.c64u)) {
78
78
  merged.c64u = json.c64u;
79
79
  }
80
+ if (!merged.u2 && json?.u2 && typeof json.u2 === "object" && !Array.isArray(json.u2)) {
81
+ merged.u2 = json.u2;
82
+ }
80
83
  if (!merged.vice && json?.vice && typeof json.vice === "object" && !Array.isArray(json.vice)) {
81
84
  merged.vice = json.vice;
82
85
  }
83
- if (merged.c64u && merged.vice) {
86
+ if ((merged.c64u || merged.u2) && merged.vice) {
84
87
  break;
85
88
  }
86
89
  }
@@ -90,17 +93,17 @@ function readConfigFile() {
90
93
  return foundCandidate ? merged : null;
91
94
  }
92
95
  class C64uBackend {
93
- constructor(config) {
94
- this.type = "c64u";
95
- const envHost = configuredString(process.env.C64U_HOST);
96
- const envPort = configuredPort(process.env.C64U_PORT);
96
+ constructor(config, type = "c64u") {
97
+ this.type = type;
98
+ const envHost = configuredString(type === "u2" ? process.env.U2_HOST ?? process.env.C64U_HOST : process.env.C64U_HOST);
99
+ const envPort = configuredPort(type === "u2" ? process.env.U2_PORT ?? process.env.C64U_PORT : process.env.C64U_PORT);
97
100
  const configBaseUrl = normaliseBaseUrl(config.baseUrl);
98
101
  const parsedConfigBaseUrl = configBaseUrl ? parseEndpoint(configBaseUrl) : {};
99
102
  const baseUrl = envHost !== undefined || envPort !== undefined
100
103
  ? buildBaseUrl(firstDefined(envHost, configuredString(config.host), configuredString(config.hostname), parsedConfigBaseUrl.hostname) ?? DEFAULT_C64U_HOST, firstDefined(envPort, configuredPort(config.port), parsedConfigBaseUrl.port) ?? DEFAULT_C64U_PORT)
101
104
  : resolveBaseUrl(config);
102
105
  this.baseUrl = baseUrl;
103
- this.networkPassword = firstDefined(configuredString(process.env.C64U_PASSWORD), configuredString(config.networkPassword));
106
+ this.networkPassword = firstDefined(configuredString(type === "u2" ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD), configuredString(config.networkPassword));
104
107
  const http = createLoggingHttpClient({
105
108
  baseURL: baseUrl,
106
109
  timeout: 10000,
@@ -108,6 +111,11 @@ class C64uBackend {
108
111
  });
109
112
  this.api = new Api(http);
110
113
  }
114
+ requireC64u(feature) {
115
+ if (this.type === "u2") {
116
+ throw unsupported(`${feature} is unavailable on U2-family cartridges`);
117
+ }
118
+ }
111
119
  getBaseUrl() { return this.baseUrl; }
112
120
  async ping() {
113
121
  try {
@@ -185,10 +193,26 @@ class C64uBackend {
185
193
  async reboot() { const res = await this.api.v1.machineRebootUpdate(":reboot"); return { success: true, details: res.data }; }
186
194
  async pause() { const res = await this.api.v1.machinePauseUpdate(":pause"); return { success: true, details: res.data }; }
187
195
  async resume() { const res = await this.api.v1.machineResumeUpdate(":resume"); return { success: true, details: res.data }; }
188
- async poweroff() { const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return { success: true, details: res.data }; }
196
+ async poweroff() { this.requireC64u("poweroff"); const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return { success: true, details: res.data }; }
197
+ async powerCycle() { this.requireC64u("powerCycle"); throw unsupported("powerCycle must be coordinated by C64Client"); }
189
198
  async menuButton() { const res = await this.api.v1.machineMenuButtonUpdate(":menu_button"); return { success: true, details: res.data }; }
190
- async debugregRead() { const res = await this.api.v1.machineDebugregList(":debugreg"); return { success: true, value: res.data.value, details: res.data }; }
191
- async debugregWrite(value) { const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value }); return { success: true, value: res.data.value, details: res.data }; }
199
+ async readMenuScreen() {
200
+ const response = await this.api.v1.machineMenuScreenList(":menu_screen", { format: "arraybuffer", headers: { Accept: "application/octet-stream" } });
201
+ const body = response.data;
202
+ return body instanceof ArrayBuffer ? new Uint8Array(body) : extractBytes(body);
203
+ }
204
+ async getInputState() {
205
+ this.requireC64u("getInputState");
206
+ const response = await this.api.v1.machineInputList(":input");
207
+ return response.data;
208
+ }
209
+ async sendInputEvents(batch) {
210
+ this.requireC64u("sendInputEvents");
211
+ const response = await this.api.v1.machineInputCreate(":input", batch);
212
+ return response.data;
213
+ }
214
+ async debugregRead() { this.requireC64u("debugregRead"); const res = await this.api.v1.machineDebugregList(":debugreg"); return { success: true, value: res.data.value, details: res.data }; }
215
+ async debugregWrite(value) { this.requireC64u("debugregWrite"); const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value }); return { success: true, value: res.data.value, details: res.data }; }
192
216
  async version() { const res = await this.api.v1.versionList(); return res.data; }
193
217
  async info() { const res = await this.api.v1.infoList(); return res.data; }
194
218
  async drivesList() { const res = await this.api.v1.drivesList(); return res.data; }
@@ -199,8 +223,8 @@ class C64uBackend {
199
223
  async driveOff(d) { const res = await this.api.v1.drivesOffUpdate(d, ":off"); return { success: true, details: res.data }; }
200
224
  async driveSetMode(d, mode) { const res = await this.api.v1.drivesSetModeUpdate(d, ":set_mode", { mode }); return { success: true, details: res.data }; }
201
225
  async driveLoadRom(d, romPath) { const res = await this.api.v1.drivesLoadRomUpdate(d, ":load_rom", { file: romPath }); return { success: true, details: res.data }; }
202
- async streamStart(s, ip) { const res = await this.api.v1.streamsStartUpdate(s, ":start", { ip }); return { success: true, details: res.data }; }
203
- async streamStop(s) { const res = await this.api.v1.streamsStopUpdate(s, ":stop"); return { success: true, details: res.data }; }
226
+ async streamStart(s, ip) { this.requireC64u("streamStart"); const res = await this.api.v1.streamsStartUpdate(s, ":start", { ip }); return { success: true, details: res.data }; }
227
+ async streamStop(s) { this.requireC64u("streamStop"); const res = await this.api.v1.streamsStopUpdate(s, ":stop"); return { success: true, details: res.data }; }
204
228
  async configsList() { const res = await this.api.v1.configsList(); return res.data; }
205
229
  async configGet(cat, item) { const res = item ? await this.api.v1.configsDetail2(cat, item) : await this.api.v1.configsDetail(cat); return res.data; }
206
230
  async configSet(cat, item, value) { const res = await this.api.v1.configsUpdate(cat, item, { value }); return { success: true, details: res.data }; }
@@ -580,6 +604,7 @@ export class ViceBackend {
580
604
  return { success: false, details };
581
605
  }
582
606
  }
607
+ async powerCycle() { return this.nuclearReset(); }
583
608
  async nuclearReset() {
584
609
  const poweroffResult = await this.poweroff();
585
610
  if (!poweroffResult.success) {
@@ -606,6 +631,9 @@ export class ViceBackend {
606
631
  }
607
632
  }
608
633
  async menuButton() { throw unsupported("menuButton"); }
634
+ async readMenuScreen() { throw unsupported("readMenuScreen"); }
635
+ async getInputState() { throw unsupported("getInputState"); }
636
+ async sendInputEvents(_batch) { throw unsupported("sendInputEvents"); }
609
637
  async debugregRead() { throw unsupported("debugregRead"); }
610
638
  async debugregWrite(_v) { throw unsupported("debugregWrite"); }
611
639
  async version() { return { emulator: "vice", host: this.host, port: this.port }; }
@@ -885,46 +913,53 @@ export function __buildViceProcessOptionsForTests(input) {
885
913
  return buildManagedViceProcessOptions(input);
886
914
  }
887
915
  export async function createFacade(logger, options) {
916
+ const envMode = (process.env.C64_MODE || "").toLowerCase().trim();
888
917
  // Caller-forced preference: use c64u with provided base URL (used by tests and server wiring)
889
918
  if (options?.preferredC64uBaseUrl) {
919
+ const type = envMode === "u2" ? "u2" : "c64u";
890
920
  const backend = new C64uBackend({
891
921
  baseUrl: options.preferredC64uBaseUrl,
892
922
  networkPassword: options.preferredC64uNetworkPassword,
893
- });
894
- logger?.info?.("Active backend: c64u (forced by caller)");
895
- return { facade: backend, selected: "c64u", reason: "forced by caller", details: { baseUrl: options.preferredC64uBaseUrl } };
923
+ }, type);
924
+ logger?.info?.(`Active backend: ${type} (forced by caller)`);
925
+ return { facade: backend, selected: type, reason: "forced by caller", details: { baseUrl: options.preferredC64uBaseUrl } };
896
926
  }
897
927
  const cfg = readConfigFile();
898
- const envMode = (process.env.C64_MODE || "").toLowerCase().trim();
899
928
  const hasC64u = Boolean(cfg?.c64u);
929
+ const hasU2 = Boolean(cfg?.u2);
900
930
  const hasVice = Boolean(cfg?.vice);
901
931
  if (envMode === "c64u") {
902
932
  const backend = new C64uBackend(cfg?.c64u ?? {});
903
933
  logger?.info?.("Active backend: c64u (from env override)");
904
934
  return { facade: backend, selected: "c64u", reason: "env override", details: { baseUrl: backend.getBaseUrl?.() } };
905
935
  }
936
+ if (envMode === "u2") {
937
+ const backend = new C64uBackend(cfg?.u2 ?? {}, "u2");
938
+ logger?.info?.("Active backend: u2 (from env override)");
939
+ return { facade: backend, selected: "u2", reason: "env override", details: { baseUrl: backend.getBaseUrl?.() } };
940
+ }
906
941
  if (envMode === "vice") {
907
942
  const backend = new ViceBackend(cfg?.vice ?? {});
908
943
  logger?.info?.("Active backend: vice (from env override)");
909
944
  const endpoint = backend.getEndpoint();
910
945
  return { facade: backend, selected: "vice", reason: "env override", details: { host: endpoint.host, port: endpoint.port } };
911
946
  }
912
- if (hasC64u && !hasVice) {
947
+ if (hasC64u) {
913
948
  const backend = new C64uBackend(cfg.c64u);
914
949
  logger?.info?.("Active backend: c64u (from config)");
915
- return { facade: backend, selected: "c64u", reason: "config only", details: { baseUrl: backend.getBaseUrl?.() } };
950
+ return { facade: backend, selected: "c64u", reason: hasVice ? "both defined (prefer c64u)" : "config only", details: { baseUrl: backend.getBaseUrl?.() } };
951
+ }
952
+ if (hasU2) {
953
+ const backend = new C64uBackend(cfg.u2, "u2");
954
+ logger?.info?.("Active backend: u2 (from config)");
955
+ return { facade: backend, selected: "u2", reason: "config only", details: { baseUrl: backend.getBaseUrl?.() } };
916
956
  }
917
- if (!hasC64u && hasVice) {
957
+ if (hasVice) {
918
958
  const backend = new ViceBackend(cfg.vice);
919
959
  logger?.info?.("Active backend: vice (from config)");
920
960
  const endpoint = backend.getEndpoint();
921
961
  return { facade: backend, selected: "vice", reason: "config only", details: { host: endpoint.host, port: endpoint.port } };
922
962
  }
923
- if (hasC64u && hasVice) {
924
- const backend = new C64uBackend(cfg.c64u);
925
- logger?.info?.("Active backend: c64u (both defined; default preference)");
926
- return { facade: backend, selected: "c64u", reason: "both defined (prefer c64u)", details: { baseUrl: backend.getBaseUrl?.() } };
927
- }
928
963
  // No configuration
929
964
  const probeBase = resolveBaseUrl({});
930
965
  try {
@@ -942,35 +977,30 @@ export async function createFacade(logger, options) {
942
977
  return { facade: backend, selected: "vice", reason: "fallback (hardware unavailable)", details: { host: endpoint.host, port: endpoint.port } };
943
978
  }
944
979
  export async function createAllFacades(logger, options) {
945
- const primary = await createFacade(logger);
980
+ // C64U/VICE preserve config-file selection semantics. U2 needs the
981
+ // caller-provided endpoint because it is selected through C64_MODE=u2.
982
+ const primary = await createFacade(logger, process.env.C64_MODE?.toLowerCase().trim() === "u2" ? options : undefined);
946
983
  const config = readConfigFile();
947
- const secondaryType = primary.selected === "c64u" ? "vice" : "c64u";
948
- if (secondaryType === "c64u" && shouldProvisionSecondaryC64u(config, options)) {
949
- const secondaryConfig = {
984
+ const facades = new Map([[primary.selected, primary.facade]]);
985
+ if (config?.c64u && primary.selected !== "c64u") {
986
+ facades.set("c64u", new C64uBackend(config.c64u));
987
+ }
988
+ if (config?.u2 && primary.selected !== "u2") {
989
+ facades.set("u2", new C64uBackend(config.u2, "u2"));
990
+ }
991
+ if (config?.vice && primary.selected !== "vice") {
992
+ facades.set("vice", new ViceBackend(config.vice));
993
+ }
994
+ if (!facades.has("c64u") && !config?.u2 && shouldProvisionSecondaryC64u(config, options) && primary.selected !== "u2") {
995
+ facades.set("c64u", new C64uBackend({
950
996
  ...(config?.c64u ?? {}),
951
997
  ...(!config?.c64u?.baseUrl && options?.preferredC64uBaseUrl ? { baseUrl: options.preferredC64uBaseUrl } : {}),
952
998
  ...(!config?.c64u?.networkPassword && options?.preferredC64uNetworkPassword
953
999
  ? { networkPassword: options.preferredC64uNetworkPassword }
954
1000
  : {}),
955
- };
956
- return {
957
- primary,
958
- secondary: new C64uBackend(secondaryConfig),
959
- secondaryType,
960
- };
961
- }
962
- if (secondaryType === "vice" && config?.vice) {
963
- return {
964
- primary,
965
- secondary: new ViceBackend(config.vice),
966
- secondaryType,
967
- };
1001
+ }));
968
1002
  }
969
- return {
970
- primary,
971
- secondary: null,
972
- secondaryType: null,
973
- };
1003
+ return { primary, facades };
974
1004
  }
975
1005
  function shouldProvisionSecondaryC64u(config, options) {
976
1006
  return Boolean(config?.c64u
@@ -165,7 +165,7 @@ async function main() {
165
165
  {
166
166
  uri: PLATFORM_RESOURCE_URI,
167
167
  mimeType: "text/markdown",
168
- text: renderPlatformStatusMarkdown(client),
168
+ text: await renderPlatformStatusMarkdown(client),
169
169
  },
170
170
  ],
171
171
  };
@@ -447,10 +447,11 @@ function createPlatformResourceDescriptor() {
447
447
  },
448
448
  };
449
449
  }
450
- function renderPlatformStatusMarkdown(client) {
450
+ async function renderPlatformStatusMarkdown(client) {
451
451
  const status = getPlatformStatus();
452
452
  const availableBackends = client.getAvailableBackends();
453
453
  const capabilities = describePlatformCapabilities(toolRegistry.list());
454
+ const nativeEndpoints = await client.getNativeEndpointCapabilities();
454
455
  const lines = [
455
456
  "# MCP Platform Status",
456
457
  "",
@@ -466,6 +467,13 @@ function renderPlatformStatusMarkdown(client) {
466
467
  status.limitedFeatures.length > 0
467
468
  ? ["## Limited or Unavailable Features", "", ...status.limitedFeatures.map((feature) => `- ${feature}`)].join("\n")
468
469
  : "",
470
+ "## Native REST Endpoint Availability",
471
+ "",
472
+ `- \`machine:input\`: **${nativeEndpoints.machineInput}**`,
473
+ `- \`machine:menu_screen\`: **${nativeEndpoints.machineMenuScreen}** (it can only be confirmed while an Ultimate menu is visible)`,
474
+ "",
475
+ "Use physical-input, menu-screen, and C64U/U64 Tool Menu power-cycle operations only when the required endpoint is available. Prefer `key` or `write_text` for ordinary C64 input when `machine:input` is unavailable.",
476
+ "",
469
477
  "## Tool Compatibility",
470
478
  "",
471
479
  ].filter(Boolean);
@@ -487,7 +495,7 @@ function renderPlatformStatusMarkdown(client) {
487
495
  }
488
496
  async function logConnectivity(client, baseUrl) {
489
497
  const backendType = await client.getBackendType();
490
- if (backendType !== "c64u") {
498
+ if (backendType === "vice") {
491
499
  writeDiagnosticEvent("connectivity_probe_skipped", { baseUrl, reason: "vice_backend" });
492
500
  console.error("Skipping direct REST connectivity probe because active backend is VICE");
493
501
  try {
package/dist/platform.js CHANGED
@@ -10,6 +10,10 @@ const PLATFORM_FEATURES = {
10
10
  ],
11
11
  limited: [],
12
12
  },
13
+ u2: {
14
+ features: ["ultimate-rest-api", "cartridge-execution", "sid-control", "drive-management", "printer-integration"],
15
+ limited: ["no-machine-input", "no-debugreg", "no-poweroff", "no-streaming"],
16
+ },
13
17
  vice: {
14
18
  features: [
15
19
  "software-emulation",
@@ -23,7 +27,7 @@ const PLATFORM_FEATURES = {
23
27
  limited: ["no-rest-api", "no-firmware-filesystem", "no-flash-config", "limited-sid"],
24
28
  },
25
29
  };
26
- let currentPlatform = process.env.C64_MODE === "vice" ? "vice" : "c64u";
30
+ let currentPlatform = process.env.C64_MODE === "vice" ? "vice" : process.env.C64_MODE === "u2" ? "u2" : "c64u";
27
31
  export function getPlatformStatus() {
28
32
  const spec = PLATFORM_FEATURES[currentPlatform];
29
33
  return {
@@ -48,11 +52,12 @@ export function isPlatformSupported(platform, supported) {
48
52
  export function describePlatformCapabilities(tools) {
49
53
  const map = {
50
54
  c64u: { available: [], unsupported: [] },
55
+ u2: { available: [], unsupported: [] },
51
56
  vice: { available: [], unsupported: [] },
52
57
  };
53
58
  for (const tool of tools) {
54
59
  const supported = tool.metadata.platforms ?? ["c64u"];
55
- for (const id of ["c64u", "vice"]) {
60
+ for (const id of ["c64u", "u2", "vice"]) {
56
61
  if (supported.includes(id)) {
57
62
  map[id].available.push(tool.name);
58
63
  }
@@ -69,6 +74,12 @@ export function describePlatformCapabilities(tools) {
69
74
  tools: map.c64u.available.sort(),
70
75
  unsupported_tools: map.c64u.unsupported.sort(),
71
76
  },
77
+ u2: {
78
+ features: PLATFORM_FEATURES.u2.features,
79
+ limited_features: PLATFORM_FEATURES.u2.limited,
80
+ tools: map.u2.available.sort(),
81
+ unsupported_tools: map.u2.unsupported.sort(),
82
+ },
72
83
  vice: {
73
84
  features: PLATFORM_FEATURES.vice.features,
74
85
  limited_features: PLATFORM_FEATURES.vice.limited,
@@ -403,7 +403,7 @@ export const audioModule = defineToolModule({
403
403
  ],
404
404
  prompts: ["sid-music"],
405
405
  defaultTags: ["sid", "audio"],
406
- supportedPlatforms: ["c64u", "vice"],
406
+ supportedPlatforms: ["c64u", "u2", "vice"],
407
407
  workflowHints: [
408
408
  "Reach for SID helpers when the user talks about sound design, playback quality, or stuck notes.",
409
409
  "After changing playback state, suggest verify-by-ear steps such as analyze_audio so the user gets concrete feedback.",
@@ -146,7 +146,7 @@ export const developerModule = defineToolModule({
146
146
  workflowHints: [
147
147
  "Run first to enumerate categories before reading or writing specific settings; summarise the top-level sections for the user.",
148
148
  ],
149
- supportedPlatforms: ["c64u", "vice"],
149
+ supportedPlatforms: ["c64u", "u2", "vice"],
150
150
  async execute(args, ctx) {
151
151
  try {
152
152
  noArgsSchema.parse(args ?? {});
@@ -182,7 +182,7 @@ export const developerModule = defineToolModule({
182
182
  workflowHints: [
183
183
  "Use after config_list to fetch details; restate category/item so the user can cross-check values before changes.",
184
184
  ],
185
- supportedPlatforms: ["c64u", "vice"],
185
+ supportedPlatforms: ["c64u", "u2", "vice"],
186
186
  async execute(args, ctx) {
187
187
  try {
188
188
  const parsed = configGetArgsSchema.parse(args ?? {});
@@ -218,7 +218,7 @@ export const developerModule = defineToolModule({
218
218
  workflowHints: [
219
219
  "Confirm the user-supplied value before writing; mention whether a reboot is required for the change to take effect.",
220
220
  ],
221
- supportedPlatforms: ["c64u", "vice"],
221
+ supportedPlatforms: ["c64u", "u2", "vice"],
222
222
  async execute(args, ctx) {
223
223
  try {
224
224
  const parsed = configSetArgsSchema.parse(args ?? {});
@@ -262,7 +262,7 @@ export const developerModule = defineToolModule({
262
262
  workflowHints: [
263
263
  "Use when multiple categories must be updated together; remind the user to review validation errors closely.",
264
264
  ],
265
- supportedPlatforms: ["c64u", "vice"],
265
+ supportedPlatforms: ["c64u", "u2", "vice"],
266
266
  async execute(args, ctx) {
267
267
  try {
268
268
  const payload = configBatchSchema.parse(args ?? {});
@@ -405,7 +405,7 @@ export const developerModule = defineToolModule({
405
405
  name: "version",
406
406
  description: "Retrieve firmware or emulator version information.",
407
407
  summary: "Calls the firmware version endpoint (Ultimate) or returns emulator info (VICE).",
408
- supportedPlatforms: ["c64u", "vice"],
408
+ supportedPlatforms: ["c64u", "u2", "vice"],
409
409
  inputSchema: noArgsSchema.jsonSchema,
410
410
  relatedResources: ["c64://guide/bootstrap"],
411
411
  tags: ["diagnostics", "version"],
@@ -437,7 +437,7 @@ export const developerModule = defineToolModule({
437
437
  name: "info",
438
438
  description: "Retrieve hardware or emulator information and status.",
439
439
  summary: "Returns the diagnostics payload reported by the firmware (Ultimate) or emulator (VICE).",
440
- supportedPlatforms: ["c64u", "vice"],
440
+ supportedPlatforms: ["c64u", "u2", "vice"],
441
441
  inputSchema: noArgsSchema.jsonSchema,
442
442
  relatedResources: ["c64://guide/bootstrap"],
443
443
  tags: ["diagnostics", "info"],
@@ -577,7 +577,7 @@ export const graphicsModule = defineToolModule({
577
577
  "Use when the user supplies sprite bytes or asks to preview graphics quickly; describe resulting coordinates and colours.",
578
578
  "Remind the user that sprites live in banked memory so further tweaks may require write_memory calls.",
579
579
  ],
580
- supportedPlatforms: ["c64u", "vice"],
580
+ supportedPlatforms: ["c64u", "u2", "vice"],
581
581
  async execute(args, ctx) {
582
582
  return executeGenerateSprite(args, ctx);
583
583
  },
@@ -594,7 +594,7 @@ export const graphicsModule = defineToolModule({
594
594
  "Use when the user wants to display an external image on screen rather than generate PETSCII art.",
595
595
  "Call out the selected bitmap and screen RAM addresses so follow-up memory inspection or raster work stays grounded.",
596
596
  ],
597
- supportedPlatforms: ["c64u", "vice"],
597
+ supportedPlatforms: ["c64u", "u2", "vice"],
598
598
  async execute(args, ctx) {
599
599
  return executeGenerateBitmap(args, ctx);
600
600
  },
@@ -619,7 +619,7 @@ export const graphicsModule = defineToolModule({
619
619
  "Call after generating PETSCII text or when the user wants border/background colour changes applied.",
620
620
  "Echo the colour indices and mention CLEAR + PRINT so the user knows what ran.",
621
621
  ],
622
- supportedPlatforms: ["c64u", "vice"],
622
+ supportedPlatforms: ["c64u", "u2", "vice"],
623
623
  async execute(args, ctx) {
624
624
  return executeRenderPetscii(args, ctx);
625
625
  },
@@ -645,7 +645,7 @@ export const graphicsModule = defineToolModule({
645
645
  "Response includes petsciiCodes array and glyphs for character-level inspection.",
646
646
  "Provide follow-up suggestions like saving the PRG or capturing the screen after rendering.",
647
647
  ],
648
- supportedPlatforms: ["c64u", "vice"],
648
+ supportedPlatforms: ["c64u", "u2", "vice"],
649
649
  async execute(args, ctx) {
650
650
  return executeCreatePetscii(args, ctx);
651
651
  },