Mounting a provider
A provider is aFastMCPApp under the hood, and you attach one to your server with addProvider. That single call mounts the provider’s entrypoint and all of its backend tools onto your server, prefix-aware and ready to use.
server.ts
addProvider is a convenience over mounting: it accepts a FastMCPApp (or a plain FastMCP) and mounts its underlying server for you, so you never have to reach for .server or call mount directly. Because it mounts, every provider tool is namespaced under the provider’s name and its action references resolve against that prefix — the wiring described in Components is what makes a mounted provider work unchanged.
Approval
TheApproval provider renders a confirm/deny card and routes the user’s choice back into the conversation. It registers the entrypoint together with two app-only backend tools — one for confirm, one for deny — so each button has a tool to land on while neither appears to the model. The card’s buttons use action references, so the provider works the same whether it is mounted standalone or under a prefix.
Reach for Approval whenever a model-proposed action needs a human to sign off before it happens: a destructive operation, a purchase, anything you want a person in the loop for.
Choice
TheChoice provider renders a list of options as clickable buttons and reports which one the user picked. It registers the entrypoint and a single app-only _select backend tool; each option button carries its own value as fixed args, so clicking an option calls _select with that choice already filled in. This is the multi-option case of the button wiring — one backend tool, distinguished by the arguments each button supplies.
Use Choice when you want the user to select from a known set rather than type a free-form answer.
File upload
TheFileUpload provider renders a file picker and handles the bytes on the server, where they belong. It registers the entrypoint plus app-only tools to submit and delete files. On submit, the submit tool mints a FileHandle — a server-generated id paired with the file’s name, MIME type, and bytes — stores the file through a FileStorageAdapter, and returns that id to the conversation. The adapter’s contract is save / load / delete over an already-minted handle, so swapping in your own storage backend never touches how handles are created. The delete tool removes a stored file by its handle.
Server-side storage is the design. The model and the conversation deal only in handles: small references they can pass to other tools or remove. The bytes live wherever your FileStorageAdapter puts them — disk, object storage, a database — and your other tools fetch them by handle when they need the content. Keeping the payload on the server means a multi-megabyte upload costs the conversation a single identifier, and the storage backend is yours to choose by supplying the adapter.
Uploads do not live forever. The default in-memory adapter expires each file after a TTL — FileUploadOptions.ttlMs, 30 minutes by default — and this is the cleanup that holds in every era, including a stateless modern-HTTP request with no session to attach a file to. On a session-based transport (stdio or legacy HTTP) the provider additionally releases a file when the session closes, but that early cleanup is best-effort, layered on top of the TTL rather than the mechanism to depend on. A custom adapter carries its own lifecycle.
Reach for FileUpload whenever an app needs a user to provide a document, image, or dataset that downstream tools will process by reference.
Form input
TheFormInput provider turns a Standard Schema into a validated form. You hand it a schema — Zod, Valibot, ArkType, or anything else Standard Schema supports — and it renders a field per property and registers an app-only _submit tool that validates the submission against that same schema before your code sees it.
The schema is the single source of truth for both halves of the form. The fields are generated from the schema’s JSON Schema properties using the same conversion that advertises tool input schemas, so the form’s shape always matches what you declared. On submit, the values are validated through the schema’s ~standard.validate, so a submission that reaches your handler has already been checked against the exact contract you wrote.
server.ts
FormInput when you need structured, validated input and want the form and its validation to stay in lockstep without maintaining them separately.