Conversation
- 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.
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsShould a new version be published when this PR is merged? React with an emoji to vote on the release type:
Current version: Deployment
|
There was a problem hiding this comment.
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.
apps/mesh/src/web/components/chat/message/parts/mcp-app-loader.tsx
Outdated
Show resolved
Hide resolved
apps/mesh/src/web/components/details/connection/resources-tab.tsx
Outdated
Show resolved
Hide resolved
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(() => { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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(); } |
There was a problem hiding this comment.
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>
| if (input.seconds) { seconds = input.seconds; initialSeconds = input.seconds; update(); } | |
| if (input.seconds !== undefined) { seconds = input.seconds; initialSeconds = input.seconds; update(); } |
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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>
https://www.loom.com/share/48f515311c1948528328721aab0bd923
CleanShot.2026-01-24.at.22.40.57.mp4
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
Bug Fixes
Written for commit 98a2748. Summary will update on new commits.