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 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 os.path.exists(path):
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 os.path.exists(path):
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 os.path.exists(alt_path):
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 to auto-discovery.
101
- - If auto-discovery fails, raise an error (including STATA_PATH failure context, if any).
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
- raise FileNotFoundError(
162
- f"STATA_PATH points to directory '{path}', but no Stata executable was found within. "
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\\Stata19\\StataMP-64.exe)."
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
- if not os.path.exists(path):
177
- raise FileNotFoundError(
178
- f"STATA_PATH points to '{path}', but that file does not exist. "
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\\Stata19Now\\StataSE-64.exe)."
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 os.path.exists(binary_dir):
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 os.path.exists(full_path):
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 os.path.exists(full_path):
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 os.path.exists(full_path):
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 os.path.exists(path):
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
- raise FileNotFoundError(
323
- "Could not automatically locate Stata after STATA_PATH failed. "
324
- f"STATA_PATH error was: {stata_path_error}. "
325
- "Fix STATA_PATH to point to the Stata executable, or install Stata in a standard location "
326
- "(e.g., /Applications/StataNow/StataMP.app/Contents/MacOS/stata-mp, /usr/local/stata18/stata-mp, "
327
- "or C:\\Program Files\\Stata18\\StataMP-64.exe)."
328
- ) from stata_path_error
329
-
330
- raise FileNotFoundError(
331
- "Could not automatically locate Stata. "
332
- "Set STATA_PATH to your Stata executable (e.g., "
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
@@ -8,6 +8,7 @@ class ErrorEnvelope(BaseModel):
8
8
  line: Optional[int] = None
9
9
  command: Optional[str] = None
10
10
  log_path: Optional[str] = None
11
+ context: Optional[str] = None
11
12
  stdout: Optional[str] = None
12
13
  stderr: Optional[str] = None
13
14
  snippet: Optional[str] = None