feat(examples): Add video-resource-server example

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

1. Server fetches video from CDN
2. Returns as base64 blob via `videos://{id}` resource template
3. Widget fetches resource and plays in `<video>` element

Includes multiple video sizes for testing (1MB to 150MB).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Anton Pidkuiko MacBook 2025-12-17 15:14:19 +00:00
commit ba45157d55
No known key found for this signature in database
3 changed files with 554 additions and 0 deletions

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,124 @@
/**
* Video Resource Player
*
* Demonstrates fetching binary content (video) via MCP resources.
* The video is served as a base64 blob and converted to a data URI for playback.
*/
import { App } from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js";
import "./global.css";
import "./mcp-app.css";
const log = {
info: console.log.bind(console, "[VIDEO]"),
error: console.error.bind(console, "[VIDEO]"),
};
// Get element references
const loadingEl = document.getElementById("loading")!;
const loadingTextEl = document.getElementById("loading-text")!;
const errorEl = document.getElementById("error")!;
const errorMessageEl = document.getElementById("error-message")!;
const playerEl = document.getElementById("player")!;
const videoEl = document.getElementById("video") as HTMLVideoElement;
const videoInfoEl = document.getElementById("video-info")!;
// Parse tool result to get video URI
function parseToolResult(
result: CallToolResult,
): { videoUri: string; description: string } | null {
const text = result.content
?.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
if (!text) return null;
try {
return JSON.parse(text) as { videoUri: string; description: string };
} catch {
return null;
}
}
// Show states
function showLoading(text: string) {
loadingTextEl.textContent = text;
loadingEl.style.display = "flex";
errorEl.style.display = "none";
playerEl.style.display = "none";
}
function showError(message: string) {
errorMessageEl.textContent = message;
loadingEl.style.display = "none";
errorEl.style.display = "block";
playerEl.style.display = "none";
}
function showPlayer(dataUri: string, info: string) {
videoEl.src = dataUri;
videoInfoEl.textContent = info;
loadingEl.style.display = "none";
errorEl.style.display = "none";
playerEl.style.display = "block";
}
// Create app instance
const app = new App({ name: "Video Resource Player", version: "1.0.0" });
// Handle tool result - this is called when the tool execution completes
app.ontoolresult = async (result) => {
log.info("Received tool result:", result);
const parsed = parseToolResult(result);
if (!parsed) {
showError("Invalid tool result - could not parse video URI");
return;
}
const { videoUri, description } = parsed;
log.info("Video URI:", videoUri, "Description:", description);
showLoading("Fetching video from MCP resource...");
try {
log.info("Requesting resource:", videoUri);
const resourceResult = await app.request(
{ method: "resources/read", params: { uri: videoUri } },
ReadResourceResultSchema,
);
log.info(
"Resource received, blob size:",
resourceResult.contents[0]?.blob?.length,
);
const content = resourceResult.contents[0];
if (!content?.blob) {
throw new Error("Resource response did not contain blob data");
}
showLoading("Converting to data URI...");
const mimeType = content.mimeType || "video/mp4";
const dataUri = `data:${mimeType};base64,${content.blob}`;
log.info("Data URI created, length:", dataUri.length);
showPlayer(dataUri, `Loaded via MCP resource (${description})`);
} catch (err) {
log.error("Error fetching resource:", err);
showError(err instanceof Error ? err.message : String(err));
}
};
app.onerror = (err) => {
log.error("App error:", err);
showError(err instanceof Error ? err.message : String(err));
};
// Connect to host
app.connect();