CallTool, keeping binary data out of the context window entirely. This example stays fully client-side to demonstrate the UI mechanics, but in practice you’d wire the DropZone’s on_change (or a button’s on_click) to a CallTool that sends the base64 payload to your server for processing.
How It Works
The DropZone is bound to state by itsname="files" prop. When a user drops files, the renderer reads each one and writes a list of file descriptors into the files state key — each entry is an object with name, type, and size fields, plus the base64-encoded contents. Everything else on the page is a reactive function of that list. STATE as state imports the state accessor so the Python reads naturally as state.files, and state.files.length() compiles to a reactive expression the renderer re-evaluates whenever the list changes.
That length expression drives the page’s conditional structure. The header Badge, the file rows, and the footer message are each wrapped in If(state.files.length()), so they appear only once at least one file is present; the Else() branch in the footer shows “No files uploaded” otherwise. The file rows themselves come from ForEach: with ForEach("files") as (i, item): walks the list, exposing the loop index i and the current descriptor item, so item.name, item.type, and item.size render each file’s metadata. The per-row × button calls PopState("files", i) to drop that one file by index, and Clear all calls SetState("files", []) to empty the list — both client-side actions, so add/remove updates the count, the badge, and the rows with no server involved.
This example stays entirely in the browser to show the UI mechanics, which is why the DropZone is labeled “files aren’t actually uploaded.” The reason file uploads matter for MCP is the client/server boundary. When an LLM passes a file through the conversation, every byte is tokenized and counted against the context window. A DropZone sidesteps that: the file lands in client state as base64, and you forward it straight to your backend with a CallTool action — wiring the DropZone’s on_change or a submit button’s on_click to CallTool(...) with the payload from state.files. The tool runs server-side, processes the bytes, and returns a result; you read $result in the action’s on_success and write it back into state to update the UI. The binary data travels client → server directly and never enters the model’s context.