dsh-windows-mcp 0.1.0 → 0.2.1
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/CHANGELOG.md +67 -1
- package/LICENSE +0 -6
- package/README.md +49 -2
- package/README.zh.md +48 -2
- package/lib/client.js +728 -11
- package/lib/dependency.js +150 -23
- package/lib/index.js +267 -27
- package/lib/installs.js +196 -0
- package/lib/mirrors.js +244 -0
- package/lib/pypi.js +171 -0
- package/lib/settings.js +251 -0
- package/lib/updater.js +502 -0
- package/package.json +1 -1
package/lib/dependency.js
CHANGED
|
@@ -80,10 +80,15 @@ export function windowsMcpExecutable() {
|
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Version of a `uv tool` installation, read from the dist-info directory inside
|
|
85
|
+
* the tool's environment. Works for the global tool root and for the managed
|
|
86
|
+
* per-version directories this plugin keeps (see `installs.js`).
|
|
87
|
+
* @param toolDirectory - the directory that contains `Scripts/` and `Lib/`,
|
|
88
|
+
* i.e. `…/tools/windows-mcp` or `…/tools/<version>/windows-mcp`.
|
|
89
|
+
*/
|
|
90
|
+
export function toolVersionIn(toolDirectory) {
|
|
91
|
+
if (typeof toolDirectory !== "string" || toolDirectory === "") return null;
|
|
87
92
|
const packages = [];
|
|
88
93
|
const collect = (sitePackages) => {
|
|
89
94
|
try {
|
|
@@ -94,10 +99,10 @@ export function windowsMcpVersion(executable = windowsMcpExecutable()) {
|
|
|
94
99
|
/* layout absent */
|
|
95
100
|
}
|
|
96
101
|
};
|
|
97
|
-
collect(join(
|
|
102
|
+
collect(join(toolDirectory, "Lib", "site-packages"));
|
|
98
103
|
try {
|
|
99
|
-
for (const entry of readdirSync(join(
|
|
100
|
-
if (entry.isDirectory() && /^python\d/u.test(entry.name)) collect(join(
|
|
104
|
+
for (const entry of readdirSync(join(toolDirectory, "lib"), { withFileTypes: true })) {
|
|
105
|
+
if (entry.isDirectory() && /^python\d/u.test(entry.name)) collect(join(toolDirectory, "lib", entry.name, "site-packages"));
|
|
101
106
|
}
|
|
102
107
|
} catch {
|
|
103
108
|
/* POSIX layout absent */
|
|
@@ -109,9 +114,17 @@ export function windowsMcpVersion(executable = windowsMcpExecutable()) {
|
|
|
109
114
|
return null;
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
/** Version of the globally installed tool, read from its dist-info directory. */
|
|
118
|
+
export function windowsMcpVersion(executable = windowsMcpExecutable()) {
|
|
119
|
+
if (executable === null) return null;
|
|
120
|
+
return toolVersionIn(windowsMcpToolDirectory());
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
/** The pinned upstream version our patch mounts, e.g. `0.8.8`, or null. */
|
|
113
124
|
export function windowsMcpVersionPin(patchText) {
|
|
114
|
-
|
|
125
|
+
// Stop at anything that ends a version in YAML or prose: whitespace, quotes,
|
|
126
|
+
// commas, closing brackets, and the backtick/paren used in comments.
|
|
127
|
+
const match = /windows-mcp@([0-9][^\s,'"\]`);]*)/u.exec(String(patchText ?? ""));
|
|
115
128
|
return match === null ? null : match[1];
|
|
116
129
|
}
|
|
117
130
|
|
|
@@ -143,11 +156,67 @@ export function dependencySnapshot(resolver, pinned, env = process.env) {
|
|
|
143
156
|
* install
|
|
144
157
|
* ------------------------------------------------------------------ */
|
|
145
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Environment for `uv tool install`.
|
|
161
|
+
*
|
|
162
|
+
* `UV_CONCURRENT_DOWNLOADS` is deliberately below uv's default of 8: the
|
|
163
|
+
* mainland mirrors rate-limit aggressive parallelism (measured: USTC answered
|
|
164
|
+
* HTTP 429 while unzipping a dependency when uv fetched 8 files at once), and a
|
|
165
|
+
* slow-but-successful install beats a fast-but-failing one.
|
|
166
|
+
*
|
|
167
|
+
* @param toolDirectory - when set, the install is redirected into that tool
|
|
168
|
+
* root (`UV_TOOL_DIR`) instead of the global one, which is how this plugin
|
|
169
|
+
* keeps several upstream versions side by side.
|
|
170
|
+
*/
|
|
171
|
+
export function uvInstallEnvironment(toolDirectory = null) {
|
|
172
|
+
const environment = {
|
|
173
|
+
...process.env,
|
|
174
|
+
UV_HTTP_TIMEOUT: "600",
|
|
175
|
+
UV_CONCURRENT_DOWNLOADS: "4",
|
|
176
|
+
UV_HTTP_RETRIES: "3",
|
|
177
|
+
};
|
|
178
|
+
if (typeof toolDirectory === "string" && toolDirectory !== "") environment.UV_TOOL_DIR = toolDirectory;
|
|
179
|
+
return environment;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Start one `uv tool install` and stream its output to `onLine`.
|
|
184
|
+
* @returns `{ child, args }` — the caller owns the child's lifecycle.
|
|
185
|
+
*/
|
|
186
|
+
export function spawnUvInstall(options = {}) {
|
|
187
|
+
const uv = options.uv;
|
|
188
|
+
const spec = options.spec;
|
|
189
|
+
const indexUrl = typeof options.indexUrl === "string" && options.indexUrl !== "" ? options.indexUrl : null;
|
|
190
|
+
const toolDirectory = typeof options.toolDirectory === "string" && options.toolDirectory !== "" ? options.toolDirectory : null;
|
|
191
|
+
const onLine = typeof options.onLine === "function" ? options.onLine : () => {};
|
|
192
|
+
const args = ["tool", "install", "--force"];
|
|
193
|
+
if (indexUrl !== null) args.push("--default-index", indexUrl);
|
|
194
|
+
args.push(spec);
|
|
195
|
+
const child = spawn(uv, args, {
|
|
196
|
+
cwd: typeof options.cwd === "string" ? options.cwd : homedir(),
|
|
197
|
+
env: { ...uvInstallEnvironment(toolDirectory), ...(options.env ?? {}) },
|
|
198
|
+
windowsHide: true,
|
|
199
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
200
|
+
});
|
|
201
|
+
try {
|
|
202
|
+
child.stdout?.on("data", onLine);
|
|
203
|
+
child.stderr?.on("data", onLine);
|
|
204
|
+
} catch {
|
|
205
|
+
/* a child without pipes still closes */
|
|
206
|
+
}
|
|
207
|
+
return { child, args };
|
|
208
|
+
}
|
|
209
|
+
|
|
146
210
|
/**
|
|
147
211
|
* One `uv tool install` at a time, with a bounded log the page can follow.
|
|
148
212
|
* @param options.warn - warning sink.
|
|
149
213
|
* @param options.resolver - PATH lookup used to find `uv`.
|
|
150
214
|
* @param options.pinned - upstream version pin.
|
|
215
|
+
* @param options.indexUrl - PyPI index to install from (a mirror), or null.
|
|
216
|
+
* @param options.toolDirectory - `UV_TOOL_DIR` for an isolated install, or null
|
|
217
|
+
* for the global tool root.
|
|
218
|
+
* @param options.resolveExecutable - where to look for the installed executable
|
|
219
|
+
* afterwards; defaults to the global `uv tool` installation.
|
|
151
220
|
* @param options.afterInstall - called with the installed executable, returns
|
|
152
221
|
* the bridge-write outcome that `snapshot()` then reports.
|
|
153
222
|
*/
|
|
@@ -202,22 +271,19 @@ export function createPrepare(options = {}) {
|
|
|
202
271
|
return { started: false, reason: state.error, state: this.snapshot() };
|
|
203
272
|
}
|
|
204
273
|
const spec = options.pinned === null || options.pinned === undefined ? WINDOWS_MCP_TOOL : `${WINDOWS_MCP_TOOL}@${options.pinned}`;
|
|
274
|
+
const indexUrl = typeof options.indexUrl === "string" && options.indexUrl !== "" ? options.indexUrl : null;
|
|
275
|
+
const toolDirectory = typeof options.toolDirectory === "string" && options.toolDirectory !== "" ? options.toolDirectory : null;
|
|
276
|
+
const resolveExecutable = typeof options.resolveExecutable === "function" ? options.resolveExecutable : windowsMcpExecutable;
|
|
205
277
|
state.phase = "running";
|
|
206
278
|
state.startedAt = new Date().toISOString();
|
|
207
279
|
state.finishedAt = null;
|
|
208
280
|
state.error = null;
|
|
209
281
|
state.bridge = null;
|
|
210
|
-
|
|
282
|
+
const command = [uv, "tool", "install", "--force", ...(indexUrl === null ? [] : ["--default-index", indexUrl]), spec].join(" ");
|
|
283
|
+
state.log = [`$ ${command}${toolDirectory === null ? "" : ` (UV_TOOL_DIR=${toolDirectory})`}`];
|
|
211
284
|
try {
|
|
212
|
-
const running =
|
|
213
|
-
cwd: homedir(),
|
|
214
|
-
env: { ...process.env, UV_HTTP_TIMEOUT: "600", UV_CONCURRENT_DOWNLOADS: "8" },
|
|
215
|
-
windowsHide: true,
|
|
216
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
217
|
-
});
|
|
285
|
+
const { child: running } = spawnUvInstall({ uv, spec, indexUrl, toolDirectory, onLine: append });
|
|
218
286
|
child = running;
|
|
219
|
-
running.stdout?.on("data", append);
|
|
220
|
-
running.stderr?.on("data", append);
|
|
221
287
|
running.on("error", (error) => settle("failed", String(error)));
|
|
222
288
|
running.on("close", (code) => {
|
|
223
289
|
if (state.phase === "cancelled") return;
|
|
@@ -225,7 +291,7 @@ export function createPrepare(options = {}) {
|
|
|
225
291
|
settle("failed", `uv exited with code ${code}`);
|
|
226
292
|
return;
|
|
227
293
|
}
|
|
228
|
-
const executable =
|
|
294
|
+
const executable = resolveExecutable();
|
|
229
295
|
if (executable === null) {
|
|
230
296
|
settle("failed", "uv reported success, but no windows-mcp executable appeared in the tool directory.");
|
|
231
297
|
return;
|
|
@@ -289,10 +355,63 @@ export function yamlScalar(value) {
|
|
|
289
355
|
return `'${String(value).replace(/'/gu, "''")}'`;
|
|
290
356
|
}
|
|
291
357
|
|
|
292
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* The bridge row values this package ships. The settings page may change them;
|
|
360
|
+
* `lib/settings.js` normalises user input against these same defaults.
|
|
361
|
+
*/
|
|
362
|
+
export const BRIDGE_DEFAULTS = Object.freeze({
|
|
363
|
+
toolCallTimeoutMs: 120000,
|
|
364
|
+
excludeTools: Object.freeze(["App", "PowerShell", "FileSystem"]),
|
|
365
|
+
screenshotScale: 0.75,
|
|
366
|
+
screenshotBackend: "auto",
|
|
367
|
+
disableFlash: true,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/** Render `args:` — `--exclude-tools` takes one comma-separated argv. */
|
|
371
|
+
function bridgeArgsLine(excludeTools) {
|
|
372
|
+
const names = Array.from(excludeTools, (name) => String(name).trim()).filter((name) => name !== "");
|
|
373
|
+
if (names.length === 0) return " args: [serve]";
|
|
374
|
+
return ` args: [serve, --exclude-tools, ${JSON.stringify(names.join(","))}]`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Render `env:` — only the keys that are set, in a stable order. */
|
|
378
|
+
function bridgeEnvLine(settings) {
|
|
379
|
+
const pairs = ['ANONYMIZED_TELEMETRY: "false"'];
|
|
380
|
+
if (settings.disableFlash !== false) pairs.push('WINDOWS_MCP_DISABLE_FLASH: "1"');
|
|
381
|
+
pairs.push(`WINDOWS_MCP_SCREENSHOT_SCALE: ${JSON.stringify(String(settings.screenshotScale))}`);
|
|
382
|
+
if (typeof settings.screenshotBackend === "string" && settings.screenshotBackend !== "auto") {
|
|
383
|
+
pairs.push(`WINDOWS_MCP_SCREENSHOT_BACKEND: ${JSON.stringify(settings.screenshotBackend)}`);
|
|
384
|
+
}
|
|
385
|
+
return ` env: { ${pairs.join(", ")} }`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Fold user settings (or their absence) into one complete bridge value set. */
|
|
389
|
+
function bridgeValues(meta) {
|
|
390
|
+
const source = meta.settings !== null && typeof meta.settings === "object" ? meta.settings : null;
|
|
391
|
+
const merged = { ...BRIDGE_DEFAULTS };
|
|
392
|
+
if (source === null) return merged;
|
|
393
|
+
if (typeof source.toolCallTimeoutMs === "number" && Number.isFinite(source.toolCallTimeoutMs)) {
|
|
394
|
+
merged.toolCallTimeoutMs = Math.round(source.toolCallTimeoutMs);
|
|
395
|
+
}
|
|
396
|
+
if (Array.isArray(source.excludeTools)) merged.excludeTools = source.excludeTools.slice();
|
|
397
|
+
if (typeof source.screenshotScale === "number" && Number.isFinite(source.screenshotScale)) {
|
|
398
|
+
merged.screenshotScale = source.screenshotScale;
|
|
399
|
+
}
|
|
400
|
+
if (typeof source.screenshotBackend === "string" && source.screenshotBackend !== "") {
|
|
401
|
+
merged.screenshotBackend = source.screenshotBackend;
|
|
402
|
+
}
|
|
403
|
+
if (typeof source.disableFlash === "boolean") merged.disableFlash = source.disableFlash;
|
|
404
|
+
return merged;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The complete bridge row, pointing at an installed executable.
|
|
409
|
+
* `meta.settings` is the optional `bridge` half of the user settings.
|
|
410
|
+
*/
|
|
293
411
|
export function bridgeOverrideBlock(executable, meta = {}) {
|
|
294
412
|
const stamp = typeof meta.stamp === "string" ? meta.stamp : new Date().toISOString();
|
|
295
413
|
const version = typeof meta.version === "string" ? meta.version : "unknown";
|
|
414
|
+
const values = bridgeValues(meta);
|
|
296
415
|
return [
|
|
297
416
|
PROFILE_BLOCK_BEGIN,
|
|
298
417
|
`# Written by ${PACKAGE_ID} ${version} at ${stamp}.`,
|
|
@@ -302,11 +421,11 @@ export function bridgeOverrideBlock(executable, meta = {}) {
|
|
|
302
421
|
" serverName: windows",
|
|
303
422
|
" transport: stdio",
|
|
304
423
|
` command: ${yamlScalar(executable)}`,
|
|
305
|
-
|
|
306
|
-
|
|
424
|
+
bridgeArgsLine(values.excludeTools),
|
|
425
|
+
` toolCallTimeoutMs: ${values.toolCallTimeoutMs}`,
|
|
307
426
|
" failOnStartupError: false",
|
|
308
427
|
" reconnect: { enabled: true, initialDelayMs: 500, maxDelayMs: 30000, maxAttempts: 10 }",
|
|
309
|
-
|
|
428
|
+
bridgeEnvLine(values),
|
|
310
429
|
PROFILE_BLOCK_END,
|
|
311
430
|
].join("\n");
|
|
312
431
|
}
|
|
@@ -460,7 +579,15 @@ export function writeBridgeOverride(executable, options = {}) {
|
|
|
460
579
|
backup = null;
|
|
461
580
|
}
|
|
462
581
|
}
|
|
463
|
-
writeFileSync(
|
|
582
|
+
writeFileSync(
|
|
583
|
+
file,
|
|
584
|
+
withBridgeOverride(
|
|
585
|
+
current,
|
|
586
|
+
bridgeOverrideBlock(executable, { stamp: new Date().toISOString(), version, settings: options.settings }),
|
|
587
|
+
eol,
|
|
588
|
+
),
|
|
589
|
+
"utf8",
|
|
590
|
+
);
|
|
464
591
|
return { ok: true, path: file, backup, executable, restartRequired: true };
|
|
465
592
|
} catch (error) {
|
|
466
593
|
warn(`could not point the bridge at the installed Windows-MCP: ${String(error)}`);
|
package/lib/index.js
CHANGED
|
@@ -33,12 +33,21 @@ import {
|
|
|
33
33
|
BRIDGE_ROUTE,
|
|
34
34
|
PREPARE_CANCEL_ROUTE,
|
|
35
35
|
PREPARE_ROUTE,
|
|
36
|
-
createPrepare,
|
|
37
36
|
dependencySnapshot,
|
|
38
37
|
windowsMcpExecutable,
|
|
39
38
|
windowsMcpVersionPin,
|
|
40
39
|
writeBridgeOverride,
|
|
41
40
|
} from "./dependency.js";
|
|
41
|
+
import { UPDATE_ACTIONS, UPDATE_ROUTE, createUpdater } from "./updater.js";
|
|
42
|
+
import {
|
|
43
|
+
SETTINGS_ROUTE,
|
|
44
|
+
bridgeSettingsMatch,
|
|
45
|
+
bridgeSettingsOf,
|
|
46
|
+
normalizeSettings,
|
|
47
|
+
readBridgeSettings,
|
|
48
|
+
readSettings,
|
|
49
|
+
writeSettings,
|
|
50
|
+
} from "./settings.js";
|
|
42
51
|
|
|
43
52
|
const PACKAGE_ID = "dsh-windows-mcp";
|
|
44
53
|
const BRIDGE_ROW_ID = "windows-mcp-bridge";
|
|
@@ -62,13 +71,17 @@ const stateDirectory = (() => {
|
|
|
62
71
|
const statePath = join(stateDirectory, "dsh-windows-mcp.state.json");
|
|
63
72
|
const STATE_ROUTE = "/dsh-windows-mcp/state";
|
|
64
73
|
const MAX_STATE_BODY_BYTES = 1024;
|
|
74
|
+
const MAX_SETTINGS_BODY_BYTES = 8192;
|
|
65
75
|
|
|
66
76
|
/** Mirrors the persisted flag; read once at boot, updated by the state route.
|
|
67
77
|
* Desktop control is opt-in: a fresh install starts OFF. */
|
|
68
78
|
const computerUse = { enabled: false };
|
|
69
79
|
|
|
70
|
-
/**
|
|
71
|
-
let
|
|
80
|
+
/** User settings; read once at boot, replaced by the settings route. */
|
|
81
|
+
let settings = null;
|
|
82
|
+
|
|
83
|
+
/** The Windows-MCP version manager, created in apply() once the logger exists. */
|
|
84
|
+
let updater = null;
|
|
72
85
|
|
|
73
86
|
/* ------------------------------------------------------------------ *
|
|
74
87
|
* small, throw-proof helpers
|
|
@@ -311,6 +324,25 @@ function buildReport(tools) {
|
|
|
311
324
|
}`,
|
|
312
325
|
);
|
|
313
326
|
|
|
327
|
+
const update = updater === null ? null : updater.view();
|
|
328
|
+
if (update !== null) {
|
|
329
|
+
const managed = update.versions;
|
|
330
|
+
lines.push(
|
|
331
|
+
`managed install : ${
|
|
332
|
+
managed.active === null
|
|
333
|
+
? "none yet — the global `uv tool` installation is used (installing or updating creates one)"
|
|
334
|
+
: `v${managed.active} under ${managed.toolchainRoot}`
|
|
335
|
+
}`,
|
|
336
|
+
);
|
|
337
|
+
if (managed.previous !== null) lines.push(` rollback to : v${managed.previous}`);
|
|
338
|
+
if (managed.available.length > 0) lines.push(` installed : ${managed.available.map((version) => `v${version}`).join(", ")}`);
|
|
339
|
+
if (update.check.latest === null) {
|
|
340
|
+
lines.push(" latest : not checked yet — press Check for updates in Settings → Plugins");
|
|
341
|
+
} else {
|
|
342
|
+
lines.push(` latest : v${update.check.latest}${managed.updateAvailable ? " — an update is available" : " — up to date"}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
314
346
|
if (uv === null && uvx === null) {
|
|
315
347
|
advice.push(
|
|
316
348
|
"Install uv (https://docs.astral.sh/uv/) and make sure `uvx` is on the PATH of the process that runs DSH — without it the bridge cannot start Windows-MCP.",
|
|
@@ -490,13 +522,52 @@ function registerStateRoute(ctx, warn) {
|
|
|
490
522
|
}
|
|
491
523
|
};
|
|
492
524
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
525
|
+
/** Map the version manager's install progress onto the prepare shape the page polls. */
|
|
526
|
+
const prepareSnapshot = () => {
|
|
527
|
+
if (updater === null) return null;
|
|
528
|
+
const install = updater.view().install;
|
|
529
|
+
return {
|
|
530
|
+
phase: install.phase === "installing" ? "running" : install.phase,
|
|
531
|
+
startedAt: install.startedAt,
|
|
532
|
+
finishedAt: install.finishedAt,
|
|
533
|
+
error: install.error,
|
|
534
|
+
log: install.log,
|
|
535
|
+
bridge: install.bridge,
|
|
536
|
+
version: install.version,
|
|
537
|
+
mirror: install.mirror,
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Settings plus what the profile patch actually says: the two only
|
|
543
|
+
* match after a restart, because the MCP client reads its row at boot.
|
|
544
|
+
*/
|
|
545
|
+
const settingsView = () => {
|
|
546
|
+
const current = settings === null ? normalizeSettings(null).settings : settings;
|
|
547
|
+
const patchText = readText(patchPath);
|
|
548
|
+
const applied = bridgeSettingsMatch(current, patchText);
|
|
549
|
+
return {
|
|
550
|
+
settings: current,
|
|
551
|
+
bridge: readBridgeSettings(patchText),
|
|
552
|
+
applied,
|
|
553
|
+
restartRequired: !applied,
|
|
554
|
+
pluginVersion: selfVersion(),
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const snapshot = () => {
|
|
559
|
+
const legacy = dependencySnapshot(which, windowsMcpVersionPin(readText(patchPath)));
|
|
560
|
+
const update = updater === null ? null : updater.view();
|
|
561
|
+
return {
|
|
562
|
+
enabled: computerUse.enabled,
|
|
563
|
+
bridged: bridgedToolCount(scoped.get?.("tools")),
|
|
564
|
+
serverName: "windows",
|
|
565
|
+
dependency: updater === null ? legacy : updater.dependencyView(legacy),
|
|
566
|
+
prepare: prepareSnapshot(),
|
|
567
|
+
update,
|
|
568
|
+
settings: settingsView(),
|
|
569
|
+
};
|
|
570
|
+
};
|
|
500
571
|
|
|
501
572
|
const handler = (request, response) => {
|
|
502
573
|
if (request.method === "GET") {
|
|
@@ -536,7 +607,7 @@ function registerStateRoute(ctx, warn) {
|
|
|
536
607
|
request.on("error", () => respond(response, 400, { error: "request failed" }));
|
|
537
608
|
};
|
|
538
609
|
|
|
539
|
-
/** Start the
|
|
610
|
+
/** Start installing the pinned (or newest known) Windows-MCP into its own tool root. */
|
|
540
611
|
const prepareHandler = (request, response) => {
|
|
541
612
|
if (request.method === "GET") {
|
|
542
613
|
respond(response, 200, snapshot());
|
|
@@ -546,17 +617,22 @@ function registerStateRoute(ctx, warn) {
|
|
|
546
617
|
respond(response, 405, { error: "method not allowed" });
|
|
547
618
|
return;
|
|
548
619
|
}
|
|
549
|
-
if (
|
|
550
|
-
respond(response, 503, { error: "the
|
|
620
|
+
if (updater === null) {
|
|
621
|
+
respond(response, 503, { error: "the version manager is not ready" });
|
|
551
622
|
return;
|
|
552
623
|
}
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
624
|
+
const view = updater.view();
|
|
625
|
+
const target = view.versions.pinned ?? view.check.latest;
|
|
626
|
+
if (target === null) {
|
|
627
|
+
respond(response, 409, { started: false, reason: "no Windows-MCP version is known yet", prepare: prepareSnapshot() });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (view.install.phase === "installing") {
|
|
631
|
+
respond(response, 409, { started: false, reason: "an install is already running", prepare: prepareSnapshot() });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
void updater.installVersion(target);
|
|
635
|
+
respond(response, 202, { started: true, spec: `windows-mcp@${target}`, prepare: prepareSnapshot() });
|
|
560
636
|
};
|
|
561
637
|
|
|
562
638
|
/** Stop a running install. */
|
|
@@ -565,31 +641,177 @@ function registerStateRoute(ctx, warn) {
|
|
|
565
641
|
respond(response, 405, { error: "method not allowed" });
|
|
566
642
|
return;
|
|
567
643
|
}
|
|
568
|
-
const
|
|
569
|
-
respond(response, 200, { cancelled
|
|
644
|
+
const result = updater === null ? { cancelled: false } : updater.cancel();
|
|
645
|
+
respond(response, 200, { cancelled: result.cancelled === true, prepare: prepareSnapshot() });
|
|
570
646
|
};
|
|
571
647
|
|
|
572
|
-
/** Point the bridge at the
|
|
648
|
+
/** Point the bridge at the active executable (managed install first). */
|
|
573
649
|
const bridgeHandler = (request, response) => {
|
|
574
650
|
if (request.method !== "POST") {
|
|
575
651
|
respond(response, 405, { error: "method not allowed" });
|
|
576
652
|
return;
|
|
577
653
|
}
|
|
578
|
-
const
|
|
654
|
+
const view = updater === null ? null : updater.view();
|
|
655
|
+
const executable = view?.versions?.activeExecutable ?? windowsMcpExecutable();
|
|
579
656
|
if (executable === null) {
|
|
580
657
|
respond(response, 409, {
|
|
581
658
|
ok: false,
|
|
582
|
-
error: "Windows-MCP is not installed
|
|
659
|
+
error: "Windows-MCP is not installed yet, so there is nothing to point the bridge at.",
|
|
583
660
|
});
|
|
584
661
|
return;
|
|
585
662
|
}
|
|
586
663
|
respond(response, 200, writeBridgeOverride(executable, { warn, version: selfVersion() }));
|
|
587
664
|
};
|
|
588
665
|
|
|
666
|
+
/** Check for, install, activate and roll back Windows-MCP versions. */
|
|
667
|
+
const updateHandler = (request, response) => {
|
|
668
|
+
if (request.method === "GET") {
|
|
669
|
+
respond(response, 200, updater === null ? { error: "the version manager is not ready" } : updater.view());
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (request.method !== "POST") {
|
|
673
|
+
respond(response, 405, { error: "method not allowed" });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (updater === null) {
|
|
677
|
+
respond(response, 503, { error: "the version manager is not ready" });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const chunks = [];
|
|
681
|
+
let size = 0;
|
|
682
|
+
request.on("data", (chunk) => {
|
|
683
|
+
size += chunk.length;
|
|
684
|
+
if (size > MAX_STATE_BODY_BYTES) {
|
|
685
|
+
request.destroy();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
chunks.push(chunk);
|
|
689
|
+
});
|
|
690
|
+
request.on("end", () => {
|
|
691
|
+
let body;
|
|
692
|
+
try {
|
|
693
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
694
|
+
} catch {
|
|
695
|
+
respond(response, 400, { error: "invalid JSON body" });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const action = typeof body?.action === "string" ? body.action : "";
|
|
699
|
+
const version = typeof body?.version === "string" && body.version !== "" ? body.version : null;
|
|
700
|
+
const allowed = [...UPDATE_ACTIONS, "cancel"];
|
|
701
|
+
if (!allowed.includes(action)) {
|
|
702
|
+
respond(response, 400, { error: `expected { action } in ${allowed.join(", ")}` });
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (action === "check") {
|
|
706
|
+
void updater.check({ force: true });
|
|
707
|
+
respond(response, 202, { started: true, action });
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (action === "install") {
|
|
711
|
+
const view = updater.view();
|
|
712
|
+
const target = version ?? view.check.latest ?? view.versions.pinned;
|
|
713
|
+
if (target === null) {
|
|
714
|
+
respond(response, 409, { ok: false, error: "no Windows-MCP version is known yet — check for updates first" });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
void updater.installVersion(target);
|
|
718
|
+
respond(response, 202, { started: true, action, version: target });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (action === "cancel") {
|
|
722
|
+
respond(response, 200, updater.cancel());
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (action === "activate") {
|
|
726
|
+
if (version === null) {
|
|
727
|
+
respond(response, 400, { error: "activate needs { version }" });
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
respond(response, 200, updater.activateInstalled(version));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
respond(response, 200, updater.rollback());
|
|
734
|
+
});
|
|
735
|
+
request.on("error", () => respond(response, 400, { error: "request failed" }));
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Read and update the user settings. Plugin settings apply at once;
|
|
740
|
+
* bridge settings are written into the profile patch row and only take
|
|
741
|
+
* effect after DSH restarts.
|
|
742
|
+
*/
|
|
743
|
+
const settingsHandler = (request, response) => {
|
|
744
|
+
if (request.method === "GET") {
|
|
745
|
+
respond(response, 200, settingsView());
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (request.method !== "POST") {
|
|
749
|
+
respond(response, 405, { error: "method not allowed" });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const chunks = [];
|
|
753
|
+
let size = 0;
|
|
754
|
+
request.on("data", (chunk) => {
|
|
755
|
+
size += chunk.length;
|
|
756
|
+
if (size > MAX_SETTINGS_BODY_BYTES) {
|
|
757
|
+
request.destroy();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
chunks.push(chunk);
|
|
761
|
+
});
|
|
762
|
+
request.on("end", () => {
|
|
763
|
+
let body;
|
|
764
|
+
try {
|
|
765
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
766
|
+
} catch {
|
|
767
|
+
respond(response, 400, { error: "invalid JSON body" });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const incoming = body !== null && typeof body === "object" && body.settings !== null && typeof body.settings === "object" ? body.settings : body;
|
|
771
|
+
const normalized = normalizeSettings(incoming);
|
|
772
|
+
const stored = writeSettings(normalized.settings, stateDirectory);
|
|
773
|
+
settings = stored.settings;
|
|
774
|
+
const wanted = bridgeSettingsOf(settings);
|
|
775
|
+
if (bridgeSettingsMatch(settings, readText(patchPath))) {
|
|
776
|
+
respond(response, 200, {
|
|
777
|
+
ok: true,
|
|
778
|
+
saved: stored.written,
|
|
779
|
+
problems: normalized.problems,
|
|
780
|
+
write: null,
|
|
781
|
+
...settingsView(),
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const view = updater === null ? null : updater.view();
|
|
786
|
+
const executable = view?.versions?.activeExecutable ?? windowsMcpExecutable();
|
|
787
|
+
if (executable === null) {
|
|
788
|
+
respond(response, 200, {
|
|
789
|
+
ok: true,
|
|
790
|
+
saved: stored.written,
|
|
791
|
+
problems: normalized.problems,
|
|
792
|
+
write: { ok: false, error: "Windows-MCP is not installed yet, so the bridge row was left untouched" },
|
|
793
|
+
...settingsView(),
|
|
794
|
+
});
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const written = writeBridgeOverride(executable, { warn, version: selfVersion(), settings: wanted });
|
|
798
|
+
respond(response, 200, {
|
|
799
|
+
ok: written.ok === true,
|
|
800
|
+
saved: stored.written,
|
|
801
|
+
problems: normalized.problems,
|
|
802
|
+
write: written,
|
|
803
|
+
...settingsView(),
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
request.on("error", () => respond(response, 400, { error: "request failed" }));
|
|
807
|
+
};
|
|
808
|
+
|
|
589
809
|
scoped.webServer.register({ kind: "exact", path: STATE_ROUTE, handler });
|
|
590
810
|
scoped.webServer.register({ kind: "exact", path: PREPARE_ROUTE, handler: prepareHandler });
|
|
591
811
|
scoped.webServer.register({ kind: "exact", path: PREPARE_CANCEL_ROUTE, handler: cancelHandler });
|
|
592
812
|
scoped.webServer.register({ kind: "exact", path: BRIDGE_ROUTE, handler: bridgeHandler });
|
|
813
|
+
scoped.webServer.register({ kind: "exact", path: UPDATE_ROUTE, handler: updateHandler });
|
|
814
|
+
scoped.webServer.register({ kind: "exact", path: SETTINGS_ROUTE, handler: settingsHandler });
|
|
593
815
|
} catch (error) {
|
|
594
816
|
warn(`could not register the Computer Use state route: ${String(error)}`);
|
|
595
817
|
}
|
|
@@ -607,16 +829,34 @@ export function apply(ctx) {
|
|
|
607
829
|
try {
|
|
608
830
|
const warn = makeLogger(ctx);
|
|
609
831
|
computerUse.enabled = readComputerUse().enabled;
|
|
610
|
-
|
|
832
|
+
settings = readSettings(stateDirectory);
|
|
833
|
+
updater = createUpdater({
|
|
611
834
|
warn,
|
|
612
835
|
resolver: which,
|
|
613
836
|
pinned: windowsMcpVersionPin(readText(patchPath)),
|
|
614
|
-
|
|
837
|
+
preferredMirror: () => (settings === null ? "auto" : settings.plugin.preferredMirror),
|
|
838
|
+
writeBridge: (executable) =>
|
|
839
|
+
writeBridgeOverride(executable, {
|
|
840
|
+
warn,
|
|
841
|
+
version: selfVersion(),
|
|
842
|
+
settings: settings === null ? null : bridgeSettingsOf(settings),
|
|
843
|
+
}),
|
|
615
844
|
});
|
|
616
845
|
auditStartup(warn);
|
|
617
846
|
registerDoctor(ctx, warn);
|
|
618
847
|
registerStateRoute(ctx, warn);
|
|
619
848
|
registerToolGate(ctx, warn);
|
|
849
|
+
// Check for a newer Windows-MCP build in the background (cheap: a ranged
|
|
850
|
+
// read per mirror, cached for hours). The page shows the result; the boot
|
|
851
|
+
// never waits for it, and a failure only costs the last known answer. The
|
|
852
|
+
// settings page can turn this off.
|
|
853
|
+
if (settings === null || settings.plugin.autoCheckUpdates === true) {
|
|
854
|
+
try {
|
|
855
|
+
void updater.check({ force: false }).catch(() => {});
|
|
856
|
+
} catch {
|
|
857
|
+
/* a failed check is not a boot problem */
|
|
858
|
+
}
|
|
859
|
+
}
|
|
620
860
|
} catch {
|
|
621
861
|
/* apply() must not throw, whatever happens */
|
|
622
862
|
}
|