c64bridge 0.9.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +19 -3
- package/README.md +190 -148
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +277 -41
- package/dist/config.js +30 -18
- package/dist/device.js +191 -125
- package/dist/knowledge.js +1 -1
- package/dist/loggingHttpClient.js +15 -3
- package/dist/mcp-server.js +65 -16
- package/dist/platform.js +13 -2
- package/dist/rag/indexer.js +11 -8
- package/dist/rag/init.js +11 -8
- package/dist/sidwaveCompiler.js +2 -2
- package/dist/tools/audio.js +27 -6
- package/dist/tools/developer.js +6 -6
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +136 -24
- package/dist/tools/machineControl.js +85 -6
- package/dist/tools/memory.js +21 -6
- package/dist/tools/meta/artifacts.js +20 -8
- package/dist/tools/meta/background.js +11 -1
- package/dist/tools/meta/index.js +1 -1
- package/dist/tools/pollValidator.js +19 -51
- package/dist/tools/printer.js +1 -0
- 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 +9 -5
- package/dist/tools/registry/printer.js +1 -0
- 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/dist/vice/mockServer.js +33 -0
- package/dist/vice/process.js +11 -1
- package/dist/vice/viceClient.js +35 -6
- package/doc/c64u/c64-openapi.yaml +1130 -774
- package/doc/reviews/hardening/01-fable/prompt-handover.md +125 -0
- package/doc/reviews/hardening/01-fable/prompt.md +98 -0
- package/doc/reviews/hardening/01-fable/review-prompt.md +468 -0
- package/doc/reviews/hardening/01-fable/review.md +1234 -0
- 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 +249 -8
- 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 +386 -44
- package/mcp.json +14 -2
- package/package.json +4 -2
- package/scripts/mockC64Server.mjs +171 -2
- package/scripts/prepare-release.mjs +13 -2
- package/scripts/update-readme.ts +18 -30
- package/server.json +3 -3
package/mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Stdio-based Model Context Protocol (MCP) server that bridges Commodore 64 Ultimate hardware with AI agents.",
|
|
5
5
|
"entry": "dist/index.js",
|
|
6
6
|
"type": "stdio",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"default": "~/.c64bridge.json"
|
|
38
38
|
},
|
|
39
39
|
"C64_MODE": {
|
|
40
|
-
"description": "Select active backend (c64u
|
|
40
|
+
"description": "Select active backend (c64u/U64 hardware, u2/U2-family cartridge, or vice emulator)",
|
|
41
41
|
"default": "c64u"
|
|
42
42
|
},
|
|
43
43
|
"C64U_HOST": {
|
|
@@ -52,6 +52,18 @@
|
|
|
52
52
|
"description": "Override the C64 Ultimate network password sent as X-Password",
|
|
53
53
|
"default": ""
|
|
54
54
|
},
|
|
55
|
+
"U2_HOST": {
|
|
56
|
+
"description": "Override the U2/U2+/U2+L host name or IP address when C64_MODE=u2",
|
|
57
|
+
"default": ""
|
|
58
|
+
},
|
|
59
|
+
"U2_PORT": {
|
|
60
|
+
"description": "Override the U2-family REST port when C64_MODE=u2",
|
|
61
|
+
"default": ""
|
|
62
|
+
},
|
|
63
|
+
"U2_PASSWORD": {
|
|
64
|
+
"description": "Override the U2-family network password sent as X-Password when C64_MODE=u2",
|
|
65
|
+
"default": ""
|
|
66
|
+
},
|
|
55
67
|
"C64BRIDGE_DIAGNOSTICS_DIR": {
|
|
56
68
|
"description": "Override the directory where persistent MCP diagnostics files are written",
|
|
57
69
|
"default": "~/.c64bridge/diagnostics"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"mcpName": "io.github.chrisgleissner/c64bridge",
|
|
5
5
|
"description": "C64 Bridge MCP server for Commodore 64 Ultimate hardware and VICE emulator control",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"check": "npm run build && npm test",
|
|
39
39
|
"check:node-compat": "node scripts/check-node-compat.mjs",
|
|
40
40
|
"build:rag": "node scripts/invoke-bun.mjs scripts/rag-rebuild.mjs",
|
|
41
|
-
"api:generate": "
|
|
41
|
+
"api:generate": "npm run api:generate:c64u && npm run api:generate:u2",
|
|
42
|
+
"api:generate:c64u": "npx --yes swagger-typescript-api@12.0.4 -p doc/c64u/c64-openapi.yaml -o generated/openapi/c64u -n index.ts --axios --single-http-client --module-name-index 0",
|
|
43
|
+
"api:generate:u2": "npx --yes swagger-typescript-api@12.0.4 -p doc/u2/u2-openapi.yaml -o generated/openapi/u2 -n index.ts --axios --single-http-client --module-name-index 0",
|
|
42
44
|
"c64:tool": "bash scripts/c64-tool.sh",
|
|
43
45
|
"rag:rebuild": "node scripts/invoke-bun.mjs scripts/rag-rebuild.mjs",
|
|
44
46
|
"rag:fetch": "node scripts/fetch-external-sources.mjs",
|
|
@@ -83,12 +83,18 @@ function sendJson(res, payload = {}, statusCode = 200) {
|
|
|
83
83
|
function createInitialState() {
|
|
84
84
|
return {
|
|
85
85
|
networkPassword: null,
|
|
86
|
+
forcedErrors: null,
|
|
87
|
+
forcedErrorsUrlMatch: null,
|
|
88
|
+
hangOnMachineInputProbe: false,
|
|
86
89
|
lastPrg: null,
|
|
90
|
+
lastLoadedPrg: null,
|
|
91
|
+
loadCount: 0,
|
|
87
92
|
runCount: 0,
|
|
88
93
|
resets: 0,
|
|
89
94
|
reboots: 0,
|
|
90
95
|
poweroffs: 0,
|
|
91
96
|
memory: new Uint8Array(0x10000),
|
|
97
|
+
heartbeat: null,
|
|
92
98
|
lastWrite: null,
|
|
93
99
|
writeLog: [],
|
|
94
100
|
lastRequest: null,
|
|
@@ -112,6 +118,8 @@ function createInitialState() {
|
|
|
112
118
|
lastConfigAction: null,
|
|
113
119
|
menuToggleCount: 0,
|
|
114
120
|
lastMenuTarget: null,
|
|
121
|
+
menuScreen: Buffer.from([0x41, 0x01, 0x42, 0x02]),
|
|
122
|
+
inputState: { keyboard: { inputs: [] }, joysticks: [{ port: 1, inputs: [] }, { port: 2, inputs: [] }] },
|
|
115
123
|
streams: {
|
|
116
124
|
video: { active: false, target: null, packetsSent: 0 },
|
|
117
125
|
audio: { active: false, target: null, packetsSent: 0 },
|
|
@@ -154,6 +162,82 @@ function toScreenCode(char) {
|
|
|
154
162
|
return SCREEN_CODE_LOOKUP.get(char) ?? SCREEN_CODE_LOOKUP.get(" ") ?? 0x20;
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
const KERNAL_KEYBOARD_NDX_ADDRESS = 0x00c6;
|
|
166
|
+
const KERNAL_KEYBOARD_BUFFER_ADDRESS = 0x0277;
|
|
167
|
+
|
|
168
|
+
const SYS_COMMAND_PATTERN = /SYS\s*\d+/i;
|
|
169
|
+
const HEARTBEAT_ADDRESS = 0x0400; // Screen-RAM byte the ASM liveness poller watches.
|
|
170
|
+
const HEARTBEAT_GRANULARITY_MS = 10; // Well below the poll interval so consecutive liveness reads always differ.
|
|
171
|
+
const HEARTBEAT_DURATION_MS = 1000; // How long a SYS-triggered program keeps "running".
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Nothing in this mock runs 6502 code to drain the KERNAL keyboard buffer
|
|
175
|
+
* itself, so a client polling NDX ($00C6) waiting for it to return to zero
|
|
176
|
+
* would otherwise hang forever. Simulate the KERNAL's IRQ-driven drain with
|
|
177
|
+
* a short delay so injectKeyboardQueue's poll loop behaves realistically.
|
|
178
|
+
*
|
|
179
|
+
* When the queued text looks like a typed `SYSnnnnn` command (the trigger
|
|
180
|
+
* upload_run_asm uses to enter an assembled program), also record a
|
|
181
|
+
* time-computed "alive" window (consumed by readMemoryWithHeartbeat). This
|
|
182
|
+
* mock never executes 6502 code, so a liveness poller watching screen RAM
|
|
183
|
+
* would otherwise see no progression. Recording a descriptor rather than
|
|
184
|
+
* mutating memory from a background timer is deliberate: nothing advances
|
|
185
|
+
* asynchronously, so an unrelated later test that merely shares this server
|
|
186
|
+
* instance can never observe a stray screen-RAM mutation.
|
|
187
|
+
*/
|
|
188
|
+
function simulateKeyboardDrain(state, address) {
|
|
189
|
+
if (address !== KERNAL_KEYBOARD_NDX_ADDRESS) return;
|
|
190
|
+
const pending = state.memory[KERNAL_KEYBOARD_NDX_ADDRESS] ?? 0;
|
|
191
|
+
const queued = pending > 0
|
|
192
|
+
? Uint8Array.from(state.memory.subarray(KERNAL_KEYBOARD_BUFFER_ADDRESS, KERNAL_KEYBOARD_BUFFER_ADDRESS + pending))
|
|
193
|
+
: null;
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
state.memory[KERNAL_KEYBOARD_NDX_ADDRESS] = 0;
|
|
196
|
+
if (!queued) return;
|
|
197
|
+
const text = Buffer.from(queued).toString("ascii");
|
|
198
|
+
if (!SYS_COMMAND_PATTERN.test(text)) return;
|
|
199
|
+
state.heartbeat = {
|
|
200
|
+
address: HEARTBEAT_ADDRESS,
|
|
201
|
+
startedAt: Date.now(),
|
|
202
|
+
durationMs: HEARTBEAT_DURATION_MS,
|
|
203
|
+
baseValue: state.memory[HEARTBEAT_ADDRESS] ?? 0,
|
|
204
|
+
};
|
|
205
|
+
}, 5);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Serve a screen-RAM read, overlaying a synthetic, monotonically advancing
|
|
210
|
+
* byte at the heartbeat address while a SYS-triggered "alive" window is open.
|
|
211
|
+
* The overlay is a pure function of elapsed time applied only to the returned
|
|
212
|
+
* copy, so it never mutates stored memory and never advances on its own.
|
|
213
|
+
*/
|
|
214
|
+
function readMemoryWithHeartbeat(state, address, length) {
|
|
215
|
+
const bytes = state.memory.slice(address, address + length);
|
|
216
|
+
const heartbeat = state.heartbeat;
|
|
217
|
+
if (!heartbeat) return bytes;
|
|
218
|
+
const elapsed = Date.now() - heartbeat.startedAt;
|
|
219
|
+
if (elapsed < 0 || elapsed >= heartbeat.durationMs) return bytes;
|
|
220
|
+
const offset = heartbeat.address - address;
|
|
221
|
+
if (offset < 0 || offset >= bytes.length) return bytes;
|
|
222
|
+
const ticks = Math.floor(elapsed / HEARTBEAT_GRANULARITY_MS);
|
|
223
|
+
bytes[offset] = (heartbeat.baseValue + ticks) & 0xff;
|
|
224
|
+
return bytes;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Any explicit write covering the heartbeat address means a client has taken
|
|
229
|
+
* over screen RAM, so end the simulated "alive" window and let the real stored
|
|
230
|
+
* bytes show through again. This keeps a lingering heartbeat from corrupting a
|
|
231
|
+
* later writeMemory/readMemory round-trip in a shared-server test.
|
|
232
|
+
*/
|
|
233
|
+
function cancelHeartbeatOnWrite(state, address, length) {
|
|
234
|
+
const heartbeat = state.heartbeat;
|
|
235
|
+
if (!heartbeat) return;
|
|
236
|
+
if (address <= heartbeat.address && heartbeat.address < address + length) {
|
|
237
|
+
state.heartbeat = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
157
241
|
function seedReadyPrompt(state) {
|
|
158
242
|
const text = "READY.";
|
|
159
243
|
const buffer = new Uint8Array(text.length + 1);
|
|
@@ -273,6 +357,20 @@ export async function startMockC64Server(options = {}) {
|
|
|
273
357
|
return;
|
|
274
358
|
}
|
|
275
359
|
|
|
360
|
+
// Test hook: simulate real firmware soft-failures, which answer HTTP 200
|
|
361
|
+
// with a non-empty `errors` array rather than a non-2xx status. Consumed
|
|
362
|
+
// once so a single forced failure does not leak into later requests.
|
|
363
|
+
if (state.forcedErrors && (!state.forcedErrorsUrlMatch || url.includes(state.forcedErrorsUrlMatch))) {
|
|
364
|
+
const errors = state.forcedErrors;
|
|
365
|
+
state.forcedErrors = null;
|
|
366
|
+
state.forcedErrorsUrlMatch = null;
|
|
367
|
+
// Drain any request body so an unread upload cannot corrupt a reused
|
|
368
|
+
// keep-alive connection's framing.
|
|
369
|
+
for await (const _chunk of req) { /* discard */ }
|
|
370
|
+
sendJson(res, { result: "error", errors });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
276
374
|
if (method === "GET" && (url === "/" || url.startsWith("/?"))) {
|
|
277
375
|
sendJson(res, { status: "ok", host: "mock" });
|
|
278
376
|
return;
|
|
@@ -295,7 +393,10 @@ export async function startMockC64Server(options = {}) {
|
|
|
295
393
|
enabled: driveState.power !== "off",
|
|
296
394
|
power: driveState.power,
|
|
297
395
|
mode: driveState.mode,
|
|
298
|
-
|
|
396
|
+
// Mirror the real firmware's DriveInfo shape (image_path is a
|
|
397
|
+
// plain string), not the internal mount-params object.
|
|
398
|
+
image_path: driveState.mountedImage?.image ?? null,
|
|
399
|
+
type: driveState.mountedImage?.type ?? driveState.mode ?? null,
|
|
299
400
|
};
|
|
300
401
|
}
|
|
301
402
|
sendJson(res, { drives });
|
|
@@ -331,6 +432,49 @@ export async function startMockC64Server(options = {}) {
|
|
|
331
432
|
return;
|
|
332
433
|
}
|
|
333
434
|
|
|
435
|
+
if (method === "GET" && url === "/v1/machine:menu_screen") {
|
|
436
|
+
res.setHeader("Content-Type", "application/octet-stream");
|
|
437
|
+
res.end(state.menuScreen);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (method === "GET" && url === "/v1/machine:input") {
|
|
442
|
+
if (state.hangOnMachineInputProbe) {
|
|
443
|
+
// Test hook: never respond, to prove a caller cannot be blocked on
|
|
444
|
+
// this probe (HARD01-031). Deliberately does not call res.end().
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
sendJson(res, state.inputState);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (method === "POST" && url === "/v1/machine:input") {
|
|
452
|
+
const batch = await readJson(req);
|
|
453
|
+
for (const event of batch.events ?? []) {
|
|
454
|
+
if (event.kind === "release_all") {
|
|
455
|
+
state.inputState.keyboard.inputs = [];
|
|
456
|
+
state.inputState.joysticks.forEach((joystick) => { joystick.inputs = []; });
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const target = event.kind === "keyboard"
|
|
460
|
+
? state.inputState.keyboard
|
|
461
|
+
: state.inputState.joysticks.find((joystick) => joystick.port === event.port);
|
|
462
|
+
if (!target) continue;
|
|
463
|
+
const inputs = new Set(target.inputs);
|
|
464
|
+
for (const input of event.inputs ?? []) {
|
|
465
|
+
if (event.transition === "release") inputs.delete(input);
|
|
466
|
+
else if (event.transition === "press") inputs.add(input);
|
|
467
|
+
else if (event.transition === "tap") {
|
|
468
|
+
inputs.add(input);
|
|
469
|
+
inputs.delete(input);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
target.inputs = [...inputs];
|
|
473
|
+
}
|
|
474
|
+
sendJson(res, state.inputState);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
334
478
|
if (method === "GET" && url === "/v1/machine:debugreg") {
|
|
335
479
|
sendJson(res, { value: state.debugreg ?? "00" });
|
|
336
480
|
return;
|
|
@@ -358,6 +502,27 @@ export async function startMockC64Server(options = {}) {
|
|
|
358
502
|
return;
|
|
359
503
|
}
|
|
360
504
|
|
|
505
|
+
if (method === "POST" && url === "/v1/runners:load_prg") {
|
|
506
|
+
const chunks = [];
|
|
507
|
+
for await (const chunk of req) {
|
|
508
|
+
chunks.push(chunk);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const prg = Buffer.concat(chunks);
|
|
512
|
+
state.lastLoadedPrg = prg;
|
|
513
|
+
state.loadCount += 1;
|
|
514
|
+
|
|
515
|
+
// Mirror real firmware: place the PRG body at its embedded load
|
|
516
|
+
// address without starting it (no RUN, no BASIC pointer changes).
|
|
517
|
+
if (prg.length >= 2) {
|
|
518
|
+
const loadAddress = prg.readUInt16LE(0);
|
|
519
|
+
state.memory.set(prg.subarray(2), loadAddress);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
sendJson(res, { result: "ok", bytes: prg.length });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
361
526
|
if (url.startsWith("/v1/runners:sidplay")) {
|
|
362
527
|
let routeUrl;
|
|
363
528
|
try {
|
|
@@ -431,7 +596,7 @@ export async function startMockC64Server(options = {}) {
|
|
|
431
596
|
const lengthValue = routeUrl.searchParams.get("length") ?? "256";
|
|
432
597
|
const address = parseNumeric(addressValue);
|
|
433
598
|
const length = Math.max(0, parseNumeric(lengthValue, 10));
|
|
434
|
-
const bytes = state
|
|
599
|
+
const bytes = readMemoryWithHeartbeat(state, address, length);
|
|
435
600
|
|
|
436
601
|
const accept = String(req.headers["accept"] || "");
|
|
437
602
|
if (accept.includes("application/octet-stream")) {
|
|
@@ -466,6 +631,8 @@ export async function startMockC64Server(options = {}) {
|
|
|
466
631
|
state.memory.set(bytes, address);
|
|
467
632
|
state.lastWrite = { address, bytes };
|
|
468
633
|
state.writeLog.push({ address, bytes: Buffer.from(bytes) });
|
|
634
|
+
cancelHeartbeatOnWrite(state, address, bytes.length);
|
|
635
|
+
simulateKeyboardDrain(state, address);
|
|
469
636
|
|
|
470
637
|
sendJson(res, { result: "wrote", address, length: bytes.length });
|
|
471
638
|
return;
|
|
@@ -485,6 +652,8 @@ export async function startMockC64Server(options = {}) {
|
|
|
485
652
|
state.memory.set(bytes, address);
|
|
486
653
|
state.lastWrite = { address, bytes };
|
|
487
654
|
state.writeLog.push({ address, bytes: Buffer.from(bytes) });
|
|
655
|
+
cancelHeartbeatOnWrite(state, address, bytes.length);
|
|
656
|
+
simulateKeyboardDrain(state, address);
|
|
488
657
|
|
|
489
658
|
sendJson(res, { result: "wrote", address, length: bytes.length });
|
|
490
659
|
return;
|
|
@@ -26,8 +26,19 @@ const fileExists = async (relativePath) => {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
const getRepositoryMetadata = (repository) => {
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
29
|
+
const rawUrl = typeof repository === 'string' ? repository : repository?.url;
|
|
30
|
+
if (!rawUrl) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let url = rawUrl.trim();
|
|
35
|
+
if (url.startsWith('git+')) {
|
|
36
|
+
url = url.slice(4);
|
|
37
|
+
}
|
|
38
|
+
if (url.endsWith('.git')) {
|
|
39
|
+
url = url.slice(0, -4);
|
|
40
|
+
}
|
|
41
|
+
if (!/^https?:\/\//.test(url)) {
|
|
31
42
|
return undefined;
|
|
32
43
|
}
|
|
33
44
|
|
package/scripts/update-readme.ts
CHANGED
|
@@ -27,6 +27,7 @@ type ManifestEnvEntry = {
|
|
|
27
27
|
type EnvCategory =
|
|
28
28
|
| "Server Runtime"
|
|
29
29
|
| "C64 Ultimate"
|
|
30
|
+
| "U2-family"
|
|
30
31
|
| "VICE Runtime"
|
|
31
32
|
| "VICE Audio Capture"
|
|
32
33
|
| "SID Playback"
|
|
@@ -55,6 +56,9 @@ function classifyEnvVariable(name: string): EnvCategory {
|
|
|
55
56
|
if (name.startsWith("C64U_")) {
|
|
56
57
|
return "C64 Ultimate";
|
|
57
58
|
}
|
|
59
|
+
if (name.startsWith("U2_")) {
|
|
60
|
+
return "U2-family";
|
|
61
|
+
}
|
|
58
62
|
if (name.startsWith("VICE_") || name === "FORCE_XVFB" || name === "DISABLE_XVFB") {
|
|
59
63
|
if (name === "VICE_MODE" || name === "VICE_LIMIT_CYCLES" || name === "VICE_RUN_TIMEOUT_MS") {
|
|
60
64
|
return "VICE Audio Capture";
|
|
@@ -79,6 +83,9 @@ function resolveJsonConfigKey(name: string): string {
|
|
|
79
83
|
C64U_HOST: "c64u.host",
|
|
80
84
|
C64U_PORT: "c64u.port",
|
|
81
85
|
C64U_PASSWORD: "c64u.networkPassword",
|
|
86
|
+
U2_HOST: "u2.host",
|
|
87
|
+
U2_PORT: "u2.port",
|
|
88
|
+
U2_PASSWORD: "u2.networkPassword",
|
|
82
89
|
VICE_BINARY: "vice.exe",
|
|
83
90
|
VICE_DIRECTORY: "vice.directory",
|
|
84
91
|
VICE_HOST: "vice.host",
|
|
@@ -104,6 +111,7 @@ export function renderEnvironmentSection(envEntries: Readonly<Record<string, Man
|
|
|
104
111
|
const orderedCategories: readonly EnvCategory[] = [
|
|
105
112
|
"Server Runtime",
|
|
106
113
|
"C64 Ultimate",
|
|
114
|
+
"U2-family",
|
|
107
115
|
"VICE Runtime",
|
|
108
116
|
"VICE Audio Capture",
|
|
109
117
|
"SID Playback",
|
|
@@ -145,7 +153,6 @@ type GroupedOperation = {
|
|
|
145
153
|
readonly required: readonly string[];
|
|
146
154
|
readonly optional: readonly string[];
|
|
147
155
|
readonly properties: Readonly<Record<string, JsonSchema | undefined>>;
|
|
148
|
-
readonly notes: readonly string[];
|
|
149
156
|
};
|
|
150
157
|
|
|
151
158
|
type GroupedToolInfo = {
|
|
@@ -168,6 +175,11 @@ function toTableValue(values: readonly string[]): string {
|
|
|
168
175
|
return values.join(", ");
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
function withVerificationNote(description: string, properties: Readonly<Record<string, JsonSchema | undefined>>): string {
|
|
179
|
+
const supportsVerification = Object.keys(properties).some((name) => name.toLowerCase().startsWith("verify"));
|
|
180
|
+
return supportsVerification ? `${description} Supports verification.` : description;
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
function formatInputName(name: string, schema?: JsonSchema): string {
|
|
172
184
|
const defaultSuffix = schema && Object.prototype.hasOwnProperty.call(schema, "default")
|
|
173
185
|
? `=${JSON.stringify(schema.default)}`
|
|
@@ -229,21 +241,12 @@ function collectFromOperationMetadata(schema: JsonSchema): readonly GroupedOpera
|
|
|
229
241
|
? entry.optional.filter((name): name is string => typeof name === "string" && name !== "op")
|
|
230
242
|
: Object.keys(properties).filter((name) => name !== "op" && !requiredProps.includes(name));
|
|
231
243
|
|
|
232
|
-
const notes: string[] = [];
|
|
233
|
-
const hasVerificationProperty = Object.keys(properties).some((name) =>
|
|
234
|
-
name.toLowerCase().startsWith("verify"),
|
|
235
|
-
);
|
|
236
|
-
if (hasVerificationProperty) {
|
|
237
|
-
notes.push("supports verify");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
244
|
operations.push({
|
|
241
245
|
op: opName,
|
|
242
|
-
description: getString(entry.description, `Operation ${opName}`),
|
|
246
|
+
description: withVerificationNote(getString(entry.description, `Operation ${opName}`), properties),
|
|
243
247
|
required: requiredProps,
|
|
244
248
|
optional: optionalProps,
|
|
245
249
|
properties,
|
|
246
|
-
notes,
|
|
247
250
|
});
|
|
248
251
|
}
|
|
249
252
|
|
|
@@ -282,21 +285,12 @@ function collectFromVariantList(variants: readonly JsonSchema[]): readonly Group
|
|
|
282
285
|
const description = getString(typedVariant.description, getString(opSchema?.description, `Operation ${opName}`));
|
|
283
286
|
const requiredProps = ((typedVariant.required as readonly string[] | undefined) ?? []).filter((name) => name !== "op");
|
|
284
287
|
|
|
285
|
-
const notes: string[] = [];
|
|
286
|
-
const hasVerificationProperty = Object.keys(properties).some((name) =>
|
|
287
|
-
name.toLowerCase().startsWith("verify"),
|
|
288
|
-
);
|
|
289
|
-
if (hasVerificationProperty) {
|
|
290
|
-
notes.push("supports verify");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
288
|
operations.push({
|
|
294
289
|
op: opName,
|
|
295
|
-
description,
|
|
290
|
+
description: withVerificationNote(description, properties),
|
|
296
291
|
required: requiredProps,
|
|
297
292
|
optional: Object.keys(properties).filter((name) => name !== "op" && !requiredProps.includes(name)),
|
|
298
293
|
properties,
|
|
299
|
-
notes,
|
|
300
294
|
});
|
|
301
295
|
}
|
|
302
296
|
|
|
@@ -321,18 +315,12 @@ function collectFromFlattenedSchema(schema: JsonSchema): readonly GroupedOperati
|
|
|
321
315
|
}
|
|
322
316
|
|
|
323
317
|
const descriptionMap = parseOperationDescriptions(getString(schema.description));
|
|
324
|
-
const hasVerificationProperty = Object.keys(properties).some((name) =>
|
|
325
|
-
name !== "op" && name.toLowerCase().startsWith("verify"),
|
|
326
|
-
);
|
|
327
|
-
const notes = hasVerificationProperty ? ["supports verify"] : [];
|
|
328
|
-
|
|
329
318
|
return opNames.map((opName) => ({
|
|
330
319
|
op: opName,
|
|
331
|
-
description: descriptionMap.get(opName) ?? `Operation ${opName}`,
|
|
320
|
+
description: withVerificationNote(descriptionMap.get(opName) ?? `Operation ${opName}`, properties),
|
|
332
321
|
required: [] as readonly string[],
|
|
333
322
|
optional: Object.keys(properties).filter((name) => name !== "op"),
|
|
334
323
|
properties,
|
|
335
|
-
notes,
|
|
336
324
|
}));
|
|
337
325
|
}
|
|
338
326
|
|
|
@@ -391,13 +379,13 @@ export function renderToolsSection(modules: readonly ToolModuleDescriptor[] = de
|
|
|
391
379
|
escapeCell(operation.description),
|
|
392
380
|
escapeCell(formatInputs(operation.required, operation.properties)),
|
|
393
381
|
escapeCell(formatInputs(operation.optional, operation.properties)),
|
|
394
|
-
operation.notes.length ? escapeCell(operation.notes.join(", ")) : "—",
|
|
395
382
|
effectivePlatforms.includes("c64u") ? "✅" : "",
|
|
383
|
+
effectivePlatforms.includes("u2") ? "✅" : "",
|
|
396
384
|
effectivePlatforms.includes("vice") ? "✅" : "",
|
|
397
385
|
];
|
|
398
386
|
});
|
|
399
387
|
|
|
400
|
-
lines.push(renderTable(["Operation", "Description", "Required Inputs", "Optional Inputs", "
|
|
388
|
+
lines.push(renderTable(["Operation", "Description", "Required Inputs", "Optional Inputs", "C64U", "U2", "VICE"], operationRows));
|
|
401
389
|
}
|
|
402
390
|
|
|
403
391
|
lines.push("");
|
package/server.json
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
"title": "C64 Bridge",
|
|
5
5
|
"description": "MCP server for Commodore 64 Ultimate hardware and VICE emulator control.",
|
|
6
6
|
"repository": {
|
|
7
|
-
"url": "
|
|
7
|
+
"url": "https://github.com/chrisgleissner/c64bridge",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
10
|
"websiteUrl": "https://github.com/chrisgleissner/c64bridge",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "1.0.1",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "c64bridge",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "1.0.1",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|