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

View file

@ -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<ScenarioTemplate[]>([]);
const [defaultInputs, setDefaultInputs] =
useState<ScenarioInputs>(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 (
<main className="main">
<main className="main" style={getSafeAreaPaddingStyle(hostContext)}>
<div className="loading">Error: {error.message}</div>
</main>
);
@ -74,25 +96,31 @@ function ScenarioModeler() {
if (!app) {
return (
<main className="main">
<main className="main" style={getSafeAreaPaddingStyle(hostContext)}>
<div className="loading">Connecting...</div>
</main>
);
}
return (
<ScenarioModelerInner templates={templates} defaultInputs={defaultInputs} />
<ScenarioModelerInner
templates={templates}
defaultInputs={defaultInputs}
hostContext={hostContext}
/>
);
}
interface ScenarioModelerInnerProps {
templates: ScenarioTemplate[];
defaultInputs: ScenarioInputs;
hostContext?: McpUiHostContext;
}
function ScenarioModelerInner({
templates,
defaultInputs,
hostContext,
}: ScenarioModelerInnerProps) {
const [inputs, setInputs] = useState<ScenarioInputs>(FALLBACK_INPUTS);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(
@ -141,7 +169,7 @@ function ScenarioModelerInner({
}, [selectedTemplate]);
return (
<main className="main">
<main className="main" style={getSafeAreaPaddingStyle(hostContext)}>
{/* Header */}
<header className="header">
<h1 className="header-title">SaaS Scenario Modeler</h1>