Merge pull request #475 from modelcontextprotocol/feat/download-file
feat: add ui/download-file method for host-mediated file downloads
This commit is contained in:
commit
e4a9766592
12 changed files with 645 additions and 0 deletions
|
|
@ -16,6 +16,7 @@ const DIST_DIR = import.meta.filename.endsWith(".ts")
|
|||
? path.join(import.meta.dirname, "dist")
|
||||
: import.meta.dirname;
|
||||
const RESOURCE_URI = "ui://get-time/mcp-app.html";
|
||||
const SAMPLE_DOWNLOAD_URI = "resource:///sample-report.txt";
|
||||
|
||||
/**
|
||||
* Creates a new MCP server instance with tools and resources registered.
|
||||
|
|
@ -70,5 +71,28 @@ export function createServer(): McpServer {
|
|||
},
|
||||
);
|
||||
|
||||
// Sample downloadable resource — used to demo ResourceLink in ui/download-file
|
||||
server.resource(
|
||||
SAMPLE_DOWNLOAD_URI,
|
||||
SAMPLE_DOWNLOAD_URI,
|
||||
{
|
||||
mimeType: "text/plain",
|
||||
},
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
const content = [
|
||||
"Integration Test Server — Sample Report",
|
||||
`Generated: ${new Date().toISOString()}`,
|
||||
"",
|
||||
"This file was downloaded via MCP ResourceLink.",
|
||||
"The host resolved it by calling resources/read on the server.",
|
||||
].join("\n");
|
||||
return {
|
||||
contents: [
|
||||
{ uri: SAMPLE_DOWNLOAD_URI, mimeType: "text/plain", text: content },
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,45 @@ function GetTimeAppInner({
|
|||
log.info("Open link request", isError ? "rejected" : "accepted");
|
||||
}, [app, linkUrl]);
|
||||
|
||||
const canDownload = app.getHostCapabilities()?.downloadFile !== undefined;
|
||||
|
||||
const handleDownloadFile = useCallback(async () => {
|
||||
const sampleContent = JSON.stringify(
|
||||
{ time: serverTime, exported: new Date().toISOString() },
|
||||
null,
|
||||
2,
|
||||
);
|
||||
log.info("Requesting file download...");
|
||||
const { isError } = await app.downloadFile({
|
||||
contents: [
|
||||
{
|
||||
type: "resource",
|
||||
resource: {
|
||||
uri: "file:///export.json",
|
||||
mimeType: "application/json",
|
||||
text: sampleContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
log.info("Download", isError ? "rejected" : "accepted");
|
||||
}, [app, serverTime]);
|
||||
|
||||
const handleDownloadLink = useCallback(async () => {
|
||||
log.info("Requesting resource link download...");
|
||||
const { isError } = await app.downloadFile({
|
||||
contents: [
|
||||
{
|
||||
type: "resource_link",
|
||||
uri: "resource:///sample-report.txt",
|
||||
name: "sample-report.txt",
|
||||
mimeType: "text/plain",
|
||||
},
|
||||
],
|
||||
});
|
||||
log.info("Resource link download", isError ? "rejected" : "accepted");
|
||||
}, [app]);
|
||||
|
||||
return (
|
||||
<main
|
||||
className={styles.main}
|
||||
|
|
@ -182,6 +221,16 @@ function GetTimeAppInner({
|
|||
/>
|
||||
<button onClick={handleOpenLink}>Open Link</button>
|
||||
</div>
|
||||
|
||||
{canDownload && (
|
||||
<div className={styles.action}>
|
||||
<p>Download file via EmbeddedResource or ResourceLink</p>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button onClick={handleDownloadFile}>Embedded</button>
|
||||
<button onClick={handleDownloadLink}>Link</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,10 @@ const JSON_SCHEMA_OUTPUT_FILE = join(GENERATED_DIR, "schema.json");
|
|||
const EXTERNAL_TYPE_SCHEMAS = [
|
||||
"ContentBlockSchema",
|
||||
"CallToolResultSchema",
|
||||
"EmbeddedResourceSchema",
|
||||
"ImplementationSchema",
|
||||
"RequestIdSchema",
|
||||
"ResourceLinkSchema",
|
||||
"ToolSchema",
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -648,6 +648,8 @@ interface HostCapabilities {
|
|||
experimental?: {};
|
||||
/** Host supports opening external URLs. */
|
||||
openLinks?: {};
|
||||
/** Host supports file downloads via ui/download-file. */
|
||||
downloadFile?: {};
|
||||
/** Host can proxy tool calls to the MCP server. */
|
||||
serverTools?: {
|
||||
/** Host supports tools/list_changed notifications. */
|
||||
|
|
@ -1013,6 +1015,73 @@ MCP Apps introduces additional JSON-RPC methods for UI-specific functionality:
|
|||
|
||||
Host SHOULD open the URL in the user's default browser or a new tab.
|
||||
|
||||
`ui/download-file` - Request host to download a file
|
||||
|
||||
```typescript
|
||||
// Request (EmbeddedResource — inline content)
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "ui/download-file",
|
||||
params: {
|
||||
contents: [
|
||||
{
|
||||
type: "resource",
|
||||
resource: {
|
||||
uri: "file:///export.json", // Used for suggested filename
|
||||
mimeType: "application/json",
|
||||
text: "{ ... }" // Text content (or `blob` for base64 binary)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Request (ResourceLink — host fetches)
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "ui/download-file",
|
||||
params: {
|
||||
contents: [
|
||||
{
|
||||
type: "resource_link",
|
||||
uri: "https://api.example.com/reports/q4.pdf",
|
||||
name: "Q4 Report",
|
||||
mimeType: "application/pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Success Response
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
result: {} // Empty result on success
|
||||
}
|
||||
|
||||
// Error Response (if denied or failed)
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
error: {
|
||||
code: -32000, // Implementation-defined error
|
||||
message: "Download denied by user" | "Invalid content" | "Policy violation"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
MCP Apps run in sandboxed iframes where direct file downloads are blocked (`allow-downloads` is not set). `ui/download-file` provides a host-mediated mechanism for apps to offer file exports — useful for visualization tools (SVG/PNG export), document editors, data analysis tools, and any app that produces downloadable artifacts.
|
||||
|
||||
The `contents` array uses standard MCP resource types (`EmbeddedResource` and `ResourceLink`), avoiding custom content formats. For `EmbeddedResource`, content is inline via `text` (UTF-8) or `blob` (base64). For `ResourceLink`, the host can retrieve the content directly from the URI.
|
||||
|
||||
Host behavior:
|
||||
* Host SHOULD show a confirmation dialog before initiating the download.
|
||||
* For `EmbeddedResource`, host SHOULD derive the filename from the last segment of `resource.uri`.
|
||||
* Host MAY reject the download based on security policy, file size limits, or user preferences.
|
||||
* Host SHOULD sanitize filenames to prevent path traversal.
|
||||
|
||||
`ui/message` - Send message content to the host's chat interface
|
||||
|
||||
```typescript
|
||||
|
|
|
|||
|
|
@ -712,6 +712,36 @@ describe("App <-> AppBridge integration", () => {
|
|||
expect(result.content).toEqual(resultContent);
|
||||
});
|
||||
|
||||
it("ondownloadfile setter registers handler for ui/download-file requests", async () => {
|
||||
const downloadParams = {
|
||||
contents: [
|
||||
{
|
||||
type: "resource" as const,
|
||||
resource: {
|
||||
uri: "file:///export.json",
|
||||
mimeType: "application/json",
|
||||
text: '{"key":"value"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const receivedRequests: unknown[] = [];
|
||||
|
||||
bridge.ondownloadfile = async (params) => {
|
||||
receivedRequests.push(params);
|
||||
return {};
|
||||
};
|
||||
|
||||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
||||
const result = await app.downloadFile(downloadParams);
|
||||
|
||||
expect(receivedRequests).toHaveLength(1);
|
||||
expect(receivedRequests[0]).toMatchObject(downloadParams);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("callServerTool throws a helpful error when called with a string instead of params object", async () => {
|
||||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ import {
|
|||
McpUiOpenLinkRequest,
|
||||
McpUiOpenLinkRequestSchema,
|
||||
McpUiOpenLinkResult,
|
||||
McpUiDownloadFileRequest,
|
||||
McpUiDownloadFileRequestSchema,
|
||||
McpUiDownloadFileResult,
|
||||
McpUiResourceTeardownRequest,
|
||||
McpUiResourceTeardownResultSchema,
|
||||
McpUiSandboxProxyReadyNotification,
|
||||
|
|
@ -614,6 +617,62 @@ export class AppBridge extends Protocol<
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler for file download requests from the View.
|
||||
*
|
||||
* The View sends `ui/download-file` requests when the user wants to
|
||||
* download a file. The params contain an array of MCP resource content
|
||||
* items — either `EmbeddedResource` (inline data) or `ResourceLink`
|
||||
* (URI the host can fetch). The host should show a confirmation dialog
|
||||
* and then trigger the download.
|
||||
*
|
||||
* @param callback - Handler that receives download params and returns a result
|
||||
* - `params.contents` - Array of `EmbeddedResource` or `ResourceLink` items
|
||||
* - `extra` - Request metadata (abort signal, session info)
|
||||
* - Returns: `Promise<McpUiDownloadFileResult>` with optional `isError` flag
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* bridge.ondownloadfile = async ({ contents }, extra) => {
|
||||
* for (const item of contents) {
|
||||
* if (item.type === "resource") {
|
||||
* // EmbeddedResource — inline content
|
||||
* const res = item.resource;
|
||||
* const blob = res.blob
|
||||
* ? new Blob([Uint8Array.from(atob(res.blob), c => c.charCodeAt(0))], { type: res.mimeType })
|
||||
* : new Blob([res.text ?? ""], { type: res.mimeType });
|
||||
* const url = URL.createObjectURL(blob);
|
||||
* const link = document.createElement("a");
|
||||
* link.href = url;
|
||||
* link.download = res.uri.split("/").pop() ?? "download";
|
||||
* link.click();
|
||||
* URL.revokeObjectURL(url);
|
||||
* } else if (item.type === "resource_link") {
|
||||
* // ResourceLink — host fetches or opens directly
|
||||
* window.open(item.uri, "_blank");
|
||||
* }
|
||||
* }
|
||||
* return {};
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for the request type
|
||||
* @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for the result type
|
||||
*/
|
||||
set ondownloadfile(
|
||||
callback: (
|
||||
params: McpUiDownloadFileRequest["params"],
|
||||
extra: RequestHandlerExtra,
|
||||
) => Promise<McpUiDownloadFileResult>,
|
||||
) {
|
||||
this.setRequestHandler(
|
||||
McpUiDownloadFileRequestSchema,
|
||||
async (request, extra) => {
|
||||
return callback(request.params, extra);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler for display mode change requests from the view.
|
||||
*
|
||||
|
|
|
|||
79
src/app.ts
79
src/app.ts
|
|
@ -33,6 +33,8 @@ import {
|
|||
McpUiMessageResultSchema,
|
||||
McpUiOpenLinkRequest,
|
||||
McpUiOpenLinkResultSchema,
|
||||
McpUiDownloadFileRequest,
|
||||
McpUiDownloadFileResultSchema,
|
||||
McpUiResourceTeardownRequest,
|
||||
McpUiResourceTeardownRequestSchema,
|
||||
McpUiResourceTeardownResult,
|
||||
|
|
@ -930,6 +932,83 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
/** @deprecated Use {@link openLink `openLink`} instead */
|
||||
sendOpenLink: App["openLink"] = this.openLink;
|
||||
|
||||
/**
|
||||
* Request the host to download a file.
|
||||
*
|
||||
* Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
|
||||
* this provides a host-mediated mechanism for file exports. The host will
|
||||
* typically show a confirmation dialog before initiating the download.
|
||||
*
|
||||
* Uses standard MCP resource types: `EmbeddedResource` for inline content
|
||||
* and `ResourceLink` for content the host can fetch directly.
|
||||
*
|
||||
* @param params - Resource contents to download
|
||||
* @param options - Request options (timeout, etc.)
|
||||
* @returns Result with `isError: true` if the host denied the request (e.g., user cancelled)
|
||||
*
|
||||
* @throws {Error} If the request times out or the connection is lost
|
||||
*
|
||||
* @example Download a JSON file (embedded text resource)
|
||||
* ```ts
|
||||
* const data = JSON.stringify({ items: selectedItems }, null, 2);
|
||||
* const { isError } = await app.downloadFile({
|
||||
* contents: [{
|
||||
* type: "resource",
|
||||
* resource: {
|
||||
* uri: "file:///export.json",
|
||||
* mimeType: "application/json",
|
||||
* text: data,
|
||||
* },
|
||||
* }],
|
||||
* });
|
||||
* if (isError) {
|
||||
* console.warn("Download denied or cancelled");
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Download binary content (embedded blob resource)
|
||||
* ```ts
|
||||
* const { isError } = await app.downloadFile({
|
||||
* contents: [{
|
||||
* type: "resource",
|
||||
* resource: {
|
||||
* uri: "file:///image.png",
|
||||
* mimeType: "image/png",
|
||||
* blob: base64EncodedPng,
|
||||
* },
|
||||
* }],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Download via resource link (host fetches)
|
||||
* ```ts
|
||||
* const { isError } = await app.downloadFile({
|
||||
* contents: [{
|
||||
* type: "resource_link",
|
||||
* uri: "https://api.example.com/reports/q4.pdf",
|
||||
* name: "Q4 Report",
|
||||
* mimeType: "application/pdf",
|
||||
* }],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for request structure
|
||||
* @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for result structure
|
||||
*/
|
||||
downloadFile(
|
||||
params: McpUiDownloadFileRequest["params"],
|
||||
options?: RequestOptions,
|
||||
) {
|
||||
return this.request(
|
||||
<McpUiDownloadFileRequest>{
|
||||
method: "ui/download-file",
|
||||
params,
|
||||
},
|
||||
McpUiDownloadFileResultSchema,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a change to the display mode.
|
||||
*
|
||||
|
|
|
|||
221
src/generated/schema.json
generated
221
src/generated/schema.json
generated
|
|
@ -81,6 +81,215 @@
|
|||
],
|
||||
"description": "Display mode for UI presentation."
|
||||
},
|
||||
"McpUiDownloadFileRequest": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string",
|
||||
"const": "ui/download-file"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "resource"
|
||||
},
|
||||
"resource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uri": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["uri", "text"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uri": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"blob": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["uri", "blob"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audience": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["user", "assistant"]
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"lastModified": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"required": ["type", "resource"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"icons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"enum": ["light", "dark"]
|
||||
}
|
||||
},
|
||||
"required": ["src"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audience": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["user", "assistant"]
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"lastModified": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "resource_link"
|
||||
}
|
||||
},
|
||||
"required": ["name", "uri", "type"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types."
|
||||
}
|
||||
},
|
||||
"required": ["contents"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["method", "params"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"McpUiDownloadFileResult": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isError": {
|
||||
"description": "True if the download failed (e.g., user cancelled or host denied).",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"McpUiHostCapabilities": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
|
|
@ -97,6 +306,12 @@
|
|||
"properties": {},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"downloadFile": {
|
||||
"description": "Host supports file downloads via ui/download-file.",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"serverTools": {
|
||||
"description": "Host can proxy tool calls to the MCP server.",
|
||||
"type": "object",
|
||||
|
|
@ -2443,6 +2658,12 @@
|
|||
"properties": {},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"downloadFile": {
|
||||
"description": "Host supports file downloads via ui/download-file.",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"serverTools": {
|
||||
"description": "Host can proxy tool calls to the MCP server.",
|
||||
"type": "object",
|
||||
|
|
|
|||
20
src/generated/schema.test.ts
generated
20
src/generated/schema.test.ts
generated
|
|
@ -35,6 +35,10 @@ export type McpUiOpenLinkResultSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiOpenLinkResultSchema
|
||||
>;
|
||||
|
||||
export type McpUiDownloadFileResultSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiDownloadFileResultSchema
|
||||
>;
|
||||
|
||||
export type McpUiMessageResultSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiMessageResultSchema
|
||||
>;
|
||||
|
|
@ -123,6 +127,10 @@ export type McpUiClientCapabilitiesSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiClientCapabilitiesSchema
|
||||
>;
|
||||
|
||||
export type McpUiDownloadFileRequestSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiDownloadFileRequestSchema
|
||||
>;
|
||||
|
||||
export type McpUiMessageRequestSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiMessageRequestSchema
|
||||
>;
|
||||
|
|
@ -179,6 +187,12 @@ expectType<spec.McpUiOpenLinkResult>(
|
|||
expectType<McpUiOpenLinkResultSchemaInferredType>(
|
||||
{} as spec.McpUiOpenLinkResult,
|
||||
);
|
||||
expectType<spec.McpUiDownloadFileResult>(
|
||||
{} as McpUiDownloadFileResultSchemaInferredType,
|
||||
);
|
||||
expectType<McpUiDownloadFileResultSchemaInferredType>(
|
||||
{} as spec.McpUiDownloadFileResult,
|
||||
);
|
||||
expectType<spec.McpUiMessageResult>({} as McpUiMessageResultSchemaInferredType);
|
||||
expectType<McpUiMessageResultSchemaInferredType>({} as spec.McpUiMessageResult);
|
||||
expectType<spec.McpUiSandboxProxyReadyNotification>(
|
||||
|
|
@ -287,6 +301,12 @@ expectType<spec.McpUiClientCapabilities>(
|
|||
expectType<McpUiClientCapabilitiesSchemaInferredType>(
|
||||
{} as spec.McpUiClientCapabilities,
|
||||
);
|
||||
expectType<spec.McpUiDownloadFileRequest>(
|
||||
{} as McpUiDownloadFileRequestSchemaInferredType,
|
||||
);
|
||||
expectType<McpUiDownloadFileRequestSchemaInferredType>(
|
||||
{} as spec.McpUiDownloadFileRequest,
|
||||
);
|
||||
expectType<spec.McpUiMessageRequest>(
|
||||
{} as McpUiMessageRequestSchemaInferredType,
|
||||
);
|
||||
|
|
|
|||
45
src/generated/schema.ts
generated
45
src/generated/schema.ts
generated
|
|
@ -5,8 +5,10 @@ import { z } from "zod";
|
|||
import {
|
||||
ContentBlockSchema,
|
||||
CallToolResultSchema,
|
||||
EmbeddedResourceSchema,
|
||||
ImplementationSchema,
|
||||
RequestIdSchema,
|
||||
ResourceLinkSchema,
|
||||
ToolSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
|
|
@ -161,6 +163,22 @@ export const McpUiOpenLinkResultSchema = z
|
|||
})
|
||||
.passthrough();
|
||||
|
||||
/**
|
||||
* @description Result from a file download request.
|
||||
* @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`}
|
||||
*/
|
||||
export const McpUiDownloadFileResultSchema = z
|
||||
.object({
|
||||
/** @description True if the download failed (e.g., user cancelled or host denied). */
|
||||
isError: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"True if the download failed (e.g., user cancelled or host denied).",
|
||||
),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
/**
|
||||
* @description Result from sending a message.
|
||||
* @see {@link McpUiMessageRequest `McpUiMessageRequest`}
|
||||
|
|
@ -476,6 +494,11 @@ export const McpUiHostCapabilitiesSchema = z.object({
|
|||
.object({})
|
||||
.optional()
|
||||
.describe("Host supports opening external URLs."),
|
||||
/** @description Host supports file downloads via ui/download-file. */
|
||||
downloadFile: z
|
||||
.object({})
|
||||
.optional()
|
||||
.describe("Host supports file downloads via ui/download-file."),
|
||||
/** @description Host can proxy tool calls to the MCP server. */
|
||||
serverTools: z
|
||||
.object({
|
||||
|
|
@ -699,6 +722,28 @@ export const McpUiClientCapabilitiesSchema = z.object({
|
|||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Request to download a file through the host.
|
||||
*
|
||||
* Sent from the View to the Host when the app wants to trigger a file download.
|
||||
* Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
|
||||
* this provides a host-mediated mechanism for file exports.
|
||||
* The host SHOULD show a confirmation dialog before initiating the download.
|
||||
*
|
||||
* @see {@link app!App.downloadFile `App.downloadFile`} for the method that sends this request
|
||||
*/
|
||||
export const McpUiDownloadFileRequestSchema = z.object({
|
||||
method: z.literal("ui/download-file"),
|
||||
params: z.object({
|
||||
/** @description Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types. */
|
||||
contents: z
|
||||
.array(z.union([EmbeddedResourceSchema, ResourceLinkSchema]))
|
||||
.describe(
|
||||
"Resource contents to download \u2014 embedded (inline data) or linked (host fetches). Uses standard MCP resource types.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Request to send a message to the host's chat interface.
|
||||
* @see {@link app!App.sendMessage `App.sendMessage`} for the method that sends this request
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@
|
|||
import type {
|
||||
CallToolResult,
|
||||
ContentBlock,
|
||||
EmbeddedResource,
|
||||
Implementation,
|
||||
RequestId,
|
||||
ResourceLink,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
|
|
@ -168,6 +170,38 @@ export interface McpUiOpenLinkResult {
|
|||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Request to download a file through the host.
|
||||
*
|
||||
* Sent from the View to the Host when the app wants to trigger a file download.
|
||||
* Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
|
||||
* this provides a host-mediated mechanism for file exports.
|
||||
* The host SHOULD show a confirmation dialog before initiating the download.
|
||||
*
|
||||
* @see {@link app!App.downloadFile `App.downloadFile`} for the method that sends this request
|
||||
*/
|
||||
export interface McpUiDownloadFileRequest {
|
||||
method: "ui/download-file";
|
||||
params: {
|
||||
/** @description Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types. */
|
||||
contents: (EmbeddedResource | ResourceLink)[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Result from a file download request.
|
||||
* @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`}
|
||||
*/
|
||||
export interface McpUiDownloadFileResult {
|
||||
/** @description True if the download failed (e.g., user cancelled or host denied). */
|
||||
isError?: boolean;
|
||||
/**
|
||||
* Index signature required for MCP SDK `Protocol` class compatibility.
|
||||
* Note: The generated schema uses passthrough() to allow additional properties.
|
||||
*/
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Request to send a message to the host's chat interface.
|
||||
* @see {@link app!App.sendMessage `App.sendMessage`} for the method that sends this request
|
||||
|
|
@ -450,6 +484,8 @@ export interface McpUiHostCapabilities {
|
|||
experimental?: {};
|
||||
/** @description Host supports opening external URLs. */
|
||||
openLinks?: {};
|
||||
/** @description Host supports file downloads via ui/download-file. */
|
||||
downloadFile?: {};
|
||||
/** @description Host can proxy tool calls to the MCP server. */
|
||||
serverTools?: {
|
||||
/** @description Host supports tools/list_changed notifications. */
|
||||
|
|
@ -746,6 +782,8 @@ export interface McpUiToolMeta {
|
|||
* ```
|
||||
*/
|
||||
export const OPEN_LINK_METHOD: McpUiOpenLinkRequest["method"] = "ui/open-link";
|
||||
export const DOWNLOAD_FILE_METHOD: McpUiDownloadFileRequest["method"] =
|
||||
"ui/download-file";
|
||||
export const MESSAGE_METHOD: McpUiMessageRequest["method"] = "ui/message";
|
||||
export const SANDBOX_PROXY_READY_METHOD: McpUiSandboxProxyReadyNotification["method"] =
|
||||
"ui/notifications/sandbox-proxy-ready";
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
export {
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
OPEN_LINK_METHOD,
|
||||
DOWNLOAD_FILE_METHOD,
|
||||
MESSAGE_METHOD,
|
||||
SANDBOX_PROXY_READY_METHOD,
|
||||
SANDBOX_RESOURCE_READY_METHOD,
|
||||
|
|
@ -34,6 +35,8 @@ export {
|
|||
type McpUiHostStyles,
|
||||
type McpUiOpenLinkRequest,
|
||||
type McpUiOpenLinkResult,
|
||||
type McpUiDownloadFileRequest,
|
||||
type McpUiDownloadFileResult,
|
||||
type McpUiMessageRequest,
|
||||
type McpUiMessageResult,
|
||||
type McpUiUpdateModelContextRequest,
|
||||
|
|
@ -68,6 +71,7 @@ export {
|
|||
import type {
|
||||
McpUiInitializeRequest,
|
||||
McpUiOpenLinkRequest,
|
||||
McpUiDownloadFileRequest,
|
||||
McpUiMessageRequest,
|
||||
McpUiUpdateModelContextRequest,
|
||||
McpUiResourceTeardownRequest,
|
||||
|
|
@ -83,6 +87,7 @@ import type {
|
|||
McpUiSandboxProxyReadyNotification,
|
||||
McpUiInitializeResult,
|
||||
McpUiOpenLinkResult,
|
||||
McpUiDownloadFileResult,
|
||||
McpUiMessageResult,
|
||||
McpUiResourceTeardownResult,
|
||||
McpUiRequestDisplayModeResult,
|
||||
|
|
@ -96,6 +101,8 @@ export {
|
|||
McpUiHostStylesSchema,
|
||||
McpUiOpenLinkRequestSchema,
|
||||
McpUiOpenLinkResultSchema,
|
||||
McpUiDownloadFileRequestSchema,
|
||||
McpUiDownloadFileResultSchema,
|
||||
McpUiMessageRequestSchema,
|
||||
McpUiMessageResultSchema,
|
||||
McpUiUpdateModelContextRequestSchema,
|
||||
|
|
@ -159,6 +166,7 @@ import {
|
|||
export type AppRequest =
|
||||
| McpUiInitializeRequest
|
||||
| McpUiOpenLinkRequest
|
||||
| McpUiDownloadFileRequest
|
||||
| McpUiMessageRequest
|
||||
| McpUiUpdateModelContextRequest
|
||||
| McpUiResourceTeardownRequest
|
||||
|
|
@ -207,6 +215,7 @@ export type AppNotification =
|
|||
export type AppResult =
|
||||
| McpUiInitializeResult
|
||||
| McpUiOpenLinkResult
|
||||
| McpUiDownloadFileResult
|
||||
| McpUiMessageResult
|
||||
| McpUiResourceTeardownResult
|
||||
| McpUiRequestDisplayModeResult
|
||||
|
|
|
|||
Loading…
Reference in a new issue