Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions examples/cute-dogs-server/README.md
Original file line number Diff line number Diff line change
@@ -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

![Example of the Cute Dogs MCP Server in action](demo-images/example.png)

## 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
12 changes: 12 additions & 0 deletions examples/cute-dogs-server/all-breeds-view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Show all breeds</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/all-breeds-view.tsx"></script>
</body>
</html>
18 changes: 18 additions & 0 deletions examples/cute-dogs-server/components.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Binary file added examples/cute-dogs-server/demo-images/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/cute-dogs-server/dog-image-view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP Show Dog Image</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/dog-image-view.tsx"></script>
</body>
</html>
136 changes: 136 additions & 0 deletions examples/cute-dogs-server/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
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<string[]> => {
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<string[]> => {
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<ReadResourceResult> => ({
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType,
text: htmlContent,
},
],
}),
);
return resource;
};
39 changes: 39 additions & 0 deletions examples/cute-dogs-server/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
6 changes: 6 additions & 0 deletions examples/cute-dogs-server/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Loading