c64bridge 0.5.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 (138) hide show
  1. package/.github/prompts/README.md +6 -0
  2. package/.github/prompts/assembly-routine-session.prompt.md +13 -0
  3. package/.github/prompts/basic-program-session.prompt.md +13 -0
  4. package/.github/prompts/drive-management-session.prompt.md +13 -0
  5. package/.github/prompts/petscii-art-session.prompt.md +13 -0
  6. package/.github/prompts/play-classic-game.prompt.md +13 -0
  7. package/.github/prompts/sid-music-session.prompt.md +13 -0
  8. package/AGENTS.md +173 -0
  9. package/LICENSE +339 -0
  10. package/README.md +582 -0
  11. package/data/README.md +16 -0
  12. package/data/api/basic-api-spec.md +154 -0
  13. package/data/api/kernal-api-spec.md +68 -0
  14. package/data/assembly/assembly-spec.md +196 -0
  15. package/data/assembly/examples/effects/sinewave.asm +5 -0
  16. package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
  17. package/data/assembly/examples/sid-file-structure.md +8 -0
  18. package/data/assembly/examples/sid-overview.md +11 -0
  19. package/data/audio/examples/Biosphere.sid +0 -0
  20. package/data/audio/sid-file-structure.md +91 -0
  21. package/data/audio/sid-programming-best-practices.md +198 -0
  22. package/data/audio/sid-spec.md +166 -0
  23. package/data/audio/sidwave.md +131 -0
  24. package/data/audio/sidwave.schema.json +64 -0
  25. package/data/basic/basic-spec.md +230 -0
  26. package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
  27. package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
  28. package/data/basic/examples/io/joystick.bas +5 -0
  29. package/data/basic/examples/video/bounce.bas +8 -0
  30. package/data/basic/examples/video/entchen-petscii.bas +16 -0
  31. package/data/basic/examples/video/hello-world.bas +4 -0
  32. package/data/basic/examples/video/wave.bas +7 -0
  33. package/data/context/bootstrap.md +148 -0
  34. package/data/context/chat.md +36 -0
  35. package/data/drive/drive-spec.md +215 -0
  36. package/data/embeddings_asm.json +1 -0
  37. package/data/embeddings_basic.json +1 -0
  38. package/data/embeddings_hardware.json +1 -0
  39. package/data/embeddings_mixed.json +1 -0
  40. package/data/embeddings_other.json +1 -0
  41. package/data/io/cia-spec.md +115 -0
  42. package/data/io/io-spec.md +124 -0
  43. package/data/memory/kernal-memory-map.md +227 -0
  44. package/data/memory/low-memory-map.md +133 -0
  45. package/data/memory/memory-map.md +186 -0
  46. package/data/printer/printer-commodore-bitmap.md +31 -0
  47. package/data/printer/printer-commodore.md +28 -0
  48. package/data/printer/printer-epson-bitmap.md +29 -0
  49. package/data/printer/printer-epson.md +30 -0
  50. package/data/printer/printer-prompts.md +29 -0
  51. package/data/printer/printer-spec.md +180 -0
  52. package/data/video/character-set.csv +257 -0
  53. package/data/video/vic-spec.md +276 -0
  54. package/dist/assemblyConverter.js +1299 -0
  55. package/dist/audio/record_and_analyze_audio.js +292 -0
  56. package/dist/basicConverter.js +193 -0
  57. package/dist/bootstrap/stdio-logger.js +32 -0
  58. package/dist/c64Client.js +1052 -0
  59. package/dist/chargen.js +197 -0
  60. package/dist/config.js +142 -0
  61. package/dist/context.js +157 -0
  62. package/dist/device.js +413 -0
  63. package/dist/index.js +7 -0
  64. package/dist/knowledge.js +454 -0
  65. package/dist/logger.js +182 -0
  66. package/dist/loggingHttpClient.js +88 -0
  67. package/dist/mcp-server.js +426 -0
  68. package/dist/petscii.js +135 -0
  69. package/dist/petsciiArt.js +356 -0
  70. package/dist/platform.js +82 -0
  71. package/dist/prompts/registry.js +560 -0
  72. package/dist/rag/discover.config.json +41 -0
  73. package/dist/rag/discover.js +401 -0
  74. package/dist/rag/embeddings.js +60 -0
  75. package/dist/rag/externalFetcher.js +934 -0
  76. package/dist/rag/indexer.js +770 -0
  77. package/dist/rag/init.js +139 -0
  78. package/dist/rag/knowledgeIndex.js +361 -0
  79. package/dist/rag/loggingRetriever.js +46 -0
  80. package/dist/rag/rateLimiter.js +127 -0
  81. package/dist/rag/retriever.js +74 -0
  82. package/dist/rag/sources.csv +31 -0
  83. package/dist/rag/types.js +5 -0
  84. package/dist/rag/urlUtils.js +38 -0
  85. package/dist/sidplayRunner.js +240 -0
  86. package/dist/sidwave.js +101 -0
  87. package/dist/sidwaveCompiler.js +329 -0
  88. package/dist/tools/audio.js +944 -0
  89. package/dist/tools/developer.js +515 -0
  90. package/dist/tools/errors.js +59 -0
  91. package/dist/tools/graphics.js +469 -0
  92. package/dist/tools/machineControl.js +267 -0
  93. package/dist/tools/memory.js +224 -0
  94. package/dist/tools/printer.js +482 -0
  95. package/dist/tools/programRunners.js +315 -0
  96. package/dist/tools/rag.js +153 -0
  97. package/dist/tools/registry.js +58 -0
  98. package/dist/tools/responses.js +36 -0
  99. package/dist/tools/schema.js +304 -0
  100. package/dist/tools/storage.js +740 -0
  101. package/dist/tools/streaming.js +132 -0
  102. package/dist/tools/types.js +99 -0
  103. package/dist/viceRunner.js +239 -0
  104. package/doc/MCP_SETUP.md +153 -0
  105. package/doc/README.md +7 -0
  106. package/doc/developer.md +312 -0
  107. package/doc/img/logo.png +0 -0
  108. package/doc/img/prompts/duck_petscii.png +0 -0
  109. package/doc/img/prompts/duck_song.png +0 -0
  110. package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
  111. package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
  112. package/doc/prompts/README.md +96 -0
  113. package/doc/prompts/user-prompt-ideas.md +147 -0
  114. package/doc/rest/c64-openapi.yaml +979 -0
  115. package/doc/rest/c64-rest-api.md +106 -0
  116. package/doc/troubleshooting-mcp.md +177 -0
  117. package/generated/c64/index.js +857 -0
  118. package/mcp.json +106 -0
  119. package/package.json +79 -0
  120. package/scripts/c64-cli.mjs +142 -0
  121. package/scripts/c64-tool.sh +83 -0
  122. package/scripts/check-package.mjs +89 -0
  123. package/scripts/cli.js +61 -0
  124. package/scripts/fetch-external-sources.mjs +67 -0
  125. package/scripts/generate-changelog.mjs +131 -0
  126. package/scripts/postbuild.mjs +101 -0
  127. package/scripts/prepare-release.mjs +58 -0
  128. package/scripts/rag-rebuild.mjs +53 -0
  129. package/scripts/register-ts-node.mjs +6 -0
  130. package/scripts/run-mcp-check.sh +104 -0
  131. package/scripts/run-tests.mjs +195 -0
  132. package/scripts/sid2wav.sh +94 -0
  133. package/scripts/smoke-run.sh +65 -0
  134. package/scripts/start-mock-c64.mjs +51 -0
  135. package/scripts/start-mock.mjs +31 -0
  136. package/scripts/start.mjs +72 -0
  137. package/scripts/update-readme.ts +164 -0
  138. package/scripts/verify-package.mjs +147 -0
@@ -0,0 +1,88 @@
1
+ import { HttpClient } from "../generated/c64/index.js";
2
+ import { formatErrorMessage, formatPayloadForDebug, loggerFor, payloadByteLength } from "./logger.js";
3
+ export function createLoggingHttpClient(config, logger = loggerFor("c64u")) {
4
+ const http = new HttpClient(config);
5
+ http.instance.interceptors.request.use((request) => {
6
+ const method = (request.method ?? "get").toUpperCase();
7
+ const path = resolvePath(request);
8
+ const meta = {
9
+ startedAt: Date.now(),
10
+ method,
11
+ path,
12
+ headers: request.headers ? { ...request.headers } : undefined,
13
+ params: request.params ? { ...request.params } : undefined,
14
+ body: request.data,
15
+ };
16
+ request.__c64Meta = meta;
17
+ return request;
18
+ });
19
+ http.instance.interceptors.response.use((response) => {
20
+ handleResponse(response, logger);
21
+ return response;
22
+ }, (error) => {
23
+ handleError(error, logger);
24
+ return Promise.reject(error);
25
+ });
26
+ return http;
27
+ }
28
+ function handleResponse(response, logger) {
29
+ const meta = extractMeta(response.config);
30
+ const latency = meta ? Date.now() - meta.startedAt : 0;
31
+ const method = meta?.method ?? (response.config.method ?? "get").toUpperCase();
32
+ const path = meta?.path ?? resolvePath(response.config);
33
+ const bytes = payloadByteLength(response.data);
34
+ logger.info(`${method} ${path} status=${response.status} bytes=${bytes} latencyMs=${latency}`);
35
+ if (logger.isDebugEnabled()) {
36
+ logger.debug(`request ${method} ${path}`, {
37
+ headers: meta?.headers,
38
+ query: meta?.params,
39
+ body: formatPayloadForDebug(meta?.body),
40
+ });
41
+ logger.debug(`response ${method} ${path}`, {
42
+ status: response.status,
43
+ headers: response.headers ?? {},
44
+ body: formatPayloadForDebug(response.data),
45
+ });
46
+ }
47
+ }
48
+ function handleError(error, logger) {
49
+ const config = error.config;
50
+ if (!config) {
51
+ logger.error(`UNKNOWN UNKNOWN status=ERR bytes=0 latencyMs=0 error=${formatErrorMessage(error)}`);
52
+ return;
53
+ }
54
+ const meta = extractMeta(config);
55
+ const latency = meta ? Date.now() - meta.startedAt : 0;
56
+ const method = meta?.method ?? (config.method ?? "get").toUpperCase();
57
+ const path = meta?.path ?? resolvePath(config);
58
+ const response = error.response;
59
+ const status = response?.status ?? "ERR";
60
+ const bytes = response ? payloadByteLength(response.data) : 0;
61
+ const message = formatErrorMessage(error);
62
+ logger.warn(`${method} ${path} status=${status} bytes=${bytes} latencyMs=${latency} error=${message}`);
63
+ if (logger.isDebugEnabled()) {
64
+ logger.debug(`request ${method} ${path}`, {
65
+ headers: meta?.headers,
66
+ query: meta?.params,
67
+ body: formatPayloadForDebug(meta?.body),
68
+ });
69
+ logger.debug(`error ${method} ${path}`, {
70
+ status,
71
+ headers: response?.headers ?? {},
72
+ body: response ? formatPayloadForDebug(response.data) : null,
73
+ message,
74
+ });
75
+ }
76
+ }
77
+ function extractMeta(config) {
78
+ const meta = config.__c64Meta;
79
+ delete config.__c64Meta;
80
+ return meta;
81
+ }
82
+ function resolvePath(config) {
83
+ if (config.url)
84
+ return config.url;
85
+ if (config.baseURL)
86
+ return config.baseURL;
87
+ return "UNKNOWN";
88
+ }
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
7
+ import { loadConfig } from "./config.js";
8
+ import { C64Client } from "./c64Client.js";
9
+ import { listKnowledgeResources, readKnowledgeResource, } from "./rag/knowledgeIndex.js";
10
+ import { initRag } from "./rag/init.js";
11
+ import { toolRegistry } from "./tools/registry.js";
12
+ import { unknownErrorResult } from "./tools/errors.js";
13
+ import { createPromptRegistry } from "./prompts/registry.js";
14
+ import { describePlatformCapabilities, getPlatformStatus, setPlatform } from "./platform.js";
15
+ import axios from "axios";
16
+ import { loggerFor, payloadByteLength, formatPayloadForDebug, formatErrorMessage } from "./logger.js";
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const PROJECT_ROOT = join(__dirname, "..");
20
+ function parseCliOptions(argv) {
21
+ const httpIndex = argv.indexOf("--http");
22
+ if (httpIndex === -1) {
23
+ return { mode: "stdio" };
24
+ }
25
+ const portCandidate = argv[httpIndex + 1];
26
+ return { mode: "http", port: parsePort(portCandidate) };
27
+ }
28
+ function parsePort(raw) {
29
+ if (!raw || raw.startsWith("--")) {
30
+ return undefined;
31
+ }
32
+ const parsed = Number(raw);
33
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
34
+ return parsed;
35
+ }
36
+ return undefined;
37
+ }
38
+ async function main() {
39
+ console.error("Starting c64bridge MCP server...");
40
+ const config = loadConfig();
41
+ const baseUrl = config.baseUrl ?? `http://${config.c64_host}`;
42
+ // Initialize C64 client (reuse existing)
43
+ const client = new C64Client(baseUrl);
44
+ const rag = await initRag();
45
+ const toolLogger = loggerFor("tool");
46
+ const resourceLogger = loggerFor("resource");
47
+ const promptLogger = loggerFor("prompt");
48
+ const promptRegistry = createPromptRegistry();
49
+ // Create MCP server
50
+ const server = new Server({
51
+ name: "c64bridge",
52
+ version: "0.4.0",
53
+ }, {
54
+ capabilities: {
55
+ resources: {},
56
+ tools: {},
57
+ prompts: {},
58
+ },
59
+ });
60
+ // RESOURCES: Expose C64 knowledge base
61
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
62
+ const startedAt = Date.now();
63
+ try {
64
+ const knowledgeResources = listKnowledgeResources().map((resource) => ({
65
+ uri: resource.uri,
66
+ name: resource.name,
67
+ description: resource.description,
68
+ mimeType: resource.mimeType,
69
+ metadata: resource.metadata,
70
+ }));
71
+ const platformResource = createPlatformResourceDescriptor();
72
+ const response = {
73
+ resources: [...knowledgeResources, platformResource],
74
+ };
75
+ const latency = Date.now() - startedAt;
76
+ const bytes = payloadByteLength(response);
77
+ resourceLogger.info(`list resources count=${response.resources.length} bytes=${bytes} latencyMs=${latency}`);
78
+ if (resourceLogger.isDebugEnabled()) {
79
+ resourceLogger.debug("list resources request", { request: {} });
80
+ resourceLogger.debug("list resources response", { response: formatPayloadForDebug(response) });
81
+ }
82
+ return response;
83
+ }
84
+ catch (error) {
85
+ const latency = Date.now() - startedAt;
86
+ resourceLogger.error(`list resources failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
87
+ if (resourceLogger.isDebugEnabled()) {
88
+ resourceLogger.debug("list resources request", { request: {} });
89
+ resourceLogger.debug("list resources error", { error: formatErrorMessage(error) });
90
+ }
91
+ throw error;
92
+ }
93
+ });
94
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
95
+ const startedAt = Date.now();
96
+ try {
97
+ let response;
98
+ if (request.params.uri === PLATFORM_RESOURCE_URI) {
99
+ response = {
100
+ contents: [
101
+ {
102
+ uri: PLATFORM_RESOURCE_URI,
103
+ mimeType: "text/markdown",
104
+ text: renderPlatformStatusMarkdown(),
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ else {
110
+ const result = readKnowledgeResource(request.params.uri, PROJECT_ROOT);
111
+ if (!result) {
112
+ throw new Error(`Unknown resource: ${request.params.uri}`);
113
+ }
114
+ response = {
115
+ contents: [result],
116
+ };
117
+ }
118
+ const latency = Date.now() - startedAt;
119
+ const bytes = payloadByteLength(response);
120
+ resourceLogger.info(`read resource uri=${request.params.uri} bytes=${bytes} latencyMs=${latency}`);
121
+ if (resourceLogger.isDebugEnabled()) {
122
+ resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
123
+ resourceLogger.debug("read resource response", { response: formatPayloadForDebug(response) });
124
+ }
125
+ return response;
126
+ }
127
+ catch (error) {
128
+ const latency = Date.now() - startedAt;
129
+ resourceLogger.error(`read resource uri=${request.params.uri} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
130
+ if (resourceLogger.isDebugEnabled()) {
131
+ resourceLogger.debug("read resource request", { request: formatPayloadForDebug(request.params) });
132
+ resourceLogger.debug("read resource error", { error: formatErrorMessage(error) });
133
+ }
134
+ throw error;
135
+ }
136
+ });
137
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
138
+ const startedAt = Date.now();
139
+ try {
140
+ const response = {
141
+ tools: toolRegistry.list(),
142
+ };
143
+ const latency = Date.now() - startedAt;
144
+ const bytes = payloadByteLength(response);
145
+ toolLogger.info(`list tools count=${response.tools.length} bytes=${bytes} latencyMs=${latency}`);
146
+ if (toolLogger.isDebugEnabled()) {
147
+ toolLogger.debug("list tools request", { request: {} });
148
+ toolLogger.debug("list tools response", { response: formatPayloadForDebug(response) });
149
+ }
150
+ return response;
151
+ }
152
+ catch (error) {
153
+ const latency = Date.now() - startedAt;
154
+ toolLogger.error(`list tools failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
155
+ if (toolLogger.isDebugEnabled()) {
156
+ toolLogger.debug("list tools request", { request: {} });
157
+ toolLogger.debug("list tools error", { error: formatErrorMessage(error) });
158
+ }
159
+ throw error;
160
+ }
161
+ });
162
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
163
+ const startedAt = Date.now();
164
+ try {
165
+ const entries = promptRegistry.list();
166
+ const response = {
167
+ prompts: entries.map((entry) => ({
168
+ name: entry.descriptor.name,
169
+ title: entry.descriptor.title,
170
+ description: entry.descriptor.description,
171
+ arguments: entry.arguments?.map((argument) => ({
172
+ name: argument.name,
173
+ description: argument.description,
174
+ required: argument.required,
175
+ options: argument.options,
176
+ })),
177
+ _meta: {
178
+ requiredResources: entry.descriptor.requiredResources,
179
+ optionalResources: entry.descriptor.optionalResources ?? [],
180
+ tools: entry.descriptor.tools,
181
+ tags: entry.descriptor.tags ?? [],
182
+ },
183
+ })),
184
+ };
185
+ const latency = Date.now() - startedAt;
186
+ const bytes = payloadByteLength(response);
187
+ promptLogger.info(`list prompts count=${response.prompts.length} bytes=${bytes} latencyMs=${latency}`);
188
+ if (promptLogger.isDebugEnabled()) {
189
+ promptLogger.debug("list prompts request", { request: {} });
190
+ promptLogger.debug("list prompts response", { response: formatPayloadForDebug(response) });
191
+ }
192
+ return response;
193
+ }
194
+ catch (error) {
195
+ const latency = Date.now() - startedAt;
196
+ promptLogger.error(`list prompts failed bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
197
+ if (promptLogger.isDebugEnabled()) {
198
+ promptLogger.debug("list prompts request", { request: {} });
199
+ promptLogger.debug("list prompts error", { error: formatErrorMessage(error) });
200
+ }
201
+ throw error;
202
+ }
203
+ });
204
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
205
+ const { name } = request.params;
206
+ const args = request.params.arguments ?? {};
207
+ const startedAt = Date.now();
208
+ if (toolLogger.isDebugEnabled()) {
209
+ toolLogger.debug("tool request", {
210
+ name,
211
+ arguments: formatPayloadForDebug(args),
212
+ });
213
+ }
214
+ try {
215
+ const result = await toolRegistry.invoke(name, args, {
216
+ client,
217
+ rag,
218
+ logger: toolLogger,
219
+ platform: getPlatformStatus(),
220
+ setPlatform,
221
+ });
222
+ const response = toCallToolResult(result);
223
+ const latency = Date.now() - startedAt;
224
+ const bytes = payloadByteLength(response);
225
+ const status = result.isError ? "error" : "ok";
226
+ toolLogger.info(`call tool name=${name} status=${status} bytes=${bytes} latencyMs=${latency}`);
227
+ if (toolLogger.isDebugEnabled()) {
228
+ toolLogger.debug("tool response", {
229
+ name,
230
+ response: formatPayloadForDebug(response),
231
+ });
232
+ }
233
+ return response;
234
+ }
235
+ catch (error) {
236
+ const latency = Date.now() - startedAt;
237
+ const fallback = unknownErrorResult(error);
238
+ const response = toCallToolResult(fallback);
239
+ const bytes = payloadByteLength(response);
240
+ toolLogger.error(`call tool name=${name} status=failed bytes=${bytes} latencyMs=${latency} error=${formatErrorMessage(error)}`);
241
+ if (toolLogger.isDebugEnabled()) {
242
+ toolLogger.debug("tool response", {
243
+ name,
244
+ response: formatPayloadForDebug(response),
245
+ error: formatErrorMessage(error),
246
+ });
247
+ }
248
+ return response;
249
+ }
250
+ });
251
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
252
+ const { name } = request.params;
253
+ const args = request.params.arguments ?? {};
254
+ const startedAt = Date.now();
255
+ if (promptLogger.isDebugEnabled()) {
256
+ promptLogger.debug("prompt request", {
257
+ name,
258
+ arguments: formatPayloadForDebug(args),
259
+ });
260
+ }
261
+ try {
262
+ const resolved = promptRegistry.resolve(name, args);
263
+ const response = {
264
+ description: resolved.description,
265
+ messages: resolved.messages.map(toPromptMessage),
266
+ _meta: {
267
+ arguments: resolved.arguments,
268
+ resources: resolved.resources.map((resource) => ({
269
+ uri: resource.uri,
270
+ name: resource.name,
271
+ description: resource.description,
272
+ mimeType: resource.mimeType,
273
+ metadata: resource.metadata,
274
+ })),
275
+ tools: resolved.tools,
276
+ },
277
+ };
278
+ const latency = Date.now() - startedAt;
279
+ const bytes = payloadByteLength(response);
280
+ promptLogger.info(`get prompt name=${name} bytes=${bytes} latencyMs=${latency}`);
281
+ if (promptLogger.isDebugEnabled()) {
282
+ promptLogger.debug("prompt response", {
283
+ name,
284
+ response: formatPayloadForDebug(response),
285
+ });
286
+ }
287
+ return response;
288
+ }
289
+ catch (error) {
290
+ const latency = Date.now() - startedAt;
291
+ promptLogger.error(`get prompt name=${name} bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
292
+ if (promptLogger.isDebugEnabled()) {
293
+ promptLogger.debug("prompt request", {
294
+ name,
295
+ arguments: formatPayloadForDebug(args),
296
+ });
297
+ promptLogger.debug("prompt error", { name, error: formatErrorMessage(error) });
298
+ }
299
+ throw error;
300
+ }
301
+ });
302
+ // Connect via stdio
303
+ const transport = new StdioServerTransport();
304
+ await server.connect(transport);
305
+ await logConnectivity(client, baseUrl);
306
+ console.error("c64bridge MCP server running on stdio");
307
+ }
308
+ const PLATFORM_RESOURCE_URI = "c64://platform/status";
309
+ function createPlatformResourceDescriptor() {
310
+ return {
311
+ uri: PLATFORM_RESOURCE_URI,
312
+ name: "Active Platform Status",
313
+ description: "Reports the active MCP platform and tool compatibility snapshot.",
314
+ mimeType: "text/markdown",
315
+ metadata: {
316
+ domain: "platform",
317
+ priority: "critical",
318
+ summary: "Current platform (c64u or vice), feature flags, and tool support overview.",
319
+ prompts: [],
320
+ tools: [],
321
+ tags: ["platform", "compatibility"],
322
+ relatedResources: [],
323
+ },
324
+ };
325
+ }
326
+ function renderPlatformStatusMarkdown() {
327
+ const status = getPlatformStatus();
328
+ const capabilities = describePlatformCapabilities(toolRegistry.list());
329
+ const lines = [
330
+ "# MCP Platform Status",
331
+ "",
332
+ `Current platform: \`${status.id}\``,
333
+ "",
334
+ status.features.length > 0
335
+ ? ["## Active Features", "", ...status.features.map((feature) => `- ${feature}`)].join("\n")
336
+ : "",
337
+ status.limitedFeatures.length > 0
338
+ ? ["## Limited or Unavailable Features", "", ...status.limitedFeatures.map((feature) => `- ${feature}`)].join("\n")
339
+ : "",
340
+ "## Tool Compatibility",
341
+ "",
342
+ ].filter(Boolean);
343
+ for (const [platformId, info] of Object.entries(capabilities.platforms)) {
344
+ lines.push(`### ${platformId.toUpperCase()}`);
345
+ lines.push("");
346
+ lines.push(info.tools.length > 0
347
+ ? `- Supported tools (${info.tools.length}): ${info.tools.map((tool) => `\`${tool}\``).join(", ")}`
348
+ : "- Supported tools: _none_");
349
+ lines.push(info.unsupported_tools.length > 0
350
+ ? `- Unsupported tools (${info.unsupported_tools.length}): ${info.unsupported_tools
351
+ .map((tool) => `\`${tool}\``)
352
+ .join(", ")}`
353
+ : "- Unsupported tools: _none_");
354
+ lines.push("");
355
+ }
356
+ lines.push("> Switching platforms currently requires restarting the MCP server with an updated configuration.");
357
+ return lines.join("\n");
358
+ }
359
+ async function logConnectivity(client, baseUrl) {
360
+ const c64Logger = loggerFor("c64u");
361
+ const startedAt = Date.now();
362
+ let response = null;
363
+ try {
364
+ const probeResponse = await axios.get(baseUrl, { timeout: 2000 });
365
+ response = probeResponse;
366
+ const latency = Date.now() - startedAt;
367
+ const bytes = payloadByteLength(probeResponse.data);
368
+ c64Logger.info(`GET ${baseUrl} status=${probeResponse.status} bytes=${bytes} latencyMs=${latency}`);
369
+ }
370
+ catch (error) {
371
+ const latency = Date.now() - startedAt;
372
+ if (axios.isAxiosError(error)) {
373
+ const status = error.response?.status ?? "ERR";
374
+ const bytes = error.response ? payloadByteLength(error.response.data) : 0;
375
+ c64Logger.warn(`GET ${baseUrl} status=${status} bytes=${bytes} latencyMs=${latency} error=${formatErrorMessage(error)}`);
376
+ }
377
+ else {
378
+ c64Logger.error(`GET ${baseUrl} status=ERR bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
379
+ }
380
+ console.log(`Skipping direct REST connectivity probe (no hardware REST base reachable at ${baseUrl})`);
381
+ return;
382
+ }
383
+ if (!response) {
384
+ console.log(`Skipping direct REST connectivity probe (no hardware REST base reachable at ${baseUrl})`);
385
+ return;
386
+ }
387
+ console.log(`Connectivity check succeeded for c64 device at ${baseUrl}`);
388
+ try {
389
+ const memoryAddress = "$0000";
390
+ const memoryResult = await client.readMemory(memoryAddress, "1");
391
+ if (memoryResult.success && memoryResult.data) {
392
+ console.log(`Zero-page probe @ ${memoryAddress}: ${memoryResult.data}`);
393
+ }
394
+ else if (memoryResult.details) {
395
+ console.warn(`Zero-page probe failed: ${JSON.stringify(memoryResult.details)}`);
396
+ }
397
+ }
398
+ catch (memoryError) {
399
+ const message = memoryError instanceof Error ? memoryError.message : String(memoryError);
400
+ console.warn(`Zero-page probe skipped or failed (may be unsupported on current backend): ${message}`);
401
+ }
402
+ }
403
+ function toCallToolResult(result) {
404
+ const base = { content: result.content };
405
+ if (result.structuredContent !== undefined) {
406
+ base.structuredContent = result.structuredContent;
407
+ }
408
+ if (result.metadata !== undefined) {
409
+ base.metadata = result.metadata;
410
+ }
411
+ return base;
412
+ }
413
+ function toPromptMessage(segment) {
414
+ const role = segment.role === "user" ? "user" : "assistant";
415
+ return {
416
+ role,
417
+ content: {
418
+ type: "text",
419
+ text: segment.content,
420
+ },
421
+ };
422
+ }
423
+ main().catch((error) => {
424
+ console.error("Fatal error in MCP server:", error);
425
+ process.exit(1);
426
+ });
@@ -0,0 +1,135 @@
1
+ /*
2
+ C64 Bridge - An MCP Server for the Commodore 64 Ultimate
3
+ Copyright (C) 2025 Christian Gleissner
4
+
5
+ Licensed under the GNU General Public License v2.0 or later.
6
+ See <https://www.gnu.org/licenses/> for details.
7
+ */
8
+ const PETSCII_TO_ASCII = {
9
+ 0x00: " ",
10
+ 0x07: "\u0007",
11
+ 0x0d: "\n",
12
+ 0x11: "\n",
13
+ 0x8d: "\n",
14
+ 0xa0: " ",
15
+ };
16
+ function petsciiByteToChar(byte) {
17
+ if (PETSCII_TO_ASCII[byte]) {
18
+ return PETSCII_TO_ASCII[byte];
19
+ }
20
+ if (byte >= 0x20 && byte <= 0x7e) {
21
+ return String.fromCharCode(byte);
22
+ }
23
+ if (byte >= 0xc1 && byte <= 0xda) {
24
+ return String.fromCharCode(byte - 0x80);
25
+ }
26
+ if (byte >= 0x41 && byte <= 0x5a) {
27
+ return String.fromCharCode(byte);
28
+ }
29
+ return " ";
30
+ }
31
+ export function petsciiToAscii(bytes) {
32
+ return Array.from(bytes)
33
+ .map(petsciiByteToChar)
34
+ .join("");
35
+ }
36
+ /**
37
+ * Minimal ASCII→PETSCII mapping used by the encoder.
38
+ * For now we pass through printable ASCII and a handful of control codes.
39
+ */
40
+ export function asciiToPetscii(char) {
41
+ if (char.length !== 1) {
42
+ throw new Error("Expected a single character");
43
+ }
44
+ const code = char.charCodeAt(0);
45
+ if (code < 0 || code > 255) {
46
+ throw new Error(`Character out of PETSCII range: ${char}`);
47
+ }
48
+ return code;
49
+ }
50
+ /** Named PETSCII and control codes that are commonly used on the C64. */
51
+ const NAMED_CHARS = Object.freeze({
52
+ // Control characters (BASIC CHR$ values)
53
+ "clear": 147, // CLR/HOME (clears screen)
54
+ "clr": 147,
55
+ "home": 19,
56
+ "bell": 7,
57
+ "beep": 7,
58
+ "newline": 13,
59
+ "return": 13,
60
+ "cr": 13,
61
+ "lf": 10,
62
+ "reverse_on": 18,
63
+ "rvon": 18,
64
+ "reverse_off": 146,
65
+ "rvoff": 146,
66
+ "cursor_up": 145,
67
+ "cursor_down": 17,
68
+ "cursor_right": 29,
69
+ "cursor_left": 157,
70
+ // Graphic glyphs (commonly referenced by name)
71
+ // Note: Exact byte shown on screen depends on mode; these are PETSCII/CHR$ values
72
+ "heart": 81, // widely used PETSCII code for heart glyph
73
+ "up_arrow": 94,
74
+ "uparrow": 94,
75
+ "left_arrow": 95,
76
+ "leftarrow": 95,
77
+ "pi": 222,
78
+ });
79
+ export function listNamedPetscii() {
80
+ return Object.entries(NAMED_CHARS)
81
+ .sort((a, b) => a[0].localeCompare(b[0]))
82
+ .map(([name, code]) => ({ name, code, hex: `$${code.toString(16).toUpperCase().padStart(2, "0")}` }));
83
+ }
84
+ export function lookupNamedPetscii(name) {
85
+ const key = name.trim().toLowerCase().replace(/\s+/g, "_");
86
+ return NAMED_CHARS[key];
87
+ }
88
+ /**
89
+ * Try to parse a named character token starting at the given index.
90
+ * Supported forms: {name}, :name:, <name>
91
+ * Returns the PETSCII code and index of the first character AFTER the token
92
+ * when matched, otherwise null.
93
+ */
94
+ export function parseNamedCharToken(source, startIndex) {
95
+ const open = source[startIndex];
96
+ if (open !== "{" && open !== ":" && open !== "<") {
97
+ return null;
98
+ }
99
+ const close = open === "{" ? "}" : open === ":" ? ":" : ">";
100
+ let idx = startIndex + 1;
101
+ let name = "";
102
+ while (idx < source.length && source[idx] !== close) {
103
+ name += source[idx]
104
+ .replace(/[^A-Za-z0-9_\s-]/g, "") // restrict to sane name characters
105
+ .trim();
106
+ idx += 1;
107
+ }
108
+ if (idx >= source.length || source[idx] !== close) {
109
+ return null;
110
+ }
111
+ const value = lookupNamedPetscii(name.replace(/[-\s]+/g, "_"));
112
+ if (value === undefined) {
113
+ return null;
114
+ }
115
+ return { value, endIndex: idx + 1 };
116
+ }
117
+ /**
118
+ * Encode a text containing optional named tokens into PETSCII bytes.
119
+ * Named tokens may appear as {heart}, :heart:, or <heart>.
120
+ */
121
+ export function encodeStringWithNames(input) {
122
+ const bytes = [];
123
+ let i = 0;
124
+ while (i < input.length) {
125
+ const maybe = parseNamedCharToken(input, i);
126
+ if (maybe) {
127
+ bytes.push(maybe.value & 0xff);
128
+ i = maybe.endIndex;
129
+ continue;
130
+ }
131
+ bytes.push(asciiToPetscii(input[i]));
132
+ i += 1;
133
+ }
134
+ return Uint8Array.from(bytes);
135
+ }