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:
parent
97990b6b28
commit
79670b4dcd
12 changed files with 267 additions and 48 deletions
|
|
@ -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<CallToolResult | null>(null);
|
||||
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
|
||||
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 <div><strong>ERROR:</strong> {error.message}</div>;
|
||||
if (!app) return <div>Connecting...</div>;
|
||||
|
||||
return <GetTimeAppInner app={app} toolResult={toolResult} />;
|
||||
return <GetTimeAppInner app={app} toolResult={toolResult} hostContext={hostContext} />;
|
||||
}
|
||||
|
||||
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<div className={styles.action}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 <div className={styles.error}>ERROR: {error.message}</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
|
||||
function CohortHeatmapInner({ app }: { app: App }) {
|
||||
function CohortHeatmapInner({
|
||||
app,
|
||||
hostContext,
|
||||
}: {
|
||||
app: App;
|
||||
hostContext?: McpUiHostContext;
|
||||
}) {
|
||||
const [data, setData] = useState<CohortData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedMetric, setSelectedMetric] = useState<MetricType>("retention");
|
||||
|
|
@ -143,7 +163,15 @@ function CohortHeatmapInner({ app }: { app: App }) {
|
|||
}, []);
|
||||
|
||||
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
|
||||
selectedMetric={selectedMetric}
|
||||
selectedPeriodType={selectedPeriodType}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
applyHostStyleVariables,
|
||||
applyHostFonts,
|
||||
applyDocumentTheme,
|
||||
type McpUiHostContext,
|
||||
} from "@modelcontextprotocol/ext-apps";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import "./global.css";
|
||||
|
|
@ -22,6 +23,7 @@ const log = {
|
|||
};
|
||||
|
||||
// DOM element references
|
||||
const mainEl = document.querySelector(".main") as HTMLElement;
|
||||
const xAxisSelect = document.getElementById("x-axis") as HTMLSelectElement;
|
||||
const yAxisSelect = document.getElementById("y-axis") as HTMLSelectElement;
|
||||
const sizeMetricSelect = document.getElementById(
|
||||
|
|
@ -449,34 +451,35 @@ applyDocumentTheme(systemDark ? "dark" : "light");
|
|||
app.onerror = log.error;
|
||||
|
||||
// Handle host context changes (theme, styles, and fonts from host)
|
||||
app.onhostcontextchanged = (params) => {
|
||||
if (params.theme) {
|
||||
applyDocumentTheme(params.theme);
|
||||
function handleHostContextChanged(ctx: McpUiHostContext) {
|
||||
if (ctx.theme) {
|
||||
applyDocumentTheme(ctx.theme);
|
||||
}
|
||||
if (params.styles?.variables) {
|
||||
applyHostStyleVariables(params.styles.variables);
|
||||
if (ctx.styles?.variables) {
|
||||
applyHostStyleVariables(ctx.styles.variables);
|
||||
}
|
||||
if (params.styles?.css?.fonts) {
|
||||
applyHostFonts(params.styles.css.fonts);
|
||||
if (ctx.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
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CallToolResult | null>(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 (
|
||||
<div>
|
||||
|
|
@ -60,14 +73,25 @@ function GetTimeApp() {
|
|||
);
|
||||
if (!app) return <div>Connecting...</div>;
|
||||
|
||||
return <GetTimeAppInner app={app} toolResult={toolResult} />;
|
||||
return (
|
||||
<GetTimeAppInner
|
||||
app={app}
|
||||
toolResult={toolResult}
|
||||
hostContext={hostContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<div className={styles.action}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* 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 {
|
||||
forceCenter,
|
||||
|
|
@ -366,5 +366,21 @@ app.onerror = (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
|
||||
app.connect();
|
||||
app.connect().then(() => {
|
||||
const ctx = app.getHostContext();
|
||||
if (ctx) {
|
||||
handleHostContextChanged(ctx);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue