Skip to main content
You can save a file, such as a CSV file or an image, from your MCP App to the user’s device. For example, in a shopping list app, a user clicks an export button to save the list as a CSV file. Claude asks the user to confirm before the download starts. Your app’s UI runs in Claude in a sandboxed frame, where no download starts from an <a download> link, a blob: URL, or a data: URL. Send each export through app.downloadFile instead.
This page is for developers whose MCP App already renders in Claude. If you don’t have an MCP App yet, start with Add an interactive UI to your MCP server.

Send a file from a view

A view is one rendered instance of your app’s UI in Claude. To save a file, call app.downloadFile with the file’s contents in the view’s script. The user sees the Download file dialog, or Download files for several files. The dialog lists the files in your request, and the user clicks Download to start the download. app.downloadFile requires @modelcontextprotocol/ext-apps 1.1.2 or later.

Check whether Claude offers downloads

Claude doesn’t offer downloads everywhere that your view can appear, so check each time a view starts. After app.connect() resolves, read app.getHostCapabilities()?.downloadFile in the view’s script:
  • If downloadFile is set: show your export control
  • If downloadFile isn’t set: offer the content another way, such as text that the user can copy

Add an export button to a view

Send the download request when the user clicks a control in the view, while the view is on screen, so that the user expects the dialog. To run this sample, add <button id="export"> and an element with id="status" to the view’s HTML. Clicks have no effect while a request is pending, and the user sees no button where Claude doesn’t offer downloads:
Open the view in Claude and click the button. The Download file dialog appears and lists shopping-list.csv.

Build the contents of a download request

Put each file’s data and name inside the request’s contents, and don’t include links:
  • File data: use the text field for text and the blob field for binary data. Encode blob as standard base64 with no data: prefix.
  • Links: omit resource_link items, because Claude doesn’t fetch them for the view. If you send only resource_link items, Claude refuses the request. If you mix them with files, Claude can also refuse the request.
  • File name: end uri with a name and an extension, such as file:///report.csv. The last path segment becomes the saved file’s name.
  • Characters in the file name: write a literal space instead of %20. Percent-encoding isn’t decoded.
    • Characters such as %, :, ", *, <, >, |, ~, and \ become _.
  • Several files: you can send several files in one request

Send one download request at a time

Wait for the result of one request before you send the next. If you send a second request from the same view while the Download file dialog is open, one of the two requests ends with isError: true.

Handle the result of a download request

Read isError in the result of app.downloadFile:
  • false: Claude started the download
  • true: tell the user that the download didn’t finish
You can’t tell from the result which of these cases occurred:
  • Claude refused the request
  • The user canceled
  • Claude closed the Download file dialog
  • Claude couldn’t start the download
If no dialog appeared, check the file types that Claude refuses. If the promise rejects and you receive no result, catch the error and find its cause.

File types that Claude refuses

If you send a file whose name ends in an extension on Claude’s blocked list, Claude refuses the file.
  • Blocked: the list includes programs, installers, shortcuts, and some script types. Blocked extensions include .exe, .bat, .js, .jar, .dmg, and .lnk.
  • Saved after the user confirms the download: other script types, such as .py and .sh
If Claude refuses one file in a request, it refuses the whole request. No dialog appears, and the result has isError: true.

Troubleshoot a rejected download request

For failures outside the isError cases, the promise from app.downloadFile rejects. Read the error’s message and code.

Invalid params for ui/download-file

The error’s message includes this text when you send a malformed request. Causes include a blob that isn’t standard base64 and an item that has no uri.

Request timed out

The error’s message includes this text when the MCP Apps SDK stops waiting for a result, after 60 seconds by default. Claude sets no time limit on the Download file dialog. To wait longer, pass timeout in milliseconds in the second argument to app.downloadFile, such as { timeout: 600_000 }. After a timeout, the dialog can remain open with a working Download button. Don’t tell the user that nothing was saved.

Code -32601, Method not found

Claude doesn’t offer downloads where this view appears. Check app.getHostCapabilities()?.downloadFile before you send the request.

Next steps