mcp-stata 1.6.2__py3-none-any.whl → 1.7.3__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 +315 -77
- mcp_stata/server.py +7 -0
- mcp_stata/stata_client.py +328 -77
- mcp_stata/ui_http.py +61 -6
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/METADATA +68 -2
- mcp_stata-1.7.3.dist-info/RECORD +14 -0
- mcp_stata-1.6.2.dist-info/RECORD +0 -14
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/WHEEL +0 -0
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/entry_points.txt +0 -0
- {mcp_stata-1.6.2.dist-info → mcp_stata-1.7.3.dist-info}/licenses/LICENSE +0 -0
mcp_stata/discovery.py
CHANGED
|
@@ -1,30 +1,143 @@
|
|
|
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
|
|
11
|
+
import sys
|
|
2
12
|
import platform
|
|
3
13
|
import glob
|
|
4
14
|
import logging
|
|
5
15
|
import shutil
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
import ntpath
|
|
17
|
+
import time
|
|
18
|
+
from typing import Tuple, List, Optional
|
|
8
19
|
|
|
9
20
|
logger = logging.getLogger("mcp_stata.discovery")
|
|
10
21
|
|
|
11
22
|
|
|
12
|
-
def
|
|
13
|
-
"""
|
|
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
|
+
|
|
117
|
+
def _normalize_env_path(raw: str, system: str) -> str:
|
|
118
|
+
"""Strip quotes/whitespace, expand variables, and normalize slashes for STATA_PATH."""
|
|
14
119
|
cleaned = raw.strip()
|
|
15
|
-
if (cleaned.startswith("
|
|
120
|
+
if (cleaned.startswith('"') and cleaned.endswith('"')) or (
|
|
16
121
|
cleaned.startswith("'") and cleaned.endswith("'")
|
|
17
122
|
):
|
|
18
123
|
cleaned = cleaned[1:-1].strip()
|
|
19
|
-
|
|
124
|
+
|
|
125
|
+
expanded = os.path.expandvars(os.path.expanduser(cleaned))
|
|
126
|
+
|
|
127
|
+
# Always normalize path separators for the intended platform. This is especially
|
|
128
|
+
# important when running Windows discovery tests on non-Windows hosts where
|
|
129
|
+
# os.path (PosixPath) would otherwise leave backslashes untouched.
|
|
130
|
+
if system == "Windows":
|
|
131
|
+
return ntpath.normpath(expanded)
|
|
132
|
+
return os.path.normpath(expanded)
|
|
20
133
|
|
|
21
134
|
|
|
22
135
|
def _is_executable(path: str, system: str) -> bool:
|
|
23
|
-
if not
|
|
136
|
+
if not _exists_with_retry(path): # Use retry logic
|
|
24
137
|
return False
|
|
25
138
|
if system == "Windows":
|
|
26
139
|
# On Windows, check if it's a file and has .exe extension
|
|
27
|
-
return os.path.isfile(path) and path.lower().endswith(
|
|
140
|
+
return os.path.isfile(path) and path.lower().endswith(".exe")
|
|
28
141
|
return os.access(path, os.X_OK)
|
|
29
142
|
|
|
30
143
|
|
|
@@ -39,12 +152,60 @@ def _dedupe_preserve(items: List[tuple]) -> List[tuple]:
|
|
|
39
152
|
return unique
|
|
40
153
|
|
|
41
154
|
|
|
155
|
+
def _dedupe_str_preserve(items: List[str]) -> List[str]:
|
|
156
|
+
seen = set()
|
|
157
|
+
out: List[str] = []
|
|
158
|
+
for s in items:
|
|
159
|
+
if not s:
|
|
160
|
+
continue
|
|
161
|
+
if s in seen:
|
|
162
|
+
continue
|
|
163
|
+
seen.add(s)
|
|
164
|
+
out.append(s)
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _resolve_windows_host_path(path: str, system: str) -> str:
|
|
169
|
+
"""
|
|
170
|
+
On non-Windows hosts running Windows-discovery code, a Windows-style path
|
|
171
|
+
(with backslashes) won't match the real filesystem layout. If the normalized
|
|
172
|
+
path does not exist and we're emulating Windows, try swapping backslashes for
|
|
173
|
+
the host separator so tests can interact with the temp filesystem.
|
|
174
|
+
"""
|
|
175
|
+
if system != "Windows":
|
|
176
|
+
return path
|
|
177
|
+
if _exists_with_retry(path): # Use retry logic
|
|
178
|
+
return path
|
|
179
|
+
if os.sep != "\\" and "\\" in path:
|
|
180
|
+
alt_path = path.replace("\\", os.sep)
|
|
181
|
+
if _exists_with_retry(alt_path): # Use retry logic
|
|
182
|
+
return alt_path
|
|
183
|
+
return path
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _detect_system() -> str:
|
|
187
|
+
"""
|
|
188
|
+
Prefer Windows detection via os.name / sys.platform instead of platform.system()
|
|
189
|
+
because some environments (e.g., Cygwin/MSYS) do not return "Windows".
|
|
190
|
+
"""
|
|
191
|
+
if os.name == "nt" or sys.platform.startswith(("cygwin", "msys")):
|
|
192
|
+
return "Windows"
|
|
193
|
+
return platform.system()
|
|
194
|
+
|
|
195
|
+
|
|
42
196
|
def find_stata_path() -> Tuple[str, str]:
|
|
43
197
|
"""
|
|
44
198
|
Attempts to automatically locate the Stata installation path.
|
|
45
199
|
Returns (path_to_executable, edition_string).
|
|
200
|
+
|
|
201
|
+
Behavior:
|
|
202
|
+
- If STATA_PATH is set and valid, use it.
|
|
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.
|
|
46
205
|
"""
|
|
47
|
-
system =
|
|
206
|
+
system = _detect_system()
|
|
207
|
+
stata_path_error: Optional[Exception] = None
|
|
208
|
+
stata_path_diagnostics: Optional[str] = None
|
|
48
209
|
|
|
49
210
|
windows_binaries = [
|
|
50
211
|
("StataMP-64.exe", "mp"),
|
|
@@ -67,63 +228,90 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
67
228
|
]
|
|
68
229
|
|
|
69
230
|
# 1. Check Environment Variable (supports quoted values and directory targets)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if
|
|
79
|
-
search_set =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
candidate = os.path.join(path, binary)
|
|
94
|
-
if _is_executable(candidate, system):
|
|
95
|
-
logger.info("Found Stata via STATA_PATH directory: %s (%s)", candidate, edition)
|
|
96
|
-
return candidate, edition
|
|
231
|
+
raw_env_path = os.environ.get("STATA_PATH")
|
|
232
|
+
if raw_env_path:
|
|
233
|
+
try:
|
|
234
|
+
path = _normalize_env_path(raw_env_path, system)
|
|
235
|
+
path = _resolve_windows_host_path(path, system)
|
|
236
|
+
logger.info("Trying STATA_PATH override (normalized): %s", path)
|
|
237
|
+
|
|
238
|
+
# If a directory is provided, try standard binaries for the platform
|
|
239
|
+
if os.path.isdir(path):
|
|
240
|
+
search_set = []
|
|
241
|
+
if system == "Windows":
|
|
242
|
+
search_set = windows_binaries
|
|
243
|
+
elif system == "Linux":
|
|
244
|
+
search_set = linux_binaries
|
|
245
|
+
elif system == "Darwin":
|
|
246
|
+
search_set = [
|
|
247
|
+
("Contents/MacOS/stata-mp", "mp"),
|
|
248
|
+
("Contents/MacOS/stata-se", "se"),
|
|
249
|
+
("Contents/MacOS/stata", "be"),
|
|
250
|
+
("stata-mp", "mp"),
|
|
251
|
+
("stata-se", "se"),
|
|
252
|
+
("stata", "be"),
|
|
253
|
+
]
|
|
97
254
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
255
|
+
for binary, edition in search_set:
|
|
256
|
+
candidate = os.path.join(path, binary)
|
|
257
|
+
if _is_executable(candidate, system):
|
|
258
|
+
logger.info(
|
|
259
|
+
"Found Stata via STATA_PATH directory: %s (%s)",
|
|
260
|
+
candidate,
|
|
261
|
+
edition,
|
|
262
|
+
)
|
|
263
|
+
return candidate, edition
|
|
264
|
+
|
|
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"
|
|
270
|
+
"Point STATA_PATH directly to the Stata binary "
|
|
271
|
+
"(e.g., C:\\Program Files\\StataNow19\\StataMP-64.exe)."
|
|
272
|
+
)
|
|
273
|
+
raise FileNotFoundError(error_msg)
|
|
102
274
|
|
|
103
|
-
edition = "be"
|
|
104
|
-
lower_path = path.lower()
|
|
105
|
-
if "mp" in lower_path:
|
|
106
|
-
edition = "mp"
|
|
107
|
-
elif "se" in lower_path:
|
|
108
|
-
edition = "se"
|
|
109
|
-
elif "be" in lower_path:
|
|
110
275
|
edition = "be"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
276
|
+
lower_path = path.lower()
|
|
277
|
+
if "mp" in lower_path:
|
|
278
|
+
edition = "mp"
|
|
279
|
+
elif "se" in lower_path:
|
|
280
|
+
edition = "se"
|
|
281
|
+
elif "be" in lower_path:
|
|
282
|
+
edition = "be"
|
|
283
|
+
|
|
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"
|
|
290
|
+
"Update STATA_PATH to your Stata binary (e.g., "
|
|
291
|
+
"/Applications/StataNow/StataMP.app/Contents/MacOS/stata-mp, "
|
|
292
|
+
"/usr/local/stata19/stata-mp or C:\\Program Files\\StataNow19\\StataMP-64.exe)."
|
|
293
|
+
)
|
|
294
|
+
raise FileNotFoundError(error_msg)
|
|
295
|
+
|
|
296
|
+
if not _is_executable(path, system):
|
|
297
|
+
raise PermissionError(
|
|
298
|
+
f"STATA_PATH points to '{path}', but it is not executable. "
|
|
299
|
+
"Ensure this is the Stata binary, not the .app directory."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
logger.info("Using STATA_PATH override: %s (%s)", path, edition)
|
|
303
|
+
return path, edition
|
|
304
|
+
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
stata_path_error = exc
|
|
307
|
+
stata_path_diagnostics = str(exc)
|
|
308
|
+
logger.warning(
|
|
309
|
+
"STATA_PATH override failed (%s). Falling back to auto-discovery.",
|
|
310
|
+
exc,
|
|
121
311
|
)
|
|
122
|
-
logger.info("Using STATA_PATH override: %s (%s)", path, edition)
|
|
123
|
-
return path, edition
|
|
124
312
|
|
|
125
313
|
# 2. Platform-specific search
|
|
126
|
-
candidates = [] # List of (path, edition)
|
|
314
|
+
candidates: List[Tuple[str, str]] = [] # List of (path, edition)
|
|
127
315
|
|
|
128
316
|
if system == "Darwin": # macOS
|
|
129
317
|
app_globs = [
|
|
@@ -139,25 +327,60 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
139
327
|
for pattern in app_globs:
|
|
140
328
|
for app_dir in glob.glob(pattern):
|
|
141
329
|
binary_dir = os.path.join(app_dir, "Contents", "MacOS")
|
|
142
|
-
if not
|
|
330
|
+
if not _exists_with_retry(binary_dir): # Use retry logic
|
|
143
331
|
continue
|
|
144
332
|
for binary, edition in [("stata-mp", "mp"), ("stata-se", "se"), ("stata", "be")]:
|
|
145
333
|
full_path = os.path.join(binary_dir, binary)
|
|
146
|
-
if
|
|
334
|
+
if _exists_with_retry(full_path): # Use retry logic
|
|
147
335
|
candidates.append((full_path, edition))
|
|
148
336
|
|
|
149
337
|
elif system == "Windows":
|
|
338
|
+
# Include ProgramW6432 (real 64-bit Program Files) and hardcode fallbacks.
|
|
339
|
+
base_dirs = _dedupe_str_preserve(
|
|
340
|
+
[
|
|
341
|
+
os.environ.get("ProgramW6432", r"C:\Program Files"),
|
|
342
|
+
os.environ.get("ProgramFiles", r"C:\Program Files"),
|
|
343
|
+
os.environ.get("ProgramFiles(Arm)", r"C:\Program Files (Arm)"),
|
|
344
|
+
os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"),
|
|
345
|
+
r"C:\Program Files",
|
|
346
|
+
r"C:\Program Files (Arm)",
|
|
347
|
+
r"C:\Program Files (x86)",
|
|
348
|
+
]
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Resolve for non-Windows hosts running Windows discovery tests.
|
|
150
352
|
base_dirs = [
|
|
151
|
-
|
|
152
|
-
os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)"),
|
|
353
|
+
_resolve_windows_host_path(ntpath.normpath(bd), system) for bd in base_dirs
|
|
153
354
|
]
|
|
355
|
+
base_dirs = _dedupe_str_preserve(base_dirs)
|
|
154
356
|
|
|
357
|
+
# Look in a few plausible layouts:
|
|
358
|
+
# base\Stata*\...
|
|
359
|
+
# base\*\Stata*\... (e.g., base\StataCorp\Stata19Now)
|
|
360
|
+
# base\Stata*\*\... (e.g., base\Stata\Stata19Now)
|
|
361
|
+
dir_globs: List[str] = []
|
|
155
362
|
for base_dir in base_dirs:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
363
|
+
dir_globs.extend(
|
|
364
|
+
[
|
|
365
|
+
os.path.join(base_dir, "Stata*"),
|
|
366
|
+
os.path.join(base_dir, "*", "Stata*"),
|
|
367
|
+
os.path.join(base_dir, "Stata*", "Stata*"),
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
dir_globs = _dedupe_str_preserve(dir_globs)
|
|
371
|
+
|
|
372
|
+
stata_dirs: List[str] = []
|
|
373
|
+
for pattern in dir_globs:
|
|
374
|
+
stata_dirs.extend(glob.glob(pattern))
|
|
375
|
+
stata_dirs = _dedupe_str_preserve(stata_dirs)
|
|
376
|
+
|
|
377
|
+
for stata_dir in stata_dirs:
|
|
378
|
+
if not os.path.isdir(stata_dir):
|
|
379
|
+
continue
|
|
380
|
+
for exe, edition in windows_binaries:
|
|
381
|
+
full_path = os.path.join(stata_dir, exe)
|
|
382
|
+
if _exists_with_retry(full_path): # Use retry logic
|
|
383
|
+
candidates.append((full_path, edition))
|
|
161
384
|
|
|
162
385
|
elif system == "Linux":
|
|
163
386
|
home_base = os.environ.get("HOME") or os.path.expanduser("~")
|
|
@@ -194,14 +417,13 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
194
417
|
continue
|
|
195
418
|
for binary, edition in linux_binaries:
|
|
196
419
|
full_path = os.path.join(base_dir, binary)
|
|
197
|
-
if
|
|
420
|
+
if _exists_with_retry(full_path): # Use retry logic
|
|
198
421
|
candidates.append((full_path, edition))
|
|
199
422
|
|
|
200
|
-
|
|
201
423
|
candidates = _dedupe_preserve(candidates)
|
|
202
424
|
|
|
203
425
|
for path, edition in candidates:
|
|
204
|
-
if not
|
|
426
|
+
if not _exists_with_retry(path): # Use retry logic
|
|
205
427
|
logger.warning("Discovered candidate missing on disk: %s", path)
|
|
206
428
|
continue
|
|
207
429
|
if not _is_executable(path, system):
|
|
@@ -210,11 +432,27 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
210
432
|
logger.info("Auto-discovered Stata at %s (%s)", path, edition)
|
|
211
433
|
return path, edition
|
|
212
434
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
435
|
+
# Build comprehensive error message
|
|
436
|
+
error_parts = ["Could not automatically locate Stata."]
|
|
437
|
+
|
|
438
|
+
if stata_path_error is not None:
|
|
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."
|
|
217
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))
|
|
218
456
|
|
|
219
457
|
|
|
220
458
|
def main() -> int:
|
|
@@ -230,4 +468,4 @@ def main() -> int:
|
|
|
230
468
|
|
|
231
469
|
|
|
232
470
|
if __name__ == "__main__": # pragma: no cover - manual utility
|
|
233
|
-
raise SystemExit(main())
|
|
471
|
+
raise SystemExit(main())
|
mcp_stata/server.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import anyio
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
3
|
from mcp.server.fastmcp import Context, FastMCP
|
|
3
4
|
import mcp.types as types
|
|
4
5
|
from .stata_client import StataClient
|
|
@@ -17,6 +18,12 @@ from .ui_http import UIChannelManager
|
|
|
17
18
|
|
|
18
19
|
LOG_LEVEL = os.getenv("MCP_STATA_LOGLEVEL", "INFO").upper()
|
|
19
20
|
logging.basicConfig(level=LOG_LEVEL, format="%(asctime)s %(levelname)s %(name)s - %(message)s")
|
|
21
|
+
try:
|
|
22
|
+
_mcp_stata_version = version("mcp-stata")
|
|
23
|
+
except PackageNotFoundError:
|
|
24
|
+
_mcp_stata_version = "unknown"
|
|
25
|
+
logging.info("mcp-stata version: %s", _mcp_stata_version)
|
|
26
|
+
logging.info("STATA_PATH env at startup: %s", os.getenv("STATA_PATH", "<not set>"))
|
|
20
27
|
|
|
21
28
|
# Initialize FastMCP
|
|
22
29
|
mcp = FastMCP("mcp_stata")
|