atlas-chat 0.1.0__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 (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Execution environment management module for code executor.
4
+ Handles creation, cleanup, and file operations for isolated execution environments.
5
+ """
6
+
7
+ import base64
8
+ import binascii
9
+ import logging
10
+ import os
11
+ import shutil
12
+ import tempfile
13
+ import traceback
14
+ import uuid
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class CodeExecutionError(Exception):
22
+ """Raised when code execution fails."""
23
+ pass
24
+
25
+
26
+ def create_execution_environment() -> Path:
27
+ """Create a secure execution environment with UUID-based directory."""
28
+ try:
29
+ exec_id = str(uuid.uuid4())
30
+ base_dir = Path(tempfile.gettempdir()) / "secure_code_exec"
31
+ exec_dir = base_dir / exec_id
32
+
33
+ # Create directory structure
34
+ base_dir.mkdir(exist_ok=True)
35
+ exec_dir.mkdir(exist_ok=True)
36
+
37
+ logger.info(f"Created execution environment: {exec_dir}")
38
+ return exec_dir
39
+ except Exception as e:
40
+ error_msg = f"Failed to create execution environment: {str(e)}"
41
+ logger.error(error_msg)
42
+ logger.error(f"Traceback: {traceback.format_exc()}")
43
+ raise CodeExecutionError(error_msg)
44
+
45
+
46
+ def cleanup_execution_environment(exec_dir: Optional[Path]):
47
+ """Clean up the execution environment."""
48
+ try:
49
+ if exec_dir and exec_dir.exists():
50
+ shutil.rmtree(exec_dir)
51
+ logger.info(f"Cleaned up execution environment: {exec_dir}")
52
+ except Exception as e:
53
+ logger.warning(f"Failed to cleanup execution environment {exec_dir}: {str(e)}")
54
+ logger.warning(f"Traceback: {traceback.format_exc()}")
55
+
56
+
57
+ def save_file_to_execution_dir(filename: str, file_data_base64: str, exec_dir: Path) -> str:
58
+ """
59
+ Save a base64-encoded file to the execution directory.
60
+
61
+ Args:
62
+ filename: Name of the file
63
+ file_data_base64: Base64-encoded file data
64
+ exec_dir: Execution directory
65
+
66
+ Returns:
67
+ The filename that was saved
68
+ """
69
+ try:
70
+ logger.info(f"Saving file {filename} to execution directory: {exec_dir}")
71
+
72
+ # Decode the base64 data
73
+ file_data = base64.b64decode(file_data_base64)
74
+
75
+ # Ensure filename is safe (no path traversal)
76
+ safe_filename = os.path.basename(filename)
77
+ file_path = exec_dir / safe_filename
78
+
79
+ # Write the file
80
+ with open(file_path, 'wb') as f:
81
+ f.write(file_data)
82
+
83
+ logger.info(f"Successfully saved file: {safe_filename} ({len(file_data)} bytes)")
84
+ return safe_filename
85
+
86
+ except binascii.Error as e:
87
+ error_msg = f"Invalid base64 data for file {filename}: {str(e)}"
88
+ logger.error(error_msg)
89
+ logger.error(f"Traceback: {traceback.format_exc()}")
90
+ raise ValueError(error_msg)
91
+ except Exception as e:
92
+ error_msg = f"Failed to save file {filename}: {str(e)}"
93
+ logger.error(error_msg)
94
+ logger.error(f"Traceback: {traceback.format_exc()}")
95
+ raise ValueError(error_msg)
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Secure Code Execution MCP Server using FastMCP
4
+ Provides safe Python code execution with security controls.
5
+ """
6
+
7
+ import base64
8
+ import logging
9
+ import os
10
+ import time
11
+ import traceback
12
+ from pathlib import Path
13
+ from typing import Annotated, Any, Dict, Optional
14
+
15
+ import requests
16
+ from execution_engine import execute_code_safely
17
+ from execution_environment import CodeExecutionError, create_execution_environment, save_file_to_execution_dir
18
+ from fastmcp import FastMCP
19
+ from result_processing import (
20
+ create_visualization_html,
21
+ detect_matplotlib_plots,
22
+ encode_generated_files,
23
+ list_generated_files,
24
+ truncate_output_for_llm,
25
+ )
26
+ from script_generation import create_safe_execution_script
27
+
28
+ # Import from modular components
29
+ from security_checker import check_code_security
30
+
31
+ # Debug logging control
32
+ VERBOSE = False
33
+
34
+ # Configure logging to use main app log with prefix
35
+ current_dir = Path(__file__).parent
36
+ print(f"Current dir: {current_dir.absolute()}")
37
+ backend_dir = current_dir.parent.parent
38
+ print(f"Backend dir: {backend_dir.absolute()}")
39
+ project_root = backend_dir.parent
40
+ print(f"Project root: {project_root.absolute()}")
41
+ logs_dir = project_root / 'logs'
42
+ print(f"Logs dir: {logs_dir.absolute()}")
43
+ main_log_path = logs_dir / 'app.jsonl'
44
+ print(f"Log path: {main_log_path.absolute()}")
45
+ print(f"Log path exists: {main_log_path.exists()}")
46
+ print(f"Logs dir exists: {logs_dir.exists()}")
47
+ logging.basicConfig(
48
+ level=logging.INFO,
49
+ format='%(asctime)s - CODE_EXECUTOR - %(name)s - %(levelname)s - %(message)s',
50
+ handlers=[
51
+ logging.FileHandler(main_log_path),
52
+ logging.StreamHandler()
53
+ ]
54
+ )
55
+ logger = logging.getLogger(__name__)
56
+
57
+ # File loading constants and helpers
58
+ _PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
59
+ RUNTIME_UPLOADS = os.environ.get(
60
+ "CHATUI_RUNTIME_UPLOADS", os.path.join(_PROJECT_ROOT, "runtime", "uploads")
61
+ )
62
+
63
+ def _is_http_url(s: str) -> bool:
64
+ return s.startswith("http://") or s.startswith("https://")
65
+
66
+ def _is_backend_download_path(s: str) -> bool:
67
+ """Detect backend-relative download paths like /api/files/download/...."""
68
+ return isinstance(s, str) and s.startswith("/api/files/download/")
69
+
70
+ def _backend_base_url() -> str:
71
+ """Resolve backend base URL from environment variable.
72
+
73
+ Fallback to http://127.0.0.1:8000.
74
+ """
75
+ return os.environ.get("CHATUI_BACKEND_BASE_URL", "http://127.0.0.1:8000")
76
+
77
+ def _extract_clean_filename(filename: str) -> str:
78
+ """Extract clean filename from backend download URLs.
79
+
80
+ Handles patterns like: /api/files/download/1755397356_8d48a218_signal_data.csv?token=...
81
+ Returns: signal_data.csv
82
+ """
83
+ import re
84
+
85
+ # First, remove query parameters (everything after ?)
86
+ clean_path = filename.split('?')[0]
87
+
88
+ if clean_path.startswith('/api/files/download/'):
89
+ url_basename = os.path.basename(clean_path)
90
+ # Try to extract original filename from pattern: timestamp_hash_originalname.ext
91
+ match = re.match(r'^\d+_[a-f0-9]+_(.+)$', url_basename)
92
+ return match.group(1) if match else url_basename
93
+ else:
94
+ return os.path.basename(clean_path)
95
+
96
+ def _load_file_bytes(filename: str, file_data_base64: str = "") -> bytes:
97
+ """Return raw file bytes from either base64, URL, or local uploads path.
98
+
99
+ Priority:
100
+ 1) file_data_base64 if provided
101
+ 2) If filename is backend download path -> GET with base URL
102
+ 3) If filename is URL -> GET
103
+ 4) Try local file in runtime uploads
104
+ Raises FileNotFoundError or requests.HTTPError as appropriate.
105
+ """
106
+ if file_data_base64:
107
+ return base64.b64decode(file_data_base64)
108
+ # Support backend-injected relative download URLs by resolving with a base URL
109
+ if filename and _is_backend_download_path(filename):
110
+ base = _backend_base_url()
111
+ url = base.rstrip("/") + filename
112
+ r = requests.get(url, timeout=20)
113
+ r.raise_for_status()
114
+ return r.content
115
+
116
+ if filename and _is_http_url(filename):
117
+ r = requests.get(filename, timeout=20)
118
+ r.raise_for_status()
119
+ return r.content
120
+
121
+ # Fallback: treat filename as a key under runtime uploads
122
+ if filename:
123
+ local_path = filename
124
+ if not os.path.isabs(local_path):
125
+ local_path = os.path.join(RUNTIME_UPLOADS, filename)
126
+ if not os.path.exists(local_path):
127
+ raise FileNotFoundError(f"File not found: {local_path}")
128
+ with open(local_path, "rb") as f:
129
+ return f.read()
130
+
131
+ raise FileNotFoundError("No filename or file data provided")
132
+
133
+ # Initialize the MCP server
134
+ mcp = FastMCP("SecureCodeExecutor")
135
+
136
+
137
+ # Security checking functionality moved to security_checker.py
138
+
139
+
140
+ # Execution environment functionality moved to execution_environment.py
141
+
142
+
143
+ # Script generation functionality moved to script_generation.py
144
+
145
+
146
+ # Execution engine functionality moved to execution_engine.py
147
+
148
+
149
+ # Result processing functionality moved to result_processing.py
150
+
151
+
152
+ # Cleanup functionality moved to execution_environment.py
153
+
154
+
155
+ # File saving functionality moved to execution_environment.py
156
+
157
+
158
+ @mcp.tool
159
+ def execute_python_code_with_file(
160
+ code: Annotated[str, "Python code to execute"],
161
+ filename: Annotated[str, "Name of the file to make available to the code (optional - leave empty if not uploading a file)"] = "",
162
+ username: Annotated[str, "Injected by backend. Trust this value."] = "",
163
+ file_data_base64: Annotated[str, "Framework may supply Base64 content as fallback."] = ""
164
+ ) -> Dict[str, Any]:
165
+ """
166
+ Safely execute Python code in an isolated environment with optional file upload.
167
+
168
+ Demonstrates two v2 behaviors described in v2_mcp_note.md:
169
+ 1) filename to downloadable URLs: If the backend rewrites filename
170
+ to /api/files/download/... URLs, this server will fetch and process them.
171
+ It also accepts file_data_base64 as a fallback for content delivery.
172
+ 2) username injection: If a `username` parameter is defined in the tool schema,
173
+ the backend can inject the authenticated user's email/username. This server
174
+ trusts the provided username value and echoes it in outputs.
175
+
176
+ This function allows you to execute Python code either standalone or with access
177
+ to an uploaded file (e.g., CSV, JSON, TXT, etc.). If a file is provided, it will
178
+ be available in the execution directory and can be accessed by filename in your code.
179
+
180
+ IMPORTANT - Output Truncation:
181
+ - Console output (print statements, etc.) is limited to 2000 characters in LLM context.
182
+ - If output exceeds this limit, it will be truncated with a warning message.
183
+ - Full output is always available in generated downloadable files.
184
+ - Large data should be saved to files rather than printed to console.
185
+ - Use plt.savefig() for plots - they will be displayed separately from text output.
186
+
187
+ Constraints:
188
+ - Only a limited set of safe modules are allowed (e.g., numpy, pandas, matplotlib, seaborn, json, csv, math, etc.).
189
+ - Imports of dangerous or unauthorized modules (e.g., os, sys, subprocess, socket, requests, pickle, threading, etc.) are blocked.
190
+ - Dangerous built-in functions (e.g., eval, exec, compile, __import__, getattr, setattr, input, exit, quit, etc.) are forbidden.
191
+ - File I/O is restricted to the execution directory, with read-only access to matplotlib/seaborn config files for plotting.
192
+ - Matplotlib and seaborn plotting is fully supported - you MUST use plt.savefig() to create plot files (plt.show() will not work).
193
+ - Attribute access to __builtins__ and double-underscore attributes is forbidden.
194
+ - Code is executed in a temporary, isolated directory that is cleaned up after execution.
195
+ - Execution is time-limited (default: 30 seconds).
196
+ - Supports data analysis, visualization, and basic Python operations in a secure sandbox.
197
+
198
+ Example usage:
199
+ If you upload a file named "data.csv", you can access it in your code like:
200
+ ```python
201
+ import pandas as pd
202
+ import matplotlib.pyplot as plt
203
+
204
+ df = pd.read_csv('data.csv')
205
+ print(df.head()) # This will be truncated if very large
206
+
207
+ # For large datasets, save to file instead of printing
208
+ df.describe().to_csv('summary.csv') # Better approach for large output
209
+ print(f"Dataset has {len(df)} rows") # Concise summary instead
210
+
211
+ # Create plots - MUST use plt.savefig() to generate plot files
212
+ plt.figure(figsize=(10, 6))
213
+ plt.plot(df['column_name'])
214
+ plt.title('My Plot')
215
+ plt.savefig('my_plot.png') # REQUIRED - plt.show() won't work in this environment
216
+ plt.close() # Good practice to close figures
217
+ ```
218
+
219
+ Args:
220
+ code: Python code to execute (string)
221
+ filename: Name of the file to upload (Backend may rewrite to a downloadable URL)
222
+ username: Injected by backend. Trust this value.
223
+ file_data_base64: Framework may supply Base64 content as fallback.
224
+
225
+ Returns (MCP Contract):
226
+ {
227
+ "results": <primary result payload or {"error": msg}>,
228
+ "meta_data": {<small supplemental info incl. timings / flags>},
229
+ "returned_file_names": [...optional...],
230
+ "returned_file_contents": [...base64 contents matching names order...]
231
+ }
232
+ """
233
+ start_time = time.time()
234
+ exec_dir: Optional[Path] = None
235
+ try:
236
+ # Log basic invocation context
237
+ logger.info(
238
+ "Code executor start: filename=%s code_chars=%d",
239
+ filename or None,
240
+ len(code)
241
+ )
242
+
243
+ # 1. Security check
244
+ violations = check_code_security(code)
245
+ if violations:
246
+ return {
247
+ "results": {"error": "Security violations detected", "violations": violations},
248
+ "meta_data": {"is_error": True, "execution_time_sec": 0}
249
+ }
250
+
251
+ # 2. Environment setup
252
+ exec_dir = create_execution_environment()
253
+
254
+ # 3. Optional uploaded file
255
+ saved_filename = None
256
+ if filename:
257
+ try:
258
+ # Load file from URL or local path
259
+ if VERBOSE:
260
+ logger.info(f"Loading file: {filename}")
261
+ file_bytes = _load_file_bytes(filename, file_data_base64)
262
+ if VERBOSE:
263
+ logger.info(f"Loaded {len(file_bytes)} bytes from file")
264
+ # Save to execution directory (need to convert to base64 for existing function)
265
+ file_data_base64 = base64.b64encode(file_bytes).decode('utf-8')
266
+ # Extract clean filename for saving (remove URL prefixes if present)
267
+ clean_filename = _extract_clean_filename(filename)
268
+ if VERBOSE:
269
+ logger.info(f"Using clean filename: {clean_filename} (from original: {filename})")
270
+ saved_filename = save_file_to_execution_dir(clean_filename, file_data_base64, exec_dir)
271
+ if VERBOSE:
272
+ logger.info(f"Saved file as: {saved_filename} in directory: {exec_dir}")
273
+ # List files in exec directory for debugging
274
+ files_in_dir = list(exec_dir.glob('*'))
275
+ logger.info(f"Files in execution directory: {files_in_dir}")
276
+ except (FileNotFoundError, requests.HTTPError, ValueError) as e:
277
+ return {
278
+ "results": {"error": f"Failed to load file '{filename}': {str(e)}"},
279
+ "meta_data": {"is_error": True, "execution_time_sec": round(time.time() - start_time, 4)}
280
+ }
281
+
282
+ # 4. Create script & execute
283
+ script_path = create_safe_execution_script(code, exec_dir)
284
+ execution_result = execute_code_safely(script_path, timeout=30)
285
+ execution_time = time.time() - start_time
286
+
287
+ # Helper to build script artifact (always produced)
288
+ if filename:
289
+ script_content = f"""# Code Analysis Script\n# Generated by Secure Code Executor\n# Original file: {filename}\n\n{code}\n"""
290
+ script_filename = f"analysis_code_{filename.replace('.', '_')}.py"
291
+ else:
292
+ script_content = f"""# Code Execution Script\n# Generated by Secure Code Executor\n\n{code}\n"""
293
+ script_filename = "generated_code.py"
294
+ script_base64 = base64.b64encode(script_content.encode("utf-8")).decode("utf-8")
295
+
296
+ if execution_result.get("success"):
297
+ # Gather artifacts
298
+ generated_files = list_generated_files(exec_dir)
299
+ encoded_generated_files = encode_generated_files(exec_dir)
300
+ plots = detect_matplotlib_plots(exec_dir)
301
+
302
+ raw_output = execution_result.get("stdout", "")
303
+ truncated_output, _ = truncate_output_for_llm(raw_output)
304
+
305
+ # Visualization HTML (optional)
306
+ html_filename = None
307
+ if plots or raw_output.strip():
308
+ try:
309
+ visualization_html = create_visualization_html(plots, raw_output)
310
+ html_filename = f"execution_results_{int(time.time())}.html"
311
+ html_content_b64 = base64.b64encode(visualization_html.encode("utf-8")).decode("utf-8")
312
+ except Exception as html_err: # noqa: BLE001
313
+ logger.warning(f"Failed to generate visualization HTML: {html_err}")
314
+
315
+ # Convert to v2 artifacts format
316
+ artifacts = []
317
+
318
+ # Add script artifact
319
+ artifacts.append({
320
+ "name": script_filename,
321
+ "b64": script_base64,
322
+ "mime": "text/x-python",
323
+ "size": len(script_content.encode("utf-8")),
324
+ "description": f"Generated execution script: {script_filename}",
325
+ "viewer": "code"
326
+ })
327
+
328
+ # Add HTML visualization if generated
329
+ if html_filename and 'html_content_b64' in locals():
330
+ artifacts.append({
331
+ "name": html_filename,
332
+ "b64": html_content_b64,
333
+ "mime": "text/html",
334
+ "size": len(visualization_html.encode("utf-8")),
335
+ "description": f"Execution results visualization: {html_filename}",
336
+ "viewer": "html"
337
+ })
338
+
339
+ # Add other generated files
340
+ for file_info in encoded_generated_files:
341
+ filename = file_info["filename"]
342
+ content_b64 = file_info["content_base64"]
343
+
344
+ # Determine MIME type based on file extension
345
+ if filename.endswith('.html'):
346
+ mime_type = "text/html"
347
+ viewer = "html"
348
+ elif filename.endswith('.py'):
349
+ mime_type = "text/x-python"
350
+ viewer = "code"
351
+ elif filename.endswith('.png'):
352
+ mime_type = "image/png"
353
+ viewer = "image"
354
+ elif filename.endswith('.jpg') or filename.endswith('.jpeg'):
355
+ mime_type = "image/jpeg"
356
+ viewer = "image"
357
+ elif filename.endswith('.txt'):
358
+ mime_type = "text/plain"
359
+ viewer = "code"
360
+ else:
361
+ mime_type = "application/octet-stream"
362
+ viewer = "auto"
363
+
364
+ # Calculate size from base64
365
+ try:
366
+ size = len(base64.b64decode(content_b64))
367
+ except Exception:
368
+ size = 0
369
+
370
+ artifacts.append({
371
+ "name": filename,
372
+ "b64": content_b64,
373
+ "mime": mime_type,
374
+ "size": size,
375
+ "description": f"Generated from code execution: {filename}",
376
+ "viewer": viewer
377
+ })
378
+
379
+ results_payload: Dict[str, Any] = {
380
+ "summary": "Execution completed successfully",
381
+ "stdout": truncated_output,
382
+ }
383
+ if execution_result.get("stderr"):
384
+ stderr_val = execution_result.get("stderr", "")
385
+ if len(stderr_val) > 800:
386
+ results_payload["stderr"] = stderr_val[:800] + "... [truncated]"
387
+ results_payload["stderr_truncated"] = True
388
+ else:
389
+ results_payload["stderr"] = stderr_val
390
+ if saved_filename:
391
+ results_payload["uploaded_file"] = saved_filename
392
+
393
+ meta_data = {
394
+ "execution_time_sec": round(execution_time, 4),
395
+ "generated_file_count": len(artifacts),
396
+ "has_plots": bool(plots),
397
+ "is_error": False,
398
+ "generated_by": username
399
+ }
400
+
401
+ # Determine primary file for display (prefer HTML visualization)
402
+ primary_file = None
403
+ if html_filename:
404
+ primary_file = html_filename
405
+ elif artifacts:
406
+ primary_file = artifacts[0]["name"]
407
+
408
+ return {
409
+ "results": results_payload,
410
+ "meta_data": meta_data,
411
+ "artifacts": artifacts,
412
+ "display": {
413
+ "open_canvas": True,
414
+ "primary_file": primary_file,
415
+ "mode": "replace",
416
+ "viewer_hint": "html" if html_filename else "auto"
417
+ }
418
+ }
419
+ else:
420
+ # Failure path
421
+ raw_output = execution_result.get("stdout", "")
422
+ truncated_output, _ = truncate_output_for_llm(raw_output)
423
+ error_msg = execution_result.get("error", "Unknown execution error")
424
+ results_payload = {
425
+ "error": error_msg,
426
+ "stdout": truncated_output
427
+ }
428
+ if execution_result.get("stderr"):
429
+ stderr_val = execution_result.get("stderr", "")
430
+ if len(stderr_val) > 800:
431
+ results_payload["stderr"] = stderr_val[:800] + "... [truncated]"
432
+ results_payload["stderr_truncated"] = True
433
+ else:
434
+ results_payload["stderr"] = stderr_val
435
+ meta_data = {
436
+ "is_error": True,
437
+ "error_type": execution_result.get("error_type"),
438
+ "execution_time_sec": round(execution_time, 4),
439
+ "generated_by": username
440
+ }
441
+ # Convert failure to v2 format
442
+ artifacts = [{
443
+ "name": script_filename,
444
+ "b64": script_base64,
445
+ "mime": "text/x-python",
446
+ "size": len(script_content.encode("utf-8")),
447
+ "description": f"Failed execution script: {script_filename}",
448
+ "viewer": "code"
449
+ }]
450
+
451
+ return {
452
+ "results": results_payload,
453
+ "meta_data": meta_data,
454
+ "artifacts": artifacts,
455
+ "display": {
456
+ "open_canvas": False, # Don't auto-open on failure
457
+ "primary_file": script_filename,
458
+ "mode": "replace",
459
+ "viewer_hint": "code"
460
+ }
461
+ }
462
+ except CodeExecutionError as ce: # Specific controlled errors
463
+ exec_time = round(time.time() - start_time, 4)
464
+ logger.error(f"Code execution error: {ce}")
465
+ # Provide minimal artifact (script)
466
+ if filename:
467
+ script_content = f"""# Code Analysis Script\n# Generated by Secure Code Executor\n# Original file: {filename}\n\n{code}\n"""
468
+ script_filename = f"analysis_code_{filename.replace('.', '_')}.py"
469
+ else:
470
+ script_content = f"""# Code Execution Script\n# Generated by Secure Code Executor\n\n{code}\n"""
471
+ script_filename = "generated_code.py"
472
+ script_base64 = base64.b64encode(script_content.encode("utf-8")).decode("utf-8")
473
+ # Convert exception to v2 format
474
+ artifacts = [{
475
+ "name": script_filename,
476
+ "b64": script_base64,
477
+ "mime": "text/x-python",
478
+ "size": len(script_content.encode("utf-8")),
479
+ "description": f"Script that caused execution error: {script_filename}",
480
+ "viewer": "code"
481
+ }]
482
+
483
+ return {
484
+ "results": {"error": f"Code execution error: {str(ce)}"},
485
+ "meta_data": {"is_error": True, "error_type": "CodeExecutionError", "execution_time_sec": exec_time, "generated_by": username},
486
+ "artifacts": artifacts,
487
+ "display": {
488
+ "open_canvas": False, # Don't auto-open on error
489
+ "primary_file": script_filename,
490
+ "mode": "replace",
491
+ "viewer_hint": "code"
492
+ }
493
+ }
494
+ except Exception as e: # Catch-all
495
+ exec_time = round(time.time() - start_time, 4)
496
+ logger.error(f"Unexpected server error: {e}")
497
+ logger.error(f"Traceback: {traceback.format_exc()}")
498
+ if filename:
499
+ script_content = f"""# Code Analysis Script\n# Generated by Secure Code Executor\n# Original file: {filename}\n\n{code}\n"""
500
+ script_filename = f"analysis_code_{filename.replace('.', '_')}.py"
501
+ else:
502
+ script_content = f"""# Code Execution Script\n# Generated by Secure Code Executor\n\n{code}\n"""
503
+ script_filename = "generated_code.py"
504
+ script_base64 = base64.b64encode(script_content.encode("utf-8")).decode("utf-8")
505
+ # Convert general exception to v2 format
506
+ artifacts = [{
507
+ "name": script_filename,
508
+ "b64": script_base64,
509
+ "mime": "text/x-python",
510
+ "size": len(script_content.encode("utf-8")),
511
+ "description": f"Script that caused server error: {script_filename}",
512
+ "viewer": "code"
513
+ }]
514
+
515
+ return {
516
+ "results": {"error": f"Server error: {str(e)}"},
517
+ "meta_data": {"is_error": True, "error_type": type(e).__name__, "execution_time_sec": exec_time, "generated_by": username},
518
+ "artifacts": artifacts,
519
+ "display": {
520
+ "open_canvas": False, # Don't auto-open on error
521
+ "primary_file": script_filename,
522
+ "mode": "replace",
523
+ "viewer_hint": "code"
524
+ }
525
+ }
526
+
527
+ if __name__ == "__main__":
528
+ mcp.run()