diff --git a/docs/patterns.md b/docs/patterns.md index 55952fe..9ac20cf 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -249,6 +249,53 @@ try { } ``` +## Serving binary blobs via resources + +Binary content (e.g., video) can be served via MCP resources as base64-encoded blobs. The server returns the data in the `blob` field of the resource content, and the App fetches it via `resources/read` for use in the browser. + +**Server-side**: Register a resource that returns binary data in the `blob` field: + + +```ts source="./patterns.tsx#binaryBlobResourceServer" +server.registerResource( + "Video", + new ResourceTemplate("video://{id}", { list: undefined }), + { + description: "Video data served as base64 blob", + mimeType: "video/mp4", + }, + async (uri, { id }): Promise => { + // Fetch or load your binary data + const idString = Array.isArray(id) ? id[0] : id; + const buffer = await getVideoData(idString); + const blob = Buffer.from(buffer).toString("base64"); + + return { contents: [{ uri: uri.href, mimeType: "video/mp4", blob }] }; + }, +); +``` + +**Client-side**: Fetch the resource and convert the base64 blob to a data URI: + + +```ts source="./patterns.tsx#binaryBlobResourceClient" +const result = await app.request( + { method: "resources/read", params: { uri: `video://${videoId}` } }, + ReadResourceResultSchema, +); + +const content = result.contents[0]; +if (!content || !("blob" in content)) { + throw new Error("Resource did not contain blob data"); +} + +const videoEl = document.querySelector("video")!; +videoEl.src = `data:${content.mimeType!};base64,${content.blob}`; +``` + +> [!NOTE] +> For a full example that implements this pattern, see: [`examples/video-resource-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/video-resource-server). + ## Adapting to host context (theme, styling, fonts, and safe areas) The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout: diff --git a/docs/patterns.tsx b/docs/patterns.tsx index f575eee..6b87692 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -14,12 +14,19 @@ import { applyHostStyleVariables, } from "../src/styles.js"; import { randomUUID } from "node:crypto"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { + CallToolResult, + ReadResourceResult, +} from "@modelcontextprotocol/sdk/types.js"; +import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js"; import type { McpUiHostContext } from "../src/types.js"; import { useEffect, useState } from "react"; import { useApp } from "../src/react/index.js"; import { registerAppTool } from "../src/server/index.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + McpServer, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; /** @@ -213,6 +220,53 @@ function chunkedDataClient(app: App, resourceId: string) { //#endregion chunkedDataClient } +/** + * Example: Serving binary blobs via resources (server-side) + */ +function binaryBlobResourceServer( + server: McpServer, + getVideoData: (id: string) => Promise, +) { + //#region binaryBlobResourceServer + server.registerResource( + "Video", + new ResourceTemplate("video://{id}", { list: undefined }), + { + description: "Video data served as base64 blob", + mimeType: "video/mp4", + }, + async (uri, { id }): Promise => { + // Fetch or load your binary data + const idString = Array.isArray(id) ? id[0] : id; + const buffer = await getVideoData(idString); + const blob = Buffer.from(buffer).toString("base64"); + + return { contents: [{ uri: uri.href, mimeType: "video/mp4", blob }] }; + }, + ); + //#endregion binaryBlobResourceServer +} + +/** + * Example: Serving binary blobs via resources (client-side) + */ +async function binaryBlobResourceClient(app: App, videoId: string) { + //#region binaryBlobResourceClient + const result = await app.request( + { method: "resources/read", params: { uri: `video://${videoId}` } }, + ReadResourceResultSchema, + ); + + const content = result.contents[0]; + if (!content || !("blob" in content)) { + throw new Error("Resource did not contain blob data"); + } + + const videoEl = document.querySelector("video")!; + videoEl.src = `data:${content.mimeType!};base64,${content.blob}`; + //#endregion binaryBlobResourceClient +} + /** * Example: Adapting to host context (theme, CSS variables, fonts, safe areas) */ @@ -406,6 +460,8 @@ void pollingVanillaJs; void pollingReact; void chunkedDataServer; void chunkedDataClient; +void binaryBlobResourceServer; +void binaryBlobResourceClient; void hostContextVanillaJs; void hostContextReact; void persistViewStateServer;