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

@ -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) {