Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7bbf2bb

Browse filesBrowse files
authored
feat(basic-host): Add theme toggle and MCP style variables (#336)
* feat(basic-host): add theme toggle and MCP style variables Add theme support to basic-host example: - theme.ts: Global theme manager with light/dark toggle - host-styles.ts: Full set of MCP CSS variables using light-dark() - ThemeToggle component with sun/moon icons - Theme-aware CSS using CSS custom properties - Pass theme + styles to apps via hostContext - Notify apps when theme changes via sendHostContextChange Apps now receive: - hostContext.theme: 'light' | 'dark' - hostContext.styles.variables: All 60+ MCP CSS variables - onhostcontextchanged notifications when theme changes * test: update e2e goldens for theme changes
1 parent ee5d3f8 commit 7bbf2bb
Copy full SHA for 7bbf2bb

10 files changed

+287-12Lines changed: 287 additions & 12 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎examples/basic-host/sandbox.html‎

Copy file name to clipboardExpand all lines: examples/basic-host/sandbox.html
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
margin: 0;
1313
height: 100vh;
1414
width: 100vw;
15+
/* Transparent background allows parent page to show through */
16+
background-color: transparent;
1517
}
1618
body {
1719
display: flex;
@@ -26,6 +28,8 @@
2628
padding: 0px;
2729
overflow: hidden;
2830
flex-grow: 1;
31+
/* Inherit color scheme from parent for consistent transparency */
32+
color-scheme: inherit;
2933
}
3034
</style>
3135
</head>
Collapse file

‎examples/basic-host/src/global.css‎

Copy file name to clipboardExpand all lines: examples/basic-host/src/global.css
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
1+
:root {
2+
color-scheme: light dark;
3+
4+
/* Light theme (default) */
5+
--color-bg: #ffffff;
6+
--color-bg-secondary: #f5f5f5;
7+
--color-text: #1f2937;
8+
--color-text-secondary: #6b7280;
9+
--color-border: #e5e7eb;
10+
--color-primary: #1e3a5f;
11+
--color-primary-hover: #2d4a7c;
12+
}
13+
14+
[data-theme="dark"] {
15+
--color-bg: #1a1a1a;
16+
--color-bg-secondary: #2d2d2d;
17+
--color-text: #f3f4f6;
18+
--color-text-secondary: #9ca3af;
19+
--color-border: #404040;
20+
--color-primary: #3b82f6;
21+
--color-primary-hover: #60a5fa;
22+
}
23+
124
* {
225
box-sizing: border-box;
326
}
427

528
html, body {
29+
margin: 0;
30+
padding: 0;
631
font-family: system-ui, -apple-system, sans-serif;
732
font-size: 1rem;
33+
background-color: var(--color-bg);
34+
color: var(--color-text);
35+
transition: background-color 0.2s, color 0.2s;
836
}
937

1038
code {
Collapse file
+113Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* MCP style variables for the basic-host example.
3+
* These are passed to apps via hostContext.styles.variables.
4+
*/
5+
import type { McpUiStyles } from "@modelcontextprotocol/ext-apps";
6+
7+
/**
8+
* MCP App style variables using light-dark() for theme adaptation.
9+
* Apps receive these and can use them as CSS custom properties.
10+
*/
11+
export const HOST_STYLE_VARIABLES: McpUiStyles = {
12+
// Background colors - using light-dark() for automatic adaptation
13+
"--color-background-primary": "light-dark(#ffffff, #1a1a1a)",
14+
"--color-background-secondary": "light-dark(#f5f5f5, #2d2d2d)",
15+
"--color-background-tertiary": "light-dark(#e5e5e5, #404040)",
16+
"--color-background-inverse": "light-dark(#1a1a1a, #ffffff)",
17+
"--color-background-ghost": "light-dark(rgba(255,255,255,0), rgba(26,26,26,0))",
18+
"--color-background-info": "light-dark(#eff6ff, #1e3a5f)",
19+
"--color-background-danger": "light-dark(#fef2f2, #7f1d1d)",
20+
"--color-background-success": "light-dark(#f0fdf4, #14532d)",
21+
"--color-background-warning": "light-dark(#fefce8, #713f12)",
22+
"--color-background-disabled": "light-dark(rgba(255,255,255,0.5), rgba(26,26,26,0.5))",
23+
24+
// Text colors
25+
"--color-text-primary": "light-dark(#1f2937, #f3f4f6)",
26+
"--color-text-secondary": "light-dark(#6b7280, #9ca3af)",
27+
"--color-text-tertiary": "light-dark(#9ca3af, #6b7280)",
28+
"--color-text-inverse": "light-dark(#f3f4f6, #1f2937)",
29+
"--color-text-ghost": "light-dark(rgba(107,114,128,0.5), rgba(156,163,175,0.5))",
30+
"--color-text-info": "light-dark(#1d4ed8, #60a5fa)",
31+
"--color-text-danger": "light-dark(#b91c1c, #f87171)",
32+
"--color-text-success": "light-dark(#15803d, #4ade80)",
33+
"--color-text-warning": "light-dark(#a16207, #fbbf24)",
34+
"--color-text-disabled": "light-dark(rgba(31,41,55,0.5), rgba(243,244,246,0.5))",
35+
36+
// Border colors
37+
"--color-border-primary": "light-dark(#e5e7eb, #404040)",
38+
"--color-border-secondary": "light-dark(#d1d5db, #525252)",
39+
"--color-border-tertiary": "light-dark(#f3f4f6, #374151)",
40+
"--color-border-inverse": "light-dark(rgba(255,255,255,0.3), rgba(0,0,0,0.3))",
41+
"--color-border-ghost": "light-dark(rgba(229,231,235,0), rgba(64,64,64,0))",
42+
"--color-border-info": "light-dark(#93c5fd, #1e40af)",
43+
"--color-border-danger": "light-dark(#fca5a5, #991b1b)",
44+
"--color-border-success": "light-dark(#86efac, #166534)",
45+
"--color-border-warning": "light-dark(#fde047, #854d0e)",
46+
"--color-border-disabled": "light-dark(rgba(229,231,235,0.5), rgba(64,64,64,0.5))",
47+
48+
// Ring colors (focus)
49+
"--color-ring-primary": "light-dark(#3b82f6, #60a5fa)",
50+
"--color-ring-secondary": "light-dark(#6b7280, #9ca3af)",
51+
"--color-ring-inverse": "light-dark(#ffffff, #1f2937)",
52+
"--color-ring-info": "light-dark(#2563eb, #3b82f6)",
53+
"--color-ring-danger": "light-dark(#dc2626, #ef4444)",
54+
"--color-ring-success": "light-dark(#16a34a, #22c55e)",
55+
"--color-ring-warning": "light-dark(#ca8a04, #eab308)",
56+
57+
// Typography - Family
58+
"--font-sans": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
59+
"--font-mono": "ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', monospace",
60+
61+
// Typography - Weight
62+
"--font-weight-normal": "400",
63+
"--font-weight-medium": "500",
64+
"--font-weight-semibold": "600",
65+
"--font-weight-bold": "700",
66+
67+
// Typography - Text Size
68+
"--font-text-xs-size": "0.75rem",
69+
"--font-text-sm-size": "0.875rem",
70+
"--font-text-md-size": "1rem",
71+
"--font-text-lg-size": "1.125rem",
72+
73+
// Typography - Heading Size
74+
"--font-heading-xs-size": "0.75rem",
75+
"--font-heading-sm-size": "0.875rem",
76+
"--font-heading-md-size": "1rem",
77+
"--font-heading-lg-size": "1.25rem",
78+
"--font-heading-xl-size": "1.5rem",
79+
"--font-heading-2xl-size": "1.875rem",
80+
"--font-heading-3xl-size": "2.25rem",
81+
82+
// Typography - Text Line Height
83+
"--font-text-xs-line-height": "1.4",
84+
"--font-text-sm-line-height": "1.4",
85+
"--font-text-md-line-height": "1.5",
86+
"--font-text-lg-line-height": "1.5",
87+
88+
// Typography - Heading Line Height
89+
"--font-heading-xs-line-height": "1.4",
90+
"--font-heading-sm-line-height": "1.4",
91+
"--font-heading-md-line-height": "1.4",
92+
"--font-heading-lg-line-height": "1.3",
93+
"--font-heading-xl-line-height": "1.25",
94+
"--font-heading-2xl-line-height": "1.2",
95+
"--font-heading-3xl-line-height": "1.1",
96+
97+
// Border radius
98+
"--border-radius-xs": "2px",
99+
"--border-radius-sm": "4px",
100+
"--border-radius-md": "6px",
101+
"--border-radius-lg": "8px",
102+
"--border-radius-xl": "12px",
103+
"--border-radius-full": "9999px",
104+
105+
// Border width
106+
"--border-width-regular": "1px",
107+
108+
// Shadows
109+
"--shadow-hairline": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
110+
"--shadow-sm": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
111+
"--shadow-md": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
112+
"--shadow-lg": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
113+
};
Collapse file

‎examples/basic-host/src/implementation.ts‎

Copy file name to clipboardExpand all lines: examples/basic-host/src/implementation.ts
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
33
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
44
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
55
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
6+
import { getTheme, onThemeChange } from "./theme";
7+
import { HOST_STYLE_VARIABLES } from "./host-styles";
68

79

810
const SANDBOX_PROXY_BASE_URL = "http://localhost:8081/sandbox.html";
@@ -270,13 +272,25 @@ export function newAppBridge(
270272
// Declare support for model context updates
271273
updateModelContext: { text: {} },
272274
}, {
275+
// Pass initial host context with theme, display mode, and style variables
273276
hostContext: {
277+
theme: getTheme(),
278+
platform: "web",
279+
styles: {
280+
variables: HOST_STYLE_VARIABLES,
281+
},
274282
containerDimensions: options?.containerDimensions ?? { maxHeight: 6000 },
275283
displayMode: options?.displayMode ?? "inline",
276284
availableDisplayModes: ["inline", "fullscreen"],
277285
},
278286
});
279287

288+
// Listen for theme changes (from toggle or system) and notify the app
289+
onThemeChange((newTheme) => {
290+
log.info("Theme changed:", newTheme);
291+
appBridge.sendHostContextChange({ theme: newTheme });
292+
});
293+
280294
// Register all handlers before calling connect(). The view can start
281295
// sending requests immediately after the initialization handshake, so any
282296
// handlers registered after connect() might miss early requests.
Collapse file

‎examples/basic-host/src/index.module.css‎

Copy file name to clipboardExpand all lines: examples/basic-host/src/index.module.css
+40-12Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
1+
/* Theme toggle button */
2+
.themeToggle {
3+
position: fixed;
4+
top: 1rem;
5+
right: 1rem;
6+
width: 2.5rem;
7+
height: 2.5rem;
8+
padding: 0;
9+
border: 1px solid var(--color-border);
10+
border-radius: 50%;
11+
background-color: var(--color-bg-secondary);
12+
font-size: 1.25rem;
13+
cursor: pointer;
14+
z-index: 1000;
15+
transition: background-color 0.2s, border-color 0.2s, transform 0.1s;
16+
17+
&:hover {
18+
background-color: var(--color-border);
19+
}
20+
21+
&:active {
22+
transform: scale(0.95);
23+
}
24+
}
25+
126
.callToolPanel, .toolCallInfoPanel {
227
margin: 0 auto;
328
padding: 1rem;
4-
border: 1px solid #ddd;
29+
border: 1px solid var(--color-border);
530
border-radius: 4px;
31+
background-color: var(--color-bg-secondary);
632

733
* + & {
834
margin-top: 1rem;
@@ -28,9 +54,11 @@
2854
select,
2955
textarea {
3056
padding: 0.5rem;
31-
border: 1px solid #ccc;
57+
border: 1px solid var(--color-border);
3258
border-radius: 4px;
3359
font-size: inherit;
60+
background-color: var(--color-bg);
61+
color: var(--color-text);
3462
}
3563

3664
.toolSelect {
@@ -43,7 +71,7 @@
4371
resize: vertical;
4472

4573
&[aria-invalid="true"] {
46-
background-color: #fdd;
74+
background-color: light-dark(#fee2e2, #7f1d1d);
4775
}
4876
}
4977

@@ -53,14 +81,14 @@
5381
padding: 0.75rem 1.5rem;
5482
border: none;
5583
border-radius: 4px;
56-
background-color: #1e3a5f;
84+
background-color: var(--color-primary);
5785
font-size: inherit;
5886
font-weight: 600;
5987
color: white;
6088
cursor: pointer;
6189

6290
&:hover:not(:disabled) {
63-
background-color: #2d4a7c;
91+
background-color: var(--color-primary-hover);
6492
}
6593

6694
&:disabled {
@@ -103,15 +131,15 @@
103131
padding: 0;
104132
border: none;
105133
border-radius: 4px;
106-
background: #e0e0e0;
134+
background: var(--color-border);
107135
font-size: 1.25rem;
108136
line-height: 1;
109-
color: #666;
137+
color: var(--color-text-secondary);
110138
cursor: pointer;
111139

112140
&:hover {
113-
background: #d0d0d0;
114-
color: #333;
141+
background: var(--color-bg-secondary);
142+
color: var(--color-text);
115143
}
116144
}
117145
}
@@ -123,7 +151,7 @@
123151
width: 100%;
124152
height: 600px;
125153
box-sizing: border-box;
126-
border: 3px dashed #888;
154+
border: 3px dashed var(--color-border);
127155
border-radius: 4px;
128156
}
129157

@@ -246,6 +274,6 @@
246274

247275
.error {
248276
padding: 1.5rem;
249-
background-color: #ddd;
250-
color: #d00;
277+
background-color: var(--color-bg-secondary);
278+
color: light-dark(#dc2626, #f87171);
251279
}
Collapse file

‎examples/basic-host/src/index.tsx‎

Copy file name to clipboardExpand all lines: examples/basic-host/src/index.tsx
+24Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
33
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
44
import { createRoot } from "react-dom/client";
55
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation";
6+
import { getTheme, toggleTheme, onThemeChange, type Theme } from "./theme";
67
import styles from "./index.module.css";
78

89
/**
@@ -64,6 +65,28 @@ function getQueryParams() {
6465
};
6566
}
6667

68+
/**
69+
* Theme toggle button with light/dark icons.
70+
*/
71+
function ThemeToggle() {
72+
const [theme, setTheme] = useState<Theme>(getTheme);
73+
74+
useEffect(() => {
75+
return onThemeChange(setTheme);
76+
}, []);
77+
78+
return (
79+
<button
80+
className={styles.themeToggle}
81+
onClick={() => toggleTheme()}
82+
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
83+
>
84+
{theme === "dark" ? "☀️" : "🌙"}
85+
</button>
86+
);
87+
}
88+
89+
6790
function Host({ serversPromise }: HostProps) {
6891
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
6992
const [destroyingIds, setDestroyingIds] = useState<Set<number>>(new Set());
@@ -84,6 +107,7 @@ function Host({ serversPromise }: HostProps) {
84107

85108
return (
86109
<>
110+
<ThemeToggle />
87111
{toolCalls.map((info) => (
88112
<ToolCallInfoPanel
89113
key={info.id}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.