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
2 changed files with 267 additions and 48 deletions

View file

@ -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<App["callServerTool"]>(
(params, options) => app!.callServerTool(params, options),

View file

@ -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 <LoadingShimmer height={height} code={partialCode} />;
return (
<div style={containerStyle}>
<LoadingShimmer height={height} code={partialCode} />
</div>
);
}
return (
<div ref={containerRef} className="threejs-container">
<div
ref={containerRef}
className="threejs-container"
style={containerStyle}
>
<canvas
ref={canvasRef}
style={{