An MCP server that lets AI agents operate .NET desktop apps (WPF, WinForms, WinUI) through UI Automation.
Netwright demonstrates solid desktop automation tool definitions with comprehensive schema coverage and clear naming conventions. All 15 tools use verb_noun patterns (desktop_*) with explicit McpServerTool annotations. Tool descriptions are present and action-oriented (median ~120 chars). Input schemas are fully declared with JSON Schema types and descriptions. However, output schemas are partially generic (ToolOutcome record used across all tools), parameter descriptions could be more prescriptive about constraints and error recovery, and error guidance is minimal. The codebase shows good architectural discipline with structured content support and CancellationToken handling, but lacks explicit recovery hints and dependency guidance that would push this into the A range.
Launch (path to .exe, or project to build a .csproj), attach (process: pid, name or window title), close, or status of the Target App.
Click an element in the background. Right/double clicks and elements without click support need foreground=true.
Verify a condition on an element or the UI (text, title, enabled/disabled, etc.). Retries until it passes or times out.
Export the actions of the current session as a C# regression test (xUnit + Netwright.Testing API).
Elements matching a Selector, with Refs. Selectors: #automationId, role "name", role ~"partial", a >> b, :nth(2).
Properties, patterns and a stable Selector of one element.
Generic output schema (ToolOutcome) used across all tools. While structured, the schema contains only 4 fields (Ok, Code, Settled, Changes, ElapsedMs) that do not capture the actual output data (text snapshot, found elements, inspection results, etc.). LLMs cannot determine what fields to expect or plan downstream tool chains effectively.
Parameter descriptions lack prescriptive constraint guidance. E.g., 'selector' in desktop_find only says 'Selector expression to match elements' but does not explain the selector syntax (#automationId, role patterns, combinators, :nth() pseudo-selectors). Params like 'timeoutMs' lack min/max guidance (should be bounded, e.g., 100-120000ms). LLMs must infer from examples in the description rather than explicit constraints.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 65 | 2025-06-18+ | v2 |
App Output: the Target App's stdout, stderr, debug output, and any crash report from the last action.
Press keys, e.g. Enter, Ctrl+S, "Tab Tab Enter". Needs foreground=true.
Image of the Target App or an element, optionally scaled down.
Scroll the target's container by direction or to an edge, or scroll the target into view.
Set one of: checked, selected, expanded, value (slider or input), item (select a child of a list, combo, tabs or tree by name).
The Target App UI as text with Refs ([e12]), filtered to what matters.
Set the text of an input in the background. foreground=true types real keys (needed for submit).
Wait until an element appears, disappears, or becomes enabled/disabled, or until text appears.
List or change Target App windows (default: main window). activate needs foreground=true.
No error recovery guidance. When desktop_app attach fails (process not found), the response is '{Ok: false, Code: "..."}' without actionable hints like 'Process not found. Try desktop_wait for the process to launch, or call desktop_app status to list running processes.' Agents cannot self-recover from failures.
Parameter descriptions for boolean flags lack explicit statement of side effects. E.g., desktop_click's 'foreground=true' says 'Real mouse; focus restored after' but does not clarify what 'focus restored' means in the UI context, does it re-hide the window? Can it fail? desktop_app's 'force=true' (Kill on close) lacks guidance on when to use it vs graceful close.
Parameter interdependencies are underdocumented. desktop_set_state accepts 6 mutually-exclusive state parameters (checked, selected, expanded, value, item). The description does not explain: Can multiple be set in one call? Do they apply to different element types? What happens if the target does not support the requested state? LLMs will guess and pass invalid combinations.
Enum constraints are documented in description text, not as formal JSON Schema enums. E.g., desktop_app's 'action' param says 'launch|attach|close|status' in description, but the schema shows type='string' with no enum field. Similarly, desktop_click's 'button' is described as 'left|right|middle' but not constrained in schema. LLMs will hallucinate values if they do not parse the text.
desktop_snapshot, desktop_find, desktop_inspect return text summaries with Refs (e.g., '[e12]'), but the schema for these responses is not documented. The tool description does not specify the format of returned data, are Refs always [eN]? Are they stable across calls? Can they be used directly as 'target' parameters? This ambiguity forces LLMs to learn the format through trial and error.
No pagination or result limiting documented for desktop_find (can match many elements) or desktop_logs (can return many lines). While the code shows 'max_size' in screenshot and 'max' in logs, there is no documented limit on the quantity of results from find. LLMs should know when results may be truncated and when to refine queries.