Skip to main content
Glama
nhantruong96

uia-mcp

by nhantruong96

uia-mcp

MCP server điều khiển Windows qua cây trợ năng UI Automation — cùng API mà NVDA và Narrator dùng để đọc màn hình cho người khiếm thị.

Model không nhìn pixel và không đoán toạ độ: nó đọc một bảng ID, rồi tác động lên ID bằng control pattern của chính ứng dụng.

Thứ tự ưu tiên — không được đảo

1. MCP riêng của ứng dụng (API)   revit-mcp · Excel-MCP · Navisworks · Outlook · ACC · SAP2000
   nhanh nhất, tin cậy nhất, không cần ứng dụng mở cửa sổ
2. UIA — server này               cho mọi ứng dụng còn lại
3. screenshot()                   CHỈ khi bậc 1 và 2 đều bó tay

Bậc 3 là lối thoát cuối, không phải công cụ mặc định. Bậc 1 và 2 cho biết trạng thái thật — enabled, on/off, giá trị ô nhập — mà ảnh không có, và rẻ hơn hàng nghìn token mỗi lần nhìn. Ảnh chỉ đáng dùng với bề mặt vẽ trên canvas: viewport 3D của Revit, canvas AutoCAD, lưới Excel Online, game.

Kỷ luật đó được mã hoá vào chính tool, không chỉ nằm trong tài liệu: screenshot() có tham số tried bắt buộc, khai qua loa thì bị từ chối kèm lời nhắc thử hai bậc trên trước.

Mọi con số đo đạc trong tài liệu này đều lấy từ máy thật, không phải ước lượng.


Related MCP server: uiautomation-mcp

Cài đặt

git clone https://github.com/nhantruong96/uia-mcp
cd uia-mcp
uv venv
uv pip install -e .

Yêu cầu: Windows 10/11, Python ≥ 3.11.

Kiểm tra bằng bộ test tự mở và tự đóng Notepad của riêng nó:

.venv\Scripts\python.exe smoke_test.py

Đăng ký với Claude Code

claude mcp add uia --scope user -- <đường-dẫn-repo>\.venv\Scripts\uia-mcp.exe

Bộ tool

Tool

Công dụng

list_windows()

Mọi cửa sổ top-level: hwnd, class, pid, tiêu đề, cái nào đang focus

observe(window, filter, max_elements)

Bảng id|type|name|state|patterns của một cửa sổ

find(query, window, by, limit, include_offscreen, within)

Tìm phần tử ở bất kỳ độ sâu nào, không bị max_elements cắt

landmark_capture(window)

Quét một lần, ghi toạ độ mọi phần tử có tên của app đó

landmark_find(query, window, type, activate)

Trỏ bằng điểm neo — mili giây thay vì giây

landmark_status(window)

Đã có bao nhiêu điểm neo, chụp lúc nào, cửa sổ có dịch chuyển không

describe_at(x, y)

Phần tử dưới một điểm màn hình (~2 ms)

read_text(id, max_chars)

Nội dung văn bản qua TextPattern — thay OCR

read_table(id, start_row, max_rows)

Bảng/lưới qua GridPattern, có phân trang

act(id, action, value)

Tác động qua control pattern, có thang fallback

press_key(combo)

Phím tắt toàn cục, ví dụ ctrl+s

notifications(limit, app, kind, since_hours)

Thông báo Windows, đọc thẳng kho SQLite — không mở panel

screenshot(tried, window)

Lối thoát cuối — chụp PNG, bắt khai đã thử gì trước đó

observe trả về gì

window: 'Untitled - Notepad' hwnd=919222 class=Notepad pid=27820
# id|type|name|state|patterns
0|Document|Text editor|focused|value,scroll,text
3|TabItem|Untitled. Unmodified.|selected|select
5|Button|Add New Tab||invoke
6|MenuItem|File|collapsed|invoke,expand
11|Button|Bold (Ctrl+B)|off|toggle

filter: interactive (mặc định) · text (nội dung đọc được) · table (lưới/bảng — dùng trước read_table) · all (mọi thứ đang hiển thị).

Cột state chứa đúng những thứ ảnh chụp màn hình không nói chắc được: focused, disabled, on/off, selected, expanded/collapsed, giá trị thật của ô nhập, phần trăm cuộn. Cột patterns cho biết phần tử chấp nhận hành động nào.

find — khi observe không với tới

observe cắt theo thứ tự duyệt cây, nên thứ nằm sâu không bao giờ tới lượt. Đo trên máy này: bảng thư Outlook là phần tử 196/325, tab thật của Edge nằm dưới EdgeTabStrip. Tăng max_elements chỉ tổ đổ cả cây vào context. find không bị giới hạn đó.

by:

auto (mặc định)

khớp name, AutomationId hoặc ClassName

name · automation_id · class

chỉ trường đó

type

control type chính xác: TabItem, Document, Table, Edit

pattern

phần tử có pattern đó: grid, scroll, value, toggle

find("EdgeTab", window=<edge>, by="class")
→ find('EdgeTab', by=class): 7 kết quả, 1 khớp chính xác (xếp đầu)
  0|TabItem|Product Development - Materials - SAP Business ByDesign…|selected|select
  5|Button|Close tab [Ctrl+W]||invoke          ← EdgeTabCloseButton, khớp tiền tố

find("grid", window=<outlook>, by="pattern")
→ 7|Table|Table View|scroll-v=1%|scroll,grid   ← rồi read_table(7)

Khớp chính xác luôn xếp trước khớp một phần, và header nói rõ có bao nhiêu cái chính xác — tìm class EdgeTab cũng trúng EdgeTabCloseButton, EdgeTabStrip, nên cần biết tin dòng nào.

within=<id> thu phạm vi về một nhánh con. Với trình duyệt thì gần như bắt buộc: tìm trong cả cửa sổ sẽ lẫn toàn bộ chrome của Edge vào kết quả của trang web. Đo trên SAP Business ByDesign: find("Button", by="type") cho 38 kết quả trên cả cửa sổ, còn within=<id Document> chỉ còn 19 — đúng các nút của trang.

read_table — phân trang và lưới ảo hoá

start_row để đọc tiếp bảng dài; khi start_row > 0, hàng 0 vẫn được kèm theo vì ở nhiều lưới đó là hàng tiêu đề cột.

RowCount là số logic, không phải số hàng có dữ liệu. Lưới của SAP khai báo 10000×6 nhưng chỉ ~11 hàng đang render là có nội dung — phần còn lại rỗng hoàn toàn. Tool không im lặng bỏ qua chúng, mà nói thẳng:

(3/3 hàng đọc được là rỗng — lưới ảo hoá, chỉ vùng đang render mới có dữ liệu.
 RowCount=10000 là số logic, không phải số hàng thật đang có.)

Muốn đọc sâu hơn thì phải act(<id lưới>, "scroll", "down") trước rồi đọc lại.

Điểm neo — cho app nặng dùng thường xuyên

Thứ đắt là duyệt cây, không phải bộ lọc. Đo trên Revit 2027 có model mở:

Quét toàn cây (701 phần tử)

16 325 ms

FindAll đã lọc theo control type

6 245 ms

find("Annotate")

11 477 ms

landmark_find("Annotate")

36 ms

landmark_find("S100 - Foundation")

6 ms

Trả một lần quét ~9–16 s, đổi lấy mọi lần trỏ sau đó nhanh gấp ~300 lần.

landmark_capture(window="<hwnd Revit>")   → đã ghi 210 điểm neo (quét mất 9 280 ms)
landmark_find("Annotate", activate=True)  → giải bằng điểm neo tại (686, 57)   36 ms

Kho lưu ở %LOCALAPPDATA%\uia-mcp\landmarks.json, khoá theo tên tiến trình (revit.exe) chứ không theo tiêu đề — tiêu đề Revit đổi theo model đang mở.

Hai điều bắt buộc phải hiểu:

Điểm neo là toạ độ màn hình. ElementFromPoint luôn trả về thứ nằm trên cùng tại điểm đó, bất kể ta định nói tới cửa sổ nào. Nếu Revit bị Edge che, điểm neo của Revit sẽ trỏ vào Edge. Vì vậy mặc định tool từ chối khi cửa sổ đích không ở trên cùng; truyền activate=True để đưa nó lên trước.

Mỗi lần dùng đều kiểm chứng lại danh tính. Giải xong vẫn phải xác nhận phần tử dưới toạ độ đó đúng là mục tiêu (hoặc con cháu của nó) rồi mới trả về. Điểm neo cũ bị từ chối kèm mô tả thứ đang nằm ở đó, chứ không bao giờ trả về nhầm phần tử — click nhầm sang ứng dụng khác là tai nạn đã xảy ra thật trong quá trình phát triển.

Chụp lại khi giao diện đổi đáng kể: đổi tab ribbon, bật/tắt panel, đổi kích thước cửa sổ.

notifications — đọc mà không đụng màn hình

Đọc thẳng wpndatabase.db của Notification Center thay vì mở panel bằng Win+N. Chỉ đọc, không can thiệp màn hình, và có cả lịch sử chứ không riêng thứ đang hiện. Không cần thư viện ngoài — sqlite3 nằm sẵn trong Python.

# thời gian|app|loại|nội dung
2026-09-09 15:56|Microsoft.Todos|tile|Hi there, · what do you want to focus on today?

Toast biến mất rất nhanh. Đo ngay trong lúc viết module: kho tụt từ 19 xuống 13 bản ghi, toast từ 4 về 0, chỉ trong vài phút — Windows xoá khi người dùng gạt đi hoặc khi hết hạn. tilebadge trụ lâu hơn. Nên "không có gì" thường nghĩa là chưa có gì gần đây, không phải tool hỏng; thử kind="all".

Kho được chép ra thư mục tạm trước khi đọc, kèm cả -wal-shm — SQLite chạy chế độ WAL, chép mỗi .db sẽ thiếu đúng những thông báo mới nhất.

Riêng tư: thông báo chứa tin nhắn và email thật. Chỉ dùng khi người dùng yêu cầu.

act — các action

click · double_click · right_click · set_value · type · toggle · select · expand · collapse · scroll · scroll_into_view · set_number · focus · click_physical · drag · add_to_selection · close · restore · minimize · maximize

click_physical bỏ qua mọi pattern và click chuột thật ngay — dùng khi provider nhận pattern rồi báo thành công nhưng ứng dụng không phản ứng (đặc trưng của web view React).

drag nhận value"x,y" hoặc "id:N". UIA không có pattern nào cho kéo–thả nên đây luôn là chuột thật, nhưng cả hai đầu đều được kẹp biên và kiểm chứng danh tính trước khi bơm sự kiện. Đường đi qua nhiều bước trung gian vì nhiều ứng dụng chỉ nhận ra thao tác kéo sau vài sự kiện di chuyển.

add_to_selection là multi-select: SelectionItemPattern.AddToSelection, tụt xuống Ctrl+click khi provider không hỗ trợ. Phím Ctrl được nhả trong finally — Ctrl kẹt ở trạng thái nhấn sẽ làm hỏng mọi thao tác sau đó của người dùng.

Ưu tiên set_value hơn type: nó ghi thẳng qua ValuePattern, tức thì, và không cần cửa sổ ở foreground.


Thang fallback

Mỗi lời gọi act đi từ trên xuống và báo lại nó dừng ở bậc nào:

1. Control Pattern                    ← không cần foreground, không cướp chuột
2. LegacyIAccessible.DoDefaultAction()
3. SetFocus() + phím                  ← vẫn không cần toạ độ
4. Chuột thật tại BoundingRectangle   ← toạ độ từ UIA, không phải từ ảnh
OK: click qua InvokePattern.Invoke
OK: set_value qua ValuePattern.SetValue (không có synthetic input)
OK: click qua chuột tại BoundingRectangle (toạ độ 812,447 lấy từ BoundingRectangle)
      — đã bỏ qua: InvokePattern.Invoke: không áp dụng được; …

Dòng "đã bỏ qua" là dữ liệu đo được về chất lượng trợ năng của từng ứng dụng. Càng nhiều lần phải tụt xuống bậc 4, ứng dụng đó càng khai báo UIA kém.


Ba quyết định thiết kế đáng chú ý

Một thread STA duy nhất. UIA là COM và đòi hỏi STA. Nhiều thread cùng CoInitialize rồi cùng chạm vào một cây UIA sẽ deadlock khi COM marshal qua lại giữa các apartment. Mọi lời gọi ở đây đều đi qua sta.STA. Muốn song song thật thì phải nhiều tiến trình, không phải nhiều thread.

CacheRequest.TreeScope = Element. FindAllBuildCache(Subtree, …) trả về N phần tử; nếu cache request cũng chứa Children/Subtree thì provider dựng cache cho subtree của từng phần tử — công việc bùng nổ bậc hai. Đo được: đặt đúng thì nhanh gấp ~2× so với đọc live, đặt sai thì chậm hơn 2–6×.

Không tin lời pattern — kiểm chứng hậu điều kiện. Đo được hai lần, trên hai pattern khác nhau:

  • Notepad Win11 nhận WindowPattern.SetWindowVisualState(Minimized), báo lại CurrentWindowVisualState = 1, trong khi IsIconic() vẫn False và cửa sổ vẫn hiện.

  • Excel Online trong Edge nhận ScrollPattern.Scroll(down) và trả về thành công, trong khi VerticalScrollPercent không nhúc nhích.

Provider nói dối, và không có quy luật nào đoán trước được app nào nói dối ở pattern nào. Với action có hậu điều kiện rẻ và đáng tin, mỗi bậc phải tự chứng minh — Win32 IsIconic/IsZoomed cho cửa sổ, phần trăm trước/sau cho cuộn — không chứng minh được thì coi như bậc đó hỏng và tụt tiếp (ShowWindow, lăn chuột). Báo "OK" khi không có gì xảy ra là thứ tệ nhất một tool có thể làm với agent.

Hai chi tiết khiến việc kiểm chứng dễ tự lừa mình: chỉ so trục được yêu cầu (dao động ngang từng làm một lệnh cuộn dọc bất động trông như thành công), và đừng làm tròn về số nguyên (0.0 → 0.4% in ra "0% → 0%" thì báo cáo tự mâu thuẫn với chữ OK của chính nó).

Toạ độ cho chuột phải là toạ độ sống. Các bậc bơm chuột đọc BoundingRectangle bằng live=True, không dùng rect trong cache. Rect cache là ảnh chụp lúc observe, có thể đã cũ hàng giây và phần tử đã dịch đi vì cuộn hay đổi layout — click theo nó chính là lỗi "click trượt" mà cả thiết kế này sinh ra để loại bỏ.

Cửa sổ minimize phải được nói ra. Windows không render cửa sổ đã minimize, nên cây UI của nó co lại chỉ còn khung — không có Document, không có nội dung. Trả về cây rỗng mà không giải thích sẽ khiến agent kết luận nhầm "ứng dụng này không có UI" và đi sai hướng. observe phát hiện IsIconic và cảnh báo; act(id, "focus") tự bung cửa sổ ra trước khi focus, vì SetFocus trên cửa sổ minimize trả về thành công mà không khôi phục gì.

Trạng thái chỉ đọc khi pattern tồn tại. UIA trả về giá trị mặc định (ToggleState=indeterminate, RangeValue=0, IsReadOnly=True) cho mọi phần tử kể cả khi provider không hỗ trợ pattern đó. Báo cáo nguyên xi sẽ dán nhãn sai lên gần như toàn bộ cây — mà trạng thái sai còn tệ hơn không có trạng thái.


Giới hạn đã biết

Vấn đề

Xử lý

Chrome/Edge/Electron/VS Code có thể chỉ lộ title bar, nội dung web vô hình

Đo trên máy này: Edge lộ đầy đủ cây web (đọc được toàn bộ text trang GitHub). Chromium bật provider khi có công cụ trợ năng yêu cầu, nên kết quả phụ thuộc từng máy — thử observe(filter="text") trước, chỉ khi rỗng mới cần --force-renderer-accessibility

Cửa sổ minimize trả về cây gần rỗng

observe cảnh báo state=minimized; gọi act(<id Window>, "restore") hoặc act(..., "focus")

Java (Swing/AWT) không có UIA

Cần Java Access Bridge — chưa làm, giai đoạn 3

Canvas / viewport 3D / game chỉ là một CustomControl

UIA không cứu được. Dùng API riêng của app, hoặc vision

Office web apps: lưới Excel Online vẽ trên canvas — không GridPattern, Document báo NoScroll

Cây trợ năng vẫn đầy đủ (thanh công thức, Name Box, ribbon đều đọc được), nhưng bản thân lưới thì không. Muốn ô nào thì qua Name Box + formula bar, hoặc dùng Excel MCP trên file gốc

Ribbon Revit — tab không có tên

22 phần tử Tab/TabControl đều rỗng Name. Không trỏ tới tab "Structure" hay "Add-Ins" bằng tên được. Nhưng nhãn tab lại là Text riêng ở dải y≈57 và có tên đầy đủ — tìm Text rồi click vào nó

Ribbon Revit — panel không có phần tử con

Panel ribbon là leaf tuyệt đối: 0 con ở cả Raw/Control/Content view, không pattern nào, LegacyIAccessible.GetIAccessible() trả None. Đúng với cả Autodesk.Windows.RibbonPanel (add-in) lẫn UIFramework.RvtRibbonPanel (native). Ngoại lệ duy nhất: tab Architecture — nó lộ ~47 lệnh có tên (Wall, Door, Window, Column…). Panel lộ rect, nên chỉ còn cách click theo toạ độ và kiểm chứng bằng hậu điều kiện thật (ví dụ nút MCP Server của revit-mcp: kiểm tra cổng socket có LISTEN không)

đã loại trừ bằng thực nghiệm

Ngoại lệ Architecture không đến từ: pyRevit (ẩn Architecture → 0 ở mọi tab, không liên quan pyRevit) · thứ tự quét (đảo chiều vẫn 0/47/0/47) · tab nào mở đầu tiên (đặt Structure làm tab đầu → vẫn 0) · loại view (Sheet và Floor Plan đều như nhau) · model đang mở (tái hiện trên hai project khác nhau). Cơ chế thật vẫn chưa rõ. Kết luận thực dụng: coi như lệnh ribbon không truy cập được, vì ngoại lệ này phụ thuộc một tab mà người dùng tắt được bất cứ lúc nào

App chạy admin

Server phải cùng mức toàn vẹn, hoặc ký với uiAccess=true

Cửa sổ treo

TransactionTimeout 30s (chỉnh bằng UIA_MCP_TRANSACTION_TIMEOUT_MS) + timeout 20s ở tầng STA; sau đó server báo kẹt và cần khởi động lại. Đặt ngắn hơn sẽ hỏng trên app nặng: Revit cần 6–16s cho một lần duyệt subtree, ngưỡng 5s làm mọi truy vấn trả UIA_E_TIMEOUT dưới dạng COMError không thông điệp — rất dễ chẩn đoán nhầm thành "app treo"

Chưa có event-driven refresh

Mỗi observe là một lần quét lại — giai đoạn 2


Nhắc lại thứ tự ưu tiên

UIA là bậc 2, không phải bậc 1. Trước khi dùng server này, hãy hỏi ứng dụng đó có API không — Revit, AutoCAD, Navisworks, SAP2000, Excel, Graph, ACC đều đã có MCP riêng và luôn nhanh hơn, tin cậy hơn việc đi qua GUI.


Bố cục mã nguồn

File

Vai trò

sta.py

Apartment STA dùng chung + timeout

uia.py

Bọc COM client: hằng số, điều kiện lọc, cache request, đọc property an toàn

registry.py

Bảng ID ổn định (RuntimeIdAutomationId → hash)

perceive.py

list_windows, observe, describe_at

actions.py

Thang fallback hành động

text.py

read_text, read_table

rawinput.py

SendInput — chỉ dùng ở các bậc fallback cuối

server.py

Định nghĩa tool MCP

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    An enterprise-grade automation server that enables AI assistants to control Windows PCs through intelligent UI element detection, window management, and system-level commands. It leverages the Windows UI Automation tree for reliable interaction, providing tools for mouse/keyboard control, application management, and high-performance screen state analysis.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to inspect and automate Windows desktop UI elements by exploring UI trees, checking properties, performing actions like clicking and typing, and generating Python automation scripts.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI coding agents to automate Windows desktop applications through semantic UI Automation instead of brittle coordinate clicks, with tools for discovering windows, finding controls by stable identifiers, and verifying actions.
    1
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to control Windows GUI by listing and focusing windows, capturing element snapshots via UIA/OCR/CDP, performing clicks/inputs/scrolls, verifying changes, waiting for screen updates, taking screenshots, and obtaining visual descriptions.
    2
    -