windows-mcp 0.5.7__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,171 +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 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
+ 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