mcp-stata 1.7.3__py3-none-any.whl → 1.13.0__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/config.py +20 -0
- mcp_stata/discovery.py +134 -59
- mcp_stata/graph_detector.py +29 -26
- mcp_stata/models.py +3 -0
- mcp_stata/server.py +647 -19
- mcp_stata/stata_client.py +1881 -989
- mcp_stata/streaming_io.py +3 -1
- mcp_stata/test_stata.py +54 -0
- mcp_stata/ui_http.py +178 -19
- {mcp_stata-1.7.3.dist-info → mcp_stata-1.13.0.dist-info}/METADATA +15 -3
- mcp_stata-1.13.0.dist-info/RECORD +16 -0
- mcp_stata-1.7.3.dist-info/RECORD +0 -14
- {mcp_stata-1.7.3.dist-info → mcp_stata-1.13.0.dist-info}/WHEEL +0 -0
- {mcp_stata-1.7.3.dist-info → mcp_stata-1.13.0.dist-info}/entry_points.txt +0 -0
- {mcp_stata-1.7.3.dist-info → mcp_stata-1.13.0.dist-info}/licenses/LICENSE +0 -0
mcp_stata/config.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Central configuration for mcp-stata server and UI channel.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
# Server Limits
|
|
8
|
+
MAX_LIMIT: Final[int] = 500 # Default row limit for JSON endpoints
|
|
9
|
+
MAX_VARS: Final[int] = 32_767 # Max variables in Stata
|
|
10
|
+
MAX_CHARS: Final[int] = 500 # Max chars per string cell to return
|
|
11
|
+
MAX_REQUEST_BYTES: Final[int] = 1_000_000 # Max size of HTTP request body
|
|
12
|
+
MAX_ARROW_LIMIT: Final[int] = 1_000_000 # Default row limit for Arrow IPC streams
|
|
13
|
+
|
|
14
|
+
# Timeouts (seconds)
|
|
15
|
+
TOKEN_TTL_S: Final[int] = 20 * 60 # Bearer token validity
|
|
16
|
+
VIEW_TTL_S: Final[int] = 30 * 60 # Filtered view handle validity
|
|
17
|
+
|
|
18
|
+
# Network
|
|
19
|
+
DEFAULT_HOST: Final[str] = "127.0.0.1"
|
|
20
|
+
DEFAULT_PORT: Final[int] = 0 # 0 = random ephemeral port
|
mcp_stata/discovery.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Optimized discovery.py with fast auto-discovery and targeted retry logic.
|
|
3
3
|
Key improvements:
|
|
4
|
-
1.
|
|
5
|
-
2.
|
|
6
|
-
3.
|
|
7
|
-
4.
|
|
4
|
+
1. Fast path checking during discovery (no retries)
|
|
5
|
+
2. Retry logic only for validation of user-provided paths
|
|
6
|
+
3. Better diagnostic logging
|
|
7
|
+
4. Fuzzy path matching for common typos
|
|
8
|
+
5. Case-insensitive path resolution on Windows
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
import os
|
|
@@ -15,15 +16,17 @@ import logging
|
|
|
15
16
|
import shutil
|
|
16
17
|
import ntpath
|
|
17
18
|
import time
|
|
19
|
+
import re
|
|
18
20
|
from typing import Tuple, List, Optional
|
|
19
21
|
|
|
20
22
|
logger = logging.getLogger("mcp_stata.discovery")
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
def _exists_with_retry(path: str, max_attempts: int =
|
|
25
|
+
def _exists_with_retry(path: str, max_attempts: int = 1, delay: float = 0.01) -> bool:
|
|
24
26
|
"""
|
|
25
27
|
Check if file exists with retry logic to handle transient failures.
|
|
26
28
|
This helps with antivirus scans, file locks, and other temporary issues.
|
|
29
|
+
Only use this for validating user-provided paths, not during discovery.
|
|
27
30
|
"""
|
|
28
31
|
for attempt in range(max_attempts):
|
|
29
32
|
if os.path.exists(path):
|
|
@@ -36,6 +39,11 @@ def _exists_with_retry(path: str, max_attempts: int = 3, delay: float = 0.1) ->
|
|
|
36
39
|
return False
|
|
37
40
|
|
|
38
41
|
|
|
42
|
+
def _exists_fast(path: str) -> bool:
|
|
43
|
+
"""Fast existence check without retries for auto-discovery."""
|
|
44
|
+
return os.path.exists(path)
|
|
45
|
+
|
|
46
|
+
|
|
39
47
|
def _find_similar_stata_dirs(target_path: str) -> List[str]:
|
|
40
48
|
"""
|
|
41
49
|
Find similar Stata directories to help diagnose path typos.
|
|
@@ -72,6 +80,7 @@ def _validate_path_with_diagnostics(path: str, system: str) -> Tuple[bool, str]:
|
|
|
72
80
|
"""
|
|
73
81
|
Validate path exists and provide detailed diagnostics if not.
|
|
74
82
|
Returns (exists, diagnostic_message)
|
|
83
|
+
Uses retry logic for validation since this is for user-provided paths.
|
|
75
84
|
"""
|
|
76
85
|
if _exists_with_retry(path):
|
|
77
86
|
return True, ""
|
|
@@ -132,8 +141,14 @@ def _normalize_env_path(raw: str, system: str) -> str:
|
|
|
132
141
|
return os.path.normpath(expanded)
|
|
133
142
|
|
|
134
143
|
|
|
135
|
-
def _is_executable(path: str, system: str) -> bool:
|
|
136
|
-
|
|
144
|
+
def _is_executable(path: str, system: str, use_retry: bool = True) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Check if path is executable.
|
|
147
|
+
use_retry: Use retry logic for user-provided paths, fast check for discovery.
|
|
148
|
+
"""
|
|
149
|
+
exists_check = _exists_with_retry if use_retry else _exists_fast
|
|
150
|
+
|
|
151
|
+
if not exists_check(path):
|
|
137
152
|
return False
|
|
138
153
|
if system == "Windows":
|
|
139
154
|
# On Windows, check if it's a file and has .exe extension
|
|
@@ -165,6 +180,37 @@ def _dedupe_str_preserve(items: List[str]) -> List[str]:
|
|
|
165
180
|
return out
|
|
166
181
|
|
|
167
182
|
|
|
183
|
+
def _extract_version_number(path: str) -> int:
|
|
184
|
+
"""
|
|
185
|
+
Extract the highest Stata version number found in path components that
|
|
186
|
+
mention 'stata'. Returns 0 if no version is found.
|
|
187
|
+
"""
|
|
188
|
+
version = 0
|
|
189
|
+
normalized = path.lower().replace("\\", os.sep)
|
|
190
|
+
for part in normalized.split(os.sep):
|
|
191
|
+
if "stata" not in part:
|
|
192
|
+
continue
|
|
193
|
+
for match in re.findall(r"(\d{1,3})", part):
|
|
194
|
+
try:
|
|
195
|
+
version = max(version, int(match))
|
|
196
|
+
except ValueError:
|
|
197
|
+
continue
|
|
198
|
+
return version
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _sort_candidates(candidates: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
|
|
202
|
+
"""Sort candidates by version desc, edition (mp>se>be), then path for stability."""
|
|
203
|
+
edition_rank = {"mp": 3, "se": 2, "be": 1}
|
|
204
|
+
|
|
205
|
+
def sort_key(item: Tuple[str, str]):
|
|
206
|
+
path, edition = item
|
|
207
|
+
version = _extract_version_number(path)
|
|
208
|
+
rank = edition_rank.get((edition or "").lower(), 0)
|
|
209
|
+
return (-version, -rank, path)
|
|
210
|
+
|
|
211
|
+
return sorted(candidates, key=sort_key)
|
|
212
|
+
|
|
213
|
+
|
|
168
214
|
def _resolve_windows_host_path(path: str, system: str) -> str:
|
|
169
215
|
"""
|
|
170
216
|
On non-Windows hosts running Windows-discovery code, a Windows-style path
|
|
@@ -174,11 +220,11 @@ def _resolve_windows_host_path(path: str, system: str) -> str:
|
|
|
174
220
|
"""
|
|
175
221
|
if system != "Windows":
|
|
176
222
|
return path
|
|
177
|
-
if
|
|
223
|
+
if _exists_fast(path):
|
|
178
224
|
return path
|
|
179
225
|
if os.sep != "\\" and "\\" in path:
|
|
180
226
|
alt_path = path.replace("\\", os.sep)
|
|
181
|
-
if
|
|
227
|
+
if _exists_fast(alt_path):
|
|
182
228
|
return alt_path
|
|
183
229
|
return path
|
|
184
230
|
|
|
@@ -193,13 +239,18 @@ def _detect_system() -> str:
|
|
|
193
239
|
return platform.system()
|
|
194
240
|
|
|
195
241
|
|
|
196
|
-
def
|
|
242
|
+
def find_stata_candidates() -> List[Tuple[str, str]]:
|
|
197
243
|
"""
|
|
198
|
-
|
|
199
|
-
|
|
244
|
+
Locate all viable Stata installations ordered by preference.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of (path_to_executable, edition_string) sorted by:
|
|
248
|
+
- Newest version number found in path (desc)
|
|
249
|
+
- Edition preference: mp > se > be
|
|
250
|
+
- Path name (stable tie-breaker)
|
|
200
251
|
|
|
201
252
|
Behavior:
|
|
202
|
-
- If STATA_PATH is set and valid, use it.
|
|
253
|
+
- If STATA_PATH is set and valid, use it (may yield multiple binaries in dir).
|
|
203
254
|
- If STATA_PATH is set but invalid, provide detailed diagnostics and fall back.
|
|
204
255
|
- If auto-discovery fails, raise an error with helpful suggestions.
|
|
205
256
|
"""
|
|
@@ -215,52 +266,62 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
215
266
|
("Stata-64.exe", "be"),
|
|
216
267
|
("Stata.exe", "be"),
|
|
217
268
|
]
|
|
218
|
-
|
|
219
269
|
linux_binaries = [
|
|
220
270
|
("stata-mp", "mp"),
|
|
221
271
|
("stata-se", "se"),
|
|
222
|
-
("stata-ic", "be"),
|
|
223
272
|
("stata", "be"),
|
|
224
273
|
("xstata-mp", "mp"),
|
|
225
274
|
("xstata-se", "se"),
|
|
226
|
-
("xstata-ic", "be"),
|
|
227
275
|
("xstata", "be"),
|
|
228
276
|
]
|
|
229
277
|
|
|
230
|
-
# 1. Check
|
|
231
|
-
|
|
232
|
-
if
|
|
278
|
+
# 1. Check STATA_PATH override with enhanced diagnostics
|
|
279
|
+
raw_stata_path = os.environ.get("STATA_PATH")
|
|
280
|
+
if raw_stata_path:
|
|
233
281
|
try:
|
|
234
|
-
path = _normalize_env_path(
|
|
282
|
+
path = _normalize_env_path(raw_stata_path, system)
|
|
235
283
|
path = _resolve_windows_host_path(path, system)
|
|
236
|
-
logger.info("Trying STATA_PATH override (normalized): %s", path)
|
|
237
284
|
|
|
238
|
-
# If a directory is provided, try standard binaries for the platform
|
|
239
285
|
if os.path.isdir(path):
|
|
240
|
-
|
|
286
|
+
candidates_in_dir = []
|
|
241
287
|
if system == "Windows":
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
("stata-mp", "mp"),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
288
|
+
for exe, edition in windows_binaries:
|
|
289
|
+
candidate = os.path.join(path, exe)
|
|
290
|
+
if _is_executable(candidate, system, use_retry=True):
|
|
291
|
+
candidates_in_dir.append((candidate, edition))
|
|
292
|
+
elif system == "Darwin" or (system != "Windows" and path.endswith(".app")):
|
|
293
|
+
# macOS app bundle logic
|
|
294
|
+
sub_path = os.path.join(path, "Contents", "MacOS")
|
|
295
|
+
if os.path.isdir(sub_path):
|
|
296
|
+
for binary, edition in [("stata-mp", "mp"), ("stata-se", "se"), ("stata", "be")]:
|
|
297
|
+
candidate = os.path.join(sub_path, binary)
|
|
298
|
+
if _is_executable(candidate, system, use_retry=True):
|
|
299
|
+
candidates_in_dir.append((candidate, edition))
|
|
300
|
+
|
|
301
|
+
# Also try direct if not in a bundle
|
|
302
|
+
if not candidates_in_dir:
|
|
303
|
+
for binary, edition in linux_binaries:
|
|
304
|
+
candidate = os.path.join(path, binary)
|
|
305
|
+
if _is_executable(candidate, system, use_retry=True):
|
|
306
|
+
candidates_in_dir.append((candidate, edition))
|
|
307
|
+
else:
|
|
308
|
+
for binary, edition in linux_binaries:
|
|
309
|
+
candidate = os.path.join(path, binary)
|
|
310
|
+
if _is_executable(candidate, system, use_retry=True):
|
|
311
|
+
candidates_in_dir.append((candidate, edition))
|
|
312
|
+
|
|
313
|
+
if candidates_in_dir:
|
|
314
|
+
resolved = []
|
|
315
|
+
for candidate, edition in _sort_candidates(candidates_in_dir):
|
|
316
|
+
if _is_executable(candidate, system, use_retry=True):
|
|
317
|
+
logger.info(
|
|
318
|
+
"Found Stata via STATA_PATH directory: %s (%s)",
|
|
319
|
+
candidate,
|
|
320
|
+
edition,
|
|
321
|
+
)
|
|
322
|
+
resolved.append((candidate, edition))
|
|
323
|
+
if resolved:
|
|
324
|
+
return resolved
|
|
264
325
|
|
|
265
326
|
# Enhanced error with diagnostics
|
|
266
327
|
exists, diagnostics = _validate_path_with_diagnostics(path, system)
|
|
@@ -281,7 +342,7 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
281
342
|
elif "be" in lower_path:
|
|
282
343
|
edition = "be"
|
|
283
344
|
|
|
284
|
-
# Use enhanced validation with diagnostics
|
|
345
|
+
# Use enhanced validation with diagnostics (with retry for user path)
|
|
285
346
|
exists, diagnostics = _validate_path_with_diagnostics(path, system)
|
|
286
347
|
if not exists:
|
|
287
348
|
error_msg = (
|
|
@@ -293,14 +354,14 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
293
354
|
)
|
|
294
355
|
raise FileNotFoundError(error_msg)
|
|
295
356
|
|
|
296
|
-
if not _is_executable(path, system):
|
|
357
|
+
if not _is_executable(path, system, use_retry=True):
|
|
297
358
|
raise PermissionError(
|
|
298
359
|
f"STATA_PATH points to '{path}', but it is not executable. "
|
|
299
360
|
"Ensure this is the Stata binary, not the .app directory."
|
|
300
361
|
)
|
|
301
362
|
|
|
302
363
|
logger.info("Using STATA_PATH override: %s (%s)", path, edition)
|
|
303
|
-
return path, edition
|
|
364
|
+
return [(path, edition)]
|
|
304
365
|
|
|
305
366
|
except Exception as exc:
|
|
306
367
|
stata_path_error = exc
|
|
@@ -310,7 +371,7 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
310
371
|
exc,
|
|
311
372
|
)
|
|
312
373
|
|
|
313
|
-
# 2. Platform-specific search
|
|
374
|
+
# 2. Platform-specific search (using fast checks, no retries)
|
|
314
375
|
candidates: List[Tuple[str, str]] = [] # List of (path, edition)
|
|
315
376
|
|
|
316
377
|
if system == "Darwin": # macOS
|
|
@@ -321,17 +382,18 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
321
382
|
"/Applications/Stata/StataMP.app",
|
|
322
383
|
"/Applications/Stata/StataSE.app",
|
|
323
384
|
"/Applications/Stata/Stata.app",
|
|
385
|
+
"/Applications/Stata*.app",
|
|
324
386
|
"/Applications/Stata*/Stata*.app",
|
|
325
387
|
]
|
|
326
388
|
|
|
327
389
|
for pattern in app_globs:
|
|
328
390
|
for app_dir in glob.glob(pattern):
|
|
329
391
|
binary_dir = os.path.join(app_dir, "Contents", "MacOS")
|
|
330
|
-
if not
|
|
392
|
+
if not _exists_fast(binary_dir):
|
|
331
393
|
continue
|
|
332
394
|
for binary, edition in [("stata-mp", "mp"), ("stata-se", "se"), ("stata", "be")]:
|
|
333
395
|
full_path = os.path.join(binary_dir, binary)
|
|
334
|
-
if
|
|
396
|
+
if _exists_fast(full_path):
|
|
335
397
|
candidates.append((full_path, edition))
|
|
336
398
|
|
|
337
399
|
elif system == "Windows":
|
|
@@ -379,7 +441,7 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
379
441
|
continue
|
|
380
442
|
for exe, edition in windows_binaries:
|
|
381
443
|
full_path = os.path.join(stata_dir, exe)
|
|
382
|
-
if
|
|
444
|
+
if _exists_fast(full_path):
|
|
383
445
|
candidates.append((full_path, edition))
|
|
384
446
|
|
|
385
447
|
elif system == "Linux":
|
|
@@ -417,20 +479,25 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
417
479
|
continue
|
|
418
480
|
for binary, edition in linux_binaries:
|
|
419
481
|
full_path = os.path.join(base_dir, binary)
|
|
420
|
-
if
|
|
482
|
+
if _exists_fast(full_path):
|
|
421
483
|
candidates.append((full_path, edition))
|
|
422
484
|
|
|
423
|
-
|
|
485
|
+
candidates = _dedupe_preserve(candidates)
|
|
424
486
|
|
|
425
|
-
|
|
426
|
-
|
|
487
|
+
# Final validation of candidates (still using fast checks)
|
|
488
|
+
validated: List[Tuple[str, str]] = []
|
|
489
|
+
for path, edition in _sort_candidates(candidates):
|
|
490
|
+
if not _exists_fast(path):
|
|
427
491
|
logger.warning("Discovered candidate missing on disk: %s", path)
|
|
428
492
|
continue
|
|
429
|
-
if not _is_executable(path, system):
|
|
493
|
+
if not _is_executable(path, system, use_retry=False):
|
|
430
494
|
logger.warning("Discovered candidate is not executable: %s", path)
|
|
431
495
|
continue
|
|
432
496
|
logger.info("Auto-discovered Stata at %s (%s)", path, edition)
|
|
433
|
-
|
|
497
|
+
validated.append((path, edition))
|
|
498
|
+
|
|
499
|
+
if validated:
|
|
500
|
+
return validated
|
|
434
501
|
|
|
435
502
|
# Build comprehensive error message
|
|
436
503
|
error_parts = ["Could not automatically locate Stata."]
|
|
@@ -455,6 +522,14 @@ def find_stata_path() -> Tuple[str, str]:
|
|
|
455
522
|
raise FileNotFoundError("\n".join(error_parts))
|
|
456
523
|
|
|
457
524
|
|
|
525
|
+
def find_stata_path() -> Tuple[str, str]:
|
|
526
|
+
"""
|
|
527
|
+
Backward-compatible wrapper returning the top-ranked candidate.
|
|
528
|
+
"""
|
|
529
|
+
candidates = find_stata_candidates()
|
|
530
|
+
return candidates[0]
|
|
531
|
+
|
|
532
|
+
|
|
458
533
|
def main() -> int:
|
|
459
534
|
"""CLI helper to print discovered Stata binary and edition."""
|
|
460
535
|
try:
|
mcp_stata/graph_detector.py
CHANGED
|
@@ -6,6 +6,7 @@ during Stata command execution and automatically cache them.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
+
import inspect
|
|
9
10
|
import re
|
|
10
11
|
import threading
|
|
11
12
|
import time
|
|
@@ -39,11 +40,14 @@ class GraphCreationDetector:
|
|
|
39
40
|
if not self._stata_client or not hasattr(self._stata_client, "stata"):
|
|
40
41
|
return ""
|
|
41
42
|
try:
|
|
42
|
-
#
|
|
43
|
-
resp = self._stata_client.
|
|
43
|
+
# Use lightweight execution to avoid heavy FS I/O for high-frequency polling
|
|
44
|
+
resp = self._stata_client.exec_lightweight(f"graph describe {graph_name}")
|
|
45
|
+
|
|
44
46
|
if resp.success and resp.stdout:
|
|
45
47
|
return resp.stdout
|
|
46
48
|
if resp.error and resp.error.snippet:
|
|
49
|
+
# If using lightweight, error might be None or just string in stderr,
|
|
50
|
+
# but run_command_structured returns proper error envelope.
|
|
47
51
|
return resp.error.snippet
|
|
48
52
|
except Exception:
|
|
49
53
|
return ""
|
|
@@ -95,15 +99,21 @@ class GraphCreationDetector:
|
|
|
95
99
|
try:
|
|
96
100
|
# Use pystata to get graph list directly
|
|
97
101
|
if self._stata_client and hasattr(self._stata_client, 'list_graphs'):
|
|
98
|
-
return self._stata_client.list_graphs()
|
|
102
|
+
return self._stata_client.list_graphs(force_refresh=True)
|
|
99
103
|
else:
|
|
100
104
|
# Fallback to sfi Macro interface - only if stata is available
|
|
101
105
|
if self._stata_client and hasattr(self._stata_client, 'stata'):
|
|
102
106
|
try:
|
|
103
107
|
from sfi import Macro
|
|
104
|
-
|
|
105
|
-
self._stata_client.stata.run("
|
|
106
|
-
|
|
108
|
+
hold_name = f"_mcp_detector_hold_{int(time.time() * 1000 % 1000000)}"
|
|
109
|
+
self._stata_client.stata.run(f"capture _return hold {hold_name}", echo=False)
|
|
110
|
+
try:
|
|
111
|
+
self._stata_client.stata.run("macro define mcp_graph_list \"\"", echo=False)
|
|
112
|
+
self._stata_client.stata.run("quietly graph dir, memory", echo=False)
|
|
113
|
+
self._stata_client.stata.run("macro define mcp_graph_list `r(list)'", echo=False)
|
|
114
|
+
graph_list_str = Macro.getGlobal("mcp_graph_list")
|
|
115
|
+
finally:
|
|
116
|
+
self._stata_client.stata.run(f"capture _return restore {hold_name}", echo=False)
|
|
107
117
|
return graph_list_str.split() if graph_list_str else []
|
|
108
118
|
except ImportError:
|
|
109
119
|
logger.warning("sfi.Macro not available for fallback graph detection")
|
|
@@ -259,6 +269,15 @@ class StreamingGraphCache:
|
|
|
259
269
|
with self._lock:
|
|
260
270
|
self._cache_callbacks.append(callback)
|
|
261
271
|
|
|
272
|
+
async def _notify_cache_callbacks(self, graph_name: str, success: bool) -> None:
|
|
273
|
+
for callback in self._cache_callbacks:
|
|
274
|
+
try:
|
|
275
|
+
result = callback(graph_name, success)
|
|
276
|
+
if inspect.isawaitable(result):
|
|
277
|
+
await result
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"Cache callback failed for {graph_name}: {e}")
|
|
280
|
+
|
|
262
281
|
|
|
263
282
|
async def cache_detected_graphs_with_pystata(self) -> List[str]:
|
|
264
283
|
"""Enhanced caching method that uses pystata for real-time graph detection."""
|
|
@@ -304,20 +323,12 @@ class StreamingGraphCache:
|
|
|
304
323
|
self._cached_graphs.add(graph_name)
|
|
305
324
|
|
|
306
325
|
# Notify callbacks
|
|
307
|
-
|
|
308
|
-
try:
|
|
309
|
-
callback(graph_name, success)
|
|
310
|
-
except Exception as e:
|
|
311
|
-
logger.warning(f"Cache callback failed for {graph_name}: {e}")
|
|
326
|
+
await self._notify_cache_callbacks(graph_name, success)
|
|
312
327
|
|
|
313
328
|
except Exception as e:
|
|
314
329
|
logger.warning(f"Failed to cache graph {graph_name}: {e}")
|
|
315
330
|
# Still notify callbacks of failure
|
|
316
|
-
|
|
317
|
-
try:
|
|
318
|
-
callback(graph_name, False)
|
|
319
|
-
except Exception:
|
|
320
|
-
pass
|
|
331
|
+
await self._notify_cache_callbacks(graph_name, False)
|
|
321
332
|
|
|
322
333
|
return cached_names
|
|
323
334
|
|
|
@@ -349,20 +360,12 @@ class StreamingGraphCache:
|
|
|
349
360
|
self._cached_graphs.add(graph_name)
|
|
350
361
|
|
|
351
362
|
# Notify callbacks
|
|
352
|
-
|
|
353
|
-
try:
|
|
354
|
-
callback(graph_name, success)
|
|
355
|
-
except Exception as e:
|
|
356
|
-
logger.warning(f"Cache callback failed for {graph_name}: {e}")
|
|
363
|
+
await self._notify_cache_callbacks(graph_name, success)
|
|
357
364
|
|
|
358
365
|
except Exception as e:
|
|
359
366
|
logger.warning(f"Failed to cache graph {graph_name}: {e}")
|
|
360
367
|
# Still notify callbacks of failure
|
|
361
|
-
|
|
362
|
-
try:
|
|
363
|
-
callback(graph_name, False)
|
|
364
|
-
except Exception:
|
|
365
|
-
pass
|
|
368
|
+
await self._notify_cache_callbacks(graph_name, False)
|
|
366
369
|
|
|
367
370
|
return cached_names
|
|
368
371
|
|
mcp_stata/models.py
CHANGED
|
@@ -8,10 +8,12 @@ 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
|
|
14
15
|
trace: Optional[bool] = None
|
|
16
|
+
smcl_output: Optional[str] = None
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class CommandResponse(BaseModel):
|
|
@@ -22,6 +24,7 @@ class CommandResponse(BaseModel):
|
|
|
22
24
|
log_path: Optional[str] = None
|
|
23
25
|
success: bool
|
|
24
26
|
error: Optional[ErrorEnvelope] = None
|
|
27
|
+
smcl_output: Optional[str] = None
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
class DataResponse(BaseModel):
|