aigroup-stata-mcp 1.0.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.
- aigroup_stata_mcp-1.0.3.dist-info/METADATA +345 -0
- aigroup_stata_mcp-1.0.3.dist-info/RECORD +38 -0
- aigroup_stata_mcp-1.0.3.dist-info/WHEEL +4 -0
- aigroup_stata_mcp-1.0.3.dist-info/entry_points.txt +5 -0
- aigroup_stata_mcp-1.0.3.dist-info/licenses/LICENSE +21 -0
- stata_mcp/__init__.py +18 -0
- stata_mcp/cli/__init__.py +8 -0
- stata_mcp/cli/_cli.py +95 -0
- stata_mcp/core/__init__.py +14 -0
- stata_mcp/core/data_info/__init__.py +11 -0
- stata_mcp/core/data_info/_base.py +288 -0
- stata_mcp/core/data_info/csv.py +123 -0
- stata_mcp/core/data_info/dta.py +70 -0
- stata_mcp/core/stata/__init__.py +13 -0
- stata_mcp/core/stata/stata_controller/__init__.py +9 -0
- stata_mcp/core/stata/stata_controller/controller.py +208 -0
- stata_mcp/core/stata/stata_do/__init__.py +9 -0
- stata_mcp/core/stata/stata_do/do.py +177 -0
- stata_mcp/core/stata/stata_finder/__init__.py +9 -0
- stata_mcp/core/stata/stata_finder/base.py +294 -0
- stata_mcp/core/stata/stata_finder/finder.py +193 -0
- stata_mcp/core/stata/stata_finder/linux.py +43 -0
- stata_mcp/core/stata/stata_finder/macos.py +88 -0
- stata_mcp/core/stata/stata_finder/windows.py +191 -0
- stata_mcp/server/__init__.py +8 -0
- stata_mcp/server/main.py +153 -0
- stata_mcp/server/prompts/__init__.py +8 -0
- stata_mcp/server/prompts/core_prompts.py +122 -0
- stata_mcp/server/tools/__init__.py +10 -0
- stata_mcp/server/tools/core_tools.py +59 -0
- stata_mcp/server/tools/file_tools.py +163 -0
- stata_mcp/server/tools/stata_tools.py +221 -0
- stata_mcp/utils/Installer/__init__.py +7 -0
- stata_mcp/utils/Installer/installer.py +85 -0
- stata_mcp/utils/Prompt/__init__.py +74 -0
- stata_mcp/utils/Prompt/string.py +91 -0
- stata_mcp/utils/__init__.py +23 -0
- stata_mcp/utils/usable.py +244 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import glob
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .linux import FinderLinux
|
|
12
|
+
from .macos import FinderMacOS
|
|
13
|
+
from .windows import FinderWindows, get_available_drives, windows_stata_match
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StataFinder:
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
FINDER_MAPPING = {
|
|
21
|
+
"Darwin": FinderMacOS,
|
|
22
|
+
"Windows": FinderWindows,
|
|
23
|
+
"Linux": FinderLinux,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def __init__(self, stata_cli: str = None):
|
|
27
|
+
"""简要描述函数功能"""
|
|
28
|
+
finder_cls = self.FINDER_MAPPING.get(platform.system())
|
|
29
|
+
self.finder = finder_cls(stata_cli)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def STATA_CLI(self) -> str | None:
|
|
33
|
+
"""简要描述函数功能"""
|
|
34
|
+
try:
|
|
35
|
+
return self.finder.find_stata()
|
|
36
|
+
except (FileNotFoundError, AttributeError):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StataFinderOLD:
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
"""A class to find Stata CLI installations across different operating systems."""
|
|
44
|
+
|
|
45
|
+
FINDER_MAPPING = {
|
|
46
|
+
"Darwin": FinderMacOS,
|
|
47
|
+
"Windows": FinderWindows,
|
|
48
|
+
"Linux": FinderLinux,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __init__(self, stata_cli: str = None):
|
|
52
|
+
"""
|
|
53
|
+
Initialize the StataFinder.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
stata_cli (str): the user updates stata_cli file path
|
|
57
|
+
"""
|
|
58
|
+
self.current_os = platform.system()
|
|
59
|
+
self._os_finders = {
|
|
60
|
+
"Darwin": self._find_stata_macos,
|
|
61
|
+
"Windows": self._find_stata_windows,
|
|
62
|
+
"Linux": self._find_stata_linux,
|
|
63
|
+
}
|
|
64
|
+
self.finder = self._os_finders.get(self.current_os, None)
|
|
65
|
+
# TODO: Change the original finder to the newer
|
|
66
|
+
# self.finder = self.FINDER_MAPPING.get(self.current_os)(stata_cli = stata_cli)
|
|
67
|
+
|
|
68
|
+
# @property
|
|
69
|
+
# def STATA_CLI(self) -> str: # 等前面的都改好了,就可以只保留这个了
|
|
70
|
+
# return self.finder.find_stata()
|
|
71
|
+
|
|
72
|
+
def _stata_version_windows(self, driver: str = "C:\\") -> list:
|
|
73
|
+
"""Find Stata installations on Windows."""
|
|
74
|
+
stata_paths = []
|
|
75
|
+
common_patterns = [
|
|
76
|
+
os.path.join(driver, "Program Files", "Stata*", "*.exe"),
|
|
77
|
+
os.path.join(driver, "Program Files(x86)", "Stata*", "*.exe"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for pattern in common_patterns:
|
|
81
|
+
try:
|
|
82
|
+
matches = glob.glob(pattern)
|
|
83
|
+
for match in matches:
|
|
84
|
+
if "stata" in match.lower() and match.lower().endswith(".exe"):
|
|
85
|
+
stata_paths.append(match)
|
|
86
|
+
|
|
87
|
+
if not stata_paths:
|
|
88
|
+
for root, dirs, files in os.walk(driver):
|
|
89
|
+
if root.count(os.sep) - driver.count(os.sep) > 3:
|
|
90
|
+
dirs.clear()
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
for file in files:
|
|
94
|
+
if (
|
|
95
|
+
file.lower().endswith(".exe")
|
|
96
|
+
and "stata" in file.lower()
|
|
97
|
+
):
|
|
98
|
+
stata_paths.append(os.path.join(root, file))
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logging.warn(e)
|
|
102
|
+
|
|
103
|
+
return stata_paths
|
|
104
|
+
|
|
105
|
+
def _find_stata_macos(self) -> Optional[str]:
|
|
106
|
+
return self.finder.find_stata()
|
|
107
|
+
|
|
108
|
+
def _default_stata_cli_path_windows(self) -> Optional[str]:
|
|
109
|
+
"""Get default Stata CLI path on Windows."""
|
|
110
|
+
drives = get_available_drives()
|
|
111
|
+
stata_cli_path_list = []
|
|
112
|
+
|
|
113
|
+
for drive in drives:
|
|
114
|
+
stata_cli_path_list += self._stata_version_windows(drive)
|
|
115
|
+
|
|
116
|
+
if len(stata_cli_path_list) == 0:
|
|
117
|
+
return None
|
|
118
|
+
elif len(stata_cli_path_list) == 1:
|
|
119
|
+
return stata_cli_path_list[0]
|
|
120
|
+
else:
|
|
121
|
+
for path in stata_cli_path_list:
|
|
122
|
+
if windows_stata_match(path):
|
|
123
|
+
return path
|
|
124
|
+
return stata_cli_path_list[0]
|
|
125
|
+
|
|
126
|
+
def _find_stata_windows(self) -> Optional[str]:
|
|
127
|
+
"""Find Stata CLI on Windows systems."""
|
|
128
|
+
return self._default_stata_cli_path_windows()
|
|
129
|
+
|
|
130
|
+
def _default_stata_cli_path_linux(self) -> Optional[str]:
|
|
131
|
+
"""Get default Stata CLI path on Linux."""
|
|
132
|
+
# TODO: Implement Linux-specific logic
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def _find_stata_linux(self) -> Optional[str]:
|
|
136
|
+
"""Find the Stata CLI path on Linux systems.
|
|
137
|
+
|
|
138
|
+
For Linux users, this function attempts to locate the Stata CLI executable.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The path to the Stata CLI executable, or None if not found.
|
|
142
|
+
"""
|
|
143
|
+
return self._default_stata_cli_path_linux()
|
|
144
|
+
|
|
145
|
+
def find_stata(self,
|
|
146
|
+
os_name: Optional[str] = None,
|
|
147
|
+
is_env: bool = True) -> Optional[str]:
|
|
148
|
+
"""Find Stata CLI installation.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
os_name: Operating system name. If None, uses current system.
|
|
152
|
+
is_env: Whether to check environment variables first.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Path to Stata CLI executable, or None if not found.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
RuntimeError: If the operating system is not supported.
|
|
159
|
+
"""
|
|
160
|
+
if is_env:
|
|
161
|
+
stata_cli = os.getenv("stata_cli", None) or os.getenv("STATA_CLI", None)
|
|
162
|
+
if stata_cli:
|
|
163
|
+
return stata_cli
|
|
164
|
+
|
|
165
|
+
target_os = os_name or self.current_os
|
|
166
|
+
finder = self._os_finders.get(target_os)
|
|
167
|
+
|
|
168
|
+
if not finder:
|
|
169
|
+
raise RuntimeError(f"Unsupported OS: {target_os!r}")
|
|
170
|
+
|
|
171
|
+
return finder()
|
|
172
|
+
|
|
173
|
+
def get_supported_os(self) -> list:
|
|
174
|
+
"""Get list of supported operating systems."""
|
|
175
|
+
return list(self._os_finders.keys())
|
|
176
|
+
|
|
177
|
+
def is_stata_available(
|
|
178
|
+
self, os_name: Optional[str] = None, is_env: bool = True
|
|
179
|
+
) -> bool:
|
|
180
|
+
"""Check if Stata is available on the system.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
os_name: Operating system name. If None, uses current system.
|
|
184
|
+
is_env: Whether to check environment variables first.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
True if Stata is found, False otherwise.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
stata_path = self.find_stata(os_name=os_name, is_env=is_env)
|
|
191
|
+
return stata_path is not None
|
|
192
|
+
except RuntimeError:
|
|
193
|
+
return False
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
from .base import FinderBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FinderLinux(FinderBase):
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def finder(self) -> str:
|
|
16
|
+
"""简要描述函数功能"""
|
|
17
|
+
bin_results = self.find_from_bin()
|
|
18
|
+
if bin_results:
|
|
19
|
+
return max(bin_results).stata_cli_path
|
|
20
|
+
else:
|
|
21
|
+
raise FileNotFoundError("Stata CLI not found")
|
|
22
|
+
|
|
23
|
+
def find_path_base(self) -> Dict[str, List[str]]:
|
|
24
|
+
"""简要描述函数功能"""
|
|
25
|
+
# Start with default bin directory
|
|
26
|
+
bin_dirs = ["/usr/local/bin"]
|
|
27
|
+
|
|
28
|
+
# Search for additional directories containing "stata" in /usr/local/bin
|
|
29
|
+
usr_local_bin = Path("/usr/local/bin")
|
|
30
|
+
if usr_local_bin.exists() and usr_local_bin.is_dir():
|
|
31
|
+
# Look for directories containing "stata" in their name
|
|
32
|
+
for item in usr_local_bin.iterdir():
|
|
33
|
+
if item.is_dir() and "stata" in item.name.lower():
|
|
34
|
+
# Add the stata directory path to search directories
|
|
35
|
+
bin_dirs.append(str(item))
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"bin": bin_dirs,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
finder = FinderLinux()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List
|
|
8
|
+
|
|
9
|
+
from .base import FinderBase, StataEditionConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FinderMacOS(FinderBase):
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def finder(self) -> str | None:
|
|
17
|
+
"""简要描述函数功能"""
|
|
18
|
+
bin_results = self.find_from_bin()
|
|
19
|
+
if bin_results:
|
|
20
|
+
return max(bin_results).stata_cli_path
|
|
21
|
+
|
|
22
|
+
application_results = self.find_from_application()
|
|
23
|
+
if application_results:
|
|
24
|
+
return max(application_results).stata_cli_path
|
|
25
|
+
else: # If there is no Stata CLI found, raise an error
|
|
26
|
+
raise FileNotFoundError("Stata CLI not found")
|
|
27
|
+
|
|
28
|
+
def find_path_base(self) -> Dict[str, List[str]]:
|
|
29
|
+
"""简要描述函数功能"""
|
|
30
|
+
return {
|
|
31
|
+
"bin": ["/usr/local/bin"],
|
|
32
|
+
"application": ["/Applications"],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def _application_find_base(self,
|
|
36
|
+
dot_app: str | Path,
|
|
37
|
+
version: int | float = None) -> StataEditionConfig | None:
|
|
38
|
+
_version = version
|
|
39
|
+
_edition = None
|
|
40
|
+
stata_cli_path = None
|
|
41
|
+
|
|
42
|
+
if not _version:
|
|
43
|
+
for isstata_file in dot_app.glob("isstata.*"):
|
|
44
|
+
if isstata_file.is_file():
|
|
45
|
+
# Extract version number from filename like "isstata.180"
|
|
46
|
+
match = re.search(r'isstata\.(\d+)', isstata_file.name.lower())
|
|
47
|
+
if match:
|
|
48
|
+
_version = float(match.group(1)) / 10
|
|
49
|
+
break
|
|
50
|
+
for stata_app in dot_app.glob("Stata*.app"):
|
|
51
|
+
if stata_app.is_dir():
|
|
52
|
+
# Extract edition from Stata app name (MP, SE, BE, IC)
|
|
53
|
+
# Remove "Stata" prefix and ".app" suffix, then convert to lowercase
|
|
54
|
+
_edition = stata_app.name.replace("Stata", "").replace(".app", "").lower()
|
|
55
|
+
__stata_cli_path = stata_app / "Contents" / "MacOS" / f"stata-{_edition}"
|
|
56
|
+
if self._is_executable(__stata_cli_path):
|
|
57
|
+
stata_cli_path = str(__stata_cli_path)
|
|
58
|
+
break
|
|
59
|
+
if _version and _edition and stata_cli_path:
|
|
60
|
+
return StataEditionConfig(_edition, _version, stata_cli_path)
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def find_from_application(self) -> List[StataEditionConfig]:
|
|
66
|
+
"""简要描述函数功能"""
|
|
67
|
+
found_executables: List[StataEditionConfig] = []
|
|
68
|
+
applications_dir = Path(self.find_path_base().get("application")[0])
|
|
69
|
+
|
|
70
|
+
# Check for /Applications/Stata directory for Multi-Stata Exist
|
|
71
|
+
stata_dir = applications_dir / "Stata"
|
|
72
|
+
if default_stata := self._application_find_base(stata_dir): # If exist default, return directly.
|
|
73
|
+
return [default_stata]
|
|
74
|
+
|
|
75
|
+
# 通过for循环来从applications_dir里找stata*.app
|
|
76
|
+
for stata_app in applications_dir.glob("Stata *"):
|
|
77
|
+
_version = None
|
|
78
|
+
if stata_app.is_dir():
|
|
79
|
+
_version = eval(stata_app.name.split()[-1])
|
|
80
|
+
if stata_app_config := self._application_find_base(stata_app, version=_version):
|
|
81
|
+
found_executables.append(stata_app_config)
|
|
82
|
+
|
|
83
|
+
return found_executables
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
finder = FinderMacOS()
|
|
88
|
+
print(finder.finder())
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import glob
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import string
|
|
9
|
+
from typing import Dict, List
|
|
10
|
+
|
|
11
|
+
from .base import FinderBase, StataEditionConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_available_drives():
|
|
15
|
+
"""简要描述函数功能"""
|
|
16
|
+
drives = []
|
|
17
|
+
for letter in string.ascii_uppercase:
|
|
18
|
+
if os.path.exists(f"{letter}:\\"):
|
|
19
|
+
drives.append(f"{letter}:\\")
|
|
20
|
+
return drives
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def windows_stata_match(path: str) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Check whether the given path matches the pattern of a Windows
|
|
26
|
+
Stata executable.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
path: Path string to be checked.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: ``True`` if the path matches a Stata executable pattern,
|
|
33
|
+
otherwise ``False``.
|
|
34
|
+
"""
|
|
35
|
+
# Regular expression matching ``Stata\d+\Stata(MP|SE|BE|IC)?.exe``
|
|
36
|
+
# ``\d+`` matches one or more digits (the version number)
|
|
37
|
+
# ``(MP|SE|BE|IC)?`` matches an optional edition suffix
|
|
38
|
+
pattern = r"Stata\d+\\\\Stata(MP|SE|BE|IC)?\.exe$"
|
|
39
|
+
|
|
40
|
+
if re.search(pattern, path):
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FinderWindows(FinderBase):
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def finder(self) -> str:
|
|
50
|
+
"""简要描述函数功能"""
|
|
51
|
+
default_results = self.find_from_default_install_path()
|
|
52
|
+
if default_results:
|
|
53
|
+
return max(default_results).stata_cli_path
|
|
54
|
+
|
|
55
|
+
driver_results = self.scan_stata_from_drivers()
|
|
56
|
+
if driver_results:
|
|
57
|
+
return max(driver_results).stata_cli_path
|
|
58
|
+
|
|
59
|
+
deep_results = self.scan_stata_deeply()
|
|
60
|
+
if deep_results:
|
|
61
|
+
return max(deep_results).stata_cli_path
|
|
62
|
+
else:
|
|
63
|
+
raise FileNotFoundError("Stata executable not found")
|
|
64
|
+
|
|
65
|
+
def find_path_base(self) -> Dict[str, List[str]]:
|
|
66
|
+
"""简要描述函数功能"""
|
|
67
|
+
return {
|
|
68
|
+
"default": [
|
|
69
|
+
r"C:\Program Files\Stata*",
|
|
70
|
+
r"C:\Program Files (x86)\Stata*",
|
|
71
|
+
r"D:\Program Files\Stata*",
|
|
72
|
+
r"D:\Program Files (x86)\Stata*",
|
|
73
|
+
],
|
|
74
|
+
"drivers": get_available_drives()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def find_from_default_install_path(self) -> List[StataEditionConfig]:
|
|
78
|
+
"""Find Stata installations in default Windows installation paths.
|
|
79
|
+
|
|
80
|
+
This function searches for Stata in standard Windows locations
|
|
81
|
+
like Program Files directories using simple matching.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of StataEditionConfig objects found in default paths
|
|
85
|
+
"""
|
|
86
|
+
found_configs = []
|
|
87
|
+
|
|
88
|
+
# Common Windows installation paths
|
|
89
|
+
common_paths = self.find_path_base().get("default")
|
|
90
|
+
|
|
91
|
+
for path_pattern in common_paths:
|
|
92
|
+
try:
|
|
93
|
+
matches = glob.glob(path_pattern)
|
|
94
|
+
for match in matches:
|
|
95
|
+
# Search in these Stata directories for executables
|
|
96
|
+
executables = glob.glob(os.path.join(match, "*.exe"))
|
|
97
|
+
for exe in executables:
|
|
98
|
+
# Simple matching like the original implementation
|
|
99
|
+
if "stata" in exe.lower() and exe.lower().endswith(".exe"):
|
|
100
|
+
config = StataEditionConfig.from_path(exe)
|
|
101
|
+
found_configs.append(config)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
return found_configs
|
|
106
|
+
|
|
107
|
+
def scan_stata_from_drivers(self) -> List[StataEditionConfig]:
|
|
108
|
+
"""Scan all available drivers for Stata installations in non-standard locations.
|
|
109
|
+
|
|
110
|
+
This method searches for Stata in locations that are NOT in standard
|
|
111
|
+
installation paths (Program Files, etc.), focusing on custom or
|
|
112
|
+
alternative installation locations.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of StataEditionConfig objects found in non-standard locations
|
|
116
|
+
"""
|
|
117
|
+
drivers_base = self.find_path_base().get("drivers")
|
|
118
|
+
found_configs = []
|
|
119
|
+
|
|
120
|
+
if not drivers_base:
|
|
121
|
+
return found_configs
|
|
122
|
+
|
|
123
|
+
for driver in drivers_base:
|
|
124
|
+
# Skip standard installation directories to avoid duplication
|
|
125
|
+
skip_patterns = [
|
|
126
|
+
os.path.join(driver, "Program Files"),
|
|
127
|
+
os.path.join(driver, "Program Files (x86)"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
# Limit search depth to avoid excessive scanning
|
|
132
|
+
for root, dirs, files in os.walk(driver):
|
|
133
|
+
# Skip standard installation directories
|
|
134
|
+
should_skip = False
|
|
135
|
+
for skip_pattern in skip_patterns:
|
|
136
|
+
if root.lower().startswith(skip_pattern.lower()):
|
|
137
|
+
should_skip = True
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if should_skip:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Limit depth to 4 levels
|
|
144
|
+
if root.count(os.sep) - driver.count(os.sep) > 4:
|
|
145
|
+
dirs.clear()
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
for file in files:
|
|
149
|
+
if (
|
|
150
|
+
file.lower().endswith(".exe")
|
|
151
|
+
and "stata" in file.lower()
|
|
152
|
+
):
|
|
153
|
+
full_path = os.path.join(root, file)
|
|
154
|
+
config = StataEditionConfig.from_path(full_path)
|
|
155
|
+
found_configs.append(config)
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
return found_configs
|
|
160
|
+
|
|
161
|
+
def scan_stata_deeply(self) -> List[StataEditionConfig]:
|
|
162
|
+
"""Perform deep scan across all drives for any Stata installation.
|
|
163
|
+
|
|
164
|
+
This is the final fallback search method that scans everywhere
|
|
165
|
+
without restrictions, including standard paths, to find any possible
|
|
166
|
+
Stata installation that might have been missed.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of StataEditionConfig objects found through deep scanning
|
|
170
|
+
"""
|
|
171
|
+
drivers_base = self.find_path_base().get("drivers")
|
|
172
|
+
found_configs = []
|
|
173
|
+
|
|
174
|
+
if not drivers_base:
|
|
175
|
+
return found_configs
|
|
176
|
+
|
|
177
|
+
for driver in drivers_base:
|
|
178
|
+
try:
|
|
179
|
+
# Deep scan without restrictions
|
|
180
|
+
for root, dirs, files in os.walk(driver):
|
|
181
|
+
for file in files:
|
|
182
|
+
if file.lower().endswith(".exe") and "stata" in file.lower():
|
|
183
|
+
full_path = os.path.join(root, file)
|
|
184
|
+
# Less strict matching for deep scan
|
|
185
|
+
if "stata" in full_path.lower():
|
|
186
|
+
config = StataEditionConfig.from_path(full_path)
|
|
187
|
+
found_configs.append(config)
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
return found_configs
|
stata_mcp/server/main.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import locale
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from mcp.server.fastmcp import Context, FastMCP, Icon
|
|
15
|
+
from mcp.server.session import ServerSession
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
from ..core.data_info import CsvDataInfo, DtaDataInfo
|
|
19
|
+
from ..core.stata import StataController, StataDo, StataFinder
|
|
20
|
+
from ..utils.Prompt import pmp
|
|
21
|
+
|
|
22
|
+
from .tools.core_tools import register_core_tools
|
|
23
|
+
from .tools.file_tools import register_file_tools
|
|
24
|
+
from .tools.stata_tools import register_stata_tools
|
|
25
|
+
from .prompts.core_prompts import register_core_prompts
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StataServerConfig(BaseModel):
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
"""Configuration for Stata MCP server."""
|
|
32
|
+
|
|
33
|
+
name: str = "stata-mcp"
|
|
34
|
+
instructions: str = "Stata-MCP lets you and LLMs run Stata do-files and fetch results"
|
|
35
|
+
website_url: str = "https://github.com/jackdark425"
|
|
36
|
+
working_directory: Optional[str] = None
|
|
37
|
+
stata_cli: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StataServerContext(BaseModel):
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
"""Application context with typed dependencies."""
|
|
44
|
+
|
|
45
|
+
config: StataServerConfig
|
|
46
|
+
stata_finder: Any = Field(description="StataFinder instance")
|
|
47
|
+
working_directory: Path
|
|
48
|
+
output_base_path: Path
|
|
49
|
+
|
|
50
|
+
class Config:
|
|
51
|
+
arbitrary_types_allowed = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@asynccontextmanager
|
|
55
|
+
async def server_lifespan(server: FastMCP, config: StataServerConfig) -> AsyncIterator[StataServerContext]:
|
|
56
|
+
"""Manage server startup and shutdown lifecycle."""
|
|
57
|
+
# Initialize system configuration
|
|
58
|
+
system_os = platform.system()
|
|
59
|
+
if system_os not in ["Darwin", "Linux", "Windows"]:
|
|
60
|
+
raise RuntimeError(f"Unsupported operating system: {system_os}")
|
|
61
|
+
|
|
62
|
+
# Find Stata CLI
|
|
63
|
+
stata_finder = StataFinder()
|
|
64
|
+
stata_cli = config.stata_cli or stata_finder.STATA_CLI
|
|
65
|
+
|
|
66
|
+
# Determine working directory
|
|
67
|
+
if config.working_directory:
|
|
68
|
+
working_directory = Path(config.working_directory)
|
|
69
|
+
else:
|
|
70
|
+
client = os.getenv("STATA-MCP-CLIENT")
|
|
71
|
+
if client == "cc": # Claude Code
|
|
72
|
+
working_directory = Path.cwd()
|
|
73
|
+
else:
|
|
74
|
+
cwd = os.getenv("STATA_MCP_CWD", os.getenv("STATA-MCP-CWD", None))
|
|
75
|
+
if cwd:
|
|
76
|
+
working_directory = Path(cwd)
|
|
77
|
+
else:
|
|
78
|
+
if system_os in ["Darwin", "Linux"]:
|
|
79
|
+
working_directory = Path.home() / "Documents"
|
|
80
|
+
else:
|
|
81
|
+
working_directory = Path(os.getenv("USERPROFILE", "~")) / "Documents"
|
|
82
|
+
|
|
83
|
+
# Create output directories
|
|
84
|
+
output_base_path = working_directory / "stata-mcp-folder"
|
|
85
|
+
output_base_path.mkdir(exist_ok=True)
|
|
86
|
+
|
|
87
|
+
# Create subdirectories
|
|
88
|
+
log_base_path = output_base_path / "stata-mcp-log"
|
|
89
|
+
dofile_base_path = output_base_path / "stata-mcp-dofile"
|
|
90
|
+
result_doc_path = output_base_path / "stata-mcp-result"
|
|
91
|
+
tmp_base_path = output_base_path / "stata-mcp-tmp"
|
|
92
|
+
|
|
93
|
+
for path in [log_base_path, dofile_base_path, result_doc_path, tmp_base_path]:
|
|
94
|
+
path.mkdir(exist_ok=True)
|
|
95
|
+
|
|
96
|
+
# Set language for prompts
|
|
97
|
+
lang_mapping = {"zh-CN": "cn", "en_US": "en"}
|
|
98
|
+
lang, _ = locale.getdefaultlocale()
|
|
99
|
+
pmp.set_lang(lang_mapping.get(lang, "en"))
|
|
100
|
+
|
|
101
|
+
yield StataServerContext(
|
|
102
|
+
config=config,
|
|
103
|
+
stata_finder=stata_finder,
|
|
104
|
+
working_directory=working_directory,
|
|
105
|
+
output_base_path=output_base_path
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_stata_server(config: Optional[StataServerConfig] = None) -> FastMCP:
|
|
110
|
+
"""Create and configure the Stata MCP server."""
|
|
111
|
+
config = config or StataServerConfig()
|
|
112
|
+
|
|
113
|
+
# Create server with lifespan
|
|
114
|
+
@asynccontextmanager
|
|
115
|
+
async def lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
|
|
116
|
+
async with server_lifespan(server, config) as context:
|
|
117
|
+
yield {"stata_context": context}
|
|
118
|
+
|
|
119
|
+
# Initialize server with latest MCP features
|
|
120
|
+
server = FastMCP(
|
|
121
|
+
name=config.name,
|
|
122
|
+
instructions=config.instructions,
|
|
123
|
+
website_url=config.website_url,
|
|
124
|
+
icons=[
|
|
125
|
+
Icon(
|
|
126
|
+
src="https://avatars.githubusercontent.com/u/201514154?v=4",
|
|
127
|
+
mimeType="image/png",
|
|
128
|
+
sizes=["460*460"]
|
|
129
|
+
)
|
|
130
|
+
],
|
|
131
|
+
lifespan=lifespan
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Register all components
|
|
135
|
+
register_core_tools(server)
|
|
136
|
+
register_file_tools(server)
|
|
137
|
+
register_stata_tools(server)
|
|
138
|
+
register_core_prompts(server)
|
|
139
|
+
|
|
140
|
+
return server
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Export the default server instance
|
|
144
|
+
stata_server = create_stata_server()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def run_server(transport: str = "stdio") -> None:
|
|
148
|
+
"""Run the Stata MCP server with the specified transport."""
|
|
149
|
+
stata_server.run(transport=transport)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
run_server()
|