Skip to content

Run natively on Windows (1.6.0) - #28

Draft
simonether wants to merge 4 commits into
mainfrom
windows-support
Draft

simonether wants to merge 4 commits into
mainfrom
windows-support

Conversation

@simonether

@simonether simonether commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

On Windows uvx kwork-mcp failed at import: the state directory, token store and write ledger relied on fcntl, POSIX modes and uids. This adds native Windows support without WSL. macOS and Linux behave as before.

All platform code goes through the new private_fs module, with the Win32 calls in windows.py (ctypes, no new dependencies):

  • State directories are created by CreateDirectoryW with an owner-only protected DACL (current user and SYSTEM), so a directory never exists with its parent's permissions. An existing directory passes only if its owner is the user, SYSTEM or Administrators and its DACL, inheritable entries included, lets no one else in. OWNER RIGHTS counts as the owner, since os.mkdir(path, 0o700) grants it. Outside the profile, no one else may rename or re-permission the directories above the state directory, the counterpart of the POSIX check on shared-writable ancestors. A junction or symlink as the state directory, UNC and device paths, alternate data streams and reserved names are rejected.
  • Token and lock files are checked by owner and DACL through their open handles, and the token also against the file that passed lstat. The SQLite ledger is checked by path before each connection, as on POSIX.
  • Tokens are sealed with DPAPI for the current Windows user, with the account scope as entropy: another user cannot read or plant one, and a file copied to another account's name does not open. A record DPAPI cannot open (reset Windows password, moved profile) answers auth_expired, and a new login replaces it. On POSIX the record stays plain JSON under 0600, so existing stores need no migration.
  • Locks use msvcrt.locking. The token is replaced by MoveFileExW with write-through, since Windows has no directory fsync. Renames and unlinks wait up to a second when an antivirus or the indexer holds a fresh file.
  • State stays in ~/.local/state/kwork-mcp under the profile (%USERPROFILE%), not in AppData: packaged (MSIX) clients such as Claude Desktop see a per-app copy of AppData, so the server they start and the terminal would split the ledger and locks.
  • login warns when run elevated (that may be another Windows account), points runs outside a console (Git Bash's mintty) to PowerShell, and prints cmd/PowerShell quoting, the Cursor config path and the Edit Config hint for Claude Desktop. Both entry points write UTF-8 to redirected stdout and stderr: in a pipe the cp1252 default crashed --help on the Russian text.

Two independent reviews (Win32 calls against the Microsoft docs; POSIX regressions, tests and docs) found no POSIX regressions; their findings are fixed here. CI gets mypy --platform win32 in the quality job and a Windows job (Python 3.12 and 3.14 on windows-2025) that runs the suite and the wheel smoke test. Coverage stays gated on Linux, where the Windows branches and windows.py are excluded. Tests of POSIX-only behavior (modes, umask, symlink walks, directory fsync) are skipped on Windows; tests/test_windows_support.py covers the Windows side with real ACL, junction, DPAPI and lock checks.

README, site and docs describe the Windows setup. Version 1.6.0.

Not checked yet: an interactive kwork-mcp login and a real client on a Windows machine, which CI cannot do without a terminal. To try the branch before release:

uvx --from https://github.com/simonether/kwork-mcp/archive/refs/heads/windows-support.zip kwork-mcp login

@simonether simonether changed the title Run natively on Windows Run natively on Windows (1.6.0) Oct 3, 2026
@simonether
simonether force-pushed the windows-support branch 3 times, most recently from 8ba0831 to e85eab2 Compare October 4, 2026 00:29
On Windows `uvx kwork-mcp` failed at import: the state directory, token
store and write ledger relied on fcntl, POSIX modes and uids.

Platform code now lives in private_fs, with the Win32 calls in windows.py
(ctypes, no new dependencies). Windows creates state directories with an
owner-only protected DACL, accepts an existing one only if its owner and
DACL, inheritable entries included, admit no one but the user, SYSTEM and
Administrators, and outside the profile requires that no one else can
rename or re-permission the directories above it, as POSIX rejects
shared-writable ancestors. It rejects junctions and ambiguous path syntax,
checks token and lock files through their handles and the ledger before
each connection, seals tokens with DPAPI bound to the account (a record it
cannot open asks for a new login), locks with msvcrt and replaces the
token with a write-through rename. State stays in ~/.local/state/kwork-mcp
under the profile, not in AppData, which packaged (MSIX) clients see
virtualized. POSIX behavior and the token format are unchanged.

login warns when run elevated, points non-console runs to PowerShell,
prints cmd/PowerShell quoting, the Cursor config path and the Edit Config
hint for Claude Desktop, and both entry points write UTF-8 to redirected
output. CI type-checks the Windows branches and runs the suite and the
wheel smoke test on windows-2025.
Claude Desktop's Edit Config only reveals claude_desktop_config.json, so
connecting it meant editing JSON by hand, worst of all on Windows.

After the account is bound, login now finds Claude Desktop (macOS,
Windows including the Microsoft Store build with its own AppData copy,
community Linux builds) and Cursor, asks, and adds the server entry with
the absolute uvx path. Other servers and settings stay as they are, the
previous file is kept as .bak, a config that is not plain JSON is never
touched, and another server under the same name is replaced only after a
separate question. The manual block is printed only for clients that
were not connected.
On Windows the first login could not replace the Microsoft Store Claude
Desktop config, while the same write succeeded a moment later: something
held the file briefly. When the rename is refused, the config is now
rewritten in place after the backup, and a failure names its error class
and Windows error code instead of a bare 'could not write'.

This branch was successfully deployed

1 active (outdated) deployment
pypi — b3e402ab Deployed Oct 4, 2026 by simonether via Publish to PyPI #25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant