Skip to content

Commit eb2e7ce

Browse files
committed
add safe mode
1 parent 3a1a862 commit eb2e7ce

File tree

3 files changed

+79
-43
lines changed

3 files changed

+79
-43
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ uv install mcp-python-interpreter
4444
"/path/to/your/work/dir",
4545
"--python-path",
4646
"/path/to/your/python"
47-
]
47+
],
48+
"env": {
49+
"MCP_ALLOW_SYSTEM_ACCESS": 0
50+
},
4851
}
4952
}
5053
}
@@ -63,7 +66,10 @@ For Windows:
6366
"C:\\path\\to\\your\\working\\directory",
6467
"--python-path",
6568
"/path/to/your/python"
66-
]
69+
],
70+
"env": {
71+
"MCP_ALLOW_SYSTEM_ACCESS": 0
72+
},
6773
}
6874
}
6975
}

mcp_python_interpreter/server.py

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
MCP Python Interpreter
33
44
A Model Context Protocol server for interacting with Python environments
5-
and executing Python code. All operations are confined to a specified working directory.
5+
and executing Python code. All operations are confined to a specified working directory
6+
or allowed system-wide if explicitly enabled.
67
"""
78

89
import os
@@ -29,6 +30,9 @@
2930
help='Custom Python interpreter path to use as default')
3031
args, unknown = parser.parse_known_args()
3132

33+
# Check if system-wide access is enabled via environment variable
34+
ALLOW_SYSTEM_ACCESS = os.environ.get('MCP_ALLOW_SYSTEM_ACCESS', 'false').lower() in ('true', '1', 'yes')
35+
3236
# Set and create working directory
3337
WORKING_DIR = Path(args.dir).absolute()
3438
WORKING_DIR.mkdir(parents=True, exist_ok=True)
@@ -39,18 +43,34 @@
3943
# Print startup message to stderr (doesn't interfere with MCP protocol)
4044
print(f"MCP Python Interpreter starting in directory: {WORKING_DIR}", file=sys.stderr)
4145
print(f"Using default Python interpreter: {DEFAULT_PYTHON_PATH}", file=sys.stderr)
46+
print(f"System-wide file access: {'ENABLED' if ALLOW_SYSTEM_ACCESS else 'DISABLED'}", file=sys.stderr)
4247

4348
# Create our MCP server
4449
mcp = FastMCP(
4550
"Python Interpreter",
46-
description=f"Execute Python code, access Python environments, and manage Python files in directory: {WORKING_DIR}",
51+
description=f"Execute Python code, access Python environments, and manage Python files{' system-wide' if ALLOW_SYSTEM_ACCESS else f' in directory: {WORKING_DIR}'}",
4752
dependencies=["mcp[cli]"]
4853
)
4954

5055
# ============================================================================
5156
# Helper functions
5257
# ============================================================================
5358

59+
def is_path_allowed(path: Path) -> bool:
60+
"""
61+
Check if a path is allowed based on security settings.
62+
63+
Args:
64+
path: Path to check
65+
66+
Returns:
67+
bool: True if path is allowed, False otherwise
68+
"""
69+
if ALLOW_SYSTEM_ACCESS:
70+
return True
71+
72+
return str(path).startswith(str(WORKING_DIR))
73+
5474
def get_python_environments() -> List[Dict[str, str]]:
5575
"""Get all available Python environments (system and conda)."""
5676
environments = []
@@ -237,13 +257,13 @@ def read_file(file_path: str, max_size_kb: int = 1024) -> str:
237257
Returns:
238258
str: File content or an error message
239259
"""
240-
# Ensure file path is within the working directory
260+
# Handle path based on security settings
241261
path = Path(file_path)
242-
if path.is_absolute() and not str(path).startswith(str(WORKING_DIR)):
243-
return f"Access denied: Path must be within {WORKING_DIR}"
244-
245-
# Make path relative to working directory if it's not already absolute
246-
if not path.is_absolute():
262+
if path.is_absolute():
263+
if not is_path_allowed(path):
264+
return f"Access denied: System-wide file access is {'DISABLED' if not ALLOW_SYSTEM_ACCESS else 'ENABLED, but this path is not allowed'}"
265+
else:
266+
# Make path relative to working directory if it's not already absolute
247267
path = WORKING_DIR / path
248268

249269
try:
@@ -288,24 +308,24 @@ def write_file(
288308
encoding: str = 'utf-8'
289309
) -> str:
290310
"""
291-
Write content to a file in the working directory.
311+
Write content to a file in the working directory or system-wide if allowed.
292312
293313
Args:
294-
file_path: Path to the file to write (relative to working directory)
314+
file_path: Path to the file to write (relative to working directory or absolute if system access is enabled)
295315
content: Content to write to the file
296316
overwrite: Whether to overwrite the file if it exists (default: False)
297317
encoding: File encoding (default: utf-8)
298318
299319
Returns:
300320
str: Status message about the file writing operation
301321
"""
302-
# Ensure file path is within the working directory
322+
# Handle path based on security settings
303323
path = Path(file_path)
304-
if path.is_absolute() and not str(path).startswith(str(WORKING_DIR)):
305-
return f"For security reasons, you can only write files inside the working directory: {WORKING_DIR}"
306-
307-
# Make path relative to working directory if it's not already
308-
if not path.is_absolute():
324+
if path.is_absolute():
325+
if not is_path_allowed(path):
326+
return f"For security reasons, you can only write files inside the working directory: {WORKING_DIR} (System-wide access is disabled)"
327+
else:
328+
# Make path relative to working directory if it's not already
309329
path = WORKING_DIR / path
310330

311331
try:
@@ -362,11 +382,11 @@ def list_directory(directory_path: str = "") -> str:
362382
else:
363383
# Handle absolute paths
364384
path = Path(directory_path)
365-
if path.is_absolute() and not str(path).startswith(str(WORKING_DIR)):
366-
return f"Access denied: Path must be within {WORKING_DIR}"
367-
368-
# Make path relative to working directory if it's not already absolute
369-
if not path.is_absolute():
385+
if path.is_absolute():
386+
if not is_path_allowed(path):
387+
return f"Access denied: System-wide file access is {'DISABLED' if not ALLOW_SYSTEM_ACCESS else 'ENABLED, but this path is not allowed'}"
388+
else:
389+
# Make path relative to working directory if it's not already absolute
370390
path = WORKING_DIR / directory_path
371391

372392
# Check if directory exists
@@ -385,13 +405,19 @@ def list_directory(directory_path: str = "") -> str:
385405

386406
# Group files by subdirectory for better organization
387407
files_by_dir = {}
408+
base_dir = path if ALLOW_SYSTEM_ACCESS else WORKING_DIR
409+
388410
for file in files:
389411
file_path = Path(file["path"])
390-
relative_path = file_path.relative_to(WORKING_DIR)
391-
parent = str(relative_path.parent)
392-
393-
if parent == ".":
394-
parent = "(root)"
412+
try:
413+
relative_path = file_path.relative_to(base_dir)
414+
parent = str(relative_path.parent)
415+
416+
if parent == ".":
417+
parent = "(root)"
418+
except ValueError:
419+
# This can happen with system-wide access enabled
420+
parent = str(file_path.parent)
395421

396422
if parent not in files_by_dir:
397423
files_by_dir[parent] = []
@@ -584,20 +610,21 @@ def write_python_file(
584610
overwrite: bool = False
585611
) -> str:
586612
"""
587-
Write content to a Python file in the working directory.
613+
Write content to a Python file in the working directory or system-wide if allowed.
588614
589615
Args:
590-
file_path: Path to the file to write (relative to working directory)
616+
file_path: Path to the file to write (relative to working directory or absolute if system access is enabled)
591617
content: Content to write to the file
592618
overwrite: Whether to overwrite the file if it exists (default: False)
593619
"""
594-
# Ensure file path is within the working directory
620+
# Handle path based on security settings
595621
path = Path(file_path)
596-
if path.is_absolute() and not str(path).startswith(str(WORKING_DIR)):
597-
return f"For security reasons, you can only write files inside the working directory: {WORKING_DIR}"
598-
599-
# Make path relative to working directory if it's not already
600-
if not path.is_absolute():
622+
if path.is_absolute():
623+
if not is_path_allowed(path):
624+
security_status = "DISABLED" if not ALLOW_SYSTEM_ACCESS else "ENABLED, but this path is not allowed"
625+
return f"For security reasons, you can only write files inside the working directory: {WORKING_DIR} (System-wide access is {security_status})"
626+
else:
627+
# Make path relative to working directory if it's not already
601628
path = WORKING_DIR / path
602629

603630
# Check if the file exists
@@ -623,19 +650,22 @@ def run_python_file(
623650
arguments: Optional[List[str]] = None
624651
) -> str:
625652
"""
626-
Execute a Python file in the working directory and return the result.
653+
Execute a Python file and return the result.
627654
628655
Args:
629-
file_path: Path to the Python file to execute (relative to working directory)
656+
file_path: Path to the Python file to execute (relative to working directory or absolute if system access is enabled)
630657
environment: Name of the Python environment to use (default if custom path provided, otherwise system)
631658
arguments: List of command-line arguments to pass to the script
632659
"""
633-
# Ensure file path is within the working directory
660+
# Handle path based on security settings
634661
path = Path(file_path)
635-
if not path.is_absolute():
662+
if path.is_absolute():
663+
if not is_path_allowed(path):
664+
security_status = "DISABLED" if not ALLOW_SYSTEM_ACCESS else "ENABLED, but this path is not allowed"
665+
return f"For security reasons, you can only run files inside the working directory: {WORKING_DIR} (System-wide access is {security_status})"
666+
else:
667+
# Make path relative to working directory if it's not already
636668
path = WORKING_DIR / path
637-
elif not str(path).startswith(str(WORKING_DIR)):
638-
return f"For security reasons, you can only run files inside the working directory: {WORKING_DIR}"
639669

640670
if not path.exists():
641671
return f"File '{path}' not found."

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "mcp-python-interpreter"
7-
version = "1.0"
7+
version = "1.1"
88
description = "MCP server for Python code execution and environment management"
99
authors = [
1010
{name = "YZFly", email = "ethereal_ai@hotmail.com"},

0 commit comments

Comments
 (0)