diff --git a/examples/cute-dogs-server/README.md b/examples/cute-dogs-server/README.md
new file mode 100644
index 00000000..b2bcda2b
--- /dev/null
+++ b/examples/cute-dogs-server/README.md
@@ -0,0 +1,109 @@
+# Cute Dogs MCP Server (MCP Apps UI)
+
+An MCP (Model Context Protocol) server + MCP Apps UI that provides interactive widgets and tools for browsing and viewing dog images from the [Dog CEO API](https://dog.ceo/dog-api/).
+
+This example server demonstrates the full MCP Apps capabilities with a React example:
+
+- Render React UI widgets. The UI is hydrated by data passed in from the MCP tool via `structuredContent`
+- Call tool within widget
+- Send follow up message
+- Open external link
+
+
+
+## Installation
+
+Install all dependencies:
+
+```bash
+npm i
+```
+
+Then start the server
+
+```bash
+npm run start
+```
+
+The terminal will then print out text `MCP Server listening on http://localhost:3001/mcp`. Connect to the MCP server with the localhost link.
+
+## Tools
+
+The server provides three MCP tools:
+
+### 1. `show-random-dog-image`
+
+Shows a dog image in an interactive UI widget. The image is displayed in the widget, not in the text response.
+
+**Parameters:**
+
+- `breed` (optional): Dog breed name (e.g., `"hound"`, `"retriever"`). If not provided, returns a random dog from any breed.
+
+**Widget:** `dog-image-view`
+
+**Example:**
+
+```json
+{
+ "name": "show-random-dog-image",
+ "arguments": {
+ "breed": "hound"
+ }
+}
+```
+
+### 2. `all-breeds-view`
+
+Shows all available dog breeds in an interactive UI widget. Users can click on any breed to send a message to the chat requesting that breed.
+
+**Parameters:** None
+
+**Widget:** `all-breeds-view`
+
+**Example:**
+
+```json
+{
+ "name": "all-breeds-view",
+ "arguments": {}
+}
+```
+
+### 3. `get-more-images`
+
+Fetches multiple random dog images from a specific breed. Returns an array of image URLs. This tool is typically called from within the `dog-image-view` widget to load additional images.
+
+**Parameters:**
+
+- `breed` (required): The dog breed name (e.g., `"hound"`, `"retriever"`)
+- `count` (optional): Number of images to fetch (1-30). Defaults to 3 if not provided.
+
+**Widget:** None (programmatic tool)
+
+**Example:**
+
+```json
+{
+ "name": "get-more-images",
+ "arguments": {
+ "breed": "hound",
+ "count": 5
+ }
+}
+```
+
+## How this server is compiled
+
+1. **Source Files** → React components written in TypeScript/TSX:
+ - `src/dog-image-view.tsx` - The React component for displaying dog images
+ - `src/all-breeds-view.tsx` - The React component for showing all breeds
+
+2. **HTML Entry Points** → Simple HTML files that load the React components:
+ - `dog-image-view.html` - Loads `dog-image-view.tsx` via a script tag
+ - `all-breeds-view.html` - Loads `all-breeds-view.tsx` via a script tag
+
+3. **Vite Build Process** → When you run `npm run build`:
+ - Vite compiles each HTML file separately (using the `INPUT` environment variable)
+ - It bundles all React code, CSS, and dependencies into a single HTML file
+ - Outputs go to `dist/dog-image-view.html` and `dist/all-breeds-view.html`
+ - These are self-contained, ready-to-serve HTML files
diff --git a/examples/cute-dogs-server/all-breeds-view.html b/examples/cute-dogs-server/all-breeds-view.html
new file mode 100644
index 00000000..d699c739
--- /dev/null
+++ b/examples/cute-dogs-server/all-breeds-view.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Show all breeds
+
+
+
+
+
+
diff --git a/examples/cute-dogs-server/components.json b/examples/cute-dogs-server/components.json
new file mode 100644
index 00000000..f4ca71e1
--- /dev/null
+++ b/examples/cute-dogs-server/components.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/styles.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui"
+ }
+}
diff --git a/examples/cute-dogs-server/demo-images/example.png b/examples/cute-dogs-server/demo-images/example.png
new file mode 100644
index 00000000..d4ef63ed
Binary files /dev/null and b/examples/cute-dogs-server/demo-images/example.png differ
diff --git a/examples/cute-dogs-server/dog-image-view.html b/examples/cute-dogs-server/dog-image-view.html
new file mode 100644
index 00000000..0c89593a
--- /dev/null
+++ b/examples/cute-dogs-server/dog-image-view.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ MCP Show Dog Image
+
+
+
+
+
+
diff --git a/examples/cute-dogs-server/helpers.ts b/examples/cute-dogs-server/helpers.ts
new file mode 100644
index 00000000..deecf42b
--- /dev/null
+++ b/examples/cute-dogs-server/helpers.ts
@@ -0,0 +1,136 @@
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import {
+ CallToolResult,
+ ReadResourceResult,
+ Resource,
+} from "@modelcontextprotocol/sdk/types.js";
+import path from "node:path";
+import fs from "node:fs/promises";
+import { fileURLToPath } from "node:url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const distDir = path.join(__dirname, "dist");
+
+// ============================================================================
+// Helper Functions
+// ============================================================================
+
+/**
+ * Load HTML file from dist directory
+ */
+export const loadHtml = async (name: string): Promise => {
+ const htmlPath = path.join(distDir, `${name}.html`);
+ return fs.readFile(htmlPath, "utf-8");
+};
+
+/**
+ * Create an error result for tool calls
+ */
+export const createErrorResult = (
+ error: unknown,
+ message: string,
+): CallToolResult => {
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify({
+ error: error instanceof Error ? error.message : message,
+ }),
+ },
+ ],
+ isError: true,
+ };
+};
+
+// ============================================================================
+// Dog CEO API Helpers
+// ============================================================================
+
+interface DogApiResponse {
+ status: string;
+ message: string | string[];
+}
+
+/**
+ * Fetch a random dog image (optionally for a specific breed)
+ */
+export const fetchRandomDogImage = async (
+ breed?: string,
+): Promise<{ message: string; breed: string }> => {
+ const apiUrl = breed
+ ? `https://dog.ceo/api/breed/${encodeURIComponent(breed)}/images/random`
+ : "https://dog.ceo/api/breeds/image/random";
+
+ const response = await fetch(apiUrl);
+ const data = (await response.json()) as DogApiResponse;
+
+ if (data.status !== "success" || !data.message) {
+ throw new Error("Failed to fetch dog image");
+ }
+
+ const dogBreed = (data.message as string).split("/")[4];
+ return { message: data.message as string, breed: dogBreed };
+};
+
+/**
+ * Fetch all dog breeds
+ */
+export const fetchAllBreeds = async (): Promise => {
+ const response = await fetch("https://dog.ceo/api/breeds/list/all");
+ const data = (await response.json()) as DogApiResponse;
+
+ if (data.status !== "success" || typeof data.message !== "object") {
+ throw new Error("Failed to fetch breeds");
+ }
+
+ return Object.keys(data.message);
+};
+
+/**
+ * Fetch multiple random images for a breed
+ */
+export const fetchBreedImages = async (
+ breed: string,
+ count: number,
+): Promise => {
+ const apiUrl = `https://dog.ceo/api/breed/${encodeURIComponent(breed)}/images/random/${count}`;
+ const response = await fetch(apiUrl);
+ const data = (await response.json()) as DogApiResponse;
+
+ if (data.status !== "success" || !Array.isArray(data.message)) {
+ throw new Error("Failed to fetch images");
+ }
+
+ return data.message;
+};
+
+// ============================================================================
+// Resource Registration
+// ============================================================================
+
+/**
+ * Register a UI resource with the server
+ */
+export const registerResource = (
+ server: McpServer,
+ resource: Resource,
+ htmlContent: string,
+): Resource => {
+ server.registerResource(
+ resource.name,
+ resource.uri,
+ resource,
+ async (): Promise => ({
+ contents: [
+ {
+ uri: resource.uri,
+ mimeType: resource.mimeType,
+ text: htmlContent,
+ },
+ ],
+ }),
+ );
+ return resource;
+};
diff --git a/examples/cute-dogs-server/package.json b/examples/cute-dogs-server/package.json
new file mode 100644
index 00000000..5e1ba9bb
--- /dev/null
+++ b/examples/cute-dogs-server/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "cute-dogs-server",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "NODE_ENV=development npm run build && npm run server",
+ "build": "concurrently 'INPUT=dog-image-view.html vite build' 'INPUT=all-breeds-view.html vite build'",
+ "server": "bun server.ts"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.22.0",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.454.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "tailwind-merge": "^2.5.4",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.25.0"
+ },
+ "devDependencies": {
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.0.0",
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "concurrently": "^9.2.1",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.14",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.0",
+ "vite-plugin-singlefile": "^2.3.0"
+ }
+}
diff --git a/examples/cute-dogs-server/postcss.config.js b/examples/cute-dogs-server/postcss.config.js
new file mode 100644
index 00000000..2aa7205d
--- /dev/null
+++ b/examples/cute-dogs-server/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/examples/cute-dogs-server/server.ts b/examples/cute-dogs-server/server.ts
new file mode 100644
index 00000000..4b5d6c53
--- /dev/null
+++ b/examples/cute-dogs-server/server.ts
@@ -0,0 +1,301 @@
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import express, { Request, Response } from "express";
+import { randomUUID } from "node:crypto";
+import { z } from "zod";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
+import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js";
+import cors from "cors";
+import { RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps";
+import {
+ loadHtml,
+ registerResource,
+ fetchRandomDogImage,
+ fetchAllBreeds,
+ fetchBreedImages,
+ createErrorResult,
+} from "./helpers.js";
+
+// ============================================================================
+// Server Setup
+// ============================================================================
+
+/**
+ * Create and configure the MCP server
+ */
+const getServer = async () => {
+ const server = new McpServer(
+ {
+ name: "cute-dogs-mcp-server",
+ version: "1.0.0",
+ icons: [
+ {
+ src: "https://dog.ceo/img/dog-api-logo.svg",
+ mimeType: "image/svg+xml",
+ },
+ ],
+ },
+ {
+ capabilities: { logging: {} },
+ instructions:
+ "This server shows images of dogs. View all dog breeds with the `all-breeds-view` tool and widget. The dog-image-view tool and widget shows the image. The dog-image-view widget can call the `get-more-images` tool to get more images of the same breed.",
+ },
+ );
+
+ // Load HTML files
+ const [showDogImageHtml, showAllBreedsHtml] = await Promise.all([
+ loadHtml("dog-image-view"),
+ loadHtml("all-breeds-view"),
+ ]);
+
+ // Register resources
+ const randomDogResource = registerResource(
+ server,
+ {
+ name: "show-random-dog-image-template",
+ uri: "ui://show-random-dog-image",
+ title: "Show Dog Image Template",
+ description: "A show dog image UI",
+ mimeType: "text/html+mcp",
+ },
+ showDogImageHtml,
+ );
+
+ const showAllBreedsResource = registerResource(
+ server,
+ {
+ name: "all-breeds-view-template",
+ uri: "ui://all-breeds-view",
+ title: "Show All Breeds Template",
+ description: "A show all breeds UI",
+ mimeType: "text/html+mcp",
+ },
+ showAllBreedsHtml,
+ );
+
+ // Register tools
+ server.registerTool(
+ "show-random-dog-image",
+ {
+ title: "Show Dog Image",
+ description:
+ "Show a dog image in an interactive UI widget. Do not show the image in the text response. The image will be shown in the UI widget.",
+ inputSchema: {
+ breed: z
+ .string()
+ .optional()
+ .describe(
+ "Optional dog breed (e.g., 'hound', 'retriever'). If not provided, returns a random dog from any breed.",
+ ),
+ },
+ _meta: {
+ [RESOURCE_URI_META_KEY]: randomDogResource.uri,
+ },
+ },
+ async ({ breed }) => {
+ try {
+ const result = await fetchRandomDogImage(breed);
+ return {
+ content: [
+ {
+ type: "text" as const,
+ text: JSON.stringify({
+ message: `Successfully fetched ${breed} image`,
+ status: "success",
+ }),
+ },
+ ],
+ structuredContent: { ...result, status: "success" },
+ };
+ } catch (error) {
+ return createErrorResult(error, "Failed to fetch dog image");
+ }
+ },
+ );
+
+ server.registerTool(
+ "all-breeds-view",
+ {
+ title: "Show All Breeds",
+ description: "Show all breeds in an interactive UI widget.",
+ _meta: {
+ [RESOURCE_URI_META_KEY]: showAllBreedsResource.uri,
+ },
+ },
+ async () => {
+ try {
+ const breeds = await fetchAllBreeds();
+ return {
+ content: [
+ { type: "text" as const, text: JSON.stringify({ breeds }) },
+ ],
+ structuredContent: { breeds },
+ };
+ } catch (error) {
+ return createErrorResult(error, "Failed to fetch breeds");
+ }
+ },
+ );
+
+ server.registerTool(
+ "get-more-images",
+ {
+ title: "Get More Images",
+ description:
+ "Get multiple random dog images from a specific breed. Returns an array of image URLs.",
+ inputSchema: {
+ breed: z
+ .string()
+ .describe(
+ "The dog breed name (e.g., 'hound', 'retriever'). Required parameter.",
+ ),
+ count: z
+ .number()
+ .int()
+ .min(1)
+ .max(30)
+ .optional()
+ .default(3)
+ .describe(
+ "Number of images to fetch (1-30). Defaults to 3 if not provided.",
+ ),
+ },
+ },
+ async ({ breed, count = 3 }) => {
+ try {
+ const images = await fetchBreedImages(breed, count);
+ return {
+ content: [
+ {
+ type: "text" as const,
+ text: JSON.stringify({ breed, images }),
+ },
+ ],
+ structuredContent: { breed, images },
+ };
+ } catch (error) {
+ return createErrorResult(error, "Failed to fetch images");
+ }
+ },
+ );
+
+ return server;
+};
+
+// ============================================================================
+// Express Server Setup
+// ============================================================================
+
+const MCP_PORT = process.env.MCP_PORT
+ ? parseInt(process.env.MCP_PORT, 10)
+ : 3001;
+
+const app = express();
+app.use(express.json());
+app.use(
+ cors({
+ origin: "*",
+ exposedHeaders: ["Mcp-Session-Id"],
+ }),
+);
+
+// Map to store transports by session ID
+const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
+
+const mcpPostHandler = async (req: Request, res: Response) => {
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
+
+ try {
+ let transport: StreamableHTTPServerTransport;
+ if (sessionId && transports[sessionId]) {
+ transport = transports[sessionId];
+ } else if (!sessionId && isInitializeRequest(req.body)) {
+ const eventStore = new InMemoryEventStore();
+ transport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: () => randomUUID(),
+ eventStore,
+ onsessioninitialized: (sessionId) => {
+ console.log(`Session initialized: ${sessionId}`);
+ transports[sessionId] = transport;
+ },
+ });
+
+ transport.onclose = () => {
+ const sid = transport.sessionId;
+ if (sid && transports[sid]) {
+ console.log(`Session closed: ${sid}`);
+ delete transports[sid];
+ }
+ };
+
+ const server = await getServer();
+ await server.connect(transport);
+ await transport.handleRequest(req, res, req.body);
+ return;
+ } else {
+ res.status(400).json({
+ jsonrpc: "2.0",
+ error: { code: -32000, message: "Bad Request: No valid session ID" },
+ id: null,
+ });
+ return;
+ }
+
+ await transport.handleRequest(req, res, req.body);
+ } catch (error) {
+ console.error("Error handling MCP request:", error);
+ if (!res.headersSent) {
+ res.status(500).json({
+ jsonrpc: "2.0",
+ error: { code: -32603, message: "Internal server error" },
+ id: null,
+ });
+ }
+ }
+};
+
+app.post("/mcp", mcpPostHandler);
+
+app.get("/mcp", async (req: Request, res: Response) => {
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
+ if (!sessionId || !transports[sessionId]) {
+ res.status(400).send("Invalid or missing session ID");
+ return;
+ }
+ const transport = transports[sessionId];
+ await transport.handleRequest(req, res);
+});
+
+app.delete("/mcp", async (req: Request, res: Response) => {
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
+ if (!sessionId || !transports[sessionId]) {
+ res.status(400).send("Invalid or missing session ID");
+ return;
+ }
+ try {
+ const transport = transports[sessionId];
+ await transport.handleRequest(req, res);
+ } catch (error) {
+ console.error("Error handling session termination:", error);
+ if (!res.headersSent) {
+ res.status(500).send("Error processing session termination");
+ }
+ }
+});
+
+app.listen(MCP_PORT, () => {
+ console.log(`MCP Server listening on http://localhost:${MCP_PORT}/mcp`);
+});
+
+process.on("SIGINT", async () => {
+ console.log("Shutting down...");
+ for (const sessionId in transports) {
+ try {
+ await transports[sessionId].close();
+ delete transports[sessionId];
+ } catch (error) {
+ console.error(`Error closing session ${sessionId}:`, error);
+ }
+ }
+ process.exit(0);
+});
diff --git a/examples/cute-dogs-server/src/all-breeds-view.tsx b/examples/cute-dogs-server/src/all-breeds-view.tsx
new file mode 100644
index 00000000..6870d203
--- /dev/null
+++ b/examples/cute-dogs-server/src/all-breeds-view.tsx
@@ -0,0 +1,104 @@
+/**
+ * @file App that displays all dog breeds and allows selecting one to show in chat.
+ */
+import "./styles.css";
+import { useState, useCallback } from "react";
+import { createRoot } from "react-dom/client";
+import { useApp } from "@modelcontextprotocol/ext-apps/react";
+import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+
+const APP_INFO: Implementation = {
+ name: "Show All Breeds App",
+ version: "1.0.0",
+};
+
+export function AllBreedsViewApp() {
+ const [breeds, setBreeds] = useState([]);
+ const [selectedBreed, setSelectedBreed] = useState(null);
+
+ const { app } = useApp({
+ appInfo: APP_INFO,
+ capabilities: {},
+ onAppCreated: (app) => {
+ app.ontoolresult = async (toolResult) => {
+ const breeds = (toolResult.structuredContent as { breeds?: string[] })
+ ?.breeds;
+ if (breeds) setBreeds(breeds);
+ };
+ },
+ });
+
+ const handleBreedClick = useCallback(
+ async (breed: string) => {
+ if (!app) return;
+ setSelectedBreed(breed);
+ try {
+ await app.sendMessage({
+ role: "user",
+ content: [{ type: "text", text: `Show me a ${breed}` }],
+ });
+ } catch (e) {
+ console.error("Failed to send message:", e);
+ }
+ },
+ [app],
+ );
+
+ return (
+