Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions examples/basic-server-react/src/mcp-app.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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: {},
Expand All @@ -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.");
Expand Down Expand Up @@ -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}>
Expand Down
21 changes: 19 additions & 2 deletions examples/basic-server-vanillajs/src/mcp-app.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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" });
Expand All @@ -51,6 +61,8 @@ app.ontoolresult = (result) => {

app.onerror = log.error;

app.onhostcontextchanged = handleHostContextChanged;


// Add event listeners
getTimeBtn.addEventListener("click", async () => {
Expand Down Expand Up @@ -92,4 +104,9 @@ openLinkBtn.addEventListener("click", async () => {


// Connect to host
app.connect();
app.connect().then(() => {
const ctx = app.getHostContext();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this behavior (triggerring the onhostcontextchanged with the initial host context) be part of the connect flow?

Copy link
Member Author

@jonathanhefner jonathanhefner Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the code correctly, it merely sets this._hostContext (does not trigger onhostcontextchanged):

ext-apps/src/app.ts

Lines 1029 to 1069 in 38126f7

override async connect(
transport: Transport = new PostMessageTransport(window.parent),
options?: RequestOptions,
): Promise<void> {
await super.connect(transport);
try {
const result = await this.request(
<McpUiInitializeRequest>{
method: "ui/initialize",
params: {
appCapabilities: this._capabilities,
appInfo: this._appInfo,
protocolVersion: LATEST_PROTOCOL_VERSION,
},
},
McpUiInitializeResultSchema,
options,
);
if (result === undefined) {
throw new Error(`Server sent invalid initialize result: ${result}`);
}
this._hostCapabilities = result.hostCapabilities;
this._hostInfo = result.hostInfo;
this._hostContext = result.hostContext;
await this.notification(<McpUiInitializedNotification>{
method: "ui/notifications/initialized",
});
if (this.options?.autoResize) {
this.setupSizeChangedNotifications();
}
} catch (error) {
// Disconnect if initialization fails.
void this.close();
throw error;
}
}

Copy link
Collaborator

@liady liady Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonathanhefner yes, that’s what I meant - I see in the examples that onhostcontextchanged is explicitly triggered from within the connect callback. Do we want this to be part of the connect logic itself in the SDK, rather than requiring developers to remember to call it manually?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to that, but my interpretation was that this behavior was intentional — onhostcontextchanged would only be called in response to ui/notifications/host-context-changed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'd keep its current behaviour to mean explicitly ui/notifications/host-context-changed happened for now.

if (ctx) {
handleHostContextChanged(ctx);
}
});
21 changes: 19 additions & 2 deletions examples/budget-allocator-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)")
Expand All @@ -631,4 +643,9 @@ window
});

// Connect to host
app.connect();
app.connect().then(() => {
const ctx = app.getHostContext();
if (ctx) {
handleHostContextChanged(ctx);
}
});
36 changes: 32 additions & 4 deletions examples/cohort-heatmap-server/src/mcp-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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}
Expand Down
39 changes: 21 additions & 18 deletions examples/customer-segmentation-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
applyHostStyleVariables,
applyHostFonts,
applyDocumentTheme,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import { Chart, registerables } from "chart.js";
import "./global.css";
Expand All @@ -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(
Expand Down Expand Up @@ -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 (ctx.styles?.variables) {
applyHostStyleVariables(ctx.styles.variables);
}
if (params.styles?.variables) {
applyHostStyleVariables(params.styles.variables);
if (ctx.styles?.css?.fonts) {
applyHostFonts(ctx.styles.css.fonts);
}
if (params.styles?.css?.fonts) {
applyHostFonts(params.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);
}
});

Expand Down
40 changes: 36 additions & 4 deletions examples/integration-server/src/mcp-app.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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: {},
Expand All @@ -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>
Expand All @@ -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.");
Expand Down Expand Up @@ -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}>
Expand Down
Loading
Loading