{"id":"cac18ad0-5d50-457c-963e-dc9e8150bf66","slug":"win-mcp-pro","kind":"mcp","name":"win-mcp-pro","summary":"Windows UI Automation MCP server. Inspect controls, use control patterns, and manage windows from an MCP client.","owner":"danzaio","repoUrl":"https://github.com/danzaio/win-mcp-pro","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-10T04:40:52.000Z","addedAt":"2026-10-28T09:09:31.086Z","logoUrl":"https://avatars.githubusercontent.com/u/213864024?u=673f50adfdc216e620cbdc5958ff21e1c62f2b73&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","design-media","memory-knowledge"],"tags":["desktop-automation","python","uiautomation","windows","claude","computer-use","llm-agents","windows-automation"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-28T09:55:34.479Z","sample":false,"url":"https://conduits.dev/tools/win-mcp-pro","language":"Python","transports":["stdio"],"install":{"pypi":"win-mcp-pro"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<p align=\"center\">\n  <img src=\"docs/diagrams/hero.svg\" alt=\"win-mcp-pro: a small Windows MCP server you can read in an afternoon\" width=\"100%\">\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/danzaio/win-mcp-pro/actions/workflows/ci.yml\"><img alt=\"ci\" src=\"https://github.com/danzaio/win-mcp-pro/actions/workflows/ci.yml/badge.svg\"></a>\n  <img alt=\"Windows 10 | 11\" src=\"https://img.shields.io/badge/Windows-10%20%7C%2011-0078D4\">\n  <img alt=\"Python 3.11+\" src=\"https://img.shields.io/badge/Python-3.11%2B-3776AB\">\n  <a href=\"LICENSE\"><img alt=\"MIT\" src=\"https://img.shields.io/badge/License-MIT-green\"></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"#quick-start\">Quick start</a> ·\n  <a href=\"#see-it-work\">Demo</a> ·\n  <a href=\"#how-a-click-is-checked\">The gate</a> ·\n  <a href=\"#safety-model\">Safety</a> ·\n  <a href=\"#tools\">Tools</a> ·\n  <a href=\"#proof\">Proof</a> ·\n  <a href=\"#limits\">Limits</a>\n</p>\n\n**An MCP server that lets an agent drive Windows apps through UI Automation,\nand refuses to act on anything it has not verified.** It can act as you, so it\nis built to be read before you trust it.\n\n| Small | Bound | Provable |\n| --- | --- | --- |\n| 6.0k lines of Python in 21 files. Read it in an afternoon. | Every coordinate input is tied to a fresh capture and the element it targets. A failed check sends nothing. | A bench drives disposable windows and checks each effect in the app's own log, not in the server's reply. |\n\n## Quick start\n\nRead-only first. This exposes inspection and waiting only: no clicks, no\ntyping, no clipboard, no files.\n\n```powershell\nuvx --from git+https://github.com/danzaio/win-mcp-pro win-mcp-pro --read-only\n```\n\nNeeds Windows 10 or 11 and [uv](https://docs.astral.sh/uv/). Python 3.11 or\nnewer is enough; uv fetches it if you do not have it.\n\n**Claude Code**\n\n```powershell\nclaude mcp add win-mcp-pro -- uvx --from git+https://github.com/danzaio/win-mcp-pro win-mcp-pro --read-only\n```\n\n**Claude Desktop, Cursor, or any stdio client**: add this to the client's MCP\nconfig (Claude Desktop: `%APPDATA%\\Claude\\claude_desktop_config.json`).\n\n```json\n{\n  \"mcpServers\": {\n    \"win-mcp-pro\": {\n      \"command\": \"uvx\",\n      \"args\": [\"--from\", \"git+https://github.com/danzaio/win-mcp-pro\", \"win-mcp-pro\", \"--read-only\"]\n    }\n  }\n}\n```\n\nTo let the client act, replace `--read-only` with an explicit list, for example\n`--tools list_windows,find,get_value,set_value,invoke`.\n\n## See it work\n\n<p align=\"center\">\n  <img src=\"docs/diagrams/demo.svg\" alt=\"A looping replay of a real session against Notepad: list windows, find the Edit control, set and verify its value, then a blind click refused with FRAME_REQUIRED\" width=\"100%\">\n</p>\n\nA real session against Notepad, driven by an MCP client. Output is trimmed with\n`...`; nothing else is changed. `set_value` reads the control back and says\nwhether it took. The blind `click` is refused before any event is sent.\n\n<details>\n<summary>Plain-text transcript</summary>\n\n```text\ntools/list   -> ['click', 'find', 'get_value', 'list_windows', 'set_value']\nlist_windows -> {\"hwnd\": 20449106, \"title\": \"Sem título - Bloco de Notas\", \"pid\": 75792, \"visible\": true}\nfind Edit    -> {\"ok\": true, \"element\": {\"id\": \"20449106:42,46794936\", \"kind\": \"Edit\", ...}}\nset_value    -> {\"ok\": true, \"before\": \"\", \"after\": \"hello from an MCP client\", \"verified\": true}\nget_value    -> {\"ok\": true, \"value\": \"hello from an MCP client\"}\nclick x=10 y=10 with no frame or target ->\n  {\"ok\": false, \"error\": \"FRAME_REQUIRED\", \"reason\": \"coordinate input needs the frame id\n   from a fresh screenshot/zoom (pass frame=..., or start the server with\n   --input-unrestricted for raw desktop coordinates)\"}\n```\n\nThe machine runs Windows in Portuguese, so the window is titled\n`Sem título - Bloco de Notas`.\n\n</details>\n\n## How it works\n\n![An MCP client talks to win-mcp-pro over stdio; the server reads and drives app controls through Windows UI Automation.](docs/diagrams/architecture.svg)\n\nA typical flow is `list_windows`, then `snapshot` or `find`, then an action,\nthen read the control again to confirm. The client asks for things by element\nid. The server answers with what it observed, not with what it hoped happened.\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant C as MCP client\n    participant S as win-mcp-pro\n    participant U as UI Automation\n    participant A as Notepad\n    C->>S: find(Edit)\n    S->>U: walk the tree\n    U-->>S: element 20449106:42,...\n    S-->>C: ok, element id\n    C->>S: set_value(id, \"hello from an MCP client\")\n    S->>U: ValuePattern.SetValue\n    U->>A: new text\n    S->>U: read the control back\n    S-->>C: ok, after, verified true\n    C->>S: click(x=10, y=10)\n    S--xC: FRAME_REQUIRED, 0 input events sent\n```\n\n## How a click is checked\n\nCoordinates are the fallback, not the default, so they get the strictest\ntreatment. A coordinate click has to pass every gate below. Any failure returns\na typed error **before** a single input event is sent.\n\n```mermaid\nflowchart TD\n    A([\"click, double_click, move ...<br/>with coordinates\"]) --> F{\"frame id given?\"}\n    F -- no --> R1[[\"FRAME_REQUIRED\"]]\n    F -- yes --> F2{\"frame known<br/>and under 30 s old?\"}\n    F2 -- unknown --> R2[[\"FRAME_UNKNOWN\"]]\n    F2 -- stale --> R3[[\"FRAME_STALE\"]]\n    F2 -- fresh --> T{\"target id given?\"}\n    T -- no --> R4[[\"TARGET_REQUIRED\"]]\n    T -- yes --> T2{\"target is a<br/>live element?\"}\n    T2 -- no --> R5[[\"TARGET_NOT_FOUND<br/>TARGET_STALE\"]]\n    T2 -- yes --> P{\"point inside the<br/>target's rectangle?\"}\n    P -- no --> R6[[\"TARGET_MISMATCH\"]]\n    P -- yes --> E{\"target runs at a higher<br/>integrity level?\"}\n    E -- yes --> R7[[\"ELEVATION_REQUIRED<br/>INTEGRITY_CHECK_FAILED\"]]\n    E -- no --> O{\"does the window at that<br/>point belong to the target?\"}\n    O -- no --> R8[[\"OCCLUDED\"]]\n    O -- yes --> S([\"SendInput\"])\n\n    classDef refuse fill:#ffe3e3,stroke:#e03131,color:#1a1a1a\n    classDef send fill:#d3f9d8,stroke:#2f9e44,color:#1a1a1a\n    class R1,R2,R3,R4,R5,R6,R7,R8 refuse\n    class S send\n```\n\n- A call that names only a target clicks its center, and still goes through the\n  live-element, elevation and occlusion checks.\n- Anything the caller provides is verified, even on a plain `move`.\n- `--input-unrestricted` turns the coordinate checks off. It is off by default.\n- `tests/test_input_binding.py` proves each refusal with a spy on `SendInput`.\n\n## Safety model\n\nWhat a client can see is decided before any call arrives. Filtered-out tools are\nabsent from `tools/list` and answer `UNKNOWN_TOOL` if called anyway.\n\n```mermaid\nflowchart LR\n    A[\"54 registered tools\"] --> B{\"--read-only?\"}\n    B -- yes --> C[\"17 inspect and wait tools\"]\n    B -- no --> D{\"--tools a,b,c?\"}\n    D -- yes --> E[\"only the listed tools\"]\n    D -- no --> F[\"all 54 tools\"]\n    C --> G{\"--exclude-tools x,y?\"}\n    E --> G\n    F --> G\n    G --> H[\"tools/list<br/>what the client can call\"]\n```\n\n| Flag | Effect |\n| --- | --- |\n| `--read-only` | Inspection and waiting only (17 tools). Cannot be combined with `--tools`. |\n| `--tools a,b,c` | Only these tools exist for the client. |\n| `--exclude-tools x,y` | Everything except these. Wins over `--tools`. |\n| `--input-unrestricted` | Allow raw coordinates and unbound keys. Off by default. |\n| `--uia-worker` | Run tree walks in a child process that is killed on a stuck call. |\n\nAn empty list is rejected rather than read as \"allow everything\".\n\n**This is a policy, not a sandbox.** An allowed tool runs with your\npermissions. `confirm=True` on a tool is an argument the client supplies, not\na human approval prompt. See [SECURITY.md](SECURITY.md).\n\nLogs go to stderr as JSON lines: tool names and argument names, never values.\n\n## Tools\n\n54 tools. Your client gets each schema from `tools/list`.\n\n```mermaid\npie showData title 54 tools by group\n    \"Inspect\" : 9\n    \"Act through UIA patterns\" : 11\n    \"Input by coordinates\" : 10\n    \"Wait\" : 6\n    \"Windows and apps\" : 10\n    \"Pixels\" : 6\n    \"Flows and health\" : 2\n```\n\n| Group | Tools |\n| --- | --- |\n| Inspect | `list_windows` `get_active_window` `snapshot` `find` `dump_tree` `get_prop` `get_patterns` `get_value` `table_extract` |\n| Act through UIA patterns | `invoke` `toggle` `select` `expand` `scroll_into_view` `focus` `set_value` `paste_text` `menu_click` `form_fill` `choose_file` |\n| Input by coordinates | `click` `double_click` `move` `scroll` `shortcut` `type_text` `key_hold` `cursor_position` `clipboard_get` `clipboard_set` |\n| Wait | `wait` `wait_text` `wait_focus` `wait_enabled` `wait_gone` `wait_change` |\n| Windows and apps | `app_launch` `open_default` `win_move` `win_resize` `win_min` `win_max` `win_restore` `win_snap` `win_close` `exclude_proc` |\n| Pixels | `screenshot` `zoom` `win_shot` `displays` `ocr_read` `ocr_click` |\n| Flows and health | `replay` `doctor` |\n\n**UIA patterns before pixels.** The client reads the accessibility tree and\nuses `invoke`, `set_value` and friends. Coordinates are the fallback, and they\nmust be tied to a fresh capture and the element they intend. `replay` re-runs a\nrecorded flow and stops at the first step whose result differs (`DRIFT`).\n\n## Why another one\n\n<p align=\"center\">\n  <img src=\"docs/diagrams/size.svg\" alt=\"Bar chart: win-mcp-pro has 6.0k lines of Python, 21 files and 54 tools; Windows-MCP v0.8.8 has 24.0k lines, 80 files and 21 tools.\" width=\"100%\">\n</p>\n\nThe most-used Windows MCP server,\n[CursorTouch/Windows-MCP](https://github.com/CursorTouch/Windows-MCP), is about\n24k lines of Python and needs Python 3.14+. This one aims to be small enough to\nread before you let it drive your desktop, and runs on 3.11+.\n\nSource size is lines of Python excluding blanks and comments, counted on\n2026-10-20. It is a measure of how much you have to read, not of quality.\nThis one has **more** tools, not fewer: most are small and single-purpose, and\nthat is a larger surface to review. It is why `--read-only` and `--tools`\nexist. Windows-MCP is the more widely used project and has features this one\ndoes not, including a desktop overlay and HTTP transports.\n\n## Proof\n\nThe unit suite runs against fakes (410 tests), so it never touches your\ndesktop. CI builds the wheel on Windows, installs it in a clean environment and\nstarts it. On top of that, `bench/run_matrix.py` is a driver-conformance matrix\nwith an outside witness: it checks each effect in the fixture's own log, never\nin the server's reply.\n\n```mermaid\nflowchart LR\n    R[\"bench runner<br/>(an MCP client)\"] -- \"tools/call over stdio\" --> S[\"win-mcp-pro\"]\n    S -- \"UIA and SendInput\" --> F[\"fixture app<br/>WinForms or Tk\"]\n    F -- \"writes its own log\" --> L[(\"fixture JSONL log\")]\n    R -. \"asserts against the log,<br/>not the reply\" .-> L\n    S -. \"wrong target?\" .-> D[\"decoy window<br/>other pid\"]\n    D --> DL[(\"decoy log<br/>must stay empty\")]\n    R -. \"asserts\" .-> DL\n```\n\n28 tasks cover identity lookup, `set_value` with read-back, `invoke`,\nframe-pinned clicks, refusals, wrong-target leakage, OCR, replay, a real Open\ndialog, and a stepped drag. The first full run failed 6 of 11 and exposed four\nreal bugs, each fixed with a regression test. Details and honest scope are in\n[bench/README.md](bench/README.md).\n\n## Limits\n\n- It only sees what an app exposes through UI Automation. Games, canvases and\n  some Electron or custom-drawn UIs expose little. `ocr_read` reads pixels as a\n  fallback; the server never switches to screenshots on its own. OCR needs\n  `winsdk`, which has no wheels for Python 3.13 yet, so on 3.13 and newer\n  `ocr_read` and `ocr_click` answer `UNAVAILABLE` and everything else works.\n- An unelevated server cannot send input to an elevated window (Windows UIPI).\n  It reports `ELEVATION_REQUIRED`. Run it unelevated unless you must.\n- Time budgets are checked between UIA calls. They cannot interrupt a COM call\n  that is already blocked; `--uia-worker` exists for that.\n- stdio only, one local client. No network transport.\n- Tested on Windows 10 Pro (build 19045) with Notepad and disposable WinForms\n  and Tk fixtures. Windows 11 is supported by the API but untested by me. That\n  is not a claim that every application works.\n\n<details>\n<summary>Troubleshooting</summary>\n\n- **A tool is missing in the client.** Check `--tools`, `--exclude-tools` and\n  `--read-only` first, then list what is registered:\n  `python -c \"from win_mcp_pro import tools; tools.load_all_modules(); print(sorted(tools.names()))\"`\n- **The client says the server died right after start.** Something wrote to\n  stdout, which is the protocol channel. Logs belong on stderr.\n- **`find` returns `NOT_FOUND` for something you can see.** The app probably\n  has no UI Automation provider. Check with Inspect.exe from the Windows SDK or\n  Accessibility Insights before blaming the query.\n- **`ELEVATION_REQUIRED`.** The target runs elevated and the server does not.\n  Restart the server elevated, or pick another target.\n\n</details>\n\n## Repo map\n\n```text\nwin-mcp-pro/\n├── win_mcp_pro/\n│   ├── server/        MCP wiring: CLI, tool registry and filter, UIPI check,\n│   │                  --uia-worker supervisor, stderr audit log\n│   └── tools/         one module per area, each exposes TOOLS\n│       ├── compat.py      coordinate input, clipboard, and the gate every click passes\n│       ├── patterns.py    invoke, toggle, select, expand, focus, scroll_into_view\n│       ├── query.py       find, dump_tree, get_prop, get_patterns\n│       ├── snapshot.py    snapshot\n│       ├── values.py      get_value, set_value, paste_text, menu_click, form_fill, ...\n│       ├── waits.py       the wait_* family\n│       ├── winproc.py     windows, processes, app launch\n│       ├── vision.py      screenshot, zoom, win_shot, displays, capture frames\n│       ├── ocr.py         ocr_read, ocr_click\n│       ├── replay.py      re-run a recorded flow, stop at the first DRIFT\n│       └── doctor.py      environment check\n├── tests/             410 tests against fakes; never touch your desktop\n├── bench/             28-task conformance matrix with an external oracle\n├── docs/diagrams/     the images used in this README\n└── .github/           CI, publish workflow, issue and PR templates\n```\n\n## Develop\n\n```powershell\ngit clone https://github.com/danzaio/win-mcp-pro.git\ncd win-mcp-pro\npython -m venv .venv\n.venv\\Scripts\\python.exe -m pip install -e \".[dev]\"\n.venv\\Scripts\\python.exe -m pytest -q\n```\n\nThe bench does touch your desktop and moves your cursor: run it on an idle\nmachine.\n\n```powershell\n.venv\\Scripts\\python.exe bench\\run_matrix.py\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md) and\n[CHANGELOG.md](CHANGELOG.md). Licensed under [MIT](LICENSE).\n"}