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.
- package/AGENTS.md +19 -3
- package/README.md +190 -148
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +127 -5
- package/dist/config.js +12 -8
- package/dist/device.js +75 -45
- package/dist/mcp-server.js +11 -3
- package/dist/platform.js +13 -2
- package/dist/tools/audio.js +1 -1
- package/dist/tools/developer.js +6 -6
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +129 -12
- package/dist/tools/machineControl.js +85 -6
- package/dist/tools/memory.js +1 -1
- package/dist/tools/meta/index.js +1 -1
- package/dist/tools/programRunners.js +5 -5
- package/dist/tools/rag.js +1 -1
- package/dist/tools/registry/batch.js +1 -1
- package/dist/tools/registry/config.js +5 -5
- package/dist/tools/registry/disk.js +2 -2
- package/dist/tools/registry/drive.js +2 -2
- package/dist/tools/registry/graphics.js +2 -2
- package/dist/tools/registry/memory.js +2 -2
- package/dist/tools/registry/platform.js +7 -5
- package/dist/tools/registry/program.js +5 -5
- package/dist/tools/registry/rag.js +1 -1
- package/dist/tools/registry/sound.js +4 -4
- package/dist/tools/registry/system.js +13 -3
- package/dist/tools/storage.js +1 -1
- package/doc/c64u/c64-openapi.yaml +1130 -774
- package/doc/u2/u2-openapi.yaml +1192 -0
- package/generated/c64/index.js +142 -9
- package/generated/openapi/c64u/index.js +1117 -0
- package/generated/openapi/u2/index.js +929 -0
- package/mcp/schemas/c64_graphics.schema.json +3 -3
- package/mcp/schemas/c64_input.schema.json +247 -6
- package/mcp/schemas/c64_memory.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +3 -3
- package/mcp/schemas/c64_select_backend.schema.json +2 -0
- package/mcp/schemas/c64_system.schema.json +41 -1
- package/mcp/server.json +1 -1
- package/mcp/tools.json +382 -41
- package/mcp.json +14 -2
- package/package.json +4 -2
- package/scripts/mockC64Server.mjs +40 -0
- package/scripts/prepare-release.mjs +13 -2
- package/scripts/update-readme.ts +18 -30
- 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,
|
|
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
|
-
|
|
75
|
-
|
|
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:
|
|
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(
|
|
41
|
-
const parsedC64uHostname = parseEndpoint(configuredString(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
191
|
-
|
|
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?.(
|
|
895
|
-
return { facade: backend, selected:
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
948
|
-
if (
|
|
949
|
-
|
|
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
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
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,
|
package/dist/tools/audio.js
CHANGED
|
@@ -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.",
|
package/dist/tools/developer.js
CHANGED
|
@@ -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"],
|
package/dist/tools/graphics.js
CHANGED
|
@@ -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
|
},
|