c64bridge 1.0.6 → 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.
Files changed (136) hide show
  1. package/.github/skills/assembly-program/SKILL.md +37 -0
  2. package/.github/skills/basic-program/SKILL.md +40 -0
  3. package/.github/skills/cross-platform-demo/SKILL.md +32 -0
  4. package/.github/skills/drive-manager/SKILL.md +33 -0
  5. package/.github/skills/graphics-demo/SKILL.md +30 -0
  6. package/.github/skills/hello-world/SKILL.md +36 -0
  7. package/.github/skills/memory-debug/SKILL.md +32 -0
  8. package/.github/skills/pr-converge/SKILL.md +33 -0
  9. package/.github/skills/printer-job/SKILL.md +30 -0
  10. package/.github/skills/sid-music/SKILL.md +35 -0
  11. package/.github/skills/software-launch/SKILL.md +31 -0
  12. package/.github/skills/stream-control/SKILL.md +29 -0
  13. package/.github/skills/system-control/SKILL.md +32 -0
  14. package/AGENTS.md +28 -7
  15. package/README.md +70 -15
  16. package/data/assembly/assembly-spec.md +2 -0
  17. package/dist/c64Client.js +238 -60
  18. package/dist/chargen.js +0 -4
  19. package/dist/config.js +53 -28
  20. package/dist/device.js +92 -37
  21. package/dist/diagnostics.js +66 -5
  22. package/dist/hostPaths.js +128 -0
  23. package/dist/logger.js +3 -2
  24. package/dist/loggingHttpClient.js +71 -2
  25. package/dist/mcp-server.js +416 -291
  26. package/dist/numericArg.js +55 -0
  27. package/dist/petscii.js +13 -6
  28. package/dist/platform.js +12 -3
  29. package/dist/prompts/registry.js +6 -1
  30. package/dist/redact.js +166 -0
  31. package/dist/tools/access.js +66 -0
  32. package/dist/tools/debug.js +6 -0
  33. package/dist/tools/developer.js +77 -36
  34. package/dist/tools/errors.js +54 -3
  35. package/dist/tools/input.js +25 -8
  36. package/dist/tools/machineControl.js +113 -21
  37. package/dist/tools/memory.js +135 -30
  38. package/dist/tools/meta/artifacts.js +5 -5
  39. package/dist/tools/meta/background.js +216 -54
  40. package/dist/tools/meta/compilation.js +6 -5
  41. package/dist/tools/meta/config.js +20 -10
  42. package/dist/tools/meta/configInventory.js +25 -2
  43. package/dist/tools/meta/diagnostics.js +2 -1
  44. package/dist/tools/meta/filesystem.js +16 -14
  45. package/dist/tools/meta/graphics.js +48 -20
  46. package/dist/tools/meta/memory.js +45 -19
  47. package/dist/tools/meta/program.js +10 -7
  48. package/dist/tools/meta/screen.js +116 -25
  49. package/dist/tools/pauseGuard.js +23 -0
  50. package/dist/tools/pollValidator.js +53 -8
  51. package/dist/tools/programRunners.js +62 -18
  52. package/dist/tools/registry/batch.js +17 -11
  53. package/dist/tools/registry/config.js +9 -2
  54. package/dist/tools/registry/disk.js +12 -2
  55. package/dist/tools/registry/drive.js +3 -0
  56. package/dist/tools/registry/extract.js +3 -0
  57. package/dist/tools/registry/graphics.js +59 -6
  58. package/dist/tools/registry/index.js +17 -1
  59. package/dist/tools/registry/memory.js +11 -1
  60. package/dist/tools/registry/platform.js +4 -1
  61. package/dist/tools/registry/printer.js +2 -1
  62. package/dist/tools/registry/program.js +5 -1
  63. package/dist/tools/registry/rag.js +1 -0
  64. package/dist/tools/registry/sound.js +12 -1
  65. package/dist/tools/registry/stream.js +2 -1
  66. package/dist/tools/registry/system.js +8 -1
  67. package/dist/tools/schema.js +7 -7
  68. package/dist/tools/streaming.js +34 -4
  69. package/dist/tools/translation/assembler.js +247 -24
  70. package/dist/tools/translation/basicDetokenizer.js +80 -0
  71. package/dist/tools/translation/basicTokenizer.js +8 -3
  72. package/dist/tools/translation/disassembler.js +14 -0
  73. package/dist/tools/types.js +258 -7
  74. package/dist/tools/vice.js +5 -16
  75. package/dist/vicBitmap.js +1 -1
  76. package/dist/vice/mockServer.js +3 -0
  77. package/dist/vice/process.js +19 -0
  78. package/dist/vice/resourcePolicy.js +36 -0
  79. package/dist/vice/viceClient.js +22 -5
  80. package/doc/vice/support-matrix.md +17 -11
  81. package/mcp/schemas/c64_batch.schema.json +2 -1
  82. package/mcp/schemas/c64_config.schema.json +33 -466
  83. package/mcp/schemas/c64_debug.schema.json +3 -602
  84. package/mcp/schemas/c64_disk.schema.json +17 -344
  85. package/mcp/schemas/c64_drive.schema.json +2 -157
  86. package/mcp/schemas/c64_extract.schema.json +10 -364
  87. package/mcp/schemas/c64_graphics.schema.json +19 -426
  88. package/mcp/schemas/c64_input.schema.json +5 -318
  89. package/mcp/schemas/c64_memory.schema.json +23 -491
  90. package/mcp/schemas/c64_printer.schema.json +3 -258
  91. package/mcp/schemas/c64_program.schema.json +16 -491
  92. package/mcp/schemas/c64_rag.schema.json +1 -81
  93. package/mcp/schemas/c64_select_backend.schema.json +2 -36
  94. package/mcp/schemas/c64_sound.schema.json +20 -822
  95. package/mcp/schemas/c64_stream.schema.json +4 -75
  96. package/mcp/schemas/c64_system.schema.json +28 -348
  97. package/mcp/schemas/c64_vice.schema.json +2 -71
  98. package/mcp/server.json +1 -1
  99. package/mcp/tools.json +1990 -6900
  100. package/mcp.json +21 -1
  101. package/package.json +19 -8
  102. package/scripts/mockC64Server.mjs +44 -0
  103. package/scripts/run-coverage.mjs +27 -1
  104. package/scripts/smoke-run.sh +3 -2
  105. package/scripts/start.mjs +23 -1
  106. package/server.json +53 -3
  107. package/dist/.tsbuildinfo +0 -1
  108. package/doc/agent-state-spec.md +0 -243
  109. package/doc/plans/bun/baseline-metrics.md +0 -16
  110. package/doc/plans/bun/migration-checklist.md +0 -55
  111. package/doc/plans/bun/migration.md +0 -83
  112. package/doc/plans/improve-knowledge/notes.md +0 -43
  113. package/doc/plans/improve-knowledge/plan.md +0 -121
  114. package/doc/plans/improve-knowledge/rollout.md +0 -110
  115. package/doc/plans/meta-tools/README.md +0 -110
  116. package/doc/plans/meta-tools/analysis.md +0 -272
  117. package/doc/plans/meta-tools/catalog.md +0 -435
  118. package/doc/plans/meta-tools/notes.md +0 -192
  119. package/doc/plans/meta-tools/rollout.md +0 -368
  120. package/doc/plans/prompts/README.md +0 -96
  121. package/doc/plans/prompts/user-prompt-ideas.md +0 -147
  122. package/doc/plans/tool-consolidation/notes.md +0 -28
  123. package/doc/plans/tool-consolidation/plan.md +0 -169
  124. package/doc/plans/tool-consolidation/rollout.md +0 -104
  125. package/doc/plans/vice-backend-switch/plan.md +0 -486
  126. package/doc/plans/vice-backend-switch-2/plan.md +0 -48
  127. package/doc/plans/vice-support/notes.md +0 -63
  128. package/doc/plans/vice-support/plan.md +0 -88
  129. package/doc/plans/vice-support/rollout.md +0 -99
  130. package/doc/reviews/audit.md +0 -143
  131. package/doc/reviews/hardening/01-fable/prompt-handover.md +0 -125
  132. package/doc/reviews/hardening/01-fable/prompt.md +0 -98
  133. package/doc/reviews/hardening/01-fable/review-prompt.md +0 -468
  134. package/doc/reviews/hardening/01-fable/review.md +0 -1234
  135. package/doc/reviews/vice-support-review.md +0 -185
  136. package/scripts/compare-rest/rest-compare-2025-11-01T00-25-21-830Z.json +0 -2513
@@ -1,23 +1,29 @@
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 { unknownErrorResult } from "./tools/errors.js";
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";
26
+ import { redactValue, registerSecret } from "./redact.js";
21
27
  import { loggerFor, payloadByteLength, formatPayloadForDebug, formatErrorMessage } from "./logger.js";
22
28
  import { getDiagnosticsSessionInfo, installProcessDiagnostics, withDiagnosticSpan, writeDiagnosticEvent } from "./diagnostics.js";
23
29
  const __filename = fileURLToPath(import.meta.url);
@@ -42,6 +48,84 @@ export function parseCliOptions(argv) {
42
48
  const portCandidate = argv[httpIndex + 1];
43
49
  return { mode: "http", port: parsePort(portCandidate) };
44
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
+ }
45
129
  function parsePort(raw) {
46
130
  if (!raw || raw.startsWith("--")) {
47
131
  return undefined;
@@ -108,344 +192,377 @@ async function main() {
108
192
  const promptRegistry = createPromptRegistry();
109
193
  const getPromptRegistry = createPromptRegistryGetter();
110
194
  const implementationInfo = getMcpServerImplementationInfo();
111
- // Create MCP server
112
- const server = new Server(implementationInfo, {
113
- capabilities: {
114
- resources: {},
115
- tools: {},
116
- prompts: {},
117
- },
118
- });
119
- // RESOURCES: Expose C64 knowledge base
120
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
121
- const startedAt = Date.now();
122
- writeDiagnosticEvent("mcp_list_resources_start");
123
- try {
124
- const knowledgeResources = listKnowledgeResources().map((resource) => ({
125
- uri: resource.uri,
126
- name: resource.name,
127
- description: resource.description,
128
- mimeType: resource.mimeType,
129
- _meta: resource.metadata,
130
- }));
131
- const platformResource = createPlatformResourceDescriptor();
132
- const response = {
133
- resources: [...knowledgeResources, platformResource],
134
- };
135
- const latency = Date.now() - startedAt;
136
- const bytes = payloadByteLength(response);
137
- resourceLogger.info(`list resources count=${response.resources.length} bytes=${bytes} latencyMs=${latency}`);
138
- if (resourceLogger.isDebugEnabled()) {
139
- resourceLogger.debug("list resources request", { request: {} });
140
- resourceLogger.debug("list resources response", { response: formatPayloadForDebug(response) });
141
- }
142
- writeDiagnosticEvent("mcp_list_resources_ok", {
143
- count: response.resources.length,
144
- latencyMs: latency,
145
- });
146
- return response;
147
- }
148
- catch (error) {
149
- const latency = Date.now() - startedAt;
150
- resourceLogger.error(`list resources failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
151
- if (resourceLogger.isDebugEnabled()) {
152
- resourceLogger.debug("list resources request", { request: {} });
153
- resourceLogger.debug("list resources error", { error: formatErrorMessage(error) });
154
- }
155
- writeDiagnosticEvent("mcp_list_resources_failed", { latencyMs: latency, error });
156
- throw error;
157
- }
158
- });
159
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
160
- const startedAt = Date.now();
161
- writeDiagnosticEvent("mcp_read_resource_start", { uri: request.params.uri });
162
- try {
163
- let response;
164
- if (request.params.uri === PLATFORM_RESOURCE_URI) {
165
- response = {
166
- contents: [
167
- {
168
- uri: PLATFORM_RESOURCE_URI,
169
- mimeType: "text/markdown",
170
- text: await renderPlatformStatusMarkdown(client),
171
- },
172
- ],
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],
173
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;
174
235
  }
175
- else {
176
- const result = readKnowledgeResource(request.params.uri, PROJECT_ROOT);
177
- if (!result) {
178
- throw new Error(`Unknown resource: ${request.params.uri}`);
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) });
179
242
  }
180
- response = {
181
- contents: [result],
182
- };
243
+ writeDiagnosticEvent("mcp_list_resources_failed", { latencyMs: latency, error });
244
+ throw error;
183
245
  }
184
- const latency = Date.now() - startedAt;
185
- const bytes = payloadByteLength(response);
186
- resourceLogger.info(`read resource uri=${request.params.uri} bytes=${bytes} latencyMs=${latency}`);
187
- if (resourceLogger.isDebugEnabled()) {
188
- resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
189
- resourceLogger.debug("read resource response", { response: formatPayloadForDebug(response) });
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;
190
284
  }
191
- writeDiagnosticEvent("mcp_read_resource_ok", {
192
- uri: request.params.uri,
193
- latencyMs: latency,
194
- });
195
- return response;
196
- }
197
- catch (error) {
198
- const latency = Date.now() - startedAt;
199
- resourceLogger.error(`read resource uri=${request.params.uri} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
200
- if (resourceLogger.isDebugEnabled()) {
201
- resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
202
- resourceLogger.debug("read resource error", { error: formatErrorMessage(error) });
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;
203
298
  }
204
- writeDiagnosticEvent("mcp_read_resource_failed", {
205
- uri: request.params.uri,
206
- latencyMs: latency,
207
- error,
208
- });
209
- throw error;
210
- }
211
- });
212
- server.setRequestHandler(ListToolsRequestSchema, async () => {
213
- const startedAt = Date.now();
214
- writeDiagnosticEvent("mcp_list_tools_start");
215
- try {
216
- const response = {
217
- tools: toolRegistry.list().map((tool) => ({
218
- name: tool.name,
219
- title: tool.metadata.summary,
220
- description: tool.description,
221
- inputSchema: tool.inputSchema,
222
- _meta: tool.metadata,
223
- })),
224
- };
225
- const latency = Date.now() - startedAt;
226
- const bytes = payloadByteLength(response);
227
- toolLogger.info(`list tools count=${response.tools.length} bytes=${bytes} latencyMs=${latency}`);
228
- if (toolLogger.isDebugEnabled()) {
229
- toolLogger.debug("list tools request", { request: {} });
230
- toolLogger.debug("list tools response", { response: formatPayloadForDebug(response) });
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;
231
326
  }
232
- writeDiagnosticEvent("mcp_list_tools_ok", {
233
- count: response.tools.length,
234
- latencyMs: latency,
235
- });
236
- return response;
237
- }
238
- catch (error) {
239
- const latency = Date.now() - startedAt;
240
- toolLogger.error(`list tools failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
241
- if (toolLogger.isDebugEnabled()) {
242
- toolLogger.debug("list tools request", { request: {} });
243
- 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;
244
336
  }
245
- writeDiagnosticEvent("mcp_list_tools_failed", { latencyMs: latency, error });
246
- throw error;
247
- }
248
- });
249
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
250
- const startedAt = Date.now();
251
- writeDiagnosticEvent("mcp_list_prompts_start");
252
- try {
253
- const entries = promptRegistry.list();
254
- const response = {
255
- prompts: entries.map((entry) => ({
256
- name: entry.descriptor.name,
257
- title: entry.descriptor.title,
258
- description: entry.descriptor.description,
259
- arguments: entry.arguments?.map((argument) => ({
260
- name: argument.name,
261
- description: argument.description,
262
- required: argument.required,
263
- options: argument.options,
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
+ },
264
363
  })),
265
- _meta: {
266
- requiredResources: entry.descriptor.requiredResources,
267
- optionalResources: entry.descriptor.optionalResources ?? [],
268
- tools: entry.descriptor.tools,
269
- tags: entry.descriptor.tags ?? [],
270
- argumentOptions: Object.fromEntries((entry.arguments ?? [])
271
- .filter((argument) => Array.isArray(argument.options) && argument.options.length > 0)
272
- .map((argument) => [argument.name, argument.options])),
273
- },
274
- })),
275
- };
276
- const latency = Date.now() - startedAt;
277
- const bytes = payloadByteLength(response);
278
- promptLogger.info(`list prompts count=${response.prompts.length} bytes=${bytes} latencyMs=${latency}`);
279
- if (promptLogger.isDebugEnabled()) {
280
- promptLogger.debug("list prompts request", { request: {} });
281
- 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;
282
377
  }
283
- writeDiagnosticEvent("mcp_list_prompts_ok", {
284
- count: response.prompts.length,
285
- latencyMs: latency,
286
- });
287
- return response;
288
- }
289
- catch (error) {
290
- const latency = Date.now() - startedAt;
291
- promptLogger.error(`list prompts failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
292
- if (promptLogger.isDebugEnabled()) {
293
- promptLogger.debug("list prompts request", { request: {} });
294
- 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;
295
387
  }
296
- writeDiagnosticEvent("mcp_list_prompts_failed", { latencyMs: latency, error });
297
- throw error;
298
- }
299
- });
300
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
301
- const { name } = request.params;
302
- const args = request.params.arguments ?? {};
303
- const startedAt = Date.now();
304
- writeDiagnosticEvent("mcp_call_tool_start", {
305
- name,
306
- arguments: formatPayloadForDebug(args),
307
388
  });
308
- if (toolLogger.isDebugEnabled()) {
309
- toolLogger.debug("tool request", {
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", {
310
394
  name,
311
395
  arguments: formatPayloadForDebug(args),
312
396
  });
313
- }
314
- try {
315
- const result = await toolRegistry.invoke(name, args, {
316
- client,
317
- rag,
318
- logger: toolLogger,
319
- platform: getPlatformStatus(),
320
- setPlatform,
321
- });
322
- const response = toCallToolResult(result);
323
- const latency = Date.now() - startedAt;
324
- const bytes = payloadByteLength(response);
325
- const status = result.isError ? "error" : "ok";
326
- toolLogger.info(`call tool name=${name} status=${status} bytes=${bytes} latencyMs=${latency}`);
327
397
  if (toolLogger.isDebugEnabled()) {
328
- toolLogger.debug("tool response", {
398
+ toolLogger.debug("tool request", {
329
399
  name,
330
- response: formatPayloadForDebug(response),
400
+ arguments: formatPayloadForDebug(args),
331
401
  });
332
402
  }
333
- writeDiagnosticEvent("mcp_call_tool_ok", {
334
- name,
335
- latencyMs: latency,
336
- isError: result.isError === true,
337
- });
338
- return response;
339
- }
340
- catch (error) {
341
- const latency = Date.now() - startedAt;
342
- const fallback = unknownErrorResult(error);
343
- const response = toCallToolResult(fallback);
344
- const bytes = payloadByteLength(response);
345
- toolLogger.error(`call tool name=${name} status=failed bytes=${bytes} latencyMs=${latency} error=${formatErrorMessage(error)}`);
346
- if (toolLogger.isDebugEnabled()) {
347
- toolLogger.debug("tool response", {
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", {
348
423
  name,
349
- response: formatPayloadForDebug(response),
350
- error: formatErrorMessage(error),
424
+ latencyMs: latency,
425
+ isError: result.isError === true,
351
426
  });
427
+ return response;
352
428
  }
353
- writeDiagnosticEvent("mcp_call_tool_failed", {
354
- name,
355
- latencyMs: latency,
356
- error,
357
- });
358
- return response;
359
- }
360
- });
361
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
362
- const { name } = request.params;
363
- const args = request.params.arguments ?? {};
364
- const startedAt = Date.now();
365
- writeDiagnosticEvent("mcp_get_prompt_start", {
366
- name,
367
- arguments: formatPayloadForDebug(args),
368
- });
369
- if (promptLogger.isDebugEnabled()) {
370
- promptLogger.debug("prompt request", {
371
- name,
372
- arguments: formatPayloadForDebug(args),
373
- });
374
- }
375
- try {
376
- const resolved = promptRegistry.resolve(name, args);
377
- const response = {
378
- description: resolved.description,
379
- messages: resolved.messages.map(toPromptMessage),
380
- _meta: {
381
- arguments: resolved.arguments,
382
- resources: resolved.resources.map((resource) => ({
383
- uri: resource.uri,
384
- name: resource.name,
385
- description: resource.description,
386
- mimeType: resource.mimeType,
387
- metadata: resource.metadata,
388
- })),
389
- tools: resolved.tools,
390
- },
391
- };
392
- const latency = Date.now() - startedAt;
393
- const bytes = payloadByteLength(response);
394
- promptLogger.info(`get prompt name=${name} bytes=${bytes} latencyMs=${latency}`);
395
- if (promptLogger.isDebugEnabled()) {
396
- 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", {
397
443
  name,
398
- response: formatPayloadForDebug(response),
444
+ latencyMs: latency,
445
+ error,
399
446
  });
447
+ return response;
400
448
  }
401
- writeDiagnosticEvent("mcp_get_prompt_ok", {
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", {
402
455
  name,
403
- latencyMs: latency,
456
+ arguments: formatPayloadForDebug(args),
404
457
  });
405
- return response;
406
- }
407
- catch (error) {
408
- const latency = Date.now() - startedAt;
409
- promptLogger.error(`get prompt name=${name} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
410
458
  if (promptLogger.isDebugEnabled()) {
411
459
  promptLogger.debug("prompt request", {
412
460
  name,
413
461
  arguments: formatPayloadForDebug(args),
414
462
  });
415
- promptLogger.debug("prompt error", { name, error: formatErrorMessage(error) });
416
463
  }
417
- writeDiagnosticEvent("mcp_get_prompt_failed", {
418
- name,
419
- latencyMs: latency,
420
- error,
421
- });
422
- throw error;
423
- }
424
- });
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
+ };
425
519
  const cli = parseCliOptions(process.argv.slice(2));
426
520
  if (cli.mode === "http") {
427
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
428
- await server.connect(transport);
429
521
  const port = cli.port ?? 3000;
522
+ const httpToken = process.env.C64BRIDGE_HTTP_TOKEN || undefined;
523
+ if (httpToken) {
524
+ registerSecret(httpToken);
525
+ }
430
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
+ }
431
535
  if (request.url?.split("?")[0] !== "/mcp") {
432
536
  response.statusCode = 404;
433
537
  response.end("Not found");
434
538
  return;
435
539
  }
436
- void transport.handleRequest(request, response).catch((error) => {
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) => {
437
551
  if (!response.headersSent)
438
552
  response.statusCode = 500;
439
553
  response.end(error instanceof Error ? error.message : "MCP transport error");
440
554
  });
441
555
  });
442
556
  await new Promise((resolve, reject) => httpServer.listen(port, "127.0.0.1", resolve).once("error", reject));
557
+ installShutdown(client, "http");
443
558
  writeDiagnosticEvent("mcp_transport_connected", { mode: "http", port });
444
559
  console.error(`c64bridge MCP server running on HTTP at http://127.0.0.1:${port}/mcp`);
445
560
  }
446
561
  else {
562
+ const server = createMcpServer();
447
563
  const transport = new StdioServerTransport();
448
564
  await server.connect(transport);
565
+ installShutdown(client, "stdio");
449
566
  writeDiagnosticEvent("mcp_transport_connected", { mode: "stdio" });
450
567
  console.error("c64bridge MCP server running on stdio");
451
568
  }
@@ -488,6 +605,8 @@ export async function renderPlatformStatusMarkdown(client) {
488
605
  "",
489
606
  `Current platform: \`${status.id}\``,
490
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
+ "",
491
610
  "## Available Backends",
492
611
  "",
493
612
  ...availableBackends.map((backend) => `- \`${backend}\`${backend === status.id ? " (active)" : ""}`),
@@ -519,6 +638,11 @@ export async function renderPlatformStatusMarkdown(client) {
519
638
  .map((tool) => `\`${tool}\``)
520
639
  .join(", ")}`
521
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_");
522
646
  lines.push("");
523
647
  }
524
648
  lines.push("> Use `c64_select_backend` to switch to another available backend without restarting the MCP server.");
@@ -558,6 +682,7 @@ async function logConnectivity(client, baseUrl, networkPassword) {
558
682
  let response = null;
559
683
  try {
560
684
  // Send the network password like every other request, or a protected device answers 403.
685
+ registerSecret(networkPassword);
561
686
  const probeResponse = await axios.get(infoUrl, {
562
687
  timeout: 2000,
563
688
  ...(networkPassword ? { headers: { "X-Password": networkPassword } } : {}),
@@ -606,13 +731,13 @@ async function logConnectivity(client, baseUrl, networkPassword) {
606
731
  console.error(`Zero-page probe skipped or failed (may be unsupported on current backend): ${message}`);
607
732
  }
608
733
  }
609
- function toCallToolResult(result) {
610
- const base = { content: result.content };
734
+ export function toCallToolResult(result) {
735
+ const base = { content: redactValue(result.content) };
611
736
  if (result.structuredContent !== undefined) {
612
- base.structuredContent = result.structuredContent;
737
+ base.structuredContent = redactValue(result.structuredContent);
613
738
  }
614
739
  if (result.metadata !== undefined) {
615
- base.metadata = result.metadata;
740
+ base.metadata = redactValue(result.metadata);
616
741
  }
617
742
  if (result.isError !== undefined) {
618
743
  base.isError = result.isError;
@@ -643,7 +768,7 @@ if (process.env.C64BRIDGE_SKIP_AUTO_START !== "1") {
643
768
  diagnosticsFile: getDiagnosticsSessionInfo()?.filePath,
644
769
  error,
645
770
  });
646
- console.error("Fatal error in MCP server:", error);
771
+ console.error("Fatal error in MCP server:", redactValue(error));
647
772
  process.exit(1);
648
773
  });
649
774
  }