diff --git a/.env.example b/.env.example index 4861345..9cde796 100644 --- a/.env.example +++ b/.env.example @@ -85,7 +85,7 @@ FEATURE_SPLASH_SCREEN_ENABLED=false # Startup splash screen for displaying poli # Useful for testing or managing multiple configurations. ############################################# # SPLASH_CONFIG_FILE=splash-config.json # Splash screen configuration file name -# MCP_CONFIG_FILE=mcp.json # MCP servers configuration file name +# MCP_CONFIG_FILE=mcp.json # MCP servers configuration file name # LLM_CONFIG_FILE=llmconfig.yml # LLM models configuration file name # HELP_CONFIG_FILE=help-config.json # Help page configuration file name @@ -133,4 +133,9 @@ USE_MOCK_S3=true # S3_USE_SSL=false -SECURITY_CSP_VALUE="default-src 'self'; img-src 'self' data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-src 'self' blob: data:; frame-ancestors 'self'" +# Content Security Policy (CSP) configuration +# IMPORTANT: To allow external URLs in iframes (for MCP tools that use iframe display), +# add the URLs to the frame-src directive. Example: +# SECURITY_CSP_VALUE="... frame-src 'self' blob: data: https://example.com https://dashboard.example.com; ..." +# HERE the www.sandia.gov is added as an allowed iframe source. +SECURITY_CSP_VALUE="default-src 'self'; img-src 'self' data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-src 'self' blob: data: https://www.sandia.gov; frame-ancestors 'self'" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2869fc8..eb145a6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -36,8 +36,9 @@ frontend/ React 19 + Vite + Tailwind; state via contexts (Chat/WS/Marketplace) ## MCP + RAG conventions - MCP servers live in mcp.json (tools/prompts) and mcp-rag.json (RAG-only inventory). Fields: groups, transport|type, url|command/cwd, compliance_level. - Transport detection order: explicit transport → command (stdio) → URL protocol (http/sse) → type fallback. -- Tool names exposed to LLM are fully-qualified: server_toolName. “canvas_canvas” is a pseudo-tool always available. +- Tool names exposed to LLM are fully-qualified: server_toolName. "canvas_canvas" is a pseudo-tool always available. - RAG over MCP tools expected: rag_discover_resources, rag_get_raw_results, optional rag_get_synthesized_results. RAG resources and servers may include complianceLevel. +- When testing or developing MCP-related features, example configurations can be found in config/mcp-example-configs/ with individual mcp-{servername}.json files for testing individual servers. ## Compliance levels (explicit allowlist) - Definitions in config/(overrides|defaults)/compliance-levels.json. core/compliance.py loads, normalizes aliases, and enforces allowed_with. @@ -72,4 +73,4 @@ Common pitfalls: “uv not found” → install uv; frontend not loading → npm # Style -No emojis please \ No newline at end of file +No emojis please diff --git a/CLAUDE.md b/CLAUDE.md index 3608881..33d27e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -256,6 +256,8 @@ MCP servers defined in `config/defaults/mcp.json`. The backend: 3. Exposes tools to LLM via `ToolManagerProtocol` 4. Supports group-based access control +When testing or developing MCP-related features, example configurations can be found in config/mcp-example-configs/ with individual mcp-{servername}.json files for testing individual servers. + ### Agent Modes Three agent loop strategies implement different reasoning patterns: - **ReAct** (`backend/application/chat/agent/react_loop.py`): Reason-Act-Observe cycle, good for tool-heavy tasks with structured reasoning diff --git a/GEMINI.md b/GEMINI.md index 45380c4..9934910 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -58,8 +58,10 @@ python main.py # NEVER use uvicorn --reload (causes problems) - **No Emojis**: No emojis should ever be added in this repo. - **Linting**: Run `ruff check backend/` for Python and `npm run lint` for the frontend before committing. +When testing or developing MCP-related features, example configurations can be found in config/mcp-example-configs/ with individual mcp-{servername}.json files for testing individual servers. -Also read. + +Also read. /workspaces/atlas-ui-3/.github/copilot-instructions.md -and CLAUDE.md \ No newline at end of file +and CLAUDE.md diff --git a/backend/application/chat/utilities/file_utils.py b/backend/application/chat/utilities/file_utils.py index 001455e..ca89e17 100644 --- a/backend/application/chat/utilities/file_utils.py +++ b/backend/application/chat/utilities/file_utils.py @@ -84,29 +84,40 @@ async def process_tool_artifacts( ) -> Dict[str, Any]: """ Process v2 MCP artifacts produced by a tool and return updated session context. - + Pure function that handles tool files without side effects on input context. """ - if not tool_result.artifacts or not file_manager: - return session_context + # Check if there's an iframe display configuration (no artifacts needed) + has_iframe_display = ( + tool_result.display_config and + isinstance(tool_result.display_config, dict) and + tool_result.display_config.get("type") == "iframe" and + tool_result.display_config.get("url") + ) - user_email = session_context.get("user_email") - if not user_email: + # Early return only if no artifacts AND no iframe display, or no file_manager + if (not tool_result.artifacts and not has_iframe_display) or not file_manager: return session_context # Work with a copy to avoid mutations updated_context = dict(session_context) - - # Process v2 artifacts - updated_context = await ingest_v2_artifacts( - session_context=updated_context, - tool_result=tool_result, - user_email=user_email, - file_manager=file_manager, - update_callback=update_callback - ) + + # Process v2 artifacts (only if we have artifacts) + if tool_result.artifacts: + user_email = session_context.get("user_email") + if not user_email: + return session_context + + updated_context = await ingest_v2_artifacts( + session_context=updated_context, + tool_result=tool_result, + user_email=user_email, + file_manager=file_manager, + update_callback=update_callback + ) # Handle canvas file notifications with v2 display config + # This handles both artifact-based displays and iframe-only displays await notify_canvas_files_v2( session_context=updated_context, tool_result=tool_result, @@ -368,17 +379,47 @@ async def notify_canvas_files_v2( ) -> None: """ Send v2 canvas files notification with display configuration. - + Pure function with no side effects on session context. """ - if not update_callback or not tool_result.artifacts: + if not update_callback: return - + + # Check if there's an iframe display configuration (no artifacts needed) + has_iframe_display = ( + tool_result.display_config and + isinstance(tool_result.display_config, dict) and + tool_result.display_config.get("type") == "iframe" and + tool_result.display_config.get("url") + ) + + # If no artifacts and no iframe display, nothing to show + if not tool_result.artifacts and not has_iframe_display: + return + try: # Get uploaded file references from session context uploaded_refs = session_context.get("files", {}) artifact_names = [artifact.get("name") for artifact in tool_result.artifacts if artifact.get("name")] - + + # Handle iframe-only display (no artifacts) + if has_iframe_display and not artifact_names: + canvas_update = { + "type": "intermediate_update", + "update_type": "canvas_files", + "data": { + "files": [], + "display": tool_result.display_config + } + } + logger.info( + "Emitting canvas_files event for iframe display: url=%s, title=%s", + tool_result.display_config.get("url"), + tool_result.display_config.get("title", "Embedded Content"), + ) + await update_callback(canvas_update) + return + if uploaded_refs and artifact_names: canvas_files = [] for fname in artifact_names: diff --git a/backend/mcp/ui-demo/main.py b/backend/mcp/ui-demo/main.py index 77fba12..506606b 100644 --- a/backend/mcp/ui-demo/main.py +++ b/backend/mcp/ui-demo/main.py @@ -31,64 +31,64 @@ def load_template(template_name: str) -> str: with open(template_path, "r") as f: return f.read() -@mcp.tool -def create_button_demo() -> Dict[str, Any]: - """ - Generate interactive HTML demonstrations showcasing advanced UI customization and dynamic interface capabilities. - - This UI prototyping tool creates sophisticated interactive demonstrations: - - **Interactive UI Components:** - - Custom HTML button interfaces with advanced styling - - Dynamic interaction patterns and user feedback systems - - Professional design templates with modern aesthetics - - Responsive layouts optimized for different screen sizes - - **UI Customization Features:** - - Advanced CSS styling with modern design patterns - - Interactive JavaScript functionality for user engagement - - Professional color schemes and typography - - Accessibility-compliant interface elements - - **Demonstration Capabilities:** - - Real-time UI modification examples - - Interactive component behavior showcases - - Design pattern implementation demonstrations - - User experience optimization examples - - **Technical Implementation:** - - Clean HTML5 structure with semantic elements - - Modern CSS3 styling with flexbox and grid layouts - - Vanilla JavaScript for cross-browser compatibility - - Base64 encoding for seamless artifact delivery - - **Use Cases:** - - UI design prototyping and concept validation - - Client demonstration and stakeholder presentations - - Design system documentation and examples - - Interactive tutorial and training materials - - A/B testing interface variations - - User experience research and testing - - **Professional Features:** - - Production-ready code quality and structure - - Cross-browser compatibility and standards compliance - - Performance-optimized implementation - - Maintainable and extensible code architecture - - **Integration Capabilities:** - - Canvas viewer integration for immediate preview - - Downloadable HTML for offline use and sharing - - Framework-agnostic implementation - - Easy customization and extension - - Returns: - Dictionary containing: - - results: Demo creation summary and success confirmation - - artifacts: Interactive HTML demonstration as downloadable content - - display: Optimized canvas viewer configuration for immediate preview - - Interactive elements ready for user testing and evaluation - Or error message if HTML generation or template loading fails +@mcp.tool +def create_button_demo() -> Dict[str, Any]: + """ + Generate interactive HTML demonstrations showcasing advanced UI customization and dynamic interface capabilities. + + This UI prototyping tool creates sophisticated interactive demonstrations: + + **Interactive UI Components:** + - Custom HTML button interfaces with advanced styling + - Dynamic interaction patterns and user feedback systems + - Professional design templates with modern aesthetics + - Responsive layouts optimized for different screen sizes + + **UI Customization Features:** + - Advanced CSS styling with modern design patterns + - Interactive JavaScript functionality for user engagement + - Professional color schemes and typography + - Accessibility-compliant interface elements + + **Demonstration Capabilities:** + - Real-time UI modification examples + - Interactive component behavior showcases + - Design pattern implementation demonstrations + - User experience optimization examples + + **Technical Implementation:** + - Clean HTML5 structure with semantic elements + - Modern CSS3 styling with flexbox and grid layouts + - Vanilla JavaScript for cross-browser compatibility + - Base64 encoding for seamless artifact delivery + + **Use Cases:** + - UI design prototyping and concept validation + - Client demonstration and stakeholder presentations + - Design system documentation and examples + - Interactive tutorial and training materials + - A/B testing interface variations + - User experience research and testing + + **Professional Features:** + - Production-ready code quality and structure + - Cross-browser compatibility and standards compliance + - Performance-optimized implementation + - Maintainable and extensible code architecture + + **Integration Capabilities:** + - Canvas viewer integration for immediate preview + - Downloadable HTML for offline use and sharing + - Framework-agnostic implementation + - Easy customization and extension + + Returns: + Dictionary containing: + - results: Demo creation summary and success confirmation + - artifacts: Interactive HTML demonstration as downloadable content + - display: Optimized canvas viewer configuration for immediate preview + - Interactive elements ready for user testing and evaluation + Or error message if HTML generation or template loading fails """ # Load the HTML template html_content = load_template("button_demo.html") @@ -255,5 +255,129 @@ def get_image() -> Dict[str, Any]: } } +@mcp.tool +def create_iframe_demo() -> Dict[str, Any]: + """ + Create a demo showing how to embed external content using iframes. + + This demonstrates the v2 MCP iframe capability for embedding interactive + external content like dashboards, visualizations, or web applications. + + IMPORTANT - CSP Configuration Required: + To display external URLs in iframes, the SECURITY_CSP_VALUE environment + variable must include the iframe URL in the frame-src directive. + + Example for https://www.sandia.gov/: + SECURITY_CSP_VALUE="... frame-src 'self' blob: data: https://www.sandia.gov/; ..." + + Without proper CSP configuration, the browser will block the iframe. + + Returns: + Dictionary with iframe display configuration + """ + return { + "results": { + "content": "Iframe demo created! An external webpage will be displayed in the canvas panel.", + "iframe_url": "https://www.sandia.gov/" + }, + "artifacts": [], + "display": { + "open_canvas": True, + "type": "iframe", + "url": "https://www.sandia.gov/", + "title": "Example Website", + "sandbox": "allow-scripts allow-same-origin", + "mode": "replace" + } + } + +@mcp.tool +def create_html_with_iframe() -> Dict[str, Any]: + """ + Create an HTML artifact that includes an embedded iframe. + + This demonstrates how MCP tools can return HTML content with embedded + iframes that will be properly rendered in the canvas panel. + + IMPORTANT - CSP Configuration Required: + To display external URLs in iframes, the SECURITY_CSP_VALUE environment + variable must include the iframe URL in the frame-src directive. + + Example for https://www.sandia.gov/: + SECURITY_CSP_VALUE="... frame-src 'self' blob: data: https://www.sandia.gov/; ..." + + Without proper CSP configuration, the browser will block the iframe. + + Returns: + Dictionary with HTML artifact containing an iframe + """ + html_content = """ + + + + + +

Embedded Content Demo

+

This HTML artifact includes an embedded iframe showing external content:

+
+ +
+ +""" + + import base64 + html_base64 = base64.b64encode(html_content.encode('utf-8')).decode('utf-8') + + return { + "results": { + "content": "HTML with embedded iframe created! Check the canvas panel." + }, + "artifacts": [ + { + "name": "iframe_demo.html", + "b64": html_base64, + "mime": "text/html", + "size": len(html_content.encode('utf-8')), + "description": "HTML page with embedded iframe", + "viewer": "html" + } + ], + "display": { + "open_canvas": True, + "primary_file": "iframe_demo.html", + "mode": "replace", + "viewer_hint": "html" + } + } + if __name__ == "__main__": mcp.run() diff --git a/backend/modules/mcp_tools/client.py b/backend/modules/mcp_tools/client.py index 08e1c27..cb08ca3 100644 --- a/backend/modules/mcp_tools/client.py +++ b/backend/modules/mcp_tools/client.py @@ -27,14 +27,14 @@ def __init__(self, config_path: Optional[str] = None): # Use config manager to get config path app_settings = config_manager.app_settings overrides_root = Path(app_settings.app_config_overrides) - + # If relative, resolve from project root if not overrides_root.is_absolute(): # This file is in backend/modules/mcp_tools/client.py backend_root = Path(__file__).parent.parent.parent project_root = backend_root.parent overrides_root = project_root / overrides_root - + candidate = overrides_root / "mcp.json" if not candidate.exists(): # Legacy fallback @@ -42,10 +42,23 @@ def __init__(self, config_path: Optional[str] = None): if not candidate.exists(): candidate = Path("backend/configfiles/mcp.json") self.config_path = str(candidate) + # Use default config manager when no path specified + mcp_config = config_manager.mcp_config + self.servers_config = {name: server.model_dump() for name, server in mcp_config.servers.items()} else: + # Load config from the specified path self.config_path = config_path - mcp_config = config_manager.mcp_config - self.servers_config = {name: server.model_dump() for name, server in mcp_config.servers.items()} + config_file = Path(config_path) + if config_file.exists(): + from modules.config.config_manager import MCPConfig + data = json.loads(config_file.read_text()) + # Convert flat structure to nested structure for Pydantic + servers_data = {"servers": data} + mcp_config = MCPConfig(**servers_data) + self.servers_config = {name: server.model_dump() for name, server in mcp_config.servers.items()} + else: + logger.warning(f"Custom config path specified but file not found: {config_path}") + self.servers_config = {} self.clients = {} self.available_tools = {} self.available_prompts = {} diff --git a/backend/tests/test_mcp_prompt_override_system_prompt.py b/backend/tests/test_mcp_prompt_override_system_prompt.py index 227d1af..db9cbd2 100644 --- a/backend/tests/test_mcp_prompt_override_system_prompt.py +++ b/backend/tests/test_mcp_prompt_override_system_prompt.py @@ -1,3 +1,6 @@ +import json +from pathlib import Path + import pytest from modules.mcp_tools.client import MCPToolManager @@ -13,7 +16,18 @@ async def test_selected_mcp_prompt_overrides_system_prompt(monkeypatch): """ # Ensure MCP clients and prompts are ready # Set up MCP manager directly (avoid importing app_factory/litellm). - mcp: MCPToolManager = MCPToolManager() + # Use the example prompts MCP config file so this test uses + # the same JSON configuration as other prompts tests. + # tests run with cwd=backend/, so resolve from backend root + backend_root = Path(__file__).parent.parent + project_root = backend_root.parent + config_path = project_root / "config" / "mcp-example-configs" / "mcp-prompts.json" + assert config_path.exists(), f"Missing example prompts config: {config_path}" + + data = json.loads(config_path.read_text()) + assert "prompts" in data, "prompts server not defined in example config" + + mcp: MCPToolManager = MCPToolManager(config_path=str(config_path)) await mcp.initialize_clients() await mcp.discover_prompts() assert "prompts" in mcp.available_prompts, "prompts server not discovered" diff --git a/backend/tests/test_mcp_prompts_server.py b/backend/tests/test_mcp_prompts_server.py index 48940ea..4396aba 100644 --- a/backend/tests/test_mcp_prompts_server.py +++ b/backend/tests/test_mcp_prompts_server.py @@ -1,11 +1,29 @@ +import json +from pathlib import Path + import pytest -from infrastructure.app_factory import app_factory +from modules.mcp_tools.client import MCPToolManager @pytest.mark.asyncio async def test_mcp_prompts_discovery_includes_expert_dog_trainer(): - mcp = app_factory.get_mcp_manager() + # Use the example prompts MCP config file so this test + # exercises the real JSON configuration used for prompts. + # tests run with cwd=backend/, so resolve from backend root + backend_root = Path(__file__).parent.parent + print(backend_root) + project_root = backend_root.parent + print(project_root) + config_path = project_root / "config" / "mcp-example-configs" / "mcp-prompts.json" + print(config_path) + assert config_path.exists(), f"Missing example prompts config: {config_path}" + + # Sanity-check that the JSON contains a "prompts" server. + data = json.loads(config_path.read_text()) + assert "prompts" in data, "prompts server not defined in example config" + + mcp = MCPToolManager(config_path=str(config_path)) # Ensure fresh clients and prompt discovery await mcp.initialize_clients() diff --git a/config/mcp-example-configs/mcp-basictable.json b/config/mcp-example-configs/mcp-basictable.json new file mode 100644 index 0000000..dab0520 --- /dev/null +++ b/config/mcp-example-configs/mcp-basictable.json @@ -0,0 +1 @@ +{"basictable": {"command": ["python", "mcp/basictable/main.py"], "cwd": "backend", "groups": ["users"], "description": "basictable MCP server", "author": "Chat UI Team", "short_description": "basictable server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-calculator.json b/config/mcp-example-configs/mcp-calculator.json new file mode 100644 index 0000000..0a3756e --- /dev/null +++ b/config/mcp-example-configs/mcp-calculator.json @@ -0,0 +1,17 @@ +{ + "calculator": { + "command": [ + "python", + "mcp/calculator/main.py" + ], + "cwd": "backend", + "groups": [ + "users" + ], + "description": "Evaluate mathematical expressions, perform calculations with basic arithmetic, trigonometry, and logarithms", + "author": "Chat UI Team", + "short_description": "Mathematical calculator", + "help_email": "support@chatui.example.com", + "compliance_level": "Public" + } +} diff --git a/config/mcp-example-configs/mcp-code-executor.json b/config/mcp-example-configs/mcp-code-executor.json new file mode 100644 index 0000000..92ddc99 --- /dev/null +++ b/config/mcp-example-configs/mcp-code-executor.json @@ -0,0 +1 @@ +{"code-executor": {"command": ["python", "mcp/code-executor/main.py"], "cwd": "backend", "groups": ["users"], "description": "code-executor MCP server", "author": "Chat UI Team", "short_description": "code-executor server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-corporate_cars.json b/config/mcp-example-configs/mcp-corporate_cars.json new file mode 100644 index 0000000..d257433 --- /dev/null +++ b/config/mcp-example-configs/mcp-corporate_cars.json @@ -0,0 +1 @@ +{"corporate_cars": {"command": ["python", "mcp/corporate_cars/main.py"], "cwd": "backend", "groups": ["users"], "description": "corporate_cars MCP server", "author": "Chat UI Team", "short_description": "corporate_cars server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-csv_reporter.json b/config/mcp-example-configs/mcp-csv_reporter.json new file mode 100644 index 0000000..44d6d9b --- /dev/null +++ b/config/mcp-example-configs/mcp-csv_reporter.json @@ -0,0 +1 @@ +{"csv_reporter": {"command": ["python", "mcp/csv_reporter/main.py"], "cwd": "backend", "groups": ["users"], "description": "csv_reporter MCP server", "author": "Chat UI Team", "short_description": "csv_reporter server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-duckduckgo.json b/config/mcp-example-configs/mcp-duckduckgo.json new file mode 100644 index 0000000..f752e9d --- /dev/null +++ b/config/mcp-example-configs/mcp-duckduckgo.json @@ -0,0 +1 @@ +{"duckduckgo": {"command": ["python", "mcp/duckduckgo/main.py"], "cwd": "backend", "groups": ["users"], "description": "duckduckgo MCP server", "author": "Chat UI Team", "short_description": "duckduckgo server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-env-demo.json b/config/mcp-example-configs/mcp-env-demo.json new file mode 100644 index 0000000..18af744 --- /dev/null +++ b/config/mcp-example-configs/mcp-env-demo.json @@ -0,0 +1 @@ +{"env-demo": {"command": ["python", "mcp/env-demo/main.py"], "cwd": "backend", "groups": ["users"], "description": "env-demo MCP server", "author": "Chat UI Team", "short_description": "env-demo server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-external-api-example.json b/config/mcp-example-configs/mcp-external-api-example.json new file mode 100644 index 0000000..ac719f2 --- /dev/null +++ b/config/mcp-example-configs/mcp-external-api-example.json @@ -0,0 +1,12 @@ +{ + "external-api-example": { + "enabled": true, + "url": "http://127.0.0.1:8005/mcp", + "transport": "http", + "groups": [ + "users" + ], + "description": "Mock HTTP MCP server for testing authentication", + "auth_token": "${MCP_EXTERNAL_API_TOKEN}" + } +} diff --git a/config/mcp-example-configs/mcp-file_size_test.json b/config/mcp-example-configs/mcp-file_size_test.json new file mode 100644 index 0000000..19b2657 --- /dev/null +++ b/config/mcp-example-configs/mcp-file_size_test.json @@ -0,0 +1 @@ +{"file_size_test": {"command": ["python", "mcp/file_size_test/main.py"], "cwd": "backend", "groups": ["users"], "description": "file_size_test MCP server", "author": "Chat UI Team", "short_description": "file_size_test server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-filesystem.json b/config/mcp-example-configs/mcp-filesystem.json new file mode 100644 index 0000000..62b099f --- /dev/null +++ b/config/mcp-example-configs/mcp-filesystem.json @@ -0,0 +1 @@ +{"filesystem": {"command": ["python", "mcp/filesystem/main.py"], "cwd": "backend", "groups": ["users"], "description": "filesystem MCP server", "author": "Chat UI Team", "short_description": "filesystem server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-many_tools_demo.json b/config/mcp-example-configs/mcp-many_tools_demo.json new file mode 100644 index 0000000..f362ea0 --- /dev/null +++ b/config/mcp-example-configs/mcp-many_tools_demo.json @@ -0,0 +1 @@ +{"many_tools_demo": {"command": ["python", "mcp/many_tools_demo/main.py"], "cwd": "backend", "groups": ["users"], "description": "many_tools_demo MCP server", "author": "Chat UI Team", "short_description": "many_tools_demo server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-order_database.json b/config/mcp-example-configs/mcp-order_database.json new file mode 100644 index 0000000..a9d415c --- /dev/null +++ b/config/mcp-example-configs/mcp-order_database.json @@ -0,0 +1 @@ +{"order_database": {"command": ["python", "mcp/order_database/main.py"], "cwd": "backend", "groups": ["users"], "description": "order_database MCP server", "author": "Chat UI Team", "short_description": "order_database server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-pdfbasic.json b/config/mcp-example-configs/mcp-pdfbasic.json new file mode 100644 index 0000000..63583b4 --- /dev/null +++ b/config/mcp-example-configs/mcp-pdfbasic.json @@ -0,0 +1 @@ +{"pdfbasic": {"command": ["python", "mcp/pdfbasic/main.py"], "cwd": "backend", "groups": ["users"], "description": "pdfbasic MCP server", "author": "Chat UI Team", "short_description": "pdfbasic server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-pptx_generator.json b/config/mcp-example-configs/mcp-pptx_generator.json new file mode 100644 index 0000000..bbc3776 --- /dev/null +++ b/config/mcp-example-configs/mcp-pptx_generator.json @@ -0,0 +1 @@ +{"pptx_generator": {"command": ["python", "mcp/pptx_generator/main.py"], "cwd": "backend", "groups": ["users"], "description": "pptx_generator MCP server", "author": "Chat UI Team", "short_description": "pptx_generator server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-progress_demo.json b/config/mcp-example-configs/mcp-progress_demo.json new file mode 100644 index 0000000..02e46fc --- /dev/null +++ b/config/mcp-example-configs/mcp-progress_demo.json @@ -0,0 +1 @@ +{"progress_demo": {"command": ["python", "mcp/progress_demo/main.py"], "cwd": "backend", "groups": ["users"], "description": "progress_demo MCP server", "author": "Chat UI Team", "short_description": "progress_demo server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-progress_updates_demo.json b/config/mcp-example-configs/mcp-progress_updates_demo.json new file mode 100644 index 0000000..d24cc48 --- /dev/null +++ b/config/mcp-example-configs/mcp-progress_updates_demo.json @@ -0,0 +1 @@ +{"progress_updates_demo": {"command": ["python", "mcp/progress_updates_demo/main.py"], "cwd": "backend", "groups": ["users"], "description": "progress_updates_demo MCP server", "author": "Chat UI Team", "short_description": "progress_updates_demo server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-prompts.json b/config/mcp-example-configs/mcp-prompts.json new file mode 100644 index 0000000..5163ca1 --- /dev/null +++ b/config/mcp-example-configs/mcp-prompts.json @@ -0,0 +1 @@ +{"prompts": {"command": ["python", "mcp/prompts/main.py"], "cwd": "backend", "groups": ["users"], "description": "prompts MCP server", "author": "Chat UI Team", "short_description": "prompts server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-thinking.json b/config/mcp-example-configs/mcp-thinking.json new file mode 100644 index 0000000..a37c3e8 --- /dev/null +++ b/config/mcp-example-configs/mcp-thinking.json @@ -0,0 +1 @@ +{"thinking": {"command": ["python", "mcp/thinking/main.py"], "cwd": "backend", "groups": ["users"], "description": "thinking MCP server", "author": "Chat UI Team", "short_description": "thinking server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-ui-demo.json b/config/mcp-example-configs/mcp-ui-demo.json new file mode 100644 index 0000000..6267ce6 --- /dev/null +++ b/config/mcp-example-configs/mcp-ui-demo.json @@ -0,0 +1 @@ +{"ui-demo": {"command": ["python", "mcp/ui-demo/main.py"], "cwd": "backend", "groups": ["users"], "description": "ui-demo MCP server", "author": "Chat UI Team", "short_description": "ui-demo server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/mcp-example-configs/mcp-vtk-example.json b/config/mcp-example-configs/mcp-vtk-example.json new file mode 100644 index 0000000..123bf4d --- /dev/null +++ b/config/mcp-example-configs/mcp-vtk-example.json @@ -0,0 +1 @@ +{"vtk-example": {"command": ["python", "mcp/vtk-example/main.py"], "cwd": "backend", "groups": ["users"], "description": "vtk-example MCP server", "author": "Chat UI Team", "short_description": "vtk-example server", "help_email": "support@chatui.example.com", "compliance_level": "Public"}} diff --git a/config/overrides/env-var-mcp.json b/config/overrides/env-var-mcp.json deleted file mode 100644 index c822644..0000000 --- a/config/overrides/env-var-mcp.json +++ /dev/null @@ -1,23 +0,0 @@ - {"env-demo": { - "command": [ - "python", - "mcp/env-demo/main.py" - ], - "cwd": "backend", - "env": { - "CLOUD_PROFILE": "demo-profile", - "CLOUD_REGION": "us-west-2", - "DEBUG_MODE": "true", - "ENVIRONMENT": "development", - "API_KEY": "${DEMO_API_KEY}" - }, - "groups": [ - "users" - ], - "description": "Demonstrates environment variable passing to MCP servers. Shows how to configure servers with env vars in mcp.json using both literal values and ${VAR} substitution.", - "author": "Chat UI Team", - "short_description": "Environment variable demonstration", - "help_email": "support@chatui.example.com", - "compliance_level": "Public" - } -} \ No newline at end of file diff --git a/config/overrides/mcp copy.json b/config/overrides/mcp copy.json deleted file mode 100644 index 0fbd222..0000000 --- a/config/overrides/mcp copy.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "thinking": { - "command": [ - "python", - "mcp/thinking/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "Structured thinking and problem analysis tool", - "author": "Chat UI Team", - "short_description": "Structured problem analysis", - "help_email": "support@chatui.example.com" - }, - "pdfbasic": { - "command": [ - "python", - "mcp/pdfbasic/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "PDF analysis tool", - "author": "Chat UI Team", - "short_description": "Analyze PDF documents", - "help_email": "support@chatui.example.com" - }, - "ui-demo": { - "command": [ - "python", - "mcp/ui-demo/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "Demo server showcasing custom UI modification capabilities", - "author": "Chat UI Team", - "short_description": "UI customization demo", - "help_email": "support@chatui.example.com" - }, - "code-executor": { - "command": [ - "python", - "mcp/code-executor/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "Secure code execution environment", - "author": "Chat UI Team", - "short_description": "Execute code securely", - "help_email": "support@chatui.example.com" - }, - "prompts": { - "command": [ - "python", - "mcp/prompts/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "Specialized system prompts for AI behavior modification", - "author": "Chat UI Team", - "short_description": "AI behavior prompts", - "help_email": "support@chatui.example.com" - } -} diff --git a/config/overrides/mcp-test.json b/config/overrides/mcp-test.json deleted file mode 100644 index 1593f04..0000000 --- a/config/overrides/mcp-test.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "thinking": { - "command": [ - "python", - "mcp/thinking/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "Structured thinking and problem analysis tool", - "author": "Chat UI Team", - "short_description": "Structured problem analysis", - "help_email": "support@chatui.example.com" - }, - "prompts": { - "command": [ - "python", - "mcp/prompts/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "Specialized system prompts for AI behavior modification", - "author": "Chat UI Team", - "short_description": "AI behavior prompts", - "help_email": "support@chatui.example.com" - } -} diff --git a/config/overrides/mcp.json b/config/overrides/mcp.json index 845daca..14abf70 100644 --- a/config/overrides/mcp.json +++ b/config/overrides/mcp.json @@ -17,6 +17,21 @@ "allow_edit": [ "evaluate" ] + }, + "ui-demo": { + "command": [ + "python", + "mcp/ui-demo/main.py" + ], + "cwd": "backend", + "groups": [ + "users" + ], + "description": "Demo server showcasing custom UI modification capabilities", + "author": "Chat UI Team", + "short_description": "UI customization demo", + "help_email": "support@chatui.example.com", + "compliance_level": "Public" }, "pptx_generator": { "command": [ @@ -118,16 +133,5 @@ "short_description": "Environment variable demonstration", "help_email": "support@chatui.example.com", "compliance_level": "Public" - }, - "external-api-example": { - "enabled": true, - "url": "http://127.0.0.1:8005/mcp", - "transport": "http", - "groups": [ - "users" - ], - "description": "Mock HTTP MCP server for testing authentication", - "auth_token": "${MCP_EXTERNAL_API_TOKEN}" } - } diff --git a/config/overrides/progress-report-mcp copy.json b/config/overrides/progress-report-mcp copy.json deleted file mode 100644 index 7e035f4..0000000 --- a/config/overrides/progress-report-mcp copy.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "progress_updates_demo": { - "transport": "stdio", - "command": [ - "python", - "mcp/progress_updates_demo/main.py" - ], - "cwd": "backend", - "groups": [ - "users" - ], - "description": "Demo MCP server showing progress updates with canvas, system messages, and artifacts", - "author": "Atlas UI Team", - "short_description": "Progress updates demo", - "help_email": "support@chatui.example.com", - "compliance_level": "Public" - } -} diff --git a/docs/admin/configuration.md b/docs/admin/configuration.md index 9103e70..4e4a25e 100644 --- a/docs/admin/configuration.md +++ b/docs/admin/configuration.md @@ -34,3 +34,60 @@ Key settings in the `.env` file include: * **Feature Flags**: Enable or disable major features like `FEATURE_AGENT_MODE_AVAILABLE`. * **S3 Connection**: Configure the connection to your S3-compatible storage. For local testing, you can set `USE_MOCK_S3=true` to use an in-memory mock instead of a real S3 bucket. **This mock must never be used in production.** * **Log Directory**: The `APP_LOG_DIR` variable points to the folder where the application log file (`app.jsonl`) will be stored. This path must be updated to a valid directory in your deployment environment. +* **Security Headers**: Configure Content Security Policy (CSP) and other security headers. See the Security Configuration section below for details. + +## Security Configuration (CSP and Headers) + +The application includes security headers middleware that sets browser security policies. These are configured via environment variables in `.env`. + +### Content Security Policy (CSP) + +The `SECURITY_CSP_VALUE` environment variable controls the Content Security Policy header, which restricts what resources the browser can load. This is critical for preventing XSS attacks. + +**Default Configuration:** +```bash +SECURITY_CSP_VALUE="default-src 'self'; img-src 'self' data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-src 'self' blob: data:; frame-ancestors 'self'" +``` + +**Key Directives:** +- `default-src 'self'` - Only allow resources from the same origin by default +- `img-src 'self' data: blob:` - Allow images from same origin, data URIs, and blob URLs +- `script-src 'self'` - Only allow JavaScript from same origin +- `style-src 'self' 'unsafe-inline'` - Allow CSS from same origin and inline styles +- `frame-src 'self' blob: data:` - Allow iframes from same origin, blob, and data URIs +- `frame-ancestors 'self'` - Prevent the app from being embedded in external iframes + +### Allowing External Iframes + +**IMPORTANT:** If your MCP tools need to display external content using iframes (dashboards, visualizations, web applications), you MUST add those domains to the `frame-src` directive. + +**Example - Allow specific external domains:** +```bash +SECURITY_CSP_VALUE="default-src 'self'; img-src 'self' data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-src 'self' blob: data: https://dashboard.example.com https://analytics.corp.com https://www.sandia.gov/; frame-ancestors 'self'" +``` + +**Security Considerations:** +- Only add domains you trust and control +- Be specific with full URLs (include `https://` and trailing path if needed) +- Wildcard subdomains (`https://*.example.com`) are supported but less secure +- Document which MCP servers require which domains in your `mcp.json` descriptions + +**Troubleshooting:** If iframes appear blank or don't load, check your browser's console for CSP violation errors. The error message will tell you which domain needs to be added to `frame-src`. + +### Other Security Headers + +Additional security headers can be configured in `.env`: + +```bash +# Enable/disable specific headers (default: true) +SECURITY_CSP_ENABLED=true +SECURITY_XFO_ENABLED=true +SECURITY_NOSNIFF_ENABLED=true +SECURITY_REFERRER_POLICY_ENABLED=true + +# Header values +SECURITY_XFO_VALUE=SAMEORIGIN +SECURITY_REFERRER_POLICY_VALUE=no-referrer +``` + +For more details on security headers implementation, see `backend/core/security_headers_middleware.py`. diff --git a/docs/archive/03_developer_guide.md b/docs/archive/03_developer_guide.md index 2dabd22..f80f1d8 100644 --- a/docs/archive/03_developer_guide.md +++ b/docs/archive/03_developer_guide.md @@ -82,6 +82,7 @@ The returned JSON object has the following structure: * `name`: The filename (e.g., `report.html`). * `b64`: The base64-encoded content of the file. * `mime`: The MIME type (e.g., `text/html`, `image/png`). + * `viewer`: The preferred viewer type (e.g., `html`, `image`, `pdf`, `iframe`). * **`display`** (optional): A JSON object that provides hints to the UI on how to display the artifacts, such as whether to open the canvas automatically. **Example tool returning an artifact:** @@ -99,7 +100,8 @@ def create_html_report(title: str) -> Dict[str, Any]: "artifacts": [{ "name": "report.html", "b64": b64_content, - "mime": "text/html" + "mime": "text/html", + "viewer": "html" }], "display": { "open_canvas": True, @@ -108,6 +110,77 @@ def create_html_report(title: str) -> Dict[str, Any]: } ``` +#### Iframe Support for External Content + +MCP tools can embed external content using iframes in two ways: + +**Approach 1: Direct Iframe via Display Config** + +Use this when you want to display an external URL directly without wrapping it in HTML: + +```python +@mcp.tool +def show_dashboard() -> Dict[str, Any]: + """Display an external dashboard in the canvas.""" + return { + "results": {"summary": "Dashboard loaded"}, + "artifacts": [], + "display": { + "type": "iframe", + "url": "https://example.com/dashboard", + "title": "Analytics Dashboard", + "sandbox": "allow-scripts allow-same-origin", + "open_canvas": True + } + } +``` + +**Approach 2: HTML Artifact with Embedded Iframe** + +Use this when you want to create a custom HTML page that includes one or more iframes: + +```python +@mcp.tool +def create_page_with_iframe() -> Dict[str, Any]: + """Create an HTML page with embedded iframe.""" + html_content = """ + + +

External Content

+ + + + """ + b64_content = base64.b64encode(html_content.encode()).decode() + + return { + "results": {"summary": "Page created"}, + "artifacts": [{ + "name": "page.html", + "b64": b64_content, + "mime": "text/html", + "viewer": "html" + }], + "display": { + "open_canvas": True, + "primary_file": "page.html" + } + } +``` + +**Security Considerations for Iframes:** + +* Iframes are automatically sandboxed for security +* Default sandbox permissions: `"allow-scripts allow-same-origin"` (restrictive by default) +* Tools can specify custom sandbox attributes via the `sandbox` field to add permissions like `"allow-forms"` if needed +* Only whitelisted iframe attributes are preserved: `src`, `sandbox`, `allow`, `allowfullscreen`, `frameborder`, `scrolling` +* All HTML content is sanitized with DOMPurify before rendering + ### 3. Registering the Server After creating your server, you must register it in `config/overrides/mcp.json`. diff --git a/docs/developer/canvas-renderers.md b/docs/developer/canvas-renderers.md index 842b69e..2e04358 100644 --- a/docs/developer/canvas-renderers.md +++ b/docs/developer/canvas-renderers.md @@ -1,13 +1,53 @@ # Adding Custom Canvas Renderers -The canvas panel displays tool-generated files (PDFs, images, HTML). To add support for new file types (e.g., `.stl`, `.obj`, `.ipynb`): +The canvas panel displays tool-generated files (PDFs, images, HTML, iframes). To add support for new file types (e.g., `.stl`, `.obj`, `.ipynb`): ## Canvas Architecture Flow -1. Backend tool returns artifacts → stored in S3 → sends `canvas_files` WebSocket message -2. Frontend receives file metadata (filename, s3_key, type) -3. Frontend fetches file content from `/api/files/download/{s3_key}` -4. `CanvasPanel` renders based on file type +1. Backend tool returns artifacts and optional `display` hints → stored in S3 → sends `canvas_files` WebSocket message +2. Frontend receives file metadata (filename, s3_key, type, viewer_hint/display type) +3. Frontend fetches file content from `/api/files/download/{s3_key}` when needed +4. `CanvasPanel` renders based on file type and viewer configuration + +## Built-in Viewers and Iframe Support + +The canvas supports several built-in viewer types, selected via the artifact `viewer` field or display configuration: + +- `html`: Render HTML content in an isolated, sanitized frame +- `image`: Display images such as PNG/JPEG +- `pdf`: Render PDF documents +- `iframe`: Embed external content from a URL + +For iframe-based content, there are two primary patterns: + +1. **Direct iframe via `display`** – the tool sets `display.type = "iframe"` and provides a `url`, `title`, and optional `sandbox` attributes. +2. **HTML artifact with embedded ` + + +""" + + html_base64 = base64.b64encode(html_content.encode('utf-8')).decode('utf-8') + + return { + "results": { + "content": "HTML with embedded iframe created" + }, + "artifacts": [{ + "name": "viewer.html", + "b64": html_base64, + "mime": "text/html", + "size": len(html_content.encode('utf-8')), + "description": "HTML page with embedded iframe", + "viewer": "html" + }], + "display": { + "open_canvas": True, + "primary_file": "viewer.html", + "mode": "replace", + "viewer_hint": "html" + } + } +``` + +### CRITICAL: CSP Configuration Required + +**External iframe URLs will be blocked by the browser unless properly configured in the application's Content Security Policy.** + +For any external URL you want to display in an iframe, the system administrator must add that domain to the `SECURITY_CSP_VALUE` environment variable's `frame-src` directive. + +**Example `.env` configuration:** +```bash +# To allow https://dashboard.example.com and https://www.sandia.gov/ +SECURITY_CSP_VALUE="default-src 'self'; img-src 'self' data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-src 'self' blob: data: https://dashboard.example.com https://www.sandia.gov/; frame-ancestors 'self'" +``` + +**Best Practice:** Document which external URLs your MCP server uses in your server's `description` field in `mcp.json`, so administrators know which domains need to be added to the CSP configuration. + +See the `backend/mcp/ui-demo/main.py` file for working examples of both iframe patterns. + ## 3. Registering the Server After creating your server, you must register it in `config/overrides/mcp.json`. diff --git a/frontend/src/components/CanvasPanel.jsx b/frontend/src/components/CanvasPanel.jsx index 6ff5d86..53e4673 100644 --- a/frontend/src/components/CanvasPanel.jsx +++ b/frontend/src/components/CanvasPanel.jsx @@ -4,6 +4,23 @@ import { marked } from 'marked' import DOMPurify from 'dompurify' import { useState, useEffect } from 'react' +// DOMPurify configuration for allowing iframes in HTML content +const IFRAME_SANITIZE_CONFIG = { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'sandbox', 'src'] +}; + +// Default sandbox permissions for iframes (restrictive by default) +const DEFAULT_IFRAME_SANDBOX = 'allow-scripts allow-same-origin'; + +// Helper function to check if a file can be downloaded +const canDownloadFile = (file) => { + if (!file) return false; + if (file.isInline) return false; // Inline files don't have backend storage + if (file.type === 'iframe') return false; // Iframes are not downloadable + return true; +}; + // Helper function to process canvas content (strings and structured objects) const processCanvasContent = (content) => { if (typeof content === 'string') { @@ -20,7 +37,7 @@ const processCanvasContent = (content) => { // Fallback to JSON for other objects try { return JSON.stringify(content, null, 2) - } catch (e) { + } catch { return String(content || '') } } @@ -148,6 +165,18 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { return; } + // Handle iframe artifacts (from display config with URL) + if (currentFile.type === 'iframe' && currentFile.url) { + setCurrentFileContent({ + type: 'iframe', + url: currentFile.url, + file: currentFile, + sandbox: currentFile.sandbox || DEFAULT_IFRAME_SANDBOX + }); + setIsLoadingFile(false); + return; + } + // Fetch file content from the backend const response = await fetch(`/api/files/download/${currentFile.s3_key}`, { method: 'GET', @@ -201,8 +230,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { const handleDownload = () => { const currentFile = canvasFiles[currentCanvasFileIndex]; - // Inline-only files are not downloadable via backend - if (currentFile && !currentFile.isInline && downloadFile) { + if (canDownloadFile(currentFile) && downloadFile) { downloadFile(currentFile.filename); } }; @@ -212,6 +240,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { case 'image': return ; case 'pdf': return ; case 'html': return ; + case 'iframe': return ; default: return ; } }; @@ -271,12 +300,24 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { ); + case 'iframe': + return ( +
+