2
2
MCP Python Interpreter
3
3
4
4
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.
6
7
"""
7
8
8
9
import os
29
30
help = 'Custom Python interpreter path to use as default' )
30
31
args , unknown = parser .parse_known_args ()
31
32
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
+
32
36
# Set and create working directory
33
37
WORKING_DIR = Path (args .dir ).absolute ()
34
38
WORKING_DIR .mkdir (parents = True , exist_ok = True )
39
43
# Print startup message to stderr (doesn't interfere with MCP protocol)
40
44
print (f"MCP Python Interpreter starting in directory: { WORKING_DIR } " , file = sys .stderr )
41
45
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 )
42
47
43
48
# Create our MCP server
44
49
mcp = FastMCP (
45
50
"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 } ' } " ,
47
52
dependencies = ["mcp[cli]" ]
48
53
)
49
54
50
55
# ============================================================================
51
56
# Helper functions
52
57
# ============================================================================
53
58
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
+
54
74
def get_python_environments () -> List [Dict [str , str ]]:
55
75
"""Get all available Python environments (system and conda)."""
56
76
environments = []
@@ -237,13 +257,13 @@ def read_file(file_path: str, max_size_kb: int = 1024) -> str:
237
257
Returns:
238
258
str: File content or an error message
239
259
"""
240
- # Ensure file path is within the working directory
260
+ # Handle path based on security settings
241
261
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
247
267
path = WORKING_DIR / path
248
268
249
269
try :
@@ -288,24 +308,24 @@ def write_file(
288
308
encoding : str = 'utf-8'
289
309
) -> str :
290
310
"""
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 .
292
312
293
313
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 )
295
315
content: Content to write to the file
296
316
overwrite: Whether to overwrite the file if it exists (default: False)
297
317
encoding: File encoding (default: utf-8)
298
318
299
319
Returns:
300
320
str: Status message about the file writing operation
301
321
"""
302
- # Ensure file path is within the working directory
322
+ # Handle path based on security settings
303
323
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
309
329
path = WORKING_DIR / path
310
330
311
331
try :
@@ -362,11 +382,11 @@ def list_directory(directory_path: str = "") -> str:
362
382
else :
363
383
# Handle absolute paths
364
384
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
370
390
path = WORKING_DIR / directory_path
371
391
372
392
# Check if directory exists
@@ -385,13 +405,19 @@ def list_directory(directory_path: str = "") -> str:
385
405
386
406
# Group files by subdirectory for better organization
387
407
files_by_dir = {}
408
+ base_dir = path if ALLOW_SYSTEM_ACCESS else WORKING_DIR
409
+
388
410
for file in files :
389
411
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 )
395
421
396
422
if parent not in files_by_dir :
397
423
files_by_dir [parent ] = []
@@ -584,20 +610,21 @@ def write_python_file(
584
610
overwrite : bool = False
585
611
) -> str :
586
612
"""
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 .
588
614
589
615
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 )
591
617
content: Content to write to the file
592
618
overwrite: Whether to overwrite the file if it exists (default: False)
593
619
"""
594
- # Ensure file path is within the working directory
620
+ # Handle path based on security settings
595
621
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
601
628
path = WORKING_DIR / path
602
629
603
630
# Check if the file exists
@@ -623,19 +650,22 @@ def run_python_file(
623
650
arguments : Optional [List [str ]] = None
624
651
) -> str :
625
652
"""
626
- Execute a Python file in the working directory and return the result.
653
+ Execute a Python file and return the result.
627
654
628
655
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 )
630
657
environment: Name of the Python environment to use (default if custom path provided, otherwise system)
631
658
arguments: List of command-line arguments to pass to the script
632
659
"""
633
- # Ensure file path is within the working directory
660
+ # Handle path based on security settings
634
661
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
636
668
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 } "
639
669
640
670
if not path .exists ():
641
671
return f"File '{ path } ' not found."
0 commit comments