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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- 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()
|