Add safeAreaInsets support to all example apps (#202)

Apply host-provided safe area insets as padding on main containers
across all examples. This ensures content doesn't render under
system UI elements like phone notches.

React apps use `useState` + `onhostcontextchanged` handler with
initial fetch via `getHostContext()` in `useEffect`. Vanilla JS
apps apply padding in the handler and invoke it after `connect()`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Hefner 2026-01-08 12:03:13 -06:00 • committed by GitHub
commit 79670b4dcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 267 additions and 48 deletions

View file

@ -1,7 +1,7 @@
/** /**
* @file App that demonstrates a few features using MCP Apps SDK + React. * @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 { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useCallback, useEffect, useState } from "react"; import { StrictMode, useCallback, useEffect, useState } from "react";
@ -27,6 +27,7 @@ function extractTime(callToolResult: CallToolResult): string {
function GetTimeApp() { function GetTimeApp() {
const [toolResult, setToolResult] = useState<CallToolResult | null>(null); const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
const { app, error } = useApp({ const { app, error } = useApp({
appInfo: IMPLEMENTATION, appInfo: IMPLEMENTATION,
capabilities: {}, capabilities: {},
@ -45,21 +46,32 @@ function GetTimeApp() {
}; };
app.onerror = log.error; app.onerror = log.error;
app.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
};
}, },
}); });
useEffect(() => {
if (app) {
setHostContext(app.getHostContext());
}
}, [app]);
if (error) return <div><strong>ERROR:</strong> {error.message}</div>; if (error) return <div><strong>ERROR:</strong> {error.message}</div>;
if (!app) return <div>Connecting...</div>; if (!app) return <div>Connecting...</div>;
return <GetTimeAppInner app={app} toolResult={toolResult} />; return <GetTimeAppInner app={app} toolResult={toolResult} hostContext={hostContext} />;
} }
interface GetTimeAppInnerProps { interface GetTimeAppInnerProps {
app: App; app: App;
toolResult: CallToolResult | null; toolResult: CallToolResult | null;
hostContext?: McpUiHostContext;
} }
function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) { function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) {
const [serverTime, setServerTime] = useState("Loading..."); const [serverTime, setServerTime] = useState("Loading...");
const [messageText, setMessageText] = useState("This is message text."); const [messageText, setMessageText] = useState("This is message text.");
const [logText, setLogText] = useState("This is log text."); const [logText, setLogText] = useState("This is log text.");
@ -109,7 +121,15 @@ function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) {
}, [app, linkUrl]); }, [app, linkUrl]);
return ( return (
<main className={styles.main}> <main
className={styles.main}
style={{
paddingTop: hostContext?.safeAreaInsets?.top,
paddingRight: hostContext?.safeAreaInsets?.right,
paddingBottom: hostContext?.safeAreaInsets?.bottom,
paddingLeft: hostContext?.safeAreaInsets?.left,
}}
>
<p className={styles.notice}>Watch activity in the DevTools console!</p> <p className={styles.notice}>Watch activity in the DevTools console!</p>
<div className={styles.action}> <div className={styles.action}>

View file

@ -1,7 +1,7 @@
/** /**
* @file App that demonstrates a few features using MCP Apps SDK with vanilla JS. * @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 type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import "./global.css"; import "./global.css";
import "./mcp-app.css"; import "./mcp-app.css";
@ -21,6 +21,7 @@ function extractTime(result: CallToolResult): string {
// Get element references // Get element references
const mainEl = document.querySelector(".main") as HTMLElement;
const serverTimeEl = document.getElementById("server-time")!; const serverTimeEl = document.getElementById("server-time")!;
const getTimeBtn = document.getElementById("get-time-btn")!; const getTimeBtn = document.getElementById("get-time-btn")!;
const messageText = document.getElementById("message-text") as HTMLTextAreaElement; 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 linkUrl = document.getElementById("link-url") as HTMLInputElement;
const openLinkBtn = document.getElementById("open-link-btn")!; 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 // Create app instance
const app = new App({ name: "Get Time App", version: "1.0.0" }); const app = new App({ name: "Get Time App", version: "1.0.0" });
@ -51,6 +61,8 @@ app.ontoolresult = (result) => {
app.onerror = log.error; app.onerror = log.error;
app.onhostcontextchanged = handleHostContextChanged;
// Add event listeners // Add event listeners
getTimeBtn.addEventListener("click", async () => { getTimeBtn.addEventListener("click", async () => {
@ -92,4 +104,9 @@ openLinkBtn.addEventListener("click", async () => {
// Connect to host // Connect to host
app.connect(); app.connect().then(() => {
const ctx = app.getHostContext();
if (ctx) {
handleHostContextChanged(ctx);
}
});

View file

@ -1,7 +1,7 @@
/** /**
* Budget Allocator App - Interactive budget allocation with real-time visualization * 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 { Chart, registerables } from "chart.js";
import "./global.css"; import "./global.css";
import "./mcp-app.css"; import "./mcp-app.css";
@ -87,6 +87,7 @@ const state: AppState = {
// DOM References // DOM References
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const appContainer = document.querySelector(".app-container") as HTMLElement;
const budgetSelector = document.getElementById( const budgetSelector = document.getElementById(
"budget-selector", "budget-selector",
) as HTMLSelectElement; ) as HTMLSelectElement;
@ -620,6 +621,17 @@ app.ontoolresult = (result) => {
app.onerror = log.error; 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 // Handle theme changes
window window
.matchMedia("(prefers-color-scheme: dark)") .matchMedia("(prefers-color-scheme: dark)")
@ -631,4 +643,9 @@ window
}); });
// Connect to host // Connect to host
app.connect(); app.connect().then(() => {
const ctx = app.getHostContext();
if (ctx) {
handleHostContextChanged(ctx);
}
});

View file

@ -4,7 +4,7 @@
* Interactive cohort retention analysis heatmap showing customer retention * Interactive cohort retention analysis heatmap showing customer retention
* over time by signup month. Hover for details, click to drill down. * 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 { useApp } from "@modelcontextprotocol/ext-apps/react";
import { StrictMode, useCallback, useEffect, useMemo, useState } from "react"; import { StrictMode, useCallback, useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
@ -65,19 +65,39 @@ function formatNumber(n: number): string {
// Main App Component // Main App Component
function CohortHeatmapApp() { function CohortHeatmapApp() {
const [hostContext, setHostContext] = useState<
McpUiHostContext | undefined
>();
const { app, error } = useApp({ const { app, error } = useApp({
appInfo: IMPLEMENTATION, appInfo: IMPLEMENTATION,
capabilities: {}, capabilities: {},
onAppCreated: (app) => {
app.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
};
},
}); });
useEffect(() => {
if (app) {
setHostContext(app.getHostContext());
}
}, [app]);
if (error) return <div className={styles.error}>ERROR: {error.message}</div>; if (error) return <div className={styles.error}>ERROR: {error.message}</div>;
if (!app) return <div className={styles.loading}>Connecting...</div>; if (!app) return <div className={styles.loading}>Connecting...</div>;
return <CohortHeatmapInner app={app} />; return <CohortHeatmapInner app={app} hostContext={hostContext} />;
} }
// Inner App with state management // Inner App with state management
function CohortHeatmapInner({ app }: { app: App }) { function CohortHeatmapInner({
app,
hostContext,
}: {
app: App;
hostContext?: McpUiHostContext;
}) {
const [data, setData] = useState<CohortData | null>(null); const [data, setData] = useState<CohortData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedMetric, setSelectedMetric] = useState<MetricType>("retention"); const [selectedMetric, setSelectedMetric] = useState<MetricType>("retention");
@ -143,7 +163,15 @@ function CohortHeatmapInner({ app }: { app: App }) {
}, []); }, []);
return ( return (
<main className={styles.container}> <main
className={styles.container}
style={{
paddingTop: hostContext?.safeAreaInsets?.top,
paddingRight: hostContext?.safeAreaInsets?.right,
paddingBottom: hostContext?.safeAreaInsets?.bottom,
paddingLeft: hostContext?.safeAreaInsets?.left,
}}
>
<Header <Header
selectedMetric={selectedMetric} selectedMetric={selectedMetric}
selectedPeriodType={selectedPeriodType} selectedPeriodType={selectedPeriodType}

View file

@ -6,6 +6,7 @@ import {
applyHostStyleVariables, applyHostStyleVariables,
applyHostFonts, applyHostFonts,
applyDocumentTheme, applyDocumentTheme,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps"; } from "@modelcontextprotocol/ext-apps";
import { Chart, registerables } from "chart.js"; import { Chart, registerables } from "chart.js";
import "./global.css"; import "./global.css";
@ -22,6 +23,7 @@ const log = {
}; };
// DOM element references // DOM element references
const mainEl = document.querySelector(".main") as HTMLElement;
const xAxisSelect = document.getElementById("x-axis") as HTMLSelectElement; const xAxisSelect = document.getElementById("x-axis") as HTMLSelectElement;
const yAxisSelect = document.getElementById("y-axis") as HTMLSelectElement; const yAxisSelect = document.getElementById("y-axis") as HTMLSelectElement;
const sizeMetricSelect = document.getElementById( const sizeMetricSelect = document.getElementById(
@ -449,34 +451,35 @@ applyDocumentTheme(systemDark ? "dark" : "light");
app.onerror = log.error; app.onerror = log.error;
// Handle host context changes (theme, styles, and fonts from host) // Handle host context changes (theme, styles, and fonts from host)
app.onhostcontextchanged = (params) => { function handleHostContextChanged(ctx: McpUiHostContext) {
if (params.theme) { if (ctx.theme) {
applyDocumentTheme(params.theme); applyDocumentTheme(ctx.theme);
} }
if (params.styles?.variables) { if (ctx.styles?.variables) {
applyHostStyleVariables(params.styles.variables); applyHostStyleVariables(ctx.styles.variables);
} }
if (params.styles?.css?.fonts) { if (ctx.styles?.css?.fonts) {
applyHostFonts(params.styles.css.fonts); applyHostFonts(ctx.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 // 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.destroy();
state.chart = initChart(); state.chart = initChart();
} }
}; }
app.onhostcontextchanged = handleHostContextChanged;
app.connect().then(() => { app.connect().then(() => {
// Apply initial host context after connection
const ctx = app.getHostContext(); const ctx = app.getHostContext();
if (ctx?.theme) { if (ctx) {
applyDocumentTheme(ctx.theme); handleHostContextChanged(ctx);
}
if (ctx?.styles?.variables) {
applyHostStyleVariables(ctx.styles.variables);
}
if (ctx?.styles?.css?.fonts) {
applyHostFonts(ctx.styles.css.fonts);
} }
}); });

View file

@ -1,7 +1,7 @@
/** /**
* @file App that demonstrates a few features using MCP Apps SDK + React. * @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 { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useCallback, useEffect, useState } from "react"; import { StrictMode, useCallback, useEffect, useState } from "react";
@ -29,6 +29,9 @@ function extractTime(callToolResult: CallToolResult): string {
function GetTimeApp() { function GetTimeApp() {
const [toolResult, setToolResult] = useState<CallToolResult | null>(null); const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
const [hostContext, setHostContext] = useState<
McpUiHostContext | undefined
>();
const { app, error } = useApp({ const { app, error } = useApp({
appInfo: IMPLEMENTATION, appInfo: IMPLEMENTATION,
capabilities: {}, capabilities: {},
@ -49,9 +52,19 @@ function GetTimeApp() {
}; };
app.onerror = log.error; app.onerror = log.error;
app.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
};
}, },
}); });
useEffect(() => {
if (app) {
setHostContext(app.getHostContext());
}
}, [app]);
if (error) if (error)
return ( return (
<div> <div>
@ -60,14 +73,25 @@ function GetTimeApp() {
); );
if (!app) return <div>Connecting...</div>; if (!app) return <div>Connecting...</div>;
return <GetTimeAppInner app={app} toolResult={toolResult} />; return (
<GetTimeAppInner
app={app}
toolResult={toolResult}
hostContext={hostContext}
/>
);
} }
interface GetTimeAppInnerProps { interface GetTimeAppInnerProps {
app: App; app: App;
toolResult: CallToolResult | null; toolResult: CallToolResult | null;
hostContext?: McpUiHostContext;
} }
function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) { function GetTimeAppInner({
app,
toolResult,
hostContext,
}: GetTimeAppInnerProps) {
const [serverTime, setServerTime] = useState("Loading..."); const [serverTime, setServerTime] = useState("Loading...");
const [messageText, setMessageText] = useState("This is message text."); const [messageText, setMessageText] = useState("This is message text.");
const [logText, setLogText] = useState("This is log text."); const [logText, setLogText] = useState("This is log text.");
@ -120,7 +144,15 @@ function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) {
}, [app, linkUrl]); }, [app, linkUrl]);
return ( return (
<main className={styles.main}> <main
className={styles.main}
style={{
paddingTop: hostContext?.safeAreaInsets?.top,
paddingRight: hostContext?.safeAreaInsets?.right,
paddingBottom: hostContext?.safeAreaInsets?.bottom,
paddingLeft: hostContext?.safeAreaInsets?.left,
}}
>
<p className={styles.notice}>Watch activity in the DevTools console!</p> <p className={styles.notice}>Watch activity in the DevTools console!</p>
<div className={styles.action}> <div className={styles.action}>

View file

@ -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(); await app.connect();
const ctx = app.getHostContext();
if (ctx) {
handleHostContextChanged(ctx);
}
</script> </script>
</body> </body>
</html> </html>

View file

@ -1,6 +1,7 @@
import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react"; import { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; 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 { createRoot } from "react-dom/client";
import { SliderRow } from "./components/SliderRow.tsx"; import { SliderRow } from "./components/SliderRow.tsx";
import { MetricCard } from "./components/MetricCard.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" }; 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) // Local defaults for immediate render (should match server's DEFAULT_INPUTS)
const FALLBACK_INPUTS: ScenarioInputs = { const FALLBACK_INPUTS: ScenarioInputs = {
startingMRR: 50000, startingMRR: 50000,
@ -51,6 +61,9 @@ function ScenarioModeler() {
const [templates, setTemplates] = useState<ScenarioTemplate[]>([]); const [templates, setTemplates] = useState<ScenarioTemplate[]>([]);
const [defaultInputs, setDefaultInputs] = const [defaultInputs, setDefaultInputs] =
useState<ScenarioInputs>(FALLBACK_INPUTS); useState<ScenarioInputs>(FALLBACK_INPUTS);
const [hostContext, setHostContext] = useState<
McpUiHostContext | undefined
>();
const { app, error } = useApp({ const { app, error } = useApp({
appInfo: APP_INFO, appInfo: APP_INFO,
@ -61,12 +74,21 @@ function ScenarioModeler() {
if (templates) setTemplates(templates); if (templates) setTemplates(templates);
if (defaultInputs) setDefaultInputs(defaultInputs); if (defaultInputs) setDefaultInputs(defaultInputs);
}; };
app.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
};
}, },
}); });
useEffect(() => {
if (app) {
setHostContext(app.getHostContext());
}
}, [app]);
if (error) { if (error) {
return ( return (
<main className="main"> <main className="main" style={getSafeAreaPaddingStyle(hostContext)}>
<div className="loading">Error: {error.message}</div> <div className="loading">Error: {error.message}</div>
</main> </main>
); );
@ -74,25 +96,31 @@ function ScenarioModeler() {
if (!app) { if (!app) {
return ( return (
<main className="main"> <main className="main" style={getSafeAreaPaddingStyle(hostContext)}>
<div className="loading">Connecting...</div> <div className="loading">Connecting...</div>
</main> </main>
); );
} }
return ( return (
<ScenarioModelerInner templates={templates} defaultInputs={defaultInputs} /> <ScenarioModelerInner
templates={templates}
defaultInputs={defaultInputs}
hostContext={hostContext}
/>
); );
} }
interface ScenarioModelerInnerProps { interface ScenarioModelerInnerProps {
templates: ScenarioTemplate[]; templates: ScenarioTemplate[];
defaultInputs: ScenarioInputs; defaultInputs: ScenarioInputs;
hostContext?: McpUiHostContext;
} }
function ScenarioModelerInner({ function ScenarioModelerInner({
templates, templates,
defaultInputs, defaultInputs,
hostContext,
}: ScenarioModelerInnerProps) { }: ScenarioModelerInnerProps) {
const [inputs, setInputs] = useState<ScenarioInputs>(FALLBACK_INPUTS); const [inputs, setInputs] = useState<ScenarioInputs>(FALLBACK_INPUTS);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>( const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(
@ -141,7 +169,7 @@ function ScenarioModelerInner({
}, [selectedTemplate]); }, [selectedTemplate]);
return ( return (
<main className="main"> <main className="main" style={getSafeAreaPaddingStyle(hostContext)}>
{/* Header */} {/* Header */}
<header className="header"> <header className="header">
<h1 className="header-title">SaaS Scenario Modeler</h1> <h1 className="header-title">SaaS Scenario Modeler</h1>

View file

@ -1,7 +1,7 @@
/** /**
* @file System Monitor App - displays real-time OS metrics with Chart.js * @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 { Chart, registerables } from "chart.js";
import "./global.css"; import "./global.css";
import "./mcp-app.css"; import "./mcp-app.css";
@ -40,6 +40,7 @@ interface SystemStats {
} }
// DOM element references // DOM element references
const mainEl = document.querySelector(".main") as HTMLElement;
const pollToggleBtn = document.getElementById("poll-toggle-btn")!; const pollToggleBtn = document.getElementById("poll-toggle-btn")!;
const statusIndicator = document.getElementById("status-indicator")!; const statusIndicator = document.getElementById("status-indicator")!;
const statusText = document.getElementById("status-text")!; const statusText = document.getElementById("status-text")!;
@ -360,7 +361,23 @@ window
// Register handlers and connect // Register handlers and connect
app.onerror = log.error; 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 // Auto-start polling after a short delay
setTimeout(startPolling, 500); setTimeout(startPolling, 500);

View file

@ -7,7 +7,7 @@
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react"; import { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; 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 { createRoot } from "react-dom/client";
import ThreeJSApp from "./threejs-app.tsx"; import ThreeJSApp from "./threejs-app.tsx";
import "./global.css"; import "./global.css";
@ -67,11 +67,21 @@ function McpAppWrapper() {
}; };
// Host context changes (theme, dimensions, etc.) // Host context changes (theme, dimensions, etc.)
app.onhostcontextchanged = (params) => { 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 // Memoized callbacks that forward to app methods
const callServerTool = useCallback<App["callServerTool"]>( const callServerTool = useCallback<App["callServerTool"]>(
(params, options) => app!.callServerTool(params, options), (params, options) => app!.callServerTool(params, options),

View file

@ -140,7 +140,7 @@ export default function ThreeJSApp({
toolInputs, toolInputs,
toolInputsPartial, toolInputsPartial,
toolResult: _toolResult, toolResult: _toolResult,
hostContext: _hostContext, hostContext,
callServerTool: _callServerTool, callServerTool: _callServerTool,
sendMessage: _sendMessage, sendMessage: _sendMessage,
openLink: _openLink, openLink: _openLink,
@ -155,6 +155,14 @@ export default function ThreeJSApp({
const partialCode = toolInputsPartial?.code; const partialCode = toolInputsPartial?.code;
const isStreaming = !toolInputs && !!toolInputsPartial; const isStreaming = !toolInputs && !!toolInputsPartial;
const safeAreaInsets = hostContext?.safeAreaInsets;
const containerStyle = {
paddingTop: safeAreaInsets?.top,
paddingRight: safeAreaInsets?.right,
paddingBottom: safeAreaInsets?.bottom,
paddingLeft: safeAreaInsets?.left,
};
useEffect(() => { useEffect(() => {
if (!code || !canvasRef.current || !containerRef.current) return; if (!code || !canvasRef.current || !containerRef.current) return;
@ -166,11 +174,19 @@ export default function ThreeJSApp({
}, [code, height]); }, [code, height]);
if (isStreaming || !code) { if (isStreaming || !code) {
return <LoadingShimmer height={height} code={partialCode} />; return (
<div style={containerStyle}>
<LoadingShimmer height={height} code={partialCode} />
</div>
);
} }
return ( return (
<div ref={containerRef} className="threejs-container"> <div
ref={containerRef}
className="threejs-container"
style={containerStyle}
>
<canvas <canvas
ref={canvasRef} ref={canvasRef}
style={{ style={{

View file

@ -1,7 +1,7 @@
/** /**
* Wiki Explorer - Force-directed graph visualization of Wikipedia link networks * Wiki Explorer - Force-directed graph visualization of Wikipedia link networks
*/ */
import { App } from "@modelcontextprotocol/ext-apps"; import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { import {
forceCenter, forceCenter,
@ -366,5 +366,21 @@ app.onerror = (err) => {
console.error("[Wiki Explorer] App error:", err); 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 // Connect to host
app.connect(); app.connect().then(() => {
const ctx = app.getHostContext();
if (ctx) {
handleHostContextChanged(ctx);
}
});