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,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,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
|
+
)
|