Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ MCP Client can access the following tools to interact with Windows:
- `Process`: List running processes or terminate them by PID or name.
- `Notification`: Send a Windows toast notification with a title and message.
- `Registry`: Read, write, delete, or list Windows Registry values and keys.
- `TextCursor`: Inspect or manipulate the caret/selection of the focused text control via UIA — read caret/selection info, move the caret (relative or absolute), select a text range, select all, or collapse a selection. Requires a control that exposes the UIA TextPattern.


## 🤝 Connect with Us
Expand Down Expand Up @@ -793,7 +794,7 @@ For detailed information on what data is collected and how it is handled, please

## 📝 Limitations

- Selecting specific sections of the text in a paragraph, as the MCP is relying on a11y tree. (⌛ Working on it.)
- Selecting a specific section of text within a paragraph is supported by the `TextCursor` tool for controls that expose the UIA TextPattern; controls that do not expose it remain unsupported.
- `Type-Tool` is meant for typing text, not programming in IDE because of it types program as a whole in a file. (⌛ Working on it.)
- This MCP server can't be used to play video games 🎮.

Expand Down
4 changes: 4 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@
{
"name": "Registry",
"description": "Accesses the Windows Registry. Use mode=\"get\" to read a value, mode=\"set\" to create/update a value, mode=\"delete\" to remove a value or key, mode=\"list\" to list values and sub-keys under a path."
},
{
"name": "TextCursor",
"description": "Inspects or manipulates the caret/selection of the focused Windows text control through UI Automation. Modes: 'get_info' (read caret/selection info), 'move_relative' and 'move_absolute' (move the caret by a signed delta or to an absolute character offset), 'select_relative' and 'select_absolute' (select a text range), 'select_all', 'collapse_selection' (collapse a selection to its start or end edge). Offsets use provider-defined UIA TextUnit_Character steps from the document start. Requires a control that exposes the UIA TextPattern."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Overlong textcursor description line 📘 Rule violation ✧ Quality

New lines exceed the 100-character maximum in manifest.json and README.md, which reduces
readability and makes diffs harder to review. These long, single-line descriptions should be wrapped
or shortened to meet the 100-character limit.
Agent Prompt
## Issue description
Several newly added lines exceed the 100-character maximum line length (notably the new `TextCursor` descriptions).

## Issue Context
The repo compliance checklist enforces a 100-character max for non-whitespace, non-comment lines.

## Fix Focus Areas
- manifest.json[153-153]
- README.md[732-732]
- README.md[797-797]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
],
"compatibility": {
Expand Down
59 changes: 59 additions & 0 deletions src/windows_mcp/text_cursor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Inspect and manipulate the caret/selection of the currently focused Windows
text control through UI Automation.

The caret/selection is discovered via IUIAutomationTextPattern.GetSelection()
on the focused element, walking up to its ancestors when needed.
IUIAutomationTextPattern2.GetCaretRange() is intentionally not used; see
discovery.try_get_caret_on_element for the rationale.

Supported modes:
- get_info
- move_relative
- move_absolute
- select_relative
- select_absolute
- select_all
- collapse_selection

Important:
- IUIAutomationTextRange.Move() only moves a client-side range.
- Select() asks the provider to apply that range as the real caret/selection.
- Write operations verify the applied range by reading it back.
- Some providers expose TextPattern but do not support moving the real caret.
"""

from __future__ import annotations

from .errors import TextCursorError, TextCursorVerificationError
from .models import (
CollapseSelectionAction,
CursorAction,
CursorSnapshot,
CursorToolResult,
GetInfoAction,
MoveAbsoluteAction,
MoveRelativeAction,
SelectAbsoluteAction,
SelectAllAction,
SelectRelativeAction,
)
from .service import run_tool

__all__ = [
"CollapseSelectionAction",
"CursorAction",
"CursorSnapshot",
"CursorToolResult",
"GetInfoAction",
"MoveAbsoluteAction",
"MoveRelativeAction",
"SelectAbsoluteAction",
"SelectAllAction",
"SelectRelativeAction",
"TextCursorError",
"TextCursorVerificationError",
"run_tool",
]
8 changes: 8 additions & 0 deletions src/windows_mcp/text_cursor/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constants used by the text cursor implementation."""

MAX_TEXT_UNIT_MOVE = 2_147_483_647

# Upper bound on the selected-text string embedded in a snapshot. Unlike the
# surrounding context (capped by context_chars), the selection can be the whole
# document (e.g. after select_all), so cap it here to keep the MCP payload small.
MAX_SELECTED_TEXT_CHARS = 4096
87 changes: 87 additions & 0 deletions src/windows_mcp/text_cursor/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Locate the focused UIA control that exposes the caret/selection TextPattern.

Runs on the server's main-thread STA and reuses the shared `windows_mcp.uia`
client, so it can use `GetFocusedControl` and `Control` navigation directly.
"""

from __future__ import annotations

from dataclasses import dataclass

from comtypes import COMError
from windows_mcp.uia import Control, GetFocusedControl, PatternId, TextPattern, TextRange


@dataclass
class UIACaretInfo:
"""UIA provider and text range that describe the current caret or selection."""

element: Control
text_pattern: TextPattern
text_range: TextRange
source: str
exact_caret: bool
selection_count: int = 1


def try_get_caret_on_element(control: Control) -> UIACaretInfo | None:
# Use TextPattern.GetSelection rather than TextPattern2.GetCaretRange:
# GetCaretRange does not expose the actual selection range, so handling it
# separately is not worth the effort. The first selection is kept as-is:
# a degenerate range is treated as a caret (exact_caret=True), and a
# non-empty one as a range whose active caret endpoint is unknown.
text_pattern = control.GetPattern(PatternId.TextPattern)

if text_pattern is not None:
try:
selections = text_pattern.GetSelection()
except (COMError, AttributeError, TypeError, ValueError):
selections = []

if selections:
selection = selections[0]
return UIACaretInfo(
element=control,
text_pattern=text_pattern,
text_range=selection,
source="TextPattern.GetSelection",
exact_caret=selection.IsDegenerate(),
selection_count=len(selections),
)

return None


def find_caret_provider(max_parent_levels: int = 8) -> UIACaretInfo:
"""
Start at the focused UIA control and walk up RawView parents.
Some controls expose TextPattern on an ancestor rather than on the exact
focused child.
"""
control = GetFocusedControl()

if control is None:
raise RuntimeError("UI Automation returned no focused element.")

try:
element_name = control.Name
except COMError:
element_name = ""

tried_cnt = 0
while control is not None and tried_cnt < max_parent_levels + 1:
tried_cnt += 1
result = try_get_caret_on_element(control)

if result is not None:
return result

try:
control = control.GetParentControl()
except COMError:
control = None

raise RuntimeError(
f"The focused element {f'"{element_name}" ' if element_name else ''}and "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Single-quoted strings in discovery.py 📘 Rule violation ✧ Quality

The new f-string in find_caret_provider() uses single-quoted string literals ('' and `f'"{...}"
'`) which violates the double-quote-only string literal style requirement. This creates inconsistent
string quoting style within the new TextCursor implementation.
Agent Prompt
## Issue description
`src/windows_mcp/text_cursor/discovery.py` introduces single-quoted string literals inside a new f-string expression.

## Issue Context
The compliance checklist requires double quotes for all string literals where possible.

## Fix Focus Areas
- src/windows_mcp/text_cursor/discovery.py[75-77]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

f"its inspected parents do not expose TextPattern."
)
9 changes: 9 additions & 0 deletions src/windows_mcp/text_cursor/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Errors raised by text cursor operations."""


class TextCursorError(RuntimeError):
"""Base error raised by TextCursor operations."""


class TextCursorVerificationError(TextCursorError):
"""Raised when a TextCursor write cannot be verified."""
Loading
Loading