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,13 @@
|
|
|
1
|
+
# PowerPoint Generator MCP Server Dependencies
|
|
2
|
+
|
|
3
|
+
# FastMCP framework for MCP server implementation
|
|
4
|
+
fastmcp>=2.3.5
|
|
5
|
+
|
|
6
|
+
# PowerPoint file generation
|
|
7
|
+
python-pptx>=0.6.21
|
|
8
|
+
|
|
9
|
+
# Image processing
|
|
10
|
+
Pillow>=10.0.0
|
|
11
|
+
|
|
12
|
+
# HTTP requests for image fetching
|
|
13
|
+
requests>=2.31.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest test_pptx_generator_security.py
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Security tests for the PPTX Generator MCP Server.
|
|
2
|
+
|
|
3
|
+
Tests XSS prevention and path traversal protection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from main import (
|
|
11
|
+
_clean_markdown_text,
|
|
12
|
+
_escape_html,
|
|
13
|
+
_is_safe_local_path,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestHTMLEscaping:
|
|
18
|
+
"""Tests for HTML escaping to prevent XSS attacks."""
|
|
19
|
+
|
|
20
|
+
def test_escape_html_basic_tags(self):
|
|
21
|
+
"""Test that basic HTML tags are escaped."""
|
|
22
|
+
assert _escape_html("<script>alert('xss')</script>") == "<script>alert('xss')</script>"
|
|
23
|
+
|
|
24
|
+
def test_escape_html_angle_brackets(self):
|
|
25
|
+
"""Test that angle brackets are escaped."""
|
|
26
|
+
assert _escape_html("<div>test</div>") == "<div>test</div>"
|
|
27
|
+
|
|
28
|
+
def test_escape_html_ampersand(self):
|
|
29
|
+
"""Test that ampersands are escaped."""
|
|
30
|
+
assert _escape_html("a & b") == "a & b"
|
|
31
|
+
|
|
32
|
+
def test_escape_html_quotes(self):
|
|
33
|
+
"""Test that quotes are escaped."""
|
|
34
|
+
result = _escape_html('test "quoted" text')
|
|
35
|
+
# Double quotes should be escaped to "
|
|
36
|
+
assert """ in result
|
|
37
|
+
assert '"' not in result
|
|
38
|
+
|
|
39
|
+
def test_escape_html_preserves_safe_text(self):
|
|
40
|
+
"""Test that safe text is preserved."""
|
|
41
|
+
safe_text = "Hello World 123"
|
|
42
|
+
assert _escape_html(safe_text) == safe_text
|
|
43
|
+
|
|
44
|
+
def test_escape_html_malicious_onclick(self):
|
|
45
|
+
"""Test that onclick handlers are escaped."""
|
|
46
|
+
malicious = '<img src="x" onerror="alert(1)">'
|
|
47
|
+
escaped = _escape_html(malicious)
|
|
48
|
+
# HTML tags should be escaped
|
|
49
|
+
assert "<img" not in escaped
|
|
50
|
+
assert "<img" in escaped
|
|
51
|
+
|
|
52
|
+
def test_escape_html_event_handlers(self):
|
|
53
|
+
"""Test that event handler injections are escaped."""
|
|
54
|
+
malicious = '" onmouseover="alert(1)" "'
|
|
55
|
+
escaped = _escape_html(malicious)
|
|
56
|
+
# Should not contain unescaped quotes that could break out of attributes
|
|
57
|
+
assert """ in escaped or "'" in escaped
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestPathTraversalProtection:
|
|
61
|
+
"""Tests for path traversal attack prevention."""
|
|
62
|
+
|
|
63
|
+
def test_safe_path_relative(self):
|
|
64
|
+
"""Test that relative paths within base directory are allowed."""
|
|
65
|
+
# Create a temporary file in the current directory
|
|
66
|
+
with tempfile.NamedTemporaryFile(delete=False, dir=".") as f:
|
|
67
|
+
temp_path = f.name
|
|
68
|
+
try:
|
|
69
|
+
# Should be safe since it's in the current directory
|
|
70
|
+
assert _is_safe_local_path(temp_path)
|
|
71
|
+
finally:
|
|
72
|
+
os.unlink(temp_path)
|
|
73
|
+
|
|
74
|
+
def test_unsafe_path_traversal(self):
|
|
75
|
+
"""Test that path traversal attempts are blocked."""
|
|
76
|
+
# Try to access /etc/passwd via path traversal
|
|
77
|
+
assert _is_safe_local_path("../../../etc/passwd") is False
|
|
78
|
+
assert _is_safe_local_path("../../../../etc/passwd") is False
|
|
79
|
+
|
|
80
|
+
def test_unsafe_path_absolute(self):
|
|
81
|
+
"""Test that absolute paths outside base directory are blocked."""
|
|
82
|
+
# These absolute paths should be blocked as they're outside base directory
|
|
83
|
+
assert _is_safe_local_path("/etc/passwd") is False
|
|
84
|
+
assert _is_safe_local_path("/var/log/syslog") is False
|
|
85
|
+
|
|
86
|
+
def test_safe_path_empty(self):
|
|
87
|
+
"""Test that empty paths are rejected."""
|
|
88
|
+
assert _is_safe_local_path("") is False
|
|
89
|
+
assert _is_safe_local_path(None) is False
|
|
90
|
+
|
|
91
|
+
def test_unsafe_path_with_null_bytes(self):
|
|
92
|
+
"""Test that paths with null bytes are handled safely."""
|
|
93
|
+
# Paths with null bytes could be used in certain attacks
|
|
94
|
+
# The function should handle these without crashing and reject them
|
|
95
|
+
try:
|
|
96
|
+
result = _is_safe_local_path("test\x00file.txt")
|
|
97
|
+
# Null bytes in paths should be rejected
|
|
98
|
+
assert result is False
|
|
99
|
+
except (ValueError, OSError):
|
|
100
|
+
# These exceptions are acceptable for malformed paths
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
def test_unsafe_path_double_encoding(self):
|
|
104
|
+
"""Test that URL-encoded paths are treated as literal filenames."""
|
|
105
|
+
# %2e%2e is URL encoding for .. but Python's Path treats it as a literal filename
|
|
106
|
+
# URL decoding should happen at the web framework layer, not in path validation
|
|
107
|
+
# This path would be treated as a literal filename in the current directory
|
|
108
|
+
result = _is_safe_local_path("..%2f..%2fetc%2fpasswd")
|
|
109
|
+
# This is actually safe because it's a literal filename, not a decoded path
|
|
110
|
+
# The important thing is that actual path traversal with .. is blocked
|
|
111
|
+
assert isinstance(result, bool)
|
|
112
|
+
|
|
113
|
+
def test_safe_path_subdirectory(self):
|
|
114
|
+
"""Test that paths to subdirectories are allowed when valid."""
|
|
115
|
+
# Create temp directory structure
|
|
116
|
+
with tempfile.TemporaryDirectory(dir=".") as tmpdir:
|
|
117
|
+
subdir = Path(tmpdir) / "subdir"
|
|
118
|
+
subdir.mkdir()
|
|
119
|
+
test_file = subdir / "test.txt"
|
|
120
|
+
test_file.write_text("test")
|
|
121
|
+
|
|
122
|
+
# Relative path to file in subdirectory should be safe
|
|
123
|
+
relative_path = str(test_file)
|
|
124
|
+
# Note: this should be safe because it's within the base directory
|
|
125
|
+
result = _is_safe_local_path(relative_path)
|
|
126
|
+
# The result depends on how the path resolves
|
|
127
|
+
assert isinstance(result, bool)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestMarkdownCleaningDoesNotPreventXSS:
|
|
131
|
+
"""Test that _clean_markdown_text alone is not sufficient for XSS prevention."""
|
|
132
|
+
|
|
133
|
+
def test_clean_markdown_allows_html(self):
|
|
134
|
+
"""Test that _clean_markdown_text does NOT escape HTML - it only cleans markdown."""
|
|
135
|
+
# This test documents the expected behavior that markdown cleaning
|
|
136
|
+
# is separate from HTML escaping
|
|
137
|
+
text_with_html = "<script>alert('xss')</script>"
|
|
138
|
+
cleaned = _clean_markdown_text(text_with_html)
|
|
139
|
+
# The markdown cleaner should NOT escape HTML - that's the job of _escape_html
|
|
140
|
+
# It might remove or preserve the text, but the key is we need _escape_html for security
|
|
141
|
+
assert isinstance(cleaned, str)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestIntegrationXSSPrevention:
|
|
145
|
+
"""Integration tests for XSS prevention in generated HTML."""
|
|
146
|
+
|
|
147
|
+
def test_html_generation_escapes_malicious_title(self):
|
|
148
|
+
"""Test that malicious content in titles is escaped in generated HTML."""
|
|
149
|
+
# This would require importing the markdown_to_pptx function
|
|
150
|
+
# and checking the generated HTML output
|
|
151
|
+
# For now, we test the building blocks are in place
|
|
152
|
+
malicious_title = "<script>alert('xss')</script>"
|
|
153
|
+
safe_title = _escape_html(_clean_markdown_text(malicious_title))
|
|
154
|
+
|
|
155
|
+
# The result should not contain executable script tags
|
|
156
|
+
assert "<script>" not in safe_title
|
|
157
|
+
assert "alert" in safe_title or "xss" in safe_title # Content preserved but escaped
|
|
158
|
+
|
|
159
|
+
def test_html_generation_escapes_malicious_content(self):
|
|
160
|
+
"""Test that malicious bullet points are escaped in generated HTML."""
|
|
161
|
+
malicious_content = "- <img src=x onerror=alert(1)>"
|
|
162
|
+
# Clean markdown first (removes bullet point marker)
|
|
163
|
+
cleaned = _clean_markdown_text(malicious_content.lstrip("- "))
|
|
164
|
+
# Then escape for HTML
|
|
165
|
+
safe_content = _escape_html(cleaned)
|
|
166
|
+
|
|
167
|
+
# Should not contain executable HTML - tags should be escaped
|
|
168
|
+
assert "<img" not in safe_content
|
|
169
|
+
assert "<img" in safe_content
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Progress Demo MCP Server using FastMCP.
|
|
2
|
+
|
|
3
|
+
This server exposes a single long-running tool that reports progress updates
|
|
4
|
+
to the client every n seconds until completion. Useful for validating end-to-end
|
|
5
|
+
progress handling in the app.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
from fastmcp import Context, FastMCP
|
|
13
|
+
|
|
14
|
+
# Initialize the MCP server
|
|
15
|
+
mcp = FastMCP("ProgressDemo")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@mcp.tool
|
|
19
|
+
async def long_task(
|
|
20
|
+
task: str = "demo",
|
|
21
|
+
duration_seconds: int = 12,
|
|
22
|
+
interval_seconds: int = 3,
|
|
23
|
+
ctx: Context | None = None,
|
|
24
|
+
) -> dict:
|
|
25
|
+
"""Execute long-running operations with real-time progress tracking and user feedback capabilities.
|
|
26
|
+
|
|
27
|
+
This advanced progress monitoring tool demonstrates professional long-running task management:
|
|
28
|
+
|
|
29
|
+
**Progress Tracking Features:**
|
|
30
|
+
- Real-time progress updates with percentage completion
|
|
31
|
+
- Configurable update intervals for optimal user experience
|
|
32
|
+
- Task labeling and identification for multi-task environments
|
|
33
|
+
- Asynchronous execution with non-blocking progress reporting
|
|
34
|
+
|
|
35
|
+
**User Experience:**
|
|
36
|
+
- Live progress bars and status indicators
|
|
37
|
+
- Descriptive progress messages with task context
|
|
38
|
+
- Predictable completion time estimation
|
|
39
|
+
- Graceful handling of interruptions and errors
|
|
40
|
+
|
|
41
|
+
**Technical Capabilities:**
|
|
42
|
+
- Async/await pattern for efficient resource utilization
|
|
43
|
+
- Context injection for framework integration
|
|
44
|
+
- Configurable timing parameters for different use cases
|
|
45
|
+
- Robust error handling and cleanup procedures
|
|
46
|
+
|
|
47
|
+
**Use Cases:**
|
|
48
|
+
- Large file processing and data migration
|
|
49
|
+
- Complex calculations and analysis workflows
|
|
50
|
+
- System maintenance and backup operations
|
|
51
|
+
- Report generation and batch processing
|
|
52
|
+
- Machine learning model training
|
|
53
|
+
- Database operations and synchronization
|
|
54
|
+
|
|
55
|
+
**Progress Reporting:**
|
|
56
|
+
- Percentage-based completion tracking
|
|
57
|
+
- Time-based milestone reporting
|
|
58
|
+
- Custom message formatting for task context
|
|
59
|
+
- Integration with UI progress indicators
|
|
60
|
+
|
|
61
|
+
**Customization Options:**
|
|
62
|
+
- Adjustable task duration for testing scenarios
|
|
63
|
+
- Variable update frequency for different performance needs
|
|
64
|
+
- Custom task labeling for organizational clarity
|
|
65
|
+
- Flexible timing configuration
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
task: Descriptive label for the operation being performed (default: "demo")
|
|
69
|
+
duration_seconds: Total time for task completion in seconds (default: 12)
|
|
70
|
+
interval_seconds: Frequency of progress updates in seconds (default: 3)
|
|
71
|
+
ctx: MCP context for progress reporting (automatically injected by framework)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dictionary containing:
|
|
75
|
+
- results: Task completion summary and final status
|
|
76
|
+
- task_info: Task parameters and execution details
|
|
77
|
+
- timing: Actual execution time and performance metrics
|
|
78
|
+
Or error message if task execution fails
|
|
79
|
+
"""
|
|
80
|
+
total = max(1, int(duration_seconds))
|
|
81
|
+
step = max(1, int(interval_seconds))
|
|
82
|
+
|
|
83
|
+
# Initial progress (0%)
|
|
84
|
+
if ctx is not None:
|
|
85
|
+
await ctx.report_progress(progress=0, total=total, message=f"{task}: starting")
|
|
86
|
+
|
|
87
|
+
elapsed = 0
|
|
88
|
+
while elapsed < total:
|
|
89
|
+
await asyncio.sleep(step)
|
|
90
|
+
elapsed = min(total, elapsed + step)
|
|
91
|
+
if ctx is not None:
|
|
92
|
+
await ctx.report_progress(
|
|
93
|
+
progress=elapsed,
|
|
94
|
+
total=total,
|
|
95
|
+
message=f"{task}: {elapsed}/{total}s",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Final completion (100%)
|
|
99
|
+
if ctx is not None:
|
|
100
|
+
await ctx.report_progress(progress=total, total=total, message=f"{task}: done")
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"results": {
|
|
104
|
+
"task": task,
|
|
105
|
+
"status": "completed",
|
|
106
|
+
"duration_seconds": total,
|
|
107
|
+
"interval_seconds": step,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@mcp.tool
|
|
113
|
+
async def status_updates(
|
|
114
|
+
stages: list[str] | None = None,
|
|
115
|
+
interval_seconds: int = 2,
|
|
116
|
+
ctx: Context | None = None,
|
|
117
|
+
) -> dict:
|
|
118
|
+
"""Emit text status updates at a fixed interval (indeterminate progress).
|
|
119
|
+
|
|
120
|
+
This demo focuses on sending human-readable status messages to the UI
|
|
121
|
+
without a known total. The UI will show an indeterminate bar and the
|
|
122
|
+
latest status message.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
stages: Optional list of stage messages to emit sequentially.
|
|
126
|
+
interval_seconds: Delay in seconds between updates.
|
|
127
|
+
ctx: FastMCP context used to report progress messages.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
dict with a simple results payload including the stages traversed.
|
|
131
|
+
"""
|
|
132
|
+
steps = stages or [
|
|
133
|
+
"Starting",
|
|
134
|
+
"Validating inputs",
|
|
135
|
+
"Preparing resources",
|
|
136
|
+
"Processing data",
|
|
137
|
+
"Uploading artifacts",
|
|
138
|
+
"Finalizing",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
# Initial status (no total, indeterminate)
|
|
142
|
+
if ctx is not None:
|
|
143
|
+
await ctx.report_progress(progress=0, message=f"{steps[0]}...")
|
|
144
|
+
|
|
145
|
+
for i, stage in enumerate(steps):
|
|
146
|
+
if i > 0:
|
|
147
|
+
await asyncio.sleep(max(1, int(interval_seconds)))
|
|
148
|
+
if ctx is not None:
|
|
149
|
+
# Report only progress counter and message; omit total for indeterminate
|
|
150
|
+
await ctx.report_progress(progress=i, message=f"{stage}...")
|
|
151
|
+
|
|
152
|
+
if ctx is not None:
|
|
153
|
+
await ctx.report_progress(progress=len(steps), message="Done.")
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"results": {
|
|
157
|
+
"status": "completed",
|
|
158
|
+
"stages": steps,
|
|
159
|
+
"updates": len(steps) + 1,
|
|
160
|
+
"interval_seconds": max(1, int(interval_seconds)),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
mcp.run()
|
|
167
|
+
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# MCP Progress Updates - Quick Start Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to use the enhanced MCP progress reporting capabilities to send viewable updates to the frontend during tool execution.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
MCP servers can now send three types of intermediate updates:
|
|
8
|
+
|
|
9
|
+
1. **Canvas Updates**: Display HTML visualizations in real-time
|
|
10
|
+
2. **System Messages**: Add rich status messages to chat history
|
|
11
|
+
3. **Progressive Artifacts**: Send files as they're generated
|
|
12
|
+
|
|
13
|
+
## Basic Setup
|
|
14
|
+
|
|
15
|
+
### 1. Enable the Demo Server
|
|
16
|
+
|
|
17
|
+
Add to `config/overrides/mcp.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"servers": {
|
|
22
|
+
"progress_updates_demo": {
|
|
23
|
+
"command": ["python", "mcp/progress_updates_demo/main.py"],
|
|
24
|
+
"cwd": "atlas",
|
|
25
|
+
"groups": ["users"],
|
|
26
|
+
"description": "Demo server showing enhanced progress updates"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Restart Backend
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Stop the backend if running
|
|
36
|
+
# Then start it again
|
|
37
|
+
cd /path/to/atlas-ui-3
|
|
38
|
+
cd atlas
|
|
39
|
+
python main.py
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Try It Out
|
|
43
|
+
|
|
44
|
+
Open the Atlas UI and try these prompts:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Show me a task with canvas updates
|
|
48
|
+
Run task_with_system_messages
|
|
49
|
+
Generate artifacts progressively
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Creating Your Own Progress Updates
|
|
53
|
+
|
|
54
|
+
### Example 1: Canvas Updates
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from fastmcp import FastMCP, Context
|
|
58
|
+
import asyncio
|
|
59
|
+
import json
|
|
60
|
+
|
|
61
|
+
mcp = FastMCP("MyServer")
|
|
62
|
+
|
|
63
|
+
@mcp.tool
|
|
64
|
+
async def visualize_progress(
|
|
65
|
+
steps: int = 5,
|
|
66
|
+
ctx: Context | None = None
|
|
67
|
+
) -> dict:
|
|
68
|
+
"""Shows visual progress in canvas."""
|
|
69
|
+
|
|
70
|
+
for step in range(1, steps + 1):
|
|
71
|
+
# Create HTML visualization
|
|
72
|
+
html = f"""
|
|
73
|
+
<html>
|
|
74
|
+
<body style="padding: 20px; font-family: Arial;">
|
|
75
|
+
<h1>Processing Step {step}/{steps}</h1>
|
|
76
|
+
<div style="width: 100%; background: #eee; height: 30px;">
|
|
77
|
+
<div style="width: {(step/steps)*100}%;
|
|
78
|
+
background: #4CAF50; height: 100%;">
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
# Send canvas update
|
|
86
|
+
if ctx:
|
|
87
|
+
update_payload = {
|
|
88
|
+
"type": "canvas_update",
|
|
89
|
+
"content": html,
|
|
90
|
+
"progress_message": f"Step {step}/{steps}"
|
|
91
|
+
}
|
|
92
|
+
await ctx.report_progress(
|
|
93
|
+
progress=step,
|
|
94
|
+
total=steps,
|
|
95
|
+
message=f"MCP_UPDATE:{json.dumps(update_payload)}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
await asyncio.sleep(1)
|
|
99
|
+
|
|
100
|
+
return {"results": {"status": "completed"}}
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
mcp.run()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Example 2: System Messages
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
@mcp.tool
|
|
110
|
+
async def process_with_updates(
|
|
111
|
+
stages: list[str] = ["Init", "Process", "Finalize"],
|
|
112
|
+
ctx: Context | None = None
|
|
113
|
+
) -> dict:
|
|
114
|
+
"""Shows status updates in chat."""
|
|
115
|
+
|
|
116
|
+
for i, stage in enumerate(stages, 1):
|
|
117
|
+
# Do work...
|
|
118
|
+
await asyncio.sleep(1)
|
|
119
|
+
|
|
120
|
+
# Send system message
|
|
121
|
+
if ctx:
|
|
122
|
+
update_payload = {
|
|
123
|
+
"type": "system_message",
|
|
124
|
+
"message": f"**{stage}** - Completed successfully ✓",
|
|
125
|
+
"subtype": "success",
|
|
126
|
+
"progress_message": f"Completed {stage}"
|
|
127
|
+
}
|
|
128
|
+
await ctx.report_progress(
|
|
129
|
+
progress=i,
|
|
130
|
+
total=len(stages),
|
|
131
|
+
message=f"MCP_UPDATE:{json.dumps(update_payload)}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return {"results": {"stages_completed": len(stages)}}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Example 3: Progressive Artifacts
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import base64
|
|
141
|
+
|
|
142
|
+
@mcp.tool
|
|
143
|
+
async def generate_reports(
|
|
144
|
+
count: int = 3,
|
|
145
|
+
ctx: Context | None = None
|
|
146
|
+
) -> dict:
|
|
147
|
+
"""Generates and displays files progressively."""
|
|
148
|
+
|
|
149
|
+
for i in range(1, count + 1):
|
|
150
|
+
# Generate content
|
|
151
|
+
html_content = f"""
|
|
152
|
+
<html>
|
|
153
|
+
<body style="padding: 20px;">
|
|
154
|
+
<h1>Report {i}</h1>
|
|
155
|
+
<p>Generated at step {i} of {count}</p>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
# Send artifact
|
|
161
|
+
if ctx:
|
|
162
|
+
artifact_data = {
|
|
163
|
+
"type": "artifacts",
|
|
164
|
+
"artifacts": [
|
|
165
|
+
{
|
|
166
|
+
"name": f"report_{i}.html",
|
|
167
|
+
"b64": base64.b64encode(html_content.encode()).decode(),
|
|
168
|
+
"mime": "text/html",
|
|
169
|
+
"size": len(html_content),
|
|
170
|
+
"description": f"Report {i}",
|
|
171
|
+
"viewer": "html"
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
"display": {
|
|
175
|
+
"open_canvas": True,
|
|
176
|
+
"primary_file": f"report_{i}.html"
|
|
177
|
+
},
|
|
178
|
+
"progress_message": f"Generated report {i}"
|
|
179
|
+
}
|
|
180
|
+
await ctx.report_progress(
|
|
181
|
+
progress=i,
|
|
182
|
+
total=count,
|
|
183
|
+
message=f"MCP_UPDATE:{json.dumps(artifact_data)}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
await asyncio.sleep(1)
|
|
187
|
+
|
|
188
|
+
return {"results": {"reports_generated": count}}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Update Types Reference
|
|
192
|
+
|
|
193
|
+
### Canvas Update
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
{
|
|
197
|
+
"type": "canvas_update",
|
|
198
|
+
"content": "<html>...</html>", # HTML string to display
|
|
199
|
+
"progress_message": "Optional progress text"
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### System Message
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
{
|
|
207
|
+
"type": "system_message",
|
|
208
|
+
"message": "Status message text",
|
|
209
|
+
"subtype": "info", # or "success", "warning", "error"
|
|
210
|
+
"progress_message": "Optional progress text"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Artifacts
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
{
|
|
218
|
+
"type": "artifacts",
|
|
219
|
+
"artifacts": [
|
|
220
|
+
{
|
|
221
|
+
"name": "filename.ext",
|
|
222
|
+
"b64": "base64_encoded_content",
|
|
223
|
+
"mime": "mime/type",
|
|
224
|
+
"size": 12345,
|
|
225
|
+
"description": "File description",
|
|
226
|
+
"viewer": "html" # or "image", "pdf", etc.
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
"display": {
|
|
230
|
+
"open_canvas": True,
|
|
231
|
+
"primary_file": "filename.ext",
|
|
232
|
+
"mode": "replace"
|
|
233
|
+
},
|
|
234
|
+
"progress_message": "Optional progress text"
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Tips
|
|
239
|
+
|
|
240
|
+
- **Always include progress_message**: This shows in the progress bar
|
|
241
|
+
- **Test with short intervals**: Start with 1-2 second delays for testing
|
|
242
|
+
- **HTML is powerful**: Use any HTML/CSS for canvas visualizations
|
|
243
|
+
- **Artifacts are stored**: Files sent as artifacts are saved to S3
|
|
244
|
+
- **Updates are async**: UI updates without blocking your tool
|
|
245
|
+
|
|
246
|
+
## Troubleshooting
|
|
247
|
+
|
|
248
|
+
### Updates not showing?
|
|
249
|
+
|
|
250
|
+
1. Check the backend logs for errors
|
|
251
|
+
2. Verify JSON is valid: `json.dumps(payload)`
|
|
252
|
+
3. Ensure `ctx` parameter is not None
|
|
253
|
+
4. Check message format: must start with `"MCP_UPDATE:"`
|
|
254
|
+
|
|
255
|
+
### Canvas not updating?
|
|
256
|
+
|
|
257
|
+
- Verify content is valid HTML
|
|
258
|
+
- Check browser console for errors
|
|
259
|
+
- Try a simple HTML first: `"<h1>Test</h1>"`
|
|
260
|
+
|
|
261
|
+
### Artifacts not displaying?
|
|
262
|
+
|
|
263
|
+
- Ensure base64 encoding is correct
|
|
264
|
+
- Check MIME type matches content
|
|
265
|
+
- Verify viewer hint is supported: html, image, pdf, etc.
|
|
266
|
+
|
|
267
|
+
## More Examples
|
|
268
|
+
|
|
269
|
+
See `/backend/mcp/progress_updates_demo/main.py` for complete working examples.
|
|
270
|
+
|
|
271
|
+
## Documentation
|
|
272
|
+
|
|
273
|
+
Full documentation: [Developer Guide - Progress Updates](../../../docs/developer/progress-updates.md)
|