c64bridge 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +151 -37
- package/dist/config.js +18 -10
- package/dist/device.js +123 -87
- package/dist/knowledge.js +1 -1
- package/dist/loggingHttpClient.js +15 -3
- package/dist/mcp-server.js +56 -15
- package/dist/rag/indexer.js +11 -8
- package/dist/rag/init.js +11 -8
- package/dist/sidwaveCompiler.js +2 -2
- package/dist/tools/audio.js +26 -5
- package/dist/tools/input.js +9 -14
- package/dist/tools/memory.js +20 -5
- package/dist/tools/meta/artifacts.js +20 -8
- package/dist/tools/meta/background.js +11 -1
- package/dist/tools/pollValidator.js +19 -51
- package/dist/tools/printer.js +1 -0
- package/dist/tools/registry/platform.js +2 -0
- package/dist/tools/registry/printer.js +1 -0
- package/dist/vice/mockServer.js +33 -0
- package/dist/vice/process.js +11 -1
- package/dist/vice/viceClient.js +35 -6
- package/doc/reviews/hardening/01-fable/prompt-handover.md +125 -0
- package/doc/reviews/hardening/01-fable/prompt.md +98 -0
- package/doc/reviews/hardening/01-fable/review-prompt.md +468 -0
- package/doc/reviews/hardening/01-fable/review.md +1234 -0
- package/mcp/schemas/c64_input.schema.json +5 -5
- package/mcp/server.json +1 -1
- package/mcp/tools.json +7 -6
- package/mcp.json +1 -1
- package/package.json +5 -5
- package/scripts/mockC64Server.mjs +131 -2
- package/server.json +2 -2
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"port": {
|
|
59
59
|
"type": "integer",
|
|
60
|
-
"description": "Joystick port (1
|
|
60
|
+
"description": "Joystick port (1 or 2).",
|
|
61
61
|
"minimum": 1,
|
|
62
62
|
"maximum": 2
|
|
63
63
|
},
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
"required": [
|
|
179
179
|
"op"
|
|
180
180
|
],
|
|
181
|
-
"description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input
|
|
181
|
+
"description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
|
|
182
182
|
"additionalProperties": false,
|
|
183
183
|
"x-c64bridge-operations": [
|
|
184
184
|
{
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
"op": "joystick",
|
|
277
|
-
"description": "Simulate joystick input. On C64U/U64 this uses machine:input
|
|
277
|
+
"description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
|
|
278
278
|
"required": [
|
|
279
279
|
"port",
|
|
280
280
|
"controls",
|
|
@@ -285,14 +285,14 @@
|
|
|
285
285
|
],
|
|
286
286
|
"inputSchema": {
|
|
287
287
|
"type": "object",
|
|
288
|
-
"description": "Simulate joystick input. On C64U/U64 this uses machine:input
|
|
288
|
+
"description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
|
|
289
289
|
"properties": {
|
|
290
290
|
"op": {
|
|
291
291
|
"const": "joystick"
|
|
292
292
|
},
|
|
293
293
|
"port": {
|
|
294
294
|
"type": "integer",
|
|
295
|
-
"description": "Joystick port (1
|
|
295
|
+
"description": "Joystick port (1 or 2).",
|
|
296
296
|
"minimum": 1,
|
|
297
297
|
"maximum": 2
|
|
298
298
|
},
|
package/mcp/server.json
CHANGED
package/mcp/tools.json
CHANGED
|
@@ -5668,7 +5668,8 @@
|
|
|
5668
5668
|
"When defining characters, remind the user to send the BASIC program returned in the payload."
|
|
5669
5669
|
],
|
|
5670
5670
|
"platforms": [
|
|
5671
|
-
"c64u"
|
|
5671
|
+
"c64u",
|
|
5672
|
+
"u2"
|
|
5672
5673
|
]
|
|
5673
5674
|
}
|
|
5674
5675
|
},
|
|
@@ -8255,7 +8256,7 @@
|
|
|
8255
8256
|
},
|
|
8256
8257
|
"port": {
|
|
8257
8258
|
"type": "integer",
|
|
8258
|
-
"description": "Joystick port (1
|
|
8259
|
+
"description": "Joystick port (1 or 2).",
|
|
8259
8260
|
"minimum": 1,
|
|
8260
8261
|
"maximum": 2
|
|
8261
8262
|
},
|
|
@@ -8376,7 +8377,7 @@
|
|
|
8376
8377
|
"required": [
|
|
8377
8378
|
"op"
|
|
8378
8379
|
],
|
|
8379
|
-
"description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input
|
|
8380
|
+
"description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
|
|
8380
8381
|
"additionalProperties": false,
|
|
8381
8382
|
"x-c64bridge-operations": [
|
|
8382
8383
|
{
|
|
@@ -8472,7 +8473,7 @@
|
|
|
8472
8473
|
},
|
|
8473
8474
|
{
|
|
8474
8475
|
"op": "joystick",
|
|
8475
|
-
"description": "Simulate joystick input. On C64U/U64 this uses machine:input
|
|
8476
|
+
"description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
|
|
8476
8477
|
"required": [
|
|
8477
8478
|
"port",
|
|
8478
8479
|
"controls",
|
|
@@ -8483,14 +8484,14 @@
|
|
|
8483
8484
|
],
|
|
8484
8485
|
"inputSchema": {
|
|
8485
8486
|
"type": "object",
|
|
8486
|
-
"description": "Simulate joystick input. On C64U/U64 this uses machine:input
|
|
8487
|
+
"description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
|
|
8487
8488
|
"properties": {
|
|
8488
8489
|
"op": {
|
|
8489
8490
|
"const": "joystick"
|
|
8490
8491
|
},
|
|
8491
8492
|
"port": {
|
|
8492
8493
|
"type": "integer",
|
|
8493
|
-
"description": "Joystick port (1
|
|
8494
|
+
"description": "Joystick port (1 or 2).",
|
|
8494
8495
|
"minimum": 1,
|
|
8495
8496
|
"maximum": 2
|
|
8496
8497
|
},
|
package/mcp.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"mcpName": "io.github.chrisgleissner/c64bridge",
|
|
5
5
|
"description": "C64 Bridge MCP server for Commodore 64 Ultimate hardware and VICE emulator control",
|
|
6
6
|
"repository": {
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
"node": ">=24"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
72
|
-
"axios": "^1.
|
|
71
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
72
|
+
"axios": "^1.19.0",
|
|
73
73
|
"date-fns": "^4.4.0",
|
|
74
|
-
"fastify": "^5.
|
|
74
|
+
"fastify": "^5.11.2",
|
|
75
75
|
"jimp": "^1.6.1",
|
|
76
76
|
"meyda": "^5.6.3",
|
|
77
77
|
"octokit": "^5.0.5",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@types/node": "^25.9.5",
|
|
87
|
-
"tsx": "^4.23.
|
|
87
|
+
"tsx": "^4.23.11",
|
|
88
88
|
"typescript": "^6.0.3"
|
|
89
89
|
},
|
|
90
90
|
"files": [
|
|
@@ -83,12 +83,18 @@ function sendJson(res, payload = {}, statusCode = 200) {
|
|
|
83
83
|
function createInitialState() {
|
|
84
84
|
return {
|
|
85
85
|
networkPassword: null,
|
|
86
|
+
forcedErrors: null,
|
|
87
|
+
forcedErrorsUrlMatch: null,
|
|
88
|
+
hangOnMachineInputProbe: false,
|
|
86
89
|
lastPrg: null,
|
|
90
|
+
lastLoadedPrg: null,
|
|
91
|
+
loadCount: 0,
|
|
87
92
|
runCount: 0,
|
|
88
93
|
resets: 0,
|
|
89
94
|
reboots: 0,
|
|
90
95
|
poweroffs: 0,
|
|
91
96
|
memory: new Uint8Array(0x10000),
|
|
97
|
+
heartbeat: null,
|
|
92
98
|
lastWrite: null,
|
|
93
99
|
writeLog: [],
|
|
94
100
|
lastRequest: null,
|
|
@@ -156,6 +162,82 @@ function toScreenCode(char) {
|
|
|
156
162
|
return SCREEN_CODE_LOOKUP.get(char) ?? SCREEN_CODE_LOOKUP.get(" ") ?? 0x20;
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
const KERNAL_KEYBOARD_NDX_ADDRESS = 0x00c6;
|
|
166
|
+
const KERNAL_KEYBOARD_BUFFER_ADDRESS = 0x0277;
|
|
167
|
+
|
|
168
|
+
const SYS_COMMAND_PATTERN = /SYS\s*\d+/i;
|
|
169
|
+
const HEARTBEAT_ADDRESS = 0x0400; // Screen-RAM byte the ASM liveness poller watches.
|
|
170
|
+
const HEARTBEAT_GRANULARITY_MS = 10; // Well below the poll interval so consecutive liveness reads always differ.
|
|
171
|
+
const HEARTBEAT_DURATION_MS = 1000; // How long a SYS-triggered program keeps "running".
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Nothing in this mock runs 6502 code to drain the KERNAL keyboard buffer
|
|
175
|
+
* itself, so a client polling NDX ($00C6) waiting for it to return to zero
|
|
176
|
+
* would otherwise hang forever. Simulate the KERNAL's IRQ-driven drain with
|
|
177
|
+
* a short delay so injectKeyboardQueue's poll loop behaves realistically.
|
|
178
|
+
*
|
|
179
|
+
* When the queued text looks like a typed `SYSnnnnn` command (the trigger
|
|
180
|
+
* upload_run_asm uses to enter an assembled program), also record a
|
|
181
|
+
* time-computed "alive" window (consumed by readMemoryWithHeartbeat). This
|
|
182
|
+
* mock never executes 6502 code, so a liveness poller watching screen RAM
|
|
183
|
+
* would otherwise see no progression. Recording a descriptor rather than
|
|
184
|
+
* mutating memory from a background timer is deliberate: nothing advances
|
|
185
|
+
* asynchronously, so an unrelated later test that merely shares this server
|
|
186
|
+
* instance can never observe a stray screen-RAM mutation.
|
|
187
|
+
*/
|
|
188
|
+
function simulateKeyboardDrain(state, address) {
|
|
189
|
+
if (address !== KERNAL_KEYBOARD_NDX_ADDRESS) return;
|
|
190
|
+
const pending = state.memory[KERNAL_KEYBOARD_NDX_ADDRESS] ?? 0;
|
|
191
|
+
const queued = pending > 0
|
|
192
|
+
? Uint8Array.from(state.memory.subarray(KERNAL_KEYBOARD_BUFFER_ADDRESS, KERNAL_KEYBOARD_BUFFER_ADDRESS + pending))
|
|
193
|
+
: null;
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
state.memory[KERNAL_KEYBOARD_NDX_ADDRESS] = 0;
|
|
196
|
+
if (!queued) return;
|
|
197
|
+
const text = Buffer.from(queued).toString("ascii");
|
|
198
|
+
if (!SYS_COMMAND_PATTERN.test(text)) return;
|
|
199
|
+
state.heartbeat = {
|
|
200
|
+
address: HEARTBEAT_ADDRESS,
|
|
201
|
+
startedAt: Date.now(),
|
|
202
|
+
durationMs: HEARTBEAT_DURATION_MS,
|
|
203
|
+
baseValue: state.memory[HEARTBEAT_ADDRESS] ?? 0,
|
|
204
|
+
};
|
|
205
|
+
}, 5);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Serve a screen-RAM read, overlaying a synthetic, monotonically advancing
|
|
210
|
+
* byte at the heartbeat address while a SYS-triggered "alive" window is open.
|
|
211
|
+
* The overlay is a pure function of elapsed time applied only to the returned
|
|
212
|
+
* copy, so it never mutates stored memory and never advances on its own.
|
|
213
|
+
*/
|
|
214
|
+
function readMemoryWithHeartbeat(state, address, length) {
|
|
215
|
+
const bytes = state.memory.slice(address, address + length);
|
|
216
|
+
const heartbeat = state.heartbeat;
|
|
217
|
+
if (!heartbeat) return bytes;
|
|
218
|
+
const elapsed = Date.now() - heartbeat.startedAt;
|
|
219
|
+
if (elapsed < 0 || elapsed >= heartbeat.durationMs) return bytes;
|
|
220
|
+
const offset = heartbeat.address - address;
|
|
221
|
+
if (offset < 0 || offset >= bytes.length) return bytes;
|
|
222
|
+
const ticks = Math.floor(elapsed / HEARTBEAT_GRANULARITY_MS);
|
|
223
|
+
bytes[offset] = (heartbeat.baseValue + ticks) & 0xff;
|
|
224
|
+
return bytes;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Any explicit write covering the heartbeat address means a client has taken
|
|
229
|
+
* over screen RAM, so end the simulated "alive" window and let the real stored
|
|
230
|
+
* bytes show through again. This keeps a lingering heartbeat from corrupting a
|
|
231
|
+
* later writeMemory/readMemory round-trip in a shared-server test.
|
|
232
|
+
*/
|
|
233
|
+
function cancelHeartbeatOnWrite(state, address, length) {
|
|
234
|
+
const heartbeat = state.heartbeat;
|
|
235
|
+
if (!heartbeat) return;
|
|
236
|
+
if (address <= heartbeat.address && heartbeat.address < address + length) {
|
|
237
|
+
state.heartbeat = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
159
241
|
function seedReadyPrompt(state) {
|
|
160
242
|
const text = "READY.";
|
|
161
243
|
const buffer = new Uint8Array(text.length + 1);
|
|
@@ -275,6 +357,20 @@ export async function startMockC64Server(options = {}) {
|
|
|
275
357
|
return;
|
|
276
358
|
}
|
|
277
359
|
|
|
360
|
+
// Test hook: simulate real firmware soft-failures, which answer HTTP 200
|
|
361
|
+
// with a non-empty `errors` array rather than a non-2xx status. Consumed
|
|
362
|
+
// once so a single forced failure does not leak into later requests.
|
|
363
|
+
if (state.forcedErrors && (!state.forcedErrorsUrlMatch || url.includes(state.forcedErrorsUrlMatch))) {
|
|
364
|
+
const errors = state.forcedErrors;
|
|
365
|
+
state.forcedErrors = null;
|
|
366
|
+
state.forcedErrorsUrlMatch = null;
|
|
367
|
+
// Drain any request body so an unread upload cannot corrupt a reused
|
|
368
|
+
// keep-alive connection's framing.
|
|
369
|
+
for await (const _chunk of req) { /* discard */ }
|
|
370
|
+
sendJson(res, { result: "error", errors });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
278
374
|
if (method === "GET" && (url === "/" || url.startsWith("/?"))) {
|
|
279
375
|
sendJson(res, { status: "ok", host: "mock" });
|
|
280
376
|
return;
|
|
@@ -297,7 +393,10 @@ export async function startMockC64Server(options = {}) {
|
|
|
297
393
|
enabled: driveState.power !== "off",
|
|
298
394
|
power: driveState.power,
|
|
299
395
|
mode: driveState.mode,
|
|
300
|
-
|
|
396
|
+
// Mirror the real firmware's DriveInfo shape (image_path is a
|
|
397
|
+
// plain string), not the internal mount-params object.
|
|
398
|
+
image_path: driveState.mountedImage?.image ?? null,
|
|
399
|
+
type: driveState.mountedImage?.type ?? driveState.mode ?? null,
|
|
301
400
|
};
|
|
302
401
|
}
|
|
303
402
|
sendJson(res, { drives });
|
|
@@ -340,6 +439,11 @@ export async function startMockC64Server(options = {}) {
|
|
|
340
439
|
}
|
|
341
440
|
|
|
342
441
|
if (method === "GET" && url === "/v1/machine:input") {
|
|
442
|
+
if (state.hangOnMachineInputProbe) {
|
|
443
|
+
// Test hook: never respond, to prove a caller cannot be blocked on
|
|
444
|
+
// this probe (HARD01-031). Deliberately does not call res.end().
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
343
447
|
sendJson(res, state.inputState);
|
|
344
448
|
return;
|
|
345
449
|
}
|
|
@@ -398,6 +502,27 @@ export async function startMockC64Server(options = {}) {
|
|
|
398
502
|
return;
|
|
399
503
|
}
|
|
400
504
|
|
|
505
|
+
if (method === "POST" && url === "/v1/runners:load_prg") {
|
|
506
|
+
const chunks = [];
|
|
507
|
+
for await (const chunk of req) {
|
|
508
|
+
chunks.push(chunk);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const prg = Buffer.concat(chunks);
|
|
512
|
+
state.lastLoadedPrg = prg;
|
|
513
|
+
state.loadCount += 1;
|
|
514
|
+
|
|
515
|
+
// Mirror real firmware: place the PRG body at its embedded load
|
|
516
|
+
// address without starting it (no RUN, no BASIC pointer changes).
|
|
517
|
+
if (prg.length >= 2) {
|
|
518
|
+
const loadAddress = prg.readUInt16LE(0);
|
|
519
|
+
state.memory.set(prg.subarray(2), loadAddress);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
sendJson(res, { result: "ok", bytes: prg.length });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
401
526
|
if (url.startsWith("/v1/runners:sidplay")) {
|
|
402
527
|
let routeUrl;
|
|
403
528
|
try {
|
|
@@ -471,7 +596,7 @@ export async function startMockC64Server(options = {}) {
|
|
|
471
596
|
const lengthValue = routeUrl.searchParams.get("length") ?? "256";
|
|
472
597
|
const address = parseNumeric(addressValue);
|
|
473
598
|
const length = Math.max(0, parseNumeric(lengthValue, 10));
|
|
474
|
-
const bytes = state
|
|
599
|
+
const bytes = readMemoryWithHeartbeat(state, address, length);
|
|
475
600
|
|
|
476
601
|
const accept = String(req.headers["accept"] || "");
|
|
477
602
|
if (accept.includes("application/octet-stream")) {
|
|
@@ -506,6 +631,8 @@ export async function startMockC64Server(options = {}) {
|
|
|
506
631
|
state.memory.set(bytes, address);
|
|
507
632
|
state.lastWrite = { address, bytes };
|
|
508
633
|
state.writeLog.push({ address, bytes: Buffer.from(bytes) });
|
|
634
|
+
cancelHeartbeatOnWrite(state, address, bytes.length);
|
|
635
|
+
simulateKeyboardDrain(state, address);
|
|
509
636
|
|
|
510
637
|
sendJson(res, { result: "wrote", address, length: bytes.length });
|
|
511
638
|
return;
|
|
@@ -525,6 +652,8 @@ export async function startMockC64Server(options = {}) {
|
|
|
525
652
|
state.memory.set(bytes, address);
|
|
526
653
|
state.lastWrite = { address, bytes };
|
|
527
654
|
state.writeLog.push({ address, bytes: Buffer.from(bytes) });
|
|
655
|
+
cancelHeartbeatOnWrite(state, address, bytes.length);
|
|
656
|
+
simulateKeyboardDrain(state, address);
|
|
528
657
|
|
|
529
658
|
sendJson(res, { result: "wrote", address, length: bytes.length });
|
|
530
659
|
return;
|
package/server.json
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
10
|
"websiteUrl": "https://github.com/chrisgleissner/c64bridge",
|
|
11
|
-
"version": "1.0.
|
|
11
|
+
"version": "1.0.2",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "c64bridge",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.2",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|