ext-apps/examples/video-resource-server/src/mcp-app.ts
Anton Pidkuiko MacBook ba45157d55
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>
2026-01-06 16:43:46 +00:00

124 lines
3.6 KiB
TypeScript

/**
* 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();