c64bridge 1.0.7 → 1.1.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/.github/skills/assembly-program/SKILL.md +37 -0
- package/.github/skills/basic-program/SKILL.md +40 -0
- package/.github/skills/cross-platform-demo/SKILL.md +32 -0
- package/.github/skills/drive-manager/SKILL.md +33 -0
- package/.github/skills/graphics-demo/SKILL.md +30 -0
- package/.github/skills/hello-world/SKILL.md +36 -0
- package/.github/skills/memory-debug/SKILL.md +32 -0
- package/.github/skills/pr-converge/SKILL.md +33 -0
- package/.github/skills/printer-job/SKILL.md +30 -0
- package/.github/skills/sid-music/SKILL.md +35 -0
- package/.github/skills/software-launch/SKILL.md +31 -0
- package/.github/skills/stream-control/SKILL.md +29 -0
- package/.github/skills/system-control/SKILL.md +32 -0
- package/AGENTS.md +28 -7
- package/README.md +69 -14
- package/data/assembly/assembly-spec.md +2 -0
- package/dist/c64Client.js +238 -60
- package/dist/config.js +48 -26
- package/dist/device.js +92 -37
- package/dist/diagnostics.js +62 -2
- package/dist/hostPaths.js +128 -0
- package/dist/loggingHttpClient.js +59 -1
- package/dist/mcp-server.js +409 -286
- package/dist/numericArg.js +55 -0
- package/dist/petscii.js +13 -1
- package/dist/platform.js +12 -3
- package/dist/prompts/registry.js +6 -1
- package/dist/tools/access.js +66 -0
- package/dist/tools/debug.js +6 -0
- package/dist/tools/developer.js +67 -34
- package/dist/tools/errors.js +54 -3
- package/dist/tools/input.js +23 -6
- package/dist/tools/machineControl.js +112 -20
- package/dist/tools/memory.js +135 -30
- package/dist/tools/meta/artifacts.js +5 -5
- package/dist/tools/meta/background.js +216 -54
- package/dist/tools/meta/compilation.js +6 -5
- package/dist/tools/meta/config.js +15 -6
- package/dist/tools/meta/filesystem.js +16 -14
- package/dist/tools/meta/graphics.js +48 -20
- package/dist/tools/meta/memory.js +45 -19
- package/dist/tools/meta/program.js +10 -7
- package/dist/tools/meta/screen.js +116 -25
- package/dist/tools/pauseGuard.js +23 -0
- package/dist/tools/pollValidator.js +53 -8
- package/dist/tools/programRunners.js +62 -18
- package/dist/tools/registry/batch.js +16 -4
- package/dist/tools/registry/config.js +9 -2
- package/dist/tools/registry/disk.js +12 -2
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/extract.js +3 -0
- package/dist/tools/registry/graphics.js +59 -6
- package/dist/tools/registry/index.js +17 -1
- package/dist/tools/registry/memory.js +11 -1
- package/dist/tools/registry/platform.js +4 -1
- package/dist/tools/registry/printer.js +2 -1
- package/dist/tools/registry/program.js +5 -1
- package/dist/tools/registry/rag.js +1 -0
- package/dist/tools/registry/sound.js +12 -1
- package/dist/tools/registry/stream.js +2 -1
- package/dist/tools/registry/system.js +8 -1
- package/dist/tools/schema.js +7 -7
- package/dist/tools/streaming.js +34 -4
- package/dist/tools/translation/assembler.js +247 -24
- package/dist/tools/translation/basicDetokenizer.js +80 -0
- package/dist/tools/translation/basicTokenizer.js +8 -3
- package/dist/tools/translation/disassembler.js +14 -0
- package/dist/tools/types.js +258 -7
- package/dist/tools/vice.js +5 -16
- package/dist/vicBitmap.js +1 -1
- package/dist/vice/mockServer.js +3 -0
- package/dist/vice/process.js +19 -0
- package/dist/vice/resourcePolicy.js +36 -0
- package/dist/vice/viceClient.js +22 -5
- package/doc/vice/support-matrix.md +17 -11
- package/mcp/schemas/c64_batch.schema.json +2 -1
- package/mcp/schemas/c64_config.schema.json +25 -475
- package/mcp/schemas/c64_debug.schema.json +3 -602
- package/mcp/schemas/c64_disk.schema.json +17 -344
- package/mcp/schemas/c64_drive.schema.json +2 -157
- package/mcp/schemas/c64_extract.schema.json +10 -364
- package/mcp/schemas/c64_graphics.schema.json +19 -426
- package/mcp/schemas/c64_input.schema.json +5 -318
- package/mcp/schemas/c64_memory.schema.json +23 -491
- package/mcp/schemas/c64_printer.schema.json +3 -258
- package/mcp/schemas/c64_program.schema.json +16 -491
- package/mcp/schemas/c64_rag.schema.json +1 -81
- package/mcp/schemas/c64_select_backend.schema.json +2 -36
- package/mcp/schemas/c64_sound.schema.json +20 -822
- package/mcp/schemas/c64_stream.schema.json +4 -75
- package/mcp/schemas/c64_system.schema.json +28 -348
- package/mcp/schemas/c64_vice.schema.json +2 -71
- package/mcp/server.json +1 -1
- package/mcp/tools.json +1983 -6910
- package/mcp.json +21 -1
- package/package.json +16 -8
- package/scripts/mockC64Server.mjs +44 -0
- package/scripts/run-coverage.mjs +27 -1
- package/scripts/smoke-run.sh +3 -2
- package/scripts/start.mjs +23 -1
- package/server.json +35 -2
- package/dist/.tsbuildinfo +0 -1
- package/doc/agent-state-spec.md +0 -243
- package/doc/audit/secret-handling.md +0 -61
- package/doc/plans/bun/baseline-metrics.md +0 -16
- package/doc/plans/bun/migration-checklist.md +0 -55
- package/doc/plans/bun/migration.md +0 -83
- package/doc/plans/improve-knowledge/notes.md +0 -43
- package/doc/plans/improve-knowledge/plan.md +0 -121
- package/doc/plans/improve-knowledge/rollout.md +0 -110
- package/doc/plans/meta-tools/README.md +0 -110
- package/doc/plans/meta-tools/analysis.md +0 -272
- package/doc/plans/meta-tools/catalog.md +0 -435
- package/doc/plans/meta-tools/notes.md +0 -192
- package/doc/plans/meta-tools/rollout.md +0 -368
- package/doc/plans/prompts/README.md +0 -96
- package/doc/plans/prompts/user-prompt-ideas.md +0 -147
- package/doc/plans/tool-consolidation/notes.md +0 -28
- package/doc/plans/tool-consolidation/plan.md +0 -169
- package/doc/plans/tool-consolidation/rollout.md +0 -104
- package/doc/plans/vice-backend-switch/plan.md +0 -486
- package/doc/plans/vice-backend-switch-2/plan.md +0 -48
- package/doc/plans/vice-support/notes.md +0 -63
- package/doc/plans/vice-support/plan.md +0 -88
- package/doc/plans/vice-support/rollout.md +0 -99
- package/doc/reviews/audit.md +0 -143
- package/doc/reviews/hardening/01-fable/prompt-handover.md +0 -125
- package/doc/reviews/hardening/01-fable/prompt.md +0 -98
- package/doc/reviews/hardening/01-fable/review-prompt.md +0 -468
- package/doc/reviews/hardening/01-fable/review.md +0 -1234
- package/doc/reviews/vice-support-review.md +0 -185
- package/scripts/compare-rest/rest-compare-2025-11-01T00-25-21-830Z.json +0 -2513
package/dist/mcp-server.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./bootstrap/stdio-logger.js";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
+
import { timingSafeEqual } from "node:crypto";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
9
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
|
-
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
10
11
|
import { loadConfig } from "./config.js";
|
|
11
12
|
import { C64Client } from "./c64Client.js";
|
|
12
13
|
import { listKnowledgeResources, readKnowledgeResource, } from "./rag/knowledgeIndex.js";
|
|
13
14
|
import { PLATFORM_RESOURCE_URI } from "./rag/resourceUris.js";
|
|
14
15
|
import { createLazyRagRetriever, initRag } from "./rag/init.js";
|
|
15
16
|
import { getMcpServerImplementationInfo } from "./mcp/metadata.js";
|
|
17
|
+
import { stopAllBackgroundTasks } from "./tools/meta/background.js";
|
|
18
|
+
import { delegateViceSignalHandling, stopManagedViceProcessesSync } from "./device.js";
|
|
16
19
|
import { toolRegistry } from "./tools/registry/index.js";
|
|
17
|
-
import {
|
|
20
|
+
import { ACCESS_ENV_VAR, getAccessMode } from "./tools/access.js";
|
|
21
|
+
import { ToolValidationError, unknownErrorResult } from "./tools/errors.js";
|
|
22
|
+
import { publishedInputSchema } from "./tools/types.js";
|
|
18
23
|
import { createPromptRegistry } from "./prompts/registry.js";
|
|
19
24
|
import { describePlatformCapabilities, getPlatformStatus, setPlatform } from "./platform.js";
|
|
20
25
|
import axios from "axios";
|
|
@@ -43,6 +48,84 @@ export function parseCliOptions(argv) {
|
|
|
43
48
|
const portCandidate = argv[httpIndex + 1];
|
|
44
49
|
return { mode: "http", port: parsePort(portCandidate) };
|
|
45
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* DNS-rebinding and cross-origin protection for the HTTP transport. Returns
|
|
53
|
+
* the rejection (status and message) or null when the request may proceed.
|
|
54
|
+
* Host must be a loopback name with the listening port, any Origin must be a
|
|
55
|
+
* loopback http origin on that port, and when a token is configured the
|
|
56
|
+
* request must carry the matching bearer token.
|
|
57
|
+
*/
|
|
58
|
+
export function checkHttpRequestAccess(headers, port, token) {
|
|
59
|
+
const allowedHosts = [`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`];
|
|
60
|
+
const host = headers.host?.toLowerCase();
|
|
61
|
+
if (!host || !allowedHosts.includes(host)) {
|
|
62
|
+
return { status: 403, message: "Forbidden: invalid Host header" };
|
|
63
|
+
}
|
|
64
|
+
const origin = headers.origin;
|
|
65
|
+
if (origin !== undefined) {
|
|
66
|
+
const allowedOrigins = allowedHosts.map((entry) => `http://${entry}`);
|
|
67
|
+
if (typeof origin !== "string" || !allowedOrigins.includes(origin.toLowerCase())) {
|
|
68
|
+
return { status: 403, message: "Forbidden: invalid Origin header" };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (token) {
|
|
72
|
+
const header = headers.authorization;
|
|
73
|
+
const presented = typeof header === "string" && /^Bearer /i.test(header) ? header.slice(7) : "";
|
|
74
|
+
const expectedBuffer = Buffer.from(token, "utf8");
|
|
75
|
+
const presentedBuffer = Buffer.from(presented, "utf8");
|
|
76
|
+
if (presentedBuffer.length !== expectedBuffer.length || !timingSafeEqual(presentedBuffer, expectedBuffer)) {
|
|
77
|
+
return { status: 401, message: "Unauthorized" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const SHUTDOWN_CLEANUP_TIMEOUT_MS = 2000;
|
|
83
|
+
/**
|
|
84
|
+
* Installs the single shutdown path. The first trigger stops background
|
|
85
|
+
* tasks, releases any capture session and stops managed VICE processes, then
|
|
86
|
+
* exits. Later triggers return the same promise. Stdin end/close is a
|
|
87
|
+
* trigger only in stdio mode (the client went away).
|
|
88
|
+
*/
|
|
89
|
+
function installShutdown(client, mode) {
|
|
90
|
+
delegateViceSignalHandling();
|
|
91
|
+
let shutdownPromise = null;
|
|
92
|
+
const shutdown = (reason, exitCode) => {
|
|
93
|
+
if (shutdownPromise)
|
|
94
|
+
return shutdownPromise;
|
|
95
|
+
writeDiagnosticEvent("server_shutdown", { reason });
|
|
96
|
+
console.error(`c64bridge shutting down (${reason})`);
|
|
97
|
+
shutdownPromise = (async () => {
|
|
98
|
+
try {
|
|
99
|
+
const cleanup = Promise.allSettled([
|
|
100
|
+
stopAllBackgroundTasks(),
|
|
101
|
+
client.releaseVideoCapture(),
|
|
102
|
+
]);
|
|
103
|
+
await Promise.race([
|
|
104
|
+
cleanup,
|
|
105
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_CLEANUP_TIMEOUT_MS).unref()),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Cleanup is best effort; always continue to exit.
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
stopManagedViceProcessesSync();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore
|
|
116
|
+
}
|
|
117
|
+
process.exit(exitCode);
|
|
118
|
+
})();
|
|
119
|
+
return shutdownPromise;
|
|
120
|
+
};
|
|
121
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM", 143));
|
|
122
|
+
process.on("SIGINT", () => void shutdown("SIGINT", 130));
|
|
123
|
+
process.on("SIGHUP", () => void shutdown("SIGHUP", 129));
|
|
124
|
+
if (mode === "stdio") {
|
|
125
|
+
process.stdin.once("end", () => void shutdown("stdin end", 0));
|
|
126
|
+
process.stdin.once("close", () => void shutdown("stdin close", 0));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
46
129
|
function parsePort(raw) {
|
|
47
130
|
if (!raw || raw.startsWith("--")) {
|
|
48
131
|
return undefined;
|
|
@@ -109,344 +192,377 @@ async function main() {
|
|
|
109
192
|
const promptRegistry = createPromptRegistry();
|
|
110
193
|
const getPromptRegistry = createPromptRegistryGetter();
|
|
111
194
|
const implementationInfo = getMcpServerImplementationInfo();
|
|
112
|
-
// Create MCP server
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (resourceLogger.isDebugEnabled()) {
|
|
140
|
-
resourceLogger.debug("list resources request", { request: {} });
|
|
141
|
-
resourceLogger.debug("list resources response", { response: formatPayloadForDebug(response) });
|
|
142
|
-
}
|
|
143
|
-
writeDiagnosticEvent("mcp_list_resources_ok", {
|
|
144
|
-
count: response.resources.length,
|
|
145
|
-
latencyMs: latency,
|
|
146
|
-
});
|
|
147
|
-
return response;
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
const latency = Date.now() - startedAt;
|
|
151
|
-
resourceLogger.error(`list resources failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
152
|
-
if (resourceLogger.isDebugEnabled()) {
|
|
153
|
-
resourceLogger.debug("list resources request", { request: {} });
|
|
154
|
-
resourceLogger.debug("list resources error", { error: formatErrorMessage(error) });
|
|
155
|
-
}
|
|
156
|
-
writeDiagnosticEvent("mcp_list_resources_failed", { latencyMs: latency, error });
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
161
|
-
const startedAt = Date.now();
|
|
162
|
-
writeDiagnosticEvent("mcp_read_resource_start", { uri: request.params.uri });
|
|
163
|
-
try {
|
|
164
|
-
let response;
|
|
165
|
-
if (request.params.uri === PLATFORM_RESOURCE_URI) {
|
|
166
|
-
response = {
|
|
167
|
-
contents: [
|
|
168
|
-
{
|
|
169
|
-
uri: PLATFORM_RESOURCE_URI,
|
|
170
|
-
mimeType: "text/markdown",
|
|
171
|
-
text: await renderPlatformStatusMarkdown(client),
|
|
172
|
-
},
|
|
173
|
-
],
|
|
195
|
+
// Create MCP server. Stateless HTTP mode needs a fresh Server and transport
|
|
196
|
+
// per request (the SDK rejects reuse of a stateless transport), so server
|
|
197
|
+
// creation and handler registration live in this factory. The handler bodies
|
|
198
|
+
// below are unchanged and intentionally not re-indented.
|
|
199
|
+
const createMcpServer = () => {
|
|
200
|
+
const server = new Server(implementationInfo, {
|
|
201
|
+
capabilities: {
|
|
202
|
+
resources: {},
|
|
203
|
+
tools: {},
|
|
204
|
+
prompts: {},
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
// RESOURCES: Expose C64 knowledge base
|
|
208
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
209
|
+
const startedAt = Date.now();
|
|
210
|
+
writeDiagnosticEvent("mcp_list_resources_start");
|
|
211
|
+
try {
|
|
212
|
+
const knowledgeResources = listKnowledgeResources().map((resource) => ({
|
|
213
|
+
uri: resource.uri,
|
|
214
|
+
name: resource.name,
|
|
215
|
+
description: resource.description,
|
|
216
|
+
mimeType: resource.mimeType,
|
|
217
|
+
_meta: resource.metadata,
|
|
218
|
+
}));
|
|
219
|
+
const platformResource = createPlatformResourceDescriptor();
|
|
220
|
+
const response = {
|
|
221
|
+
resources: [...knowledgeResources, platformResource],
|
|
174
222
|
};
|
|
223
|
+
const latency = Date.now() - startedAt;
|
|
224
|
+
const bytes = payloadByteLength(response);
|
|
225
|
+
resourceLogger.info(`list resources count=${response.resources.length} bytes=${bytes} latencyMs=${latency}`);
|
|
226
|
+
if (resourceLogger.isDebugEnabled()) {
|
|
227
|
+
resourceLogger.debug("list resources request", { request: {} });
|
|
228
|
+
resourceLogger.debug("list resources response", { response: formatPayloadForDebug(response) });
|
|
229
|
+
}
|
|
230
|
+
writeDiagnosticEvent("mcp_list_resources_ok", {
|
|
231
|
+
count: response.resources.length,
|
|
232
|
+
latencyMs: latency,
|
|
233
|
+
});
|
|
234
|
+
return response;
|
|
175
235
|
}
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
236
|
+
catch (error) {
|
|
237
|
+
const latency = Date.now() - startedAt;
|
|
238
|
+
resourceLogger.error(`list resources failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
239
|
+
if (resourceLogger.isDebugEnabled()) {
|
|
240
|
+
resourceLogger.debug("list resources request", { request: {} });
|
|
241
|
+
resourceLogger.debug("list resources error", { error: formatErrorMessage(error) });
|
|
180
242
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
};
|
|
243
|
+
writeDiagnosticEvent("mcp_list_resources_failed", { latencyMs: latency, error });
|
|
244
|
+
throw error;
|
|
184
245
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
246
|
+
});
|
|
247
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
248
|
+
const startedAt = Date.now();
|
|
249
|
+
writeDiagnosticEvent("mcp_read_resource_start", { uri: request.params.uri });
|
|
250
|
+
try {
|
|
251
|
+
let response;
|
|
252
|
+
if (request.params.uri === PLATFORM_RESOURCE_URI) {
|
|
253
|
+
response = {
|
|
254
|
+
contents: [
|
|
255
|
+
{
|
|
256
|
+
uri: PLATFORM_RESOURCE_URI,
|
|
257
|
+
mimeType: "text/markdown",
|
|
258
|
+
text: await renderPlatformStatusMarkdown(client),
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const result = readKnowledgeResource(request.params.uri, PROJECT_ROOT);
|
|
265
|
+
if (!result) {
|
|
266
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
267
|
+
}
|
|
268
|
+
response = {
|
|
269
|
+
contents: [result],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const latency = Date.now() - startedAt;
|
|
273
|
+
const bytes = payloadByteLength(response);
|
|
274
|
+
resourceLogger.info(`read resource uri=${request.params.uri} bytes=${bytes} latencyMs=${latency}`);
|
|
275
|
+
if (resourceLogger.isDebugEnabled()) {
|
|
276
|
+
resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
|
|
277
|
+
resourceLogger.debug("read resource response", { response: formatPayloadForDebug(response) });
|
|
278
|
+
}
|
|
279
|
+
writeDiagnosticEvent("mcp_read_resource_ok", {
|
|
280
|
+
uri: request.params.uri,
|
|
281
|
+
latencyMs: latency,
|
|
282
|
+
});
|
|
283
|
+
return response;
|
|
191
284
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
latencyMs
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
285
|
+
catch (error) {
|
|
286
|
+
const latency = Date.now() - startedAt;
|
|
287
|
+
resourceLogger.error(`read resource uri=${request.params.uri} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
288
|
+
if (resourceLogger.isDebugEnabled()) {
|
|
289
|
+
resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
|
|
290
|
+
resourceLogger.debug("read resource error", { error: formatErrorMessage(error) });
|
|
291
|
+
}
|
|
292
|
+
writeDiagnosticEvent("mcp_read_resource_failed", {
|
|
293
|
+
uri: request.params.uri,
|
|
294
|
+
latencyMs: latency,
|
|
295
|
+
error,
|
|
296
|
+
});
|
|
297
|
+
throw error;
|
|
204
298
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
299
|
+
});
|
|
300
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
301
|
+
const startedAt = Date.now();
|
|
302
|
+
writeDiagnosticEvent("mcp_list_tools_start");
|
|
303
|
+
try {
|
|
304
|
+
const response = {
|
|
305
|
+
tools: toolRegistry.list().map((tool) => ({
|
|
306
|
+
name: tool.name,
|
|
307
|
+
title: tool.metadata.summary,
|
|
308
|
+
description: tool.description,
|
|
309
|
+
inputSchema: publishedInputSchema(tool.inputSchema),
|
|
310
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
311
|
+
_meta: tool.metadata,
|
|
312
|
+
})),
|
|
313
|
+
};
|
|
314
|
+
const latency = Date.now() - startedAt;
|
|
315
|
+
const bytes = payloadByteLength(response);
|
|
316
|
+
toolLogger.info(`list tools count=${response.tools.length} bytes=${bytes} latencyMs=${latency}`);
|
|
317
|
+
if (toolLogger.isDebugEnabled()) {
|
|
318
|
+
toolLogger.debug("list tools request", { request: {} });
|
|
319
|
+
toolLogger.debug("list tools response", { response: formatPayloadForDebug(response) });
|
|
320
|
+
}
|
|
321
|
+
writeDiagnosticEvent("mcp_list_tools_ok", {
|
|
322
|
+
count: response.tools.length,
|
|
323
|
+
latencyMs: latency,
|
|
324
|
+
});
|
|
325
|
+
return response;
|
|
232
326
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
latencyMs
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (toolLogger.isDebugEnabled()) {
|
|
243
|
-
toolLogger.debug("list tools request", { request: {} });
|
|
244
|
-
toolLogger.debug("list tools error", { error: formatErrorMessage(error) });
|
|
327
|
+
catch (error) {
|
|
328
|
+
const latency = Date.now() - startedAt;
|
|
329
|
+
toolLogger.error(`list tools failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
330
|
+
if (toolLogger.isDebugEnabled()) {
|
|
331
|
+
toolLogger.debug("list tools request", { request: {} });
|
|
332
|
+
toolLogger.debug("list tools error", { error: formatErrorMessage(error) });
|
|
333
|
+
}
|
|
334
|
+
writeDiagnosticEvent("mcp_list_tools_failed", { latencyMs: latency, error });
|
|
335
|
+
throw error;
|
|
245
336
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
337
|
+
});
|
|
338
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
339
|
+
const startedAt = Date.now();
|
|
340
|
+
writeDiagnosticEvent("mcp_list_prompts_start");
|
|
341
|
+
try {
|
|
342
|
+
const entries = promptRegistry.list();
|
|
343
|
+
const response = {
|
|
344
|
+
prompts: entries.map((entry) => ({
|
|
345
|
+
name: entry.descriptor.name,
|
|
346
|
+
title: entry.descriptor.title,
|
|
347
|
+
description: entry.descriptor.description,
|
|
348
|
+
arguments: entry.arguments?.map((argument) => ({
|
|
349
|
+
name: argument.name,
|
|
350
|
+
description: argument.description,
|
|
351
|
+
required: argument.required,
|
|
352
|
+
options: argument.options,
|
|
353
|
+
})),
|
|
354
|
+
_meta: {
|
|
355
|
+
requiredResources: entry.descriptor.requiredResources,
|
|
356
|
+
optionalResources: entry.descriptor.optionalResources ?? [],
|
|
357
|
+
tools: entry.descriptor.tools,
|
|
358
|
+
tags: entry.descriptor.tags ?? [],
|
|
359
|
+
argumentOptions: Object.fromEntries((entry.arguments ?? [])
|
|
360
|
+
.filter((argument) => Array.isArray(argument.options) && argument.options.length > 0)
|
|
361
|
+
.map((argument) => [argument.name, argument.options])),
|
|
362
|
+
},
|
|
265
363
|
})),
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
promptLogger.info(`list prompts count=${response.prompts.length} bytes=${bytes} latencyMs=${latency}`);
|
|
280
|
-
if (promptLogger.isDebugEnabled()) {
|
|
281
|
-
promptLogger.debug("list prompts request", { request: {} });
|
|
282
|
-
promptLogger.debug("list prompts response", { response: formatPayloadForDebug(response) });
|
|
364
|
+
};
|
|
365
|
+
const latency = Date.now() - startedAt;
|
|
366
|
+
const bytes = payloadByteLength(response);
|
|
367
|
+
promptLogger.info(`list prompts count=${response.prompts.length} bytes=${bytes} latencyMs=${latency}`);
|
|
368
|
+
if (promptLogger.isDebugEnabled()) {
|
|
369
|
+
promptLogger.debug("list prompts request", { request: {} });
|
|
370
|
+
promptLogger.debug("list prompts response", { response: formatPayloadForDebug(response) });
|
|
371
|
+
}
|
|
372
|
+
writeDiagnosticEvent("mcp_list_prompts_ok", {
|
|
373
|
+
count: response.prompts.length,
|
|
374
|
+
latencyMs: latency,
|
|
375
|
+
});
|
|
376
|
+
return response;
|
|
283
377
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
latencyMs
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (promptLogger.isDebugEnabled()) {
|
|
294
|
-
promptLogger.debug("list prompts request", { request: {} });
|
|
295
|
-
promptLogger.debug("list prompts error", { error: formatErrorMessage(error) });
|
|
378
|
+
catch (error) {
|
|
379
|
+
const latency = Date.now() - startedAt;
|
|
380
|
+
promptLogger.error(`list prompts failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
381
|
+
if (promptLogger.isDebugEnabled()) {
|
|
382
|
+
promptLogger.debug("list prompts request", { request: {} });
|
|
383
|
+
promptLogger.debug("list prompts error", { error: formatErrorMessage(error) });
|
|
384
|
+
}
|
|
385
|
+
writeDiagnosticEvent("mcp_list_prompts_failed", { latencyMs: latency, error });
|
|
386
|
+
throw error;
|
|
296
387
|
}
|
|
297
|
-
writeDiagnosticEvent("mcp_list_prompts_failed", { latencyMs: latency, error });
|
|
298
|
-
throw error;
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
302
|
-
const { name } = request.params;
|
|
303
|
-
const args = request.params.arguments ?? {};
|
|
304
|
-
const startedAt = Date.now();
|
|
305
|
-
writeDiagnosticEvent("mcp_call_tool_start", {
|
|
306
|
-
name,
|
|
307
|
-
arguments: formatPayloadForDebug(args),
|
|
308
388
|
});
|
|
309
|
-
|
|
310
|
-
|
|
389
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
390
|
+
const { name } = request.params;
|
|
391
|
+
const args = request.params.arguments ?? {};
|
|
392
|
+
const startedAt = Date.now();
|
|
393
|
+
writeDiagnosticEvent("mcp_call_tool_start", {
|
|
311
394
|
name,
|
|
312
395
|
arguments: formatPayloadForDebug(args),
|
|
313
396
|
});
|
|
314
|
-
}
|
|
315
|
-
try {
|
|
316
|
-
const result = await toolRegistry.invoke(name, args, {
|
|
317
|
-
client,
|
|
318
|
-
rag,
|
|
319
|
-
logger: toolLogger,
|
|
320
|
-
platform: getPlatformStatus(),
|
|
321
|
-
setPlatform,
|
|
322
|
-
});
|
|
323
|
-
const response = toCallToolResult(result);
|
|
324
|
-
const latency = Date.now() - startedAt;
|
|
325
|
-
const bytes = payloadByteLength(response);
|
|
326
|
-
const status = result.isError ? "error" : "ok";
|
|
327
|
-
toolLogger.info(`call tool name=${name} status=${status} bytes=${bytes} latencyMs=${latency}`);
|
|
328
397
|
if (toolLogger.isDebugEnabled()) {
|
|
329
|
-
toolLogger.debug("tool
|
|
398
|
+
toolLogger.debug("tool request", {
|
|
330
399
|
name,
|
|
331
|
-
|
|
400
|
+
arguments: formatPayloadForDebug(args),
|
|
332
401
|
});
|
|
333
402
|
}
|
|
334
|
-
|
|
335
|
-
name,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
403
|
+
try {
|
|
404
|
+
const result = await toolRegistry.invoke(name, args, {
|
|
405
|
+
client,
|
|
406
|
+
rag,
|
|
407
|
+
logger: toolLogger,
|
|
408
|
+
platform: getPlatformStatus(),
|
|
409
|
+
setPlatform,
|
|
410
|
+
});
|
|
411
|
+
const response = toCallToolResult(result);
|
|
412
|
+
const latency = Date.now() - startedAt;
|
|
413
|
+
const bytes = payloadByteLength(response);
|
|
414
|
+
const status = result.isError ? "error" : "ok";
|
|
415
|
+
toolLogger.info(`call tool name=${name} status=${status} bytes=${bytes} latencyMs=${latency}`);
|
|
416
|
+
if (toolLogger.isDebugEnabled()) {
|
|
417
|
+
toolLogger.debug("tool response", {
|
|
418
|
+
name,
|
|
419
|
+
response: formatPayloadForDebug(response),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
writeDiagnosticEvent("mcp_call_tool_ok", {
|
|
349
423
|
name,
|
|
350
|
-
|
|
351
|
-
|
|
424
|
+
latencyMs: latency,
|
|
425
|
+
isError: result.isError === true,
|
|
352
426
|
});
|
|
427
|
+
return response;
|
|
353
428
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
arguments: formatPayloadForDebug(args),
|
|
369
|
-
});
|
|
370
|
-
if (promptLogger.isDebugEnabled()) {
|
|
371
|
-
promptLogger.debug("prompt request", {
|
|
372
|
-
name,
|
|
373
|
-
arguments: formatPayloadForDebug(args),
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
try {
|
|
377
|
-
const resolved = promptRegistry.resolve(name, args);
|
|
378
|
-
const response = {
|
|
379
|
-
description: resolved.description,
|
|
380
|
-
messages: resolved.messages.map(toPromptMessage),
|
|
381
|
-
_meta: {
|
|
382
|
-
arguments: resolved.arguments,
|
|
383
|
-
resources: resolved.resources.map((resource) => ({
|
|
384
|
-
uri: resource.uri,
|
|
385
|
-
name: resource.name,
|
|
386
|
-
description: resource.description,
|
|
387
|
-
mimeType: resource.mimeType,
|
|
388
|
-
metadata: resource.metadata,
|
|
389
|
-
})),
|
|
390
|
-
tools: resolved.tools,
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
const latency = Date.now() - startedAt;
|
|
394
|
-
const bytes = payloadByteLength(response);
|
|
395
|
-
promptLogger.info(`get prompt name=${name} bytes=${bytes} latencyMs=${latency}`);
|
|
396
|
-
if (promptLogger.isDebugEnabled()) {
|
|
397
|
-
promptLogger.debug("prompt response", {
|
|
429
|
+
catch (error) {
|
|
430
|
+
const latency = Date.now() - startedAt;
|
|
431
|
+
const fallback = unknownErrorResult(error);
|
|
432
|
+
const response = toCallToolResult(fallback);
|
|
433
|
+
const bytes = payloadByteLength(response);
|
|
434
|
+
toolLogger.error(`call tool name=${name} status=failed bytes=${bytes} latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
435
|
+
if (toolLogger.isDebugEnabled()) {
|
|
436
|
+
toolLogger.debug("tool response", {
|
|
437
|
+
name,
|
|
438
|
+
response: formatPayloadForDebug(response),
|
|
439
|
+
error: formatErrorMessage(error),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
writeDiagnosticEvent("mcp_call_tool_failed", {
|
|
398
443
|
name,
|
|
399
|
-
|
|
444
|
+
latencyMs: latency,
|
|
445
|
+
error,
|
|
400
446
|
});
|
|
447
|
+
return response;
|
|
401
448
|
}
|
|
402
|
-
|
|
449
|
+
});
|
|
450
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
451
|
+
const { name } = request.params;
|
|
452
|
+
const args = request.params.arguments ?? {};
|
|
453
|
+
const startedAt = Date.now();
|
|
454
|
+
writeDiagnosticEvent("mcp_get_prompt_start", {
|
|
403
455
|
name,
|
|
404
|
-
|
|
456
|
+
arguments: formatPayloadForDebug(args),
|
|
405
457
|
});
|
|
406
|
-
return response;
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
const latency = Date.now() - startedAt;
|
|
410
|
-
promptLogger.error(`get prompt name=${name} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
411
458
|
if (promptLogger.isDebugEnabled()) {
|
|
412
459
|
promptLogger.debug("prompt request", {
|
|
413
460
|
name,
|
|
414
461
|
arguments: formatPayloadForDebug(args),
|
|
415
462
|
});
|
|
416
|
-
promptLogger.debug("prompt error", { name, error: formatErrorMessage(error) });
|
|
417
463
|
}
|
|
418
|
-
|
|
419
|
-
name,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
464
|
+
try {
|
|
465
|
+
const resolved = promptRegistry.resolve(name, args);
|
|
466
|
+
const response = {
|
|
467
|
+
description: resolved.description,
|
|
468
|
+
messages: resolved.messages.map(toPromptMessage),
|
|
469
|
+
_meta: {
|
|
470
|
+
arguments: resolved.arguments,
|
|
471
|
+
resources: resolved.resources.map((resource) => ({
|
|
472
|
+
uri: resource.uri,
|
|
473
|
+
name: resource.name,
|
|
474
|
+
description: resource.description,
|
|
475
|
+
mimeType: resource.mimeType,
|
|
476
|
+
metadata: resource.metadata,
|
|
477
|
+
})),
|
|
478
|
+
tools: resolved.tools,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const latency = Date.now() - startedAt;
|
|
482
|
+
const bytes = payloadByteLength(response);
|
|
483
|
+
promptLogger.info(`get prompt name=${name} bytes=${bytes} latencyMs=${latency}`);
|
|
484
|
+
if (promptLogger.isDebugEnabled()) {
|
|
485
|
+
promptLogger.debug("prompt response", {
|
|
486
|
+
name,
|
|
487
|
+
response: formatPayloadForDebug(response),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
writeDiagnosticEvent("mcp_get_prompt_ok", {
|
|
491
|
+
name,
|
|
492
|
+
latencyMs: latency,
|
|
493
|
+
});
|
|
494
|
+
return response;
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
const latency = Date.now() - startedAt;
|
|
498
|
+
promptLogger.error(`get prompt name=${name} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
499
|
+
if (promptLogger.isDebugEnabled()) {
|
|
500
|
+
promptLogger.debug("prompt request", {
|
|
501
|
+
name,
|
|
502
|
+
arguments: formatPayloadForDebug(args),
|
|
503
|
+
});
|
|
504
|
+
promptLogger.debug("prompt error", { name, error: formatErrorMessage(error) });
|
|
505
|
+
}
|
|
506
|
+
writeDiagnosticEvent("mcp_get_prompt_failed", {
|
|
507
|
+
name,
|
|
508
|
+
latencyMs: latency,
|
|
509
|
+
error,
|
|
510
|
+
});
|
|
511
|
+
if (error instanceof ToolValidationError) {
|
|
512
|
+
throw new McpError(ErrorCode.InvalidParams, error.message);
|
|
513
|
+
}
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
return server;
|
|
518
|
+
};
|
|
426
519
|
const cli = parseCliOptions(process.argv.slice(2));
|
|
427
520
|
if (cli.mode === "http") {
|
|
428
|
-
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
429
|
-
await server.connect(transport);
|
|
430
521
|
const port = cli.port ?? 3000;
|
|
522
|
+
const httpToken = process.env.C64BRIDGE_HTTP_TOKEN || undefined;
|
|
523
|
+
if (httpToken) {
|
|
524
|
+
registerSecret(httpToken);
|
|
525
|
+
}
|
|
431
526
|
const httpServer = createServer((request, response) => {
|
|
527
|
+
const rejection = checkHttpRequestAccess(request.headers, port, httpToken);
|
|
528
|
+
if (rejection) {
|
|
529
|
+
response.statusCode = rejection.status;
|
|
530
|
+
if (rejection.status === 401)
|
|
531
|
+
response.setHeader("WWW-Authenticate", "Bearer");
|
|
532
|
+
response.end(rejection.message);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
432
535
|
if (request.url?.split("?")[0] !== "/mcp") {
|
|
433
536
|
response.statusCode = 404;
|
|
434
537
|
response.end("Not found");
|
|
435
538
|
return;
|
|
436
539
|
}
|
|
437
|
-
|
|
540
|
+
// Stateless mode: one Server and one transport per request.
|
|
541
|
+
const server = createMcpServer();
|
|
542
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
543
|
+
response.once("close", () => {
|
|
544
|
+
void transport.close().catch(() => { });
|
|
545
|
+
void server.close().catch(() => { });
|
|
546
|
+
});
|
|
547
|
+
void server
|
|
548
|
+
.connect(transport)
|
|
549
|
+
.then(() => transport.handleRequest(request, response))
|
|
550
|
+
.catch((error) => {
|
|
438
551
|
if (!response.headersSent)
|
|
439
552
|
response.statusCode = 500;
|
|
440
553
|
response.end(error instanceof Error ? error.message : "MCP transport error");
|
|
441
554
|
});
|
|
442
555
|
});
|
|
443
556
|
await new Promise((resolve, reject) => httpServer.listen(port, "127.0.0.1", resolve).once("error", reject));
|
|
557
|
+
installShutdown(client, "http");
|
|
444
558
|
writeDiagnosticEvent("mcp_transport_connected", { mode: "http", port });
|
|
445
559
|
console.error(`c64bridge MCP server running on HTTP at http://127.0.0.1:${port}/mcp`);
|
|
446
560
|
}
|
|
447
561
|
else {
|
|
562
|
+
const server = createMcpServer();
|
|
448
563
|
const transport = new StdioServerTransport();
|
|
449
564
|
await server.connect(transport);
|
|
565
|
+
installShutdown(client, "stdio");
|
|
450
566
|
writeDiagnosticEvent("mcp_transport_connected", { mode: "stdio" });
|
|
451
567
|
console.error("c64bridge MCP server running on stdio");
|
|
452
568
|
}
|
|
@@ -489,6 +605,8 @@ export async function renderPlatformStatusMarkdown(client) {
|
|
|
489
605
|
"",
|
|
490
606
|
`Current platform: \`${status.id}\``,
|
|
491
607
|
"",
|
|
608
|
+
`Access mode: \`${getAccessMode()}\` (\`${ACCESS_ENV_VAR}\`; full allows everything, no-destructive blocks power, flash, drive ROM, image creation and debug register operations, read-only allows only read operations)`,
|
|
609
|
+
"",
|
|
492
610
|
"## Available Backends",
|
|
493
611
|
"",
|
|
494
612
|
...availableBackends.map((backend) => `- \`${backend}\`${backend === status.id ? " (active)" : ""}`),
|
|
@@ -520,6 +638,11 @@ export async function renderPlatformStatusMarkdown(client) {
|
|
|
520
638
|
.map((tool) => `\`${tool}\``)
|
|
521
639
|
.join(", ")}`
|
|
522
640
|
: "- Unsupported tools: _none_");
|
|
641
|
+
lines.push(info.unsupported_operations.length > 0
|
|
642
|
+
? `- Unsupported operations of supported tools (${info.unsupported_operations.length}): ${info.unsupported_operations
|
|
643
|
+
.map((operation) => `\`${operation}\``)
|
|
644
|
+
.join(", ")}`
|
|
645
|
+
: "- Unsupported operations of supported tools: _none_");
|
|
523
646
|
lines.push("");
|
|
524
647
|
}
|
|
525
648
|
lines.push("> Use `c64_select_backend` to switch to another available backend without restarting the MCP server.");
|