Improve consistency and organization of examples for AI learning

- Add `.gitignore` files to all 19 examples (`node_modules/`, `dist/`)
- Replace custom `log` objects with standard `console` methods
- Add `ontoolcancelled` handler to all `basic-server-*` examples
- Add host styling integration (`applyDocumentTheme`, `applyHostFonts`,
  `applyHostStyleVariables`) to non-React `basic-server-*` examples
- Add section headers for better code organization in feature examples
- Update READMEs with Build System section explaining single-file
  bundling

These changes make examples more consistent and easier to adapt.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Hefner 2026-01-12 17:06:44 -06:00
commit 4140565b77
40 changed files with 449 additions and 363 deletions

2
examples/basic-host/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -28,3 +28,7 @@ npm run dev
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
2. When the tool is invoked, the Host renders the UI from the resource.
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
## Build System
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.

View file

@ -1,23 +1,18 @@
/**
* @file App that demonstrates a few features using MCP Apps SDK + Preact.
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import {
App,
applyDocumentTheme,
applyHostFonts,
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useState } from "preact/hooks";
import { render } from "preact";
import styles from "./mcp-app.module.css";
const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" };
const log = {
info: console.log.bind(console, "[APP]"),
warn: console.warn.bind(console, "[APP]"),
error: console.error.bind(console, "[APP]"),
};
function extractTime(callToolResult: CallToolResult): string {
const { text } = callToolResult.content?.find((c) => c.type === "text")!;
return text;
@ -30,19 +25,36 @@ function GetTimeApp() {
const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
// Apply host styles reactively when hostContext changes
useEffect(() => {
const instance = new App(IMPLEMENTATION);
if (hostContext?.theme) {
applyDocumentTheme(hostContext.theme);
}
if (hostContext?.styles?.variables) {
applyHostStyleVariables(hostContext.styles.variables);
}
if (hostContext?.styles?.css?.fonts) {
applyHostFonts(hostContext.styles.css.fonts);
}
}, [hostContext]);
useEffect(() => {
const instance = new App({ name: "Get Time App", version: "1.0.0" });
instance.ontoolinput = async (input) => {
log.info("Received tool call input:", input);
console.info("Received tool call input:", input);
};
instance.ontoolresult = async (result) => {
log.info("Received tool call result:", result);
console.info("Received tool call result:", result);
setToolResult(result);
};
instance.onerror = log.error;
instance.ontoolcancelled = (params) => {
console.info("Tool call cancelled:", params.reason);
};
instance.onerror = console.error;
instance.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
@ -83,12 +95,12 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps)
const handleGetTime = useCallback(async () => {
try {
log.info("Calling get-time tool...");
console.info("Calling get-time tool...");
const result = await app.callServerTool({ name: "get-time", arguments: {} });
log.info("get-time result:", result);
console.info("get-time result:", result);
setServerTime(extractTime(result));
} catch (e) {
log.error(e);
console.error(e);
setServerTime("[ERROR]");
}
}, [app]);
@ -96,26 +108,26 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps)
const handleSendMessage = useCallback(async () => {
const signal = AbortSignal.timeout(5000);
try {
log.info("Sending message text to Host:", messageText);
console.info("Sending message text to Host:", messageText);
const { isError } = await app.sendMessage(
{ role: "user", content: [{ type: "text", text: messageText }] },
{ signal },
);
log.info("Message", isError ? "rejected" : "accepted");
console.info("Message", isError ? "rejected" : "accepted");
} catch (e) {
log.error("Message send error:", signal.aborted ? "timed out" : e);
console.error("Message send error:", signal.aborted ? "timed out" : e);
}
}, [app, messageText]);
const handleSendLog = useCallback(async () => {
log.info("Sending log text to Host:", logText);
console.info("Sending log text to Host:", logText);
await app.sendLog({ level: "info", data: logText });
}, [app, logText]);
const handleOpenLink = useCallback(async () => {
log.info("Sending open link request to Host:", linkUrl);
console.info("Sending open link request to Host:", linkUrl);
const { isError } = await app.openLink({ url: linkUrl });
log.info("Open link request", isError ? "rejected" : "accepted");
console.info("Open link request", isError ? "rejected" : "accepted");
}, [app, linkUrl]);
return (

View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -30,3 +30,7 @@ npm run dev
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
2. When the tool is invoked, the Host renders the UI from the resource.
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
## Build System
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.

View file

@ -9,16 +9,6 @@ import { createRoot } from "react-dom/client";
import styles from "./mcp-app.module.css";
const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" };
const log = {
info: console.log.bind(console, "[APP]"),
warn: console.warn.bind(console, "[APP]"),
error: console.error.bind(console, "[APP]"),
};
function extractTime(callToolResult: CallToolResult): string {
const { text } = callToolResult.content?.find((c) => c.type === "text")!;
return text;
@ -28,24 +18,31 @@ function extractTime(callToolResult: CallToolResult): string {
function GetTimeApp() {
const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
// `useApp` (1) creates an `App` instance, (2) calls `onAppCreated` to
// register handlers, and (3) calls `connect()` on the `App` instance.
const { app, error } = useApp({
appInfo: IMPLEMENTATION,
appInfo: { name: "Get Time App", version: "1.0.0" },
capabilities: {},
onAppCreated: (app) => {
app.onteardown = async () => {
log.info("App is being torn down");
console.info("App is being torn down");
return {};
};
app.ontoolinput = async (input) => {
log.info("Received tool call input:", input);
console.info("Received tool call input:", input);
};
app.ontoolresult = async (result) => {
log.info("Received tool call result:", result);
console.info("Received tool call result:", result);
setToolResult(result);
};
app.onerror = log.error;
app.ontoolcancelled = (params) => {
console.info("Tool call cancelled:", params.reason);
};
app.onerror = console.error;
app.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
@ -85,12 +82,12 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps)
const handleGetTime = useCallback(async () => {
try {
log.info("Calling get-time tool...");
console.info("Calling get-time tool...");
const result = await app.callServerTool({ name: "get-time", arguments: {} });
log.info("get-time result:", result);
console.info("get-time result:", result);
setServerTime(extractTime(result));
} catch (e) {
log.error(e);
console.error(e);
setServerTime("[ERROR]");
}
}, [app]);
@ -98,26 +95,26 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps)
const handleSendMessage = useCallback(async () => {
const signal = AbortSignal.timeout(5000);
try {
log.info("Sending message text to Host:", messageText);
console.info("Sending message text to Host:", messageText);
const { isError } = await app.sendMessage(
{ role: "user", content: [{ type: "text", text: messageText }] },
{ signal },
);
log.info("Message", isError ? "rejected" : "accepted");
console.info("Message", isError ? "rejected" : "accepted");
} catch (e) {
log.error("Message send error:", signal.aborted ? "timed out" : e);
console.error("Message send error:", signal.aborted ? "timed out" : e);
}
}, [app, messageText]);
const handleSendLog = useCallback(async () => {
log.info("Sending log text to Host:", logText);
console.info("Sending log text to Host:", logText);
await app.sendLog({ level: "info", data: logText });
}, [app, logText]);
const handleOpenLink = useCallback(async () => {
log.info("Sending open link request to Host:", linkUrl);
console.info("Sending open link request to Host:", linkUrl);
const { isError } = await app.openLink({ url: linkUrl });
log.info("Open link request", isError ? "rejected" : "accepted");
console.info("Open link request", isError ? "rejected" : "accepted");
}, [app, linkUrl]);
return (

View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -28,3 +28,7 @@ npm run dev
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
2. When the tool is invoked, the Host renders the UI from the resource.
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
## Build System
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.

View file

@ -1,23 +1,18 @@
/**
* @file App that demonstrates a few features using MCP Apps SDK + Solid.
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import {
App,
applyDocumentTheme,
applyHostFonts,
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { createEffect, createSignal, onMount, Show } from "solid-js";
import { render } from "solid-js/web";
import styles from "./mcp-app.module.css";
const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" };
const log = {
info: console.log.bind(console, "[APP]"),
warn: console.warn.bind(console, "[APP]"),
error: console.error.bind(console, "[APP]"),
};
function extractTime(callToolResult: CallToolResult): string {
const { text } = callToolResult.content?.find((c) => c.type === "text")!;
return text;
@ -30,19 +25,37 @@ function GetTimeApp() {
const [toolResult, setToolResult] = createSignal<CallToolResult | null>(null);
const [hostContext, setHostContext] = createSignal<McpUiHostContext | undefined>();
// Apply host styles reactively when hostContext changes
createEffect(() => {
const ctx = hostContext();
if (ctx?.theme) {
applyDocumentTheme(ctx.theme);
}
if (ctx?.styles?.variables) {
applyHostStyleVariables(ctx.styles.variables);
}
if (ctx?.styles?.css?.fonts) {
applyHostFonts(ctx.styles.css.fonts);
}
});
onMount(async () => {
const instance = new App(IMPLEMENTATION);
const instance = new App({ name: "Get Time App", version: "1.0.0" });
instance.ontoolinput = async (input) => {
log.info("Received tool call input:", input);
console.info("Received tool call input:", input);
};
instance.ontoolresult = async (result) => {
log.info("Received tool call result:", result);
console.info("Received tool call result:", result);
setToolResult(result);
};
instance.onerror = log.error;
instance.ontoolcancelled = (params) => {
console.info("Tool call cancelled:", params.reason);
};
instance.onerror = console.error;
instance.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
@ -87,12 +100,12 @@ function GetTimeAppInner(props: GetTimeAppInnerProps) {
async function handleGetTime() {
try {
log.info("Calling get-time tool...");
console.info("Calling get-time tool...");
const result = await props.app.callServerTool({ name: "get-time", arguments: {} });
log.info("get-time result:", result);
console.info("get-time result:", result);
setServerTime(extractTime(result));
} catch (e) {
log.error(e);
console.error(e);
setServerTime("[ERROR]");
}
}
@ -100,26 +113,26 @@ function GetTimeAppInner(props: GetTimeAppInnerProps) {
async function handleSendMessage() {
const signal = AbortSignal.timeout(5000);
try {
log.info("Sending message text to Host:", messageText());
console.info("Sending message text to Host:", messageText());
const { isError } = await props.app.sendMessage(
{ role: "user", content: [{ type: "text", text: messageText() }] },
{ signal },
);
log.info("Message", isError ? "rejected" : "accepted");
console.info("Message", isError ? "rejected" : "accepted");
} catch (e) {
log.error("Message send error:", signal.aborted ? "timed out" : e);
console.error("Message send error:", signal.aborted ? "timed out" : e);
}
}
async function handleSendLog() {
log.info("Sending log text to Host:", logText());
console.info("Sending log text to Host:", logText());
await props.app.sendLog({ level: "info", data: logText() });
}
async function handleOpenLink() {
log.info("Sending open link request to Host:", linkUrl());
console.info("Sending open link request to Host:", linkUrl());
const { isError } = await props.app.openLink({ url: linkUrl() });
log.info("Open link request", isError ? "rejected" : "accepted");
console.info("Open link request", isError ? "rejected" : "accepted");
}
return (

View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -28,3 +28,7 @@ npm run dev
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
2. When the tool is invoked, the Host renders the UI from the resource.
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
## Build System
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.

View file

@ -1,19 +1,20 @@
<script lang="ts">
import { onMount } from "svelte";
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import {
App,
applyDocumentTheme,
applyHostFonts,
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
const log = {
info: console.log.bind(console, "[APP]"),
warn: console.warn.bind(console, "[APP]"),
error: console.error.bind(console, "[APP]"),
};
function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
return text;
}
let app = $state<App | null>(null);
let hostContext = $state<McpUiHostContext | undefined>();
let serverTime = $state("Loading...");
@ -21,19 +22,36 @@ let messageText = $state("This is message text.");
let logText = $state("This is log text.");
let linkUrl = $state("https://modelcontextprotocol.io/");
// Apply host styles reactively when hostContext changes
$effect(() => {
if (hostContext?.theme) {
applyDocumentTheme(hostContext.theme);
}
if (hostContext?.styles?.variables) {
applyHostStyleVariables(hostContext.styles.variables);
}
if (hostContext?.styles?.css?.fonts) {
applyHostFonts(hostContext.styles.css.fonts);
}
});
onMount(async () => {
const instance = new App({ name: "Get Time App", version: "1.0.0" });
instance.ontoolinput = (params) => {
log.info("Received tool call input:", params);
console.info("Received tool call input:", params);
};
instance.ontoolresult = (result) => {
log.info("Received tool call result:", result);
console.info("Received tool call result:", result);
serverTime = extractTime(result);
};
instance.onerror = log.error;
instance.ontoolcancelled = (params) => {
console.info("Tool call cancelled:", params.reason);
};
instance.onerror = console.error;
instance.onhostcontextchanged = (params) => {
hostContext = { ...hostContext, ...params };
@ -47,12 +65,12 @@ onMount(async () => {
async function handleGetTime() {
if (!app) return;
try {
log.info("Calling get-time tool...");
console.info("Calling get-time tool...");
const result = await app.callServerTool({ name: "get-time", arguments: {} });
log.info("get-time result:", result);
console.info("get-time result:", result);
serverTime = extractTime(result);
} catch (e) {
log.error(e);
console.error(e);
serverTime = "[ERROR]";
}
}
@ -61,28 +79,28 @@ async function handleSendMessage() {
if (!app) return;
const signal = AbortSignal.timeout(5000);
try {
log.info("Sending message text to Host:", messageText);
console.info("Sending message text to Host:", messageText);
const { isError } = await app.sendMessage(
{ role: "user", content: [{ type: "text", text: messageText }] },
{ signal },
);
log.info("Message", isError ? "rejected" : "accepted");
console.info("Message", isError ? "rejected" : "accepted");
} catch (e) {
log.error("Message send error:", signal.aborted ? "timed out" : e);
console.error("Message send error:", signal.aborted ? "timed out" : e);
}
}
async function handleSendLog() {
if (!app) return;
log.info("Sending log text to Host:", logText);
console.info("Sending log text to Host:", logText);
await app.sendLog({ level: "info", data: logText });
}
async function handleOpenLink() {
if (!app) return;
log.info("Sending open link request to Host:", linkUrl);
console.info("Sending open link request to Host:", linkUrl);
const { isError } = await app.openLink({ url: linkUrl });
log.info("Open link request", isError ? "rejected" : "accepted");
console.info("Open link request", isError ? "rejected" : "accepted");
}
</script>

View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -10,6 +10,7 @@ An MCP App example with a vanilla JavaScript UI (no framework).
- Tool registration with a linked UI resource
- Vanilla JS UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class directly
- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
- Theme integration via [`applyDocumentTheme()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyDocumentTheme.html), [`applyHostStyleVariables()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostStyleVariables.html), and [`applyHostFonts()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostFonts.html)
## Key Files
@ -29,28 +30,6 @@ npm run dev
2. When the tool is invoked, the Host renders the UI from the resource.
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
## Vanilla JS Pattern
## Build System
```typescript
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
// Get element references from static HTML
const button = document.getElementById("my-button")!;
// Create app instance
const app = new App({ name: "My App", version: "1.0.0" });
// Register handlers BEFORE connecting
app.ontoolresult = (result) => {
/* handle result */
};
app.onerror = console.error;
// Add event listeners
button.addEventListener("click", () => {
/* ... */
});
// Connect to host
app.connect();
```
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.

View file

@ -1,26 +1,24 @@
/**
* @file App that demonstrates a few features using MCP Apps SDK with vanilla JS.
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import {
App,
applyDocumentTheme,
applyHostFonts,
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import "./global.css";
import "./mcp-app.css";
const log = {
info: console.log.bind(console, "[APP]"),
warn: console.warn.bind(console, "[APP]"),
error: console.error.bind(console, "[APP]"),
};
function extractTime(result: CallToolResult): string {
const { time } = (result.structuredContent as { time?: string }) ?? {};
return time ?? "[ERROR]";
}
// Get element references
const mainEl = document.querySelector(".main") as HTMLElement;
const serverTimeEl = document.getElementById("server-time")!;
const getTimeBtn = document.getElementById("get-time-btn")!;
@ -31,7 +29,17 @@ 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.theme) {
applyDocumentTheme(ctx.theme);
}
if (ctx.styles?.variables) {
applyHostStyleVariables(ctx.styles.variables);
}
if (ctx.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`;
@ -41,38 +49,42 @@ function handleHostContextChanged(ctx: McpUiHostContext) {
}
// Create app instance
// 1. Create app instance
const app = new App({ name: "Get Time App", version: "1.0.0" });
// 2. Register handlers BEFORE connecting
app.onteardown = async () => {
log.info("App is being torn down");
console.info("App is being torn down");
return {};
};
// Register handlers BEFORE connecting
app.ontoolinput = (params) => {
log.info("Received tool call input:", params);
console.info("Received tool call input:", params);
};
app.ontoolresult = (result) => {
log.info("Received tool call result:", result);
console.info("Received tool call result:", result);
serverTimeEl.textContent = extractTime(result);
};
app.onerror = log.error;
app.ontoolcancelled = (params) => {
console.info("Tool call cancelled:", params.reason);
};
app.onerror = console.error;
app.onhostcontextchanged = handleHostContextChanged;
// Add event listeners
getTimeBtn.addEventListener("click", async () => {
try {
log.info("Calling get-time tool...");
console.info("Calling get-time tool...");
const result = await app.callServerTool({ name: "get-time", arguments: {} });
log.info("get-time result:", result);
console.info("get-time result:", result);
serverTimeEl.textContent = extractTime(result);
} catch (e) {
log.error(e);
console.error(e);
serverTimeEl.textContent = "[ERROR]";
}
});
@ -80,30 +92,30 @@ getTimeBtn.addEventListener("click", async () => {
sendMessageBtn.addEventListener("click", async () => {
const signal = AbortSignal.timeout(5000);
try {
log.info("Sending message text to Host:", messageText.value);
console.info("Sending message text to Host:", messageText.value);
const { isError } = await app.sendMessage(
{ role: "user", content: [{ type: "text", text: messageText.value }] },
{ signal },
);
log.info("Message", isError ? "rejected" : "accepted");
console.info("Message", isError ? "rejected" : "accepted");
} catch (e) {
log.error("Message send error:", signal.aborted ? "timed out" : e);
console.error("Message send error:", signal.aborted ? "timed out" : e);
}
});
sendLogBtn.addEventListener("click", async () => {
log.info("Sending log text to Host:", logText.value);
console.info("Sending log text to Host:", logText.value);
await app.sendLog({ level: "info", data: logText.value });
});
openLinkBtn.addEventListener("click", async () => {
log.info("Sending open link request to Host:", linkUrl.value);
console.info("Sending open link request to Host:", linkUrl.value);
const { isError } = await app.openLink({ url: linkUrl.value });
log.info("Open link request", isError ? "rejected" : "accepted");
console.info("Open link request", isError ? "rejected" : "accepted");
});
// Connect to host
// 3. Connect to host
app.connect().then(() => {
const ctx = app.getHostContext();
if (ctx) {

2
examples/basic-server-vue/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -28,3 +28,7 @@ npm run dev
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
2. When the tool is invoked, the Host renders the UI from the resource.
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
## Build System
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.

View file

@ -1,19 +1,20 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { ref, onMounted, watchEffect } from "vue";
import {
App,
applyDocumentTheme,
applyHostFonts,
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
const log = {
info: console.log.bind(console, "[APP]"),
warn: console.warn.bind(console, "[APP]"),
error: console.error.bind(console, "[APP]"),
};
function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
return text;
}
const app = ref<App | null>(null);
const hostContext = ref<McpUiHostContext | undefined>();
const serverTime = ref("Loading...");
@ -21,19 +22,37 @@ const messageText = ref("This is message text.");
const logText = ref("This is log text.");
const linkUrl = ref("https://modelcontextprotocol.io/");
// Apply host styles reactively when hostContext changes
watchEffect(() => {
const ctx = hostContext.value;
if (ctx?.theme) {
applyDocumentTheme(ctx.theme);
}
if (ctx?.styles?.variables) {
applyHostStyleVariables(ctx.styles.variables);
}
if (ctx?.styles?.css?.fonts) {
applyHostFonts(ctx.styles.css.fonts);
}
});
onMounted(async () => {
const instance = new App({ name: "Get Time App", version: "1.0.0" });
instance.ontoolinput = (params) => {
log.info("Received tool call input:", params);
console.info("Received tool call input:", params);
};
instance.ontoolresult = (result) => {
log.info("Received tool call result:", result);
console.info("Received tool call result:", result);
serverTime.value = extractTime(result);
};
instance.onerror = log.error;
instance.ontoolcancelled = (params) => {
console.info("Tool call cancelled:", params.reason);
};
instance.onerror = console.error;
instance.onhostcontextchanged = (params) => {
hostContext.value = { ...hostContext.value, ...params };
@ -47,12 +66,12 @@ onMounted(async () => {
async function handleGetTime() {
if (!app.value) return;
try {
log.info("Calling get-time tool...");
console.info("Calling get-time tool...");
const result = await app.value.callServerTool({ name: "get-time", arguments: {} });
log.info("get-time result:", result);
console.info("get-time result:", result);
serverTime.value = extractTime(result);
} catch (e) {
log.error(e);
console.error(e);
serverTime.value = "[ERROR]";
}
}
@ -61,28 +80,28 @@ async function handleSendMessage() {
if (!app.value) return;
const signal = AbortSignal.timeout(5000);
try {
log.info("Sending message text to Host:", messageText.value);
console.info("Sending message text to Host:", messageText.value);
const { isError } = await app.value.sendMessage(
{ role: "user", content: [{ type: "text", text: messageText.value }] },
{ signal },
);
log.info("Message", isError ? "rejected" : "accepted");
console.info("Message", isError ? "rejected" : "accepted");
} catch (e) {
log.error("Message send error:", signal.aborted ? "timed out" : e);
console.error("Message send error:", signal.aborted ? "timed out" : e);
}
}
async function handleSendLog() {
if (!app.value) return;
log.info("Sending log text to Host:", logText.value);
console.info("Sending log text to Host:", logText.value);
await app.value.sendLog({ level: "info", data: logText.value });
}
async function handleOpenLink() {
if (!app.value) return;
log.info("Sending open link request to Host:", linkUrl.value);
console.info("Sending open link request to Host:", linkUrl.value);
const { isError } = await app.value.openLink({ url: linkUrl.value });
log.info("Open link request", isError ? "rejected" : "accepted");
console.info("Open link request", isError ? "rejected" : "accepted");
}
</script>

View file

@ -0,0 +1,2 @@
node_modules/
dist/

Some files were not shown because too many files have changed in this diff Show more