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.
Files changed (132) 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 +69 -14
  16. package/data/assembly/assembly-spec.md +2 -0
  17. package/dist/c64Client.js +238 -60
  18. package/dist/config.js +48 -26
  19. package/dist/device.js +92 -37
  20. package/dist/diagnostics.js +62 -2
  21. package/dist/hostPaths.js +128 -0
  22. package/dist/loggingHttpClient.js +59 -1
  23. package/dist/mcp-server.js +409 -286
  24. package/dist/numericArg.js +55 -0
  25. package/dist/petscii.js +13 -1
  26. package/dist/platform.js +12 -3
  27. package/dist/prompts/registry.js +6 -1
  28. package/dist/tools/access.js +66 -0
  29. package/dist/tools/debug.js +6 -0
  30. package/dist/tools/developer.js +67 -34
  31. package/dist/tools/errors.js +54 -3
  32. package/dist/tools/input.js +23 -6
  33. package/dist/tools/machineControl.js +112 -20
  34. package/dist/tools/memory.js +135 -30
  35. package/dist/tools/meta/artifacts.js +5 -5
  36. package/dist/tools/meta/background.js +216 -54
  37. package/dist/tools/meta/compilation.js +6 -5
  38. package/dist/tools/meta/config.js +15 -6
  39. package/dist/tools/meta/filesystem.js +16 -14
  40. package/dist/tools/meta/graphics.js +48 -20
  41. package/dist/tools/meta/memory.js +45 -19
  42. package/dist/tools/meta/program.js +10 -7
  43. package/dist/tools/meta/screen.js +116 -25
  44. package/dist/tools/pauseGuard.js +23 -0
  45. package/dist/tools/pollValidator.js +53 -8
  46. package/dist/tools/programRunners.js +62 -18
  47. package/dist/tools/registry/batch.js +16 -4
  48. package/dist/tools/registry/config.js +9 -2
  49. package/dist/tools/registry/disk.js +12 -2
  50. package/dist/tools/registry/drive.js +3 -0
  51. package/dist/tools/registry/extract.js +3 -0
  52. package/dist/tools/registry/graphics.js +59 -6
  53. package/dist/tools/registry/index.js +17 -1
  54. package/dist/tools/registry/memory.js +11 -1
  55. package/dist/tools/registry/platform.js +4 -1
  56. package/dist/tools/registry/printer.js +2 -1
  57. package/dist/tools/registry/program.js +5 -1
  58. package/dist/tools/registry/rag.js +1 -0
  59. package/dist/tools/registry/sound.js +12 -1
  60. package/dist/tools/registry/stream.js +2 -1
  61. package/dist/tools/registry/system.js +8 -1
  62. package/dist/tools/schema.js +7 -7
  63. package/dist/tools/streaming.js +34 -4
  64. package/dist/tools/translation/assembler.js +247 -24
  65. package/dist/tools/translation/basicDetokenizer.js +80 -0
  66. package/dist/tools/translation/basicTokenizer.js +8 -3
  67. package/dist/tools/translation/disassembler.js +14 -0
  68. package/dist/tools/types.js +258 -7
  69. package/dist/tools/vice.js +5 -16
  70. package/dist/vicBitmap.js +1 -1
  71. package/dist/vice/mockServer.js +3 -0
  72. package/dist/vice/process.js +19 -0
  73. package/dist/vice/resourcePolicy.js +36 -0
  74. package/dist/vice/viceClient.js +22 -5
  75. package/doc/vice/support-matrix.md +17 -11
  76. package/mcp/schemas/c64_batch.schema.json +2 -1
  77. package/mcp/schemas/c64_config.schema.json +25 -475
  78. package/mcp/schemas/c64_debug.schema.json +3 -602
  79. package/mcp/schemas/c64_disk.schema.json +17 -344
  80. package/mcp/schemas/c64_drive.schema.json +2 -157
  81. package/mcp/schemas/c64_extract.schema.json +10 -364
  82. package/mcp/schemas/c64_graphics.schema.json +19 -426
  83. package/mcp/schemas/c64_input.schema.json +5 -318
  84. package/mcp/schemas/c64_memory.schema.json +23 -491
  85. package/mcp/schemas/c64_printer.schema.json +3 -258
  86. package/mcp/schemas/c64_program.schema.json +16 -491
  87. package/mcp/schemas/c64_rag.schema.json +1 -81
  88. package/mcp/schemas/c64_select_backend.schema.json +2 -36
  89. package/mcp/schemas/c64_sound.schema.json +20 -822
  90. package/mcp/schemas/c64_stream.schema.json +4 -75
  91. package/mcp/schemas/c64_system.schema.json +28 -348
  92. package/mcp/schemas/c64_vice.schema.json +2 -71
  93. package/mcp/server.json +1 -1
  94. package/mcp/tools.json +1983 -6910
  95. package/mcp.json +21 -1
  96. package/package.json +16 -8
  97. package/scripts/mockC64Server.mjs +44 -0
  98. package/scripts/run-coverage.mjs +27 -1
  99. package/scripts/smoke-run.sh +3 -2
  100. package/scripts/start.mjs +23 -1
  101. package/server.json +35 -2
  102. package/dist/.tsbuildinfo +0 -1
  103. package/doc/agent-state-spec.md +0 -243
  104. package/doc/audit/secret-handling.md +0 -61
  105. package/doc/plans/bun/baseline-metrics.md +0 -16
  106. package/doc/plans/bun/migration-checklist.md +0 -55
  107. package/doc/plans/bun/migration.md +0 -83
  108. package/doc/plans/improve-knowledge/notes.md +0 -43
  109. package/doc/plans/improve-knowledge/plan.md +0 -121
  110. package/doc/plans/improve-knowledge/rollout.md +0 -110
  111. package/doc/plans/meta-tools/README.md +0 -110
  112. package/doc/plans/meta-tools/analysis.md +0 -272
  113. package/doc/plans/meta-tools/catalog.md +0 -435
  114. package/doc/plans/meta-tools/notes.md +0 -192
  115. package/doc/plans/meta-tools/rollout.md +0 -368
  116. package/doc/plans/prompts/README.md +0 -96
  117. package/doc/plans/prompts/user-prompt-ideas.md +0 -147
  118. package/doc/plans/tool-consolidation/notes.md +0 -28
  119. package/doc/plans/tool-consolidation/plan.md +0 -169
  120. package/doc/plans/tool-consolidation/rollout.md +0 -104
  121. package/doc/plans/vice-backend-switch/plan.md +0 -486
  122. package/doc/plans/vice-backend-switch-2/plan.md +0 -48
  123. package/doc/plans/vice-support/notes.md +0 -63
  124. package/doc/plans/vice-support/plan.md +0 -88
  125. package/doc/plans/vice-support/rollout.md +0 -99
  126. package/doc/reviews/audit.md +0 -143
  127. package/doc/reviews/hardening/01-fable/prompt-handover.md +0 -125
  128. package/doc/reviews/hardening/01-fable/prompt.md +0 -98
  129. package/doc/reviews/hardening/01-fable/review-prompt.md +0 -468
  130. package/doc/reviews/hardening/01-fable/review.md +0 -1234
  131. package/doc/reviews/vice-support-review.md +0 -185
  132. package/scripts/compare-rest/rest-compare-2025-11-01T00-25-21-830Z.json +0 -2513
@@ -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 { 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";
@@ -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
- const server = new Server(implementationInfo, {
114
- capabilities: {
115
- resources: {},
116
- tools: {},
117
- prompts: {},
118
- },
119
- });
120
- // RESOURCES: Expose C64 knowledge base
121
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
122
- const startedAt = Date.now();
123
- writeDiagnosticEvent("mcp_list_resources_start");
124
- try {
125
- const knowledgeResources = listKnowledgeResources().map((resource) => ({
126
- uri: resource.uri,
127
- name: resource.name,
128
- description: resource.description,
129
- mimeType: resource.mimeType,
130
- _meta: resource.metadata,
131
- }));
132
- const platformResource = createPlatformResourceDescriptor();
133
- const response = {
134
- resources: [...knowledgeResources, platformResource],
135
- };
136
- const latency = Date.now() - startedAt;
137
- const bytes = payloadByteLength(response);
138
- resourceLogger.info(`list resources count=${response.resources.length} bytes=${bytes} latencyMs=${latency}`);
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
- else {
177
- const result = readKnowledgeResource(request.params.uri, PROJECT_ROOT);
178
- if (!result) {
179
- 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) });
180
242
  }
181
- response = {
182
- contents: [result],
183
- };
243
+ writeDiagnosticEvent("mcp_list_resources_failed", { latencyMs: latency, error });
244
+ throw error;
184
245
  }
185
- const latency = Date.now() - startedAt;
186
- const bytes = payloadByteLength(response);
187
- resourceLogger.info(`read resource uri=${request.params.uri} bytes=${bytes} latencyMs=${latency}`);
188
- if (resourceLogger.isDebugEnabled()) {
189
- resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
190
- 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;
191
284
  }
192
- writeDiagnosticEvent("mcp_read_resource_ok", {
193
- uri: request.params.uri,
194
- latencyMs: latency,
195
- });
196
- return response;
197
- }
198
- catch (error) {
199
- const latency = Date.now() - startedAt;
200
- resourceLogger.error(`read resource uri=${request.params.uri} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
201
- if (resourceLogger.isDebugEnabled()) {
202
- resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
203
- 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;
204
298
  }
205
- writeDiagnosticEvent("mcp_read_resource_failed", {
206
- uri: request.params.uri,
207
- latencyMs: latency,
208
- error,
209
- });
210
- throw error;
211
- }
212
- });
213
- server.setRequestHandler(ListToolsRequestSchema, async () => {
214
- const startedAt = Date.now();
215
- writeDiagnosticEvent("mcp_list_tools_start");
216
- try {
217
- const response = {
218
- tools: toolRegistry.list().map((tool) => ({
219
- name: tool.name,
220
- title: tool.metadata.summary,
221
- description: tool.description,
222
- inputSchema: tool.inputSchema,
223
- _meta: tool.metadata,
224
- })),
225
- };
226
- const latency = Date.now() - startedAt;
227
- const bytes = payloadByteLength(response);
228
- toolLogger.info(`list tools count=${response.tools.length} bytes=${bytes} latencyMs=${latency}`);
229
- if (toolLogger.isDebugEnabled()) {
230
- toolLogger.debug("list tools request", { request: {} });
231
- 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;
232
326
  }
233
- writeDiagnosticEvent("mcp_list_tools_ok", {
234
- count: response.tools.length,
235
- latencyMs: latency,
236
- });
237
- return response;
238
- }
239
- catch (error) {
240
- const latency = Date.now() - startedAt;
241
- toolLogger.error(`list tools failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
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
- writeDiagnosticEvent("mcp_list_tools_failed", { latencyMs: latency, error });
247
- throw error;
248
- }
249
- });
250
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
251
- const startedAt = Date.now();
252
- writeDiagnosticEvent("mcp_list_prompts_start");
253
- try {
254
- const entries = promptRegistry.list();
255
- const response = {
256
- prompts: entries.map((entry) => ({
257
- name: entry.descriptor.name,
258
- title: entry.descriptor.title,
259
- description: entry.descriptor.description,
260
- arguments: entry.arguments?.map((argument) => ({
261
- name: argument.name,
262
- description: argument.description,
263
- required: argument.required,
264
- 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
+ },
265
363
  })),
266
- _meta: {
267
- requiredResources: entry.descriptor.requiredResources,
268
- optionalResources: entry.descriptor.optionalResources ?? [],
269
- tools: entry.descriptor.tools,
270
- tags: entry.descriptor.tags ?? [],
271
- argumentOptions: Object.fromEntries((entry.arguments ?? [])
272
- .filter((argument) => Array.isArray(argument.options) && argument.options.length > 0)
273
- .map((argument) => [argument.name, argument.options])),
274
- },
275
- })),
276
- };
277
- const latency = Date.now() - startedAt;
278
- const bytes = payloadByteLength(response);
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
- writeDiagnosticEvent("mcp_list_prompts_ok", {
285
- count: response.prompts.length,
286
- latencyMs: latency,
287
- });
288
- return response;
289
- }
290
- catch (error) {
291
- const latency = Date.now() - startedAt;
292
- promptLogger.error(`list prompts failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
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
- if (toolLogger.isDebugEnabled()) {
310
- 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", {
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 response", {
398
+ toolLogger.debug("tool request", {
330
399
  name,
331
- response: formatPayloadForDebug(response),
400
+ arguments: formatPayloadForDebug(args),
332
401
  });
333
402
  }
334
- writeDiagnosticEvent("mcp_call_tool_ok", {
335
- name,
336
- latencyMs: latency,
337
- isError: result.isError === true,
338
- });
339
- return response;
340
- }
341
- catch (error) {
342
- const latency = Date.now() - startedAt;
343
- const fallback = unknownErrorResult(error);
344
- const response = toCallToolResult(fallback);
345
- const bytes = payloadByteLength(response);
346
- toolLogger.error(`call tool name=${name} status=failed bytes=${bytes} latencyMs=${latency} error=${formatErrorMessage(error)}`);
347
- if (toolLogger.isDebugEnabled()) {
348
- 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", {
349
423
  name,
350
- response: formatPayloadForDebug(response),
351
- error: formatErrorMessage(error),
424
+ latencyMs: latency,
425
+ isError: result.isError === true,
352
426
  });
427
+ return response;
353
428
  }
354
- writeDiagnosticEvent("mcp_call_tool_failed", {
355
- name,
356
- latencyMs: latency,
357
- error,
358
- });
359
- return response;
360
- }
361
- });
362
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
363
- const { name } = request.params;
364
- const args = request.params.arguments ?? {};
365
- const startedAt = Date.now();
366
- writeDiagnosticEvent("mcp_get_prompt_start", {
367
- name,
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
- response: formatPayloadForDebug(response),
444
+ latencyMs: latency,
445
+ error,
400
446
  });
447
+ return response;
401
448
  }
402
- 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", {
403
455
  name,
404
- latencyMs: latency,
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
- writeDiagnosticEvent("mcp_get_prompt_failed", {
419
- name,
420
- latencyMs: latency,
421
- error,
422
- });
423
- throw error;
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
- 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) => {
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.");