Skip to content

Feat/mcp apps spec support#2315

Open
vibegui wants to merge 13 commits intomainfrom
feat/mcp-apps-spec-support
Open

Feat/mcp apps spec support#2315
vibegui wants to merge 13 commits intomainfrom
feat/mcp-apps-spec-support

Conversation

@vibegui
Copy link
Contributor

@vibegui vibegui commented Jan 24, 2026

https://www.loom.com/share/48f515311c1948528328721aab0bd923

CleanShot.2026-01-24.at.22.40.57.mp4
CleanShot 2026-01-24 at 22 06 19 CleanShot 2026-01-24 at 18 30 47 CleanShot 2026-01-24 at 18 31 00 CleanShot 2026-01-24 at 18 31 34

Summary by cubic

Adds MCP Apps support so tools can render interactive UIs declared via ui:// resources. Chat now shows app UIs for tool results, and the Resources tab includes a UI Apps section with inline previews, all rendered securely in a sandboxed iframe.

  • New Features

    • Render MCP App UIs in chat when tool results include _meta["ui/resourceUri"], with expand/collapse and dynamic height.
    • Add a secure renderer (iframe + CSP), app model (JSON-RPC messaging), and a resource loader (dev mode disables cache for hot reload).
    • Resources tab: new UI Apps section and full inline preview of ui:// resources; added a preview dialog with example inputs.
    • Tool metadata and APIs now include uiResourceUri and connectionId so the UI can read resources and call tools.
    • Always include ui:// resources in both inclusion and exclusion selection modes.
    • Configuration added: MCP_APPS_ENABLED, display mode height bounds, and an option to show raw JSON output in developer mode.
    • Added a broad set of UI widget tools (e.g., counter, chart, table, metric, avatar, calendar) with registered ui:// resources served by the management MCP.
  • Bug Fixes

    • Fix race condition where iframe could load before the app model attached, and prevent UI flash during resource preview loading.
    • Make MCP client usage Suspense-safe by moving it into a dedicated MCPAppLoader component.
    • Add retry with exponential backoff for app initialization to handle transient load issues.
    • Fix back button navigation in the connection detail view.

Written for commit 98a2748. Summary will update on new commits.

- Inject connectionId into tool result _meta in ToolAggregator so
  frontend knows which connection to use for reading UI resources
- Pass _meta through toModelOutput in decopilot helpers so tool
  results include UI resource metadata
- Always include ui:// resources in ResourceAggregator regardless
  of selection mode (MCP Apps should always be accessible)
- Extract MCPAppLoader into separate component with proper Suspense
  handling for MCP client hooks
- Refactor ToolCallPart to use MCPAppLoader with correct hook usage
- Updated MCPAppRenderer to support dynamic height adjustments based on minHeight and maxHeight props.
- Introduced MCP_APP_DISPLAY_MODES for consistent display mode configurations across the application.
- Implemented expand/collapse functionality in MCPAppLoader for improved user experience.
- Added inline UI app preview in ResourcesTab to display apps directly within the connection view.
- Disabled caching in development mode for hot reloading of UI resources.
Add fallback initialization check for when iframe loads before
the model is attached, preventing apps from staying in loading state.
@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.44.4-alpha.1
🎉 Patch 2.44.4
❤️ Minor 2.45.0
🚀 Major 3.0.0

Current version: 2.44.3

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found across 18 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/mesh/src/mcp-apps/mcp-app-model.ts">

<violation number="1" location="apps/mesh/src/mcp-apps/mcp-app-model.ts:369">
P2: Add `noopener,noreferrer` when opening a new tab to prevent reverse tabnabbing.</violation>
</file>

<file name="apps/mesh/src/web/components/chat/message/parts/mcp-app-loader.tsx">

<violation number="1" location="apps/mesh/src/web/components/chat/message/parts/mcp-app-loader.tsx:134">
P2: Triggering `loadMCPApp()` during render causes side effects and can re-render in a loop (especially in Strict Mode). Move the load to a `useEffect` so state updates happen after render.</violation>
</file>

<file name="apps/mesh/src/mcp-apps/use-tool-ui-resource.ts">

<violation number="1" location="apps/mesh/src/mcp-apps/use-tool-ui-resource.ts:45">
P1: `useMCPClient` is a hook but it’s called conditionally inside an `if`/`try` block. Hooks must run unconditionally in the same order on every render; otherwise hook state can break when `virtualMcpId` or `org` changes.</violation>
</file>

<file name="apps/mesh/src/mcp-apps/csp-injector.ts">

<violation number="1" location="apps/mesh/src/mcp-apps/csp-injector.ts:13">
P3: The comment says `default-src 'self'`, but the actual default policy is `default-src 'none'`. Update the comment to reflect the real CSP to avoid confusion.</violation>

<violation number="2" location="apps/mesh/src/mcp-apps/csp-injector.ts:63">
P2: When allowExternalConnections is true and allowedHosts is an empty array, the CSP becomes `connect-src ` (invalid). Treat empty arrays the same as undefined to avoid generating an invalid directive.</violation>
</file>

<file name="apps/mesh/src/mcp-apps/app-preview-dialog.tsx">

<violation number="1" location="apps/mesh/src/mcp-apps/app-preview-dialog.tsx:98">
P2: Avoid triggering async state updates during render; move the `loadResource()` call into a `useEffect` tied to `open`/`uri` so state updates happen after render.</violation>
</file>

<file name="apps/mesh/src/core/constants.ts">

<violation number="1" location="apps/mesh/src/core/constants.ts:18">
P2: The comment states this experimental feature is disabled by default, but the flag is set to true. Set the default to false (or update the comment) to avoid inconsistent behavior.</violation>
</file>

<file name="apps/mesh/src/web/components/details/connection/resources-tab.tsx">

<violation number="1" location="apps/mesh/src/web/components/details/connection/resources-tab.tsx:338">
P2: Avoid invoking the async loadResource side-effect directly in the render path. This causes state updates during render and can lead to repeated renders. Trigger the initial load in a useEffect instead.</violation>
</file>

<file name="apps/mesh/src/mcp-apps/mcp-app-renderer.tsx">

<violation number="1" location="apps/mesh/src/mcp-apps/mcp-app-renderer.tsx:160">
P2: The interval created in the ref callback is never cleaned up because React ignores return values from ref callbacks. Use useEffect to manage the interval lifecycle or clear it in a proper cleanup path to avoid timer leaks.</violation>
</file>

<file name="apps/mesh/src/mcp-apps/resource-loader.ts">

<violation number="1" location="apps/mesh/src/mcp-apps/resource-loader.ts:185">
P1: Guard against `maxCacheSize <= 0` before the eviction loop; otherwise a configured size of 0 causes an infinite loop when caching.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

- Add 20 UI widget tools: counter, metric, progress, timer, chart,
  table, code, markdown, json-viewer, diff, confirmation, quote,
  greeting, todo, sparkline, form-result, image, notification, error
- Register UI resources with management MCP server
- Add retry mechanism with exponential backoff for MCP App init
- Use z.coerce.number() for form input compatibility
- Fix UI_METRIC output schema to include description field
- Pass example inputs for resource preview mode
Hide iframe while loading and use solid background overlay to prevent
visible content flash before initialization completes.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/mesh/src/mcp-apps/app-preview-dialog.tsx">

<violation number="1" location="apps/mesh/src/mcp-apps/app-preview-dialog.tsx:74">
P2: Avoid triggering state updates and async side effects during render. Move this resource-loading logic into a useEffect tied to `open`, `uri`, and the relevant state so React can schedule it safely.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Add 45 tests covering:
- CSP injector: default policy, HTML injection, external connections
- Types: constants, display modes, URI detection, metadata helpers
- Resource loader: loading, error handling, caching behavior
Move setLoading() calls to after render by wrapping in queueMicrotask
to avoid triggering state updates during React's render phase.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/mesh/src/mcp-apps/app-preview-dialog.tsx">

<violation number="1" location="apps/mesh/src/mcp-apps/app-preview-dialog.tsx:74">
P2: Avoid scheduling async side effects during render; move the load initiation into a useEffect so it only runs after commit and isn’t triggered on discarded renders.</violation>
</file>

<file name="apps/mesh/src/web/components/chat/message/parts/mcp-app-loader.tsx">

<violation number="1" location="apps/mesh/src/web/components/chat/message/parts/mcp-app-loader.tsx:120">
P2: Scheduling the load via `queueMicrotask` inside render is still a render-time side effect; in concurrent React, this can run after an aborted render/unmount and trigger state updates on an unmounted component. Move the load into a `useEffect` tied to `shouldLoad` so it only runs after commit and can be cleaned up.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if (shouldLoad) {
loadStartedRef.current = true;
// Defer state updates to after render using queueMicrotask
queueMicrotask(() => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Scheduling the load via queueMicrotask inside render is still a render-time side effect; in concurrent React, this can run after an aborted render/unmount and trigger state updates on an unmounted component. Move the load into a useEffect tied to shouldLoad so it only runs after commit and can be cleaned up.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/chat/message/parts/mcp-app-loader.tsx, line 120:

<comment>Scheduling the load via `queueMicrotask` inside render is still a render-time side effect; in concurrent React, this can run after an aborted render/unmount and trigger state updates on an unmounted component. Move the load into a `useEffect` tied to `shouldLoad` so it only runs after commit and can be cleaned up.</comment>

<file context>
@@ -111,27 +111,32 @@ export function MCPAppLoader({
-      }
-    })();
+    // Defer state updates to after render using queueMicrotask
+    queueMicrotask(() => {
+      setAppLoading(true);
+      (async () => {
</file context>
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/mesh/src/tools/ui-widgets/resources.ts">

<violation number="1" location="apps/mesh/src/tools/ui-widgets/resources.ts:370">
P2: The truthy check skips valid 0-second inputs, so re-initializing the timer with 0 won’t reset the countdown. Use an explicit undefined/null check so 0 is honored.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

let msg; try { msg = JSON.parse(e.data); } catch { return; }
if (msg.method === 'ui/initialize') {
const input = msg.params?.toolInput || {};
if (input.seconds) { seconds = input.seconds; initialSeconds = input.seconds; update(); }
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The truthy check skips valid 0-second inputs, so re-initializing the timer with 0 won’t reset the countdown. Use an explicit undefined/null check so 0 is honored.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/tools/ui-widgets/resources.ts, line 370:

<comment>The truthy check skips valid 0-second inputs, so re-initializing the timer with 0 won’t reset the countdown. Use an explicit undefined/null check so 0 is honored.</comment>

<file context>
@@ -346,16 +347,27 @@ export const UI_WIDGET_RESOURCES: Record<string, UIWidgetResource> = {
       if (msg.method === 'ui/initialize') {
         const input = msg.params?.toolInput || {};
-        if (input.seconds) { seconds = input.seconds; update(); }
+        if (input.seconds) { seconds = input.seconds; initialSeconds = input.seconds; update(); }
         parent.postMessage(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: {} }), '*');
       }
</file context>
Suggested change
if (input.seconds) { seconds = input.seconds; initialSeconds = input.seconds; update(); }
if (input.seconds !== undefined) { seconds = input.seconds; initialSeconds = input.seconds; update(); }
Fix with Cubic

New widgets:
- Avatar: User display with image, fallback initials, status indicator
- Badge: Multiple variants (default, secondary, destructive, outline, success, warning)
- Skeleton: Animated loading placeholder (card, list, text variants)
- Switch: Toggle switch with label and description
- Slider: Range input with real-time value display
- Rating: Star rating display with review count
- Kbd: Keyboard shortcuts display
- Stats Grid: Dashboard-style grid of metric cards
- Area Chart: Beautiful area chart with gradient fill and tabs
- Calendar: Mini calendar with selected dates

Improvements:
- Metric: Upgraded to shadcn dashboard style with trend badge
- Table: Added hover states, rounded borders, row count badge
- Chart: Fixed bars growing upward from bottom (was inverted)
- Connection detail: Fixed back button navigation (was affected by tab history)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 14 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/mesh/src/tools/ui-widgets/calendar.ts">

<violation number="1" location="apps/mesh/src/tools/ui-widgets/calendar.ts:12">
P2: Calendar inputs should be constrained to integers; fractional month/day values pass validation and break the calendar renderer (e.g., `months[month - 1]` becomes undefined).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

- Introduced LoadTrigger component to handle loading on mount, avoiding render-time side effects.
- Simplified resource loading logic by consolidating state updates and error handling.
- Enhanced state management for dialog open/close events to ensure proper resource loading and cleanup.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/mesh/src/mcp-apps/app-preview-dialog.tsx">

<violation number="1" location="apps/mesh/src/mcp-apps/app-preview-dialog.tsx:64">
P2: Avoid render-time side effects in LoadTrigger. Mutating the ref and queueing a microtask during render can fire onLoad for renders that never commit under concurrent rendering. Move the trigger to useEffect so it only runs after the component mounts.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

*/
function LoadTrigger({ onLoad }: { onLoad: () => void }) {
const loadedRef = useRef(false);
if (!loadedRef.current) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid render-time side effects in LoadTrigger. Mutating the ref and queueing a microtask during render can fire onLoad for renders that never commit under concurrent rendering. Move the trigger to useEffect so it only runs after the component mounts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/mcp-apps/app-preview-dialog.tsx, line 64:

<comment>Avoid render-time side effects in LoadTrigger. Mutating the ref and queueing a microtask during render can fire onLoad for renders that never commit under concurrent rendering. Move the trigger to useEffect so it only runs after the component mounts.</comment>

<file context>
@@ -56,6 +56,18 @@ export interface AppPreviewDialogProps {
+ */
+function LoadTrigger({ onLoad }: { onLoad: () => void }) {
+  const loadedRef = useRef(false);
+  if (!loadedRef.current) {
+    loadedRef.current = true;
+    queueMicrotask(onLoad);
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant