windows-mcp 0.5.7__py3-none-any.whl → 0.5.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- windows_mcp/__main__.py +69 -82
- windows_mcp/analytics.py +170 -171
- windows_mcp/desktop/config.py +20 -20
- windows_mcp/desktop/service.py +279 -99
- windows_mcp/desktop/views.py +59 -57
- windows_mcp/tree/cache_utils.py +126 -0
- windows_mcp/tree/config.py +75 -50
- windows_mcp/tree/service.py +424 -348
- windows_mcp/tree/utils.py +21 -21
- windows_mcp/tree/views.py +51 -25
- windows_mcp/uia/__init__.py +4 -0
- windows_mcp/uia/controls.py +4790 -0
- windows_mcp/uia/core.py +3278 -0
- windows_mcp/uia/enums.py +1963 -0
- windows_mcp/uia/events.py +83 -0
- windows_mcp/uia/patterns.py +2106 -0
- windows_mcp/vdm/__init__.py +1 -0
- windows_mcp/vdm/core.py +490 -0
- windows_mcp/watchdog/__init__.py +1 -0
- windows_mcp/watchdog/event_handlers.py +55 -0
- windows_mcp/watchdog/service.py +199 -0
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.9.dist-info}/METADATA +30 -24
- windows_mcp-0.5.9.dist-info/RECORD +29 -0
- windows_mcp-0.5.7.dist-info/RECORD +0 -17
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.9.dist-info}/WHEEL +0 -0
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.9.dist-info}/entry_points.txt +0 -0
- {windows_mcp-0.5.7.dist-info → windows_mcp-0.5.9.dist-info}/licenses/LICENSE.md +0 -0
windows_mcp/desktop/views.py
CHANGED
|
@@ -1,58 +1,60 @@
|
|
|
1
|
-
from windows_mcp.tree.views import TreeState
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from tabulate import tabulate
|
|
4
|
-
from typing import Optional
|
|
5
|
-
from PIL.Image import Image
|
|
6
|
-
from enum import Enum
|
|
7
|
-
|
|
8
|
-
class Browser(Enum):
|
|
9
|
-
CHROME='Chrome'
|
|
10
|
-
EDGE='Edge'
|
|
11
|
-
FIREFOX='Firefox'
|
|
12
|
-
|
|
13
|
-
class Status(Enum):
|
|
14
|
-
MAXIMIZED='Maximized'
|
|
15
|
-
MINIMIZED='Minimized'
|
|
16
|
-
NORMAL='Normal'
|
|
17
|
-
HIDDEN='Hidden'
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass
|
|
21
|
-
class App:
|
|
22
|
-
name:str
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
from windows_mcp.tree.views import TreeState,BoundingBox
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from tabulate import tabulate
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from PIL.Image import Image
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
class Browser(Enum):
|
|
9
|
+
CHROME='Chrome'
|
|
10
|
+
EDGE='Edge'
|
|
11
|
+
FIREFOX='Firefox'
|
|
12
|
+
|
|
13
|
+
class Status(Enum):
|
|
14
|
+
MAXIMIZED='Maximized'
|
|
15
|
+
MINIMIZED='Minimized'
|
|
16
|
+
NORMAL='Normal'
|
|
17
|
+
HIDDEN='Hidden'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class App:
|
|
22
|
+
name:str
|
|
23
|
+
runtime_id:tuple[int]
|
|
24
|
+
is_browser:bool
|
|
25
|
+
depth:int
|
|
26
|
+
status:Status
|
|
27
|
+
bounding_box:BoundingBox
|
|
28
|
+
handle: int
|
|
29
|
+
process_id:int
|
|
30
|
+
|
|
31
|
+
def to_row(self):
|
|
32
|
+
return [self.name, self.depth, self.status.value, self.bounding_box.width, self.bounding_box.height, self.handle]
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Size:
|
|
36
|
+
width:int
|
|
37
|
+
height:int
|
|
38
|
+
|
|
39
|
+
def to_string(self):
|
|
40
|
+
return f'({self.width},{self.height})'
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class DesktopState:
|
|
44
|
+
apps:list[App]
|
|
45
|
+
active_app:Optional[App]
|
|
46
|
+
screenshot:Optional[Image]=None
|
|
47
|
+
tree_state:Optional[TreeState]=None
|
|
48
|
+
|
|
49
|
+
def active_app_to_string(self):
|
|
50
|
+
if self.active_app is None:
|
|
51
|
+
return 'No active app found'
|
|
52
|
+
headers = ["Name", "Depth", "Status", "Width", "Height", "Handle"]
|
|
53
|
+
return tabulate([self.active_app.to_row()], headers=headers, tablefmt="simple")
|
|
54
|
+
|
|
55
|
+
def apps_to_string(self):
|
|
56
|
+
if not self.apps:
|
|
57
|
+
return 'No apps running in background'
|
|
58
|
+
headers = ["Name", "Depth", "Status", "Width", "Height", "Handle"]
|
|
59
|
+
rows = [app.to_row() for app in self.apps]
|
|
58
60
|
return tabulate(rows, headers=headers, tablefmt="simple")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UIA Caching Utilities for Performance Optimization
|
|
3
|
+
|
|
4
|
+
This module provides utilities for implementing UI Automation caching
|
|
5
|
+
to reduce cross-process COM calls during tree traversal.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from windows_mcp.uia import CacheRequest, PropertyId, PatternId, TreeScope, Control
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CacheRequestFactory:
|
|
16
|
+
"""Factory for creating optimized cache requests for different scenarios."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def create_tree_traversal_cache() -> CacheRequest:
|
|
20
|
+
"""
|
|
21
|
+
Creates a cache request optimized for tree traversal.
|
|
22
|
+
Caches all commonly accessed properties and patterns.
|
|
23
|
+
|
|
24
|
+
This cache request is designed to minimize COM calls during
|
|
25
|
+
the tree_traversal() operation in tree/service.py.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
CacheRequest configured for tree traversal
|
|
29
|
+
"""
|
|
30
|
+
cache_request = CacheRequest()
|
|
31
|
+
|
|
32
|
+
# Set scope to cache element and its children
|
|
33
|
+
# This allows us to get children with pre-cached properties
|
|
34
|
+
cache_request.TreeScope = TreeScope.TreeScope_Element | TreeScope.TreeScope_Children
|
|
35
|
+
|
|
36
|
+
# Basic identification properties
|
|
37
|
+
cache_request.AddProperty(PropertyId.NameProperty)
|
|
38
|
+
cache_request.AddProperty(PropertyId.AutomationIdProperty)
|
|
39
|
+
cache_request.AddProperty(PropertyId.LocalizedControlTypeProperty)
|
|
40
|
+
cache_request.AddProperty(PropertyId.AcceleratorKeyProperty)
|
|
41
|
+
cache_request.AddProperty(PropertyId.ClassNameProperty)
|
|
42
|
+
cache_request.AddProperty(PropertyId.ControlTypeProperty)
|
|
43
|
+
|
|
44
|
+
# State properties for visibility and interaction checks
|
|
45
|
+
cache_request.AddProperty(PropertyId.IsEnabledProperty)
|
|
46
|
+
cache_request.AddProperty(PropertyId.IsOffscreenProperty)
|
|
47
|
+
cache_request.AddProperty(PropertyId.IsControlElementProperty)
|
|
48
|
+
cache_request.AddProperty(PropertyId.HasKeyboardFocusProperty)
|
|
49
|
+
cache_request.AddProperty(PropertyId.IsKeyboardFocusableProperty)
|
|
50
|
+
|
|
51
|
+
# Layout properties
|
|
52
|
+
cache_request.AddProperty(PropertyId.BoundingRectangleProperty)
|
|
53
|
+
|
|
54
|
+
# REMOVED: Expensive patterns and less critical properties to improve performance
|
|
55
|
+
# Patterns like LegacyIAccessible are very expensive to marshal for every element.
|
|
56
|
+
# We will fetch them live only for the few elements that actually need them.
|
|
57
|
+
|
|
58
|
+
return cache_request
|
|
59
|
+
|
|
60
|
+
class CachedControlHelper:
|
|
61
|
+
"""Helper class for working with cached controls."""
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def build_cached_control(node: Control, cache_request: Optional[CacheRequest] = None) -> Control:
|
|
65
|
+
"""
|
|
66
|
+
Build a cached version of a control.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
node: The control to cache
|
|
70
|
+
cache_request: Optional custom cache request. If None, uses tree traversal cache.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A control with cached properties, or the original control if caching fails
|
|
74
|
+
"""
|
|
75
|
+
if cache_request is None:
|
|
76
|
+
cache_request = CacheRequestFactory.create_tree_traversal_cache()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
cached_node = node.BuildUpdatedCache(cache_request)
|
|
80
|
+
cached_node._is_cached = True
|
|
81
|
+
return cached_node
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.debug(f"Failed to build cached control: {e}")
|
|
84
|
+
return node
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def get_cached_children(node: Control, cache_request: Optional[CacheRequest] = None) -> list[Control]:
|
|
88
|
+
"""
|
|
89
|
+
Get children with pre-cached properties.
|
|
90
|
+
|
|
91
|
+
This is the most significant optimization - it retrieves all children
|
|
92
|
+
with their properties already cached, eliminating the need for individual
|
|
93
|
+
property access calls on each child.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
node: The parent control
|
|
97
|
+
cache_request: Optional custom cache request. If None, uses tree traversal cache.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of children with cached properties
|
|
101
|
+
"""
|
|
102
|
+
if cache_request is None:
|
|
103
|
+
cache_request = CacheRequestFactory.create_tree_traversal_cache()
|
|
104
|
+
|
|
105
|
+
# Ensure the cache request includes children
|
|
106
|
+
# Note: We do NOT set this here to avoid modifying shared cache request objects
|
|
107
|
+
# The caller is responsible for providing a CacheRequest with TreeScope_Children
|
|
108
|
+
if (cache_request.TreeScope & TreeScope.TreeScope_Children) == 0:
|
|
109
|
+
logger.warning("Cache request passed to get_cached_children does not have Children scope!")
|
|
110
|
+
|
|
111
|
+
# Try to use existing cache first if available
|
|
112
|
+
try:
|
|
113
|
+
# Build updated cache that includes children
|
|
114
|
+
cached_node = node.BuildUpdatedCache(cache_request)
|
|
115
|
+
children = cached_node.GetCachedChildren()
|
|
116
|
+
|
|
117
|
+
for child in children:
|
|
118
|
+
child._is_cached = True
|
|
119
|
+
|
|
120
|
+
logger.debug(f"Retrieved {len(children)} cached children (newly built)")
|
|
121
|
+
return children
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.debug(f"Failed to get cached children, falling back to regular access: {e}")
|
|
125
|
+
return node.GetChildren()
|
|
126
|
+
|
windows_mcp/tree/config.py
CHANGED
|
@@ -1,51 +1,76 @@
|
|
|
1
|
-
INTERACTIVE_CONTROL_TYPE_NAMES=set([
|
|
2
|
-
'ButtonControl',
|
|
3
|
-
'ListItemControl',
|
|
4
|
-
'MenuItemControl',
|
|
5
|
-
'EditControl',
|
|
6
|
-
'CheckBoxControl',
|
|
7
|
-
'RadioButtonControl',
|
|
8
|
-
'ComboBoxControl',
|
|
9
|
-
'HyperlinkControl',
|
|
10
|
-
'SplitButtonControl',
|
|
11
|
-
'TabItemControl',
|
|
12
|
-
'TreeItemControl',
|
|
13
|
-
'DataItemControl',
|
|
14
|
-
'HeaderItemControl',
|
|
15
|
-
'TextBoxControl',
|
|
16
|
-
'SpinnerControl',
|
|
17
|
-
'ScrollBarControl'
|
|
18
|
-
])
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
'
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
INTERACTIVE_CONTROL_TYPE_NAMES=set([
|
|
2
|
+
'ButtonControl',
|
|
3
|
+
'ListItemControl',
|
|
4
|
+
'MenuItemControl',
|
|
5
|
+
'EditControl',
|
|
6
|
+
'CheckBoxControl',
|
|
7
|
+
'RadioButtonControl',
|
|
8
|
+
'ComboBoxControl',
|
|
9
|
+
'HyperlinkControl',
|
|
10
|
+
'SplitButtonControl',
|
|
11
|
+
'TabItemControl',
|
|
12
|
+
'TreeItemControl',
|
|
13
|
+
'DataItemControl',
|
|
14
|
+
'HeaderItemControl',
|
|
15
|
+
'TextBoxControl',
|
|
16
|
+
'SpinnerControl',
|
|
17
|
+
'ScrollBarControl'
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
INTERACTIVE_ROLES = {
|
|
21
|
+
# Buttons
|
|
22
|
+
'PushButton', 'SplitButton', 'ButtonDropDown', 'ButtonMenu',
|
|
23
|
+
'ButtonDropDownGrid', 'OutlineButton',
|
|
24
|
+
|
|
25
|
+
# Links
|
|
26
|
+
'Link',
|
|
27
|
+
|
|
28
|
+
# Inputs & Selection
|
|
29
|
+
'Text', 'IpAddress', 'HotkeyField', 'ComboBox', 'DropList',
|
|
30
|
+
'CheckButton', 'RadioButton',
|
|
31
|
+
|
|
32
|
+
# Menus & Tabs
|
|
33
|
+
'MenuItem', 'ListItem', 'PageTab',
|
|
34
|
+
|
|
35
|
+
# Trees
|
|
36
|
+
'OutlineItem',
|
|
37
|
+
|
|
38
|
+
# Values
|
|
39
|
+
'Slider', 'SpinButton', 'Dial', 'ScrollBar', 'Grip',
|
|
40
|
+
|
|
41
|
+
# Grids
|
|
42
|
+
'ColumnHeader', 'RowHeader', 'Cell'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
DOCUMENT_CONTROL_TYPE_NAMES=set([
|
|
46
|
+
'DocumentControl'
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
STRUCTURAL_CONTROL_TYPE_NAMES = set([
|
|
50
|
+
'PaneControl',
|
|
51
|
+
'GroupControl',
|
|
52
|
+
'CustomControl'
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
INFORMATIVE_CONTROL_TYPE_NAMES=set([
|
|
56
|
+
'TextControl',
|
|
57
|
+
'ImageControl',
|
|
58
|
+
'StatusBarControl',
|
|
59
|
+
# 'ProgressBarControl',
|
|
60
|
+
# 'ToolTipControl',
|
|
61
|
+
# 'TitleBarControl',
|
|
62
|
+
# 'SeparatorControl',
|
|
63
|
+
# 'HeaderControl',
|
|
64
|
+
# 'HeaderItemControl',
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
DEFAULT_ACTIONS=set([
|
|
68
|
+
'Click',
|
|
69
|
+
'Press',
|
|
70
|
+
'Jump',
|
|
71
|
+
'Check',
|
|
72
|
+
'Uncheck',
|
|
73
|
+
'Double Click'
|
|
74
|
+
])
|
|
75
|
+
|
|
51
76
|
THREAD_MAX_RETRIES = 3
|