windows-mcp 0.5.5__tar.gz → 0.5.7__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.
Files changed (30) hide show
  1. windows_mcp-0.5.7/.mcpbignore +5 -0
  2. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/PKG-INFO +28 -4
  3. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/README.md +23 -2
  4. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/manifest.json +17 -5
  5. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/pyproject.toml +43 -40
  6. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/__main__.py +45 -17
  7. windows_mcp-0.5.7/src/windows_mcp/analytics.py +171 -0
  8. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/desktop/service.py +8 -7
  9. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/uv.lock +55 -5
  10. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/.gitignore +0 -0
  11. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/.python-version +0 -0
  12. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/CONTRIBUTING.md +0 -0
  13. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/LICENSE.md +0 -0
  14. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/SECURITY.md +0 -0
  15. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/assets/demo1.mov +0 -0
  16. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/assets/demo2.mov +0 -0
  17. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/assets/logo.png +0 -0
  18. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/assets/screenshots/screenshot_1.png +0 -0
  19. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/assets/screenshots/screenshot_2.png +0 -0
  20. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/assets/screenshots/screenshot_3.png +0 -0
  21. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/server.json +0 -0
  22. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/__init__.py +0 -0
  23. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/desktop/__init__.py +0 -0
  24. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/desktop/config.py +0 -0
  25. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/desktop/views.py +0 -0
  26. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/tree/__init__.py +0 -0
  27. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/tree/config.py +0 -0
  28. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/tree/service.py +0 -0
  29. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/tree/utils.py +0 -0
  30. {windows_mcp-0.5.5 → windows_mcp-0.5.7}/src/windows_mcp/tree/views.py +0 -0
@@ -0,0 +1,5 @@
1
+ .venv
2
+ __pycache__
3
+ build
4
+ dist
5
+ notebook.ipynb
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: windows-mcp
3
- Version: 0.5.5
3
+ Version: 0.5.7
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>
@@ -33,18 +33,21 @@ Requires-Dist: fastmcp>=2.8.1
33
33
  Requires-Dist: fuzzywuzzy>=0.18.0
34
34
  Requires-Dist: humancursor>=1.1.5
35
35
  Requires-Dist: ipykernel>=6.30.0
36
- Requires-Dist: live-inspect>=0.1.1
36
+ Requires-Dist: live-inspect>=0.1.2
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
  [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/cursortouch-windows-mcp-badge.png)](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 usage data to help improve the MCP server. 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 usage data to help improve the MCP server. 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,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.2",
3
3
  "name": "Windows-MCP",
4
- "version": "0.5.0",
4
+ "version": "0.5.6",
5
5
  "description": "MCP Server that enables Claude to interact with Windows OS",
6
6
  "long_description": "Windows MCP is an open-source project that enables seamless integration between AI agents and the Windows operating system. Acting as an MCP server bridges the gap between LLMs and the Windows operating system, allowing agents to perform tasks such as **file navigation, application control, UI interaction, QA testing,** and more.\\n\\n## Key Features\\n\\n- **Seamless Windows Integration**: Interacts natively with Windows UI elements, opens apps, controls windows, simulates user input, and more.\\n- **Use Any LLM (Vision Optional)**: Unlike many automation tools, Windows MCP doesn't rely on any traditional computer vision techniques or specific fine-tuned models; it works with any LLMs, reducing complexity and setup time.\\n- **Rich Toolset for UI Automation**: Includes tools for basic keyboard, mouse operation and capturing window/UI state.\\n- **Lightweight & Open-Source**: Minimal dependencies and easy setup with full source code available under MIT license.\\n- **Customizable & Extendable**: Easily adapt or extend tools to suit your unique automation or AI integration needs.\\n- **Real-Time Interaction**: Typical latency between actions (e.g., from one mouse click to the next) ranges from **1.5 to 2.3 secs**, and may slightly vary based on the number of active applications and system load, also the inferencing speed of the llm.\\n\\n## Requirements\\n\\n### UV Package Manager\\nThis MCP server requires [UV](https://github.com/astral-sh/uv), a fast Python package manager. \\n\\n```bash\\npip install uv\\n```\\n\\nFor detailed installation instructions, see the [UV documentation](https://github.com/astral-sh/uv#installation).",
7
7
  "author": {
@@ -19,15 +19,27 @@
19
19
  ],
20
20
  "server": {
21
21
  "type": "python",
22
- "entry_point": "main.py",
22
+ "entry_point": "./src/windows_mcp/__main__.py",
23
23
  "mcp_config": {
24
24
  "command": "uv",
25
25
  "args": [
26
26
  "--directory",
27
27
  "${__dirname}",
28
28
  "run",
29
- "main.py"
30
- ]
29
+ "windows-mcp"
30
+ ],
31
+ "env": {
32
+ "ANONYMIZED_TELEMETRY": "${user_config.anonymized_telemetry}"
33
+ }
34
+ }
35
+ },
36
+ "user_config": {
37
+ "anonymized_telemetry": {
38
+ "type": "boolean",
39
+ "title": "Anonymized Telemetry",
40
+ "description": "Windows-MCP collects usage data to help improve the MCP server. No personal information, no tool arguments, no outputs are tracked.",
41
+ "required": false,
42
+ "default": true
31
43
  }
32
44
  },
33
45
  "tools": [
@@ -73,7 +85,7 @@
73
85
  },
74
86
  {
75
87
  "name": "Scrape Tool",
76
- "description": "Fetch and convert webpage content to markdown format. Provide full URL including protocol (http/https). Returns structured text content suitable for analysis."
88
+ "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."
77
89
  }
78
90
  ],
79
91
  "compatibility": {
@@ -1,40 +1,43 @@
1
- [project]
2
- name = "windows-mcp"
3
- version = "0.5.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
- "psutil>=7.0.0",
24
- "pyautogui>=0.9.54",
25
- "pygetwindow>=0.0.9",
26
- "python-levenshtein>=0.27.1",
27
- "pywinauto>=0.6.9",
28
- "requests>=2.32.3",
29
- "tabulate>=0.9.0",
30
- "uiautomation>=2.0.24",
31
- ]
32
-
33
- [project.scripts]
34
- windows-mcp = "windows_mcp.__main__:main"
35
-
36
- [build-system]
37
- requires = ["hatchling"]
38
- build-backend = "hatchling.build"
39
-
40
-
1
+ [project]
2
+ name = "windows-mcp"
3
+ version = "0.5.7"
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.2",
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 fastmcp import FastMCP, Context
10
+ from dotenv import load_dotenv
7
11
  from textwrap import dedent
8
- 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,7 +62,8 @@ mcp=FastMCP(name='windows-mcp',instructions=instructions,lifespan=lifespan)
49
62
  openWorldHint=False
50
63
  )
51
64
  )
52
- def app_tool(mode:Literal['launch','resize','switch'],name:str|None=None,window_loc:list[int]|None=None,window_size:list[int]|None=None):
65
+ @with_analytics(analytics, "App-Tool")
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, ctx: Context = None):
53
67
  return desktop.app(mode,name,window_loc,window_size)
54
68
 
55
69
  @mcp.tool(
@@ -63,7 +77,8 @@ def app_tool(mode:Literal['launch','resize','switch'],name:str|None=None,window_
63
77
  openWorldHint=True
64
78
  )
65
79
  )
66
- def powershell_tool(command: str) -> str:
80
+ @with_analytics(analytics, "Powershell-Tool")
81
+ def powershell_tool(command: str, ctx: Context = None) -> str:
67
82
  response,status_code=desktop.execute_command(command)
68
83
  return f'Response: {response}\nStatus Code: {status_code}'
69
84
 
@@ -78,7 +93,8 @@ def powershell_tool(command: str) -> str:
78
93
  openWorldHint=False
79
94
  )
80
95
  )
81
- def state_tool(use_vision:bool=False,use_dom:bool=False):
96
+ @with_analytics(analytics, "State-Tool")
97
+ def state_tool(use_vision:bool=False,use_dom:bool=False, ctx: Context = None):
82
98
  # Calculate scale factor to cap resolution at 1080p (1920x1080)
83
99
  max_width, max_height = 1920, 1080
84
100
  scale_width = max_width / screen_width if screen_width > max_width else 1.0
@@ -118,7 +134,8 @@ def state_tool(use_vision:bool=False,use_dom:bool=False):
118
134
  openWorldHint=False
119
135
  )
120
136
  )
121
- def click_tool(loc:list[int],button:Literal['left','right','middle']='left',clicks:int=1)->str:
137
+ @with_analytics(analytics, "Click-Tool")
138
+ def click_tool(loc:list[int],button:Literal['left','right','middle']='left',clicks:int=1, ctx: Context = None)->str:
122
139
  if len(loc) != 2:
123
140
  raise ValueError("Location must be a list of exactly 2 integers [x, y]")
124
141
  x,y=loc[0],loc[1]
@@ -137,7 +154,8 @@ def click_tool(loc:list[int],button:Literal['left','right','middle']='left',clic
137
154
  openWorldHint=False
138
155
  )
139
156
  )
140
- def type_tool(loc:list[int],text:str,clear:bool=False,press_enter:bool=False)->str:
157
+ @with_analytics(analytics, "Type-Tool")
158
+ def type_tool(loc:list[int],text:str,clear:bool=False,press_enter:bool=False, ctx: Context = None)->str:
141
159
  if len(loc) != 2:
142
160
  raise ValueError("Location must be a list of exactly 2 integers [x, y]")
143
161
  x,y=loc[0],loc[1]
@@ -155,7 +173,8 @@ def type_tool(loc:list[int],text:str,clear:bool=False,press_enter:bool=False)->s
155
173
  openWorldHint=False
156
174
  )
157
175
  )
158
- def scroll_tool(loc:list[int]=None,type:Literal['horizontal','vertical']='vertical',direction:Literal['up','down','left','right']='down',wheel_times:int=1)->str:
176
+ @with_analytics(analytics, "Scroll-Tool")
177
+ def scroll_tool(loc:list[int]=None,type:Literal['horizontal','vertical']='vertical',direction:Literal['up','down','left','right']='down',wheel_times:int=1, ctx: Context = None)->str:
159
178
  if loc and len(loc) != 2:
160
179
  raise ValueError("Location must be a list of exactly 2 integers [x, y]")
161
180
  response=desktop.scroll(loc,type,direction,wheel_times)
@@ -174,7 +193,8 @@ def scroll_tool(loc:list[int]=None,type:Literal['horizontal','vertical']='vertic
174
193
  openWorldHint=False
175
194
  )
176
195
  )
177
- def drag_tool(to_loc:list[int])->str:
196
+ @with_analytics(analytics, "Drag-Tool")
197
+ def drag_tool(to_loc:list[int], ctx: Context = None)->str:
178
198
  if len(to_loc) != 2:
179
199
  raise ValueError("to_loc must be a list of exactly 2 integers [x, y]")
180
200
  desktop.drag(to_loc)
@@ -192,7 +212,8 @@ def drag_tool(to_loc:list[int])->str:
192
212
  openWorldHint=False
193
213
  )
194
214
  )
195
- def move_tool(to_loc:list[int])->str:
215
+ @with_analytics(analytics, "Move-Tool")
216
+ def move_tool(to_loc:list[int], ctx: Context = None)->str:
196
217
  if len(to_loc) != 2:
197
218
  raise ValueError("to_loc must be a list of exactly 2 integers [x, y]")
198
219
  x,y=to_loc[0],to_loc[1]
@@ -210,7 +231,8 @@ def move_tool(to_loc:list[int])->str:
210
231
  openWorldHint=False
211
232
  )
212
233
  )
213
- def shortcut_tool(shortcut:str):
234
+ @with_analytics(analytics, "Shortcut-Tool")
235
+ def shortcut_tool(shortcut:str, ctx: Context = None):
214
236
  desktop.shortcut(shortcut)
215
237
  return f"Pressed {shortcut}."
216
238
 
@@ -225,13 +247,14 @@ def shortcut_tool(shortcut:str):
225
247
  openWorldHint=False
226
248
  )
227
249
  )
228
- def wait_tool(duration:int)->str:
250
+ @with_analytics(analytics, "Wait-Tool")
251
+ def wait_tool(duration:int, ctx: Context = None)->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='Extracts visible text content from the currently focused browser tab. Returns content in plain text format with scroll status indicators (top/bottom reached or more content available). Only works when a browser with DOM is active. Use State-Tool with use_dom=True first to ensure browser is ready.',
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
- def scrape_tool(url:str)->str:
244
- desktop_state=desktop.desktop_state
266
+ @with_analytics(analytics, "Scrape-Tool")
267
+ def scrape_tool(url:str,use_dom:bool=False, ctx: Context = None)->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,171 @@
1
+ from typing import Optional, Dict, Any, TypeVar, Callable, Protocol, Awaitable
2
+ from tempfile import TemporaryDirectory
3
+ from uuid_extensions import uuid7str
4
+ from fastmcp import Context
5
+ from functools import wraps
6
+ from pathlib import Path
7
+ import posthog
8
+ import asyncio
9
+ import logging
10
+ import time
11
+ import os
12
+
13
+ logging.basicConfig(level=logging.DEBUG)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ T = TypeVar("T")
17
+
18
+ class Analytics(Protocol):
19
+ async def track_tool(self, tool_name: str, result: Dict[str, Any]) -> None:
20
+ """Tracks the execution of a tool."""
21
+ ...
22
+
23
+ async def track_error(self, error: Exception, context: Dict[str, Any]) -> None:
24
+ """Tracks an error that occurred during the execution of a tool."""
25
+ ...
26
+
27
+ async def is_feature_enabled(self, feature: str) -> bool:
28
+ """Checks if a feature flag is enabled."""
29
+ ...
30
+
31
+ async def close(self) -> None:
32
+ """Closes the analytics client."""
33
+ ...
34
+
35
+ class PostHogAnalytics:
36
+ TEMP_FOLDER = Path(TemporaryDirectory().name).parent
37
+ API_KEY = 'phc_uxdCItyVTjXNU0sMPr97dq3tcz39scQNt3qjTYw5vLV'
38
+ HOST = 'https://us.i.posthog.com'
39
+
40
+ def __init__(self):
41
+ self.client = posthog.Posthog(
42
+ self.API_KEY,
43
+ host=self.HOST,
44
+ disable_geoip=False,
45
+ enable_exception_autocapture=True,
46
+ debug=True
47
+ )
48
+ self._user_id = None
49
+ self.mcp_interaction_id = f"mcp_{int(time.time()*1000)}_{os.getpid()}"
50
+
51
+ if self.client:
52
+ logger.debug(f"Initialized with user ID: {self.user_id} and session ID: {self.mcp_interaction_id}")
53
+
54
+ @property
55
+ def user_id(self) -> str:
56
+ if self._user_id:
57
+ return self._user_id
58
+
59
+ user_id_file = self.TEMP_FOLDER / '.windows-mcp-user-id'
60
+ if user_id_file.exists():
61
+ self._user_id = user_id_file.read_text(encoding='utf-8').strip()
62
+ else:
63
+ self._user_id = uuid7str()
64
+ try:
65
+ user_id_file.write_text(self._user_id, encoding='utf-8')
66
+ except Exception as e:
67
+ logger.warning(f"Could not persist user ID: {e}")
68
+
69
+ return self._user_id
70
+
71
+ async def track_tool(self, tool_name: str, result: Dict[str, Any]) -> None:
72
+ if self.client:
73
+ self.client.capture(
74
+ distinct_id=self.user_id,
75
+ event="tool_executed",
76
+ properties={
77
+ "tool_name": tool_name,
78
+ "session_id": self.mcp_interaction_id,
79
+ "process_person_profile": True,
80
+ **result
81
+ }
82
+ )
83
+
84
+ duration = result.get("duration_ms", 0)
85
+ success_mark = "SUCCESS" if result.get("success") else "FAILED"
86
+ # Using print for immediate visibility in console during debugging
87
+ print(f"[Analytics] {tool_name}: {success_mark} ({duration}ms)")
88
+ logger.info(f"{tool_name}: {success_mark} ({duration}ms)")
89
+ if self.client:
90
+ self.client.flush()
91
+
92
+ async def track_error(self, error: Exception, context: Dict[str, Any]) -> None:
93
+ if self.client:
94
+ self.client.capture(
95
+ distinct_id=self.user_id,
96
+ event="exception",
97
+ properties={
98
+ "exception": str(error),
99
+ "traceback": str(error) if not hasattr(error, '__traceback__') else str(error),
100
+ "session_id": self.mcp_interaction_id,
101
+ "process_person_profile": True,
102
+ **context
103
+ }
104
+ )
105
+
106
+ if self.client:
107
+ self.client.flush()
108
+
109
+ logger.error(f"ERROR in {context.get('tool_name')}: {error}")
110
+
111
+ async def is_feature_enabled(self, feature: str) -> bool:
112
+ if not self.client:
113
+ return False
114
+ return self.client.is_feature_enabled(feature, self.user_id)
115
+
116
+ async def close(self) -> None:
117
+ if self.client:
118
+ self.client.shutdown()
119
+ logger.debug("Closed analytics")
120
+
121
+ def with_analytics(analytics_instance: Optional[Analytics], tool_name: str):
122
+ """
123
+ Decorator to wrap tool functions with analytics tracking.
124
+ """
125
+ def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
126
+ @wraps(func)
127
+ async def wrapper(*args, **kwargs) -> T:
128
+ start = time.time()
129
+
130
+ # Capture client info from Context passed as argument
131
+ client_data = {}
132
+ try:
133
+ ctx = next((arg for arg in args if isinstance(arg, Context)), None)
134
+ if not ctx:
135
+ ctx = next((val for val in kwargs.values() if isinstance(val, Context)), None)
136
+
137
+ if ctx and ctx.session and ctx.session.client_params and ctx.session.client_params.clientInfo:
138
+ info = ctx.session.client_params.clientInfo
139
+ client_data["client_name"] = info.name
140
+ client_data["client_version"] = info.version
141
+ except Exception:
142
+ pass
143
+
144
+ try:
145
+ if asyncio.iscoroutinefunction(func):
146
+ result = await func(*args, **kwargs)
147
+ else:
148
+ # Run sync function in thread to avoid blocking loop
149
+ result = await asyncio.to_thread(func, *args, **kwargs)
150
+
151
+ duration_ms = int((time.time() - start) * 1000)
152
+
153
+ if analytics_instance:
154
+ await analytics_instance.track_tool(tool_name, {
155
+ "duration_ms": duration_ms,
156
+ "success": True,
157
+ **client_data
158
+ })
159
+
160
+ return result
161
+ except Exception as error:
162
+ duration_ms = int((time.time() - start) * 1000)
163
+ if analytics_instance:
164
+ await analytics_instance.track_error(error, {
165
+ "tool_name": tool_name,
166
+ "duration_ms": duration_ms,
167
+ **client_data
168
+ })
169
+ raise error
170
+ return wrapper
171
+ 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
@@ -53,7 +53,7 @@ class Desktop:
53
53
  sleep(0.1)
54
54
  apps=self.get_apps()
55
55
  active_app=self.get_active_app()
56
- if active_app is not None:
56
+ if active_app is not None and active_app in apps:
57
57
  apps.remove(active_app)
58
58
  logger.debug(f"Active app: {active_app}")
59
59
  logger.debug(f"Apps: {apps}")
@@ -172,7 +172,7 @@ class Desktop:
172
172
  sleep(1.25)
173
173
  if status!=0:
174
174
  return response
175
- consecutive_waits=3
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 (name,f'{name.title()} not found in start menu.',1)
204
- if name.endswith('.exe'):
205
- response,status=self.execute_command(f'Start-Process {appid}')
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
- response,status=self.execute_command(f'Start-Process shell:AppsFolder\\{appid}')
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"
@@ -488,15 +506,15 @@ wheels = [
488
506
 
489
507
  [[package]]
490
508
  name = "live-inspect"
491
- version = "0.1.1"
509
+ version = "0.1.2"
492
510
  source = { registry = "https://pypi.org/simple" }
493
511
  dependencies = [
494
512
  { name = "clr" },
495
513
  { name = "pythonnet" },
496
514
  ]
497
- sdist = { url = "https://files.pythonhosted.org/packages/49/19/17117df902e41bff25d0880b7e72ea7def90c0f575d9704abc8707728e1f/live_inspect-0.1.1.tar.gz", hash = "sha256:b2beaf0a74dd8656aeac9343810e09c5a8efa83c2ce183577040cae7e3acd80a", size = 235834, upload-time = "2025-06-25T14:04:00.17Z" }
515
+ sdist = { url = "https://files.pythonhosted.org/packages/57/e8/720c2b160126c19d1b03decc7db4f5101f05c8a6e83875da845eb383f6a5/live_inspect-0.1.2.tar.gz", hash = "sha256:c78e4f59f080c8c53486fa4a8686651b5af0fcbec43c9cd651c5eb703df9a19d", size = 235924, upload-time = "2025-12-19T21:11:36.083Z" }
498
516
  wheels = [
499
- { url = "https://files.pythonhosted.org/packages/45/f7/3ee6a7ddb88ad0bf8e4fbc581f90a55079b383c1c1ac229917d6da7dd198/live_inspect-0.1.1-py3-none-any.whl", hash = "sha256:406daee02db1d5b76783ad61952582caf76aae02c816ad466d788f6dd671199d", size = 237608, upload-time = "2025-06-25T14:03:58.638Z" },
517
+ { url = "https://files.pythonhosted.org/packages/84/98/ad29db410762e9415a5171b0da7d6a282f45070f2ca13df12fd084c0072f/live_inspect-0.1.2-py3-none-any.whl", hash = "sha256:de1966789a3d745c33dbf914dea81e42c7dcb7f5561ce4fe3deefb9cbfbbbf73", size = 237670, upload-time = "2025-12-19T21:11:34.013Z" },
500
518
  ]
501
519
 
502
520
  [[package]]
@@ -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.4"
1474
+ version = "0.5.7"
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]
@@ -1456,18 +1503,21 @@ requires-dist = [
1456
1503
  { name = "fuzzywuzzy", specifier = ">=0.18.0" },
1457
1504
  { name = "humancursor", specifier = ">=1.1.5" },
1458
1505
  { name = "ipykernel", specifier = ">=6.30.0" },
1459
- { name = "live-inspect", specifier = ">=0.1.1" },
1506
+ { name = "live-inspect", specifier = ">=0.1.2" },
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