Skip to content

Add FileUpload provider#3669

Merged
jlowin merged 8 commits intomainfrom
file-upload-provider
Mar 28, 2026
Merged

Add FileUpload provider#3669
jlowin merged 8 commits intomainfrom
file-upload-provider

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Mar 28, 2026

Extracts the file upload example into a reusable FileUpload provider. Adding file upload to any server is now one line:

from fastmcp import FastMCP
from fastmcp.apps.file_upload import FileUpload

mcp = FastMCP("My Server")
mcp.add_provider(FileUpload())

This registers a drag-and-drop UI tool, a backend store_files tool (app-only), and model-visible list_files/read_file tools. Files are scoped by MCP session and stored in memory by default. Storage methods receive the current Context, so subclasses can partition by user, tenant, or any other dimension — and have full access to auth tokens and request metadata for persistent backends.

class S3Upload(FileUpload):
    def on_store(self, files, ctx):
        user_id = ctx.access_token["sub"]
        ...

    def on_list(self, ctx): ...
    def on_read(self, name, ctx): ...

Also adds a new "Providers" group to the Apps docs nav, reorganizes the Apps sidebar into Building Apps / Providers / Advanced, and fixes an MDX parse error in the changelog.

@marvin-context-protocol marvin-context-protocol Bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. provider Related to the FastMCP Provider class server Related to FastMCP server implementation or server-side functionality. labels Mar 28, 2026
Base automatically changed from app-tool-prefixed-names to main March 28, 2026 01:46
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae23027aa2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +157 to +160
for f in files:
session_files[f["name"]] = {
"name": f["name"],
"size": f["size"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce server-side max_file_size checks

on_store persists every uploaded file without validating against self._max_file_size, so the configured limit is only a UI hint. Because app tools can still be invoked directly by name (for example Files___store_files), a client can bypass the DropZone constraint and submit arbitrarily large base64 payloads, leading to unbounded in-memory growth in _store despite max_file_size being set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 614bc2b. The validation now happens in the tool function itself (before on_store), so subclasses don't need to re-implement the size check.

Comment on lines +231 to +233
@self.tool(model=True)
def list_files() -> list[dict]:
"""List all uploaded files with metadata."""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Namespace model-visible file tools to avoid collisions

Registering model-visible tools with generic names like list_files/read_file makes this provider conflict with host servers that already define those names. FastMCP deduplicates tool listings by name and resolves call_tool by a single winner for that name, so in collisions the FileUpload tools can be hidden or unreachable to the model, undermining the “add to any server” behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what namespaces are for — server.add_provider(FileUpload(), namespace="files") gives you files_list_files, files_read_file, etc. Generic names are intentional for the simple case where there's no collision.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jlowin jlowin force-pushed the file-upload-provider branch from 614bc2b to c054a78 Compare March 28, 2026 01:54
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Mar 28, 2026

Test Failure Analysis

(Edited: updated for workflow run `23694104842`, commit `9cd5a1c8` — "Add FileUpload provider". The failure is identical to the previous analysis.)

Summary: One pre-existing test is failing across all Python versions — `test_background_task_can_read_snapshotted_request_headers` — due to HTTP request context not being available when a background task calls `get_http_request()` directly (without using FastMCP dependency injection).

Note: This failure is not introduced by this PR. The failing test was added to `main` in commit `1d8a8bc` ("chore: Update SDK documentation #3668") and is present on `main` independently of this PR's changes.


Root Cause: When a Docket background worker executes a tool, HTTP request headers are snapshotted to Redis at submission time (in `submit_to_docket`). However, those headers are only restored from Redis when the tool uses `CurrentRequest()` or `CurrentHeaders()` as dependency-injected parameters — not when the tool calls `get_http_request()` directly.

The test calls `get_http_request()` directly inside a `task=True` tool:
```python
@server.tool(task=True)
async def check_request_header() -> str:
request = get_http_request() # fails — _task_http_headers ContextVar is not set
return request.headers.get("x-tenant-id", "missing")
```

`get_http_request()` reads `_task_http_headers.get()`, which is `None` in the worker because `_restore_task_http_headers()` was never called before the function ran. That restoration only happens inside `_CurrentRequest.aenter` and `_CurrentHeaders.aenter`.

Suggested Solution: Restore `_task_http_headers` early in the background task execution path, before the user's function runs. In `FunctionTool.run()` (or a new wrapper registered with Docket), check for task context and call `_restore_task_http_headers()` if not already set:

```python

At the start of FunctionTool.run() (or a Docket-registered wrapper):

if _task_http_headers.get() is None:
task_info = get_task_context()
if task_info is not None:
await _restore_task_http_headers(task_info.session_id, task_info.task_id)
```

This mirrors what `_CurrentRequest.aenter` already does (see `dependencies.py:1186-1197`), but applied at the tool execution level so it covers all tools regardless of whether they use dependency injection.

Detailed Analysis

Failure log excerpt (all 4 failing jobs show the same failure):
```
FAILED tests/server/http/test_http_dependencies.py::test_background_task_can_read_snapshotted_request_headers
fastmcp.exceptions.ToolError: No active HTTP request found.
```

Jobs failing: Python 3.10 (windows-latest), Python 3.10 (ubuntu-latest), Python 3.13 (ubuntu-latest), Tests with lowest-direct dependencies — all with the same single test failure. MCP conformance tests and Integration tests both passed.

The contrast with the passing test (`test_background_task_current_http_dependencies_restore_headers`):
```python

This PASSES — uses dependency injection

@server.tool(task=True)
async def check_headers(
headers: dict[str, str] = CurrentHeaders(), # triggers _restore_task_http_headers
request: Request = CurrentRequest(), # triggers _restore_task_http_headers
) -> dict[str, str]: ...
```

The failing test (added in commit `1d8a8bc`):
```python

This FAILS — calls get_http_request() directly

@server.tool(task=True)
async def check_request_header() -> str:
request = get_http_request() # _task_http_headers not set in worker
return request.headers.get("x-tenant-id", "missing")
```

Code flow:

  • `submit_to_docket()` (handlers.py:127-143) — snapshots headers to Redis ✓
  • `_restore_task_http_headers()` (dependencies.py:847) — restores from Redis, but only called from `_CurrentRequest.aenter` / `_CurrentHeaders.aenter`
  • `get_http_request()` (dependencies.py:444) — reads `_task_http_headers.get()` which is `None` ✗
Related Files
  • `tests/server/http/test_http_dependencies.py` — the failing test (added in commit `1d8a8bc`)
  • `src/fastmcp/server/dependencies.py` (~line 444) — `get_http_request()`, reads `_task_http_headers.get()`
  • `src/fastmcp/server/dependencies.py` (~line 847) — `_restore_task_http_headers()`, restores from Redis
  • `src/fastmcp/server/dependencies.py` (~line 1186) — `_CurrentRequest.aenter`, where restoration currently happens
  • `src/fastmcp/server/tasks/handlers.py` (~line 127) — `submit_to_docket()`, where headers are snapshotted to Redis
  • `src/fastmcp/tools/function_tool.py` — `FunctionTool.run()`, candidate location for the fix

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b66e215f8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

def store_files(files: list[dict]) -> list[dict]:
"""Store uploaded files. Receives file objects with name, size, type, data (base64)."""
for f in files:
if f.get("size", 0) > provider._max_file_size:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce max_file_size against decoded payload bytes

The size guard in store_files trusts the client-provided size field, so callers can bypass max_file_size by sending a very large base64 data value with a small declared size (for example size=1). In that case the file is still persisted in memory by on_store, which defeats the server-side limit and can still cause unbounded memory growth under direct tool calls.

Useful? React with 👍 / 👎.

if is_text:
try:
result["content"] = base64.b64decode(entry["data"]).decode("utf-8")
except UnicodeDecodeError:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Catch base64 decoding errors for text reads

read_file only catches UnicodeDecodeError for text files, but malformed base64 (which can be stored via direct store_files calls) raises binascii.Error before decoding to UTF-8. That turns read_file into a hard tool failure instead of returning the intended content_base64 fallback preview for unreadable content.

Useful? React with 👍 / 👎.

@jlowin jlowin force-pushed the file-upload-provider branch from 9cd5a1c to dff412c Compare March 28, 2026 23:04
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dff412c822

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/fastmcp/apps/file_upload.py Outdated
For custom persistence, override the storage methods::

class S3Upload(FileUpload):
def on_store(self, files: list[dict]) -> list[dict]:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include Context in storage hook override signatures

The custom-storage example omits ctx in the on_store/on_list/on_read signatures, but the provider calls these hooks with context (on_store(files, ctx), etc.). Implementing the documented signatures will fail at runtime with TypeError when a file is stored or read, so the example should include the ctx: Context parameter on each override.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the module docstring now includes ctx in the override signatures.

@jlowin jlowin merged commit 5338629 into main Mar 28, 2026
10 checks passed
@jlowin jlowin deleted the file-upload-provider branch March 28, 2026 23:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. provider Related to the FastMCP Provider class server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant