/** * Windows-MCP manager (Electron integration) */ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { spawn } from "child_process"; import { createRequire } from "module"; import { app } from "electron"; import log from "electron-log"; import { ElectronProcessRunner } from "./windowsMcpRunner.js"; import { getAppEnv, getResourcesPath, getUvBinPath, } from "../system/dependencies"; import { isWindows } from "../system/shellEnv"; import { getGuiMcpPort } from "./guiAgentServer"; import { killProcessTreesListeningOnTcpPort } from "../utils/processTree"; import { APP_DATA_DIR_NAME } from "../constants"; import { t } from "../i18n"; type WindowsMcpManagerType = import("agent-gui-server").WindowsMcpManager; type ProcessConfig = import("agent-gui-server").ProcessConfig; type InstallSource = "offline" | "online-fallback"; interface WindowsMcpBundleManifest { packageName: string; version: string; resolvedSpec: string; generatedAt: string; files: string[]; } interface WindowsMcpInstallRequest { packageName: string; version: string | null; resolvedSpec: string; runtimeVersionKey: string; } interface WindowsMcpRuntimeReceipt { schemaVersion: 1; packageName: string; version: string | null; resolvedSpec: string; source: InstallSource; installedAt: string; runtimeRoot: string; integrity: "ok"; } interface RuntimeInfo { runtimeRoot: string; binPath: string; request: WindowsMcpInstallRequest; } interface CommandResult { code: number; stdout: string; stderr: string; spawnError?: string; } const WINDOWS_MCP_PACKAGE_NAME = "windows-mcp"; /** 须与 `scripts/prepare/prepare-windows-mcp.js` 的 PIP_DOWNLOAD_PYTHON 一致,否则离线 wheel(如 cp313)可能与默认解释器不匹配 */ const WINDOWS_MCP_UV_PYTHON = "3.13"; const WINDOWS_MCP_MANIFEST_FILE = "manifest.json"; const WINDOWS_MCP_RECEIPT_FILE = "receipt.json"; const WINDOWS_MCP_LOCK_FILE = "install.lock"; const INSTALL_LOCK_TIMEOUT_MS = 45_000; const INSTALL_LOCK_STALE_MS = 5 * 60 * 1000; let windowsMcpManager: WindowsMcpManagerType | null = null; let processRunner: ElectronProcessRunner | null = null; const runtimeRequire = createRequire(__filename); function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function getWindowsMcpBundleRoot(): string { return path.join(getResourcesPath(), "windows-mcp"); } function getWindowsMcpWheelsDir(): string { return path.join(getWindowsMcpBundleRoot(), "wheels"); } function getWindowsMcpManifestPath(): string { return path.join(getWindowsMcpBundleRoot(), WINDOWS_MCP_MANIFEST_FILE); } function getWindowsMcpDataRoot(): string { return path.join( app.getPath("home"), APP_DATA_DIR_NAME, "windows-mcp-runtime", ); } function safeVersionSegment(version: string): string { return version.replace(/[^A-Za-z0-9._-]/g, "_"); } function getRuntimeRoot(request: WindowsMcpInstallRequest): string { return path.join(getWindowsMcpDataRoot(), request.runtimeVersionKey); } function getRuntimeBinPath(runtimeRoot: string): string { return path.join(runtimeRoot, "bin", "windows-mcp.exe"); } function getRuntimeReceiptPath(runtimeRoot: string): string { return path.join(runtimeRoot, WINDOWS_MCP_RECEIPT_FILE); } function buildInstallRequest( manifest: WindowsMcpBundleManifest | null, ): WindowsMcpInstallRequest { if (manifest) { return { packageName: manifest.packageName, version: manifest.version, resolvedSpec: manifest.resolvedSpec, runtimeVersionKey: safeVersionSegment(manifest.version), }; } log.warn( "[WindowsMcp] Bundle manifest missing or invalid; using unpinned install request", ); return { packageName: WINDOWS_MCP_PACKAGE_NAME, version: null, resolvedSpec: WINDOWS_MCP_PACKAGE_NAME, runtimeVersionKey: "unversioned", }; } function readWindowsMcpBundleManifest(): WindowsMcpBundleManifest | null { const manifestPath = getWindowsMcpManifestPath(); if (!fs.existsSync(manifestPath)) { return null; } try { const parsed = JSON.parse( fs.readFileSync(manifestPath, "utf-8"), ) as Partial; if ( typeof parsed.packageName !== "string" || typeof parsed.version !== "string" || typeof parsed.resolvedSpec !== "string" || typeof parsed.generatedAt !== "string" || !Array.isArray(parsed.files) ) { return null; } return { packageName: parsed.packageName, version: parsed.version, resolvedSpec: parsed.resolvedSpec, generatedAt: parsed.generatedAt, files: parsed.files.filter( (item): item is string => typeof item === "string", ), }; } catch (error) { log.warn("[WindowsMcp] Failed to read bundle manifest:", error); return null; } } function readRuntimeReceipt( runtimeRoot: string, ): WindowsMcpRuntimeReceipt | null { const receiptPath = getRuntimeReceiptPath(runtimeRoot); if (!fs.existsSync(receiptPath)) { return null; } try { const parsed = JSON.parse( fs.readFileSync(receiptPath, "utf-8"), ) as Partial; if ( parsed.schemaVersion !== 1 || typeof parsed.packageName !== "string" || typeof parsed.resolvedSpec !== "string" || (parsed.version !== null && typeof parsed.version !== "string") || typeof parsed.source !== "string" || typeof parsed.installedAt !== "string" || typeof parsed.runtimeRoot !== "string" || parsed.integrity !== "ok" ) { return null; } return { schemaVersion: 1, packageName: parsed.packageName, version: parsed.version ?? null, resolvedSpec: parsed.resolvedSpec, source: parsed.source as InstallSource, installedAt: parsed.installedAt, runtimeRoot: parsed.runtimeRoot, integrity: "ok", }; } catch { return null; } } function writeRuntimeReceipt( runtimeRoot: string, request: WindowsMcpInstallRequest, source: InstallSource, ): void { const receipt: WindowsMcpRuntimeReceipt = { schemaVersion: 1, packageName: request.packageName, version: request.version, resolvedSpec: request.resolvedSpec, source, installedAt: new Date().toISOString(), runtimeRoot, integrity: "ok", }; fs.writeFileSync( getRuntimeReceiptPath(runtimeRoot), JSON.stringify(receipt, null, 2), "utf-8", ); } function isRuntimeReady( runtimeRoot: string, request: WindowsMcpInstallRequest, ): boolean { const binPath = getRuntimeBinPath(runtimeRoot); if (!fs.existsSync(binPath)) { return false; } const receipt = readRuntimeReceipt(runtimeRoot); if (!receipt) { return false; } if (receipt.packageName !== request.packageName) { return false; } if (receipt.resolvedSpec !== request.resolvedSpec) { return false; } if (request.version && receipt.version !== request.version) { return false; } return true; } function quoteArgForLog(arg: string): string { return arg.includes(" ") ? `"${arg}"` : arg; } function commandForLog(command: string, args: string[]): string { return `${command} ${args.map(quoteArgForLog).join(" ")}`; } async function runCommand( command: string, args: string[], options: { cwd: string; env: Record; }, ): Promise { return new Promise((resolve) => { const child = spawn(command, args, { cwd: options.cwd, env: options.env, shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; let spawnError: string | undefined; child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); child.on("error", (error) => { spawnError = error.message; }); child.on("close", (code) => { resolve({ code: code ?? -1, stdout: stdout.trim(), stderr: stderr.trim(), spawnError, }); }); }); } /** ~/.qimingclaw/uv/python(与 getAppEnv 的 UV_PYTHON_INSTALL_DIR 一致) */ function getUvPythonInstallRoot(): string { return path.join(app.getPath("home"), APP_DATA_DIR_NAME, "uv", "python"); } /** * 与 uv tool 启动器硬编码路径一致:cpython-{major.minor}-windows-x86_64-none(无补丁段)。 * Windows ARM64 上 uv 通常仍安装 x86_64 解释器(仿真),目录名同为 windows-x86_64-none。 */ function getUvToolPythonTriple(): string { if (process.platform !== "win32") { return "x86_64-unknown-linux-gnu"; } return "windows-x86_64-none"; } function getToolShimPythonExePath(version: string): string { const root = getUvPythonInstallRoot(); const triple = getUvToolPythonTriple(); return path.join(root, `cpython-${version}-${triple}`, "python.exe"); } function isToolShimPythonPresent(version: string): boolean { return fs.existsSync(getToolShimPythonExePath(version)); } /** * uv python install 常解压为 cpython-3.13.12-windows-x86_64-none,而 tool.exe 只认 * cpython-3.13-windows-x86_64-none。此时建目录联接,使 shim 路径可用。 */ function tryJunctionShimDirToPatchInstall(version: string): boolean { const root = getUvPythonInstallRoot(); if (!fs.existsSync(root)) { return false; } const triple = getUvToolPythonTriple(); const shimDir = path.join(root, `cpython-${version}-${triple}`); const shimExe = path.join(shimDir, "python.exe"); if (fs.existsSync(shimExe)) { return true; } const escapedTriple = triple.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const patchDirRe = new RegExp( `^cpython-${version.replace(/\./g, "\\.")}\\.\\d+-${escapedTriple}$`, "i", ); let entries: fs.Dirent[]; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch (error) { log.warn( "[WindowsMcp] tryJunctionShimDirToPatchInstall: readdir failed", error, ); return false; } const candidates = entries .filter((e) => e.isDirectory() && patchDirRe.test(e.name)) .map((e) => e.name) .sort() .reverse(); const patchName = candidates[0]; if (!patchName) { return false; } const patchDir = path.join(root, patchName); const patchExe = path.join(patchDir, "python.exe"); if (!fs.existsSync(patchExe)) { return false; } try { if (fs.existsSync(shimDir)) { const st = fs.lstatSync(shimDir); if (st.isDirectory() && !st.isSymbolicLink()) { log.warn( `[WindowsMcp] shim_dir_exists_not_link skip_junction path=${shimDir}`, ); return fs.existsSync(shimExe); } fs.rmSync(shimDir, { recursive: true, force: true }); } } catch (error) { log.warn( "[WindowsMcp] tryJunctionShimDirToPatchInstall: cleanup failed", error, ); return false; } try { fs.symlinkSync(patchDir, shimDir, "junction"); log.info(`[WindowsMcp] uv_python_shim_junction ${shimDir} -> ${patchDir}`); } catch (error) { log.warn( "[WindowsMcp] tryJunctionShimDirToPatchInstall: junction failed", error, ); return false; } return fs.existsSync(shimExe); } /** * uv 生成的 windows-mcp.exe 会查找 UV_PYTHON_INSTALL_DIR 下 **无补丁段** 的目录名; * 仅存在 cpython-3.13.12-... 时也会 exit 103。此处对齐 shim 路径或建 junction。 */ async function ensureUvManagedPythonForWindowsMcp(): Promise { if (isToolShimPythonPresent(WINDOWS_MCP_UV_PYTHON)) { return; } if (tryJunctionShimDirToPatchInstall(WINDOWS_MCP_UV_PYTHON)) { log.info( "[WindowsMcp] uv_managed_python_ready (existing patch -> shim junction)", ); return; } const uvBinPath = getUvBinPath(); if (!fs.existsSync(uvBinPath)) { throw new Error(`Bundled uv not found: ${uvBinPath}`); } const env: Record = { ...getAppEnv({ includeSystemPath: true }), }; delete env.UV_NO_INSTALL; env.UV_PYTHON_DOWNLOADS = process.env.UV_PYTHON_DOWNLOADS ?? env.UV_PYTHON_DOWNLOADS ?? "automatic"; log.info( `[WindowsMcp] uv_managed_python_missing shim=${getToolShimPythonExePath(WINDOWS_MCP_UV_PYTHON)} running: uv python install ${WINDOWS_MCP_UV_PYTHON}`, ); const result = await runCommand( uvBinPath, ["python", "install", WINDOWS_MCP_UV_PYTHON], { cwd: app.getPath("home"), env, }, ); if (result.code !== 0) { throw new Error( buildInstallError( "uv python install failed (windows-mcp needs managed Python under ~/.qimingclaw/uv/python)", uvBinPath, ["python", "install", WINDOWS_MCP_UV_PYTHON], result, ), ); } if (isToolShimPythonPresent(WINDOWS_MCP_UV_PYTHON)) { log.info("[WindowsMcp] uv_managed_python_ready"); return; } if (tryJunctionShimDirToPatchInstall(WINDOWS_MCP_UV_PYTHON)) { log.info("[WindowsMcp] uv_managed_python_ready (shim junction)"); return; } throw new Error( `uv python install finished but tool shim path still missing: ${getToolShimPythonExePath(WINDOWS_MCP_UV_PYTHON)} (see ~/.qimingclaw/uv/python)`, ); } function buildRuntimeWindowsMcpEnv( runtimeRoot: string, options?: { forInstall?: boolean }, ): Record { const env: Record = { ...getAppEnv({ includeSystemPath: true }), ANONYMIZED_TELEMETRY: "false", UV_TOOL_DIR: path.join(runtimeRoot, ".uv-tool"), UV_TOOL_BIN_DIR: path.join(runtimeRoot, "bin"), }; if (!options?.forInstall) { env.UV_NO_INSTALL = "1"; } else { delete env.UV_NO_INSTALL; } log.info( `[WindowsMcp] Using runtime UV dirs: UV_TOOL_DIR=${env.UV_TOOL_DIR}, UV_TOOL_BIN_DIR=${env.UV_TOOL_BIN_DIR}`, ); return env; } async function withInstallLock(task: () => Promise): Promise { const lockPath = path.join(getWindowsMcpDataRoot(), WINDOWS_MCP_LOCK_FILE); fs.mkdirSync(path.dirname(lockPath), { recursive: true }); const startedAt = Date.now(); for (;;) { try { const fd = fs.openSync(lockPath, "wx"); fs.writeFileSync( fd, JSON.stringify( { pid: process.pid, createdAt: new Date().toISOString(), host: os.hostname(), }, null, 2, ), "utf-8", ); try { return await task(); } finally { try { fs.closeSync(fd); } catch { // ignore } try { fs.rmSync(lockPath, { force: true }); } catch { // ignore } } } catch (error) { const err = error as NodeJS.ErrnoException; if (err.code !== "EEXIST") { throw error; } let isStale = false; try { const stat = fs.statSync(lockPath); isStale = Date.now() - stat.mtimeMs > INSTALL_LOCK_STALE_MS; } catch { isStale = true; } if (isStale) { try { fs.rmSync(lockPath, { force: true }); continue; } catch { // ignore and continue waiting } } if (Date.now() - startedAt > INSTALL_LOCK_TIMEOUT_MS) { throw new Error( `Timed out waiting for windows-mcp install lock: ${lockPath}`, ); } await sleep(250); } } } function buildInstallError( title: string, command: string, args: string[], result: CommandResult, ): string { const lines = [ title, `command: ${commandForLog(command, args)}`, `exitCode: ${result.code}`, ]; if (result.spawnError) { lines.push(`spawnError: ${result.spawnError}`); } if (result.stderr) { lines.push(`stderr: ${result.stderr}`); } if (result.stdout) { lines.push(`stdout: ${result.stdout}`); } return lines.join("\n"); } async function installWindowsMcpRuntime( runtimeRoot: string, request: WindowsMcpInstallRequest, ): Promise { const uvBinPath = getUvBinPath(); if (!fs.existsSync(uvBinPath)) { throw new Error(`Bundled uv not found: ${uvBinPath}`); } fs.rmSync(runtimeRoot, { recursive: true, force: true }); fs.mkdirSync(runtimeRoot, { recursive: true }); const env = buildRuntimeWindowsMcpEnv(runtimeRoot, { forInstall: true }); const installCwd = runtimeRoot; const wheelsDir = getWindowsMcpWheelsDir(); const offlineArgs = [ "tool", "install", "--force", "--python", WINDOWS_MCP_UV_PYTHON, "--no-index", "--find-links", wheelsDir, request.resolvedSpec, ]; let offlineError: string | null = null; if (fs.existsSync(wheelsDir) && fs.readdirSync(wheelsDir).length > 0) { log.info( `[WindowsMcp] offline_install_start command=${commandForLog(uvBinPath, offlineArgs)}`, ); const offlineResult = await runCommand(uvBinPath, offlineArgs, { cwd: installCwd, env, }); if (offlineResult.code === 0) { log.info("[WindowsMcp] offline_install_success"); return "offline"; } offlineError = buildInstallError( "Offline install failed", uvBinPath, offlineArgs, offlineResult, ); log.warn(`[WindowsMcp] offline_install_fail\n${offlineError}`); } else { offlineError = `Offline wheels not found: ${wheelsDir}`; log.warn(`[WindowsMcp] offline_install_fail ${offlineError}`); } const onlineArgs = [ "tool", "install", "--force", "--python", WINDOWS_MCP_UV_PYTHON, request.resolvedSpec, ]; log.info( `[WindowsMcp] online_fallback_start offline_failed_then_online=true command=${commandForLog(uvBinPath, onlineArgs)}`, ); const onlineResult = await runCommand(uvBinPath, onlineArgs, { cwd: installCwd, env, }); if (onlineResult.code === 0) { log.info("[WindowsMcp] online_fallback_success"); return "online-fallback"; } const onlineError = buildInstallError( "Online fallback install failed", uvBinPath, onlineArgs, onlineResult, ); log.warn(`[WindowsMcp] online_fallback_fail\n${onlineError}`); throw new Error(`${offlineError}\n\n${onlineError}`); } async function cleanupOldWindowsMcpRuntimes( currentRuntimeRoot: string, ): Promise { const dataRoot = getWindowsMcpDataRoot(); if (!fs.existsSync(dataRoot)) { return; } const entries = fs .readdirSync(dataRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => { const fullPath = path.join(dataRoot, entry.name); let mtimeMs = 0; try { mtimeMs = fs.statSync(fullPath).mtimeMs; } catch { // ignore stat failure } return { fullPath, mtimeMs }; }) .sort((a, b) => b.mtimeMs - a.mtimeMs); const currentNormalized = path.resolve(currentRuntimeRoot); let keptOld = 0; for (const entry of entries) { if (path.resolve(entry.fullPath) === currentNormalized) { continue; } if (keptOld < 1) { keptOld += 1; continue; } try { fs.rmSync(entry.fullPath, { recursive: true, force: true }); log.info(`[WindowsMcp] Removed old runtime: ${entry.fullPath}`); } catch (error) { log.warn("[WindowsMcp] Failed to remove old runtime:", error); } } } async function ensureWindowsMcpRuntime(options?: { forceReinstall?: boolean; }): Promise { const manifest = readWindowsMcpBundleManifest(); const request = buildInstallRequest(manifest); const runtimeRoot = getRuntimeRoot(request); const binPath = getRuntimeBinPath(runtimeRoot); log.info( `[WindowsMcp] runtime_check runtimeRoot=${runtimeRoot} spec=${request.resolvedSpec} forceReinstall=${String(options?.forceReinstall ?? false)}`, ); if (!options?.forceReinstall && isRuntimeReady(runtimeRoot, request)) { return { runtimeRoot, binPath, request }; } return withInstallLock(async () => { if (!options?.forceReinstall && isRuntimeReady(runtimeRoot, request)) { return { runtimeRoot, binPath, request }; } const source = await installWindowsMcpRuntime(runtimeRoot, request); writeRuntimeReceipt(runtimeRoot, request, source); await cleanupOldWindowsMcpRuntimes(runtimeRoot); if (!fs.existsSync(binPath)) { throw new Error( `windows-mcp executable not found after install: ${binPath}`, ); } return { runtimeRoot, binPath, request }; }); } function isTrampolineScriptPathError(errorMessage: string): boolean { const lower = errorMessage.toLowerCase(); return ( lower.includes("uv trampoline failed to canonicalize script path") || lower.includes("failed to canonicalize script path") ); } function getWindowsMcpManager(): WindowsMcpManagerType { if (!isWindows()) { throw new Error("WindowsMcpManager is only available on Windows"); } if (!windowsMcpManager) { const resourcesPath = getResourcesPath(); const libBundlePath = path.join( resourcesPath, "agent-gui-server", "dist", "lib.bundle.cjs", ); const { WindowsMcpManager: WMM } = runtimeRequire(libBundlePath) as { WindowsMcpManager: new (cfg: { healthCheckInterval: number; startupTimeout: number; healthCheckTimeout: number; maxRestarts: number; }) => WindowsMcpManagerType; }; windowsMcpManager = new WMM({ healthCheckInterval: 30000, startupTimeout: 30000, healthCheckTimeout: 5000, maxRestarts: 3, }); processRunner = new ElectronProcessRunner(); windowsMcpManager.setProcessRunner(processRunner); log.info( "[WindowsMcp] WindowsMcpManager initialized from bundled agent-gui-server", ); } return windowsMcpManager; } async function startWindowsMcpInternal(allowSelfHeal: boolean): Promise<{ success: boolean; port?: number; error?: string; }> { const runtime = await ensureWindowsMcpRuntime(); try { await ensureUvManagedPythonForWindowsMcp(); } catch (error) { const message = error instanceof Error ? error.message : String(error); log.error(`[WindowsMcp] ensureUvManagedPythonForWindowsMcp: ${message}`); return { success: false, error: message }; } const buildConfig = (_port: number): ProcessConfig => ({ command: runtime.binPath, args: [ "--transport", "streamable-http", "--host", "127.0.0.1", "--port", getGuiMcpPort().toString(), ], cwd: path.dirname(runtime.binPath), env: buildRuntimeWindowsMcpEnv(runtime.runtimeRoot), }); const port = getGuiMcpPort(); try { log.info(`[WindowsMcp] Pre-start port sweep for ${port}...`); await killProcessTreesListeningOnTcpPort(port); await sleep(450); } catch (error) { log.warn("[WindowsMcp] Pre-start port sweep:", error); } log.info(`[WindowsMcp] Starting (runtime) on port ${port}...`); const result = await getWindowsMcpManager().start(port, buildConfig); if (result.success) { log.info(`[WindowsMcp] Started successfully on port ${result.port}`); return result; } const err = result.error ?? ""; log.error(`[WindowsMcp] Failed to start: ${err}`); if (allowSelfHeal && isTrampolineScriptPathError(err)) { log.warn( "[WindowsMcp] self_heal_triggered reason=canonicalize_script_path", ); try { await getWindowsMcpManager().stop(); } catch { // ignore } try { await ensureWindowsMcpRuntime({ forceReinstall: true }); const retry = await startWindowsMcpInternal(false); log.info( `[WindowsMcp] self_heal_retry_result success=${String(retry.success)} error=${retry.error ?? ""}`, ); if (retry.success) { return retry; } return { success: false, error: `${err}\nSelf-heal retry failed: ${retry.error ?? "unknown error"}`, }; } catch (selfHealError) { const message = selfHealError instanceof Error ? selfHealError.message : String(selfHealError); log.error(`[WindowsMcp] self-heal failed: ${message}`); return { success: false, error: `${err}\nSelf-heal failed: ${message}`, }; } } const likelyPortConflict = err.includes("10048") || err.includes("EADDRINUSE") || err.includes("Address already in use") || err.includes("ready within timeout"); if (likelyPortConflict) { return { success: false, error: `${err} — ${t("Claw.WindowsMcp.portInUseHint", { port: String(port) })}`, }; } return result; } export async function startWindowsMcp(): Promise<{ success: boolean; port?: number; error?: string; }> { if (!isWindows()) { log.info("[WindowsMcp] Skipped: not Windows platform"); return { success: true }; } const status = getWindowsMcpManager().getStatus(); if (status.running) { return { success: true, port: status.port }; } try { return await startWindowsMcpInternal(true); } catch (error) { const message = error instanceof Error ? error.message : String(error); log.error(`[WindowsMcp] Failed before start: ${message}`); return { success: false, error: message }; } } export async function stopWindowsMcp(): Promise<{ success: boolean; error?: string; }> { if (!isWindows()) { return { success: true }; } log.info("[WindowsMcp] Stopping..."); const result = await getWindowsMcpManager().stop(); try { await killProcessTreesListeningOnTcpPort(getGuiMcpPort()); } catch (error) { log.warn("[WindowsMcp] TCP port sweep after stop:", error); } if (result.success) { log.info("[WindowsMcp] Stopped successfully"); } else { log.error(`[WindowsMcp] Failed to stop: ${result.error}`); } return result; } export function getWindowsMcpStatus() { if (!isWindows()) { return { running: false }; } return getWindowsMcpManager().getStatus(); } export function getWindowsMcpUrl(): string | null { if (!isWindows()) { return null; } const status = getWindowsMcpManager().getStatus(); if (!status.running || status.port === undefined) { return null; } return `http://127.0.0.1:${status.port}/mcp`; } export function isWindowsMcpAvailable(): boolean { if (!isWindows()) { return false; } const uvBinPath = getUvBinPath(); const bundleRoot = getWindowsMcpBundleRoot(); return fs.existsSync(uvBinPath) && fs.existsSync(bundleRoot); }