@nataliapc/mcp-openmsx 1.2.9 → 1.2.10
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 +21 -0
- package/bin/win-x64/mcp-openmsx-sspi-proxy.exe +0 -0
- package/dist/openmsx.js +113 -248
- package/dist/openmsx_windows.js +316 -0
- package/package.json +15 -13
package/README.md
CHANGED
|
@@ -249,6 +249,27 @@ Edit it to include the following JSON entry:
|
|
|
249
249
|
| `MCP_TRANSPORT` | Transport mode (`stdio` or `http`) | `stdio` | `http` |
|
|
250
250
|
| `MCP_HTTP_PORT` | Port number for HTTP transport mode | `3000` | `8080` |
|
|
251
251
|
| `MCP_ALLOWED_ORIGINS` | Comma-separated list of allowed origins for HTTP transport | Empty for all allowed | `http://localhost,http://mydomain.com` |
|
|
252
|
+
| `OPENMSX_WINDOWS_CONTROL` | **Windows only.** How the server talks to openMSX's control socket (see below) | `stdio-proxy` | `direct-sspi` |
|
|
253
|
+
| `OPENMSX_WINDOWS_PROXY_EXECUTABLE` | **Windows only.** Override path to the SSPI proxy helper (development) | Bundled `bin/win-x64/mcp-openmsx-sspi-proxy.exe` | `C:\path\to\mcp-openmsx-sspi-proxy.exe` |
|
|
254
|
+
|
|
255
|
+
#### Windows control modes (`OPENMSX_WINDOWS_CONTROL`)
|
|
256
|
+
|
|
257
|
+
On Windows, openMSX is a GUI app whose TCP control socket requires SSPI (Negotiate/NTLM) authentication. The server supports several transports:
|
|
258
|
+
|
|
259
|
+
| Value | Description |
|
|
260
|
+
|-------|-------------|
|
|
261
|
+
| `stdio-proxy` | **Default.** Launches a self-contained .NET helper that performs SSPI and exposes a clean XML stdio channel — the most robust path. |
|
|
262
|
+
| `direct-sspi` | Authenticates from Node via the optional `node-expose-sspi` package. Fallback / debugging. |
|
|
263
|
+
| `socket` | Legacy alias of `direct-sspi`. |
|
|
264
|
+
|
|
265
|
+
Linux and macOS are unaffected (`openmsx -control stdio`).
|
|
266
|
+
|
|
267
|
+
The bundled proxy is built from `helpers/openmsx-sspi-proxy` and can be rebuilt reproducibly from Linux with Docker (no local .NET required):
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
cd mcp-server
|
|
271
|
+
pnpm build:proxy:win-x64:docker # → bin/win-x64/mcp-openmsx-sspi-proxy.exe
|
|
272
|
+
```
|
|
252
273
|
|
|
253
274
|
|
|
254
275
|
## Advanced Manual Usage
|
|
Binary file
|
package/dist/openmsx.js
CHANGED
|
@@ -5,21 +5,23 @@
|
|
|
5
5
|
* @license GPL2
|
|
6
6
|
*/
|
|
7
7
|
import fs from "fs/promises";
|
|
8
|
-
import fsSync from "fs";
|
|
9
8
|
import { extractDescriptionFromXML, decodeHtmlEntities, encodeHtmlEntities } from "./utils.js";
|
|
10
9
|
import { spawn } from 'child_process';
|
|
11
|
-
import net from 'net';
|
|
12
|
-
import os from 'os';
|
|
13
10
|
import path from 'path';
|
|
11
|
+
import { OpenMsxWindowsConnector } from "./openmsx_windows.js";
|
|
14
12
|
/** True when running on Windows. Evaluated once at module load. */
|
|
15
13
|
const IS_WINDOWS = process.platform === 'win32';
|
|
16
14
|
export class OpenMSX {
|
|
17
15
|
lastMachine = null;
|
|
18
16
|
process = null;
|
|
19
17
|
isConnected = false;
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// Active writable control channel: process.stdin (Linux/macOS) or the
|
|
19
|
+
// Windows connection's input (TCP socket / SSPI proxy stdin).
|
|
20
|
+
controlWritable = null;
|
|
21
|
+
// Windows-only control connection (TCP+SSPI or stdio proxy). Owns the
|
|
22
|
+
// transport and its teardown; null on Linux/macOS. See openmsx_windows.ts.
|
|
23
|
+
controlConnection = null;
|
|
24
|
+
// Accumulated I/O data for readData() — shared by all transports (never coexist)
|
|
23
25
|
ioBuffer = '';
|
|
24
26
|
// Notify callback: fired when new I/O data arrives — shared by both platforms
|
|
25
27
|
ioNotify = null;
|
|
@@ -51,6 +53,23 @@ export class OpenMSX {
|
|
|
51
53
|
}
|
|
52
54
|
this.resetIO();
|
|
53
55
|
this.commandQueue = Promise.resolve(''); // reset queue for new session
|
|
56
|
+
// Resolve the Windows control mode up front so an invalid value
|
|
57
|
+
// fails before we spawn openMSX (avoids orphaned GUI processes).
|
|
58
|
+
let windowsMode = null;
|
|
59
|
+
if (IS_WINDOWS) {
|
|
60
|
+
try {
|
|
61
|
+
windowsMode = OpenMsxWindowsConnector.getControlMode();
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
safeResolve(`Error: ${e instanceof Error ? e.message : e}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (windowsMode === 'pipe') {
|
|
68
|
+
safeResolve('Error: OPENMSX_WINDOWS_CONTROL=pipe is reserved but not implemented yet');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
diag(`Windows control mode: ${windowsMode}`);
|
|
72
|
+
}
|
|
54
73
|
// Build args
|
|
55
74
|
const args = [];
|
|
56
75
|
if (!IS_WINDOWS) {
|
|
@@ -169,85 +188,88 @@ export class OpenMSX {
|
|
|
169
188
|
});
|
|
170
189
|
}
|
|
171
190
|
/**
|
|
172
|
-
* Windows connection
|
|
173
|
-
*
|
|
191
|
+
* Windows connection (both modes). All transport — socket-file polling, TCP
|
|
192
|
+
* connect, SSPI, or proxy launch — is delegated to {@link OpenMsxWindowsConnector},
|
|
193
|
+
* which returns a uniform connection with SSPI already done. Here we only
|
|
194
|
+
* attach the generic XML stream handlers and open the session. Both Windows
|
|
195
|
+
* modes use the TCP semantics: we send `<openmsx-control>` and openMSX replies
|
|
196
|
+
* with `<openmsx-output>`.
|
|
174
197
|
*/
|
|
175
198
|
launchConnectWindows(ctx) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.
|
|
199
|
+
const connector = new OpenMsxWindowsConnector({
|
|
200
|
+
openmsxProcess: this.process,
|
|
201
|
+
diag: ctx.diag,
|
|
202
|
+
});
|
|
203
|
+
connector.connect().then((connection) => {
|
|
204
|
+
this.controlConnection = connection;
|
|
205
|
+
this.controlWritable = connection.input;
|
|
183
206
|
this.ioBuffer = '';
|
|
184
|
-
|
|
207
|
+
// Diagnostics from the SSPI proxy (stderr only — never stdout).
|
|
208
|
+
connection.errorOutput?.on('data', (data) => {
|
|
209
|
+
const msg = data.toString().trim();
|
|
210
|
+
if (msg && !this.isConnected)
|
|
211
|
+
ctx.diag(`control stderr: ${msg.substring(0, 300)}`);
|
|
212
|
+
});
|
|
213
|
+
// stdio-proxy: proxy exiting before we connect means SSPI/connect failed.
|
|
214
|
+
connection.controlProcess?.on('exit', (code, signal) => {
|
|
215
|
+
ctx.diag(`control process exit: code=${code} signal=${signal} isConnected=${this.isConnected}`);
|
|
216
|
+
if (!this.isConnected && !ctx.isResolved()) {
|
|
217
|
+
ctx.safeResolve(`Error: control process exited before connecting (code=${code}, signal=${signal}).`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// direct-sspi: TCP socket errors / close.
|
|
221
|
+
connection.tcpSocket?.on('error', e => {
|
|
185
222
|
ctx.diag(`TCP socket error: ${e.message}`);
|
|
186
223
|
if (!ctx.isResolved())
|
|
187
224
|
ctx.safeResolve(`Error: TCP socket error: ${e.message}`);
|
|
188
225
|
});
|
|
189
|
-
|
|
226
|
+
connection.tcpSocket?.on('close', () => {
|
|
190
227
|
ctx.diag('TCP socket closed');
|
|
191
|
-
this.tcpSocket = null;
|
|
192
228
|
this.isConnected = false;
|
|
193
229
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// - during SSPI: ioBuffer accumulates binary SSPI tokens
|
|
199
|
-
// (harmless, won't contain '<openmsx-output>')
|
|
200
|
-
// - after SSPI: ioBuffer is cleared, then <openmsx-control>
|
|
201
|
-
// is sent, and <openmsx-output> is detected normally.
|
|
202
|
-
socket.on('data', (data) => {
|
|
203
|
-
const chunk = data.toString();
|
|
204
|
-
// Accumulate all incoming data
|
|
205
|
-
this.ioBuffer += chunk;
|
|
206
|
-
// Signal readData() that new data arrived (notify pattern)
|
|
207
|
-
if (this.ioNotify) {
|
|
208
|
-
const notify = this.ioNotify;
|
|
209
|
-
this.ioNotify = null;
|
|
210
|
-
notify();
|
|
211
|
-
}
|
|
212
|
-
// During launch: detect <openmsx-output> in the accumulated stream
|
|
213
|
-
if (!this.isConnected && this.ioBuffer.includes('<openmsx-output>')) {
|
|
214
|
-
this.isConnected = true;
|
|
215
|
-
// Discard everything up to and including <openmsx-output>
|
|
216
|
-
this.ioBuffer = this.ioBuffer.substring(this.ioBuffer.indexOf('<openmsx-output>') + '<openmsx-output>'.length);
|
|
217
|
-
setTimeout(async () => {
|
|
218
|
-
if (!ctx.isResolved()) {
|
|
219
|
-
try {
|
|
220
|
-
await ctx.onReady(false);
|
|
221
|
-
} // Windows: don't resend <openmsx-control>
|
|
222
|
-
catch (e) {
|
|
223
|
-
ctx.safeResolve(`Error: Failed to send control commands - ${e instanceof Error ? e.message : e}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}, 300);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
// SSPI authentication (adds its own parallel 'data' listener internally)
|
|
230
|
-
try {
|
|
231
|
-
ctx.diag('starting SSPI authentication...');
|
|
232
|
-
await this.performSspiAuth(socket);
|
|
233
|
-
ctx.diag('SSPI authentication successful');
|
|
234
|
-
}
|
|
235
|
-
catch (e) {
|
|
236
|
-
ctx.safeResolve(`Error: SSPI authentication failed: ${e instanceof Error ? e.message : e}. ` +
|
|
237
|
-
`Make sure 'node-expose-sspi' is installed: npm install node-expose-sspi`);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
// Clear SSPI binary garbage from ioBuffer before XML session
|
|
241
|
-
this.ioBuffer = '';
|
|
242
|
-
// Send <openmsx-control> to initiate the XML session.
|
|
243
|
-
// On Windows/TCP, openMSX sends <openmsx-output> in response.
|
|
230
|
+
connection.output.on('error', (e) => ctx.diag(`control stream error: ${e.message}`));
|
|
231
|
+
this.attachOutputHandler(connection.output, ctx);
|
|
232
|
+
// Open the XML session: we send <openmsx-control>; openMSX replies with
|
|
233
|
+
// <openmsx-output>. (In stdio-proxy the proxy forwards it after SSPI.)
|
|
244
234
|
ctx.diag('sending <openmsx-control> to start XML session');
|
|
245
|
-
|
|
246
|
-
}).catch(err => {
|
|
247
|
-
ctx.diag(`
|
|
235
|
+
connection.input.write('<openmsx-control>\n');
|
|
236
|
+
}).catch((err) => {
|
|
237
|
+
ctx.diag(`windows connect failed: ${err.message}`);
|
|
248
238
|
ctx.safeResolve(`Error: ${err.message}`);
|
|
249
239
|
});
|
|
250
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Shared output handler for both Windows modes (proxy stdout or TCP socket):
|
|
243
|
+
* accumulate into `ioBuffer`, wake any pending `readData()`, and on the first
|
|
244
|
+
* `<openmsx-output>` mark connected and trigger the initial command sequence
|
|
245
|
+
* with `onReady(false)` (we already sent `<openmsx-control>`). Linux/macOS uses
|
|
246
|
+
* its own variant in {@link launchConnectLinux} (openMSX emits the tag unprompted).
|
|
247
|
+
*/
|
|
248
|
+
attachOutputHandler(output, ctx) {
|
|
249
|
+
output.on('data', (data) => {
|
|
250
|
+
const chunk = data.toString();
|
|
251
|
+
this.ioBuffer += chunk;
|
|
252
|
+
if (this.ioNotify) {
|
|
253
|
+
const notify = this.ioNotify;
|
|
254
|
+
this.ioNotify = null;
|
|
255
|
+
notify();
|
|
256
|
+
}
|
|
257
|
+
if (!this.isConnected && this.ioBuffer.includes('<openmsx-output>')) {
|
|
258
|
+
this.isConnected = true;
|
|
259
|
+
this.ioBuffer = this.ioBuffer.substring(this.ioBuffer.indexOf('<openmsx-output>') + '<openmsx-output>'.length);
|
|
260
|
+
setTimeout(async () => {
|
|
261
|
+
if (!ctx.isResolved()) {
|
|
262
|
+
try {
|
|
263
|
+
await ctx.onReady(false);
|
|
264
|
+
} // <openmsx-control> already sent
|
|
265
|
+
catch (e) {
|
|
266
|
+
ctx.safeResolve(`Error: Failed to send control commands - ${e instanceof Error ? e.message : e}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}, 300);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
251
273
|
/**
|
|
252
274
|
* Linux/macOS connection: stdio pipes.
|
|
253
275
|
* Registers stdout handler for ioBuffer accumulation and <openmsx-output> detection.
|
|
@@ -255,6 +277,7 @@ export class OpenMSX {
|
|
|
255
277
|
launchConnectLinux(ctx) {
|
|
256
278
|
const FATAL_ERROR_GRACE_PERIOD = 500;
|
|
257
279
|
let connectionTime = null;
|
|
280
|
+
this.controlWritable = this.process.stdin;
|
|
258
281
|
this.process.stdout.on('data', (data) => {
|
|
259
282
|
const output = data.toString();
|
|
260
283
|
if (!this.isConnected) {
|
|
@@ -294,164 +317,17 @@ export class OpenMSX {
|
|
|
294
317
|
}
|
|
295
318
|
});
|
|
296
319
|
}
|
|
297
|
-
/**
|
|
298
|
-
* SSPI (Negotiate/NTLM) authentication handshake — Windows only.
|
|
299
|
-
* Required since openMSX 0.7.1 for TCP socket connections.
|
|
300
|
-
* Uses `node-expose-sspi` v0.1.x optional npm package.
|
|
301
|
-
*
|
|
302
|
-
* Reference C++ implementation: openMSX debugger SspiNegotiateClient.cpp
|
|
303
|
-
* Protocol: loop until SEC_E_OK — each round:
|
|
304
|
-
* client → [4-byte BE length][SSPI token]
|
|
305
|
-
* server → [4-byte BE length][SSPI response] (if SEC_I_CONTINUE_NEEDED)
|
|
306
|
-
* After SEC_E_OK: no server read — proceed directly with XML protocol.
|
|
307
|
-
*/
|
|
308
|
-
async performSspiAuth(socket) {
|
|
309
|
-
let nes;
|
|
310
|
-
try {
|
|
311
|
-
const { createRequire } = await import('module');
|
|
312
|
-
const req = createRequire(import.meta.url);
|
|
313
|
-
nes = req('node-expose-sspi');
|
|
314
|
-
}
|
|
315
|
-
catch (e) {
|
|
316
|
-
throw new Error(`node-expose-sspi not available (${e instanceof Error ? e.message : e}). ` +
|
|
317
|
-
`Install with: npm install node-expose-sspi`);
|
|
318
|
-
}
|
|
319
|
-
// Accumulate TCP data for length-prefixed reads during SSPI phase.
|
|
320
|
-
// Pattern: onSspiData appends to buffer and NOTIFIES (does not clear).
|
|
321
|
-
// readLengthPrefixed checks the buffer size after each notification.
|
|
322
|
-
let sspiBuffer = Buffer.alloc(0);
|
|
323
|
-
let sspiNotify = null;
|
|
324
|
-
const onSspiData = (chunk) => {
|
|
325
|
-
sspiBuffer = Buffer.concat([sspiBuffer, chunk]);
|
|
326
|
-
if (sspiNotify) {
|
|
327
|
-
const notify = sspiNotify;
|
|
328
|
-
sspiNotify = null;
|
|
329
|
-
notify(); // just signal — buffer stays intact
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
socket.on('data', onSspiData);
|
|
333
|
-
// Wait until new data arrives (doesn't clear the buffer)
|
|
334
|
-
const waitMore = () => new Promise(resolve => {
|
|
335
|
-
sspiNotify = resolve;
|
|
336
|
-
});
|
|
337
|
-
const readLengthPrefixed = async () => {
|
|
338
|
-
// Wait until we have at least the 4-byte length prefix
|
|
339
|
-
while (sspiBuffer.length < 4)
|
|
340
|
-
await waitMore();
|
|
341
|
-
const len = sspiBuffer.readUInt32BE(0);
|
|
342
|
-
const total = 4 + len;
|
|
343
|
-
// Wait until the full payload has arrived
|
|
344
|
-
while (sspiBuffer.length < total)
|
|
345
|
-
await waitMore();
|
|
346
|
-
const result = sspiBuffer.slice(4, total);
|
|
347
|
-
sspiBuffer = sspiBuffer.slice(total); // consume only what we read
|
|
348
|
-
return result;
|
|
349
|
-
};
|
|
350
|
-
const sendToken = (token) => {
|
|
351
|
-
const buf = Buffer.from(token);
|
|
352
|
-
const lenBuf = Buffer.alloc(4);
|
|
353
|
-
lenBuf.writeUInt32BE(buf.length, 0);
|
|
354
|
-
socket.write(lenBuf);
|
|
355
|
-
socket.write(buf);
|
|
356
|
-
};
|
|
357
|
-
try {
|
|
358
|
-
// node-expose-sspi v0.1.x API:
|
|
359
|
-
// AcquireCredentialsHandle → CredentialWithExpiry { credential, tsExpiry }
|
|
360
|
-
// InitializeSecurityContextInput.credential = CredHandle (the .credential property)
|
|
361
|
-
// InitializeSecurityContextInput.SecBufferDesc = server's token (NOT serverSecurityContext)
|
|
362
|
-
// InitializeSecurityContextInput.contextReq = string[] of ISC_REQ_* flags
|
|
363
|
-
// Flags from openMSX C++ ref: ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONNECTION | ISC_REQ_STREAM
|
|
364
|
-
const credWithExpiry = nes.sspi.AcquireCredentialsHandle({
|
|
365
|
-
packageName: 'Negotiate',
|
|
366
|
-
credentialUse: 'SECPKG_CRED_OUTBOUND',
|
|
367
|
-
});
|
|
368
|
-
const credential = credWithExpiry.credential;
|
|
369
|
-
const packageInfo = nes.sspi.QuerySecurityPackageInfo('Negotiate');
|
|
370
|
-
const ISC_FLAGS = ['ISC_REQ_ALLOCATE_MEMORY', 'ISC_REQ_CONNECTION', 'ISC_REQ_STREAM'];
|
|
371
|
-
let contextHandle = undefined;
|
|
372
|
-
let serverSecBufDesc = undefined;
|
|
373
|
-
// Loop until SEC_E_OK (mirrors C++ reference implementation)
|
|
374
|
-
while (true) {
|
|
375
|
-
const ctxInput = {
|
|
376
|
-
credential,
|
|
377
|
-
targetName: '',
|
|
378
|
-
cbMaxToken: packageInfo.cbMaxToken,
|
|
379
|
-
contextReq: ISC_FLAGS,
|
|
380
|
-
targetDataRep: 'SECURITY_NETWORK_DREP',
|
|
381
|
-
};
|
|
382
|
-
if (contextHandle !== undefined)
|
|
383
|
-
ctxInput.contextHandle = contextHandle;
|
|
384
|
-
if (serverSecBufDesc !== undefined)
|
|
385
|
-
ctxInput.SecBufferDesc = serverSecBufDesc;
|
|
386
|
-
const clientCtx = nes.sspi.InitializeSecurityContext(ctxInput);
|
|
387
|
-
contextHandle = clientCtx.contextHandle;
|
|
388
|
-
// Send our token to the server (if non-empty)
|
|
389
|
-
const tokenBuf = clientCtx.SecBufferDesc?.buffers?.[0];
|
|
390
|
-
if (tokenBuf && tokenBuf.byteLength > 0) {
|
|
391
|
-
sendToken(tokenBuf);
|
|
392
|
-
}
|
|
393
|
-
if (clientCtx.SECURITY_STATUS === 'SEC_E_OK') {
|
|
394
|
-
// Auth complete — no final read from server, proceed to XML
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
if (clientCtx.SECURITY_STATUS !== 'SEC_I_CONTINUE_NEEDED') {
|
|
398
|
-
throw new Error(`SSPI error: ${clientCtx.SECURITY_STATUS}`);
|
|
399
|
-
}
|
|
400
|
-
// Read server's response token
|
|
401
|
-
const response = await readLengthPrefixed();
|
|
402
|
-
const responseAB = response.buffer.slice(response.byteOffset, response.byteOffset + response.byteLength);
|
|
403
|
-
serverSecBufDesc = { ulVersion: 0, buffers: [responseAB] };
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
finally {
|
|
407
|
-
// Remove SSPI data handler — main handler will be added after this returns
|
|
408
|
-
socket.removeListener('data', onSspiData);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Poll for the TCP socket file, read the port, connect.
|
|
413
|
-
*/
|
|
414
|
-
connectWindowsTCP(socketFile) {
|
|
415
|
-
return new Promise((resolve, reject) => {
|
|
416
|
-
const maxWaitMs = 8000;
|
|
417
|
-
const pollMs = 200;
|
|
418
|
-
let elapsed = 0;
|
|
419
|
-
const poll = () => {
|
|
420
|
-
if (fsSync.existsSync(socketFile)) {
|
|
421
|
-
let port;
|
|
422
|
-
try {
|
|
423
|
-
port = parseInt(fsSync.readFileSync(socketFile, 'utf8').trim(), 10);
|
|
424
|
-
}
|
|
425
|
-
catch (e) {
|
|
426
|
-
return reject(new Error(`Cannot read socket file: ${e}`));
|
|
427
|
-
}
|
|
428
|
-
if (!port || isNaN(port))
|
|
429
|
-
return reject(new Error(`Invalid port in socket file`));
|
|
430
|
-
const sock = net.createConnection(port, '127.0.0.1');
|
|
431
|
-
sock.once('connect', () => resolve({ socket: sock, port }));
|
|
432
|
-
sock.once('error', err => { sock.destroy(); reject(new Error(`TCP connect to ${port} failed: ${err.message}`)); });
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
elapsed += pollMs;
|
|
436
|
-
if (elapsed >= maxWaitMs) {
|
|
437
|
-
reject(new Error(`openMSX socket file not found after ${maxWaitMs}ms: ${socketFile}`));
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
setTimeout(poll, pollMs);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
poll();
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
320
|
resetIO() {
|
|
448
|
-
|
|
321
|
+
// Tear down the Windows control connection (TCP socket / SSPI proxy).
|
|
322
|
+
// No-op on Linux/macOS where controlConnection is null.
|
|
323
|
+
if (this.controlConnection) {
|
|
449
324
|
try {
|
|
450
|
-
this.
|
|
325
|
+
this.controlConnection.forceClose();
|
|
451
326
|
}
|
|
452
327
|
catch (_) { /* ignore */ }
|
|
453
|
-
this.
|
|
328
|
+
this.controlConnection = null;
|
|
454
329
|
}
|
|
330
|
+
this.controlWritable = null;
|
|
455
331
|
this.ioBuffer = '';
|
|
456
332
|
this.ioNotify = null;
|
|
457
333
|
}
|
|
@@ -472,6 +348,7 @@ export class OpenMSX {
|
|
|
472
348
|
this.lastMachine = null;
|
|
473
349
|
this.isConnected = false;
|
|
474
350
|
this.process = null;
|
|
351
|
+
// resetIO() also tears down the Windows control connection (proxy/socket).
|
|
475
352
|
this.resetIO();
|
|
476
353
|
safeResolve("Ok: Emulator process closed successfully");
|
|
477
354
|
});
|
|
@@ -572,16 +449,10 @@ export class OpenMSX {
|
|
|
572
449
|
writeData(data) {
|
|
573
450
|
if (!this.process || !this.isConnected)
|
|
574
451
|
throw new Error('openMSX process not running or not connected');
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
if (!this.process.stdin)
|
|
582
|
-
throw new Error('openMSX stdin not available');
|
|
583
|
-
this.process.stdin.write(data);
|
|
584
|
-
}
|
|
452
|
+
const channel = this.controlWritable;
|
|
453
|
+
if (!channel || channel.destroyed)
|
|
454
|
+
throw new Error('openMSX control channel not available');
|
|
455
|
+
channel.write(data);
|
|
585
456
|
}
|
|
586
457
|
readData() {
|
|
587
458
|
return new Promise((resolve, reject) => {
|
|
@@ -589,19 +460,11 @@ export class OpenMSX {
|
|
|
589
460
|
reject(new Error('openMSX process not running or not connected'));
|
|
590
461
|
return;
|
|
591
462
|
}
|
|
592
|
-
if (
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
if (!this.process?.stdout) {
|
|
600
|
-
reject(new Error('openMSX stdout not available'));
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
463
|
+
if (!this.controlWritable) {
|
|
464
|
+
reject(new Error('openMSX control channel not available'));
|
|
465
|
+
return;
|
|
603
466
|
}
|
|
604
|
-
// Unified for
|
|
467
|
+
// Unified for all transports: accumulate in ioBuffer until a complete
|
|
605
468
|
// <reply>…</reply> block, then extract and return it.
|
|
606
469
|
const RESPONSE_TIMEOUT = 10000;
|
|
607
470
|
const timer = setTimeout(() => {
|
|
@@ -629,14 +492,16 @@ export class OpenMSX {
|
|
|
629
492
|
await this.emu_close();
|
|
630
493
|
}
|
|
631
494
|
forceClose() {
|
|
495
|
+
// Kill the openMSX emulator; resetIO() force-closes the control connection
|
|
496
|
+
// (TCP socket / SSPI proxy) on Windows.
|
|
632
497
|
if (this.process && !this.process.killed) {
|
|
633
498
|
try {
|
|
634
499
|
this.process.kill('SIGKILL');
|
|
635
500
|
}
|
|
636
501
|
catch (_) { /* ignore */ }
|
|
637
|
-
this.process = null;
|
|
638
|
-
this.isConnected = false;
|
|
639
502
|
}
|
|
503
|
+
this.process = null;
|
|
504
|
+
this.isConnected = false;
|
|
640
505
|
this.resetIO();
|
|
641
506
|
}
|
|
642
507
|
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows control-channel connector for openMSX.
|
|
3
|
+
*
|
|
4
|
+
* On Windows, openMSX is a /SUBSYSTEM:WINDOWS GUI app whose stdin/stdout cannot
|
|
5
|
+
* be piped, and its TCP control socket requires SSPI (Negotiate/NTLM) auth since
|
|
6
|
+
* openMSX 0.7.1. This module owns ALL Windows-specific transport details (mode
|
|
7
|
+
* selection, socket-file polling, TCP connect, SSPI handshake, proxy launch) so
|
|
8
|
+
* that `openmsx.ts` keeps only platform-agnostic orchestration: it consumes the
|
|
9
|
+
* returned {@link WindowsControlConnection} and attaches the same generic XML
|
|
10
|
+
* stream handlers it uses on Linux/macOS.
|
|
11
|
+
*
|
|
12
|
+
* Modes (env var `OPENMSX_WINDOWS_CONTROL`):
|
|
13
|
+
* - `stdio-proxy` (default): launch the bundled .NET helper
|
|
14
|
+
* `bin/win-x64/mcp-openmsx-sspi-proxy.exe <port>` which does SSPI and exposes
|
|
15
|
+
* a clean XML stdio channel, exactly like `openmsx -control stdio` on Linux.
|
|
16
|
+
* - `direct-sspi`: Node opens the TCP socket and authenticates with
|
|
17
|
+
* `node-expose-sspi`. Fallback path.
|
|
18
|
+
* - `socket`: alias of `direct-sspi`.
|
|
19
|
+
* - `pipe`: reserved/not implemented.
|
|
20
|
+
*
|
|
21
|
+
* @author Natalia Pujol Cremades (@nataliapc)
|
|
22
|
+
* @license GPL2
|
|
23
|
+
*/
|
|
24
|
+
import fsSync from 'fs';
|
|
25
|
+
import net from 'net';
|
|
26
|
+
import os from 'os';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import { spawn } from 'child_process';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import { sleep } from './utils.js';
|
|
31
|
+
/** Directory of this module (works under both `src/` (tsx) and `dist/`). */
|
|
32
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
/** Resolve the Windows temp directory openMSX writes its socket file into. */
|
|
34
|
+
function windowsTempDir(env) {
|
|
35
|
+
return env.TEMP ?? env.TMP ?? path.join(os.homedir(), 'AppData', 'Local', 'Temp');
|
|
36
|
+
}
|
|
37
|
+
export class OpenMsxWindowsConnector {
|
|
38
|
+
options;
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.options = options;
|
|
41
|
+
}
|
|
42
|
+
get env() {
|
|
43
|
+
return this.options.env ?? process.env;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the requested Windows control mode from `OPENMSX_WINDOWS_CONTROL`.
|
|
47
|
+
* Defaults to `stdio-proxy`. `socket` is a legacy alias of `direct-sspi`.
|
|
48
|
+
* Throws on an unrecognised value.
|
|
49
|
+
*/
|
|
50
|
+
static getControlMode(env = process.env) {
|
|
51
|
+
const raw = env.OPENMSX_WINDOWS_CONTROL?.trim().toLowerCase() || 'stdio-proxy';
|
|
52
|
+
if (raw === 'socket')
|
|
53
|
+
return 'direct-sspi';
|
|
54
|
+
if (raw === 'stdio-proxy' || raw === 'direct-sspi' || raw === 'pipe')
|
|
55
|
+
return raw;
|
|
56
|
+
throw new Error(`Invalid OPENMSX_WINDOWS_CONTROL="${raw}". ` +
|
|
57
|
+
`Supported values: stdio-proxy, direct-sspi, socket, pipe.`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the path to the SSPI proxy executable.
|
|
61
|
+
* Honours `OPENMSX_WINDOWS_PROXY_EXECUTABLE` for development overrides,
|
|
62
|
+
* otherwise points at the bundled `bin/win-x64/mcp-openmsx-sspi-proxy.exe`
|
|
63
|
+
* relative to this module (i.e. `../bin/win-x64/...` from `dist/`).
|
|
64
|
+
*/
|
|
65
|
+
static resolveProxyExecutable(env = process.env) {
|
|
66
|
+
const override = env.OPENMSX_WINDOWS_PROXY_EXECUTABLE?.trim();
|
|
67
|
+
if (override)
|
|
68
|
+
return override;
|
|
69
|
+
return path.resolve(MODULE_DIR, '..', 'bin', 'win-x64', 'mcp-openmsx-sspi-proxy.exe');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Poll for openMSX's socket file and return the TCP control port it contains.
|
|
73
|
+
* The file appears at `%TEMP%\openmsx-default\socket.<pid>` and holds a port
|
|
74
|
+
* number (9938-9958).
|
|
75
|
+
*/
|
|
76
|
+
async waitForWindowsSocketPort(socketFile, maxWaitMs = 8000, pollMs = 200) {
|
|
77
|
+
let elapsed = 0;
|
|
78
|
+
// eslint-disable-next-line no-constant-condition
|
|
79
|
+
while (true) {
|
|
80
|
+
if (fsSync.existsSync(socketFile)) {
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = fsSync.readFileSync(socketFile, 'utf8').trim();
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
throw new Error(`Cannot read openMSX socket file: ${e instanceof Error ? e.message : e}`);
|
|
87
|
+
}
|
|
88
|
+
const port = parseInt(raw, 10);
|
|
89
|
+
if (!port || isNaN(port)) {
|
|
90
|
+
throw new Error(`Invalid port in openMSX socket file: "${raw}"`);
|
|
91
|
+
}
|
|
92
|
+
return port;
|
|
93
|
+
}
|
|
94
|
+
elapsed += pollMs;
|
|
95
|
+
if (elapsed >= maxWaitMs) {
|
|
96
|
+
throw new Error(`openMSX socket file not found after ${maxWaitMs}ms: ${socketFile}`);
|
|
97
|
+
}
|
|
98
|
+
await sleep(pollMs);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Establish the Windows control channel for the configured mode and return a
|
|
103
|
+
* uniform connection. SSPI is fully completed before returning: for
|
|
104
|
+
* `direct-sspi` here in Node; for `stdio-proxy` inside the .NET helper. The
|
|
105
|
+
* caller only has to attach generic XML stream handlers and send
|
|
106
|
+
* `<openmsx-control>`.
|
|
107
|
+
*/
|
|
108
|
+
async connect() {
|
|
109
|
+
const mode = OpenMsxWindowsConnector.getControlMode(this.env);
|
|
110
|
+
if (mode === 'pipe') {
|
|
111
|
+
throw new Error('OPENMSX_WINDOWS_CONTROL=pipe is reserved but not implemented yet');
|
|
112
|
+
}
|
|
113
|
+
const pid = this.options.openmsxProcess.pid;
|
|
114
|
+
if (!pid)
|
|
115
|
+
throw new Error('openMSX process has no pid');
|
|
116
|
+
const socketFile = path.join(windowsTempDir(this.env), 'openmsx-default', `socket.${pid}`);
|
|
117
|
+
this.options.diag(`waiting for openMSX socket file: ${socketFile}`);
|
|
118
|
+
const port = await this.waitForWindowsSocketPort(socketFile);
|
|
119
|
+
this.options.diag(`openMSX control port: ${port}`);
|
|
120
|
+
return mode === 'stdio-proxy'
|
|
121
|
+
? this.connectStdioProxy(port)
|
|
122
|
+
: this.connectDirectSspi(port);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `stdio-proxy`: launch the bundled .NET helper (it connects to openMSX, does
|
|
126
|
+
* SSPI and pipes raw XML over its stdio). SSPI never touches Node.
|
|
127
|
+
*/
|
|
128
|
+
connectStdioProxy(port) {
|
|
129
|
+
const proxyExe = OpenMsxWindowsConnector.resolveProxyExecutable(this.env);
|
|
130
|
+
if (!fsSync.existsSync(proxyExe)) {
|
|
131
|
+
throw new Error(`SSPI proxy executable not found: ${proxyExe}. ` +
|
|
132
|
+
`Build it with: pnpm build:proxy:win-x64:docker ` +
|
|
133
|
+
`(or set OPENMSX_WINDOWS_PROXY_EXECUTABLE to a built proxy).`);
|
|
134
|
+
}
|
|
135
|
+
this.options.diag(`launching SSPI proxy: "${proxyExe}" ${port}`);
|
|
136
|
+
const proxy = spawn(proxyExe, [String(port)], {
|
|
137
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
138
|
+
windowsHide: true,
|
|
139
|
+
});
|
|
140
|
+
if (!proxy.stdin || !proxy.stdout || !proxy.stderr) {
|
|
141
|
+
throw new Error('Failed to create SSPI proxy stdio pipes');
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
mode: 'stdio-proxy',
|
|
145
|
+
controlProcess: proxy,
|
|
146
|
+
input: proxy.stdin,
|
|
147
|
+
output: proxy.stdout,
|
|
148
|
+
errorOutput: proxy.stderr,
|
|
149
|
+
close: () => { try {
|
|
150
|
+
proxy.stdin?.end();
|
|
151
|
+
}
|
|
152
|
+
catch { /* ignore */ } },
|
|
153
|
+
forceClose: () => { try {
|
|
154
|
+
if (!proxy.killed)
|
|
155
|
+
proxy.kill('SIGKILL');
|
|
156
|
+
}
|
|
157
|
+
catch { /* ignore */ } },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `direct-sspi`: open the TCP socket and authenticate from Node with
|
|
162
|
+
* `node-expose-sspi`. The returned socket is a Duplex used for both XML
|
|
163
|
+
* directions.
|
|
164
|
+
*/
|
|
165
|
+
async connectDirectSspi(port) {
|
|
166
|
+
const socket = await this.tcpConnect(port);
|
|
167
|
+
this.options.diag('starting SSPI authentication...');
|
|
168
|
+
await this.performSspiAuth(socket);
|
|
169
|
+
this.options.diag('SSPI authentication successful');
|
|
170
|
+
return {
|
|
171
|
+
mode: 'direct-sspi',
|
|
172
|
+
controlProcess: null,
|
|
173
|
+
tcpSocket: socket,
|
|
174
|
+
input: socket,
|
|
175
|
+
output: socket,
|
|
176
|
+
close: () => { try {
|
|
177
|
+
socket.end();
|
|
178
|
+
}
|
|
179
|
+
catch { /* ignore */ } },
|
|
180
|
+
forceClose: () => { try {
|
|
181
|
+
socket.destroy();
|
|
182
|
+
}
|
|
183
|
+
catch { /* ignore */ } },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Open a TCP connection to openMSX's loopback control port. */
|
|
187
|
+
tcpConnect(port) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const socket = net.createConnection(port, '127.0.0.1');
|
|
190
|
+
socket.once('connect', () => resolve(socket));
|
|
191
|
+
socket.once('error', err => {
|
|
192
|
+
socket.destroy();
|
|
193
|
+
reject(new Error(`TCP connect to ${port} failed: ${err.message}`));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* SSPI (Negotiate/NTLM) authentication handshake — Windows only.
|
|
199
|
+
* Required since openMSX 0.7.1 for TCP socket connections.
|
|
200
|
+
* Uses the optional `node-expose-sspi` v0.1.x package.
|
|
201
|
+
*
|
|
202
|
+
* Reference C++ implementation: openMSX debugger SspiNegotiateClient.cpp
|
|
203
|
+
* Protocol: loop until SEC_E_OK — each round:
|
|
204
|
+
* client → [4-byte BE length][SSPI token]
|
|
205
|
+
* server → [4-byte BE length][SSPI response] (if SEC_I_CONTINUE_NEEDED)
|
|
206
|
+
* After SEC_E_OK: no server read — the XML protocol begins.
|
|
207
|
+
*
|
|
208
|
+
* The socket is paused before returning so any data openMSX sends after auth
|
|
209
|
+
* buffers until the caller attaches its own `data` handler (no data is lost).
|
|
210
|
+
*/
|
|
211
|
+
async performSspiAuth(socket) {
|
|
212
|
+
let nes;
|
|
213
|
+
try {
|
|
214
|
+
const { createRequire } = await import('module');
|
|
215
|
+
const req = createRequire(import.meta.url);
|
|
216
|
+
nes = req('node-expose-sspi');
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
throw new Error(`node-expose-sspi not available (${e instanceof Error ? e.message : e}). ` +
|
|
220
|
+
`Install with: npm install node-expose-sspi`);
|
|
221
|
+
}
|
|
222
|
+
// Accumulate TCP data for length-prefixed reads during SSPI phase.
|
|
223
|
+
// Pattern: onSspiData appends to buffer and NOTIFIES (does not clear).
|
|
224
|
+
// readLengthPrefixed checks the buffer size after each notification.
|
|
225
|
+
let sspiBuffer = Buffer.alloc(0);
|
|
226
|
+
let sspiNotify = null;
|
|
227
|
+
const onSspiData = (chunk) => {
|
|
228
|
+
sspiBuffer = Buffer.concat([sspiBuffer, chunk]);
|
|
229
|
+
if (sspiNotify) {
|
|
230
|
+
const notify = sspiNotify;
|
|
231
|
+
sspiNotify = null;
|
|
232
|
+
notify(); // just signal — buffer stays intact
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
socket.on('data', onSspiData);
|
|
236
|
+
// Wait until new data arrives (doesn't clear the buffer)
|
|
237
|
+
const waitMore = () => new Promise(resolve => {
|
|
238
|
+
sspiNotify = resolve;
|
|
239
|
+
});
|
|
240
|
+
const readLengthPrefixed = async () => {
|
|
241
|
+
// Wait until we have at least the 4-byte length prefix
|
|
242
|
+
while (sspiBuffer.length < 4)
|
|
243
|
+
await waitMore();
|
|
244
|
+
const len = sspiBuffer.readUInt32BE(0);
|
|
245
|
+
const total = 4 + len;
|
|
246
|
+
// Wait until the full payload has arrived
|
|
247
|
+
while (sspiBuffer.length < total)
|
|
248
|
+
await waitMore();
|
|
249
|
+
const result = sspiBuffer.subarray(4, total);
|
|
250
|
+
sspiBuffer = sspiBuffer.subarray(total); // consume only what we read
|
|
251
|
+
return result;
|
|
252
|
+
};
|
|
253
|
+
const sendToken = (token) => {
|
|
254
|
+
const buf = Buffer.from(token);
|
|
255
|
+
const lenBuf = Buffer.alloc(4);
|
|
256
|
+
lenBuf.writeUInt32BE(buf.length, 0);
|
|
257
|
+
socket.write(lenBuf);
|
|
258
|
+
socket.write(buf);
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
// node-expose-sspi v0.1.x API:
|
|
262
|
+
// AcquireCredentialsHandle → CredentialWithExpiry { credential, tsExpiry }
|
|
263
|
+
// InitializeSecurityContextInput.credential = CredHandle (the .credential property)
|
|
264
|
+
// InitializeSecurityContextInput.SecBufferDesc = server's token (NOT serverSecurityContext)
|
|
265
|
+
// InitializeSecurityContextInput.contextReq = string[] of ISC_REQ_* flags
|
|
266
|
+
// Flags from openMSX C++ ref: ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONNECTION | ISC_REQ_STREAM
|
|
267
|
+
const credWithExpiry = nes.sspi.AcquireCredentialsHandle({
|
|
268
|
+
packageName: 'Negotiate',
|
|
269
|
+
credentialUse: 'SECPKG_CRED_OUTBOUND',
|
|
270
|
+
});
|
|
271
|
+
const credential = credWithExpiry.credential;
|
|
272
|
+
const packageInfo = nes.sspi.QuerySecurityPackageInfo('Negotiate');
|
|
273
|
+
const ISC_FLAGS = ['ISC_REQ_ALLOCATE_MEMORY', 'ISC_REQ_CONNECTION', 'ISC_REQ_STREAM'];
|
|
274
|
+
let contextHandle = undefined;
|
|
275
|
+
let serverSecBufDesc = undefined;
|
|
276
|
+
// Loop until SEC_E_OK (mirrors C++ reference implementation)
|
|
277
|
+
while (true) {
|
|
278
|
+
const ctxInput = {
|
|
279
|
+
credential,
|
|
280
|
+
targetName: '',
|
|
281
|
+
cbMaxToken: packageInfo.cbMaxToken,
|
|
282
|
+
contextReq: ISC_FLAGS,
|
|
283
|
+
targetDataRep: 'SECURITY_NETWORK_DREP',
|
|
284
|
+
};
|
|
285
|
+
if (contextHandle !== undefined)
|
|
286
|
+
ctxInput.contextHandle = contextHandle;
|
|
287
|
+
if (serverSecBufDesc !== undefined)
|
|
288
|
+
ctxInput.SecBufferDesc = serverSecBufDesc;
|
|
289
|
+
const clientCtx = nes.sspi.InitializeSecurityContext(ctxInput);
|
|
290
|
+
contextHandle = clientCtx.contextHandle;
|
|
291
|
+
// Send our token to the server (if non-empty)
|
|
292
|
+
const tokenBuf = clientCtx.SecBufferDesc?.buffers?.[0];
|
|
293
|
+
if (tokenBuf && tokenBuf.byteLength > 0) {
|
|
294
|
+
sendToken(tokenBuf);
|
|
295
|
+
}
|
|
296
|
+
if (clientCtx.SECURITY_STATUS === 'SEC_E_OK') {
|
|
297
|
+
// Auth complete — no final read from server, proceed to XML
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
if (clientCtx.SECURITY_STATUS !== 'SEC_I_CONTINUE_NEEDED') {
|
|
301
|
+
throw new Error(`SSPI error: ${clientCtx.SECURITY_STATUS}`);
|
|
302
|
+
}
|
|
303
|
+
// Read server's response token
|
|
304
|
+
const response = await readLengthPrefixed();
|
|
305
|
+
const responseAB = response.buffer.slice(response.byteOffset, response.byteOffset + response.byteLength);
|
|
306
|
+
serverSecBufDesc = { ulVersion: 0, buffers: [responseAB] };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
// Remove the SSPI data listener and pause the socket so any post-auth
|
|
311
|
+
// bytes buffer until the caller attaches its own 'data' handler.
|
|
312
|
+
socket.removeListener('data', onSspiData);
|
|
313
|
+
socket.pause();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nataliapc/mcp-openmsx",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "Model context protocol server for openMSX automation and control",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,22 +30,23 @@
|
|
|
30
30
|
"node": ">=18.0.0"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "
|
|
34
|
-
"watch": "
|
|
33
|
+
"build": "shx rm -rf dist && tsc && shx chmod +x dist/server.js",
|
|
34
|
+
"watch": "tsc --watch",
|
|
35
35
|
"start": "node dist/server.js",
|
|
36
36
|
"dev": "tsx src/server.ts",
|
|
37
37
|
"prepublishOnly": "npm run build",
|
|
38
38
|
"test": "vitest run",
|
|
39
39
|
"test:watch": "vitest",
|
|
40
|
-
"test:coverage": "vitest run --coverage"
|
|
40
|
+
"test:coverage": "vitest run --coverage",
|
|
41
|
+
"build:proxy:win-x64": "dotnet publish helpers/openmsx-sspi-proxy/OpenMsxSspiProxy.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:TrimMode=partial -p:EnableCompressionInSingleFile=true -o bin/win-x64",
|
|
42
|
+
"build:proxy:win-x64:docker": "docker run --rm --user $(id -u):$(id -g) -e HOME=/tmp -e DOTNET_CLI_HOME=/tmp -v \"$PWD\":/work -w /work mcr.microsoft.com/dotnet/sdk:8.0 dotnet publish helpers/openmsx-sspi-proxy/OpenMsxSspiProxy.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:TrimMode=partial -p:EnableCompressionInSingleFile=true -o bin/win-x64",
|
|
43
|
+
"build:all": "pnpm build && pnpm build:proxy:win-x64:docker"
|
|
41
44
|
},
|
|
42
45
|
"optionalDependencies": {
|
|
43
46
|
"node-expose-sspi": "^0.1.60"
|
|
44
47
|
},
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"sharp": "$sharp"
|
|
48
|
-
}
|
|
48
|
+
"overrides": {
|
|
49
|
+
"sharp": "^0.34.5"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
@@ -56,25 +57,26 @@
|
|
|
56
57
|
"debug": "^4.4.3",
|
|
57
58
|
"express": "^5.2.1",
|
|
58
59
|
"mime-types": "^3.0.2",
|
|
59
|
-
"sanitize-html": "^2.17.
|
|
60
|
+
"sanitize-html": "^2.17.4",
|
|
60
61
|
"sharp": "^0.34.5",
|
|
61
|
-
"tsx": "^4.
|
|
62
|
+
"tsx": "^4.22.0",
|
|
62
63
|
"vectra": "^0.11.1",
|
|
63
64
|
"zod": "^3.25.76"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@modelcontextprotocol/inspector": "^0.20.0",
|
|
67
|
-
"@types/node": "^25.
|
|
68
|
+
"@types/node": "^25.7.0",
|
|
68
69
|
"@types/sanitize-html": "^2.16.0",
|
|
69
|
-
"@vitest/coverage-v8": "^4.1.
|
|
70
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
70
71
|
"shx": "^0.4.0",
|
|
71
72
|
"typescript": "^5.9.3",
|
|
72
|
-
"vitest": "^4.1.
|
|
73
|
+
"vitest": "^4.1.6"
|
|
73
74
|
},
|
|
74
75
|
"files": [
|
|
75
76
|
"dist/**/*",
|
|
76
77
|
"resources/**/*",
|
|
77
78
|
"vector-db/**/*",
|
|
79
|
+
"bin/win-x64/**/*",
|
|
78
80
|
"README.md",
|
|
79
81
|
"LICENSE"
|
|
80
82
|
],
|