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.
|
* @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}>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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={{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue