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
3 changed files with 449 additions and 363 deletions

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 (