Skip to main content
useFiles is a React hook for file upload and download inside widgets. It exposes an isSupported flag plus an upload and a getDownloadUrl function.
File operations are only available through the OpenAI window.openai extension. The MCP Apps specification (SEP-1865) has deferred standard file handling, so MCP Apps clients such as Claude, Goose, and VS Code do not support file operations. Always check isSupported before calling upload or getDownloadUrl. Calling either when isSupported is false throws.

useFiles

Hook for file upload and download operations in widgets.
import { useFiles } from "mcp-use/react"
Returns the file operations API for the current host. isSupported is computed once on mount via useMemo and is true only when the OpenAI window.openai extension exposes both window.openai.uploadFile and window.openai.getFileDownloadUrl as functions. In all other environments (MCP Apps clients, URL params fallback, server-side rendering) it is false. The hook takes no arguments. Returns
returns
UseFilesResult
An object with isSupported, upload, and getDownloadUrl. See UseFilesResult.
Signature
function useFiles(): UseFilesResult

Basic usage

Always gate file operations behind isSupported. The following server registers a widget that opens a file manager, and the widget component uses the hook.
import { MCPServer, widget } from "mcp-use/server"
import { z } from "zod"

const server = new MCPServer({
  name: "files-example",
  version: "1.0.0",
  description:
    "Demonstrates useFiles() hook for file upload and download with isSupported detection",
})

server.tool(
  {
    name: "open-file-manager",
    description:
      "Open an interactive file manager widget. Supports uploading files and retrieving download URLs. " +
      "File operations are only available in ChatGPT. The widget will show a notice in other clients.",
    schema: z.object({}),
    widget: {
      name: "file-manager",
      invoking: "Opening file manager...",
      invoked: "File manager ready",
    },
  },
  async () => {
    return widget({
      props: {},
      message:
        "File manager opened. You can upload files and retrieve download links. " +
        "Note: file operations are only available in ChatGPT.",
    })
  }
)

await server.listen()
import { useFiles } from "mcp-use/react"

function FileManager() {
  const { upload, getDownloadUrl, isSupported } = useFiles()

  if (!isSupported) {
    return <p>File operations are not available in this host.</p>
  }

  async function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (!file) return

    // Model-visible by default: ChatGPT includes the file in context
    const { fileId } = await upload(file)

    // Upload privately: the model will not see it
    const { fileId: privateFileId } = await upload(file, { modelVisible: false })
  }

  async function handleDownload(fileId: string) {
    const { downloadUrl } = await getDownloadUrl({ fileId })
    window.open(downloadUrl, "_blank")
  }

  return <input type="file" onChange={handleFileSelect} />
}

Returns

The object returned by useFiles.

UseFilesResult

The shape of the value returned by useFiles. Every field is required and present on every call. Returns
isSupported
boolean
required
Whether the host supports file operations. true only when the OpenAI window.openai extension exposes window.openai.uploadFile and window.openai.getFileDownloadUrl. Always check this flag before calling upload or getDownloadUrl.
upload
(file: File, options?: UploadOptions) => Promise<FileMetadata>
required
Upload a file to the host. Returns a FileMetadata reference ({ fileId }) that can be passed to getDownloadUrl or stored in widget state for later retrieval. By default the file is tracked in widget state (imageIds) so the model can see it; pass { modelVisible: false } in UploadOptions to upload privately. Throws if called when isSupported is false.
getDownloadUrl
(file: FileMetadata) => Promise<{ downloadUrl: string }>
required
Get a temporary download URL for a previously uploaded file. The returned URL is valid for a limited time (typically 5 minutes). Do not store the URL; call getDownloadUrl again when you need to display or fetch the file. Throws if called when isSupported is false.
Signature
interface UseFilesResult {
  isSupported: boolean
  upload: (file: File, options?: UploadOptions) => Promise<FileMetadata>
  getDownloadUrl: (file: FileMetadata) => Promise<{ downloadUrl: string }>
}

Types

UploadOptions

Options passed as the optional second argument to upload. Configures a single upload call. All fields are optional. Parameters
modelVisible
boolean
default:"true"
Whether the uploaded file should be visible to the model. When true (the default), the fileId is appended to imageIds in widget state so the ChatGPT host includes the file in the model’s conversation context on future turns. When false, the file is uploaded but not tracked in imageIds, so the model will not see it. Useful for files used only by the widget (for example a user-provided config file or a privately processed image).
Signature
interface UploadOptions {
  modelVisible?: boolean
}

FileMetadata

The opaque file reference returned by upload. Opaque file reference returned by useFiles().upload(). Pass it to getDownloadUrl() to retrieve a temporary download URL. Defined in mcp-use/react (re-exported from the widget types module). Returns
fileId
string
required
Opaque string identifier assigned by the host. Store it in widget state to retrieve the download URL later.
Signature
type FileMetadata = { fileId: string }

Model visibility

By default, every uploaded file is tracked in widget state under imageIds. ChatGPT reads this field to include the file in the model’s conversation context on future turns, so the model can reference the content of the uploaded image. Internally, upload preserves existing imageIds (and other widget state) when appending, so uploading a file does not wipe other state. Pass { modelVisible: false } when you want to upload a file for widget-only use without exposing it to the model.
// User uploads a private reference image: the widget uses it, the model does not see it
const { fileId } = await upload(referenceFile, { modelVisible: false })
const { downloadUrl } = await getDownloadUrl({ fileId })
// Use downloadUrl for an <img src={...} /> in the widget only
imageIds state is preserved across setWidgetState calls, so uploading a file does not wipe other widget state. If setWidgetState fails while tracking the imageId, the error is logged with console.warn and the upload still resolves with its FileMetadata.

Full upload and download example

import { useFiles, useWidgetState } from "mcp-use/react"
import { useState } from "react"

interface FileState {
  uploadedFileId: string | null
}

function FileWidget() {
  const { upload, getDownloadUrl, isSupported } = useFiles()
  const [state, setState] = useWidgetState<FileState>()
  const [isUploading, setIsUploading] = useState(false)
  const [downloadUrl, setDownloadUrl] = useState<string | null>(null)

  if (!isSupported) {
    return (
      <div className="notice">
        File operations require the OpenAI window.openai extension.
        This host does not support file uploads yet.
      </div>
    )
  }

  async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (!file) return

    setIsUploading(true)
    try {
      const { fileId } = await upload(file)
      // Persist the fileId in widget state so the model can reference it
      await setState({ uploadedFileId: fileId })
    } finally {
      setIsUploading(false)
    }
  }

  async function handleGetLink() {
    if (!state?.uploadedFileId) return
    const { downloadUrl } = await getDownloadUrl({
      fileId: state.uploadedFileId,
    })
    setDownloadUrl(downloadUrl)
  }

  return (
    <div>
      <input type="file" onChange={handleUpload} disabled={isUploading} />

      {state?.uploadedFileId && !downloadUrl && (
        <button onClick={handleGetLink}>Get download link</button>
      )}

      {downloadUrl && (
        <a href={downloadUrl} target="_blank" rel="noreferrer">
          Download file
        </a>
      )}
    </div>
  )
}
Download URLs are temporary (typically valid for 5 minutes). Do not store the URL; call getDownloadUrl each time you need to display or fetch the file. Store the fileId in widget state instead.

Error handling

Calling upload or getDownloadUrl when isSupported is false throws a descriptive error. For upload:
[useFiles] File upload is not supported in this host.
Check `isSupported` before calling `upload`.
File operations are only available through the OpenAI window.openai extension.
And for getDownloadUrl:
[useFiles] File download is not supported in this host.
Check `isSupported` before calling `getDownloadUrl`.
File operations are only available through the OpenAI window.openai extension.
Wrap calls in try/catch to handle host-level errors (network failures, policy violations).
try {
  const { fileId } = await upload(file)
} catch (err) {
  console.error("Upload failed:", err)
}