windows-mcp 0.5.6__py3-none-any.whl → 0.5.8__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/analytics.py CHANGED
@@ -1,150 +1,175 @@
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
+ 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
+ logger = logging.getLogger(__name__)
14
+ logger.setLevel(logging.INFO)
15
+ handler = logging.StreamHandler()
16
+ formatter = logging.Formatter('[%(levelname)s] %(message)s')
17
+ handler.setFormatter(formatter)
18
+ logger.addHandler(handler)
19
+
20
+ T = TypeVar("T")
21
+
22
+ class Analytics(Protocol):
23
+ async def track_tool(self, tool_name: str, result: Dict[str, Any]) -> None:
24
+ """Tracks the execution of a tool."""
25
+ ...
26
+
27
+ async def track_error(self, error: Exception, context: Dict[str, Any]) -> None:
28
+ """Tracks an error that occurred during the execution of a tool."""
29
+ ...
30
+
31
+ async def is_feature_enabled(self, feature: str) -> bool:
32
+ """Checks if a feature flag is enabled."""
33
+ ...
34
+
35
+ async def close(self) -> None:
36
+ """Closes the analytics client."""
37
+ ...
38
+
39
+ class PostHogAnalytics:
40
+ TEMP_FOLDER = Path(TemporaryDirectory().name).parent
41
+ API_KEY = 'phc_uxdCItyVTjXNU0sMPr97dq3tcz39scQNt3qjTYw5vLV'
42
+ HOST = 'https://us.i.posthog.com'
43
+
44
+ def __init__(self):
45
+ self.client = posthog.Posthog(
46
+ self.API_KEY,
47
+ host=self.HOST,
48
+ disable_geoip=False,
49
+ enable_exception_autocapture=True,
50
+ debug=False
51
+ )
52
+ self._user_id = None
53
+ self.mcp_interaction_id = f"mcp_{int(time.time()*1000)}_{os.getpid()}"
54
+
55
+ if self.client:
56
+ logger.debug(f"Initialized with user ID: {self.user_id} and session ID: {self.mcp_interaction_id}")
57
+
58
+ @property
59
+ def user_id(self) -> str:
60
+ if self._user_id:
61
+ return self._user_id
62
+
63
+ user_id_file = self.TEMP_FOLDER / '.windows-mcp-user-id'
64
+ if user_id_file.exists():
65
+ self._user_id = user_id_file.read_text(encoding='utf-8').strip()
66
+ else:
67
+ self._user_id = uuid7str()
68
+ try:
69
+ user_id_file.write_text(self._user_id, encoding='utf-8')
70
+ except Exception as e:
71
+ logger.warning(f"Could not persist user ID: {e}")
72
+
73
+ return self._user_id
74
+
75
+ async def track_tool(self, tool_name: str, result: Dict[str, Any]) -> None:
76
+ if self.client:
77
+ self.client.capture(
78
+ distinct_id=self.user_id,
79
+ event="tool_executed",
80
+ properties={
81
+ "tool_name": tool_name,
82
+ "session_id": self.mcp_interaction_id,
83
+ "process_person_profile": True,
84
+ **result
85
+ }
86
+ )
87
+
88
+ duration = result.get("duration_ms", 0)
89
+ success_mark = "SUCCESS" if result.get("success") else "FAILED"
90
+ # Using print for immediate visibility in console during debugging
91
+ print(f"[Analytics] {tool_name}: {success_mark} ({duration}ms)")
92
+ logger.info(f"{tool_name}: {success_mark} ({duration}ms)")
93
+ if self.client:
94
+ self.client.flush()
95
+
96
+ async def track_error(self, error: Exception, context: Dict[str, Any]) -> None:
97
+ if self.client:
98
+ self.client.capture(
99
+ distinct_id=self.user_id,
100
+ event="exception",
101
+ properties={
102
+ "exception": str(error),
103
+ "traceback": str(error) if not hasattr(error, '__traceback__') else str(error),
104
+ "session_id": self.mcp_interaction_id,
105
+ "process_person_profile": True,
106
+ **context
107
+ }
108
+ )
109
+
110
+ if self.client:
111
+ self.client.flush()
112
+
113
+ logger.error(f"ERROR in {context.get('tool_name')}: {error}")
114
+
115
+ async def is_feature_enabled(self, feature: str) -> bool:
116
+ if not self.client:
117
+ return False
118
+ return self.client.is_feature_enabled(feature, self.user_id)
119
+
120
+ async def close(self) -> None:
121
+ if self.client:
122
+ self.client.shutdown()
123
+ logger.debug("Closed analytics")
124
+
125
+ def with_analytics(analytics_instance: Optional[Analytics], tool_name: str):
126
+ """
127
+ Decorator to wrap tool functions with analytics tracking.
128
+ """
129
+ def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
130
+ @wraps(func)
131
+ async def wrapper(*args, **kwargs) -> T:
132
+ start = time.time()
133
+
134
+ # Capture client info from Context passed as argument
135
+ client_data = {}
136
+ try:
137
+ ctx = next((arg for arg in args if isinstance(arg, Context)), None)
138
+ if not ctx:
139
+ ctx = next((val for val in kwargs.values() if isinstance(val, Context)), None)
140
+
141
+ if ctx and ctx.session and ctx.session.client_params and ctx.session.client_params.clientInfo:
142
+ info = ctx.session.client_params.clientInfo
143
+ client_data["client_name"] = info.name
144
+ client_data["client_version"] = info.version
145
+ except Exception:
146
+ pass
147
+
148
+ try:
149
+ if asyncio.iscoroutinefunction(func):
150
+ result = await func(*args, **kwargs)
151
+ else:
152
+ # Run sync function in thread to avoid blocking loop
153
+ result = await asyncio.to_thread(func, *args, **kwargs)
154
+
155
+ duration_ms = int((time.time() - start) * 1000)
156
+
157
+ if analytics_instance:
158
+ await analytics_instance.track_tool(tool_name, {
159
+ "duration_ms": duration_ms,
160
+ "success": True,
161
+ **client_data
162
+ })
163
+
164
+ return result
165
+ except Exception as error:
166
+ duration_ms = int((time.time() - start) * 1000)
167
+ if analytics_instance:
168
+ await analytics_instance.track_error(error, {
169
+ "tool_name": tool_name,
170
+ "duration_ms": duration_ms,
171
+ **client_data
172
+ })
173
+ raise error
174
+ return wrapper
175
+ return decorator
@@ -1,21 +1,21 @@
1
- from typing import Set
2
-
3
- BROWSER_NAMES=set([
4
- 'msedge.exe',
5
- 'chrome.exe',
6
- 'firefox.exe'
7
- ])
8
-
9
- AVOIDED_APPS:Set[str]=set([
10
- 'AgentUI'
11
- ])
12
-
13
- EXCLUDED_APPS:Set[str]=set([
14
- 'Progman',
15
- 'Shell_TrayWnd',
16
- 'Shell_SecondaryTrayWnd',
17
- 'Microsoft.UI.Content.PopupWindowSiteBridge',
18
- 'Windows.UI.Core.CoreWindow',
19
- ])
20
-
1
+ from typing import Set
2
+
3
+ BROWSER_NAMES=set([
4
+ 'msedge.exe',
5
+ 'chrome.exe',
6
+ 'firefox.exe'
7
+ ])
8
+
9
+ AVOIDED_APPS:Set[str]=set([
10
+ 'AgentUI'
11
+ ])
12
+
13
+ EXCLUDED_APPS:Set[str]=set([
14
+ 'Progman',
15
+ 'Shell_TrayWnd',
16
+ 'Shell_SecondaryTrayWnd',
17
+ 'Microsoft.UI.Content.PopupWindowSiteBridge',
18
+ 'Windows.UI.Core.CoreWindow',
19
+ ])
20
+
21
21
  PROCESS_PER_MONITOR_DPI_AWARE = 2