diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx
index ef280f3..8e4910a 100644
--- a/examples/integration-server/src/mcp-app.tsx
+++ b/examples/integration-server/src/mcp-app.tsx
@@ -137,6 +137,24 @@ 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({
+ filename: "export.json",
+ content: sampleContent,
+ mimeType: "application/json",
+ });
+ log.info("Download", isError ? "rejected" : "accepted");
+ }, [app, serverTime]);
+
return (
+
+ {canDownload && (
+
+
Export current server time as JSON file
+
+
+ )}
);
}
diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx
index cb73cab..d12ba12 100644
--- a/specification/draft/apps.mdx
+++ b/specification/draft/apps.mdx
@@ -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,49 @@ 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
+{
+ jsonrpc: "2.0",
+ id: 1,
+ method: "ui/download-file",
+ params: {
+ filename: string, // Suggested filename
+ content: string, // File content (text or base64-encoded)
+ mimeType: string, // MIME type (e.g. "image/svg+xml", "application/json")
+ encoding?: "utf-8" | "base64" // Content encoding, defaults to "utf-8"
+ }
+}
+
+// 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.
+
+Host behavior:
+* Host SHOULD show a confirmation dialog before initiating the download.
+* Host SHOULD use the `filename` as the suggested filename for the download.
+* For `encoding: "base64"`, host MUST decode the content from base64 before creating the file.
+* Host MAY reject the download based on security policy, file size limits, or user preferences.
+* Host SHOULD sanitize the filename to prevent path traversal.
+
`ui/message` - Send message content to the host's chat interface
```typescript
diff --git a/src/app-bridge.ts b/src/app-bridge.ts
index 6e1c021..de7d876 100644
--- a/src/app-bridge.ts
+++ b/src/app-bridge.ts
@@ -69,6 +69,9 @@ import {
McpUiOpenLinkRequest,
McpUiOpenLinkRequestSchema,
McpUiOpenLinkResult,
+ McpUiDownloadFileRequest,
+ McpUiDownloadFileRequestSchema,
+ McpUiDownloadFileResult,
McpUiResourceTeardownRequest,
McpUiResourceTeardownResultSchema,
McpUiSandboxProxyReadyNotification,
@@ -614,6 +617,60 @@ 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 host should show a confirmation dialog and then
+ * trigger the download.
+ *
+ * @param callback - Handler that receives download params and returns a result
+ * - `params.filename` - Suggested filename for the download
+ * - `params.content` - File content (text or base64-encoded)
+ * - `params.mimeType` - MIME type of the file
+ * - `params.encoding` - Content encoding ("utf-8" or "base64", defaults to "utf-8")
+ * - `extra` - Request metadata (abort signal, session info)
+ * - Returns: `Promise` with optional `isError` flag
+ *
+ * @example
+ * ```ts
+ * bridge.ondownloadfile = async ({ filename, content, mimeType, encoding }, extra) => {
+ * const confirmed = await showDialog({
+ * message: `Download "${filename}"?`,
+ * buttons: ["Download", "Cancel"],
+ * });
+ * if (!confirmed) return { isError: true };
+ *
+ * const blob = encoding === "base64"
+ * ? new Blob([Uint8Array.from(atob(content), c => c.charCodeAt(0))], { type: mimeType })
+ * : new Blob([content], { type: mimeType });
+ * const url = URL.createObjectURL(blob);
+ * const link = document.createElement("a");
+ * link.href = url;
+ * link.download = filename;
+ * link.click();
+ * URL.revokeObjectURL(url);
+ * 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,
+ ) {
+ this.setRequestHandler(
+ McpUiDownloadFileRequestSchema,
+ async (request, extra) => {
+ return callback(request.params, extra);
+ },
+ );
+ }
+
/**
* Register a handler for display mode change requests from the view.
*
diff --git a/src/app.ts b/src/app.ts
index 16f8da6..ebf7e81 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -33,6 +33,8 @@ import {
McpUiMessageResultSchema,
McpUiOpenLinkRequest,
McpUiOpenLinkResultSchema,
+ McpUiDownloadFileRequest,
+ McpUiDownloadFileResultSchema,
McpUiResourceTeardownRequest,
McpUiResourceTeardownRequestSchema,
McpUiResourceTeardownResult,
@@ -930,6 +932,59 @@ export class App extends Protocol {
/** @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.
+ *
+ * @param params - File content, filename, MIME type, and optional encoding
+ * @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 export
+ * ```ts
+ * const data = JSON.stringify({ items: selectedItems }, null, 2);
+ * const { isError } = await app.downloadFile({
+ * filename: "export.json",
+ * content: data,
+ * mimeType: "application/json",
+ * });
+ * if (isError) {
+ * console.warn("Download denied or cancelled");
+ * }
+ * ```
+ *
+ * @example Download binary content (base64)
+ * ```ts
+ * const { isError } = await app.downloadFile({
+ * filename: "image.png",
+ * content: base64EncodedPng,
+ * mimeType: "image/png",
+ * encoding: "base64",
+ * });
+ * ```
+ *
+ * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for request structure
+ * @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for result structure
+ */
+ downloadFile(
+ params: McpUiDownloadFileRequest["params"],
+ options?: RequestOptions,
+ ) {
+ return this.request(
+ {
+ method: "ui/download-file",
+ params,
+ },
+ McpUiDownloadFileResultSchema,
+ options,
+ );
+ }
+
/**
* Request a change to the display mode.
*
diff --git a/src/generated/schema.json b/src/generated/schema.json
index d9e4b58..9cc87ae 100644
--- a/src/generated/schema.json
+++ b/src/generated/schema.json
@@ -81,6 +81,61 @@
],
"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": {
+ "filename": {
+ "type": "string",
+ "description": "Suggested filename for the download."
+ },
+ "content": {
+ "type": "string",
+ "description": "File content — text or base64-encoded binary."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIME type of the file (e.g. \"image/svg+xml\", \"application/json\")."
+ },
+ "encoding": {
+ "description": "Content encoding. Defaults to \"utf-8\". Use \"base64\" for binary content.",
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "utf-8"
+ },
+ {
+ "type": "string",
+ "const": "base64"
+ }
+ ]
+ }
+ },
+ "required": ["filename", "content", "mimeType"],
+ "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 +152,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 +2504,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",
diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts
index 95ec2f2..4e7c336 100644
--- a/src/generated/schema.test.ts
+++ b/src/generated/schema.test.ts
@@ -35,6 +35,14 @@ export type McpUiOpenLinkResultSchemaInferredType = z.infer<
typeof generated.McpUiOpenLinkResultSchema
>;
+export type McpUiDownloadFileRequestSchemaInferredType = z.infer<
+ typeof generated.McpUiDownloadFileRequestSchema
+>;
+
+export type McpUiDownloadFileResultSchemaInferredType = z.infer<
+ typeof generated.McpUiDownloadFileResultSchema
+>;
+
export type McpUiMessageResultSchemaInferredType = z.infer<
typeof generated.McpUiMessageResultSchema
>;
@@ -179,6 +187,18 @@ expectType(
expectType(
{} as spec.McpUiOpenLinkResult,
);
+expectType(
+ {} as McpUiDownloadFileRequestSchemaInferredType,
+);
+expectType(
+ {} as spec.McpUiDownloadFileRequest,
+);
+expectType(
+ {} as McpUiDownloadFileResultSchemaInferredType,
+);
+expectType(
+ {} as spec.McpUiDownloadFileResult,
+);
expectType({} as McpUiMessageResultSchemaInferredType);
expectType({} as spec.McpUiMessageResult);
expectType(
diff --git a/src/generated/schema.ts b/src/generated/schema.ts
index 9c75c36..a0cd659 100644
--- a/src/generated/schema.ts
+++ b/src/generated/schema.ts
@@ -161,6 +161,57 @@ export const McpUiOpenLinkResultSchema = z
})
.passthrough();
+/**
+ * @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 Suggested filename for the download. */
+ filename: z.string().describe("Suggested filename for the download."),
+ /** @description File content — text or base64-encoded binary. */
+ content: z
+ .string()
+ .describe("File content \u2014 text or base64-encoded binary."),
+ /** @description MIME type of the file (e.g. "image/svg+xml", "application/json"). */
+ mimeType: z
+ .string()
+ .describe(
+ 'MIME type of the file (e.g. "image/svg+xml", "application/json").',
+ ),
+ /** @description Content encoding. Defaults to "utf-8". Use "base64" for binary content. */
+ encoding: z
+ .union([z.literal("utf-8"), z.literal("base64")])
+ .optional()
+ .describe(
+ 'Content encoding. Defaults to "utf-8". Use "base64" for binary content.',
+ ),
+ }),
+});
+
+/**
+ * @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 +527,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({
diff --git a/src/spec.types.ts b/src/spec.types.ts
index 469ca19..a1fa81c 100644
--- a/src/spec.types.ts
+++ b/src/spec.types.ts
@@ -168,6 +168,44 @@ 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 Suggested filename for the download. */
+ filename: string;
+ /** @description File content — text or base64-encoded binary. */
+ content: string;
+ /** @description MIME type of the file (e.g. "image/svg+xml", "application/json"). */
+ mimeType: string;
+ /** @description Content encoding. Defaults to "utf-8". Use "base64" for binary content. */
+ encoding?: "utf-8" | "base64";
+ };
+}
+
+/**
+ * @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 +488,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 +786,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";
diff --git a/src/types.ts b/src/types.ts
index a4770fd..739da6f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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