diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx index 7d14b144..04598c73 100644 --- a/examples/basic-server-react/src/mcp-app.tsx +++ b/examples/basic-server-react/src/mcp-app.tsx @@ -1,7 +1,7 @@ /** * @file App that demonstrates a few features using MCP Apps SDK + React. */ -import type { App } from "@modelcontextprotocol/ext-apps"; +import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { StrictMode, useCallback, useEffect, useState } from "react"; @@ -27,6 +27,7 @@ function extractTime(callToolResult: CallToolResult): string { function GetTimeApp() { const [toolResult, setToolResult] = useState(null); + const [hostContext, setHostContext] = useState(); const { app, error } = useApp({ appInfo: IMPLEMENTATION, capabilities: {}, @@ -45,21 +46,32 @@ function GetTimeApp() { }; app.onerror = log.error; + + app.onhostcontextchanged = (params) => { + setHostContext((prev) => ({ ...prev, ...params })); + }; }, }); + useEffect(() => { + if (app) { + setHostContext(app.getHostContext()); + } + }, [app]); + if (error) return
ERROR: {error.message}
; if (!app) return
Connecting...
; - return ; + return ; } interface GetTimeAppInnerProps { app: App; toolResult: CallToolResult | null; + hostContext?: McpUiHostContext; } -function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) { +function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) { const [serverTime, setServerTime] = useState("Loading..."); const [messageText, setMessageText] = useState("This is message text."); const [logText, setLogText] = useState("This is log text."); @@ -109,7 +121,15 @@ function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) { }, [app, linkUrl]); return ( -
+

Watch activity in the DevTools console!

diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 7acae864..59e71282 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -1,7 +1,7 @@ /** * @file App that demonstrates a few features using MCP Apps SDK with vanilla JS. */ -import { App } from "@modelcontextprotocol/ext-apps"; +import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import "./global.css"; import "./mcp-app.css"; @@ -21,6 +21,7 @@ function extractTime(result: CallToolResult): string { // Get element references +const mainEl = document.querySelector(".main") as HTMLElement; const serverTimeEl = document.getElementById("server-time")!; const getTimeBtn = document.getElementById("get-time-btn")!; const messageText = document.getElementById("message-text") as HTMLTextAreaElement; @@ -30,6 +31,15 @@ const sendLogBtn = document.getElementById("send-log-btn")!; const linkUrl = document.getElementById("link-url") as HTMLInputElement; const openLinkBtn = document.getElementById("open-link-btn")!; +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.safeAreaInsets) { + mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } +} + // Create app instance const app = new App({ name: "Get Time App", version: "1.0.0" }); @@ -51,6 +61,8 @@ app.ontoolresult = (result) => { app.onerror = log.error; +app.onhostcontextchanged = handleHostContextChanged; + // Add event listeners getTimeBtn.addEventListener("click", async () => { @@ -92,4 +104,9 @@ openLinkBtn.addEventListener("click", async () => { // Connect to host -app.connect(); +app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } +}); diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index 9cddc8ad..4510a854 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -1,7 +1,7 @@ /** * Budget Allocator App - Interactive budget allocation with real-time visualization */ -import { App } from "@modelcontextprotocol/ext-apps"; +import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { Chart, registerables } from "chart.js"; import "./global.css"; import "./mcp-app.css"; @@ -87,6 +87,7 @@ const state: AppState = { // DOM References // --------------------------------------------------------------------------- +const appContainer = document.querySelector(".app-container") as HTMLElement; const budgetSelector = document.getElementById( "budget-selector", ) as HTMLSelectElement; @@ -620,6 +621,17 @@ app.ontoolresult = (result) => { app.onerror = log.error; +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.safeAreaInsets) { + appContainer.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + appContainer.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + appContainer.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + appContainer.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } +} + +app.onhostcontextchanged = handleHostContextChanged; + // Handle theme changes window .matchMedia("(prefers-color-scheme: dark)") @@ -631,4 +643,9 @@ window }); // Connect to host -app.connect(); +app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } +}); diff --git a/examples/cohort-heatmap-server/src/mcp-app.tsx b/examples/cohort-heatmap-server/src/mcp-app.tsx index 42270a99..ce5c3e1f 100644 --- a/examples/cohort-heatmap-server/src/mcp-app.tsx +++ b/examples/cohort-heatmap-server/src/mcp-app.tsx @@ -4,7 +4,7 @@ * Interactive cohort retention analysis heatmap showing customer retention * over time by signup month. Hover for details, click to drill down. */ -import type { App } from "@modelcontextprotocol/ext-apps"; +import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import { StrictMode, useCallback, useEffect, useMemo, useState } from "react"; import { createRoot } from "react-dom/client"; @@ -65,19 +65,39 @@ function formatNumber(n: number): string { // Main App Component function CohortHeatmapApp() { + const [hostContext, setHostContext] = useState< + McpUiHostContext | undefined + >(); const { app, error } = useApp({ appInfo: IMPLEMENTATION, capabilities: {}, + onAppCreated: (app) => { + app.onhostcontextchanged = (params) => { + setHostContext((prev) => ({ ...prev, ...params })); + }; + }, }); + useEffect(() => { + if (app) { + setHostContext(app.getHostContext()); + } + }, [app]); + if (error) return
ERROR: {error.message}
; if (!app) return
Connecting...
; - return ; + return ; } // Inner App with state management -function CohortHeatmapInner({ app }: { app: App }) { +function CohortHeatmapInner({ + app, + hostContext, +}: { + app: App; + hostContext?: McpUiHostContext; +}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [selectedMetric, setSelectedMetric] = useState("retention"); @@ -143,7 +163,15 @@ function CohortHeatmapInner({ app }: { app: App }) { }, []); return ( -
+
{ - if (params.theme) { - applyDocumentTheme(params.theme); +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); } - if (params.styles?.variables) { - applyHostStyleVariables(params.styles.variables); + if (ctx.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); } - if (params.styles?.css?.fonts) { - applyHostFonts(params.styles.css.fonts); + if (ctx.safeAreaInsets) { + mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; } // Recreate chart to pick up new colors - if (state.chart && (params.theme || params.styles?.variables)) { + if (state.chart && (ctx.theme || ctx.styles?.variables)) { state.chart.destroy(); state.chart = initChart(); } -}; +} + +app.onhostcontextchanged = handleHostContextChanged; app.connect().then(() => { - // Apply initial host context after connection const ctx = app.getHostContext(); - if (ctx?.theme) { - applyDocumentTheme(ctx.theme); - } - if (ctx?.styles?.variables) { - applyHostStyleVariables(ctx.styles.variables); - } - if (ctx?.styles?.css?.fonts) { - applyHostFonts(ctx.styles.css.fonts); + if (ctx) { + handleHostContextChanged(ctx); } }); diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx index 926696c6..fadffdbb 100644 --- a/examples/integration-server/src/mcp-app.tsx +++ b/examples/integration-server/src/mcp-app.tsx @@ -1,7 +1,7 @@ /** * @file App that demonstrates a few features using MCP Apps SDK + React. */ -import type { App } from "@modelcontextprotocol/ext-apps"; +import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { StrictMode, useCallback, useEffect, useState } from "react"; @@ -29,6 +29,9 @@ function extractTime(callToolResult: CallToolResult): string { function GetTimeApp() { const [toolResult, setToolResult] = useState(null); + const [hostContext, setHostContext] = useState< + McpUiHostContext | undefined + >(); const { app, error } = useApp({ appInfo: IMPLEMENTATION, capabilities: {}, @@ -49,9 +52,19 @@ function GetTimeApp() { }; app.onerror = log.error; + + app.onhostcontextchanged = (params) => { + setHostContext((prev) => ({ ...prev, ...params })); + }; }, }); + useEffect(() => { + if (app) { + setHostContext(app.getHostContext()); + } + }, [app]); + if (error) return (
@@ -60,14 +73,25 @@ function GetTimeApp() { ); if (!app) return
Connecting...
; - return ; + return ( + + ); } interface GetTimeAppInnerProps { app: App; toolResult: CallToolResult | null; + hostContext?: McpUiHostContext; } -function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) { +function GetTimeAppInner({ + app, + toolResult, + hostContext, +}: GetTimeAppInnerProps) { const [serverTime, setServerTime] = useState("Loading..."); const [messageText, setMessageText] = useState("This is message text."); const [logText, setLogText] = useState("This is log text."); @@ -120,7 +144,15 @@ function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) { }, [app, linkUrl]); return ( -
+

Watch activity in the DevTools console!

diff --git a/examples/qr-server/widget.html b/examples/qr-server/widget.html index 9275ff68..e5a95ffd 100644 --- a/examples/qr-server/widget.html +++ b/examples/qr-server/widget.html @@ -47,7 +47,22 @@ } }; + function handleHostContextChanged(ctx) { + if (ctx.safeAreaInsets) { + document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + document.body.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + document.body.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } + } + + app.onhostcontextchanged = handleHostContextChanged; + await app.connect(); + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } diff --git a/examples/scenario-modeler-server/src/mcp-app.tsx b/examples/scenario-modeler-server/src/mcp-app.tsx index 9fa411f3..e436e5a8 100644 --- a/examples/scenario-modeler-server/src/mcp-app.tsx +++ b/examples/scenario-modeler-server/src/mcp-app.tsx @@ -1,6 +1,7 @@ +import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { StrictMode, useState, useMemo, useCallback } from "react"; +import { StrictMode, useState, useMemo, useCallback, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { SliderRow } from "./components/SliderRow.tsx"; import { MetricCard } from "./components/MetricCard.tsx"; @@ -38,6 +39,15 @@ function extractResultData(result: CallToolResult): CallToolResultData { const APP_INFO = { name: "SaaS Scenario Modeler", version: "1.0.0" }; +function getSafeAreaPaddingStyle(hostContext?: McpUiHostContext) { + return { + paddingTop: hostContext?.safeAreaInsets?.top, + paddingRight: hostContext?.safeAreaInsets?.right, + paddingBottom: hostContext?.safeAreaInsets?.bottom, + paddingLeft: hostContext?.safeAreaInsets?.left, + }; +} + // Local defaults for immediate render (should match server's DEFAULT_INPUTS) const FALLBACK_INPUTS: ScenarioInputs = { startingMRR: 50000, @@ -51,6 +61,9 @@ function ScenarioModeler() { const [templates, setTemplates] = useState([]); const [defaultInputs, setDefaultInputs] = useState(FALLBACK_INPUTS); + const [hostContext, setHostContext] = useState< + McpUiHostContext | undefined + >(); const { app, error } = useApp({ appInfo: APP_INFO, @@ -61,12 +74,21 @@ function ScenarioModeler() { if (templates) setTemplates(templates); if (defaultInputs) setDefaultInputs(defaultInputs); }; + app.onhostcontextchanged = (params) => { + setHostContext((prev) => ({ ...prev, ...params })); + }; }, }); + useEffect(() => { + if (app) { + setHostContext(app.getHostContext()); + } + }, [app]); + if (error) { return ( -
+
Error: {error.message}
); @@ -74,25 +96,31 @@ function ScenarioModeler() { if (!app) { return ( -
+
Connecting...
); } return ( - + ); } interface ScenarioModelerInnerProps { templates: ScenarioTemplate[]; defaultInputs: ScenarioInputs; + hostContext?: McpUiHostContext; } function ScenarioModelerInner({ templates, defaultInputs, + hostContext, }: ScenarioModelerInnerProps) { const [inputs, setInputs] = useState(FALLBACK_INPUTS); const [selectedTemplateId, setSelectedTemplateId] = useState( @@ -141,7 +169,7 @@ function ScenarioModelerInner({ }, [selectedTemplate]); return ( -
+
{/* Header */}

SaaS Scenario Modeler

diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 1dc7ff2f..39730734 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -1,7 +1,7 @@ /** * @file System Monitor App - displays real-time OS metrics with Chart.js */ -import { App } from "@modelcontextprotocol/ext-apps"; +import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { Chart, registerables } from "chart.js"; import "./global.css"; import "./mcp-app.css"; @@ -40,6 +40,7 @@ interface SystemStats { } // DOM element references +const mainEl = document.querySelector(".main") as HTMLElement; const pollToggleBtn = document.getElementById("poll-toggle-btn")!; const statusIndicator = document.getElementById("status-indicator")!; const statusText = document.getElementById("status-text")!; @@ -360,7 +361,23 @@ window // Register handlers and connect app.onerror = log.error; -app.connect(); +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.safeAreaInsets) { + mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } +} + +app.onhostcontextchanged = handleHostContextChanged; + +app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } +}); // Auto-start polling after a short delay setTimeout(startPolling, 500); diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 42df7527..beef4d8b 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -7,7 +7,7 @@ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { StrictMode, useState, useCallback } from "react"; +import { StrictMode, useState, useCallback, useEffect } from "react"; import { createRoot } from "react-dom/client"; import ThreeJSApp from "./threejs-app.tsx"; import "./global.css"; @@ -67,11 +67,21 @@ function McpAppWrapper() { }; // Host context changes (theme, dimensions, etc.) app.onhostcontextchanged = (params) => { - setHostContext(params); + setHostContext((prev) => ({ ...prev, ...params })); }; }, }); + // Get initial host context after connection + useEffect(() => { + if (app) { + const ctx = app.getHostContext(); + if (ctx) { + setHostContext(ctx); + } + } + }, [app]); + // Memoized callbacks that forward to app methods const callServerTool = useCallback( (params, options) => app!.callServerTool(params, options), diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 8851f881..571364e2 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -140,7 +140,7 @@ export default function ThreeJSApp({ toolInputs, toolInputsPartial, toolResult: _toolResult, - hostContext: _hostContext, + hostContext, callServerTool: _callServerTool, sendMessage: _sendMessage, openLink: _openLink, @@ -155,6 +155,14 @@ export default function ThreeJSApp({ const partialCode = toolInputsPartial?.code; const isStreaming = !toolInputs && !!toolInputsPartial; + const safeAreaInsets = hostContext?.safeAreaInsets; + const containerStyle = { + paddingTop: safeAreaInsets?.top, + paddingRight: safeAreaInsets?.right, + paddingBottom: safeAreaInsets?.bottom, + paddingLeft: safeAreaInsets?.left, + }; + useEffect(() => { if (!code || !canvasRef.current || !containerRef.current) return; @@ -166,11 +174,19 @@ export default function ThreeJSApp({ }, [code, height]); if (isStreaming || !code) { - return ; + return ( +
+ +
+ ); } return ( -
+
{ console.error("[Wiki Explorer] App error:", err); }; +function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.safeAreaInsets) { + document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + document.body.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + document.body.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } +} + +app.onhostcontextchanged = handleHostContextChanged; + // Connect to host -app.connect(); +app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } +});