)}
- {!isTodo &&
+ {!mcpApp &&
+ !isTodo &&
!hideCollapsedOutput &&
result &&
!showExpandedSummaryPanel &&
@@ -1490,7 +1500,7 @@ export const ToolLine = memo(function ToolLine({
{renderWithSessionLinks(result, transcriptRenderMode)}
)}
- {!isTodo && expanded && detailView && (
+ {!mcpApp && !isTodo && expanded && detailView && (
getMcpAppDisplay(tool.rawOutput));
const hasForegroundActiveTool = tools.some(
(tool) =>
isActiveToolStatus(tool.status) && !isBackgroundSubAgentToolCall(tool),
@@ -1682,6 +1697,11 @@ export const ToolGroup = memo(function ToolGroup({
setChatExpanded(false);
monitorDetailsRequestRef.current = null;
}, [monitorDetailsAvailable, singleMonitor?.callId]);
+ useEffect(() => {
+ if (singleMcpAppResourceUri || hasMcpApp) {
+ setChatExpanded(true);
+ }
+ }, [singleMcpAppResourceUri, hasMcpApp]);
const tryOpenMonitorDetails = () => {
if (!singleMonitor || !monitorDetails) return;
diff --git a/packages/web-shell/client/mcpAppHostContext.ts b/packages/web-shell/client/mcpAppHostContext.ts
new file mode 100644
index 00000000000..aaf08912f39
--- /dev/null
+++ b/packages/web-shell/client/mcpAppHostContext.ts
@@ -0,0 +1,3 @@
+import { createContext } from 'react';
+
+export const McpAppHostContext = createContext(undefined);
diff --git a/packages/web-shell/client/vite-config.test.ts b/packages/web-shell/client/vite-config.test.ts
index 133424e1db6..2f57a7f941a 100644
--- a/packages/web-shell/client/vite-config.test.ts
+++ b/packages/web-shell/client/vite-config.test.ts
@@ -40,6 +40,15 @@ describe('Web Shell Voice development proxy', () => {
});
});
+describe('Web Shell MCP App development proxy', () => {
+ it('proxies the sandbox document to the daemon', () => {
+ const sandboxProxy = loadConfig().server?.proxy?.['/mcp-app-sandbox'];
+ expect(sandboxProxy).not.toBeTypeOf('string');
+ expect(sandboxProxy).toBeDefined();
+ expect((sandboxProxy as ProxyOptions).bypass).toBeUndefined();
+ });
+});
+
describe('Web Shell client source proxy bypass', () => {
it('serves session catalog source modules instead of proxying them', () => {
const sessionProxy = loadConfig().server?.proxy?.['/session'];
diff --git a/packages/web-shell/package.json b/packages/web-shell/package.json
index 58982f07892..3fd3303695c 100644
--- a/packages/web-shell/package.json
+++ b/packages/web-shell/package.json
@@ -42,6 +42,7 @@
"@datafe-open/markdown-chart": "^0.1.12",
"@datafe-open/markdown-chart-echarts": "^0.1.12",
"@datafe-open/markdown-chart-react": "^0.1.12",
+ "@modelcontextprotocol/ext-apps": "^1.7.5",
"@tanstack/react-virtual": "^3.13.26",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/packages/web-shell/vite.config.ts b/packages/web-shell/vite.config.ts
index 9f018324e40..11316bf43e8 100644
--- a/packages/web-shell/vite.config.ts
+++ b/packages/web-shell/vite.config.ts
@@ -85,6 +85,7 @@ export default defineConfig(({ command }) => ({
proxy: {
'/health': daemonProxy,
'/capabilities': daemonProxy,
+ '/mcp-app-sandbox': { ...daemonProxy, bypass: undefined },
// Daemon status report; scoped to the exact route the dashboard uses (a
// bare `/daemon` prefix would proxy unrelated `/daemon/*` paths). Without
// it the SPA fallback answers with index.html and the dialog fails JSON