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.
Files changed (38) hide show
  1. aigroup_stata_mcp-1.0.3.dist-info/METADATA +345 -0
  2. aigroup_stata_mcp-1.0.3.dist-info/RECORD +38 -0
  3. aigroup_stata_mcp-1.0.3.dist-info/WHEEL +4 -0
  4. aigroup_stata_mcp-1.0.3.dist-info/entry_points.txt +5 -0
  5. aigroup_stata_mcp-1.0.3.dist-info/licenses/LICENSE +21 -0
  6. stata_mcp/__init__.py +18 -0
  7. stata_mcp/cli/__init__.py +8 -0
  8. stata_mcp/cli/_cli.py +95 -0
  9. stata_mcp/core/__init__.py +14 -0
  10. stata_mcp/core/data_info/__init__.py +11 -0
  11. stata_mcp/core/data_info/_base.py +288 -0
  12. stata_mcp/core/data_info/csv.py +123 -0
  13. stata_mcp/core/data_info/dta.py +70 -0
  14. stata_mcp/core/stata/__init__.py +13 -0
  15. stata_mcp/core/stata/stata_controller/__init__.py +9 -0
  16. stata_mcp/core/stata/stata_controller/controller.py +208 -0
  17. stata_mcp/core/stata/stata_do/__init__.py +9 -0
  18. stata_mcp/core/stata/stata_do/do.py +177 -0
  19. stata_mcp/core/stata/stata_finder/__init__.py +9 -0
  20. stata_mcp/core/stata/stata_finder/base.py +294 -0
  21. stata_mcp/core/stata/stata_finder/finder.py +193 -0
  22. stata_mcp/core/stata/stata_finder/linux.py +43 -0
  23. stata_mcp/core/stata/stata_finder/macos.py +88 -0
  24. stata_mcp/core/stata/stata_finder/windows.py +191 -0
  25. stata_mcp/server/__init__.py +8 -0
  26. stata_mcp/server/main.py +153 -0
  27. stata_mcp/server/prompts/__init__.py +8 -0
  28. stata_mcp/server/prompts/core_prompts.py +122 -0
  29. stata_mcp/server/tools/__init__.py +10 -0
  30. stata_mcp/server/tools/core_tools.py +59 -0
  31. stata_mcp/server/tools/file_tools.py +163 -0
  32. stata_mcp/server/tools/stata_tools.py +221 -0
  33. stata_mcp/utils/Installer/__init__.py +7 -0
  34. stata_mcp/utils/Installer/installer.py +85 -0
  35. stata_mcp/utils/Prompt/__init__.py +74 -0
  36. stata_mcp/utils/Prompt/string.py +91 -0
  37. stata_mcp/utils/__init__.py +23 -0
  38. stata_mcp/utils/usable.py +244 -0
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import os
6
+ from datetime import datetime
7
+ from typing import Dict, Optional
8
+
9
+ from mcp.server.fastmcp import Context, FastMCP
10
+ from mcp.server.session import ServerSession
11
+ from pydantic import BaseModel, Field
12
+
13
+ from ...utils.Prompt import pmp
14
+
15
+
16
+ class PromptResult(BaseModel):
17
+
18
+
19
+ """Result model for prompt operations."""
20
+
21
+ prompt_id: str = Field(description="The identifier of the prompt")
22
+ content: str = Field(description="The generated prompt content")
23
+ language: str = Field(description="The language of the prompt")
24
+ timestamp: str = Field(description="The timestamp when the prompt was generated")
25
+
26
+
27
+ def register_core_prompts(server: FastMCP) -> None:
28
+ """Register core prompts with the MCP server."""
29
+
30
+ @server.prompt()
31
+ def stata_assistant_role(ctx: Context[ServerSession, Dict], lang: Optional[str] = None) -> PromptResult:
32
+ """
33
+ Return the Stata assistant role prompt content.
34
+
35
+ This function retrieves a predefined prompt that defines the role and capabilities
36
+ of a Stata analysis assistant. The prompt helps set expectations and context for
37
+ the assistant's behavior when handling Stata-related tasks.
38
+
39
+ Args:
40
+ lang: Language code for localization of the prompt content.
41
+ If None, returns the default language version. Defaults to None.
42
+ Examples: "en" for English, "cn" for Chinese.
43
+
44
+ Returns:
45
+ PromptResult: Structured result containing prompt content and metadata.
46
+ """
47
+ content = pmp.get_prompt(prompt_id="stata_assistant_role", lang=lang)
48
+ actual_lang = lang or "default"
49
+
50
+ return PromptResult(
51
+ prompt_id="stata_assistant_role",
52
+ content=content,
53
+ language=actual_lang,
54
+ timestamp=datetime.now().isoformat()
55
+ )
56
+
57
+ @server.prompt()
58
+ def stata_analysis_strategy(ctx: Context[ServerSession, Dict], lang: Optional[str] = None) -> PromptResult:
59
+ """
60
+ Return the Stata analysis strategy prompt content.
61
+
62
+ This function retrieves a predefined prompt that outlines the recommended
63
+ strategy for conducting data analysis using Stata. The prompt includes
64
+ guidelines for data preparation, code generation, results management,
65
+ reporting, and troubleshooting.
66
+
67
+ Args:
68
+ lang: Language code for localization of the prompt content.
69
+ If None, returns the default language version. Defaults to None.
70
+ Examples: "en" for English, "cn" for Chinese.
71
+
72
+ Returns:
73
+ PromptResult: Structured result containing prompt content and metadata.
74
+ """
75
+ content = pmp.get_prompt(prompt_id="stata_analysis_strategy", lang=lang)
76
+ actual_lang = lang or "default"
77
+
78
+ return PromptResult(
79
+ prompt_id="stata_analysis_strategy",
80
+ content=content,
81
+ language=actual_lang,
82
+ timestamp=datetime.now().isoformat()
83
+ )
84
+
85
+ @server.prompt()
86
+ def results_doc_path(ctx: Context[ServerSession, Dict]) -> PromptResult:
87
+ """
88
+ Generate and return a result document storage path based on the current timestamp.
89
+
90
+ This function performs the following operations:
91
+ 1. Gets the current system time and formats it as a '%Y%m%d%H%M%S' timestamp string
92
+ 2. Concatenates this timestamp string with the preset result_doc_path base path to form a complete path
93
+ 3. Creates the directory corresponding to that path (no error if directory already exists)
94
+ 4. Returns the complete path string of the newly created directory
95
+
96
+ Returns:
97
+ PromptResult: Structured result containing the generated path and metadata.
98
+ """
99
+ stata_context = ctx.request_context.lifespan_context["stata_context"]
100
+ result_doc_path = stata_context.output_base_path / "stata-mcp-result"
101
+
102
+ path = result_doc_path / datetime.now().strftime("%Y%m%d%H%M%S")
103
+ path.mkdir(exist_ok=True)
104
+
105
+ content = f"""
106
+ The result document path has been created at:
107
+ {path}
108
+
109
+ You can use this path for Stata commands that generate output files, such as:
110
+ - outreg2: outreg2 using "{path}/results", replace
111
+ - esttab: esttab using "{path}/regression_results.rtf", replace
112
+ - graph export: graph export "{path}/figure.png", replace
113
+
114
+ Make sure to include the appropriate file extensions in your Stata commands.
115
+ """
116
+
117
+ return PromptResult(
118
+ prompt_id="results_doc_path",
119
+ content=content,
120
+ language="en",
121
+ timestamp=datetime.now().isoformat()
122
+ )
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ """Stata MCP Tools package."""
6
+ from .core_tools import register_core_tools
7
+ from .file_tools import register_file_tools
8
+ from .stata_tools import register_stata_tools
9
+
10
+ __all__ = ["register_core_tools", "register_file_tools", "register_stata_tools"]
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import os
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+
10
+ from mcp.server.fastmcp import Context, FastMCP
11
+ from mcp.server.session import ServerSession
12
+ from pydantic import BaseModel, Field
13
+
14
+ from ...core.stata import StataController
15
+
16
+
17
+ def register_core_tools(server: FastMCP) -> None:
18
+ """Register core Stata tools with the MCP server."""
19
+
20
+ @server.tool()
21
+ def mk_dir(ctx: Context[ServerSession, Dict], path: str) -> bool:
22
+ """
23
+ Safely create a directory using pathvalidate for security validation.
24
+
25
+ Args:
26
+ path: The path you want to create
27
+
28
+ Returns:
29
+ bool: True if the path exists now, False if not successful
30
+
31
+ Raises:
32
+ ValueError: if path is invalid or contains unsafe components
33
+ PermissionError: if insufficient permissions to create directory
34
+ """
35
+ from pathvalidate import ValidationError, sanitize_filepath
36
+
37
+ # Input validation
38
+ if not path or not isinstance(path, str):
39
+ raise ValueError("Path must be a non-empty string")
40
+
41
+ try:
42
+ # Use pathvalidate to sanitize and validate path
43
+ safe_path = sanitize_filepath(path, platform="auto")
44
+
45
+ # Get absolute path for further validation
46
+ absolute_path = os.path.abspath(safe_path)
47
+
48
+ # Create directory with reasonable permissions
49
+ os.makedirs(absolute_path, exist_ok=True, mode=0o755)
50
+
51
+ # Verify successful creation
52
+ return os.path.exists(absolute_path) and os.path.isdir(absolute_path)
53
+
54
+ except ValidationError as e:
55
+ raise ValueError(f"Invalid path detected: {e}")
56
+ except PermissionError:
57
+ raise PermissionError(f"Insufficient permissions to create directory: {path}")
58
+ except OSError as e:
59
+ raise OSError(f"Failed to create directory {path}: {str(e)}")
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import os
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+
10
+ from mcp.server.fastmcp import Context, FastMCP, Image
11
+ from mcp.server.session import ServerSession
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ class ReadFileResult(BaseModel):
16
+
17
+
18
+ """Result model for file reading operation."""
19
+
20
+ file_path: str = Field(description="The path to the file that was read")
21
+ content: str = Field(description="The content of the file")
22
+ encoding: str = Field(description="The encoding used to read the file")
23
+
24
+
25
+ class WriteDofileResult(BaseModel):
26
+
27
+
28
+ """Result model for dofile writing operation."""
29
+
30
+ file_path: str = Field(description="The path to the created dofile")
31
+ content_length: int = Field(description="The length of the content written")
32
+ timestamp: str = Field(description="The timestamp when the file was created")
33
+
34
+
35
+ class AppendDofileResult(BaseModel):
36
+
37
+
38
+ """Result model for dofile appending operation."""
39
+
40
+ new_file_path: str = Field(description="The path to the new dofile")
41
+ original_exists: bool = Field(description="Whether the original file existed")
42
+ total_content_length: int = Field(description="Total length of content after appending")
43
+
44
+
45
+ def register_file_tools(server: FastMCP) -> None:
46
+ """Register file-related tools with the MCP server."""
47
+
48
+ @server.tool()
49
+ def read_file(ctx: Context[ServerSession, Dict], file_path: str, encoding: str = "utf-8") -> ReadFileResult:
50
+ """
51
+ Reads a file and returns its content as a string.
52
+
53
+ Args:
54
+ file_path: The full path to the file to be read.
55
+ encoding: The encoding used to decode the file. Defaults to "utf-8".
56
+
57
+ Returns:
58
+ ReadFileResult: Structured result containing file content and metadata.
59
+ """
60
+ if not os.path.exists(file_path):
61
+ raise FileNotFoundError(f"The file at {file_path} does not exist.")
62
+
63
+ try:
64
+ with open(file_path, "r", encoding=encoding) as file:
65
+ log_content = file.read()
66
+
67
+ return ReadFileResult(
68
+ file_path=file_path,
69
+ content=log_content,
70
+ encoding=encoding
71
+ )
72
+ except IOError as e:
73
+ raise IOError(f"An error occurred while reading the file: {e}")
74
+
75
+ @server.tool()
76
+ def write_dofile(ctx: Context[ServerSession, Dict], content: str, encoding: str = "utf-8") -> WriteDofileResult:
77
+ """
78
+ Write stata code to a dofile and return the do-file path.
79
+
80
+ Args:
81
+ content: The stata code content which will be written to the designated do-file.
82
+ encoding: The encoding method for the dofile, default -> 'utf-8'
83
+
84
+ Returns:
85
+ WriteDofileResult: Structured result containing file path and metadata.
86
+ """
87
+ stata_context = ctx.request_context.lifespan_context["stata_context"]
88
+ dofile_base_path = stata_context.output_base_path / "stata-mcp-dofile"
89
+
90
+ file_path = dofile_base_path / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.do"
91
+
92
+ with open(file_path, "w", encoding=encoding) as f:
93
+ f.write(content)
94
+
95
+ return WriteDofileResult(
96
+ file_path=str(file_path),
97
+ content_length=len(content),
98
+ timestamp=datetime.now().isoformat()
99
+ )
100
+
101
+ @server.tool()
102
+ def append_dofile(ctx: Context[ServerSession, Dict], original_dofile_path: str, content: str, encoding: str = "utf-8") -> AppendDofileResult:
103
+ """
104
+ Append stata code to an existing dofile or create a new one.
105
+
106
+ Args:
107
+ original_dofile_path: Path to the original dofile to append to.
108
+ If empty or invalid, a new file will be created.
109
+ content: The stata code content which will be appended to the designated do-file.
110
+ encoding: The encoding method for the dofile, default -> 'utf-8'
111
+
112
+ Returns:
113
+ AppendDofileResult: Structured result containing new file path and metadata.
114
+ """
115
+ stata_context = ctx.request_context.lifespan_context["stata_context"]
116
+ dofile_base_path = stata_context.output_base_path / "stata-mcp-dofile"
117
+
118
+ # Create a new file path for the output
119
+ new_file_path = dofile_base_path / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.do"
120
+
121
+ # Check if original file exists and is valid
122
+ original_exists = False
123
+ original_content = ""
124
+ if original_dofile_path and os.path.exists(original_dofile_path):
125
+ try:
126
+ with open(original_dofile_path, "r", encoding=encoding) as f:
127
+ original_content = f.read()
128
+ original_exists = True
129
+ except Exception:
130
+ # If there's any error reading the file, we'll create a new one
131
+ original_exists = False
132
+
133
+ # Write to the new file (either copying original content + new content, or just new content)
134
+ with open(new_file_path, "w", encoding=encoding) as f:
135
+ if original_exists:
136
+ f.write(original_content)
137
+ # Add a newline if the original file doesn't end with one
138
+ if original_content and not original_content.endswith("\n"):
139
+ f.write("\n")
140
+ f.write(content)
141
+
142
+ total_length = len(original_content) + len(content) if original_exists else len(content)
143
+
144
+ return AppendDofileResult(
145
+ new_file_path=str(new_file_path),
146
+ original_exists=original_exists,
147
+ total_content_length=total_length
148
+ )
149
+
150
+ @server.tool()
151
+ def load_figure(ctx: Context[ServerSession, Dict], figure_path: str) -> Image:
152
+ """
153
+ Load figure from device.
154
+
155
+ Args:
156
+ figure_path: the figure file path, only support png and jpg format
157
+
158
+ Returns:
159
+ Image: the figure thumbnail
160
+ """
161
+ if not os.path.exists(figure_path):
162
+ raise FileNotFoundError(f"{figure_path} not found")
163
+ return Image(figure_path)
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import os
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Callable, Dict, List, Optional
9
+
10
+ from mcp.server.fastmcp import Context, FastMCP
11
+ from mcp.server.session import ServerSession
12
+ from pydantic import BaseModel, Field
13
+
14
+ from ...core.data_info import CsvDataInfo, DtaDataInfo
15
+ from ...core.stata import StataDo
16
+
17
+
18
+ class DataInfoResult(BaseModel):
19
+
20
+
21
+ """Result model for data information operation."""
22
+
23
+ data_path: str = Field(description="The path to the data file")
24
+ file_extension: str = Field(description="The file extension of the data file")
25
+ summary: Dict[str, Dict] = Field(description="Summary statistics of the data")
26
+ info_file_path: Optional[str] = Field(description="Path to the saved info file, if applicable")
27
+
28
+
29
+ class StataDoResult(BaseModel):
30
+
31
+
32
+ """Result model for Stata dofile execution."""
33
+
34
+ dofile_path: str = Field(description="Path to the executed dofile")
35
+ log_file_path: str = Field(description="Path to the generated log file")
36
+ log_content: Optional[str] = Field(description="Content of the log file, if requested")
37
+ execution_time: str = Field(description="Timestamp of execution")
38
+
39
+
40
+ class SscInstallResult(BaseModel):
41
+
42
+
43
+ """Result model for SSC package installation."""
44
+
45
+ package: str = Field(description="The package that was installed")
46
+ success: bool = Field(description="Whether installation was successful")
47
+ log_content: str = Field(description="Installation log content")
48
+
49
+
50
+ def register_stata_tools(server: FastMCP) -> None:
51
+ """Register Stata-specific tools with the MCP server."""
52
+
53
+ @server.tool()
54
+ def get_data_info(
55
+ ctx: Context[ServerSession, Dict],
56
+ data_path: str,
57
+ vars_list: Optional[List[str]] = None,
58
+ encoding: str = "utf-8",
59
+ file_extension: Optional[str] = None,
60
+ is_save: bool = True,
61
+ save_path: Optional[str] = None,
62
+ info_file_encoding: str = "utf-8"
63
+ ) -> DataInfoResult:
64
+ """
65
+ Get descriptive statistics for the data file.
66
+
67
+ Args:
68
+ data_path: the data file's absolute path.
69
+ vars_list: the vars you want to get info (default is None, means all vars).
70
+ encoding: data file encoding method (dta file is not supported this arg).
71
+ file_extension: the data file's extension, default is None, then would find it automatically.
72
+ is_save: whether save the result to a txt file.
73
+ save_path: the data-info saved file path.
74
+ info_file_encoding: the data-info saved file encoding.
75
+
76
+ Returns:
77
+ DataInfoResult: Structured result containing data information.
78
+ """
79
+ stata_context = ctx.request_context.lifespan_context["stata_context"]
80
+ tmp_base_path = stata_context.output_base_path / "stata-mcp-tmp"
81
+
82
+ EXTENSION_METHOD_MAPPING: Dict[str, Callable] = {
83
+ "dta": DtaDataInfo,
84
+ "csv": CsvDataInfo
85
+ }
86
+
87
+ if file_extension is None:
88
+ file_extension = Path(data_path).suffix
89
+ file_extension = file_extension.split(".")[-1].lower()
90
+
91
+ if file_extension not in EXTENSION_METHOD_MAPPING:
92
+ raise ValueError(f"Unsupported file extension: {file_extension}")
93
+
94
+ cls = EXTENSION_METHOD_MAPPING.get(file_extension)
95
+ data_info = cls(
96
+ data_path=data_path,
97
+ vars_list=vars_list,
98
+ encoding=encoding,
99
+ is_save=is_save,
100
+ save_path=save_path,
101
+ info_file_encoding=info_file_encoding
102
+ ).info
103
+
104
+ info_file_path = None
105
+ if is_save:
106
+ if save_path is None:
107
+ data_name = Path(data_path).name.split(".")[0]
108
+ info_file_path = tmp_base_path / f"{data_name}.txt"
109
+ else:
110
+ info_file_path = Path(save_path)
111
+
112
+ info_file_path.parent.mkdir(parents=True, exist_ok=True)
113
+ with open(info_file_path, "w", encoding=info_file_encoding) as f:
114
+ f.write(str(data_info))
115
+
116
+ return DataInfoResult(
117
+ data_path=data_path,
118
+ file_extension=file_extension,
119
+ summary=data_info,
120
+ info_file_path=str(info_file_path) if info_file_path else None
121
+ )
122
+
123
+ @server.tool()
124
+ def stata_do(
125
+ ctx: Context[ServerSession, Dict],
126
+ dofile_path: str,
127
+ log_file_name: Optional[str] = None,
128
+ is_read_log: bool = True
129
+ ) -> StataDoResult:
130
+ """
131
+ Execute a Stata do-file and return the log file path with optional log content.
132
+
133
+ Args:
134
+ dofile_path: Absolute or relative path to the Stata do-file (.do) to execute.
135
+ log_file_name: Set log file name without a time-string. If None, using nowtime as filename.
136
+ is_read_log: Whether to read and return the log file content.
137
+
138
+ Returns:
139
+ StataDoResult: Structured result containing execution details.
140
+ """
141
+ stata_context = ctx.request_context.lifespan_context["stata_context"]
142
+ stata_cli = stata_context.stata_finder.STATA_CLI
143
+ log_base_path = stata_context.output_base_path / "stata-mcp-log"
144
+ dofile_base_path = stata_context.output_base_path / "stata-mcp-dofile"
145
+
146
+ # Initialize Stata executor with system configuration
147
+ stata_executor = StataDo(
148
+ stata_cli=stata_cli,
149
+ log_file_path=str(log_base_path),
150
+ dofile_base_path=str(dofile_base_path),
151
+ sys_os=platform.system()
152
+ )
153
+
154
+ # Execute the do-file and get log file path
155
+ log_file_path = stata_executor.execute_dofile(dofile_path, log_file_name)
156
+
157
+ # Return log content based on user preference
158
+ log_content = None
159
+ if is_read_log:
160
+ log_content = stata_executor.read_log(log_file_path)
161
+
162
+ return StataDoResult(
163
+ dofile_path=dofile_path,
164
+ log_file_path=log_file_path,
165
+ log_content=log_content,
166
+ execution_time=datetime.now().isoformat()
167
+ )
168
+
169
+ @server.tool()
170
+ def ssc_install(
171
+ ctx: Context[ServerSession, Dict],
172
+ command: str,
173
+ is_replace: bool = True
174
+ ) -> SscInstallResult:
175
+ """
176
+ Install a package from SSC.
177
+
178
+ Args:
179
+ command: The name of the package to be installed from SSC.
180
+ is_replace: Whether to force replacement of an existing installation.
181
+
182
+ Returns:
183
+ SscInstallResult: Structured result containing installation details.
184
+ """
185
+ stata_context = ctx.request_context.lifespan_context["stata_context"]
186
+ stata_cli = stata_context.stata_finder.STATA_CLI
187
+ log_base_path = stata_context.output_base_path / "stata-mcp-log"
188
+ dofile_base_path = stata_context.output_base_path / "stata-mcp-dofile"
189
+
190
+ replace_clause = ", replace" if is_replace else ""
191
+
192
+ # Create dofile content
193
+ content = f"ssc install {command}{replace_clause}"
194
+ file_path = dofile_base_path / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.do"
195
+
196
+ with open(file_path, "w", encoding="utf-8") as f:
197
+ f.write(content)
198
+
199
+ # Execute the dofile using StataDo directly
200
+ stata_executor = StataDo(
201
+ stata_cli=stata_cli,
202
+ log_file_path=str(log_base_path),
203
+ dofile_base_path=str(dofile_base_path),
204
+ sys_os=platform.system()
205
+ )
206
+
207
+ log_file_path = stata_executor.execute_dofile(str(file_path))
208
+ log_content = stata_executor.read_log(log_file_path)
209
+
210
+ # Check for success
211
+ success = "not found" not in log_content.lower() if log_content else False
212
+
213
+ return SscInstallResult(
214
+ package=command,
215
+ success=success,
216
+ log_content=log_content or ""
217
+ )
218
+
219
+
220
+ # Import platform for system detection
221
+ import platform
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ from .installer import Installer
6
+
7
+ __all__ = ["Installer"]
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import json
6
+ import os
7
+
8
+ from ...core.stata import StataFinder
9
+
10
+
11
+ class Installer:
12
+
13
+
14
+
15
+ def __init__(self, sys_os, is_env=False):
16
+ """简要描述函数功能"""
17
+ self.config_file_path: str = None
18
+ if sys_os == "Darwin":
19
+ self.config_file_path = os.path.expanduser(
20
+ "~/Library/Application Support/Claude/claude_desktop_config.json"
21
+ )
22
+ elif sys_os == "Linux":
23
+ print(
24
+ "There is not a Linux version of Claude yet, please use the Windows or macOS version."
25
+ )
26
+ elif sys_os == "Windows":
27
+ appdata = os.getenv(
28
+ "APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
29
+ self.config_file_path = os.path.join(
30
+ appdata, "Claude", "claude_desktop_config.json"
31
+ )
32
+
33
+ os.makedirs(os.path.dirname(self.config_file_path), exist_ok=True)
34
+
35
+ # Create an empty file if it does not already exist
36
+ if not os.path.exists(self.config_file_path):
37
+ with open(self.config_file_path, "w", encoding="utf-8") as f:
38
+ # Or write the default configuration
39
+ f.write('{"mcpServers": {}}')
40
+
41
+ stata_cli = StataFinder().STATA_CLI
42
+ self.stata_mcp_config = {
43
+ "stata-mcp": {
44
+ "command": "uvx",
45
+ "args": ["aigroup-stata-mcp"],
46
+ "env": {"STATA_CLI": stata_cli},
47
+ }
48
+ }
49
+
50
+ def install(self):
51
+ """简要描述函数功能"""
52
+ server_cfg = self.stata_mcp_config["stata-mcp"]
53
+ stata_cli_path = server_cfg["env"]["STATA_CLI"]
54
+ print("About to install the following MCP server into your Claude config:\n")
55
+ print(" Server name: stata-mcp")
56
+ print(f" Command: {server_cfg['command']}")
57
+ print(f" Args: {server_cfg['args']}")
58
+ print(f" STATA_CLI path: {stata_cli_path}\n")
59
+ print(f"Configuration file to modify:\n {self.config_file_path}\n")
60
+
61
+ # Ask the user for confirmation
62
+ choice = input(
63
+ "Do you want to proceed and add this configuration? [y/N]: ")
64
+ if choice.strip().lower() != "y":
65
+ print("Installation aborted.")
66
+ return
67
+
68
+ # Read the now config
69
+ try:
70
+ with open(self.config_file_path, "r", encoding="utf-8") as f:
71
+ config = json.load(f)
72
+ except (FileNotFoundError, json.JSONDecodeError):
73
+ config = {"mcpServers": {}}
74
+
75
+ # Update MCP_Config
76
+ servers = config.setdefault("mcpServers", {})
77
+ servers.update(self.stata_mcp_config)
78
+
79
+ # Write it
80
+ with open(self.config_file_path, "w", encoding="utf-8") as f:
81
+ json.dump(config, f, ensure_ascii=False, indent=2)
82
+
83
+ print(
84
+ f"✅ Successfully wrote 'stata-mcp' configuration to: {self.config_file_path}"
85
+ )