# MCP Apps & Interactive UI

MCP Apps let you build interactive micro-applications that run directly inside DarcyIQ. Create dashboards, forms, data visualizations, and fully interactive experiences—all powered by your MCP integration.

{% hint style="success" %}
**Beyond Text Responses**: While standard MCP tools return text, MCP Apps return rich HTML interfaces that users can interact with directly in Chat or the Artifact panel.
{% endhint %}

## What are MCP Apps?

<figure><img src="/files/KZVxJptD05nxOLj5mJqE" alt=""><figcaption></figcaption></figure>

MCP Apps extend the standard MCP tool pattern by returning interactive user interfaces instead of (or alongside) text responses. When your tool returns a UI resource, DarcyIQ renders it as a fully interactive web application within a secure sandbox.

| Feature              | Standard MCP Tool      | MCP App                                   |
| -------------------- | ---------------------- | ----------------------------------------- |
| **Output**           | Text/JSON data         | Interactive HTML/JavaScript UI            |
| **User Interaction** | None (read-only)       | Buttons, forms, charts, navigation        |
| **Visual Richness**  | Plain text or markdown | Full HTML/CSS styling                     |
| **Callbacks**        | N/A                    | UI can trigger other tools                |
| **State**            | Stateless              | Can maintain state and update dynamically |

## Use Cases for MCP Apps

<figure><img src="/files/8Dyk6s2C8wT3su1S75oS" alt=""><figcaption></figcaption></figure>

MCP Apps are ideal for scenarios where visual interaction enhances the user experience:

| Use Case                 | Example                          | Why MCP App?                              |
| ------------------------ | -------------------------------- | ----------------------------------------- |
| **Dashboards**           | Sales metrics, system status     | Real-time data visualization with charts  |
| **Data Browsers**        | Customer lists, product catalogs | Sortable tables, filters, pagination      |
| **Configuration Panels** | Settings, preferences            | Forms with validation and save buttons    |
| **Wizards**              | Multi-step workflows             | Guided step-by-step interfaces            |
| **Reports**              | Financial summaries, analytics   | Formatted tables, charts, export options  |
| **Interactive Forms**    | Data entry, surveys              | Input validation, dropdowns, date pickers |
| **Approval Workflows**   | Review and approve items         | Action buttons, status indicators         |

## How MCP Apps Work

### Architecture Overview

```mermaid
graph TB
    subgraph DarcyIQ[DarcyIQ Chat]
        subgraph MCPApp[MCP App - Sandboxed Iframe]
            HTMLApp[Your HTML/CSS/JavaScript Application<br/>Charts, Tables, Forms<br/>User interactions<br/>postMessage to invoke tools]
        end
        
        HTMLApp -->|postMessage| CallbackHandler[MCP-UI Callback Handler<br/>Validates tool request<br/>Routes to MCP server<br/>Returns result to iframe]
    end
    
    CallbackHandler -->|Tool Invocation| MCPServer[Your MCP Server<br/>Python/Node.js]
    MCPServer -->|Response| CallbackHandler
```

### The UI Resource Format

MCP Apps return a special `UIResource` object that tells DarcyIQ to render interactive content:

```python
{
    "ui": {
        "uri": "app://my-dashboard",           # Unique identifier
        "name": "Sales Dashboard",              # Display name
        "mimeType": "text/html",               # Content type
        "content": "<html>...</html>",         # Your HTML/JS/CSS
        "description": "Interactive sales metrics"  # Optional description
    }
}
```

| Field         | Required | Description                             |
| ------------- | -------- | --------------------------------------- |
| `uri`         | Yes      | Unique identifier for the UI resource   |
| `name`        | Yes      | Display name shown in the UI header     |
| `mimeType`    | Yes      | Must be `text/html` for interactive UIs |
| `content`     | Yes      | Your HTML, CSS, and JavaScript code     |
| `description` | No       | Brief description of the UI             |

## Building Your First MCP App

### Step 1: Create the MCP Server

Start with a standard MCP server and add a tool that returns a UI resource:

```python
from mcp.server import Server
from mcp.types import TextContent
import json

app = Server("my-dashboard-app")

@app.tool()
async def show_dashboard() -> list[TextContent]:
    """
    Display an interactive dashboard with sales metrics.
    
    Returns:
        Interactive dashboard UI
    """
    # Your dashboard HTML
    html_content = '''
    <!DOCTYPE html>
    <html>
    <head>
        <style>
            body { 
                font-family: -apple-system, BlinkMacSystemFont, sans-serif;
                padding: 20px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                min-height: 100vh;
                margin: 0;
            }
            .card {
                background: white;
                border-radius: 12px;
                padding: 24px;
                margin-bottom: 16px;
                box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            }
            .metric {
                font-size: 36px;
                font-weight: bold;
                color: #1a1a2e;
            }
            .label {
                color: #666;
                font-size: 14px;
                margin-bottom: 8px;
            }
            .grid {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
                gap: 16px;
            }
            button {
                background: #667eea;
                color: white;
                border: none;
                padding: 12px 24px;
                border-radius: 8px;
                cursor: pointer;
                font-size: 14px;
                transition: transform 0.2s;
            }
            button:hover {
                transform: translateY(-2px);
            }
        </style>
    </head>
    <body>
        <div class="card">
            <h2 style="margin-top:0">Sales Dashboard</h2>
            <div class="grid">
                <div>
                    <div class="label">Total Revenue</div>
                    <div class="metric">$124,500</div>
                </div>
                <div>
                    <div class="label">New Customers</div>
                    <div class="metric">48</div>
                </div>
                <div>
                    <div class="label">Conversion Rate</div>
                    <div class="metric">3.2%</div>
                </div>
            </div>
        </div>
        <div class="card">
            <button onclick="refreshData()">Refresh Data</button>
            <button onclick="exportReport()">Export Report</button>
        </div>
        
        <script>
            function refreshData() {
                // Call another tool on the MCP server
                window.parent.postMessage({
                    type: 'tool',
                    payload: {
                        toolName: 'get_latest_metrics',
                        params: {}
                    }
                }, '*');
            }
            
            function exportReport() {
                window.parent.postMessage({
                    type: 'tool',
                    payload: {
                        toolName: 'export_sales_report',
                        params: { format: 'pdf' }
                    }
                }, '*');
            }
        </script>
    </body>
    </html>
    '''
    
    # Return both UI and data
    result = {
        "ui": {
            "uri": "app://sales-dashboard",
            "name": "Sales Dashboard",
            "mimeType": "text/html",
            "content": html_content,
            "description": "Interactive sales metrics dashboard"
        },
        "data": {
            "revenue": 124500,
            "customers": 48,
            "conversion": 3.2
        }
    }
    
    return [TextContent(type="text", text=json.dumps(result))]

if __name__ == "__main__":
    app.run()
```

### Step 2: Add Callback Tools

Create additional tools that the UI can invoke:

```python
@app.tool()
async def get_latest_metrics() -> list[TextContent]:
    """
    Fetch the latest metrics from the database.
    Called by the dashboard UI refresh button.
    """
    # Fetch real data from your database
    metrics = await fetch_metrics_from_db()
    
    # Return updated UI with new data
    updated_html = generate_dashboard_html(metrics)
    
    result = {
        "ui": {
            "uri": "app://sales-dashboard",
            "name": "Sales Dashboard (Updated)",
            "mimeType": "text/html",
            "content": updated_html
        }
    }
    
    return [TextContent(type="text", text=json.dumps(result))]


@app.tool()
async def export_sales_report(format: str = "pdf") -> list[TextContent]:
    """
    Export the sales report in the specified format.
    
    Args:
        format: Export format (pdf, csv, xlsx)
    """
    # Generate and return the report
    report_url = await generate_report(format)
    
    return [TextContent(
        type="text",
        text=f"Report generated: {report_url}"
    )]
```

### Step 3: Deploy and Test

1. Deploy your MCP server using MCP Studio
2. Open DarcyIQ Chat
3. Ask the AI to "show me the sales dashboard"
4. Interact with the dashboard—buttons will call back to your tools!

## MCP-UI Callbacks

The most powerful feature of MCP Apps is the ability to call back to your MCP server from the UI.

### How Callbacks Work

1. User clicks a button or triggers an action in your UI
2. Your JavaScript sends a `postMessage` to the parent window
3. DarcyIQ intercepts the message and validates it
4. The tool is invoked on your MCP server
5. The result is sent back to your iframe
6. Your UI updates based on the result

### Callback Message Format

```javascript
window.parent.postMessage({
    type: 'tool',                    // Must be 'tool' for tool invocations
    payload: {
        toolName: 'my_tool_name',    // Name of the tool to call
        params: {                     // Parameters to pass
            key: 'value',
            another: 123
        }
    }
}, '*');
```

### Receiving Callback Results

Your UI can listen for results:

```javascript
// Listen for tool results
window.addEventListener('message', function(event) {
    // Check if this is a tool result
    if (event.data && event.data.success !== undefined) {
        if (event.data.success) {
            console.log('Tool result:', event.data.result);
            
            // If the tool returned new UI, it will automatically render
            // But you can also update your current UI based on result data
            updateDashboard(event.data.result);
        } else {
            console.error('Tool error:', event.data.error);
            showError(event.data.error);
        }
    }
});

function updateDashboard(data) {
    document.getElementById('revenue').textContent = '$' + data.revenue;
    document.getElementById('customers').textContent = data.customers;
}

function showError(message) {
    alert('Error: ' + message);
}
```

### Dynamic UI Updates

When a callback tool returns a new `ui` resource, DarcyIQ automatically replaces the current UI:

```python
@app.tool()
async def navigate_to_details(customer_id: str) -> list[TextContent]:
    """Navigate to customer details view."""
    customer = await get_customer(customer_id)
    
    details_html = f'''
    <html>
    <body>
        <h1>{customer['name']}</h1>
        <p>Email: {customer['email']}</p>
        <button onclick="goBack()">Back to List</button>
        
        <script>
            function goBack() {{
                window.parent.postMessage({{
                    type: 'tool',
                    payload: {{ toolName: 'show_customer_list', params: {{}} }}
                }}, '*');
            }}
        </script>
    </body>
    </html>
    '''
    
    return [TextContent(type="text", text=json.dumps({
        "ui": {
            "uri": f"app://customer/{customer_id}",
            "name": f"Customer: {customer['name']}",
            "mimeType": "text/html",
            "content": details_html
        }
    }))]
```

## Best Practices

### Security

| Practice                         | Why                                  |
| -------------------------------- | ------------------------------------ |
| **Sanitize inputs**              | Prevent XSS attacks in your HTML     |
| **Validate callback params**     | Don't trust data from the UI blindly |
| **Use HTTPS resources**          | External assets must be HTTPS        |
| **Avoid sensitive data in HTML** | UI content may be cached             |

{% hint style="warning" %}
**Sandbox Security**: MCP Apps run in a sandboxed iframe with restricted permissions. Some browser APIs may not be available.
{% endhint %}

### Performance

| Practice                 | Description                                |
| ------------------------ | ------------------------------------------ |
| **Inline CSS/JS**        | Avoid external dependencies when possible  |
| **Minimize HTML size**   | Large UIs take longer to render            |
| **Lazy load data**       | Fetch data via callbacks, not initial load |
| **Cache static content** | Reuse UI components across calls           |

### User Experience

| Practice               | Description                                 |
| ---------------------- | ------------------------------------------- |
| **Loading states**     | Show spinners during callback operations    |
| **Error handling**     | Display friendly error messages             |
| **Responsive design**  | UIs should work at different sizes          |
| **Fullscreen support** | Design for both inline and fullscreen modes |

### Example: Loading State

```javascript
async function fetchData() {
    // Show loading
    document.getElementById('content').innerHTML = '<p>Loading...</p>';
    
    // Make the callback
    window.parent.postMessage({
        type: 'tool',
        payload: { toolName: 'get_data', params: {} }
    }, '*');
}

// Handle result
window.addEventListener('message', function(event) {
    if (event.data && event.data.success !== undefined) {
        if (event.data.success) {
            document.getElementById('content').innerHTML = 
                formatData(event.data.result);
        } else {
            document.getElementById('content').innerHTML = 
                '<p class="error">Error: ' + event.data.error + '</p>';
        }
    }
});
```

## Example MCP Apps

### Interactive Data Table

```python
@app.tool()
async def show_customer_table() -> list[TextContent]:
    """Display an interactive customer data table."""
    
    customers = await fetch_customers()
    
    rows_html = "".join([
        f'''<tr>
            <td>{c['name']}</td>
            <td>{c['email']}</td>
            <td>{c['status']}</td>
            <td>
                <button onclick="viewCustomer('{c['id']}')">View</button>
                <button onclick="editCustomer('{c['id']}')">Edit</button>
            </td>
        </tr>'''
        for c in customers
    ])
    
    html = f'''
    <html>
    <head>
        <style>
            table {{ width: 100%; border-collapse: collapse; }}
            th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
            th {{ background: #f5f5f5; }}
            button {{ margin-right: 8px; }}
        </style>
    </head>
    <body>
        <h2>Customer List</h2>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                    <th>Status</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>{rows_html}</tbody>
        </table>
        
        <script>
            function viewCustomer(id) {{
                window.parent.postMessage({{
                    type: 'tool',
                    payload: {{ toolName: 'view_customer', params: {{ customer_id: id }} }}
                }}, '*');
            }}
            
            function editCustomer(id) {{
                window.parent.postMessage({{
                    type: 'tool',
                    payload: {{ toolName: 'edit_customer_form', params: {{ customer_id: id }} }}
                }}, '*');
            }}
        </script>
    </body>
    </html>
    '''
    
    return [TextContent(type="text", text=json.dumps({
        "ui": {
            "uri": "app://customers",
            "name": "Customer List",
            "mimeType": "text/html",
            "content": html
        }
    }))]
```

### Form with Validation

```python
@app.tool()
async def show_contact_form() -> list[TextContent]:
    """Display a contact form with client-side validation."""
    
    html = '''
    <html>
    <head>
        <style>
            form { max-width: 400px; }
            label { display: block; margin-top: 16px; font-weight: 500; }
            input, textarea { 
                width: 100%; padding: 8px; margin-top: 4px;
                border: 1px solid #ddd; border-radius: 4px;
            }
            input:invalid { border-color: #e74c3c; }
            button { 
                margin-top: 20px; padding: 12px 24px;
                background: #3498db; color: white; border: none;
                border-radius: 4px; cursor: pointer;
            }
            .error { color: #e74c3c; font-size: 12px; }
        </style>
    </head>
    <body>
        <h2>Contact Us</h2>
        <form id="contactForm" onsubmit="submitForm(event)">
            <label>Name *</label>
            <input type="text" id="name" required>
            
            <label>Email *</label>
            <input type="email" id="email" required>
            
            <label>Message *</label>
            <textarea id="message" rows="4" required></textarea>
            
            <button type="submit">Send Message</button>
        </form>
        
        <div id="status"></div>
        
        <script>
            function submitForm(e) {
                e.preventDefault();
                
                const data = {
                    name: document.getElementById('name').value,
                    email: document.getElementById('email').value,
                    message: document.getElementById('message').value
                };
                
                document.getElementById('status').textContent = 'Sending...';
                
                window.parent.postMessage({
                    type: 'tool',
                    payload: { 
                        toolName: 'submit_contact_form', 
                        params: data 
                    }
                }, '*');
            }
            
            window.addEventListener('message', function(event) {
                if (event.data && event.data.success !== undefined) {
                    if (event.data.success) {
                        document.getElementById('status').innerHTML = 
                            '<p style="color:green">Message sent successfully!</p>';
                        document.getElementById('contactForm').reset();
                    } else {
                        document.getElementById('status').innerHTML = 
                            '<p class="error">Error: ' + event.data.error + '</p>';
                    }
                }
            });
        </script>
    </body>
    </html>
    '''
    
    return [TextContent(type="text", text=json.dumps({
        "ui": {
            "uri": "app://contact-form",
            "name": "Contact Form",
            "mimeType": "text/html",
            "content": html
        }
    }))]
```

## Displaying MCP Apps

MCP Apps can be displayed in multiple contexts within DarcyIQ:

| Context            | Description                                | Size                |
| ------------------ | ------------------------------------------ | ------------------- |
| **Chat Inline**    | Embedded directly in the chat conversation | Compact, expandable |
| **Artifact Panel** | Full artifact view alongside chat          | Large, side panel   |
| **Fullscreen**     | Maximized view (click expand button)       | Full browser window |

### Fullscreen Mode

Users can click the expand button to view your MCP App in fullscreen mode. Design your UI to take advantage of the extra space:

```css
/* Responsive design for fullscreen */
@media (min-height: 600px) {
    .dashboard {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 24px;
    }
}

@media (max-height: 599px) {
    .dashboard {
        display: block;
    }
}
```

## Limitations

| Limitation                     | Details                                    |
| ------------------------------ | ------------------------------------------ |
| **No localStorage**            | Sandbox prevents persistent storage        |
| **No cookies**                 | Third-party cookies blocked                |
| **Limited APIs**               | Some browser APIs restricted in sandbox    |
| **Same-origin callbacks only** | Can only call tools on the same MCP server |
| **No external fetch**          | Cannot make HTTP requests from iframe      |

{% hint style="info" %}
**Working Around Limitations**: Use tool callbacks to fetch external data. Your MCP server can make HTTP requests and return the data to your UI.
{% endhint %}

## Next Steps

| Goal                   | Documentation                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| Build custom MCPs      | [Building Custom MCPs](/integrations-and-configuration/mcp-studio/building-custom-mcps.md) |
| View more code samples | [Code Samples](/integrations-and-configuration/mcp-studio/mcp-studio-samples.md)           |
| Deploy your app        | [Deploying & Managing](/integrations-and-configuration/mcp-studio/deploying-mcps.md)       |
| Troubleshoot issues    | [Troubleshooting](/integrations-and-configuration/mcp-studio/troubleshooting.md)           |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.darcyiq.com/integrations-and-configuration/mcp-studio/mcp-apps.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
