refactor: use MCP resource types for ui/download-file params

Replace custom {filename, content, mimeType, encoding} params with
standard MCP resource types: EmbeddedResource for inline content and
ResourceLink for host-resolved references.

New params shape:
  params: { contents: (EmbeddedResource | ResourceLink)[] }

This reuses existing MCP primitives instead of inventing new ones,
and enables hosts to show resource 'bubbles' or resolve ResourceLink
URIs via resources/read on the MCP server.
This commit is contained in:
Anton Pidkuiko MacBook 2026-02-20 12:00:35 +00:00
commit 3757271641
No known key found for this signature in database
3 changed files with 366 additions and 125 deletions

View file

@ -92,33 +92,187 @@
"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"
}
]
"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": ["filename", "content", "mimeType"],
"required": ["contents"],
"additionalProperties": false
}
},

View file

@ -35,10 +35,6 @@ export type McpUiOpenLinkResultSchemaInferredType = z.infer<
typeof generated.McpUiOpenLinkResultSchema
>;
export type McpUiDownloadFileRequestSchemaInferredType = z.infer<
typeof generated.McpUiDownloadFileRequestSchema
>;
export type McpUiDownloadFileResultSchemaInferredType = z.infer<
typeof generated.McpUiDownloadFileResultSchema
>;
@ -131,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
>;
@ -187,12 +187,6 @@ expectType<spec.McpUiOpenLinkResult>(
expectType<McpUiOpenLinkResultSchemaInferredType>(
{} as spec.McpUiOpenLinkResult,
);
expectType<spec.McpUiDownloadFileRequest>(
{} as McpUiDownloadFileRequestSchemaInferredType,
);
expectType<McpUiDownloadFileRequestSchemaInferredType>(
{} as spec.McpUiDownloadFileRequest,
);
expectType<spec.McpUiDownloadFileResult>(
{} as McpUiDownloadFileResultSchemaInferredType,
);
@ -307,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,
);

View file

@ -5,8 +5,10 @@ import { z } from "zod";
import {
ContentBlockSchema,
CallToolResultSchema,
EmbeddedResourceSchema,
ImplementationSchema,
RequestIdSchema,
ResourceLinkSchema,
ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
@ -161,41 +163,6 @@ 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`}
@ -755,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