mcp-host-styling-integration
Integrate MCP App UIs with the host application's theming system so apps look native in Claude Desktop, ChatGPT, VS Code, Goose, Postman, and other MCP-enabled hosts.
Overview
MCP Apps render in sandboxed iframes inside host applications. Each host has its own visual theme (colors, fonts, border radii, spacing). The MCP Apps SDK provides:
- CSS variables (
--color-*, --font-*, --border-radius-*) injected by the host
- SDK helpers (
applyDocumentTheme, applyHostStyleVariables, applyHostFonts) to apply them
- React hooks (
useHostStyles, useHostStyleVariables, useHostFonts) for React apps
onhostcontextchanged event fired when theme changes (e.g., dark mode toggle)
The key principle is: always use CSS variable fallbacks so the app looks correct both as an MCP App (host provides variables) and standalone (fallback values apply).
Capabilities
Host CSS Variable Integration
- Apply all host CSS variables with sensible fallback values
- Support color variables:
--color-background-primary, --color-background-secondary, --color-text-primary, --color-text-secondary, --color-border-primary
- Support font variables:
--font-sans, --font-mono, --font-text-base-size, --font-text-sm-size
- Support layout variables:
--border-radius-sm, --border-radius-md, --border-radius-lg
onhostcontextchanged Handler
- Listen for theme changes from the host
- Reapply styling when theme changes (e.g., light to dark mode)
- Access host context: theme, display mode, safe area insets
Safe Area Insets
- Apply safe area padding for mobile or embedded contexts
- Handle
env(safe-area-inset-top), env(safe-area-inset-bottom), etc.
Display Mode Detection
- Detect embedded vs fullscreen mode
- Adapt layout based on available space
- Configure fullscreen mode via tool metadata
SDK Helper Functions
applyDocumentTheme(theme) -- sets document-level theme class
applyHostStyleVariables(context) -- applies all CSS variables from host
applyHostFonts(context) -- loads and applies host fonts
React Hook Integration
useHostStyles() -- combined hook applying theme, variables, and fonts
useHostStyleVariables() -- CSS variables only
useHostFonts() -- font loading only
Usage
Vanilla JS: Full Host Styling
import {
App,
PostMessageTransport,
applyDocumentTheme,
applyHostStyleVariables,
applyHostFonts,
} from '@modelcontextprotocol/ext-apps';
const app = new App({ transport: new PostMessageTransport() });
// Register handler BEFORE connect()
app.onhostcontextchanged = (params) => {
const ctx = params.context;
// Apply theme (light/dark)
if (ctx.theme) {
applyDocumentTheme(ctx.theme);
}
// Apply CSS variables
applyHostStyleVariables(ctx);
// Load and apply fonts
applyHostFonts(ctx);
};
// THEN connect
await app.connect();
React: useHostStyles Hook
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
function MyApp() {
const app = useApp();
useHostStyles(); // Handles all theme/variable/font application
return (
<div className="app-container">
<h1>My MCP App</h1>
</div>
);
}
CSS with Fallback Values
/* Always use fallbacks so app works standalone too */
.app-container {
background-color: var(--color-background-primary, #ffffff);
color: var(--color-text-primary, #1a1a1a);
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
font-size: var(--font-text-base-size, 14px);
border-radius: var(--border-radius-md, 8px);
}
.card {
background-color: var(--color-background-secondary, #f5f5f5);
border: 1px solid var(--color-border-primary, #e0e0e0);
border-radius: var(--border-radius-sm, 4px);
padding: 16px;
}
.label {
color: var(--color-text-secondary, #666666);
font-size: var(--font-text-sm-size, 12px);
}
.code {
font-family: var(--font-mono, 'Courier New', monospace);
}
/* Safe area insets for mobile/embedded contexts */
.app-root {
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
padding-left: env(safe-area-inset-left, 0px);
padding-right: env(safe-area-inset-right, 0px);
}
Available Host CSS Variables
| Variable |
Category |
Description |
--color-background-primary |
Color |
Main background |
--color-background-secondary |
Color |
Card/section background |
--color-background-tertiary |
Color |
Nested/subtle background |
--color-text-primary |
Color |
Main text |
--color-text-secondary |
Color |
Secondary/muted text |
--color-text-tertiary |
Color |
Subtle/hint text |
--color-border-primary |
Color |
Main borders |
--color-border-secondary |
Color |
Subtle borders |
--color-accent |
Color |
Interactive elements |
--color-error |
Color |
Error states |
--color-success |
Color |
Success states |
--color-warning |
Color |
Warning states |
--font-sans |
Font |
Sans-serif font family |
--font-mono |
Font |
Monospace font family |
--font-text-xs-size |
Font |
Extra small text size |
--font-text-sm-size |
Font |
Small text size |
--font-text-base-size |
Font |
Base text size |
--font-text-lg-size |
Font |
Large text size |
--font-text-xl-size |
Font |
Extra large text size |
--border-radius-sm |
Layout |
Small border radius |
--border-radius-md |
Layout |
Medium border radius |
--border-radius-lg |
Layout |
Large border radius |
--border-radius-full |
Layout |
Full/pill border radius |
Hybrid App Styling (MCP + Standalone)
/* Works in BOTH modes because of fallback values */
body {
margin: 0;
padding: 0;
background-color: var(--color-background-primary, #ffffff);
color: var(--color-text-primary, #1a1a1a);
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
}
/* When host provides variables, they override fallbacks automatically */
/* When running standalone, fallback values apply */
Fullscreen Mode
// Configure fullscreen in the tool registration
registerAppTool(server, {
name: 'show_dashboard',
resourceUri: 'app:///dashboard',
// Request fullscreen display
displayMode: 'fullscreen',
async handler(args) { /* ... */ },
});
Common Pitfalls
- Hardcoding colors/fonts: Always use CSS variables with fallbacks. Never hardcode
#ffffff or Arial without a variable.
- Forgetting fallbacks: Without fallback values, standalone mode will have no styling.
- Not handling theme changes: The host can switch themes at any time. Always implement
onhostcontextchanged.
- Ignoring safe area insets: On mobile or certain embedded contexts, content can be obscured without safe area padding.
- Applying styles after connect(): Register
onhostcontextchanged BEFORE app.connect().
Verification Checklist
Task Definition
const mcpHostStylingTask = defineTask({
name: 'mcp-host-styling-integration',
description: 'Integrate MCP App UI with host theming system',
inputs: {
framework: { type: 'string', required: true },
hybrid: { type: 'boolean', default: false },
fullscreen: { type: 'boolean', default: false }
},
outputs: {
cssFileCreated: { type: 'boolean' },
handlerRegistered: { type: 'boolean' },
artifacts: { type: 'array' }
},
async run(inputs, taskCtx) {
return {
kind: 'skill',
title: `Integrate host styling (${inputs.framework})`,
skill: {
name: 'mcp-host-styling-integration',
context: {
framework: inputs.framework,
hybrid: inputs.hybrid,
fullscreen: inputs.fullscreen,
instructions: [
'Create CSS with host variable fallbacks',
'Implement onhostcontextchanged handler',
'Apply theme, style variables, and fonts via SDK helpers',
'Add safe area inset padding',
'Verify styling in both MCP and standalone modes'
]
}
},
io: {
inputJsonPath: `tasks/${taskCtx.effectId}/input.json`,
outputJsonPath: `tasks/${taskCtx.effectId}/result.json`
}
};
}
});
Applicable Processes
- create-mcp-app.js
- add-app-to-mcp-server.js
- convert-web-app-to-mcp.js
External Dependencies
@modelcontextprotocol/ext-apps (applyDocumentTheme, applyHostStyleVariables, applyHostFonts)
@modelcontextprotocol/ext-apps/react (useHostStyles, useHostStyleVariables, useHostFonts) -- React only
References
Related Skills
- mcp-app-scaffolding
- mcp-tool-resource-pattern
- mcp-app-verification
- single-file-bundling
Related Agents
- mcp-ui-developer
- mcp-app-architect
1---2name: mcp-host-styling-integration3description: Integrates MCP App UI with host theming system. Applies host CSS variables, handles onhostcontextchanged, safe area insets, display mode detection, and fullscreen configuration.4---5
6# mcp-host-styling-integration
7
8Integrate MCP App UIs with the host application's theming system so apps look native in Claude Desktop, ChatGPT, VS Code, Goose, Postman, and other MCP-enabled hosts.
9
10## Overview
11
12MCP Apps render in sandboxed iframes inside host applications. Each host has its own visual theme (colors, fonts, border radii, spacing). The MCP Apps SDK provides:
13- **CSS variables** (`--color-*`, `--font-*`, `--border-radius-*`) injected by the host
14- **SDK helpers** (`applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts`) to apply them
15- **React hooks** (`useHostStyles`, `useHostStyleVariables`, `useHostFonts`) for React apps
16- **`onhostcontextchanged` event** fired when theme changes (e.g., dark mode toggle)
17
18The key principle is: always use CSS variable fallbacks so the app looks correct both as an MCP App (host provides variables) and standalone (fallback values apply).
19
20## Capabilities
21
22### Host CSS Variable Integration
23- Apply all host CSS variables with sensible fallback values
24- Support color variables: `--color-background-primary`, `--color-background-secondary`, `--color-text-primary`, `--color-text-secondary`, `--color-border-primary`
25- Support font variables: `--font-sans`, `--font-mono`, `--font-text-base-size`, `--font-text-sm-size`
26- Support layout variables: `--border-radius-sm`, `--border-radius-md`, `--border-radius-lg`
27
28### onhostcontextchanged Handler
29- Listen for theme changes from the host
30- Reapply styling when theme changes (e.g., light to dark mode)
31- Access host context: theme, display mode, safe area insets
32
33### Safe Area Insets
34- Apply safe area padding for mobile or embedded contexts
35- Handle `env(safe-area-inset-top)`, `env(safe-area-inset-bottom)`, etc.
36
37### Display Mode Detection
38- Detect embedded vs fullscreen mode
39- Adapt layout based on available space
40- Configure fullscreen mode via tool metadata
41
42### SDK Helper Functions
43- `applyDocumentTheme(theme)` -- sets document-level theme class
44- `applyHostStyleVariables(context)` -- applies all CSS variables from host
45- `applyHostFonts(context)` -- loads and applies host fonts
46
47### React Hook Integration
48- `useHostStyles()` -- combined hook applying theme, variables, and fonts
49- `useHostStyleVariables()` -- CSS variables only
50- `useHostFonts()` -- font loading only
51
52## Usage
53
54### Vanilla JS: Full Host Styling
55
56```typescript
57import {
58 App,
59 PostMessageTransport,
60 applyDocumentTheme,
61 applyHostStyleVariables,
62 applyHostFonts,
63} from '@modelcontextprotocol/ext-apps';
64
65const app = new App({ transport: new PostMessageTransport() });
66
67// Register handler BEFORE connect()
68app.onhostcontextchanged = (params) => {
69 const ctx = params.context;
70
71 // Apply theme (light/dark)
72 if (ctx.theme) {
73 applyDocumentTheme(ctx.theme);
74 }
75
76 // Apply CSS variables
77 applyHostStyleVariables(ctx);
78
79 // Load and apply fonts
80 applyHostFonts(ctx);
81};
82
83// THEN connect
84await app.connect();
85```
86
87### React: useHostStyles Hook
88
89```tsx
90import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
91
92function MyApp() {
93 const app = useApp();
94 useHostStyles(); // Handles all theme/variable/font application
95
96 return (
97 <div className="app-container">
98 <h1>My MCP App</h1>
99 </div>
100 );
101}
102```
103
104### CSS with Fallback Values
105
106```css
107/* Always use fallbacks so app works standalone too */
108.app-container {
109 background-color: var(--color-background-primary, #ffffff);
110 color: var(--color-text-primary, #1a1a1a);
111 font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
112 font-size: var(--font-text-base-size, 14px);
113 border-radius: var(--border-radius-md, 8px);
114}
115
116.card {
117 background-color: var(--color-background-secondary, #f5f5f5);
118 border: 1px solid var(--color-border-primary, #e0e0e0);
119 border-radius: var(--border-radius-sm, 4px);
120 padding: 16px;
121}
122
123.label {
124 color: var(--color-text-secondary, #666666);
125 font-size: var(--font-text-sm-size, 12px);
126}
127
128.code {
129 font-family: var(--font-mono, 'Courier New', monospace);
130}
131
132/* Safe area insets for mobile/embedded contexts */
133.app-root {
134 padding-top: env(safe-area-inset-top, 0px);
135 padding-bottom: env(safe-area-inset-bottom, 0px);
136 padding-left: env(safe-area-inset-left, 0px);
137 padding-right: env(safe-area-inset-right, 0px);
138}
139```
140
141### Available Host CSS Variables
142
143| Variable | Category | Description |
144|----------|----------|-------------|
145| `--color-background-primary` | Color | Main background |
146| `--color-background-secondary` | Color | Card/section background |
147| `--color-background-tertiary` | Color | Nested/subtle background |
148| `--color-text-primary` | Color | Main text |
149| `--color-text-secondary` | Color | Secondary/muted text |
150| `--color-text-tertiary` | Color | Subtle/hint text |
151| `--color-border-primary` | Color | Main borders |
152| `--color-border-secondary` | Color | Subtle borders |
153| `--color-accent` | Color | Interactive elements |
154| `--color-error` | Color | Error states |
155| `--color-success` | Color | Success states |
156| `--color-warning` | Color | Warning states |
157| `--font-sans` | Font | Sans-serif font family |
158| `--font-mono` | Font | Monospace font family |
159| `--font-text-xs-size` | Font | Extra small text size |
160| `--font-text-sm-size` | Font | Small text size |
161| `--font-text-base-size` | Font | Base text size |
162| `--font-text-lg-size` | Font | Large text size |
163| `--font-text-xl-size` | Font | Extra large text size |
164| `--border-radius-sm` | Layout | Small border radius |
165| `--border-radius-md` | Layout | Medium border radius |
166| `--border-radius-lg` | Layout | Large border radius |
167| `--border-radius-full` | Layout | Full/pill border radius |
168
169### Hybrid App Styling (MCP + Standalone)
170
171```css
172/* Works in BOTH modes because of fallback values */
173body {
174 margin: 0;
175 padding: 0;
176 background-color: var(--color-background-primary, #ffffff);
177 color: var(--color-text-primary, #1a1a1a);
178 font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
179}
180
181/* When host provides variables, they override fallbacks automatically */
182/* When running standalone, fallback values apply */
183```
184
185### Fullscreen Mode
186
187```typescript
188// Configure fullscreen in the tool registration
189registerAppTool(server, {
190 name: 'show_dashboard',
191 resourceUri: 'app:///dashboard',
192 // Request fullscreen display
193 displayMode: 'fullscreen',
194 async handler(args) { /* ... */ },
195});
196```
197
198## Common Pitfalls
199
2001. **Hardcoding colors/fonts**: Always use CSS variables with fallbacks. Never hardcode `#ffffff` or `Arial` without a variable.
2012. **Forgetting fallbacks**: Without fallback values, standalone mode will have no styling.
2023. **Not handling theme changes**: The host can switch themes at any time. Always implement `onhostcontextchanged`.
2034. **Ignoring safe area insets**: On mobile or certain embedded contexts, content can be obscured without safe area padding.
2045. **Applying styles after connect()**: Register `onhostcontextchanged` BEFORE `app.connect()`.
205
206## Verification Checklist
207
208- [ ] All colors use `var(--color-*, fallback)` pattern
209- [ ] All fonts use `var(--font-*, fallback)` pattern
210- [ ] `onhostcontextchanged` handler registered BEFORE `app.connect()`
211- [ ] `applyDocumentTheme` / `applyHostStyleVariables` / `applyHostFonts` called in handler
212- [ ] CSS fallback values are sensible defaults (not broken/empty)
213- [ ] Safe area insets applied to root container
214- [ ] App looks correct in both MCP mode (host variables) and standalone (fallbacks)
215- [ ] Theme switch (light/dark) handled dynamically
216
217## Task Definition
218
219```javascript
220const mcpHostStylingTask = defineTask({
221 name: 'mcp-host-styling-integration',
222 description: 'Integrate MCP App UI with host theming system',
223
224 inputs: {
225 framework: { type: 'string', required: true },
226 hybrid: { type: 'boolean', default: false },
227 fullscreen: { type: 'boolean', default: false }
228 },
229
230 outputs: {
231 cssFileCreated: { type: 'boolean' },
232 handlerRegistered: { type: 'boolean' },
233 artifacts: { type: 'array' }
234 },
235
236 async run(inputs, taskCtx) {
237 return {
238 kind: 'skill',
239 title: `Integrate host styling (${inputs.framework})`,
240 skill: {
241 name: 'mcp-host-styling-integration',
242 context: {
243 framework: inputs.framework,
244 hybrid: inputs.hybrid,
245 fullscreen: inputs.fullscreen,
246 instructions: [
247 'Create CSS with host variable fallbacks',
248 'Implement onhostcontextchanged handler',
249 'Apply theme, style variables, and fonts via SDK helpers',
250 'Add safe area inset padding',
251 'Verify styling in both MCP and standalone modes'
252 ]
253 }
254 },
255 io: {
256 inputJsonPath: `tasks/${taskCtx.effectId}/input.json`,
257 outputJsonPath: `tasks/${taskCtx.effectId}/result.json`
258 }
259 };
260 }
261});
262```
263
264## Applicable Processes
265
266- create-mcp-app.js
267- add-app-to-mcp-server.js
268- convert-web-app-to-mcp.js
269
270## External Dependencies
271
272- `@modelcontextprotocol/ext-apps` (applyDocumentTheme, applyHostStyleVariables, applyHostFonts)
273- `@modelcontextprotocol/ext-apps/react` (useHostStyles, useHostStyleVariables, useHostFonts) -- React only
274
275## References
276
277- [MCP Apps SDK - Styles](https://github.com/modelcontextprotocol/ext-apps/blob/main/src/styles.ts)
278- [MCP Apps SDK - React Hooks](https://github.com/modelcontextprotocol/ext-apps/blob/main/src/react/useHostStyles.ts)
279- [MCP Apps Patterns](https://github.com/modelcontextprotocol/ext-apps/blob/main/docs/patterns.md)
280
281## Related Skills
282
283- mcp-app-scaffolding
284- mcp-tool-resource-pattern
285- mcp-app-verification
286- single-file-bundling
287
288## Related Agents
289
290- mcp-ui-developer
291- mcp-app-architect