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:
parent
d925af5257
commit
ba45157d55
3 changed files with 554 additions and 0 deletions
10
examples/video-resource-server/src/global.css
Normal file
10
examples/video-resource-server/src/global.css
Normal 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;
|
||||
}
|
||||
54
examples/video-resource-server/src/mcp-app.css
Normal file
54
examples/video-resource-server/src/mcp-app.css
Normal 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;
|
||||
}
|
||||
124
examples/video-resource-server/src/mcp-app.ts
Normal file
124
examples/video-resource-server/src/mcp-app.ts
Normal 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();
|
||||
Loading…
Reference in a new issue