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
22 changes: 22 additions & 0 deletions examples/video-resource-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Video Resource Server

Demonstrates serving binary content (video) via MCP resources using the base64 blob pattern.

## Quick Start

```bash
npm install
npm run dev
```

## Tools

- **play_video** - Plays a video loaded via MCP resource
- `videoId`: Choose from various sizes (`bunny-1mb`, `bunny-5mb`, `bunny-10mb`, etc.)

## How It Works

1. The `play_video` tool returns a `videoUri` pointing to an MCP resource
2. The widget fetches the resource via `resources/read`
3. The server fetches the video from CDN and returns it as a base64 blob
4. The widget decodes the blob and plays it in a `<video>` element
28 changes: 28 additions & 0 deletions examples/video-resource-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>Video Resource Player</title>
</head>
<body>
<main class="main">
<div id="loading" class="loading">
<div class="spinner"></div>
<p id="loading-text">Waiting for video...</p>
</div>

<div id="error" class="error" style="display: none;">
<p class="error-title">Error loading video</p>
<p id="error-message"></p>
</div>

<div id="player" class="player" style="display: none;">
<video id="video" controls></video>
<p id="video-info" class="video-info"></p>
</div>
</main>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions examples/video-resource-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "video-resource-server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "cross-env INPUT=mcp-app.html vite build",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun server.ts",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "../..",
"@modelcontextprotocol/sdk": "^1.24.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"express": "^5.1.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
}
}
187 changes: 187 additions & 0 deletions examples/video-resource-server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* Video Resource Server
*
* Demonstrates serving binary content (video) via MCP resources.
* The server fetches videos from CDN and serves them as base64 blobs.
*/
import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import type {
CallToolResult,
ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import fs from "node:fs/promises";
import path from "node:path";
import {
registerAppTool,
registerAppResource,
RESOURCE_MIME_TYPE,
RESOURCE_URI_META_KEY,
} from "@modelcontextprotocol/ext-apps/server";
import { startServer } from "../shared/server-utils.js";

const DIST_DIR = path.join(import.meta.dirname, "dist");
const RESOURCE_URI = "ui://video-player/mcp-app.html";

/**
* Video library with different sizes for testing.
*/
const VIDEO_LIBRARY: Record<string, { url: string; description: string }> = {
"bunny-1mb": {
url: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4",
description: "1MB",
},
"bunny-5mb": {
url: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_5MB.mp4",
description: "5MB",
},
"bunny-10mb": {
url: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_10MB.mp4",
description: "10MB",
},
"bunny-20mb": {
url: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_20MB.mp4",
description: "20MB",
},
"bunny-30mb": {
url: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_30MB.mp4",
description: "30MB",
},
"bunny-50mb": {
url: "https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_50mb.mp4",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This URL isn't loading for me.

I also tried https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_50MB.mp4, but that's 404.

description: "50MB",
},
"bunny-150mb": {
url: "https://cdn.jsdelivr.net/npm/[email protected]/video.mp4",
description: "~150MB (full 1080p)",
},
};

function createServer(): McpServer {
const server = new McpServer({
name: "Video Resource Server",
version: "1.0.0",
});

// Register video resource template
// This fetches video from CDN and returns as base64 blob
server.resource(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to use server.registerResource() here (resource() is deprecated).

"video",
new ResourceTemplate("videos://{id}", { list: undefined }),
{
description: "Video served via MCP resource (base64 blob)",
mimeType: "video/mp4",
},
async (uri, { id }): Promise<ReadResourceResult> => {
const idStr = Array.isArray(id) ? id[0] : id;
const video = VIDEO_LIBRARY[idStr];

if (!video) {
throw new Error(
`Video not found: ${idStr}. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`,
);
}

console.error(`[video-resource] Fetching: ${video.url}`);

const response = await fetch(video.url);
if (!response.ok) {
throw new Error(
`Failed to fetch video: ${response.status} ${response.statusText}`,
);
}

const buffer = await response.arrayBuffer();
const base64 = Buffer.from(buffer).toString("base64");

console.error(
`[video-resource] Size: ${buffer.byteLength} bytes -> ${base64.length} base64 chars`,
);

return {
contents: [
{
uri: uri.href,
mimeType: "video/mp4",
blob: base64,
},
],
};
},
);

// Register the video player tool
registerAppTool(
server,
"play_video",
{
title: "Play Video via Resource",
description: `Play a video loaded via MCP resource.
Available videos:
${Object.entries(VIDEO_LIBRARY)
.map(([id, v]) => `- ${id}: ${v.description}`)
.join("\n")}`,
inputSchema: {
videoId: z
.enum(Object.keys(VIDEO_LIBRARY) as [string, ...string[]])
.describe(
`Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`,
),
},
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
},
async ({ videoId }): Promise<CallToolResult> => {
const video = VIDEO_LIBRARY[videoId];
return {
content: [
{
type: "text",
text: JSON.stringify({
videoUri: `videos://${videoId}`,
description: video.description,
}),
},
],
};
},
);

// Register the MCP App resource (the UI)
registerAppResource(
server,
RESOURCE_URI,
RESOURCE_URI,
{ mimeType: RESOURCE_MIME_TYPE },
async (): Promise<ReadResourceResult> => {
const html = await fs.readFile(
path.join(DIST_DIR, "mcp-app.html"),
"utf-8",
);
return {
contents: [
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);

return server;
}

async function main() {
if (process.argv.includes("--stdio")) {
await createServer().connect(new StdioServerTransport());
} else {
const port = parseInt(process.env.PORT ?? "3105", 10);
await startServer(createServer, { port, name: "Video Resource Server" });
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
10 changes: 10 additions & 0 deletions examples/video-resource-server/src/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
* {
box-sizing: border-box;
}

html, body {
font-family: system-ui, -apple-system, sans-serif;
font-size: 1rem;
margin: 0;
padding: 0;
}
54 changes: 54 additions & 0 deletions examples/video-resource-server/src/mcp-app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.main {
width: 100%;
max-width: 640px;
}

.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
gap: 1rem;
color: #6b7280;
}

.spinner {
width: 48px;
height: 48px;
border: 4px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
to { transform: rotate(360deg); }
}

.error {
padding: 1rem;
color: #dc2626;
}

.error-title {
font-weight: 600;
margin: 0 0 0.5rem 0;
}

.error p {
margin: 0;
}

.player video {
width: 100%;
max-height: 480px;
border-radius: 8px;
background-color: #000;
}

.video-info {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6b7280;
}
Loading
Loading