Skip to main content
The useFiles hook provides file upload and download capabilities for widgets. It exposes an isSupported flag so you can gracefully handle environments where files are not available.
ChatGPT only. File operations (upload and getDownloadUrl) are only available in the ChatGPT Apps SDK environment. The MCP Apps specification (SEP-1865) has deferred file handling, meaning MCP Apps clients such as Claude, Goose, and VS Code do not support file operations. Always check isSupported before calling upload or getDownloadUrl.

Import

import { useFiles } from "mcp-use/react";

Return values

PropertyTypeDescription
isSupportedbooleantrue only in the ChatGPT Apps SDK environment where window.openai.uploadFile and window.openai.getFileDownloadUrl are available.
upload(file: File, options?: UploadOptions) => Promise<FileMetadata>Upload a file to the host. Returns { fileId }. Throws if isSupported is false.
getDownloadUrl(file: FileMetadata) => Promise<{ downloadUrl: string }>Get a temporary download URL for a previously uploaded file. Throws if isSupported is false.

UploadOptions

OptionTypeDefaultDescription
modelVisiblebooleantrueWhether the uploaded file should be visible to the model. When true, the fileId is appended to imageIds in widget state so ChatGPT includes the file in the model’s conversation context. When false, the file is uploaded but the model won’t see it.

Basic usage

Always gate file operations behind isSupported:
import { useFiles } from "mcp-use/react";

function MyWidget() {
  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 will include the file in context
    const { fileId } = await upload(file);

    // Upload privately — model won't 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 (
    <div>
      <input type="file" onChange={handleFileSelect} />
    </div>
  );
}

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 — the model can then reference the content of the uploaded image. 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 — widget uses it, model doesn't see it
const { fileId } = await upload(referenceFile, { modelVisible: false });
const { downloadUrl } = await getDownloadUrl({ fileId });
// Use downloadUrl for <img src={...} /> in the widget only
imageIds state is preserved across setWidgetState calls — uploading a file won’t wipe other widget state.

## `FileMetadata` type

```typescript
type FileMetadata = { fileId: string };
fileId is an opaque string identifier assigned by the host. Store it in widget state to retrieve the download URL later.

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 ChatGPT Apps SDK.
        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.

Detection logic

isSupported is computed once on mount:
const isSupported =
  typeof window !== "undefined" &&
  typeof window.openai?.uploadFile === "function" &&
  typeof window.openai?.getFileDownloadUrl === "function";
This is true only in the ChatGPT Apps SDK host. In all other environments (MCP Apps clients, URL params fallback, SSR) it is false.

Error handling

Calling upload or getDownloadUrl when isSupported is false throws a descriptive error:
[useFiles] File upload is not supported in this host.
Check `isSupported` before calling `upload`.
File operations are only available in the ChatGPT Apps SDK environment.
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);
}