feat: add readServerResource() and listServerResources() api to App (#470)
* feat: add readServerResource() and listServerResources() api to App class * test: add listServerResources and readServerResource integration tests
This commit is contained in:
parent
62c75203b6
commit
4f9a7ebc74
3 changed files with 502 additions and 40 deletions
|
|
@ -779,6 +779,27 @@ describe("App <-> AppBridge integration", () => {
|
|||
expect(result.resources).toEqual(resources);
|
||||
});
|
||||
|
||||
it("onlistresources handles listServerResources() calls from App", async () => {
|
||||
const resources = [
|
||||
{ uri: "test://res-1", name: "Resource 1" },
|
||||
{ uri: "test://res-2", name: "Resource 2", mimeType: "video/mp4" },
|
||||
];
|
||||
const receivedRequests: unknown[] = [];
|
||||
|
||||
bridge.onlistresources = async (params) => {
|
||||
receivedRequests.push(params);
|
||||
return { resources };
|
||||
};
|
||||
|
||||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
||||
const result = await app.listServerResources({});
|
||||
|
||||
expect(receivedRequests).toHaveLength(1);
|
||||
expect(result.resources).toEqual(resources);
|
||||
});
|
||||
|
||||
it("onreadresource setter registers handler for resources/read requests", async () => {
|
||||
const requestParams = { uri: "test://resource" };
|
||||
const contents = [{ uri: "test://resource", text: "content" }];
|
||||
|
|
@ -802,6 +823,28 @@ describe("App <-> AppBridge integration", () => {
|
|||
expect(result.contents).toEqual(contents);
|
||||
});
|
||||
|
||||
it("onreadresource handles readServerResource() calls from App", async () => {
|
||||
const requestParams = { uri: "videos://bunny-1mb" };
|
||||
const contents = [
|
||||
{ uri: "videos://bunny-1mb", blob: "dmlkZW9kYXRh", mimeType: "video/mp4" },
|
||||
];
|
||||
const receivedRequests: unknown[] = [];
|
||||
|
||||
bridge.onreadresource = async (params) => {
|
||||
receivedRequests.push(params);
|
||||
return { contents };
|
||||
};
|
||||
|
||||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
||||
const result = await app.readServerResource(requestParams);
|
||||
|
||||
expect(receivedRequests).toHaveLength(1);
|
||||
expect(receivedRequests[0]).toMatchObject(requestParams);
|
||||
expect(result.contents).toEqual(contents);
|
||||
});
|
||||
|
||||
it("onlistresourcetemplates setter registers handler for resources/templates/list requests", async () => {
|
||||
const requestParams = {};
|
||||
const resourceTemplates = [
|
||||
|
|
|
|||
|
|
@ -297,6 +297,60 @@ async function App_callServerTool_fetchWeather(app: App) {
|
|||
//#endregion App_callServerTool_fetchWeather
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Read a video resource and play it.
|
||||
*/
|
||||
async function App_readServerResource_playVideo(
|
||||
app: App,
|
||||
videoElement: HTMLVideoElement,
|
||||
) {
|
||||
//#region App_readServerResource_playVideo
|
||||
try {
|
||||
const result = await app.readServerResource({
|
||||
uri: "videos://bunny-1mb",
|
||||
});
|
||||
const content = result.contents[0];
|
||||
if (content && "blob" in content) {
|
||||
const binary = Uint8Array.from(atob(content.blob), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([binary], { type: content.mimeType || "video/mp4" }),
|
||||
);
|
||||
videoElement.src = url;
|
||||
videoElement.play();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to read resource:", error);
|
||||
}
|
||||
//#endregion App_readServerResource_playVideo
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Discover available videos and build a picker UI.
|
||||
*/
|
||||
async function App_listServerResources_buildPicker(
|
||||
app: App,
|
||||
selectElement: HTMLSelectElement,
|
||||
) {
|
||||
//#region App_listServerResources_buildPicker
|
||||
try {
|
||||
const result = await app.listServerResources({});
|
||||
const videoResources = result.resources.filter((r) =>
|
||||
r.mimeType?.startsWith("video/"),
|
||||
);
|
||||
videoResources.forEach((resource) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = resource.uri;
|
||||
option.textContent = resource.description || resource.name;
|
||||
selectElement.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to list resources:", error);
|
||||
}
|
||||
//#endregion App_listServerResources_buildPicker
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Send a text message from user interaction.
|
||||
*/
|
||||
|
|
|
|||
104
src/app.ts
104
src/app.ts
|
|
@ -11,10 +11,16 @@ import {
|
|||
CallToolResultSchema,
|
||||
EmptyResultSchema,
|
||||
Implementation,
|
||||
ListResourcesRequest,
|
||||
ListResourcesResult,
|
||||
ListResourcesResultSchema,
|
||||
ListToolsRequest,
|
||||
ListToolsRequestSchema,
|
||||
LoggingMessageNotification,
|
||||
PingRequestSchema,
|
||||
ReadResourceRequest,
|
||||
ReadResourceResult,
|
||||
ReadResourceResultSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { AppNotification, AppRequest, AppResult } from "./types";
|
||||
import { PostMessageTransport } from "./message-transport";
|
||||
|
|
@ -739,6 +745,104 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a resource from the originating MCP server (proxied through the host).
|
||||
*
|
||||
* Apps can read resources to access files, data, or other content provided by
|
||||
* the MCP server. Resources are identified by URI (e.g., `file:///path/to/file`
|
||||
* or custom schemes like `videos://bunny-1mb`). The host proxies the request to
|
||||
* the actual MCP server and returns the resource content.
|
||||
*
|
||||
* @param params - Resource URI to read
|
||||
* @param options - Request options (timeout, etc.)
|
||||
* @returns Resource content with URI, name, description, mimeType, and contents array
|
||||
*
|
||||
* @throws {Error} If the resource does not exist on the server
|
||||
* @throws {Error} If the request times out or the connection is lost
|
||||
* @throws {Error} If the host rejects the request
|
||||
*
|
||||
* @example Read a video resource and play it
|
||||
* ```ts source="./app.examples.ts#App_readServerResource_playVideo"
|
||||
* try {
|
||||
* const result = await app.readServerResource({
|
||||
* uri: "videos://bunny-1mb",
|
||||
* });
|
||||
* const content = result.contents[0];
|
||||
* if (content && "blob" in content) {
|
||||
* const binary = Uint8Array.from(atob(content.blob), (c) =>
|
||||
* c.charCodeAt(0),
|
||||
* );
|
||||
* const url = URL.createObjectURL(
|
||||
* new Blob([binary], { type: content.mimeType || "video/mp4" }),
|
||||
* );
|
||||
* videoElement.src = url;
|
||||
* videoElement.play();
|
||||
* }
|
||||
* } catch (error) {
|
||||
* console.error("Failed to read resource:", error);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link listServerResources `listServerResources`} to discover available resources
|
||||
*/
|
||||
async readServerResource(
|
||||
params: ReadResourceRequest["params"],
|
||||
options?: RequestOptions,
|
||||
): Promise<ReadResourceResult> {
|
||||
return await this.request(
|
||||
{ method: "resources/read", params },
|
||||
ReadResourceResultSchema,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List available resources from the originating MCP server (proxied through the host).
|
||||
*
|
||||
* Apps can list resources to discover what content is available on the MCP server.
|
||||
* This enables dynamic resource discovery and building resource browsers or pickers.
|
||||
* The host proxies the request to the actual MCP server and returns the resource list.
|
||||
*
|
||||
* Results may be paginated using the `cursor` parameter for servers with many resources.
|
||||
*
|
||||
* @param params - Optional parameters (empty object `{}` for all resources, or `{ cursor }` for pagination)
|
||||
* @param options - Request options (timeout, etc.)
|
||||
* @returns List of resources with their URIs, names, descriptions, mimeTypes, and optional pagination cursor
|
||||
*
|
||||
* @throws {Error} If the request times out or the connection is lost
|
||||
* @throws {Error} If the host rejects the request
|
||||
*
|
||||
* @example Discover available videos and build a picker UI
|
||||
* ```ts source="./app.examples.ts#App_listServerResources_buildPicker"
|
||||
* try {
|
||||
* const result = await app.listServerResources({});
|
||||
* const videoResources = result.resources.filter((r) =>
|
||||
* r.mimeType?.startsWith("video/"),
|
||||
* );
|
||||
* videoResources.forEach((resource) => {
|
||||
* const option = document.createElement("option");
|
||||
* option.value = resource.uri;
|
||||
* option.textContent = resource.description || resource.name;
|
||||
* selectElement.appendChild(option);
|
||||
* });
|
||||
* } catch (error) {
|
||||
* console.error("Failed to list resources:", error);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link readServerResource `readServerResource`} to read a specific resource
|
||||
*/
|
||||
async listServerResources(
|
||||
params: ListResourcesRequest["params"],
|
||||
options?: RequestOptions,
|
||||
): Promise<ListResourcesResult> {
|
||||
return await this.request(
|
||||
{ method: "resources/list", params },
|
||||
ListResourcesResultSchema,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the host's chat interface.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue