mcp-stata 1.6.8__py3-none-any.whl → 1.7.6__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.
Potentially problematic release.
This version of mcp-stata might be problematic. Click here for more details.
- mcp_stata/discovery.py +151 -31
- mcp_stata/models.py +1 -0
- mcp_stata/stata_client.py +323 -264
- mcp_stata/ui_http.py +37 -1
- {mcp_stata-1.6.8.dist-info → mcp_stata-1.7.6.dist-info}/METADATA +60 -2
- mcp_stata-1.7.6.dist-info/RECORD +14 -0
- mcp_stata-1.6.8.dist-info/RECORD +0 -14
- {mcp_stata-1.6.8.dist-info → mcp_stata-1.7.6.dist-info}/WHEEL +0 -0
- {mcp_stata-1.6.8.dist-info → mcp_stata-1.7.6.dist-info}/entry_points.txt +0 -0
- {mcp_stata-1.6.8.dist-info → mcp_stata-1.7.6.dist-info}/licenses/LICENSE +0 -0
mcp_stata/discovery.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Improved discovery.py with better error handling for intermittent failures.
|
|
3
|
+
Key improvements:
|
|
4
|
+
1. Retry logic for file existence checks
|
|
5
|
+
2. Better diagnostic logging
|
|
6
|
+
3. Fuzzy path matching for common typos
|
|
7
|
+
4. Case-insensitive path resolution on Windows
|
|
8
|
+
"""
|
|
9
|
+
|
|
1
10
|
import os
|
|
2
11
|
import sys
|
|
3
12
|
import platform
|
|
@@ -5,12 +14,106 @@ import glob
|
|
|
5
14
|
import logging
|
|
6
15
|
import shutil
|
|
7
16
|
import ntpath
|
|
8
|
-
|
|
17
|
+
import time
|
|
9
18
|
from typing import Tuple, List, Optional
|
|
10
19
|
|
|
11
20
|
logger = logging.getLogger("mcp_stata.discovery")
|
|
12
21
|
|
|
13
22
|
|
|
23
|
+
def _exists_with_retry(path: str, max_attempts: int = 3, delay: float = 0.1) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Check if file exists with retry logic to handle transient failures.
|
|
26
|
+
This helps with antivirus scans, file locks, and other temporary issues.
|
|
27
|
+
"""
|
|
28
|
+
for attempt in range(max_attempts):
|
|
29
|
+
if os.path.exists(path):
|
|
30
|
+
return True
|
|
31
|
+
if attempt < max_attempts - 1:
|
|
32
|
+
logger.debug(
|
|
33
|
+
f"File existence check attempt {attempt + 1} failed for: {path}"
|
|
34
|
+
)
|
|
35
|
+
time.sleep(delay)
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _find_similar_stata_dirs(target_path: str) -> List[str]:
|
|
40
|
+
"""
|
|
41
|
+
Find similar Stata directories to help diagnose path typos.
|
|
42
|
+
Useful when user has 'Stata19Now' instead of 'StataNow19'.
|
|
43
|
+
"""
|
|
44
|
+
parent = os.path.dirname(target_path)
|
|
45
|
+
|
|
46
|
+
# If parent doesn't exist, try grandparent (for directory name typos)
|
|
47
|
+
search_dir = parent
|
|
48
|
+
if not os.path.exists(parent):
|
|
49
|
+
search_dir = os.path.dirname(parent)
|
|
50
|
+
|
|
51
|
+
if not os.path.exists(search_dir):
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
subdirs = [
|
|
56
|
+
d for d in os.listdir(search_dir)
|
|
57
|
+
if os.path.isdir(os.path.join(search_dir, d))
|
|
58
|
+
]
|
|
59
|
+
# Filter to Stata-related directories (case-insensitive)
|
|
60
|
+
stata_dirs = [
|
|
61
|
+
os.path.join(search_dir, d)
|
|
62
|
+
for d in subdirs
|
|
63
|
+
if 'stata' in d.lower()
|
|
64
|
+
]
|
|
65
|
+
return stata_dirs
|
|
66
|
+
except (OSError, PermissionError) as e:
|
|
67
|
+
logger.debug(f"Could not list directory {search_dir}: {e}")
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _validate_path_with_diagnostics(path: str, system: str) -> Tuple[bool, str]:
|
|
72
|
+
"""
|
|
73
|
+
Validate path exists and provide detailed diagnostics if not.
|
|
74
|
+
Returns (exists, diagnostic_message)
|
|
75
|
+
"""
|
|
76
|
+
if _exists_with_retry(path):
|
|
77
|
+
return True, ""
|
|
78
|
+
|
|
79
|
+
# Build diagnostic message
|
|
80
|
+
diagnostics = []
|
|
81
|
+
diagnostics.append(f"File not found: '{path}'")
|
|
82
|
+
|
|
83
|
+
parent_dir = os.path.dirname(path)
|
|
84
|
+
filename = os.path.basename(path)
|
|
85
|
+
|
|
86
|
+
if _exists_with_retry(parent_dir):
|
|
87
|
+
diagnostics.append(f"✓ Parent directory exists: '{parent_dir}'")
|
|
88
|
+
try:
|
|
89
|
+
files_in_parent = os.listdir(parent_dir)
|
|
90
|
+
# Look for similar filenames
|
|
91
|
+
similar_files = [
|
|
92
|
+
f for f in files_in_parent
|
|
93
|
+
if 'stata' in f.lower() and f.lower().endswith('.exe' if system == 'Windows' else '')
|
|
94
|
+
]
|
|
95
|
+
if similar_files:
|
|
96
|
+
diagnostics.append(f"Found {len(similar_files)} Stata file(s) in parent:")
|
|
97
|
+
for f in similar_files[:5]: # Show max 5
|
|
98
|
+
diagnostics.append(f" - {f}")
|
|
99
|
+
else:
|
|
100
|
+
diagnostics.append(f"No Stata executables found in parent directory")
|
|
101
|
+
diagnostics.append(f"Files present: {', '.join(files_in_parent[:10])}")
|
|
102
|
+
except (OSError, PermissionError) as e:
|
|
103
|
+
diagnostics.append(f"✗ Could not list parent directory: {e}")
|
|
104
|
+
else:
|
|
105
|
+
diagnostics.append(f"✗ Parent directory does not exist: '{parent_dir}'")
|
|
106
|
+
|
|
107
|
+
# Check for similar directories (typo detection)
|
|
108
|
+
similar_dirs = _find_similar_stata_dirs(path)
|
|
109
|
+
if similar_dirs:
|
|
110
|
+
diagnostics.append("\nDid you mean one of these directories?")
|
|
111
|
+
for dir_path in similar_dirs[:5]:
|
|
112
|
+
diagnostics.append(f" - {dir_path}")
|
|
113
|
+
|
|
114
|
+
return False, "\n".join(diagnostics)
|
|
115
|
+
|
|
116
|
+
|
|
14
117
|
def _normalize_env_path(raw: str, system: str) -> str:
|
|
15
118
|
"""Strip quotes/whitespace, expand variables, and normalize slashes for STATA_PATH."""
|
|
16
119
|
cleaned = raw.strip()
|
|
@@ -30,7 +133,7 @@ def _normalize_env_path(raw: str, system: str) -> str:
|
|
|
30
133
|
|
|
31
134
|
|
|
32
135
|
def _is_executable(path: str, system: str) -> bool:
|
|
33
|
-
if not
|
|
136
|
+
if not _exists_with_retry(path): # Use retry logic
|
|
34
137
|
return False
|
|
35
138
|
if system == "Windows":
|
|
36
139
|
# On Windows, check if it's a file and has .exe extension
|
|
@@ -71,11 +174,11 @@ def _resolve_windows_host_path(path: str, system: str) -> str:
|
|
|
71
174
|
"""
|
|
72
175
|
if system != "Windows":
|
|
73
176
|
return path
|
|
74
|
-
if
|
|
177
|
+
if _exists_with_retry(path): # Use retry logic
|
|
75
178
|
return path
|
|
76
179
|
if os.sep != "\\" and "\\" in path:
|
|
77
180
|
alt_path = path.replace("\\", os.sep)
|
|
78
|
-
if
|
|
181
|
+
if _exists_with_retry(alt_path): # Use retry logic
|
|
79
182
|
return alt_path
|
|
80
183
|
return path
|
|
81
184
|
|
|
@@ -97,11 +200,12 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
97
200
|
|
|
98
201
|
Behavior:
|
|
99
202
|
- If STATA_PATH is set and valid, use it.
|
|
100
|
-
- If STATA_PATH is set but invalid, fall back
|
|
101
|
-
- If auto-discovery fails, raise an error
|
|
203
|
+
- If STATA_PATH is set but invalid, provide detailed diagnostics and fall back.
|
|
204
|
+
- If auto-discovery fails, raise an error with helpful suggestions.
|
|
102
205
|
"""
|
|
103
206
|
system = _detect_system()
|
|
104
207
|
stata_path_error: Optional[Exception] = None
|
|
208
|
+
stata_path_diagnostics: Optional[str] = None
|
|
105
209
|
|
|
106
210
|
windows_binaries = [
|
|
107
211
|
("StataMP-64.exe", "mp"),
|
|
@@ -158,11 +262,15 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
158
262
|
)
|
|
159
263
|
return candidate, edition
|
|
160
264
|
|
|
161
|
-
|
|
162
|
-
|
|
265
|
+
# Enhanced error with diagnostics
|
|
266
|
+
exists, diagnostics = _validate_path_with_diagnostics(path, system)
|
|
267
|
+
error_msg = (
|
|
268
|
+
f"STATA_PATH points to directory '{path}', but no Stata executable was found within.\n"
|
|
269
|
+
f"{diagnostics}\n\n"
|
|
163
270
|
"Point STATA_PATH directly to the Stata binary "
|
|
164
|
-
"(e.g., C:\\Program Files\\
|
|
271
|
+
"(e.g., C:\\Program Files\\StataNow19\\StataMP-64.exe)."
|
|
165
272
|
)
|
|
273
|
+
raise FileNotFoundError(error_msg)
|
|
166
274
|
|
|
167
275
|
edition = "be"
|
|
168
276
|
lower_path = path.lower()
|
|
@@ -173,13 +281,18 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
173
281
|
elif "be" in lower_path:
|
|
174
282
|
edition = "be"
|
|
175
283
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
284
|
+
# Use enhanced validation with diagnostics
|
|
285
|
+
exists, diagnostics = _validate_path_with_diagnostics(path, system)
|
|
286
|
+
if not exists:
|
|
287
|
+
error_msg = (
|
|
288
|
+
f"STATA_PATH points to '{path}', but that file does not exist.\n"
|
|
289
|
+
f"{diagnostics}\n\n"
|
|
179
290
|
"Update STATA_PATH to your Stata binary (e.g., "
|
|
180
291
|
"/Applications/StataNow/StataMP.app/Contents/MacOS/stata-mp, "
|
|
181
|
-
"/usr/local/stata19/stata-mp or C:\\Program Files\\
|
|
292
|
+
"/usr/local/stata19/stata-mp or C:\\Program Files\\StataNow19\\StataMP-64.exe)."
|
|
182
293
|
)
|
|
294
|
+
raise FileNotFoundError(error_msg)
|
|
295
|
+
|
|
183
296
|
if not _is_executable(path, system):
|
|
184
297
|
raise PermissionError(
|
|
185
298
|
f"STATA_PATH points to '{path}', but it is not executable. "
|
|
@@ -191,6 +304,7 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
191
304
|
|
|
192
305
|
except Exception as exc:
|
|
193
306
|
stata_path_error = exc
|
|
307
|
+
stata_path_diagnostics = str(exc)
|
|
194
308
|
logger.warning(
|
|
195
309
|
"STATA_PATH override failed (%s). Falling back to auto-discovery.",
|
|
196
310
|
exc,
|
|
@@ -213,11 +327,11 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
213
327
|
for pattern in app_globs:
|
|
214
328
|
for app_dir in glob.glob(pattern):
|
|
215
329
|
binary_dir = os.path.join(app_dir, "Contents", "MacOS")
|
|
216
|
-
if not
|
|
330
|
+
if not _exists_with_retry(binary_dir): # Use retry logic
|
|
217
331
|
continue
|
|
218
332
|
for binary, edition in [("stata-mp", "mp"), ("stata-se", "se"), ("stata", "be")]:
|
|
219
333
|
full_path = os.path.join(binary_dir, binary)
|
|
220
|
-
if
|
|
334
|
+
if _exists_with_retry(full_path): # Use retry logic
|
|
221
335
|
candidates.append((full_path, edition))
|
|
222
336
|
|
|
223
337
|
elif system == "Windows":
|
|
@@ -265,7 +379,7 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
265
379
|
continue
|
|
266
380
|
for exe, edition in windows_binaries:
|
|
267
381
|
full_path = os.path.join(stata_dir, exe)
|
|
268
|
-
if
|
|
382
|
+
if _exists_with_retry(full_path): # Use retry logic
|
|
269
383
|
candidates.append((full_path, edition))
|
|
270
384
|
|
|
271
385
|
elif system == "Linux":
|
|
@@ -303,13 +417,13 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
303
417
|
continue
|
|
304
418
|
for binary, edition in linux_binaries:
|
|
305
419
|
full_path = os.path.join(base_dir, binary)
|
|
306
|
-
if
|
|
420
|
+
if _exists_with_retry(full_path): # Use retry logic
|
|
307
421
|
candidates.append((full_path, edition))
|
|
308
422
|
|
|
309
423
|
candidates = _dedupe_preserve(candidates)
|
|
310
424
|
|
|
311
425
|
for path, edition in candidates:
|
|
312
|
-
if not
|
|
426
|
+
if not _exists_with_retry(path): # Use retry logic
|
|
313
427
|
logger.warning("Discovered candidate missing on disk: %s", path)
|
|
314
428
|
continue
|
|
315
429
|
if not _is_executable(path, system):
|
|
@@ -318,21 +432,27 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
318
432
|
logger.info("Auto-discovered Stata at %s (%s)", path, edition)
|
|
319
433
|
return path, edition
|
|
320
434
|
|
|
435
|
+
# Build comprehensive error message
|
|
436
|
+
error_parts = ["Could not automatically locate Stata."]
|
|
437
|
+
|
|
321
438
|
if stata_path_error is not None:
|
|
322
|
-
|
|
323
|
-
"
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"/Applications/StataNow/StataMP.app/Contents/MacOS/stata-mp, /usr/local/stata18/stata-mp, "
|
|
334
|
-
"or C:\\Program Files\\Stata18\\StataMP-64.exe)."
|
|
439
|
+
error_parts.append(
|
|
440
|
+
f"\nSTATA_PATH was set but failed:\n{stata_path_diagnostics}"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
error_parts.append(
|
|
444
|
+
"\nTo fix this issue:\n"
|
|
445
|
+
"1. Set STATA_PATH to point to your Stata executable, for example:\n"
|
|
446
|
+
" - Windows: C:\\Program Files\\StataNow19\\StataMP-64.exe\n"
|
|
447
|
+
" - macOS: /Applications/StataNow/StataMP.app/Contents/MacOS/stata-mp\n"
|
|
448
|
+
" - Linux: /usr/local/stata19/stata-mp\n"
|
|
449
|
+
"\n2. Or install Stata in a standard location where it can be auto-discovered."
|
|
335
450
|
)
|
|
451
|
+
|
|
452
|
+
if stata_path_error is not None:
|
|
453
|
+
raise FileNotFoundError("\n".join(error_parts)) from stata_path_error
|
|
454
|
+
else:
|
|
455
|
+
raise FileNotFoundError("\n".join(error_parts))
|
|
336
456
|
|
|
337
457
|
|
|
338
458
|
def main() -> int:
|
mcp_stata/models.py
CHANGED