diff --git a/examples/basic-host/sandbox.html b/examples/basic-host/sandbox.html
index e0790fb0..fb4cd8d2 100644
--- a/examples/basic-host/sandbox.html
+++ b/examples/basic-host/sandbox.html
@@ -12,6 +12,8 @@
margin: 0;
height: 100vh;
width: 100vw;
+ /* Transparent background allows parent page to show through */
+ background-color: transparent;
}
body {
display: flex;
@@ -26,6 +28,8 @@
padding: 0px;
overflow: hidden;
flex-grow: 1;
+ /* Inherit color scheme from parent for consistent transparency */
+ color-scheme: inherit;
}
diff --git a/examples/basic-host/src/global.css b/examples/basic-host/src/global.css
index 97cda440..74b57514 100644
--- a/examples/basic-host/src/global.css
+++ b/examples/basic-host/src/global.css
@@ -1,10 +1,38 @@
+:root {
+ color-scheme: light dark;
+
+ /* Light theme (default) */
+ --color-bg: #ffffff;
+ --color-bg-secondary: #f5f5f5;
+ --color-text: #1f2937;
+ --color-text-secondary: #6b7280;
+ --color-border: #e5e7eb;
+ --color-primary: #1e3a5f;
+ --color-primary-hover: #2d4a7c;
+}
+
+[data-theme="dark"] {
+ --color-bg: #1a1a1a;
+ --color-bg-secondary: #2d2d2d;
+ --color-text: #f3f4f6;
+ --color-text-secondary: #9ca3af;
+ --color-border: #404040;
+ --color-primary: #3b82f6;
+ --color-primary-hover: #60a5fa;
+}
+
* {
box-sizing: border-box;
}
html, body {
+ margin: 0;
+ padding: 0;
font-family: system-ui, -apple-system, sans-serif;
font-size: 1rem;
+ background-color: var(--color-bg);
+ color: var(--color-text);
+ transition: background-color 0.2s, color 0.2s;
}
code {
diff --git a/examples/basic-host/src/host-styles.ts b/examples/basic-host/src/host-styles.ts
new file mode 100644
index 00000000..f6d4887c
--- /dev/null
+++ b/examples/basic-host/src/host-styles.ts
@@ -0,0 +1,113 @@
+/**
+ * MCP style variables for the basic-host example.
+ * These are passed to apps via hostContext.styles.variables.
+ */
+import type { McpUiStyles } from "@modelcontextprotocol/ext-apps";
+
+/**
+ * MCP App style variables using light-dark() for theme adaptation.
+ * Apps receive these and can use them as CSS custom properties.
+ */
+export const HOST_STYLE_VARIABLES: McpUiStyles = {
+ // Background colors - using light-dark() for automatic adaptation
+ "--color-background-primary": "light-dark(#ffffff, #1a1a1a)",
+ "--color-background-secondary": "light-dark(#f5f5f5, #2d2d2d)",
+ "--color-background-tertiary": "light-dark(#e5e5e5, #404040)",
+ "--color-background-inverse": "light-dark(#1a1a1a, #ffffff)",
+ "--color-background-ghost": "light-dark(rgba(255,255,255,0), rgba(26,26,26,0))",
+ "--color-background-info": "light-dark(#eff6ff, #1e3a5f)",
+ "--color-background-danger": "light-dark(#fef2f2, #7f1d1d)",
+ "--color-background-success": "light-dark(#f0fdf4, #14532d)",
+ "--color-background-warning": "light-dark(#fefce8, #713f12)",
+ "--color-background-disabled": "light-dark(rgba(255,255,255,0.5), rgba(26,26,26,0.5))",
+
+ // Text colors
+ "--color-text-primary": "light-dark(#1f2937, #f3f4f6)",
+ "--color-text-secondary": "light-dark(#6b7280, #9ca3af)",
+ "--color-text-tertiary": "light-dark(#9ca3af, #6b7280)",
+ "--color-text-inverse": "light-dark(#f3f4f6, #1f2937)",
+ "--color-text-ghost": "light-dark(rgba(107,114,128,0.5), rgba(156,163,175,0.5))",
+ "--color-text-info": "light-dark(#1d4ed8, #60a5fa)",
+ "--color-text-danger": "light-dark(#b91c1c, #f87171)",
+ "--color-text-success": "light-dark(#15803d, #4ade80)",
+ "--color-text-warning": "light-dark(#a16207, #fbbf24)",
+ "--color-text-disabled": "light-dark(rgba(31,41,55,0.5), rgba(243,244,246,0.5))",
+
+ // Border colors
+ "--color-border-primary": "light-dark(#e5e7eb, #404040)",
+ "--color-border-secondary": "light-dark(#d1d5db, #525252)",
+ "--color-border-tertiary": "light-dark(#f3f4f6, #374151)",
+ "--color-border-inverse": "light-dark(rgba(255,255,255,0.3), rgba(0,0,0,0.3))",
+ "--color-border-ghost": "light-dark(rgba(229,231,235,0), rgba(64,64,64,0))",
+ "--color-border-info": "light-dark(#93c5fd, #1e40af)",
+ "--color-border-danger": "light-dark(#fca5a5, #991b1b)",
+ "--color-border-success": "light-dark(#86efac, #166534)",
+ "--color-border-warning": "light-dark(#fde047, #854d0e)",
+ "--color-border-disabled": "light-dark(rgba(229,231,235,0.5), rgba(64,64,64,0.5))",
+
+ // Ring colors (focus)
+ "--color-ring-primary": "light-dark(#3b82f6, #60a5fa)",
+ "--color-ring-secondary": "light-dark(#6b7280, #9ca3af)",
+ "--color-ring-inverse": "light-dark(#ffffff, #1f2937)",
+ "--color-ring-info": "light-dark(#2563eb, #3b82f6)",
+ "--color-ring-danger": "light-dark(#dc2626, #ef4444)",
+ "--color-ring-success": "light-dark(#16a34a, #22c55e)",
+ "--color-ring-warning": "light-dark(#ca8a04, #eab308)",
+
+ // Typography - Family
+ "--font-sans": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ "--font-mono": "ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', monospace",
+
+ // Typography - Weight
+ "--font-weight-normal": "400",
+ "--font-weight-medium": "500",
+ "--font-weight-semibold": "600",
+ "--font-weight-bold": "700",
+
+ // Typography - Text Size
+ "--font-text-xs-size": "0.75rem",
+ "--font-text-sm-size": "0.875rem",
+ "--font-text-md-size": "1rem",
+ "--font-text-lg-size": "1.125rem",
+
+ // Typography - Heading Size
+ "--font-heading-xs-size": "0.75rem",
+ "--font-heading-sm-size": "0.875rem",
+ "--font-heading-md-size": "1rem",
+ "--font-heading-lg-size": "1.25rem",
+ "--font-heading-xl-size": "1.5rem",
+ "--font-heading-2xl-size": "1.875rem",
+ "--font-heading-3xl-size": "2.25rem",
+
+ // Typography - Text Line Height
+ "--font-text-xs-line-height": "1.4",
+ "--font-text-sm-line-height": "1.4",
+ "--font-text-md-line-height": "1.5",
+ "--font-text-lg-line-height": "1.5",
+
+ // Typography - Heading Line Height
+ "--font-heading-xs-line-height": "1.4",
+ "--font-heading-sm-line-height": "1.4",
+ "--font-heading-md-line-height": "1.4",
+ "--font-heading-lg-line-height": "1.3",
+ "--font-heading-xl-line-height": "1.25",
+ "--font-heading-2xl-line-height": "1.2",
+ "--font-heading-3xl-line-height": "1.1",
+
+ // Border radius
+ "--border-radius-xs": "2px",
+ "--border-radius-sm": "4px",
+ "--border-radius-md": "6px",
+ "--border-radius-lg": "8px",
+ "--border-radius-xl": "12px",
+ "--border-radius-full": "9999px",
+
+ // Border width
+ "--border-width-regular": "1px",
+
+ // Shadows
+ "--shadow-hairline": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
+ "--shadow-sm": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
+ "--shadow-md": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
+ "--shadow-lg": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
+};
diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts
index f9237370..434169a9 100644
--- a/examples/basic-host/src/implementation.ts
+++ b/examples/basic-host/src/implementation.ts
@@ -3,6 +3,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
+import { getTheme, onThemeChange } from "./theme";
+import { HOST_STYLE_VARIABLES } from "./host-styles";
const SANDBOX_PROXY_BASE_URL = "http://localhost:8081/sandbox.html";
@@ -270,13 +272,25 @@ export function newAppBridge(
// Declare support for model context updates
updateModelContext: { text: {} },
}, {
+ // Pass initial host context with theme, display mode, and style variables
hostContext: {
+ theme: getTheme(),
+ platform: "web",
+ styles: {
+ variables: HOST_STYLE_VARIABLES,
+ },
containerDimensions: options?.containerDimensions ?? { maxHeight: 6000 },
displayMode: options?.displayMode ?? "inline",
availableDisplayModes: ["inline", "fullscreen"],
},
});
+ // Listen for theme changes (from toggle or system) and notify the app
+ onThemeChange((newTheme) => {
+ log.info("Theme changed:", newTheme);
+ appBridge.sendHostContextChange({ theme: newTheme });
+ });
+
// Register all handlers before calling connect(). The view can start
// sending requests immediately after the initialization handshake, so any
// handlers registered after connect() might miss early requests.
diff --git a/examples/basic-host/src/index.module.css b/examples/basic-host/src/index.module.css
index 75fa1de3..eb4d6795 100644
--- a/examples/basic-host/src/index.module.css
+++ b/examples/basic-host/src/index.module.css
@@ -1,8 +1,34 @@
+/* Theme toggle button */
+.themeToggle {
+ position: fixed;
+ top: 1rem;
+ right: 1rem;
+ width: 2.5rem;
+ height: 2.5rem;
+ padding: 0;
+ border: 1px solid var(--color-border);
+ border-radius: 50%;
+ background-color: var(--color-bg-secondary);
+ font-size: 1.25rem;
+ cursor: pointer;
+ z-index: 1000;
+ transition: background-color 0.2s, border-color 0.2s, transform 0.1s;
+
+ &:hover {
+ background-color: var(--color-border);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+}
+
.callToolPanel, .toolCallInfoPanel {
margin: 0 auto;
padding: 1rem;
- border: 1px solid #ddd;
+ border: 1px solid var(--color-border);
border-radius: 4px;
+ background-color: var(--color-bg-secondary);
* + & {
margin-top: 1rem;
@@ -28,9 +54,11 @@
select,
textarea {
padding: 0.5rem;
- border: 1px solid #ccc;
+ border: 1px solid var(--color-border);
border-radius: 4px;
font-size: inherit;
+ background-color: var(--color-bg);
+ color: var(--color-text);
}
.toolSelect {
@@ -43,7 +71,7 @@
resize: vertical;
&[aria-invalid="true"] {
- background-color: #fdd;
+ background-color: light-dark(#fee2e2, #7f1d1d);
}
}
@@ -53,14 +81,14 @@
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
- background-color: #1e3a5f;
+ background-color: var(--color-primary);
font-size: inherit;
font-weight: 600;
color: white;
cursor: pointer;
&:hover:not(:disabled) {
- background-color: #2d4a7c;
+ background-color: var(--color-primary-hover);
}
&:disabled {
@@ -103,15 +131,15 @@
padding: 0;
border: none;
border-radius: 4px;
- background: #e0e0e0;
+ background: var(--color-border);
font-size: 1.25rem;
line-height: 1;
- color: #666;
+ color: var(--color-text-secondary);
cursor: pointer;
&:hover {
- background: #d0d0d0;
- color: #333;
+ background: var(--color-bg-secondary);
+ color: var(--color-text);
}
}
}
@@ -123,7 +151,7 @@
width: 100%;
height: 600px;
box-sizing: border-box;
- border: 3px dashed #888;
+ border: 3px dashed var(--color-border);
border-radius: 4px;
}
@@ -246,6 +274,6 @@
.error {
padding: 1.5rem;
- background-color: #ddd;
- color: #d00;
+ background-color: var(--color-bg-secondary);
+ color: light-dark(#dc2626, #f87171);
}
diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx
index cbed7c79..0b4fda77 100644
--- a/examples/basic-host/src/index.tsx
+++ b/examples/basic-host/src/index.tsx
@@ -3,6 +3,7 @@ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation";
+import { getTheme, toggleTheme, onThemeChange, type Theme } from "./theme";
import styles from "./index.module.css";
/**
@@ -64,6 +65,28 @@ function getQueryParams() {
};
}
+/**
+ * Theme toggle button with light/dark icons.
+ */
+function ThemeToggle() {
+ const [theme, setTheme] = useState(getTheme);
+
+ useEffect(() => {
+ return onThemeChange(setTheme);
+ }, []);
+
+ return (
+
+ );
+}
+
+
function Host({ serversPromise }: HostProps) {
const [toolCalls, setToolCalls] = useState([]);
const [destroyingIds, setDestroyingIds] = useState>(new Set());
@@ -84,6 +107,7 @@ function Host({ serversPromise }: HostProps) {
return (
<>
+
{toolCalls.map((info) => (
void;
+
+const listeners = new Set();
+
+// Get initial theme from system preference
+let currentTheme: Theme = window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+
+// Apply theme to document
+function applyTheme(theme: Theme) {
+ document.documentElement.setAttribute("data-theme", theme);
+ document.documentElement.style.colorScheme = theme;
+}
+
+// Initial application
+applyTheme(currentTheme);
+
+/**
+ * Get current theme.
+ */
+export function getTheme(): Theme {
+ return currentTheme;
+}
+
+/**
+ * Set theme and notify all listeners.
+ */
+export function setTheme(theme: Theme): void {
+ if (theme === currentTheme) return;
+ currentTheme = theme;
+ applyTheme(theme);
+ listeners.forEach((listener) => listener(theme));
+}
+
+/**
+ * Toggle between light and dark themes.
+ */
+export function toggleTheme(): Theme {
+ const newTheme = currentTheme === "dark" ? "light" : "dark";
+ setTheme(newTheme);
+ return newTheme;
+}
+
+/**
+ * Subscribe to theme changes.
+ * Returns unsubscribe function.
+ */
+export function onThemeChange(listener: ThemeListener): () => void {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+}
+
+// Also listen for system preference changes
+window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
+ setTheme(e.matches ? "dark" : "light");
+});
diff --git a/tests/e2e/servers.spec.ts-snapshots/basic-react.png b/tests/e2e/servers.spec.ts-snapshots/basic-react.png
index bfc28bb1..a03d8176 100644
Binary files a/tests/e2e/servers.spec.ts-snapshots/basic-react.png and b/tests/e2e/servers.spec.ts-snapshots/basic-react.png differ
diff --git a/tests/e2e/servers.spec.ts-snapshots/basic-vanillajs.png b/tests/e2e/servers.spec.ts-snapshots/basic-vanillajs.png
index 57586967..787a0f95 100644
Binary files a/tests/e2e/servers.spec.ts-snapshots/basic-vanillajs.png and b/tests/e2e/servers.spec.ts-snapshots/basic-vanillajs.png differ
diff --git a/tests/e2e/servers.spec.ts-snapshots/integration.png b/tests/e2e/servers.spec.ts-snapshots/integration.png
index 60b52e7c..45362b7e 100644
Binary files a/tests/e2e/servers.spec.ts-snapshots/integration.png and b/tests/e2e/servers.spec.ts-snapshots/integration.png differ