Skip to content

Commit ef46849

Browse files
Merge pull request modelcontextprotocol#391 from jonathanhefner/add-blob-resource-to-patterns-guide
Add binary blob resource pattern to patterns guide
2 parents 246e1ab + 1361561 commit ef46849

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

docs/patterns.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,53 @@ try {
249249
}
250250
```
251251

252+
## Serving binary blobs via resources
253+
254+
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.
255+
256+
**Server-side**: Register a resource that returns binary data in the `blob` field:
257+
258+
<!-- prettier-ignore -->
259+
```ts source="./patterns.tsx#binaryBlobResourceServer"
260+
server.registerResource(
261+
"Video",
262+
new ResourceTemplate("video://{id}", { list: undefined }),
263+
{
264+
description: "Video data served as base64 blob",
265+
mimeType: "video/mp4",
266+
},
267+
async (uri, { id }): Promise<ReadResourceResult> => {
268+
// Fetch or load your binary data
269+
const idString = Array.isArray(id) ? id[0] : id;
270+
const buffer = await getVideoData(idString);
271+
const blob = Buffer.from(buffer).toString("base64");
272+
273+
return { contents: [{ uri: uri.href, mimeType: "video/mp4", blob }] };
274+
},
275+
);
276+
```
277+
278+
**Client-side**: Fetch the resource and convert the base64 blob to a data URI:
279+
280+
<!-- prettier-ignore -->
281+
```ts source="./patterns.tsx#binaryBlobResourceClient"
282+
const result = await app.request(
283+
{ method: "resources/read", params: { uri: `video://${videoId}` } },
284+
ReadResourceResultSchema,
285+
);
286+
287+
const content = result.contents[0];
288+
if (!content || !("blob" in content)) {
289+
throw new Error("Resource did not contain blob data");
290+
}
291+
292+
const videoEl = document.querySelector("video")!;
293+
videoEl.src = `data:${content.mimeType!};base64,${content.blob}`;
294+
```
295+
296+
> [!NOTE]
297+
> 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).
298+
252299
## Adapting to host context (theme, styling, fonts, and safe areas)
253300

254301
The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout:

docs/patterns.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@ import {
1414
applyHostStyleVariables,
1515
} from "../src/styles.js";
1616
import { randomUUID } from "node:crypto";
17-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
17+
import type {
18+
CallToolResult,
19+
ReadResourceResult,
20+
} from "@modelcontextprotocol/sdk/types.js";
21+
import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js";
1822
import type { McpUiHostContext } from "../src/types.js";
1923
import { useEffect, useState } from "react";
2024
import { useApp } from "../src/react/index.js";
2125
import { registerAppTool } from "../src/server/index.js";
22-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26+
import {
27+
McpServer,
28+
ResourceTemplate,
29+
} from "@modelcontextprotocol/sdk/server/mcp.js";
2330
import { z } from "zod";
2431

2532
/**
@@ -213,6 +220,53 @@ function chunkedDataClient(app: App, resourceId: string) {
213220
//#endregion chunkedDataClient
214221
}
215222

223+
/**
224+
* Example: Serving binary blobs via resources (server-side)
225+
*/
226+
function binaryBlobResourceServer(
227+
server: McpServer,
228+
getVideoData: (id: string) => Promise<ArrayBuffer>,
229+
) {
230+
//#region binaryBlobResourceServer
231+
server.registerResource(
232+
"Video",
233+
new ResourceTemplate("video://{id}", { list: undefined }),
234+
{
235+
description: "Video data served as base64 blob",
236+
mimeType: "video/mp4",
237+
},
238+
async (uri, { id }): Promise<ReadResourceResult> => {
239+
// Fetch or load your binary data
240+
const idString = Array.isArray(id) ? id[0] : id;
241+
const buffer = await getVideoData(idString);
242+
const blob = Buffer.from(buffer).toString("base64");
243+
244+
return { contents: [{ uri: uri.href, mimeType: "video/mp4", blob }] };
245+
},
246+
);
247+
//#endregion binaryBlobResourceServer
248+
}
249+
250+
/**
251+
* Example: Serving binary blobs via resources (client-side)
252+
*/
253+
async function binaryBlobResourceClient(app: App, videoId: string) {
254+
//#region binaryBlobResourceClient
255+
const result = await app.request(
256+
{ method: "resources/read", params: { uri: `video://${videoId}` } },
257+
ReadResourceResultSchema,
258+
);
259+
260+
const content = result.contents[0];
261+
if (!content || !("blob" in content)) {
262+
throw new Error("Resource did not contain blob data");
263+
}
264+
265+
const videoEl = document.querySelector("video")!;
266+
videoEl.src = `data:${content.mimeType!};base64,${content.blob}`;
267+
//#endregion binaryBlobResourceClient
268+
}
269+
216270
/**
217271
* Example: Adapting to host context (theme, CSS variables, fonts, safe areas)
218272
*/
@@ -406,6 +460,8 @@ void pollingVanillaJs;
406460
void pollingReact;
407461
void chunkedDataServer;
408462
void chunkedDataClient;
463+
void binaryBlobResourceServer;
464+
void binaryBlobResourceClient;
409465
void hostContextVanillaJs;
410466
void hostContextReact;
411467
void persistViewStateServer;

0 commit comments

Comments
 (0)