@henols/c64-re-tools 1.0.0 → 2.0.0-rc.10
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/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
package/src/mcp/main.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
|
|
5
|
+
import { ViceSessionClient } from "../host-client/vice-session.ts";
|
|
6
|
+
import { elapsedMs, startTrace, trace } from "../trace.ts";
|
|
7
|
+
import { VIDEO_STANDARDS, WireFailure, type VideoStandard } from "../protocol/messages.ts";
|
|
8
|
+
import { createMcpServer, type SessionSource } from "./server.ts";
|
|
9
|
+
import { debugTools } from "./tools/debug.ts";
|
|
10
|
+
import { executionTools } from "./tools/execution.ts";
|
|
11
|
+
import { inputTools } from "./tools/input.ts";
|
|
12
|
+
import { machineTools } from "./tools/machine.ts";
|
|
13
|
+
import { mediaTools } from "./tools/media.ts";
|
|
14
|
+
import { memoryTools } from "./tools/memory.ts";
|
|
15
|
+
import { videoTools } from "./tools/video.ts";
|
|
16
|
+
import { PACKAGE } from "../package.ts";
|
|
17
|
+
|
|
18
|
+
const HELP = `c64-re-tools-mcp
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-mcp
|
|
22
|
+
c64-re-tools-mcp --help
|
|
23
|
+
|
|
24
|
+
Runs the stateful C64 MCP server on stdio. Each c64-re-tools-mcp process owns
|
|
25
|
+
one C64 emulator, started by the Host Runtime (c64-re-tools-host) when this
|
|
26
|
+
server starts and stopped when it exits. c64-re-tools install declares it for
|
|
27
|
+
the agent, through npx with the newest version of its channel.
|
|
28
|
+
|
|
29
|
+
Environment:
|
|
30
|
+
C64RT_VIDEO pal (default) or ntsc; fixed for the life of the process.
|
|
31
|
+
C64RT_HOST host:port of the Host Runtime when it is not on 127.0.0.1:6464,
|
|
32
|
+
host.docker.internal:6464 or host.containers.internal:6464.`;
|
|
33
|
+
|
|
34
|
+
function log(line: string): void {
|
|
35
|
+
process.stderr.write(`c64-re-tools-mcp: ${line}\n`);
|
|
36
|
+
trace().log(line);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function videoStandard(env: NodeJS.ProcessEnv): VideoStandard {
|
|
40
|
+
const value = env.C64RT_VIDEO ?? "pal";
|
|
41
|
+
if ((VIDEO_STANDARDS as readonly string[]).includes(value)) return value as VideoStandard;
|
|
42
|
+
throw new WireFailure("installation-incomplete", "C64RT_VIDEO must be pal or ntsc. Fix the MCP server configuration and restart it.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One session at a time (1 MCP = 1 session = 1 VICE). It opens at start; if
|
|
47
|
+
* that fails (for example the host was not running yet), the next tool call
|
|
48
|
+
* tries again. When the host connection is lost, the next call opens a new
|
|
49
|
+
* session (D57). A lost session that the emulator answered first gives its
|
|
50
|
+
* machine-state-lost once, unless a pending call got it already. Close stops
|
|
51
|
+
* an open that is still waiting for the host at once.
|
|
52
|
+
*/
|
|
53
|
+
function sessionSource(): { get: SessionSource; close(): Promise<void> } {
|
|
54
|
+
let opening: Promise<ViceSessionClient> | undefined;
|
|
55
|
+
let lostNotice: WireFailure | undefined;
|
|
56
|
+
let closed = false;
|
|
57
|
+
const stop = new AbortController();
|
|
58
|
+
const open = () => {
|
|
59
|
+
const started = performance.now();
|
|
60
|
+
const current = (async () => ViceSessionClient.open({ videoStandard: videoStandard(process.env), signal: stop.signal }))();
|
|
61
|
+
opening = current;
|
|
62
|
+
void current.then(
|
|
63
|
+
(client) => {
|
|
64
|
+
trace().event("session.opened", { ms: elapsedMs(started) });
|
|
65
|
+
void client.closed.then(() => drop(current, client));
|
|
66
|
+
},
|
|
67
|
+
() => {},
|
|
68
|
+
);
|
|
69
|
+
current.catch((error: unknown) => {
|
|
70
|
+
log(`session not opened: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
if (opening === current) opening = undefined;
|
|
72
|
+
});
|
|
73
|
+
return current;
|
|
74
|
+
};
|
|
75
|
+
/** Forgets a lost session, once; the next call opens a new one. */
|
|
76
|
+
const drop = (current: Promise<ViceSessionClient>, client: ViceSessionClient) => {
|
|
77
|
+
if (closed || opening !== current) return;
|
|
78
|
+
log("the connection to the host runtime was lost; the next call opens a new session");
|
|
79
|
+
opening = undefined;
|
|
80
|
+
if (client.answered) lostNotice = client.unreportedLoss;
|
|
81
|
+
};
|
|
82
|
+
open();
|
|
83
|
+
const get = async (): Promise<ViceSessionClient> => {
|
|
84
|
+
if (closed) throw new WireFailure("machine-unavailable", "The C64 session is shutting down.");
|
|
85
|
+
if (lostNotice !== undefined) {
|
|
86
|
+
const notice = lostNotice;
|
|
87
|
+
lostNotice = undefined;
|
|
88
|
+
throw notice;
|
|
89
|
+
}
|
|
90
|
+
const current = opening ?? open();
|
|
91
|
+
const client = await current;
|
|
92
|
+
if (!client.lost) return client;
|
|
93
|
+
// The loss is known here before the close event reaches drop.
|
|
94
|
+
drop(current, client);
|
|
95
|
+
return get();
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
get,
|
|
99
|
+
async close() {
|
|
100
|
+
closed = true;
|
|
101
|
+
stop.abort();
|
|
102
|
+
const session = await opening?.catch(() => undefined);
|
|
103
|
+
await session?.close();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main(): Promise<void> {
|
|
109
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
110
|
+
process.stdout.write(`${HELP}\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
startTrace("mcp", { warn: log });
|
|
114
|
+
const session = sessionSource();
|
|
115
|
+
const server = createMcpServer({ tools: [...machineTools, ...executionTools, ...debugTools, ...memoryTools, ...videoTools, ...inputTools, ...mediaTools], session: session.get, log });
|
|
116
|
+
|
|
117
|
+
let stopping = false;
|
|
118
|
+
const stop = async () => {
|
|
119
|
+
if (stopping) return;
|
|
120
|
+
stopping = true;
|
|
121
|
+
trace().event("mcp.stop");
|
|
122
|
+
await session.close();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
};
|
|
125
|
+
// The harness ends the server by closing stdin or by a signal; both end the session and its emulator.
|
|
126
|
+
server.onclose = () => void stop();
|
|
127
|
+
process.stdin.once("end", () => void stop());
|
|
128
|
+
process.once("SIGINT", () => void stop());
|
|
129
|
+
process.once("SIGTERM", () => void stop());
|
|
130
|
+
|
|
131
|
+
await server.connect(new StdioServerTransport());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await main();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Tool results written to a project file instead of the context (D39). The
|
|
2
|
+
// MCP server runs on the agent's machine with the project as its working
|
|
3
|
+
// directory, so it writes the file itself; the host never sees the path.
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import { writeProjectFile } from "../host-client/transfer.ts";
|
|
10
|
+
|
|
11
|
+
export const OutInput = z.string().min(1).describe("write the result to this file, relative to the project directory, in place of returning it. An existing file is overwritten");
|
|
12
|
+
|
|
13
|
+
/** What a result says in place of the bytes it wrote. */
|
|
14
|
+
export interface Written {
|
|
15
|
+
out: string;
|
|
16
|
+
bytes: number;
|
|
17
|
+
sha256: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The fields of a result with out, optional in an output schema that also has the inline form. */
|
|
21
|
+
export const WrittenOutput = {
|
|
22
|
+
out: z.string().optional(),
|
|
23
|
+
bytes: z.number().int().min(0).optional(),
|
|
24
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Writes `data` to the project file `path` and gives what the result shows in its place. */
|
|
28
|
+
export function writeOut(path: string, data: Uint8Array | string, root?: string): Written {
|
|
29
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
30
|
+
writeProjectFile(path, bytes, { what: "out", ...(root === undefined ? {} : { root }) });
|
|
31
|
+
return { out: path, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
32
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// MCP server creation, tool registration and the one place where failures
|
|
2
|
+
// become MCP results. Tool groups live in tools/; nothing here knows VICE.
|
|
3
|
+
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult, type Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { formatC64Address, parseC64Address } from "../c64.ts";
|
|
11
|
+
import { LostBeforeAnswer, type ViceSessionClient } from "../host-client/vice-session.ts";
|
|
12
|
+
import { shownMessage, WireFailure, type WireError } from "../protocol/messages.ts";
|
|
13
|
+
import { COMPARISONS, CONDITION_REGISTERS, MEMORY_VIEWS, SPACES, type Condition } from "../protocol/vice.ts";
|
|
14
|
+
import { elapsedMs, trace, type TraceFields } from "../trace.ts";
|
|
15
|
+
|
|
16
|
+
/** The session operations tools may call. */
|
|
17
|
+
export type ViceSessionApi = Pick<
|
|
18
|
+
ViceSessionClient,
|
|
19
|
+
| "status"
|
|
20
|
+
| "memoryRead"
|
|
21
|
+
| "registersGet"
|
|
22
|
+
| "memoryWrite"
|
|
23
|
+
| "memorySearch"
|
|
24
|
+
| "memoryCompare"
|
|
25
|
+
| "disassemble"
|
|
26
|
+
| "registersSet"
|
|
27
|
+
| "execution"
|
|
28
|
+
| "reset"
|
|
29
|
+
| "warp"
|
|
30
|
+
| "window"
|
|
31
|
+
| "programLoad"
|
|
32
|
+
| "autostart"
|
|
33
|
+
| "diskAttach"
|
|
34
|
+
| "keyboard"
|
|
35
|
+
| "joystick"
|
|
36
|
+
| "screenCapture"
|
|
37
|
+
| "screenCompare"
|
|
38
|
+
| "screenBaselines"
|
|
39
|
+
| "screenDiscard"
|
|
40
|
+
| "snapshot"
|
|
41
|
+
| "snapshotExport"
|
|
42
|
+
| "snapshotRestore"
|
|
43
|
+
| "cpuHistory"
|
|
44
|
+
| "backtrace"
|
|
45
|
+
| "timing"
|
|
46
|
+
| "profile"
|
|
47
|
+
| "memmap"
|
|
48
|
+
| "vicii"
|
|
49
|
+
| "sprites"
|
|
50
|
+
| "cia"
|
|
51
|
+
| "sid"
|
|
52
|
+
| "observe"
|
|
53
|
+
| "breakpoint"
|
|
54
|
+
| "watchpoint"
|
|
55
|
+
| "runUntil"
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
/** Resolves the session, opening it on first use when the start-up attempt failed. */
|
|
59
|
+
export type SessionSource = () => Promise<ViceSessionApi>;
|
|
60
|
+
|
|
61
|
+
type JsonObject = Record<string, unknown>;
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Public schema primitives shared by the tool groups
|
|
65
|
+
|
|
66
|
+
/** Input address: "$" and four hex digits, either case. Parses to a number. */
|
|
67
|
+
export const AddressInput = z
|
|
68
|
+
.string()
|
|
69
|
+
.regex(/^\$[0-9a-fA-F]{4}$/, "must be $ followed by four hex digits, for example $c000")
|
|
70
|
+
.transform((value) => parseC64Address(value))
|
|
71
|
+
.describe("C64 address, $0000 to $ffff");
|
|
72
|
+
|
|
73
|
+
/** Output address: "$" and four lowercase hex digits. */
|
|
74
|
+
export const AddressOutput = z.string().regex(/^\$[0-9a-f]{4}$/).describe("C64 address, $0000 to $ffff");
|
|
75
|
+
|
|
76
|
+
export const Byte = z.number().int().min(0).max(255);
|
|
77
|
+
|
|
78
|
+
export const HexData = z.string().regex(/^(?:[0-9a-f]{2})*$/).describe("bytes as lowercase hex, two digits per byte, no separators");
|
|
79
|
+
|
|
80
|
+
/** Input bytes: hex in either case, two digits per byte, at least one byte. Parses to lowercase. */
|
|
81
|
+
export const HexDataInput = z
|
|
82
|
+
.string()
|
|
83
|
+
.regex(/^(?:[0-9a-fA-F]{2})+$/, "must be hex bytes, two digits per byte, no separators, for example a9008d20d0")
|
|
84
|
+
.transform((value) => value.toLowerCase());
|
|
85
|
+
|
|
86
|
+
export const SpaceInput = z.enum(SPACES).default("c64").describe("c64 is the computer. drive8 is the 1541 disk drive CPU");
|
|
87
|
+
|
|
88
|
+
export const MemoryViewInput = z
|
|
89
|
+
.enum(MEMORY_VIEWS)
|
|
90
|
+
.default("cpu")
|
|
91
|
+
.describe("cpu reads what the CPU sees now (ROM and I/O where banked in). ram reads the RAM underneath (c64 only)");
|
|
92
|
+
|
|
93
|
+
/** A name the agent gives to something the session keeps, such as a screen baseline or a snapshot. */
|
|
94
|
+
export const TransientName = z
|
|
95
|
+
.string()
|
|
96
|
+
.regex(/^[A-Za-z0-9._-]{1,64}$/, "must be 1 to 64 letters, digits, dots, underscores or hyphens")
|
|
97
|
+
.describe("a name you choose: 1 to 64 letters, digits, dots, underscores or hyphens");
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks the fields of one action of a multi-action tool: a field that the
|
|
101
|
+
* action does not use, or a missing required field, is an invalid-input error.
|
|
102
|
+
*/
|
|
103
|
+
export function requireFields(action: string, given: Record<string, unknown>, allowed: readonly string[], required: readonly string[] = []): void {
|
|
104
|
+
for (const [field, value] of Object.entries(given)) {
|
|
105
|
+
if (value !== undefined && !allowed.includes(field)) throw new WireFailure("invalid-input", `${field} is not used with action ${action}.`);
|
|
106
|
+
}
|
|
107
|
+
for (const field of required) {
|
|
108
|
+
if (given[field] === undefined) throw new WireFailure("invalid-input", `Action ${action} needs ${field}.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A tool result that carries content blocks (for example an image) beside its structured result. */
|
|
113
|
+
export class ToolOutput<T> {
|
|
114
|
+
readonly structured: T;
|
|
115
|
+
readonly content: CallToolResult["content"];
|
|
116
|
+
|
|
117
|
+
constructor(structured: T, content: CallToolResult["content"]) {
|
|
118
|
+
this.structured = structured;
|
|
119
|
+
this.content = content;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ToolDefinition<I extends z.ZodObject = z.ZodObject, O extends z.ZodObject = z.ZodObject> {
|
|
124
|
+
name: string;
|
|
125
|
+
title: string;
|
|
126
|
+
description: string;
|
|
127
|
+
inputSchema: I;
|
|
128
|
+
outputSchema: O;
|
|
129
|
+
/** True for operations that only observe the machine. */
|
|
130
|
+
readOnly: boolean;
|
|
131
|
+
run(input: z.output<I>, session: ViceSessionApi): Promise<z.input<O> | ToolOutput<z.input<O>>>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A typed condition: register, memory or raster. No expression language. */
|
|
135
|
+
export const ConditionInput = z
|
|
136
|
+
.discriminatedUnion("kind", [
|
|
137
|
+
z
|
|
138
|
+
.object({
|
|
139
|
+
kind: z.literal("register"),
|
|
140
|
+
register: z.enum(CONDITION_REGISTERS),
|
|
141
|
+
operator: z.enum(COMPARISONS),
|
|
142
|
+
value: Byte,
|
|
143
|
+
})
|
|
144
|
+
.strict(),
|
|
145
|
+
z
|
|
146
|
+
.object({
|
|
147
|
+
kind: z.literal("memory"),
|
|
148
|
+
address: AddressInput,
|
|
149
|
+
operator: z.enum(COMPARISONS),
|
|
150
|
+
value: Byte,
|
|
151
|
+
space: SpaceInput,
|
|
152
|
+
view: MemoryViewInput,
|
|
153
|
+
})
|
|
154
|
+
.strict(),
|
|
155
|
+
z
|
|
156
|
+
.object({
|
|
157
|
+
kind: z.literal("raster"),
|
|
158
|
+
line: z.number().int().min(0).describe("raster line: PAL 0-311, NTSC 0-262"),
|
|
159
|
+
cycle: z.number().int().min(0).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. True from this cycle on"),
|
|
160
|
+
})
|
|
161
|
+
.strict(),
|
|
162
|
+
])
|
|
163
|
+
.describe("register: a CPU register compared to a byte. memory: a byte in memory compared to a byte. raster: the raster position");
|
|
164
|
+
|
|
165
|
+
/** A condition as a result shows it: the input form, with the memory address as "$xxxx". */
|
|
166
|
+
export const ConditionOutput = z
|
|
167
|
+
.discriminatedUnion("kind", [
|
|
168
|
+
z.object({ kind: z.literal("register"), register: z.enum(CONDITION_REGISTERS), operator: z.enum(COMPARISONS), value: Byte }),
|
|
169
|
+
z.object({ kind: z.literal("memory"), address: AddressOutput, operator: z.enum(COMPARISONS), value: Byte, space: z.enum(SPACES), view: z.enum(MEMORY_VIEWS) }),
|
|
170
|
+
z.object({ kind: z.literal("raster"), line: z.number().int().min(0), cycle: z.number().int().min(0).optional() }),
|
|
171
|
+
])
|
|
172
|
+
.describe("the condition given at add");
|
|
173
|
+
|
|
174
|
+
/** A protocol condition in its result form. */
|
|
175
|
+
export function showCondition(condition: Condition): z.input<typeof ConditionOutput> {
|
|
176
|
+
return condition.kind === "memory" ? { ...condition, address: formatC64Address(condition.address) } : condition;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Removes keys whose value is undefined, so optional fields stay absent. */
|
|
180
|
+
export function defined<T extends Record<string, unknown>>(value: T): T {
|
|
181
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A parsed condition input as the protocol type. */
|
|
185
|
+
export function toCondition(input: z.output<typeof ConditionInput>): Condition {
|
|
186
|
+
return defined(input) as Condition;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Keeps a tool's handler typed against its own schemas. */
|
|
190
|
+
export function defineTool<I extends z.ZodObject, O extends z.ZodObject>(tool: ToolDefinition<I, O>): ToolDefinition {
|
|
191
|
+
return tool as unknown as ToolDefinition;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function jsonSchema(schema: z.ZodObject, io: "input" | "output"): McpTool["inputSchema"] {
|
|
195
|
+
const { $schema: _ignored, ...rest } = z.toJSONSchema(schema, { io }) as JsonObject;
|
|
196
|
+
return rest as McpTool["inputSchema"];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function toolListing(tool: ToolDefinition): McpTool {
|
|
200
|
+
return {
|
|
201
|
+
name: tool.name,
|
|
202
|
+
title: tool.title,
|
|
203
|
+
description: tool.description,
|
|
204
|
+
inputSchema: jsonSchema(tool.inputSchema, "input"),
|
|
205
|
+
outputSchema: jsonSchema(tool.outputSchema, "output"),
|
|
206
|
+
annotations: { title: tool.title, readOnlyHint: tool.readOnly, openWorldHint: false },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function errorResult(error: WireError): CallToolResult {
|
|
211
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ code: error.code, message: shownMessage(error) }) }] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** "address: Invalid string: must match ..." style messages, one sentence per problem. */
|
|
215
|
+
function describeIssues(error: z.ZodError): string {
|
|
216
|
+
return error.issues
|
|
217
|
+
.map((issue) => {
|
|
218
|
+
const where = issue.path.length === 0 ? "input" : issue.path.join(".");
|
|
219
|
+
return `${where}: ${issue.message}`;
|
|
220
|
+
})
|
|
221
|
+
.join(". ");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const INTERNAL_FAILURE: WireError = { code: "operation-failed", message: "The operation failed inside the c64-re-tools MCP server." };
|
|
225
|
+
|
|
226
|
+
/** Runs one tool call and translates every outcome into an MCP result. The trace gets the call, its time and its outcome. */
|
|
227
|
+
export async function callTool(
|
|
228
|
+
tools: ReadonlyMap<string, ToolDefinition>,
|
|
229
|
+
name: string,
|
|
230
|
+
args: unknown,
|
|
231
|
+
session: SessionSource,
|
|
232
|
+
log: (line: string) => void = () => {},
|
|
233
|
+
): Promise<CallToolResult> {
|
|
234
|
+
const started = performance.now();
|
|
235
|
+
const result = await runCall(tools, name, args, session, log);
|
|
236
|
+
trace().event("tool.call", { name, args, ms: elapsedMs(started), ...outcome(result) });
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** What the trace records of a result: the structured result, or the code and message of the error. */
|
|
241
|
+
function outcome(result: CallToolResult): TraceFields {
|
|
242
|
+
if (result.isError !== true) return { ok: true, result: result.structuredContent };
|
|
243
|
+
const first = result.content[0];
|
|
244
|
+
const text = first?.type === "text" ? first.text : "";
|
|
245
|
+
try {
|
|
246
|
+
return { ok: false, ...(JSON.parse(text) as WireError) };
|
|
247
|
+
} catch {
|
|
248
|
+
return { ok: false, message: text };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function runCall(
|
|
253
|
+
tools: ReadonlyMap<string, ToolDefinition>,
|
|
254
|
+
name: string,
|
|
255
|
+
args: unknown,
|
|
256
|
+
session: SessionSource,
|
|
257
|
+
log: (line: string) => void,
|
|
258
|
+
): Promise<CallToolResult> {
|
|
259
|
+
const tool = tools.get(name);
|
|
260
|
+
if (tool === undefined) return errorResult({ code: "invalid-input", message: `There is no tool named ${name}.` });
|
|
261
|
+
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
262
|
+
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error) });
|
|
263
|
+
try {
|
|
264
|
+
let output: unknown;
|
|
265
|
+
try {
|
|
266
|
+
output = await tool.run(parsed.data, await session());
|
|
267
|
+
} catch (error) {
|
|
268
|
+
// A session that the emulator never answered had no machine state: the call goes again on a new session (D57).
|
|
269
|
+
if (!(error instanceof LostBeforeAnswer)) throw error;
|
|
270
|
+
output = await tool.run(parsed.data, await session());
|
|
271
|
+
}
|
|
272
|
+
// The result goes out in the shape that the tool listing advertises: keys
|
|
273
|
+
// that the output schema does not name are removed, and a result that does
|
|
274
|
+
// not fit the schema is an internal failure.
|
|
275
|
+
const checked = tool.outputSchema.safeParse(output instanceof ToolOutput ? output.structured : output);
|
|
276
|
+
if (!checked.success) {
|
|
277
|
+
log(`${name} gave a result outside its output schema: ${describeIssues(checked.error)}`);
|
|
278
|
+
return errorResult(INTERNAL_FAILURE);
|
|
279
|
+
}
|
|
280
|
+
const result = checked.data as JsonObject;
|
|
281
|
+
const extra = output instanceof ToolOutput ? output.content : [];
|
|
282
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }, ...extra], structuredContent: result };
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error instanceof WireFailure) return errorResult(error.toWire());
|
|
285
|
+
log(`${name} failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
286
|
+
return errorResult(INTERNAL_FAILURE);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The version of this package. A package.json without a version is a broken installation. */
|
|
291
|
+
function packageVersion(): string {
|
|
292
|
+
const { version } = createRequire(import.meta.url)("../../package.json") as { version?: unknown };
|
|
293
|
+
if (typeof version !== "string" || version === "") {
|
|
294
|
+
throw new Error("The package.json of c64-re-tools has no version. Install c64-re-tools again.");
|
|
295
|
+
}
|
|
296
|
+
return version;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const INSTRUCTIONS =
|
|
300
|
+
"These tools control one live Commodore 64 emulator that belongs to this session. " +
|
|
301
|
+
"Addresses are four hex digits after a dollar sign, for example $c000. " +
|
|
302
|
+
"Memory data is lowercase hex without separators. " +
|
|
303
|
+
"A read does not change the run state of the machine.";
|
|
304
|
+
|
|
305
|
+
export function createMcpServer(options: { tools: readonly ToolDefinition[]; session: SessionSource; log?: (line: string) => void }): Server {
|
|
306
|
+
const tools = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
307
|
+
const server = new Server(
|
|
308
|
+
{ name: "c64-re-tools", version: packageVersion() },
|
|
309
|
+
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS },
|
|
310
|
+
);
|
|
311
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: options.tools.map(toolListing) }));
|
|
312
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
|
313
|
+
callTool(tools, request.params.name, request.params.arguments, options.session, options.log),
|
|
314
|
+
);
|
|
315
|
+
return server;
|
|
316
|
+
}
|