c64bridge 0.8.0 → 0.9.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 (79) hide show
  1. package/.github/prompts/steer.prompt.md +55 -0
  2. package/AGENTS.md +9 -7
  3. package/README.md +139 -95
  4. package/data/embeddings_asm.json +1 -1
  5. package/data/embeddings_basic.json +1 -1
  6. package/data/embeddings_hardware.json +1 -1
  7. package/data/embeddings_mixed.json +1 -1
  8. package/data/embeddings_other.json +1 -1
  9. package/data/graphics/petscii-style-guide.md +4 -4
  10. package/data/graphics/sprite-charset-best-practices.md +9 -9
  11. package/data/vice/vice-binary-monitor-spec.md +921 -0
  12. package/dist/.tsbuildinfo +1 -1
  13. package/dist/device.js +86 -10
  14. package/dist/mcp-server.js +1 -1
  15. package/dist/prompts/registry.js +27 -25
  16. package/dist/rag/knowledgeIndex.js +112 -79
  17. package/dist/rag/resourceUris.js +93 -0
  18. package/dist/tools/audio.js +11 -11
  19. package/dist/tools/debug.js +1 -1
  20. package/dist/tools/developer.js +13 -13
  21. package/dist/tools/graphics.js +10 -10
  22. package/dist/tools/machineControl.js +5 -5
  23. package/dist/tools/memory.js +19 -11
  24. package/dist/tools/meta/index.js +1 -1
  25. package/dist/tools/printer.js +11 -12
  26. package/dist/tools/programRunners.js +16 -16
  27. package/dist/tools/rag.js +9 -9
  28. package/dist/tools/registry/config.js +1 -1
  29. package/dist/tools/registry/disk.js +1 -1
  30. package/dist/tools/registry/drive.js +1 -1
  31. package/dist/tools/registry/extract.js +1 -1
  32. package/dist/tools/registry/graphics.js +1 -1
  33. package/dist/tools/registry/memory.js +1 -1
  34. package/dist/tools/registry/platform.js +1 -1
  35. package/dist/tools/registry/printer.js +1 -1
  36. package/dist/tools/registry/program.js +1 -1
  37. package/dist/tools/registry/rag.js +1 -1
  38. package/dist/tools/registry/sound.js +1 -1
  39. package/dist/tools/registry/stream.js +1 -1
  40. package/dist/tools/registry/system.js +1 -1
  41. package/dist/tools/storage.js +15 -15
  42. package/dist/tools/streaming.js +3 -3
  43. package/dist/tools/types.js +41 -4
  44. package/dist/tools/vice.js +2 -5
  45. package/dist/vice/process.js +79 -2
  46. package/dist/vice/readiness.js +35 -0
  47. package/dist/vice/smokeOptions.js +44 -0
  48. package/dist/viceRunner.js +2 -0
  49. package/doc/developer.md +10 -2
  50. package/doc/plans/prompts/README.md +7 -7
  51. package/mcp/prompts.json +42 -59
  52. package/mcp/protocol-examples.json +2 -2
  53. package/mcp/resources.json +179 -166
  54. package/mcp/schemas/c64_config.schema.json +109 -326
  55. package/mcp/schemas/c64_debug.schema.json +158 -337
  56. package/mcp/schemas/c64_disk.schema.json +180 -262
  57. package/mcp/schemas/c64_drive.schema.json +31 -111
  58. package/mcp/schemas/c64_extract.schema.json +193 -291
  59. package/mcp/schemas/c64_graphics.schema.json +208 -308
  60. package/mcp/schemas/c64_memory.schema.json +94 -144
  61. package/mcp/schemas/c64_printer.schema.json +151 -205
  62. package/mcp/schemas/c64_program.schema.json +229 -367
  63. package/mcp/schemas/c64_rag.schema.json +25 -59
  64. package/mcp/schemas/c64_select_backend.schema.json +19 -26
  65. package/mcp/schemas/c64_sound.schema.json +346 -622
  66. package/mcp/schemas/c64_stream.schema.json +25 -54
  67. package/mcp/schemas/c64_system.schema.json +79 -212
  68. package/mcp/schemas/c64_vice.schema.json +24 -55
  69. package/mcp/server.json +1 -1
  70. package/mcp/tools.json +1935 -3443
  71. package/mcp.json +10 -9
  72. package/package.json +20 -15
  73. package/scripts/fetch-external-sources.mjs +1 -2
  74. package/scripts/prepare-release.mjs +59 -2
  75. package/scripts/run-coverage.mjs +26 -1
  76. package/scripts/run-tests.mjs +2 -2
  77. package/scripts/run-tests.ts +273 -52
  78. package/scripts/update-readme.ts +54 -2
  79. package/server.json +94 -0
@@ -9,14 +9,77 @@ import { fileURLToPath } from "node:url";
9
9
  const DEFAULT_TARGET = "mock";
10
10
  const DEFAULT_PLATFORM = "c64u";
11
11
  const DEFAULT_BUN_FILE_LIMIT = 4;
12
- const DEFAULT_BUN_BATCH_SIZE = 12;
12
+ // Keep default Bun shards small enough to avoid descriptor exhaustion on broad suites.
13
+ const DEFAULT_BUN_BATCH_SIZE = DEFAULT_BUN_FILE_LIMIT;
14
+ const DEFAULT_VICE_MOCK_TEST_FILES = [
15
+ "test/device.test.mjs",
16
+ "test/viceIntegration.test.mjs",
17
+ "test/viceModule.test.mjs",
18
+ "test/groupedToolsShims.test.mjs",
19
+ "test/toolsTypes.test.mjs",
20
+ "test/platformRegistry.test.mjs",
21
+ "test/meta/program.test.mjs",
22
+ "test/mcpServerIntegration.test.mjs",
23
+ "test/c64Client.test.mjs",
24
+ "test/vice/viceSmokeTest.ts",
25
+ ];
26
+ const DEFAULT_VICE_DEVICE_TEST_FILES = [
27
+ "test/device.test.mjs",
28
+ "test/vice/viceSmokeTest.ts",
29
+ ];
30
+ const BUN_ONLY_TEST_IMPORT_RE = /from\s+["']bun:test["']/;
31
+ const BUN_RUNTIME_REQUIRED_TEST_FILES = new Set([
32
+ "test/generateMcpInterface.test.mjs",
33
+ "test/scripts/start.test.mjs",
34
+ ]);
35
+ const ISOLATED_NODE_TEST_FILES = new Set([
36
+ "test/mcpServerIntegration.test.mjs",
37
+ "test/mcpServerPlatformInit.test.mjs",
38
+ "test/meta/background.test.mjs",
39
+ "test/pollIntegration.test.mjs",
40
+ "test/pollValidator.test.mjs",
41
+ "test/programRunnersModule.test.mjs",
42
+ "test/toolsCoverage.test.mjs",
43
+ ]);
13
44
  const ISOLATED_BUN_TEST_FILES = new Set([
14
45
  "test/audioRuntime.test.mjs",
46
+ "test/mcpServerIntegration.test.mjs",
47
+ "test/mcpServerPlatformInit.test.mjs",
15
48
  ]);
16
49
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
50
  const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
18
51
  const defaultTestFiles = listRepoTestFiles(path.join(repoRoot, "test"));
19
52
 
53
+ function findExecutableOnPath(binary: string, envPath: string | undefined = process.env.PATH): string | null {
54
+ const normalized = binary.trim();
55
+ if (!normalized) {
56
+ return null;
57
+ }
58
+ if (path.isAbsolute(normalized) || normalized.includes(path.sep)) {
59
+ try {
60
+ fs.accessSync(normalized, fs.constants.X_OK);
61
+ return normalized;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ for (const entry of (envPath ?? "").split(path.delimiter)) {
68
+ if (!entry) {
69
+ continue;
70
+ }
71
+ const candidate = path.join(entry, normalized);
72
+ try {
73
+ fs.accessSync(candidate, fs.constants.X_OK);
74
+ return candidate;
75
+ } catch {
76
+ // Continue searching PATH.
77
+ }
78
+ }
79
+
80
+ return null;
81
+ }
82
+
20
83
  function resolveNodeExecutable(): string {
21
84
  const candidates = [
22
85
  process.env.C64BRIDGE_TEST_NODE_BIN,
@@ -35,6 +98,31 @@ function resolveNodeExecutable(): string {
35
98
  return "node";
36
99
  }
37
100
 
101
+ function resolveBunExecutable(env: NodeJS.ProcessEnv = process.env): string | null {
102
+ if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
103
+ return process.execPath;
104
+ }
105
+
106
+ const candidates = [
107
+ env.C64BRIDGE_TEST_BUN_BIN,
108
+ env.C64BRIDGE_BUN_BIN,
109
+ process.env.C64BRIDGE_TEST_BUN_BIN,
110
+ process.env.C64BRIDGE_BUN_BIN,
111
+ "bun",
112
+ ];
113
+
114
+ for (const candidate of candidates) {
115
+ if (candidate && typeof candidate === "string" && candidate.trim()) {
116
+ const resolved = findExecutableOnPath(candidate.trim(), env.PATH ?? process.env.PATH);
117
+ if (resolved) {
118
+ return resolved;
119
+ }
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
38
126
  export type RunTestsArgs = {
39
127
  target: string;
40
128
  platform: "c64u" | "vice";
@@ -43,6 +131,53 @@ export type RunTestsArgs = {
43
131
  passthrough: string[];
44
132
  };
45
133
 
134
+ export function buildMatrixEnv(
135
+ platform: "c64u" | "vice",
136
+ target: "mock" | "device",
137
+ explicitBaseUrl: string | null,
138
+ envSource: NodeJS.ProcessEnv = process.env,
139
+ ): Record<string, string> {
140
+ const env: Record<string, string> = { ...envSource } as Record<string, string>;
141
+ env.C64_MODE = platform;
142
+ env.C64_TEST_TARGET = target === "device" ? "real" : "mock";
143
+
144
+ if (platform === "vice") {
145
+ const useViceMock = target !== "device";
146
+ env.VICE_TEST_TARGET = useViceMock ? "mock" : "vice";
147
+ env.C64_TEST_ENABLE_VICE_MOCK = useViceMock ? "1" : "0";
148
+ if (!useViceMock) {
149
+ env.VICE_AVAILABLE = "1";
150
+ }
151
+ } else {
152
+ delete env.VICE_TEST_TARGET;
153
+ delete env.C64_TEST_ENABLE_VICE_MOCK;
154
+ }
155
+
156
+ if (!env.RAG_EMBEDDINGS_DIR) {
157
+ env.RAG_EMBEDDINGS_DIR = defaultEmbeddingsDir;
158
+ }
159
+ if (explicitBaseUrl) {
160
+ env.C64_TEST_BASE_URL = explicitBaseUrl;
161
+ }
162
+ if (platform === "c64u" && target === "device" && !env.C64_TEST_BASE_URL) {
163
+ env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig(env) ?? "http://c64u";
164
+ }
165
+
166
+ return env;
167
+ }
168
+
169
+ export function resolveDefaultMatrixTestFiles(
170
+ platform: "c64u" | "vice",
171
+ target: "mock" | "device",
172
+ ): string[] {
173
+ if (platform !== "vice") {
174
+ return [...defaultTestFiles];
175
+ }
176
+ return target === "device"
177
+ ? [...DEFAULT_VICE_DEVICE_TEST_FILES]
178
+ : [...DEFAULT_VICE_MOCK_TEST_FILES];
179
+ }
180
+
46
181
  export function parseRunTestsArgs(args: string[]): RunTestsArgs {
47
182
  let target = DEFAULT_TARGET;
48
183
  let platform: "c64u" | "vice" = DEFAULT_PLATFORM;
@@ -106,6 +241,10 @@ export function shouldUseNodeFallback(runCoverage: boolean, passthrough: string[
106
241
  if (runCoverage) {
107
242
  return false;
108
243
  }
244
+ if (String(env.C64_MODE ?? "").trim().toLowerCase() === "vice"
245
+ && String(env.VICE_TEST_TARGET ?? "").trim().toLowerCase() === "vice") {
246
+ return true;
247
+ }
109
248
 
110
249
  const explicitFiles = passthrough.filter(looksLikeTestFileArg);
111
250
  if (passthrough.length === 0) {
@@ -139,11 +278,72 @@ export function buildBunTestBatches(passthrough: string[], env: NodeJS.ProcessEn
139
278
  ];
140
279
  }
141
280
 
281
+ export function splitPassthroughByRuntime(
282
+ passthrough: string[],
283
+ root: string = repoRoot,
284
+ ): { nodePassthrough: string[]; bunPassthrough: string[] } {
285
+ const explicitFiles = passthrough.filter(looksLikeTestFileArg);
286
+ if (explicitFiles.length === 0) {
287
+ return { nodePassthrough: [...passthrough], bunPassthrough: [] };
288
+ }
289
+
290
+ const sharedArgs = passthrough.filter((arg) => !looksLikeTestFileArg(arg));
291
+ const bunFiles = explicitFiles.filter((file) => isBunOnlyTestFile(path.join(root, file)));
292
+ const nodeFiles = explicitFiles.filter((file) => !bunFiles.includes(file));
293
+
294
+ return {
295
+ nodePassthrough: nodeFiles.length > 0 ? [...nodeFiles, ...sharedArgs] : [],
296
+ bunPassthrough: bunFiles.length > 0 ? [...bunFiles, ...sharedArgs] : [],
297
+ };
298
+ }
299
+
300
+ export function buildNodeFallbackBatches(
301
+ passthrough: string[],
302
+ root: string = repoRoot,
303
+ ): string[][] {
304
+ const explicitFiles = passthrough.filter(looksLikeTestFileArg);
305
+ if (explicitFiles.length === 0) {
306
+ return [passthrough];
307
+ }
308
+
309
+ const sharedArgs = passthrough.filter((arg) => !looksLikeTestFileArg(arg));
310
+ const isolatedFiles = explicitFiles.filter((file) => isNodeIsolatedTestFile(path.join(root, file)));
311
+ const sharedFiles = explicitFiles.filter((file) => !isolatedFiles.includes(file));
312
+ const batches: string[][] = [];
313
+
314
+ if (sharedFiles.length > 0) {
315
+ batches.push([...sharedFiles, ...sharedArgs]);
316
+ }
317
+ for (const file of isolatedFiles) {
318
+ batches.push([file, ...sharedArgs]);
319
+ }
320
+
321
+ return batches.length > 0 ? batches : [passthrough];
322
+ }
323
+
324
+ function isBunOnlyTestFile(filePath: string): boolean {
325
+ const relativePath = path.relative(repoRoot, filePath).split(path.sep).join("/");
326
+ if (BUN_RUNTIME_REQUIRED_TEST_FILES.has(relativePath)) {
327
+ return true;
328
+ }
329
+ try {
330
+ return BUN_ONLY_TEST_IMPORT_RE.test(fs.readFileSync(filePath, "utf8"));
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+
336
+ function isNodeIsolatedTestFile(filePath: string): boolean {
337
+ const relativePath = path.relative(repoRoot, filePath).split(path.sep).join("/");
338
+ return ISOLATED_NODE_TEST_FILES.has(relativePath);
339
+ }
340
+
142
341
  async function runNodeFallback(target: string, explicitBaseUrl: string | null, passthrough: string[], env: Record<string, string>, runCoverage: boolean): Promise<number> {
143
342
  console.warn("[run-tests] Using Node runner for this test set to avoid Bun memory growth on broad suites");
144
343
  if (runCoverage) {
145
344
  console.warn("[run-tests] Coverage reporting is unavailable in Node fallback mode");
146
345
  }
346
+ const { nodePassthrough, bunPassthrough } = splitPassthroughByRuntime(passthrough);
147
347
  const nodeExecutable = resolveNodeExecutable();
148
348
  const nodeScript = path.join(repoRoot, "scripts", "run-tests.mjs");
149
349
  const fallbackArgs = [] as string[];
@@ -153,23 +353,53 @@ async function runNodeFallback(target: string, explicitBaseUrl: string | null, p
153
353
  if (explicitBaseUrl) {
154
354
  fallbackArgs.push(`--base-url=${explicitBaseUrl}`);
155
355
  }
156
- fallbackArgs.push(...passthrough);
157
- return await new Promise<number>((resolve) => {
158
- const childProcess = spawn(nodeExecutable, [nodeScript, ...fallbackArgs], {
159
- cwd: repoRoot,
160
- env,
161
- stdio: "inherit",
162
- }) as unknown as {
163
- on(event: "error", listener: (error: Error) => void): void;
164
- on(event: "exit", listener: (code: number | null) => void): void;
165
- };
166
- childProcess.on("error", (error) => {
167
- console.error("[run-tests] Failed to launch Node fallback:", error);
168
- resolve(1);
169
- });
170
- childProcess.on("exit", (code) => {
171
- resolve(typeof code === "number" ? code : 1);
172
- });
356
+ if (nodePassthrough.length > 0 || bunPassthrough.length === 0) {
357
+ const nodeBatches = buildNodeFallbackBatches(nodePassthrough);
358
+ for (let index = 0; index < nodeBatches.length; index += 1) {
359
+ const batch = nodeBatches[index] ?? [];
360
+ if (nodeBatches.length > 1) {
361
+ console.warn(`[run-tests] Node fallback batch ${index + 1}/${nodeBatches.length} (${batch.filter(looksLikeTestFileArg).length} files)`);
362
+ }
363
+ const nodeExitCode = await new Promise<number>((resolve) => {
364
+ const childProcess = spawn(nodeExecutable, [nodeScript, ...fallbackArgs, ...batch], {
365
+ cwd: repoRoot,
366
+ env,
367
+ stdio: "inherit",
368
+ }) as unknown as {
369
+ on(event: "error", listener: (error: Error) => void): void;
370
+ on(event: "exit", listener: (code: number | null) => void): void;
371
+ };
372
+ childProcess.on("error", (error) => {
373
+ console.error("[run-tests] Failed to launch Node fallback:", error);
374
+ resolve(1);
375
+ });
376
+ childProcess.on("exit", (code) => {
377
+ resolve(typeof code === "number" ? code : 1);
378
+ });
379
+ });
380
+
381
+ if (nodeExitCode !== 0) {
382
+ return nodeExitCode;
383
+ }
384
+ }
385
+ }
386
+
387
+ if (bunPassthrough.length === 0) {
388
+ return 0;
389
+ }
390
+
391
+ const bunExecutable = resolveBunExecutable(env);
392
+ if (!bunExecutable) {
393
+ console.error("[run-tests] Bun-only tests were selected but no Bun executable is available");
394
+ return 1;
395
+ }
396
+
397
+ console.warn("[run-tests] Running Bun-only tests under Bun to preserve runtime coverage");
398
+ const bunBatches = buildBunTestBatches(bunPassthrough, env);
399
+ return await runBunBatches(env, bunBatches, {
400
+ coverage: false,
401
+ bunExecutable,
402
+ labelPrefix: "bun-only-suite",
173
403
  });
174
404
  }
175
405
 
@@ -200,24 +430,11 @@ async function main(): Promise<number> {
200
430
  fs.mkdirSync(defaultEmbeddingsDir, { recursive: true });
201
431
  }
202
432
 
203
- const env: Record<string, string> = { ...process.env } as Record<string, string>;
204
433
  const normalizedTarget = normalizeTarget(target);
205
- env.C64_MODE = platform;
206
- env.C64_TEST_TARGET = normalizedTarget === "device" ? "real" : "mock";
207
- if (platform === "vice") {
208
- env.VICE_TEST_TARGET = normalizedTarget === "device" ? "vice" : "mock";
209
- } else {
210
- delete env.VICE_TEST_TARGET;
211
- }
212
- if (!env.RAG_EMBEDDINGS_DIR) {
213
- env.RAG_EMBEDDINGS_DIR = defaultEmbeddingsDir;
214
- }
215
- if (explicitBaseUrl) {
216
- env.C64_TEST_BASE_URL = explicitBaseUrl;
217
- }
218
- if (platform === "c64u" && normalizedTarget === "device" && !env.C64_TEST_BASE_URL) {
219
- env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig(env) ?? "http://c64u";
220
- }
434
+ const env = buildMatrixEnv(platform, normalizedTarget, explicitBaseUrl);
435
+ const effectivePassthrough = passthrough.length > 0
436
+ ? passthrough
437
+ : resolveDefaultMatrixTestFiles(platform, normalizedTarget);
221
438
 
222
439
  printMatrixHeading({
223
440
  platform,
@@ -230,24 +447,20 @@ async function main(): Promise<number> {
230
447
  const bunRuntime = (globalThis as { Bun?: unknown }).Bun;
231
448
  if (!bunRuntime) {
232
449
  console.warn("[run-tests] Bun runtime not detected; falling back to Node runner");
233
- return await runNodeFallback(target, explicitBaseUrl, passthrough, env, runCoverage);
450
+ return await runNodeFallback(target, explicitBaseUrl, effectivePassthrough, env, runCoverage);
234
451
  }
235
452
 
236
- if (!runCoverage && passthrough.length === 0) {
237
- return await runBunBatches(env, buildBunTestBatches([], env), {
238
- coverage: false,
239
- labelPrefix: "default-suite",
240
- });
453
+ if (shouldUseNodeFallback(runCoverage, effectivePassthrough, env)) {
454
+ return await runNodeFallback(target, explicitBaseUrl, effectivePassthrough, env, runCoverage);
241
455
  }
242
456
 
243
- if (shouldUseNodeFallback(runCoverage, passthrough, env)) {
244
- if (!runCoverage) {
245
- return await runBunBatches(env, buildBunTestBatches(passthrough, env), {
246
- coverage: false,
247
- labelPrefix: "sharded-suite",
248
- });
249
- }
250
- return await runNodeFallback(target, explicitBaseUrl, passthrough, env, runCoverage);
457
+ if (!runCoverage) {
458
+ const batches = buildBunTestBatches(effectivePassthrough, env);
459
+ return await runBunBatches(env, batches, {
460
+ bunExecutable: process.execPath,
461
+ coverage: false,
462
+ labelPrefix: passthrough.length === 0 ? "default-suite" : "sharded-suite",
463
+ });
251
464
  }
252
465
 
253
466
  const bunArgs = [
@@ -259,7 +472,7 @@ async function main(): Promise<number> {
259
472
  "--coverage-reporter=text",
260
473
  ]
261
474
  : []),
262
- ...(passthrough.length > 0 ? passthrough : defaultTestFiles),
475
+ ...effectivePassthrough.map((arg) => normalizeBunBatchArg(arg)),
263
476
  ];
264
477
  return await runExternalCommand(process.execPath, bunArgs, env);
265
478
  }
@@ -345,7 +558,7 @@ function chunkFiles(files: string[], chunkSize: number): string[][] {
345
558
  async function runBunBatches(
346
559
  env: Record<string, string>,
347
560
  batches: string[][],
348
- options: { coverage: boolean; labelPrefix: string },
561
+ options: { coverage: boolean; labelPrefix: string; bunExecutable: string },
349
562
  ): Promise<number> {
350
563
  const coverageArgs = options.coverage
351
564
  ? ["--coverage", "--coverage-reporter=lcov", "--coverage-reporter=text"]
@@ -354,7 +567,8 @@ async function runBunBatches(
354
567
  for (let index = 0; index < batches.length; index += 1) {
355
568
  const batch = batches[index] ?? [];
356
569
  console.log(`[run-tests] ${options.labelPrefix} batch ${index + 1}/${batches.length} (${batch.length} entries)`);
357
- const exitCode = await runExternalCommand(process.execPath, ["test", ...coverageArgs, ...batch], env);
570
+ const bunArgs = batch.map((arg) => normalizeBunBatchArg(arg));
571
+ const exitCode = await runExternalCommand(options.bunExecutable, ["test", ...coverageArgs, ...bunArgs], env);
358
572
  if (exitCode !== 0) {
359
573
  return exitCode;
360
574
  }
@@ -363,6 +577,13 @@ async function runBunBatches(
363
577
  return 0;
364
578
  }
365
579
 
580
+ export function normalizeBunBatchArg(arg: string): string {
581
+ if (looksLikeTestFileArg(arg) && !arg.startsWith("./") && !arg.startsWith("../") && !path.isAbsolute(arg)) {
582
+ return `./${arg}`;
583
+ }
584
+ return arg;
585
+ }
586
+
366
587
  function listRepoTestFiles(testRoot: string): string[] {
367
588
  const files: string[] = [];
368
589
 
@@ -173,10 +173,14 @@ function collectGroupedOperations(schema?: JsonSchema): readonly GroupedOperatio
173
173
  const discriminator = (schema as JsonSchema & { discriminator?: { propertyName?: string } }).discriminator;
174
174
  const variants = (schema as JsonSchema & { oneOf?: readonly JsonSchema[] }).oneOf;
175
175
 
176
- if (!discriminator || discriminator.propertyName !== "op" || !Array.isArray(variants)) {
177
- return [];
176
+ if (discriminator && discriminator.propertyName === "op" && Array.isArray(variants)) {
177
+ return collectFromVariantList(variants);
178
178
  }
179
179
 
180
+ return collectFromFlattenedSchema(schema);
181
+ }
182
+
183
+ function collectFromVariantList(variants: readonly JsonSchema[]): readonly GroupedOperation[] {
180
184
  const operations: GroupedOperation[] = [];
181
185
 
182
186
  for (const variant of variants) {
@@ -227,6 +231,54 @@ function collectGroupedOperations(schema?: JsonSchema): readonly GroupedOperatio
227
231
  return operations;
228
232
  }
229
233
 
234
+ function collectFromFlattenedSchema(schema: JsonSchema): readonly GroupedOperation[] {
235
+ const properties = (schema.properties ?? {}) as Record<string, JsonSchema | undefined>;
236
+ const opSchema = properties.op;
237
+ if (!opSchema || !isObject(opSchema)) {
238
+ return [];
239
+ }
240
+
241
+ const enumValues = (opSchema as JsonSchema).enum;
242
+ if (!Array.isArray(enumValues) || enumValues.length === 0) {
243
+ return [];
244
+ }
245
+
246
+ const opNames = enumValues.filter((value): value is string => typeof value === "string" && value.length > 0);
247
+ if (opNames.length === 0) {
248
+ return [];
249
+ }
250
+
251
+ const descriptionMap = parseOperationDescriptions(getString(schema.description));
252
+ const hasVerificationProperty = Object.keys(properties).some((name) =>
253
+ name !== "op" && name.toLowerCase().startsWith("verify"),
254
+ );
255
+ const notes = hasVerificationProperty ? ["supports verify"] : [];
256
+
257
+ return opNames.map((opName) => ({
258
+ op: opName,
259
+ description: descriptionMap.get(opName) ?? `Operation ${opName}`,
260
+ required: [] as readonly string[],
261
+ notes,
262
+ }));
263
+ }
264
+
265
+ function parseOperationDescriptions(description: string): Map<string, string> {
266
+ const result = new Map<string, string>();
267
+ if (!description) {
268
+ return result;
269
+ }
270
+
271
+ const lines = description.split(/\r?\n/);
272
+ for (const line of lines) {
273
+ const match = line.match(/^\s*-\s+([A-Za-z0-9_]+):\s*(.+)$/);
274
+ if (match) {
275
+ result.set(match[1], match[2].trim());
276
+ }
277
+ }
278
+
279
+ return result;
280
+ }
281
+
230
282
  export function renderToolsSection(modules: readonly ToolModuleDescriptor[] = describeToolModules()): string[] {
231
283
  const lines: string[] = ["### Tools", ""];
232
284
  const groupedTools: GroupedToolInfo[] = [];
package/server.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chrisgleissner/c64bridge",
4
+ "title": "C64 Bridge",
5
+ "description": "MCP server for Commodore 64 Ultimate hardware and VICE emulator control.",
6
+ "repository": {
7
+ "url": "https://github.com/chrisgleissner/c64bridge",
8
+ "source": "github"
9
+ },
10
+ "websiteUrl": "https://github.com/chrisgleissner/c64bridge",
11
+ "version": "0.9.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "identifier": "c64bridge",
16
+ "version": "0.9.0",
17
+ "transport": {
18
+ "type": "stdio"
19
+ },
20
+ "environmentVariables": [
21
+ {
22
+ "name": "LOG_LEVEL",
23
+ "description": "Logger verbosity.",
24
+ "format": "string",
25
+ "default": "info",
26
+ "choices": [
27
+ "debug",
28
+ "info",
29
+ "warn",
30
+ "error"
31
+ ]
32
+ },
33
+ {
34
+ "name": "C64BRIDGE_CONFIG",
35
+ "description": "Path to the C64 Bridge configuration file.",
36
+ "format": "filepath",
37
+ "default": "~/.c64bridge.json"
38
+ },
39
+ {
40
+ "name": "C64_MODE",
41
+ "description": "Active backend.",
42
+ "format": "string",
43
+ "default": "c64u",
44
+ "choices": [
45
+ "c64u",
46
+ "vice"
47
+ ]
48
+ },
49
+ {
50
+ "name": "C64U_HOST",
51
+ "description": "Ultimate 64 host name or IP address.",
52
+ "format": "string",
53
+ "default": "c64u"
54
+ },
55
+ {
56
+ "name": "C64U_PORT",
57
+ "description": "Ultimate 64 REST port.",
58
+ "format": "number",
59
+ "default": "80"
60
+ },
61
+ {
62
+ "name": "C64U_PASSWORD",
63
+ "description": "Ultimate 64 network password sent as X-Password.",
64
+ "format": "string",
65
+ "isSecret": true
66
+ },
67
+ {
68
+ "name": "VICE_BINARY",
69
+ "description": "VICE emulator binary to launch.",
70
+ "format": "string",
71
+ "default": "x64sc"
72
+ },
73
+ {
74
+ "name": "VICE_VISIBLE",
75
+ "description": "Launch VICE with a visible window when possible.",
76
+ "format": "boolean",
77
+ "default": "true"
78
+ },
79
+ {
80
+ "name": "VICE_WARP",
81
+ "description": "Enable VICE warp mode.",
82
+ "format": "boolean",
83
+ "default": "false"
84
+ },
85
+ {
86
+ "name": "GITHUB_TOKEN",
87
+ "description": "GitHub token for optional RAG discovery against GitHub.",
88
+ "format": "string",
89
+ "isSecret": true
90
+ }
91
+ ]
92
+ }
93
+ ]
94
+ }