Merge pull request #391 from jonathanhefner/add-blob-resource-to-patterns-guide
Add binary blob resource pattern to patterns guide
This commit is contained in:
commit
ef46849c09
2 changed files with 105 additions and 2 deletions
|
|
@ -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:
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
```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<ReadResourceResult> => {
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
```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)
|
## 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:
|
The host provides context about its environment via {@link types!McpUiHostContext `McpUiHostContext`}. Use this to adapt your app's appearance and layout:
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,19 @@ import {
|
||||||
applyHostStyleVariables,
|
applyHostStyleVariables,
|
||||||
} from "../src/styles.js";
|
} from "../src/styles.js";
|
||||||
import { randomUUID } from "node:crypto";
|
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 type { McpUiHostContext } from "../src/types.js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useApp } from "../src/react/index.js";
|
import { useApp } from "../src/react/index.js";
|
||||||
import { registerAppTool } from "../src/server/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";
|
import { z } from "zod";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -213,6 +220,53 @@ function chunkedDataClient(app: App, resourceId: string) {
|
||||||
//#endregion chunkedDataClient
|
//#endregion chunkedDataClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Serving binary blobs via resources (server-side)
|
||||||
|
*/
|
||||||
|
function binaryBlobResourceServer(
|
||||||
|
server: McpServer,
|
||||||
|
getVideoData: (id: string) => Promise<ArrayBuffer>,
|
||||||
|
) {
|
||||||
|
//#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<ReadResourceResult> => {
|
||||||
|
// 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)
|
* Example: Adapting to host context (theme, CSS variables, fonts, safe areas)
|
||||||
*/
|
*/
|
||||||
|
|
@ -406,6 +460,8 @@ void pollingVanillaJs;
|
||||||
void pollingReact;
|
void pollingReact;
|
||||||
void chunkedDataServer;
|
void chunkedDataServer;
|
||||||
void chunkedDataClient;
|
void chunkedDataClient;
|
||||||
|
void binaryBlobResourceServer;
|
||||||
|
void binaryBlobResourceClient;
|
||||||
void hostContextVanillaJs;
|
void hostContextVanillaJs;
|
||||||
void hostContextReact;
|
void hostContextReact;
|
||||||
void persistViewStateServer;
|
void persistViewStateServer;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue