# Flask MAX_CONTENT_LENGTH: File Upload Limits, 413 Errors, and Streaming

Flask MAX_CONTENT_LENGTH defines the maximum allowable byte size for incoming request bodies, returning an HTTP 413 error when clients send oversized payloads. While configuring this setting protects application memory, production architectures must coordinate reverse proxy buffers in Nginx, handle Werkzeug spooling thresholds, and adopt streaming or external workspace storage for large data sets.

Source: https://fast.io/resources/flask-max-content-length/
Author: [Tom Langridge](https://fast.io/authors/tom-langridge/)
Last reviewed: 2026-10-18

## How to Configure Flask MAX_CONTENT_LENGTH and Catch 413 Errors

According to official [Flask Configuration Documentation](https://flask.palletsprojects.com/en/stable/config/), Flask sets `MAX_CONTENT_LENGTH` to `None` by default, meaning the framework itself imposes no cap on incoming request body bytes until configured.

Flask MAX_CONTENT_LENGTH is a configuration option that sets the maximum allowable byte size of an incoming HTTP request body, automatically rejecting oversized uploads with an HTTP 413 status code.

Without an explicit boundary in your application configuration, an unconstrained endpoint creates serious stability risks. A single client transmitting an uncompressed multi-gigabyte video or archive can overwhelm server RAM or exhaust container temporary storage. Setting a hard limit protects backend processes from memory exhaustion and denial-of-service transmissions.

Configuring the limit requires a single configuration key on your Flask application object. When an incoming request exceeds this value, Flask raises a `RequestEntityTooLarge` exception from Werkzeug's HTTP exception hierarchy:

```python
from flask import Flask, jsonify, request
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16 MB limit

@app.errorhandler(RequestEntityTooLarge)
@app.errorhandler(413)
def handle_payload_too_large(error):
    return jsonify({
        "error": "Payload Too Large",
        "message": "The uploaded payload exceeds the configured limit."
    }), 413

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({"error": "No file part in request"}), 400
    uploaded_file = request.files['file']
    return jsonify({
        "filename": uploaded_file.filename,
        "status": "received"
    }), 200
```

### How Flask Evaluates Incoming Request Sizes

Flask does not read the entire incoming socket before evaluating the payload size. Instead, it inspects the HTTP `Content-Length` header submitted by the client during request initialization.

When your route handler or middleware accesses request payload attributes like `request.files`, `request.form`, or `request.data`, Flask invokes Werkzeug's WSGI environment parser. If the client declared a `Content-Length` header whose integer value exceeds `MAX_CONTENT_LENGTH`, Flask raises `RequestEntityTooLarge` immediately. The underlying socket read terminates before your application allocates memory buffers for the body.

If an incoming request omits the `Content-Length` header and uses chunked transfer encoding (`Transfer-Encoding: chunked`), Flask cannot predict the total byte count in advance. In this scenario, Werkzeug streams data into its internal buffers chunk by chunk, tracking accumulated bytes. The moment the total received bytes cross `MAX_CONTENT_LENGTH`, Werkzeug halts further reading and raises the 413 exception.

When handling standard form posts, the default Werkzeug behavior generates a plain HTML error page. Registering an explicit error handler with `@app.errorhandler(413)` or `@app.errorhandler(RequestEntityTooLarge)` converts this failure into clean, structured JSON suitable for REST APIs and automated clients.

### How to Configure Per-Route Content Length Limits

Global application limits apply across every route, which can become problematic when an application requires different thresholds for different endpoints. For example, user profile metadata forms may require a strict `2 MB` ceiling, while a document ingestion route must accept up to `50 MB`.

In standard Flask architectures, developers handle route-specific limits by setting a permissive global `MAX_CONTENT_LENGTH` (or leaving it unconfigured) and verifying file sizes inside specific view functions or decorators. Inspecting `request.content_length` before accessing `request.files` allows you to reject oversized payloads early:

```python
from functools import wraps
from flask import abort, jsonify, request

def limit_content_length(max_bytes):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            cl = request.content_length
            if cl is not None and cl > max_bytes:
                abort(413)
            return f(*args, **kwargs)
        return wrapper
    return decorator

@app.route('/api/avatar', methods=['POST'])
@limit_content_length(2 * 1024 * 1024)  # 2 MB limit
def upload_avatar():
    file = request.files['avatar']
    return jsonify({"status": "avatar uploaded"}), 200

@app.route('/api/document', methods=['POST'])
@limit_content_length(50 * 1024 * 1024)  # 50 MB limit
def upload_document():
    file = request.files['document']
    return jsonify({"status": "document uploaded"}), 200
```

Starting in Flask 3.1, the framework introduced per-request limits via `Request.max_content_length`. By subclassing `Request` or modifying the request instance inside a `before_request` hook, you can assign dynamic ceilings based on the request path or authenticated user permissions.

## What Happens in Werkzeug During Multipart Uploads and Memory Spooling

Flask delegates all low-level HTTP parsing and multipart form processing to Werkzeug, the WSGI utility library developed by the Pallets project. Understanding how Werkzeug manages memory allocation during uploads clarifies what happens between the initial socket read and your route code.

When a client transmits a `multipart/form-data` request, Werkzeug separates standard text form fields from binary file streams. Each part undergoes validation against specific memory and disk thresholds.

Werkzeug enforces an internal memory threshold: `max_form_memory_size` defaults to `500KB`. Non-file text form parts are stored in RAM up to this ceiling. If an individual text field exceeds `500KB`, Werkzeug halts parsing and raises a `RequestEntityTooLarge` error. This mechanism shields applications from memory exhaustion attacks triggered by oversized text fields or deep JSON strings submitted via multipart forms.

File uploads, by contrast, behave differently once they pass the initial `Content-Length` check. Werkzeug relies on Python's `tempfile.SpooledTemporaryFile` to manage binary file streams:

1. **In-Memory Buffer (< 500KB):** When a file upload begins, Werkzeug stores incoming bytes in a memory buffer. Files smaller than `500KB` remain entirely in RAM for fast processing.
2. **Disk Spooling (>= 500KB):** Once an uploaded file stream crosses the `500KB` threshold, Python's spooled temporary file rolls over from RAM to an unlinked physical temporary file on disk (typically within `/tmp` on Linux systems).
3. **FileStorage Abstraction:** Your route receives a `FileStorage` object wrapping this temporary file. You can read, seek, or save the stream using `file.save(destination_path)`.

Because Werkzeug spools files exceeding `500KB` to disk, an application accepting `100 MB` uploads will not consume `100 MB` of system RAM per concurrent request. However, it will consume temporary disk space until the request completes.

### How MAX_CONTENT_LENGTH Differs from max_form_memory_size

Developers frequently confuse `MAX_CONTENT_LENGTH` with `max_form_memory_size`. While both directives trigger an HTTP 413 error when breached, they govern completely different aspects of request processing:

| Directive | Scope | Default Value | Target Component |
| --- | --- | --- | --- |
| `MAX_CONTENT_LENGTH` | Total HTTP request body | `None` (unlimited) | Entire request body across all parts and files |
| `max_form_memory_size` | Individual non-file form fields | `500KB` (500,000 bytes) | Text fields inside multipart/form-data requests |
| `MAX_FORM_PARTS` | Maximum form parts allowed | 1,000 parts | Total number of form fields and files in a request |

If a user uploads a `10 MB` file and your application sets `MAX_CONTENT_LENGTH = 16 * 1024 * 1024`, the upload succeeds. Werkzeug buffers the first `500KB` in memory, spools the remaining `9.5 MB` to a temporary file on disk, and passes the resulting `FileStorage` object to your view.

Conversely, if a client sends a multipart form containing a single text field with `2 MB` of raw data, Werkzeug rejects the request due to `max_form_memory_size`, even if `MAX_CONTENT_LENGTH` is set to `50 MB`. If your application legitimately requires massive text fields, raise the limit with the `MAX_FORM_MEMORY_SIZE` config key (Flask 3.1 and later) or customize `max_form_memory_size` on a custom request class:

```python
from flask import Flask, Request

class CustomRequest(Request):
    max_form_memory_size = 2 * 1024 * 1024  # 2 MB limit for non-file text fields

app = Flask(__name__)
app.request_class = CustomRequest
app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024  # 32 MB total limit
```

### Disk IO Overhead and Container Storage Constraints

While disk spooling protects server RAM, it introduces secondary performance constraints that appear under production workloads.

Every file upload exceeding `500KB` requires write operations to the host filesystem followed by read operations when saving the file to its final destination. In high-throughput APIs handling concurrent uploads, continuous disk I/O can degrade performance and saturate storage controllers.

In containerized environments such as Kubernetes pods or AWS ECS tasks, container storage often uses ephemeral disk overlays with strict size quotas. Ten concurrent uploads of `200 MB` each require `2 GB` of available space in `/tmp`. If the temporary filesystem runs out of space, Python raises an `OSError` with code `Errno 28` indicating that no space is left on the device, causing unhandled 500 Internal Server Errors rather than controlled 413 rejections.

Werkzeug unlinks and destroys temporary files created by `SpooledTemporaryFile` when the `FileStorage` stream closes at the end of the request context. However, if your code spawns background threads that reference the file after the request finishes, the temporary file may be closed prematurely, resulting in an I/O operation error on a closed file.

## Why Nginx Rejects Uploads Before Flask Sees Them

A frequent point of confusion in Python web deployments occurs when an engineer configures `app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024`, deploys the application behind Nginx and Gunicorn, and tests a `15 MB` upload. Instead of receiving the custom JSON error response defined in their Flask error handler, the client receives a generic, unstyled HTML page stating `413 Request Entity Too Large`.

This failure occurs because Nginx sits in front of Gunicorn and evaluates request limits before forwarding data upstream.

Nginx enforces its own request body limit through the `client_max_body_size` directive. By default, Nginx sets `client_max_body_size` to `1m` (1 megabyte).

When a client initiates an HTTP POST request with a `15 MB` payload, it sends a `Content-Length: 15728640` header. Nginx reads the request line and headers. Comparing the declared size against its default `1 MB` limit, Nginx terminates the connection immediately. It generates an internal HTTP 413 response and closes the TCP socket. The request never reaches the Gunicorn WSGI server, and your Flask application code never executes.

To allow larger uploads through Nginx to Flask, you must align `client_max_body_size` in your Nginx configuration with your Flask `MAX_CONTENT_LENGTH` setting:

```nginx
http {
    client_max_body_size 10M;
    server {
        listen 80;
        server_name api.example.com;
        location / {
            proxy_pass http://127.0.0.1:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        location /api/v1/uploads/ {
            client_max_body_size 50M;
            proxy_pass http://127.0.0.1:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_read_timeout 300s;
            proxy_connect_timeout 75s;
        }
    }
}
```

After modifying `/etc/nginx/nginx.conf`, test the syntax with `nginx -t` and reload the service using `nginx -s reload`.

### How to Diagnose the Originating Rejection Layer

When troubleshooting an unexpected 413 error in staging or production, identifying which component rejected the request determines the necessary fix:

- **Nginx Rejection:** The response contains a `Server: nginx` header, the body consists of default Nginx HTML, and your Gunicorn application access logs show no entry for the request. The solution is increasing `client_max_body_size` in `nginx.conf`.
- **Flask / Werkzeug Rejection:** The response matches your application's custom JSON schema or default Werkzeug HTML, and Gunicorn logs record an incoming request returning status code 413. The solution is adjusting `MAX_CONTENT_LENGTH` in `app.config`.
- **Cloudflare Edge Rejection:** If your domain routes through Cloudflare's proxy, Cloudflare inspects payload sizes at the edge network. Free and Pro accounts enforce a strict `100 MB` request body limit, Business accounts allow `200 MB`, and Enterprise plans support up to `5 GB`. If an upload exceeds your plan threshold, Cloudflare returns an edge 413 error before Nginx or Flask are ever contacted.

### Comparing Request Body Limits Across Web Layers

To prevent unexpected payload rejections, every layer along the request path must be configured to accommodate your target upload size. The lowest limit along the chain dictates the effective ceiling:

| Infrastructure Layer | Default Payload Limit | Configuration Setting | Failure Mechanism |
| --- | --- | --- | --- |
| Nginx Reverse Proxy | `1 MB` (`1m`) | `client_max_body_size` | Returns Nginx 413 HTML; closes client TCP socket |
| Apache HTTP Server | `1 GiB` (or unlimited) | `LimitRequestBody` | Returns Apache 413 error page |
| Microsoft IIS | `30 MB` (30,000,000 bytes) | `maxAllowedContentLength` | Returns IIS 413.1 or 404.13 status code |
| Cloudflare Proxy (Free/Pro) | `100 MB` | Cloudflare zone network settings | Edge 413 rejection; origin is never contacted |
| Cloudflare Proxy (Business) | `200 MB` | Cloudflare zone network settings | Edge 413 rejection; origin is never contacted |
| Gunicorn WSGI Server | Unlimited | Inherits worker request processing | Forwards body to Flask application layer |
| Flask Application | `None` (unlimited) | `app.config['MAX_CONTENT_LENGTH']` | Raises `RequestEntityTooLarge` (HTTP 413) |
| Werkzeug Form Parser | `500KB` (memory threshold) | `Request.max_form_memory_size` | Spools files to disk; rejects text fields > 500KB |

When configuring an application to handle `30 MB` uploads, you must set Nginx `client_max_body_size 35M;` (providing headroom for multipart boundary overhead) and Flask `app.config['MAX_CONTENT_LENGTH'] = 35 * 1024 * 1024`.

## When to Use Streaming and Chunked Uploads in Flask

Raising `MAX_CONTENT_LENGTH` and `client_max_body_size` to hundreds of megabytes or gigabytes solves the immediate 413 rejection, but it introduces architectural bottlenecks in production web applications.

Standard WSGI application servers like Gunicorn operate with a fixed pool of synchronous worker processes (typically 2 to 4 workers per CPU core). When a client uploads a `500 MB` file over an average consumer connection, that upload may take 30 to 60 seconds to complete. During this entire transfer window, a Gunicorn worker remains tied to the incoming socket, reading data into temporary storage.

If four users initiate large file transfers simultaneously, a four-worker Gunicorn service becomes completely saturated. All incoming requests for login pages, API queries, and lightweight assets must wait in the socket backlog, leading to gateway timeouts (`HTTP 504`) across your entire platform.

Overcoming this limitation requires moving away from monolithic multipart uploads toward streaming, chunking, or direct-to-storage architectures.

### How to Consume Raw Request Streams in Flask

For endpoints that ingest raw binary files, disk images, or streaming sensors, you can bypass Werkzeug's multipart parser entirely by reading from `request.stream`.

Reading directly from `request.stream` processes incoming bytes iteratively in fixed chunk sizes (such as `64KB` or `1 MB`). The application writes each chunk directly to an external destination, permanent storage path, or hashing engine without buffering the full file into memory:

```python
import os
from flask import Flask, jsonify, request
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024  # 500 MB limit

@app.route('/stream-upload', methods=['POST'])
def stream_upload():
    destination_file = '/var/data/uploads/received_asset.bin'
    chunk_size = 64 * 1024  # 64 KB read buffer
    total_bytes = 0
    with open(destination_file, 'wb') as f:
        while True:
            chunk = request.stream.read(chunk_size)
            if not chunk:
                break
            f.write(chunk)
            total_bytes += len(chunk)
    return jsonify({
        "status": "success",
        "bytes_stored": total_bytes
    }), 201
```

When reading `request.stream`, do not access `request.form`, `request.files`, or `request.data` earlier in the request lifecycle. Accessing those properties triggers Werkzeug's multipart parser, consuming the stream before your manual read loop begins.

### How Client-Side Chunking Prevents Server Timeouts

For web interfaces and mobile applications where users upload large media assets, client-side chunking provides a resilient alternative to single-socket uploads.

In a chunked architecture, the frontend client splits a large file into smaller slices (typically `5 MB` to `10 MB` each) using the browser `Blob.slice()` API. Each slice is transmitted as an independent HTTP POST or PUT request:

1. **Bypasses Proxy Ceilings:** Because every chunk is under `10 MB`, transfers never approach the `100 MB` Cloudflare ceiling or standard Nginx buffer limits.
2. **Resilience to Network Drops:** If a connection fails midway through a large file transfer, the client only retries the failed `10 MB` chunk rather than restarting the entire upload from byte zero.
3. **Reduced Worker Occupation:** Each request finishes in fractions of a second, freeing Gunicorn workers to handle other traffic.

Standardized protocols like Tus or custom chunked handlers store parts on disk or object storage, assembling the complete asset once the final chunk arrives.

### How Presigned Direct-to-Storage Uploads Eliminate Web Server Load

The cleanest architectural pattern for high-scale applications completely removes file payload bytes from the Flask application server.

Instead of routing multi-megabyte payloads through Nginx, Gunicorn, and Flask:

1. The client requests an upload authorization token from a lightweight Flask API endpoint (`POST /api/upload-ticket`).
2. Flask validates user authentication and permissions, determines target storage keys, and generates a presigned URL with an expiration window.
3. The client uploads the binary payload directly to the storage service via HTTP PUT.
4. Upon transfer completion, the storage service fires an event, or the client notifies Flask (`POST /api/upload-complete`) to record file metadata in the application database.

This pattern isolates your Flask API from network bandwidth bottlenecks, eliminates 413 configuration issues across intermediate proxies, and allows your application servers to remain lightweight and stateless.

## How AI Agent Workspaces Manage Large Corpora Beyond Upload Limits

In modern engineering workflows, Flask microservices frequently serve as ingestion gateways for artificial intelligence agents, autonomous coding assistants, and context retrieval pipelines. Developers build upload endpoints to ingest legal binders, technical documentation, financial reports, and codebase archives for downstream language model analysis.

However, routing large document collections through standard HTTP APIs quickly hits both network boundaries and model inference constraints.

Context limits enforce strict boundaries in AI environments. As documented by the [Anthropic Help Center](https://support.claude.com/en/articles/8241126-upload-files-to-claude), Claude chat sessions accept up to 20 files at up to 500 MB per file, while Claude Projects accept files up to 30 MB each with no fixed file-count cap, bounded only by the model's context window.

When development teams or autonomous agent workflows attempt to attach massive document sets directly to language model prompts, they encounter context window exhaustion. Similarly, pushing large corporate archives through Flask REST endpoints to feed local vector databases triggers Gunicorn memory pressure, proxy timeouts, and HTTP 413 rejections.

Solving this coordination problem requires decoupling file storage and search indexing from the application server.

### How to Decouple File Storage from Agent Context via MCP

Rather than streaming entire file collections through Flask endpoints or attaching raw files to LLM prompts, teams store corpora in persistent, searchable cloud workspaces.

Fastio provides intelligent workspaces designed for human teams and autonomous agents. When documents land in a Fastio workspace, Intelligence Mode automatically indexes the contents for hybrid search, combining full-text indexing, semantic search, and metadata filtering. Instead of loading an entire `500 MB` archive into an LLM context window, agents search the workspace index and retrieve only the specific passages and facts needed to answer a query, complete with source document citations.

Autonomous agents and developer tools connect to workspaces over Streamable HTTP using the remote Model Context Protocol (MCP) server. Teams building [storage for agents](/storage-for-agents/) can connect assistants at `https://mcp.fast.io/mcp/tools` (for general assistants and Claude applications) or `https://mcp.fast.io/mcp/code` (for coding agents like Claude Code, Cursor, and Gemini CLI). Full setup instructions are documented at [Fastio MCP Documentation](https://mcp.fast.io/docs).

Through MCP, an agent queries documents across the organization without requiring local disk allocation, file streaming across Flask proxies, or context-saturating attachments.

### How Cloud Sync and Intelligent Workspaces Replace Custom Upload Forms

Ingesting corporate data into an agent workspace does not require writing custom Flask upload forms or managing complex chunking scripts.

Fastio Cloud Sync connects Google Drive, Dropbox, Box, and OneDrive folders directly to a workspace. Synchronization runs one-way or two-way, on a schedule or on demand. When team members update files in an external Google Drive or OneDrive folder, the workspace syncs the updates, triggers background indexing, and makes the updated knowledge available to connected agents.

For applications that need direct programmatic uploads, Fastio handles large file transfers through chunked upload mechanics, supporting large assets up to plan limits: `25 GB` on Starter, `50 GB` on Business, and `100 GB` on Enterprise.

Monthly plans start with a 14-day free trial that requires a credit card. Plans detailed on the [workspace pricing](/pricing/) page include Starter at `$9.99/mo` (3 seats, `250 GB` storage, 5 workspaces, 100,000 credits a month), Business at `$49.99/mo` (10 seats, `5 TB` storage, 50 workspaces, 600,000 credits a month), and Enterprise at `$199.99/mo` (30 seats included, `25 TB` storage, 200 workspaces, 3,000,000 credits a month).

By delegating file persistence, synchronization, and RAG indexing to an intelligent workspace, your Flask microservices stay lightweight and avoid most HTTP 413 upload bottlenecks.

## Frequently asked questions

### How do I set the maximum upload file size in Flask?

Set the `MAX_CONTENT_LENGTH` configuration key on your Flask application instance to the desired byte limit, such as `app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024` for a 16 MB ceiling. If an incoming request body exceeds this number of bytes, Flask halts processing and raises a `RequestEntityTooLarge` (HTTP 413) exception.

### Why do I get 413 Request Entity Too Large in Flask?

An HTTP 413 error occurs when an incoming request body exceeds the configured `MAX_CONTENT_LENGTH` in Flask, or exceeds the `client_max_body_size` directive on an intermediate reverse proxy like Nginx. Flask checks the `Content-Length` header before reading data and aborts the request if the declared size is larger than the allowable threshold.

### How does Werkzeug handle file uploads larger than MAX_CONTENT_LENGTH?

Werkzeug checks the `Content-Length` header against `MAX_CONTENT_LENGTH` during initial request parsing. If the header exceeds the limit, Werkzeug immediately raises `werkzeug.exceptions.RequestEntityTooLarge` without reading the file stream. If chunked transfer encoding is used without a content length, Werkzeug reads stream chunks until the accumulated byte count crosses the limit, at which point it halts and raises 413.

### Why does Nginx return a 413 error even after setting MAX_CONTENT_LENGTH in Flask?

Nginx enforces its own payload boundary via `client_max_body_size`, which defaults to `1m` (1 megabyte). Because Nginx sits in front of Gunicorn or Flask, it evaluates the `Content-Length` header first. If an upload exceeds 1 MB, Nginx terminates the connection and returns its own HTML 413 error before Gunicorn or Flask ever receives the request.

### How can Flask stream request data without loading entire files into memory?

You can stream incoming request bodies iteratively by reading directly from `request.stream` using a loop with a fixed buffer size, such as `chunk = request.stream.read(64 * 1024)`. Avoid accessing `request.files`, `request.form`, or `request.data`, as reading those properties triggers Werkzeug's multipart parser and consumes the stream into temporary storage.

### How do AI agents access large file collections that exceed context windows and HTTP upload limits?

Rather than streaming multi-gigabyte document collections through Flask endpoints or attaching large files directly to model prompts, teams place files into intelligent cloud workspaces like Fastio. The workspace indexes the corpus for hybrid search, and agents connect via the Model Context Protocol (MCP) server to query specific passages without exceeding HTTP upload limits or context windows.

## Sources

- [Flask Documentation: Configuration Handling](https://flask.palletsprojects.com/en/stable/config/): Flask raises an HTTP 413 RequestEntityTooLarge error when an incoming request exceeds the byte limit configured by MAX_CONTENT_LENGTH.
- [Claude Help Center: Upload files to Claude](https://support.claude.com/en/articles/8241126-upload-files-to-claude): Claude chat sessions accept up to 20 files at up to 500 MB per file, while Claude Projects accept files up to 30 MB each with no fixed file-count cap, bounded only by the model's context window.

## About Fast.io

Fast.io provides shared workspaces where people and AI agents work on the same files, with built-in semantic search and citation-backed chat over what they hold. Agents reach it through a remote MCP server, a REST API at https://api.fast.io/current/, and a command line client published on npm as @vividengine/fastio-cli. MCP setup is at https://mcp.fast.io/docs: Claude and most MCP clients connect to https://mcp.fast.io/mcp/tools, ChatGPT to https://mcp.fast.io/mcp/operations, and coding agents to https://mcp.fast.io/mcp/code.
