From 17c6231a2068abd6c78394542a955b23c153ea84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 02:39:31 +0000 Subject: [PATCH 01/13] Initial plan From 3b19b6b0e4874481e6080bfb3392cd4054226dcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 02:49:25 +0000 Subject: [PATCH 02/13] Add iframe support to canvas panel for MCP tools - Added support for iframe type artifacts in CanvasPanel - Updated DOMPurify configuration to allow iframes with proper sandboxing - Added handling for display.canvas.type = "iframe" from v2 MCP spec - Created demo MCP tool to test iframe functionality - Configured proper security attributes (sandbox) for iframes Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- backend/mcp/ui-demo/main.py | 27 ++++++++++ frontend/src/components/CanvasPanel.jsx | 53 ++++++++++++++++--- .../src/handlers/chat/websocketHandlers.js | 19 ++++++- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/backend/mcp/ui-demo/main.py b/backend/mcp/ui-demo/main.py index 77fba12..fab2d16 100644 --- a/backend/mcp/ui-demo/main.py +++ b/backend/mcp/ui-demo/main.py @@ -255,5 +255,32 @@ 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. + + 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://example.com" + }, + "artifacts": [], + "display": { + "open_canvas": True, + "type": "iframe", + "url": "https://example.com", + "title": "Example Website", + "sandbox": "allow-scripts allow-same-origin", + "mode": "replace" + } + } + if __name__ == "__main__": mcp.run() diff --git a/frontend/src/components/CanvasPanel.jsx b/frontend/src/components/CanvasPanel.jsx index 6ff5d86..a4b09d5 100644 --- a/frontend/src/components/CanvasPanel.jsx +++ b/frontend/src/components/CanvasPanel.jsx @@ -20,7 +20,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 +148,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 || 'allow-scripts allow-same-origin allow-forms' + }); + setIsLoadingFile(false); + return; + } + // Fetch file content from the backend const response = await fetch(`/api/files/download/${currentFile.s3_key}`, { method: 'GET', @@ -201,8 +213,8 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { const handleDownload = () => { const currentFile = canvasFiles[currentCanvasFileIndex]; - // Inline-only files are not downloadable via backend - if (currentFile && !currentFile.isInline && downloadFile) { + // Inline-only files and iframes are not downloadable via backend + if (currentFile && !currentFile.isInline && currentFile.type !== 'iframe' && downloadFile) { downloadFile(currentFile.filename); } }; @@ -212,6 +224,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { case 'image': return ; case 'pdf': return ; case 'html': return ; + case 'iframe': return ; default: return ; } }; @@ -271,12 +284,27 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { ); + case 'iframe': + return ( +
+ +
+ +""" + + 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() From 74bbf6bb788b67d8dd3410a682eb86f8df388f96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 02:54:11 +0000 Subject: [PATCH 05/13] Add iframe support documentation to developer guide Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- docs/03_developer_guide.md | 75 +++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/03_developer_guide.md b/docs/03_developer_guide.md index 2dabd22..dbcbf02 100644 --- a/docs/03_developer_guide.md +++ b/docs/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 allow-forms"` +* Tools can specify custom sandbox attributes via the `sandbox` field +* 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`. From 2329e055a71b9a0b5a5a30a5889019ed94a29745 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 02:57:06 +0000 Subject: [PATCH 06/13] Address code review feedback - Extract DOMPurify config to constant to avoid duplication - Simplify download check logic with helper variable - Make default sandbox more restrictive (removed allow-forms) - Update documentation with more secure defaults Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- docs/03_developer_guide.md | 4 +-- frontend/src/components/CanvasPanel.jsx | 32 ++++++++----------- .../src/handlers/chat/websocketHandlers.js | 2 +- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/docs/03_developer_guide.md b/docs/03_developer_guide.md index dbcbf02..f80f1d8 100644 --- a/docs/03_developer_guide.md +++ b/docs/03_developer_guide.md @@ -176,8 +176,8 @@ def create_page_with_iframe() -> Dict[str, Any]: **Security Considerations for Iframes:** * Iframes are automatically sandboxed for security -* Default sandbox permissions: `"allow-scripts allow-same-origin allow-forms"` -* Tools can specify custom sandbox attributes via the `sandbox` field +* 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 diff --git a/frontend/src/components/CanvasPanel.jsx b/frontend/src/components/CanvasPanel.jsx index a4b09d5..8ff4f9d 100644 --- a/frontend/src/components/CanvasPanel.jsx +++ b/frontend/src/components/CanvasPanel.jsx @@ -4,6 +4,12 @@ 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'] +}; + // Helper function to process canvas content (strings and structured objects) const processCanvasContent = (content) => { if (typeof content === 'string') { @@ -154,7 +160,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { type: 'iframe', url: currentFile.url, file: currentFile, - sandbox: currentFile.sandbox || 'allow-scripts allow-same-origin allow-forms' + sandbox: currentFile.sandbox || 'allow-scripts allow-same-origin' }); setIsLoadingFile(false); return; @@ -213,8 +219,8 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { const handleDownload = () => { const currentFile = canvasFiles[currentCanvasFileIndex]; - // Inline-only files and iframes are not downloadable via backend - if (currentFile && !currentFile.isInline && currentFile.type !== 'iframe' && downloadFile) { + const canDownload = currentFile && !currentFile.isInline && currentFile.type !== 'iframe'; + if (canDownload && downloadFile) { downloadFile(currentFile.filename); } }; @@ -301,10 +307,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => {
); @@ -313,10 +316,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { try { // Try to parse as markdown first const markdownHtml = marked.parse(currentFileContent.content); - const sanitizedHtml = DOMPurify.sanitize(markdownHtml, { - ADD_TAGS: ['iframe'], - ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'sandbox', 'src'] - }); + const sanitizedHtml = DOMPurify.sanitize(markdownHtml, IFRAME_SANITIZE_CONFIG); return (
{
) @@ -374,10 +371,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { try { const markdownHtml = marked.parse(content) - const sanitizedHtml = DOMPurify.sanitize(markdownHtml, { - ADD_TAGS: ['iframe'], - ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'sandbox', 'src'] - }) + const sanitizedHtml = DOMPurify.sanitize(markdownHtml, IFRAME_SANITIZE_CONFIG) return (
Date: Sat, 22 Nov 2025 02:59:12 +0000 Subject: [PATCH 07/13] Extract constants and helper function for better maintainability - Add DEFAULT_IFRAME_SANDBOX constant for consistency - Extract canDownloadFile helper function for readability - Use constants in both CanvasPanel and websocketHandlers Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- frontend/src/components/CanvasPanel.jsx | 16 +++++++++++++--- frontend/src/handlers/chat/websocketHandlers.js | 5 ++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/CanvasPanel.jsx b/frontend/src/components/CanvasPanel.jsx index 8ff4f9d..53e4673 100644 --- a/frontend/src/components/CanvasPanel.jsx +++ b/frontend/src/components/CanvasPanel.jsx @@ -10,6 +10,17 @@ const IFRAME_SANITIZE_CONFIG = { 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') { @@ -160,7 +171,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { type: 'iframe', url: currentFile.url, file: currentFile, - sandbox: currentFile.sandbox || 'allow-scripts allow-same-origin' + sandbox: currentFile.sandbox || DEFAULT_IFRAME_SANDBOX }); setIsLoadingFile(false); return; @@ -219,8 +230,7 @@ const CanvasPanel = ({ isOpen, onClose, onWidthChange }) => { const handleDownload = () => { const currentFile = canvasFiles[currentCanvasFileIndex]; - const canDownload = currentFile && !currentFile.isInline && currentFile.type !== 'iframe'; - if (canDownload && downloadFile) { + if (canDownloadFile(currentFile) && downloadFile) { downloadFile(currentFile.filename); } }; diff --git a/frontend/src/handlers/chat/websocketHandlers.js b/frontend/src/handlers/chat/websocketHandlers.js index ae12318..b7e409e 100644 --- a/frontend/src/handlers/chat/websocketHandlers.js +++ b/frontend/src/handlers/chat/websocketHandlers.js @@ -1,5 +1,8 @@ // Handlers extracted from original ChatContext to keep provider lean +// Default sandbox permissions for iframes (restrictive by default) +const DEFAULT_IFRAME_SANDBOX = 'allow-scripts allow-same-origin'; + export function createWebSocketHandler(deps) { const { addMessage, @@ -174,7 +177,7 @@ export function createWebSocketHandler(deps) { filename: updateData.display.title || 'Embedded Content', type: 'iframe', url: updateData.display.url, - sandbox: updateData.display.sandbox || 'allow-scripts allow-same-origin', + sandbox: updateData.display.sandbox || DEFAULT_IFRAME_SANDBOX, isInline: true } canvasFiles = [iframeFile, ...canvasFiles] From 4e0d132483284a79045bce37d30547b655660d95 Mon Sep 17 00:00:00 2001 From: Anthony Date: Sat, 22 Nov 2025 04:24:01 +0000 Subject: [PATCH 08/13] docs(ui-demo): enhance create_button_demo docstring with detailed sections Improve documentation for the create_button_demo function to provide clearer descriptions of its capabilities, use cases, technical implementation, and integration features. This update helps developers and users better understand the tool's features and benefits for UI prototyping and demonstrations. --- backend/mcp/ui-demo/main.py | 128 ++++++++++++------------- docs/developer/canvas-renderers.md | 26 ++++- docs/developer/creating-mcp-servers.md | 2 + 3 files changed, 87 insertions(+), 69 deletions(-) diff --git a/backend/mcp/ui-demo/main.py b/backend/mcp/ui-demo/main.py index 0299b65..c6249df 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") @@ -269,13 +269,13 @@ def create_iframe_demo() -> Dict[str, Any]: return { "results": { "content": "Iframe demo created! An external webpage will be displayed in the canvas panel.", - "iframe_url": "https://example.com" + "iframe_url": "https://www.sandia.gov/" }, "artifacts": [], "display": { "open_canvas": True, "type": "iframe", - "url": "https://example.com", + "url": "https://www.sandia.gov/", "title": "Example Website", "sandbox": "allow-scripts allow-same-origin", "mode": "replace" @@ -325,10 +325,10 @@ def create_html_with_iframe() -> Dict[str, Any]:

Embedded Content Demo

This HTML artifact includes an embedded iframe showing external content:

- diff --git a/docs/developer/canvas-renderers.md b/docs/developer/canvas-renderers.md index 842b69e..352e3f8 100644 --- a/docs/developer/canvas-renderers.md +++ b/docs/developer/canvas-renderers.md @@ -1,13 +1,29 @@ # 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/contexts/ChatContext.jsx b/frontend/src/contexts/ChatContext.jsx index cb6ff97..803998f 100644 --- a/frontend/src/contexts/ChatContext.jsx +++ b/frontend/src/contexts/ChatContext.jsx @@ -96,6 +96,7 @@ export const ChatProvider = ({ children }) => { setCanvasFiles: files.setCanvasFiles, setCurrentCanvasFileIndex: files.setCurrentCanvasFileIndex, setCustomUIContent: files.setCustomUIContent, + setIsCanvasOpen: config.setIsCanvasOpen, setSessionFiles: files.setSessionFiles, getFileType: files.getFileType, triggerFileDownload, diff --git a/frontend/src/handlers/chat/websocketHandlers.js b/frontend/src/handlers/chat/websocketHandlers.js index b7e409e..7c1442a 100644 --- a/frontend/src/handlers/chat/websocketHandlers.js +++ b/frontend/src/handlers/chat/websocketHandlers.js @@ -15,6 +15,7 @@ export function createWebSocketHandler(deps) { setCanvasFiles, setCurrentCanvasFileIndex, setCustomUIContent, + setIsCanvasOpen, setSessionFiles, getFileType, triggerFileDownload, @@ -155,6 +156,9 @@ export function createWebSocketHandler(deps) { setCurrentCanvasFileIndex(0) } if (display.open_canvas) { + if (typeof setIsCanvasOpen === 'function') { + setIsCanvasOpen(true) + } setCanvasContent('') setCustomUIContent(null) } @@ -169,7 +173,7 @@ export function createWebSocketHandler(deps) { case 'canvas_files': if (updateData && Array.isArray(updateData.files)) { let canvasFiles = updateData.files - + // Check if display config specifies an iframe if (updateData.display && updateData.display.type === 'iframe' && updateData.display.url) { // Add iframe as a virtual canvas file @@ -182,7 +186,7 @@ export function createWebSocketHandler(deps) { } canvasFiles = [iframeFile, ...canvasFiles] } - + setCanvasFiles(canvasFiles) // If backend provided display hints, respect them (e.g., primary_file) if (updateData.display && updateData.display.primary_file) { @@ -191,6 +195,12 @@ export function createWebSocketHandler(deps) { } else { setCurrentCanvasFileIndex(0) } + + // Open canvas panel if display.open_canvas is true + if (updateData.display && updateData.display.open_canvas && typeof setIsCanvasOpen === 'function') { + setIsCanvasOpen(true) + } + setCanvasContent('') setCustomUIContent(null) } diff --git a/frontend/src/handlers/chat/websocketHandlers.test.js b/frontend/src/handlers/chat/websocketHandlers.test.js index e41e4d8..5b4e71b 100644 --- a/frontend/src/handlers/chat/websocketHandlers.test.js +++ b/frontend/src/handlers/chat/websocketHandlers.test.js @@ -16,6 +16,7 @@ const makeDeps = () => { setCanvasFiles: vi.fn(), setCurrentCanvasFileIndex: vi.fn(), setCustomUIContent: vi.fn(), + setIsCanvasOpen: vi.fn(), setSessionFiles: vi.fn(), getFileType: vi.fn(), triggerFileDownload: vi.fn(), @@ -104,6 +105,9 @@ describe('createWebSocketHandler – intermediate updates', () => { expect(deps.setCurrentCanvasFileIndex).toHaveBeenCalledWith(0) expect(deps.setCanvasContent).toHaveBeenCalledWith('') expect(deps.setCustomUIContent).toHaveBeenCalledWith(null) + + // Should open the canvas panel when display.open_canvas is true + expect(deps.setIsCanvasOpen).toHaveBeenCalledWith(true) }) it('creates iframe canvas file from display config with type=iframe', () => { @@ -144,5 +148,8 @@ describe('createWebSocketHandler – intermediate updates', () => { expect(deps.setCurrentCanvasFileIndex).toHaveBeenCalledWith(0) expect(deps.setCanvasContent).toHaveBeenCalledWith('') expect(deps.setCustomUIContent).toHaveBeenCalledWith(null) + + // Should open the canvas panel when display.open_canvas is true + expect(deps.setIsCanvasOpen).toHaveBeenCalledWith(true) }) }) diff --git a/frontend/src/hooks/chat/useChatConfig.js b/frontend/src/hooks/chat/useChatConfig.js index 677c038..38108d5 100644 --- a/frontend/src/hooks/chat/useChatConfig.js +++ b/frontend/src/hooks/chat/useChatConfig.js @@ -19,6 +19,7 @@ export function useChatConfig() { const [dataSources, setDataSources] = useState([]) const [ragServers, setRagServers] = useState([]) // New state for rich RAG server data const [features, setFeatures] = useState(DEFAULT_FEATURES) + const [isCanvasOpen, setIsCanvasOpen] = useState(false) // Load saved model from localStorage const [currentModel, setCurrentModel] = useState(() => { try { @@ -95,6 +96,8 @@ export function useChatConfig() { ragServers, // Expose new state features, setFeatures, + isCanvasOpen, + setIsCanvasOpen, currentModel, setCurrentModel: (model) => { setCurrentModel(model) From 8978ea9cdc83fc0fdf56300c3d7cff1811323fc4 Mon Sep 17 00:00:00 2001 From: Anthony Date: Sat, 22 Nov 2025 14:48:55 +0000 Subject: [PATCH 10/13] feat(mcp): add example configurations for various MCP servers and update documentation --- .env.example | 2 +- .github/copilot-instructions.md | 5 +- CLAUDE.md | 2 + GEMINI.md | 6 +- .../application/chat/utilities/file_utils.py | 38 ++++++++-- .../mcp-example-configs/mcp-basictable.json | 1 + .../mcp-example-configs/mcp-calculator.json | 17 +++++ .../mcp-code-executor.json | 1 + .../mcp-corporate_cars.json | 1 + .../mcp-example-configs/mcp-csv_reporter.json | 1 + .../mcp-example-configs/mcp-duckduckgo.json | 1 + config/mcp-example-configs/mcp-env-demo.json | 1 + .../mcp-file_size_test.json | 1 + .../mcp-example-configs/mcp-filesystem.json | 1 + .../mcp-many_tools_demo.json | 1 + .../mcp-order_database.json | 1 + config/mcp-example-configs/mcp-pdfbasic.json | 1 + .../mcp-pptx_generator.json | 1 + .../mcp-progress_demo.json | 1 + .../mcp-progress_updates_demo.json | 1 + config/mcp-example-configs/mcp-prompts.json | 1 + config/mcp-example-configs/mcp-thinking.json | 1 + config/mcp-example-configs/mcp-ui-demo.json | 1 + .../mcp-example-configs/mcp-vtk-example.json | 1 + config/overrides/env-var-mcp.json | 23 ------ config/overrides/mcp copy.json | 72 ------------------- config/overrides/mcp-test.json | 30 -------- config/overrides/mcp.json | 15 ++++ 28 files changed, 94 insertions(+), 134 deletions(-) create mode 100644 config/mcp-example-configs/mcp-basictable.json create mode 100644 config/mcp-example-configs/mcp-calculator.json create mode 100644 config/mcp-example-configs/mcp-code-executor.json create mode 100644 config/mcp-example-configs/mcp-corporate_cars.json create mode 100644 config/mcp-example-configs/mcp-csv_reporter.json create mode 100644 config/mcp-example-configs/mcp-duckduckgo.json create mode 100644 config/mcp-example-configs/mcp-env-demo.json create mode 100644 config/mcp-example-configs/mcp-file_size_test.json create mode 100644 config/mcp-example-configs/mcp-filesystem.json create mode 100644 config/mcp-example-configs/mcp-many_tools_demo.json create mode 100644 config/mcp-example-configs/mcp-order_database.json create mode 100644 config/mcp-example-configs/mcp-pdfbasic.json create mode 100644 config/mcp-example-configs/mcp-pptx_generator.json create mode 100644 config/mcp-example-configs/mcp-progress_demo.json create mode 100644 config/mcp-example-configs/mcp-progress_updates_demo.json create mode 100644 config/mcp-example-configs/mcp-prompts.json create mode 100644 config/mcp-example-configs/mcp-thinking.json create mode 100644 config/mcp-example-configs/mcp-ui-demo.json create mode 100644 config/mcp-example-configs/mcp-vtk-example.json delete mode 100644 config/overrides/env-var-mcp.json delete mode 100644 config/overrides/mcp copy.json delete mode 100644 config/overrides/mcp-test.json diff --git a/.env.example b/.env.example index d24d83d..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 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..72edd43 100644 --- a/backend/application/chat/utilities/file_utils.py +++ b/backend/application/chat/utilities/file_utils.py @@ -368,17 +368,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/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-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..aff748e 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": [ From 72a5972f5ab3659ac593ba4a457c3249e8db4c63 Mon Sep 17 00:00:00 2001 From: Anthony Date: Sat, 22 Nov 2025 15:14:05 +0000 Subject: [PATCH 11/13] feat(mcp): enhance tool artifact processing to support iframe display configuration and update related tests --- .../application/chat/utilities/file_utils.py | 39 ++++++++++++------- .../test_mcp_prompt_override_system_prompt.py | 15 ++++++- backend/tests/test_mcp_prompts_server.py | 18 ++++++++- .../mcp-external-api-example.json | 12 ++++++ config/overrides/mcp.json | 11 ------ .../overrides/progress-report-mcp copy.json | 18 --------- 6 files changed, 67 insertions(+), 46 deletions(-) create mode 100644 config/mcp-example-configs/mcp-external-api-example.json delete mode 100644 config/overrides/progress-report-mcp copy.json diff --git a/backend/application/chat/utilities/file_utils.py b/backend/application/chat/utilities/file_utils.py index 72edd43..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, diff --git a/backend/tests/test_mcp_prompt_override_system_prompt.py b/backend/tests/test_mcp_prompt_override_system_prompt.py index 227d1af..ae6f2cb 100644 --- a/backend/tests/test_mcp_prompt_override_system_prompt.py +++ b/backend/tests/test_mcp_prompt_override_system_prompt.py @@ -1,4 +1,5 @@ import pytest +from unittest.mock import patch, Mock from modules.mcp_tools.client import MCPToolManager from modules.config import ConfigManager @@ -13,7 +14,19 @@ 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() + # Patch config_manager so MCPToolManager sees a "prompts" server + # without depending on actual file-based config. + with patch("modules.mcp_tools.client.config_manager") as mock_cm: + mock_cm.mcp_config.servers = { + "prompts": Mock(name="prompts_server_config") + } + mock_cm.mcp_config.servers["prompts"].model_dump.return_value = { + "command": ["python", "mcp/prompts/main.py"], + "cwd": "backend", + "groups": ["users"], + } + + mcp: MCPToolManager = MCPToolManager() 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..00ede91 100644 --- a/backend/tests/test_mcp_prompts_server.py +++ b/backend/tests/test_mcp_prompts_server.py @@ -1,11 +1,25 @@ import pytest +from unittest.mock import patch, Mock -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 to ensure the prompts + # server is available in tests regardless of app settings + # and avoid depending on the global config manager. + with patch("modules.mcp_tools.client.config_manager") as mock_cm: + mock_cm.mcp_config.servers = { + "prompts": Mock(name="prompts_server_config") + } + mock_cm.mcp_config.servers["prompts"].model_dump.return_value = { + "command": ["python", "mcp/prompts/main.py"], + "cwd": "backend", + "groups": ["users"], + } + + mcp = MCPToolManager() # Ensure fresh clients and prompt discovery await mcp.initialize_clients() 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/overrides/mcp.json b/config/overrides/mcp.json index aff748e..14abf70 100644 --- a/config/overrides/mcp.json +++ b/config/overrides/mcp.json @@ -133,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" - } -} From 2385c59d8bf6d351dfe4f567f21fbde63ac51de8 Mon Sep 17 00:00:00 2001 From: Anthony Date: Sat, 22 Nov 2025 15:31:37 +0000 Subject: [PATCH 12/13] feat(tests): update MCP prompt tests to use example config and enhance backend test script output --- .../test_mcp_prompt_override_system_prompt.py | 29 ++++++++-------- backend/tests/test_mcp_prompts_server.py | 34 +++++++++++-------- test/backend_tests.sh | 1 + 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/backend/tests/test_mcp_prompt_override_system_prompt.py b/backend/tests/test_mcp_prompt_override_system_prompt.py index ae6f2cb..db9cbd2 100644 --- a/backend/tests/test_mcp_prompt_override_system_prompt.py +++ b/backend/tests/test_mcp_prompt_override_system_prompt.py @@ -1,5 +1,7 @@ +import json +from pathlib import Path + import pytest -from unittest.mock import patch, Mock from modules.mcp_tools.client import MCPToolManager from modules.config import ConfigManager @@ -14,19 +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). - # Patch config_manager so MCPToolManager sees a "prompts" server - # without depending on actual file-based config. - with patch("modules.mcp_tools.client.config_manager") as mock_cm: - mock_cm.mcp_config.servers = { - "prompts": Mock(name="prompts_server_config") - } - mock_cm.mcp_config.servers["prompts"].model_dump.return_value = { - "command": ["python", "mcp/prompts/main.py"], - "cwd": "backend", - "groups": ["users"], - } - - 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 00ede91..4396aba 100644 --- a/backend/tests/test_mcp_prompts_server.py +++ b/backend/tests/test_mcp_prompts_server.py @@ -1,25 +1,29 @@ +import json +from pathlib import Path + import pytest -from unittest.mock import patch, Mock from modules.mcp_tools.client import MCPToolManager @pytest.mark.asyncio async def test_mcp_prompts_discovery_includes_expert_dog_trainer(): - # Use the example prompts MCP config to ensure the prompts - # server is available in tests regardless of app settings - # and avoid depending on the global config manager. - with patch("modules.mcp_tools.client.config_manager") as mock_cm: - mock_cm.mcp_config.servers = { - "prompts": Mock(name="prompts_server_config") - } - mock_cm.mcp_config.servers["prompts"].model_dump.return_value = { - "command": ["python", "mcp/prompts/main.py"], - "cwd": "backend", - "groups": ["users"], - } - - mcp = MCPToolManager() + # 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/test/backend_tests.sh b/test/backend_tests.sh index 6ea79a7..e175efb 100755 --- a/test/backend_tests.sh +++ b/test/backend_tests.sh @@ -39,6 +39,7 @@ cd "$BACKEND_DIR" echo "" echo "\n🧪 Running Backend Tests..." +echo "BACKEND_DIR full path: $(pwd)" echo "==============================" # If legacy targeted tests exist, run them; otherwise run all tests in backend/tests From a745e6778b104a542fc6981748416a5a1a84cef2 Mon Sep 17 00:00:00 2001 From: Anthony Date: Sat, 22 Nov 2025 15:36:26 +0000 Subject: [PATCH 13/13] feat(mcp): enhance MCPToolManager to support custom config paths and improve config loading --- backend/modules/mcp_tools/client.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) 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 = {}