windows-mcp 0.5.5__tar.gz → 0.5.6__tar.gz
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-0.5.5 → windows_mcp-0.5.6}/PKG-INFO +27 -3
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/README.md +23 -2
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/pyproject.toml +43 -40
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/__main__.py +34 -6
- windows_mcp-0.5.6/src/windows_mcp/analytics.py +150 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/desktop/service.py +7 -6
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/uv.lock +51 -1
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/.gitignore +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/.python-version +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/CONTRIBUTING.md +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/LICENSE.md +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/SECURITY.md +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/assets/demo1.mov +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/assets/demo2.mov +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/assets/logo.png +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/assets/screenshots/screenshot_1.png +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/assets/screenshots/screenshot_2.png +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/assets/screenshots/screenshot_3.png +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/manifest.json +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/server.json +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/__init__.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/desktop/__init__.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/desktop/config.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/desktop/views.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/tree/__init__.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/tree/config.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/tree/service.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/tree/utils.py +0 -0
- {windows_mcp-0.5.5 → windows_mcp-0.5.6}/src/windows_mcp/tree/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: windows-mcp
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: Lightweight MCP Server for interacting with Windows Operating System.
|
|
5
5
|
Project-URL: homepage, https://github.com/CursorTouch
|
|
6
6
|
Author-email: Jeomon George <jeogeoalukka@gmail.com>
|
|
@@ -37,14 +37,17 @@ Requires-Dist: live-inspect>=0.1.1
|
|
|
37
37
|
Requires-Dist: markdownify>=1.1.0
|
|
38
38
|
Requires-Dist: pdfplumber>=0.11.7
|
|
39
39
|
Requires-Dist: pillow>=11.2.1
|
|
40
|
+
Requires-Dist: posthog>=7.4.0
|
|
40
41
|
Requires-Dist: psutil>=7.0.0
|
|
41
42
|
Requires-Dist: pyautogui>=0.9.54
|
|
42
43
|
Requires-Dist: pygetwindow>=0.0.9
|
|
44
|
+
Requires-Dist: python-dotenv>=1.1.0
|
|
43
45
|
Requires-Dist: python-levenshtein>=0.27.1
|
|
44
46
|
Requires-Dist: pywinauto>=0.6.9
|
|
45
47
|
Requires-Dist: requests>=2.32.3
|
|
46
48
|
Requires-Dist: tabulate>=0.9.0
|
|
47
49
|
Requires-Dist: uiautomation>=2.0.24
|
|
50
|
+
Requires-Dist: uuid7>=0.1.0
|
|
48
51
|
Description-Content-Type: text/markdown
|
|
49
52
|
|
|
50
53
|
[](https://mseep.ai/app/cursortouch-windows-mcp)
|
|
@@ -261,7 +264,6 @@ npm install -g @google/gemini-cli
|
|
|
261
264
|
{
|
|
262
265
|
"theme": "Default",
|
|
263
266
|
...
|
|
264
|
-
//MCP Server Config
|
|
265
267
|
"mcpServers": {
|
|
266
268
|
"windows-mcp": {
|
|
267
269
|
"command": "uvx",
|
|
@@ -295,7 +297,6 @@ npm install -g @qwen-code/qwen-code@latest
|
|
|
295
297
|
|
|
296
298
|
```json
|
|
297
299
|
{
|
|
298
|
-
//MCP Server Config
|
|
299
300
|
"mcpServers": {
|
|
300
301
|
"windows-mcp": {
|
|
301
302
|
"command": "uvx",
|
|
@@ -384,6 +385,28 @@ For detailed security information, including:
|
|
|
384
385
|
|
|
385
386
|
Please read our [Security Policy](SECURITY.md).
|
|
386
387
|
|
|
388
|
+
## 📊 Telemetry
|
|
389
|
+
|
|
390
|
+
Windows-MCP collects anonymized usage data to help improve the tool. No personal information, no tool arguments, no outputs are tracked.
|
|
391
|
+
|
|
392
|
+
To disable telemetry, add the following to your MCP client configuration:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"mcpServers": {
|
|
397
|
+
"windows-mcp": {
|
|
398
|
+
"command": "uvx",
|
|
399
|
+
"args": [
|
|
400
|
+
"windows-mcp"
|
|
401
|
+
],
|
|
402
|
+
"env": {
|
|
403
|
+
"ANONYMIZED_TELEMETRY": "false"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
387
410
|
## 📝 Limitations
|
|
388
411
|
|
|
389
412
|
- Selecting specific sections of the text in a paragraph, as the MCP is relying on a11y tree. (⌛ Working on it.)
|
|
@@ -421,3 +444,4 @@ Made with ❤️ by [CursorTouch](https://github.com/CursorTouch)
|
|
|
421
444
|
url={https://github.com/CursorTouch/Windows-MCP}
|
|
422
445
|
}
|
|
423
446
|
```
|
|
447
|
+
|
|
@@ -212,7 +212,6 @@ npm install -g @google/gemini-cli
|
|
|
212
212
|
{
|
|
213
213
|
"theme": "Default",
|
|
214
214
|
...
|
|
215
|
-
//MCP Server Config
|
|
216
215
|
"mcpServers": {
|
|
217
216
|
"windows-mcp": {
|
|
218
217
|
"command": "uvx",
|
|
@@ -246,7 +245,6 @@ npm install -g @qwen-code/qwen-code@latest
|
|
|
246
245
|
|
|
247
246
|
```json
|
|
248
247
|
{
|
|
249
|
-
//MCP Server Config
|
|
250
248
|
"mcpServers": {
|
|
251
249
|
"windows-mcp": {
|
|
252
250
|
"command": "uvx",
|
|
@@ -335,6 +333,28 @@ For detailed security information, including:
|
|
|
335
333
|
|
|
336
334
|
Please read our [Security Policy](SECURITY.md).
|
|
337
335
|
|
|
336
|
+
## 📊 Telemetry
|
|
337
|
+
|
|
338
|
+
Windows-MCP collects anonymized usage data to help improve the tool. No personal information, no tool arguments, no outputs are tracked.
|
|
339
|
+
|
|
340
|
+
To disable telemetry, add the following to your MCP client configuration:
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{
|
|
344
|
+
"mcpServers": {
|
|
345
|
+
"windows-mcp": {
|
|
346
|
+
"command": "uvx",
|
|
347
|
+
"args": [
|
|
348
|
+
"windows-mcp"
|
|
349
|
+
],
|
|
350
|
+
"env": {
|
|
351
|
+
"ANONYMIZED_TELEMETRY": "false"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
338
358
|
## 📝 Limitations
|
|
339
359
|
|
|
340
360
|
- Selecting specific sections of the text in a paragraph, as the MCP is relying on a11y tree. (⌛ Working on it.)
|
|
@@ -372,3 +392,4 @@ Made with ❤️ by [CursorTouch](https://github.com/CursorTouch)
|
|
|
372
392
|
url={https://github.com/CursorTouch/Windows-MCP}
|
|
373
393
|
}
|
|
374
394
|
```
|
|
395
|
+
|
|
@@ -1,40 +1,43 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "windows-mcp"
|
|
3
|
-
version = "0.5.
|
|
4
|
-
description = "Lightweight MCP Server for interacting with Windows Operating System."
|
|
5
|
-
authors = [
|
|
6
|
-
{ name = "Jeomon George", email = "jeogeoalukka@gmail.com" }
|
|
7
|
-
]
|
|
8
|
-
readme = "README.md"
|
|
9
|
-
license = { file = "LICENSE.md" }
|
|
10
|
-
urls = { homepage = "https://github.com/CursorTouch" }
|
|
11
|
-
keywords = ["windows", "mcp", "ai", "desktop","ai agent"]
|
|
12
|
-
requires-python = ">=3.13"
|
|
13
|
-
dependencies = [
|
|
14
|
-
"click>=8.2.1",
|
|
15
|
-
"fastmcp>=2.8.1",
|
|
16
|
-
"fuzzywuzzy>=0.18.0",
|
|
17
|
-
"humancursor>=1.1.5",
|
|
18
|
-
"ipykernel>=6.30.0",
|
|
19
|
-
"live-inspect>=0.1.1",
|
|
20
|
-
"markdownify>=1.1.0",
|
|
21
|
-
"pdfplumber>=0.11.7",
|
|
22
|
-
"pillow>=11.2.1",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
[
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
[project]
|
|
2
|
+
name = "windows-mcp"
|
|
3
|
+
version = "0.5.6"
|
|
4
|
+
description = "Lightweight MCP Server for interacting with Windows Operating System."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Jeomon George", email = "jeogeoalukka@gmail.com" }
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { file = "LICENSE.md" }
|
|
10
|
+
urls = { homepage = "https://github.com/CursorTouch" }
|
|
11
|
+
keywords = ["windows", "mcp", "ai", "desktop","ai agent"]
|
|
12
|
+
requires-python = ">=3.13"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"click>=8.2.1",
|
|
15
|
+
"fastmcp>=2.8.1",
|
|
16
|
+
"fuzzywuzzy>=0.18.0",
|
|
17
|
+
"humancursor>=1.1.5",
|
|
18
|
+
"ipykernel>=6.30.0",
|
|
19
|
+
"live-inspect>=0.1.1",
|
|
20
|
+
"markdownify>=1.1.0",
|
|
21
|
+
"pdfplumber>=0.11.7",
|
|
22
|
+
"pillow>=11.2.1",
|
|
23
|
+
"posthog>=7.4.0",
|
|
24
|
+
"psutil>=7.0.0",
|
|
25
|
+
"pyautogui>=0.9.54",
|
|
26
|
+
"pygetwindow>=0.0.9",
|
|
27
|
+
"python-dotenv>=1.1.0",
|
|
28
|
+
"python-levenshtein>=0.27.1",
|
|
29
|
+
"pywinauto>=0.6.9",
|
|
30
|
+
"requests>=2.32.3",
|
|
31
|
+
"tabulate>=0.9.0",
|
|
32
|
+
"uiautomation>=2.0.24",
|
|
33
|
+
"uuid7>=0.1.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
windows-mcp = "windows_mcp.__main__:main"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
from windows_mcp.analytics import PostHogAnalytics, with_analytics
|
|
1
2
|
from live_inspect.watch_cursor import WatchCursor
|
|
3
|
+
from windows_mcp.desktop.service import Desktop
|
|
2
4
|
from contextlib import asynccontextmanager
|
|
3
5
|
from fastmcp.utilities.types import Image
|
|
4
|
-
from windows_mcp.desktop.service import Desktop
|
|
5
6
|
from mcp.types import ToolAnnotations
|
|
7
|
+
from typing import Literal, Optional
|
|
6
8
|
from humancursor import SystemCursor
|
|
9
|
+
from dotenv import load_dotenv
|
|
7
10
|
from textwrap import dedent
|
|
8
11
|
from fastmcp import FastMCP
|
|
9
|
-
from typing import Literal
|
|
10
12
|
import pyautogui as pg
|
|
11
13
|
import asyncio
|
|
12
14
|
import click
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
load_dotenv()
|
|
13
18
|
|
|
14
19
|
pg.FAILSAFE=False
|
|
15
20
|
pg.PAUSE=1.0
|
|
@@ -26,6 +31,12 @@ Windows MCP server provides tools to interact directly with the {windows_version
|
|
|
26
31
|
thus enabling to operate the desktop on the user's behalf.
|
|
27
32
|
''')
|
|
28
33
|
|
|
34
|
+
# Initialize analytics at module level to be used in decorators
|
|
35
|
+
if os.getenv("ANONYMIZED_TELEMETRY", "true").lower() == "false":
|
|
36
|
+
analytics = None
|
|
37
|
+
else:
|
|
38
|
+
analytics = PostHogAnalytics()
|
|
39
|
+
|
|
29
40
|
@asynccontextmanager
|
|
30
41
|
async def lifespan(app: FastMCP):
|
|
31
42
|
"""Runs initialization code before the server starts and cleanup code after it shuts down."""
|
|
@@ -35,6 +46,8 @@ async def lifespan(app: FastMCP):
|
|
|
35
46
|
yield
|
|
36
47
|
finally:
|
|
37
48
|
watch_cursor.stop()
|
|
49
|
+
if analytics:
|
|
50
|
+
await analytics.close()
|
|
38
51
|
|
|
39
52
|
mcp=FastMCP(name='windows-mcp',instructions=instructions,lifespan=lifespan)
|
|
40
53
|
|
|
@@ -49,6 +62,7 @@ mcp=FastMCP(name='windows-mcp',instructions=instructions,lifespan=lifespan)
|
|
|
49
62
|
openWorldHint=False
|
|
50
63
|
)
|
|
51
64
|
)
|
|
65
|
+
@with_analytics(analytics, "App-Tool")
|
|
52
66
|
def app_tool(mode:Literal['launch','resize','switch'],name:str|None=None,window_loc:list[int]|None=None,window_size:list[int]|None=None):
|
|
53
67
|
return desktop.app(mode,name,window_loc,window_size)
|
|
54
68
|
|
|
@@ -63,6 +77,7 @@ def app_tool(mode:Literal['launch','resize','switch'],name:str|None=None,window_
|
|
|
63
77
|
openWorldHint=True
|
|
64
78
|
)
|
|
65
79
|
)
|
|
80
|
+
@with_analytics(analytics, "Powershell-Tool")
|
|
66
81
|
def powershell_tool(command: str) -> str:
|
|
67
82
|
response,status_code=desktop.execute_command(command)
|
|
68
83
|
return f'Response: {response}\nStatus Code: {status_code}'
|
|
@@ -78,6 +93,7 @@ def powershell_tool(command: str) -> str:
|
|
|
78
93
|
openWorldHint=False
|
|
79
94
|
)
|
|
80
95
|
)
|
|
96
|
+
@with_analytics(analytics, "State-Tool")
|
|
81
97
|
def state_tool(use_vision:bool=False,use_dom:bool=False):
|
|
82
98
|
# Calculate scale factor to cap resolution at 1080p (1920x1080)
|
|
83
99
|
max_width, max_height = 1920, 1080
|
|
@@ -118,6 +134,7 @@ def state_tool(use_vision:bool=False,use_dom:bool=False):
|
|
|
118
134
|
openWorldHint=False
|
|
119
135
|
)
|
|
120
136
|
)
|
|
137
|
+
@with_analytics(analytics, "Click-Tool")
|
|
121
138
|
def click_tool(loc:list[int],button:Literal['left','right','middle']='left',clicks:int=1)->str:
|
|
122
139
|
if len(loc) != 2:
|
|
123
140
|
raise ValueError("Location must be a list of exactly 2 integers [x, y]")
|
|
@@ -137,6 +154,7 @@ def click_tool(loc:list[int],button:Literal['left','right','middle']='left',clic
|
|
|
137
154
|
openWorldHint=False
|
|
138
155
|
)
|
|
139
156
|
)
|
|
157
|
+
@with_analytics(analytics, "Type-Tool")
|
|
140
158
|
def type_tool(loc:list[int],text:str,clear:bool=False,press_enter:bool=False)->str:
|
|
141
159
|
if len(loc) != 2:
|
|
142
160
|
raise ValueError("Location must be a list of exactly 2 integers [x, y]")
|
|
@@ -155,6 +173,7 @@ def type_tool(loc:list[int],text:str,clear:bool=False,press_enter:bool=False)->s
|
|
|
155
173
|
openWorldHint=False
|
|
156
174
|
)
|
|
157
175
|
)
|
|
176
|
+
@with_analytics(analytics, "Scroll-Tool")
|
|
158
177
|
def scroll_tool(loc:list[int]=None,type:Literal['horizontal','vertical']='vertical',direction:Literal['up','down','left','right']='down',wheel_times:int=1)->str:
|
|
159
178
|
if loc and len(loc) != 2:
|
|
160
179
|
raise ValueError("Location must be a list of exactly 2 integers [x, y]")
|
|
@@ -174,6 +193,7 @@ def scroll_tool(loc:list[int]=None,type:Literal['horizontal','vertical']='vertic
|
|
|
174
193
|
openWorldHint=False
|
|
175
194
|
)
|
|
176
195
|
)
|
|
196
|
+
@with_analytics(analytics, "Drag-Tool")
|
|
177
197
|
def drag_tool(to_loc:list[int])->str:
|
|
178
198
|
if len(to_loc) != 2:
|
|
179
199
|
raise ValueError("to_loc must be a list of exactly 2 integers [x, y]")
|
|
@@ -192,6 +212,7 @@ def drag_tool(to_loc:list[int])->str:
|
|
|
192
212
|
openWorldHint=False
|
|
193
213
|
)
|
|
194
214
|
)
|
|
215
|
+
@with_analytics(analytics, "Move-Tool")
|
|
195
216
|
def move_tool(to_loc:list[int])->str:
|
|
196
217
|
if len(to_loc) != 2:
|
|
197
218
|
raise ValueError("to_loc must be a list of exactly 2 integers [x, y]")
|
|
@@ -210,6 +231,7 @@ def move_tool(to_loc:list[int])->str:
|
|
|
210
231
|
openWorldHint=False
|
|
211
232
|
)
|
|
212
233
|
)
|
|
234
|
+
@with_analytics(analytics, "Shortcut-Tool")
|
|
213
235
|
def shortcut_tool(shortcut:str):
|
|
214
236
|
desktop.shortcut(shortcut)
|
|
215
237
|
return f"Pressed {shortcut}."
|
|
@@ -225,13 +247,14 @@ def shortcut_tool(shortcut:str):
|
|
|
225
247
|
openWorldHint=False
|
|
226
248
|
)
|
|
227
249
|
)
|
|
250
|
+
@with_analytics(analytics, "Wait-Tool")
|
|
228
251
|
def wait_tool(duration:int)->str:
|
|
229
252
|
pg.sleep(duration)
|
|
230
253
|
return f'Waited for {duration} seconds.'
|
|
231
254
|
|
|
232
255
|
@mcp.tool(
|
|
233
256
|
name='Scrape-Tool',
|
|
234
|
-
description='
|
|
257
|
+
description='Fetch content from a URL or the active browser tab. By default (use_dom=False), performs a lightweight HTTP request to the URL and returns markdown content of complete webpage. Note: Some websites may block automated HTTP requests. If this fails, open the page in a browser and retry with use_dom=True to extract visible text from the active tab\'s DOM within the viewport.',
|
|
235
258
|
annotations=ToolAnnotations(
|
|
236
259
|
title="Scrape Tool",
|
|
237
260
|
readOnlyHint=True,
|
|
@@ -240,8 +263,13 @@ def wait_tool(duration:int)->str:
|
|
|
240
263
|
openWorldHint=True
|
|
241
264
|
)
|
|
242
265
|
)
|
|
243
|
-
|
|
244
|
-
|
|
266
|
+
@with_analytics(analytics, "Scrape-Tool")
|
|
267
|
+
def scrape_tool(url:str,use_dom:bool=False)->str:
|
|
268
|
+
if not use_dom:
|
|
269
|
+
content=desktop.scrape(url)
|
|
270
|
+
return f'URL:{url}\nContent:\n{content}'
|
|
271
|
+
|
|
272
|
+
desktop_state=desktop.get_state(use_vision=False,use_dom=use_dom)
|
|
245
273
|
tree_state=desktop_state.tree_state
|
|
246
274
|
if not tree_state.dom_info:
|
|
247
275
|
return f'No DOM information found. Please open {url} in browser first.'
|
|
@@ -250,7 +278,7 @@ def scrape_tool(url:str)->str:
|
|
|
250
278
|
content='\n'.join([node.text for node in tree_state.dom_informative_nodes])
|
|
251
279
|
header_status = "Reached top" if vertical_scroll_percent <= 0 else "Scroll up to see more"
|
|
252
280
|
footer_status = "Reached bottom" if vertical_scroll_percent >= 100 else "Scroll down to see more"
|
|
253
|
-
return f'URL:{url}\nContent:\n{header_status}\n{content}\n{footer_status}'
|
|
281
|
+
return f'URL:{url}\nContent:\n[{header_status}]\n{content}\n[{footer_status}]'
|
|
254
282
|
|
|
255
283
|
|
|
256
284
|
@click.command()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any, TypeVar, Callable, Protocol, Awaitable
|
|
2
|
+
from tempfile import TemporaryDirectory
|
|
3
|
+
from uuid_extensions import uuid7str
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import posthog
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
class Analytics(Protocol):
|
|
18
|
+
async def track_tool(self, tool_name: str, result: Dict[str, Any]) -> None:
|
|
19
|
+
"""Tracks the execution of a tool."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def track_error(self, error: Exception, context: Dict[str, Any]) -> None:
|
|
23
|
+
"""Tracks an error that occurred during the execution of a tool."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
async def is_feature_enabled(self, feature: str) -> bool:
|
|
27
|
+
"""Checks if a feature flag is enabled."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
async def close(self) -> None:
|
|
31
|
+
"""Closes the analytics client."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
class PostHogAnalytics:
|
|
35
|
+
TEMP_FOLDER = Path(TemporaryDirectory().name).parent
|
|
36
|
+
API_KEY = 'phc_uxdCItyVTjXNU0sMPr97dq3tcz39scQNt3qjTYw5vLV'
|
|
37
|
+
HOST = 'https://us.i.posthog.com'
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.client = posthog.Posthog(
|
|
41
|
+
self.API_KEY,
|
|
42
|
+
host=self.HOST,
|
|
43
|
+
disable_geoip=False,
|
|
44
|
+
enable_exception_autocapture=True,
|
|
45
|
+
debug=True
|
|
46
|
+
)
|
|
47
|
+
self._user_id = None
|
|
48
|
+
self.mcp_interaction_id = f"mcp_{int(time.time()*1000)}_{os.getpid()}"
|
|
49
|
+
|
|
50
|
+
if self.client:
|
|
51
|
+
logger.debug(f"Initialized with user ID: {self.user_id} and session ID: {self.mcp_interaction_id}")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def user_id(self) -> str:
|
|
55
|
+
if self._user_id:
|
|
56
|
+
return self._user_id
|
|
57
|
+
|
|
58
|
+
user_id_file = self.TEMP_FOLDER / '.windows-mcp-user-id'
|
|
59
|
+
if user_id_file.exists():
|
|
60
|
+
self._user_id = user_id_file.read_text(encoding='utf-8').strip()
|
|
61
|
+
else:
|
|
62
|
+
self._user_id = uuid7str()
|
|
63
|
+
try:
|
|
64
|
+
user_id_file.write_text(self._user_id, encoding='utf-8')
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.warning(f"Could not persist user ID: {e}")
|
|
67
|
+
|
|
68
|
+
return self._user_id
|
|
69
|
+
|
|
70
|
+
async def track_tool(self, tool_name: str, result: Dict[str, Any]) -> None:
|
|
71
|
+
if self.client:
|
|
72
|
+
self.client.capture(
|
|
73
|
+
distinct_id=self.user_id,
|
|
74
|
+
event="tool_executed",
|
|
75
|
+
properties={
|
|
76
|
+
"tool_name": tool_name,
|
|
77
|
+
"session_id": self.mcp_interaction_id,
|
|
78
|
+
"process_person_profile": True,
|
|
79
|
+
**result
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
duration = result.get("duration_ms", 0)
|
|
84
|
+
success_mark = "SUCCESS" if result.get("success") else "FAILED"
|
|
85
|
+
# Using print for immediate visibility in console during debugging
|
|
86
|
+
print(f"[Analytics] {tool_name}: {success_mark} ({duration}ms)")
|
|
87
|
+
logger.info(f"{tool_name}: {success_mark} ({duration}ms)")
|
|
88
|
+
if self.client:
|
|
89
|
+
self.client.flush()
|
|
90
|
+
|
|
91
|
+
async def track_error(self, error: Exception, context: Dict[str, Any]) -> None:
|
|
92
|
+
if self.client:
|
|
93
|
+
self.client.capture(
|
|
94
|
+
distinct_id=self.user_id,
|
|
95
|
+
event="exception",
|
|
96
|
+
properties={
|
|
97
|
+
"exception": str(error),
|
|
98
|
+
"traceback": str(error) if not hasattr(error, '__traceback__') else str(error),
|
|
99
|
+
"session_id": self.mcp_interaction_id,
|
|
100
|
+
"process_person_profile": True,
|
|
101
|
+
**context
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if self.client:
|
|
106
|
+
self.client.flush()
|
|
107
|
+
|
|
108
|
+
logger.error(f"ERROR in {context.get('tool_name')}: {error}")
|
|
109
|
+
|
|
110
|
+
async def is_feature_enabled(self, feature: str) -> bool:
|
|
111
|
+
if not self.client:
|
|
112
|
+
return False
|
|
113
|
+
return self.client.is_feature_enabled(feature, self.user_id)
|
|
114
|
+
|
|
115
|
+
async def close(self) -> None:
|
|
116
|
+
if self.client:
|
|
117
|
+
self.client.shutdown()
|
|
118
|
+
logger.debug("Closed analytics")
|
|
119
|
+
|
|
120
|
+
def with_analytics(analytics_instance: Optional[Analytics], tool_name: str):
|
|
121
|
+
"""
|
|
122
|
+
Decorator to wrap tool functions with analytics tracking.
|
|
123
|
+
"""
|
|
124
|
+
def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
|
|
125
|
+
@wraps(func)
|
|
126
|
+
async def wrapper(*args, **kwargs) -> T:
|
|
127
|
+
start = time.time()
|
|
128
|
+
try:
|
|
129
|
+
if asyncio.iscoroutinefunction(func):
|
|
130
|
+
result = await func(*args, **kwargs)
|
|
131
|
+
else:
|
|
132
|
+
# Run sync function in thread to avoid blocking loop
|
|
133
|
+
result = await asyncio.to_thread(func, *args, **kwargs)
|
|
134
|
+
|
|
135
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
136
|
+
|
|
137
|
+
if analytics_instance:
|
|
138
|
+
await analytics_instance.track_tool(tool_name, {"duration_ms": duration_ms, "success": True})
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
except Exception as error:
|
|
142
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
143
|
+
if analytics_instance:
|
|
144
|
+
await analytics_instance.track_error(error, {
|
|
145
|
+
"tool_name": tool_name,
|
|
146
|
+
"duration_ms": duration_ms
|
|
147
|
+
})
|
|
148
|
+
raise error
|
|
149
|
+
return wrapper
|
|
150
|
+
return decorator
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from windows_mcp.desktop.config import BROWSER_NAMES, PROCESS_PER_MONITOR_DPI_AWARE
|
|
2
2
|
from windows_mcp.desktop.views import DesktopState, App, Size, Status
|
|
3
|
+
from windows_mcp.tree.service import Tree
|
|
3
4
|
from locale import getpreferredencoding
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from typing import Optional,Literal
|
|
6
7
|
from markdownify import markdownify
|
|
7
|
-
from windows_mcp.tree.service import Tree
|
|
8
8
|
from fuzzywuzzy import process
|
|
9
9
|
from psutil import Process
|
|
10
10
|
from time import sleep
|
|
@@ -172,7 +172,7 @@ class Desktop:
|
|
|
172
172
|
sleep(1.25)
|
|
173
173
|
if status!=0:
|
|
174
174
|
return response
|
|
175
|
-
consecutive_waits=
|
|
175
|
+
consecutive_waits=10
|
|
176
176
|
for _ in range(consecutive_waits):
|
|
177
177
|
if not self.is_app_running(name):
|
|
178
178
|
sleep(1.25)
|
|
@@ -200,11 +200,12 @@ class Desktop:
|
|
|
200
200
|
app_name,_=matched_app
|
|
201
201
|
appid=apps_map.get(app_name)
|
|
202
202
|
if appid is None:
|
|
203
|
-
return (
|
|
204
|
-
if
|
|
205
|
-
|
|
203
|
+
return (f'{name.title()} not found in start menu.',1)
|
|
204
|
+
if appid.endswith('.exe'):
|
|
205
|
+
command=f"Start-Process '{appid}'"
|
|
206
206
|
else:
|
|
207
|
-
|
|
207
|
+
command=f"Start-Process shell:AppsFolder\\{appid}"
|
|
208
|
+
response,status=self.execute_command(command)
|
|
208
209
|
return response,status
|
|
209
210
|
|
|
210
211
|
def switch_app(self,name:str):
|
|
@@ -63,6 +63,15 @@ wheels = [
|
|
|
63
63
|
{ url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981, upload-time = "2025-05-23T00:21:43.075Z" },
|
|
64
64
|
]
|
|
65
65
|
|
|
66
|
+
[[package]]
|
|
67
|
+
name = "backoff"
|
|
68
|
+
version = "2.2.1"
|
|
69
|
+
source = { registry = "https://pypi.org/simple" }
|
|
70
|
+
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
|
|
71
|
+
wheels = [
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
|
73
|
+
]
|
|
74
|
+
|
|
66
75
|
[[package]]
|
|
67
76
|
name = "beautifulsoup4"
|
|
68
77
|
version = "4.13.4"
|
|
@@ -246,6 +255,15 @@ wheels = [
|
|
|
246
255
|
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
|
|
247
256
|
]
|
|
248
257
|
|
|
258
|
+
[[package]]
|
|
259
|
+
name = "distro"
|
|
260
|
+
version = "1.9.0"
|
|
261
|
+
source = { registry = "https://pypi.org/simple" }
|
|
262
|
+
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
|
263
|
+
wheels = [
|
|
264
|
+
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
|
265
|
+
]
|
|
266
|
+
|
|
249
267
|
[[package]]
|
|
250
268
|
name = "exceptiongroup"
|
|
251
269
|
version = "1.3.0"
|
|
@@ -733,6 +751,23 @@ wheels = [
|
|
|
733
751
|
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
|
|
734
752
|
]
|
|
735
753
|
|
|
754
|
+
[[package]]
|
|
755
|
+
name = "posthog"
|
|
756
|
+
version = "7.4.0"
|
|
757
|
+
source = { registry = "https://pypi.org/simple" }
|
|
758
|
+
dependencies = [
|
|
759
|
+
{ name = "backoff" },
|
|
760
|
+
{ name = "distro" },
|
|
761
|
+
{ name = "python-dateutil" },
|
|
762
|
+
{ name = "requests" },
|
|
763
|
+
{ name = "six" },
|
|
764
|
+
{ name = "typing-extensions" },
|
|
765
|
+
]
|
|
766
|
+
sdist = { url = "https://files.pythonhosted.org/packages/14/e5/5262d1604a3eb19b23d4e896bce87b4603fd39ec366a96b27e19e3299aef/posthog-7.4.0.tar.gz", hash = "sha256:1fb97b11960e24fcf0b80f0a6450b2311478e5a3ee6ea3c6f9284ff89060a876", size = 143780, upload-time = "2025-12-16T23:42:05.829Z" }
|
|
767
|
+
wheels = [
|
|
768
|
+
{ url = "https://files.pythonhosted.org/packages/9f/8b/13066693d7a6f94fb5da3407417bbbc3f6aa8487051294d0ef766c1567fa/posthog-7.4.0-py3-none-any.whl", hash = "sha256:f9d4e32c1c0f2110256b1aae7046ed90af312c1dbb1eecc6a9cb427733b22970", size = 166079, upload-time = "2025-12-16T23:42:04.33Z" },
|
|
769
|
+
]
|
|
770
|
+
|
|
736
771
|
[[package]]
|
|
737
772
|
name = "prompt-toolkit"
|
|
738
773
|
version = "3.0.51"
|
|
@@ -1394,6 +1429,15 @@ socks = [
|
|
|
1394
1429
|
{ name = "pysocks" },
|
|
1395
1430
|
]
|
|
1396
1431
|
|
|
1432
|
+
[[package]]
|
|
1433
|
+
name = "uuid7"
|
|
1434
|
+
version = "0.1.0"
|
|
1435
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1436
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5c/19/7472bd526591e2192926247109dbf78692e709d3e56775792fec877a7720/uuid7-0.1.0.tar.gz", hash = "sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c", size = 14052, upload-time = "2021-12-29T01:38:21.897Z" }
|
|
1437
|
+
wheels = [
|
|
1438
|
+
{ url = "https://files.pythonhosted.org/packages/b5/77/8852f89a91453956582a85024d80ad96f30a41fed4c2b3dce0c9f12ecc7e/uuid7-0.1.0-py2.py3-none-any.whl", hash = "sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61", size = 7477, upload-time = "2021-12-29T01:38:20.418Z" },
|
|
1439
|
+
]
|
|
1440
|
+
|
|
1397
1441
|
[[package]]
|
|
1398
1442
|
name = "uvicorn"
|
|
1399
1443
|
version = "0.34.3"
|
|
@@ -1427,7 +1471,7 @@ wheels = [
|
|
|
1427
1471
|
|
|
1428
1472
|
[[package]]
|
|
1429
1473
|
name = "windows-mcp"
|
|
1430
|
-
version = "0.5.
|
|
1474
|
+
version = "0.5.6"
|
|
1431
1475
|
source = { editable = "." }
|
|
1432
1476
|
dependencies = [
|
|
1433
1477
|
{ name = "click" },
|
|
@@ -1439,14 +1483,17 @@ dependencies = [
|
|
|
1439
1483
|
{ name = "markdownify" },
|
|
1440
1484
|
{ name = "pdfplumber" },
|
|
1441
1485
|
{ name = "pillow" },
|
|
1486
|
+
{ name = "posthog" },
|
|
1442
1487
|
{ name = "psutil" },
|
|
1443
1488
|
{ name = "pyautogui" },
|
|
1444
1489
|
{ name = "pygetwindow" },
|
|
1490
|
+
{ name = "python-dotenv" },
|
|
1445
1491
|
{ name = "python-levenshtein" },
|
|
1446
1492
|
{ name = "pywinauto" },
|
|
1447
1493
|
{ name = "requests" },
|
|
1448
1494
|
{ name = "tabulate" },
|
|
1449
1495
|
{ name = "uiautomation" },
|
|
1496
|
+
{ name = "uuid7" },
|
|
1450
1497
|
]
|
|
1451
1498
|
|
|
1452
1499
|
[package.metadata]
|
|
@@ -1460,14 +1507,17 @@ requires-dist = [
|
|
|
1460
1507
|
{ name = "markdownify", specifier = ">=1.1.0" },
|
|
1461
1508
|
{ name = "pdfplumber", specifier = ">=0.11.7" },
|
|
1462
1509
|
{ name = "pillow", specifier = ">=11.2.1" },
|
|
1510
|
+
{ name = "posthog", specifier = ">=7.4.0" },
|
|
1463
1511
|
{ name = "psutil", specifier = ">=7.0.0" },
|
|
1464
1512
|
{ name = "pyautogui", specifier = ">=0.9.54" },
|
|
1465
1513
|
{ name = "pygetwindow", specifier = ">=0.0.9" },
|
|
1514
|
+
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
|
1466
1515
|
{ name = "python-levenshtein", specifier = ">=0.27.1" },
|
|
1467
1516
|
{ name = "pywinauto", specifier = ">=0.6.9" },
|
|
1468
1517
|
{ name = "requests", specifier = ">=2.32.3" },
|
|
1469
1518
|
{ name = "tabulate", specifier = ">=0.9.0" },
|
|
1470
1519
|
{ name = "uiautomation", specifier = ">=2.0.24" },
|
|
1520
|
+
{ name = "uuid7", specifier = ">=0.1.0" },
|
|
1471
1521
|
]
|
|
1472
1522
|
|
|
1473
1523
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|