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

@ -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 (