Changelog¶
0.3.12 (2026-07-08)¶
- Fix:
set_widget/clickwith a non-string identifier (e.g.Noneor a number — an agent that omits or nulls theidentifierargument) now raise a cleanWidgetNotFoundinstead of crashing withTypeError: expected string or bytes-like object. Thekind[index]resolver added in #41 ranre.fullmatch()on the identifier; aNone/int slipped past the key/label comparisons and hit the regex, which raised a rawTypeError._findnow rejects a non-string identifier up front. (Surfaced by the StreamlitArena benchmark, driving apps with a small model that emitted a null identifier.)
0.3.11 (2026-07-07)¶
- Fix: the CLI no longer mangles
text_input/text_areavalues that look like JSON (#43).--setJSON-parses values so typed widgets get real numbers/lists/booleans, but that parse was applied unconditionally — soComment=trueon a text field became the booleanTrue, then wasstr()-ified to"True";null→"None"; a pasted{"a": 1, "b": true}came back as a Pythonrepr("{'a': 1, 'b': True}", no longer valid JSON). The same value over MCP is stored verbatim, so this was silent data corruption on the most common widget kind and a human↔agent parity break. The CLI now resolves the target widget kind first and passes the raw string through fortext_input/text_area(keeping JSON parsing for list/number/boolean widgets), soComment=truestores"true"— matching MCP.
0.3.10 (2026-07-06)¶
- Fix: the
kind[index]identifier thatlist_widgets/get_layout/inspectadvertise for a keyless, empty-label widget (e.g.text_input[1]) is now resolvable byset_widget/click(#41)._identifierminted thekind[index]fallback but_findonly matched key/label, so the one handle the tools ever exposed for such a widget was a dead handle —set_widget/clickrejected it withno widget matching, breaking thelist_widgets→set_widgetround-trip on both CLI and MCP._findnow resolves thekind[index]form. To guarantee it names the same widgetsnapshot()advertised (accessor order ≠ document order once a sidebar is involved),snapshot()and_findnow share one document-order widget iterator, so the numbering can't drift between what's shown and what's resolved.
0.3.9 (2026-07-05)¶
- Fix:
read_output/get_layout/list_widgets/inspectnow return elements in document/render order instead of grouping them by kind (#39).snapshot()built its lists by iterating each kind's typed accessor (at.title,at.markdown,at.text_input, …) in turn, so every heading was hoisted above all body text and a form's fields were regrouped by type — the transcript an agent read back bore no relation to how the app renders. It now walks the block tree (sidebar then main, recursing into columns/expanders) in render order, so the "rendered element tree" the README advertises is actually ordered. Same elements are captured — only their order changes — on every surface (text CLI,--json, MCP). Order within a kind was already correct; this fixes the cross-kind grouping.
0.3.8 (2026-07-04)¶
Proactive hardening of guardrail enforcement coverage, from a self-audit of every action/read path against read-only, allow-list, and bearer auth (the class behind #4/#7/#26):
- Security fix: the
--allowallow-list no longer leaks a hidden widget's value. A non-listed widget was correctly dropped fromlist_widgets/get_layoutand blocked fromset_widget/click, but its value still came back through everysession_state-bearing read (get_state,read_output,get_layout, and the dict a write returns) — so the allow-list guarded the widgets surface but not the state surface (the same "guard misses a path" shape as #26). The allow-list now filters those hidden widgets' values out ofsession_stateon all read paths, on both the CLI and MCP. App state that isn't a widget (counters, flags an app stashes insession_state) is preserved, so nothing is over-hidden;--read-onlyand the no-guard path are unchanged (they don't filter reads). Seedocs/security.mdfor the one remaining caveat (the allow-list governs widget state, not what an app chooses to render).
0.3.7 (2026-07-04)¶
Proactive hardening of set_widget value coercion, from a self-audit of the whole widget
surface for the silent-revert / atomicity class (siblings of #12, #31, #33):
- Fix: an out-of-range element in a
date_inputrange ((start, end)) is now rejected instead of silently reverting. The range value stayed a list of raw strings (only bare-string dates were coerced), so_validate_rangehit astr < dateTypeError, bailed, and let the bad date slip through to a silent revert-and-report-success.date_inputnow coerces each end of a range to a real date, so the bounds check runs and rejects the bad end up front, leaving the prior value untouched (atomic, CLI + MCP). - Cleaner errors: an unparseable
number_input,date_input, ortime_inputvalue now raises a clear, actionable message (e.g.'abc' is not a valid number for number_input;'25:99' is not a valid time for time_input; use 24-hour 'HH:MM' like '09:30') instead of a raw PythonValueError(could not convert string to float,Invalid isoformat string). - Boolean spellings:
checkbox/toggleaccept the natural string/int spellings a human passes on the CLI (true/false/1/0/yes/no/on/off, case-insensitive) and reject anything else with a clearnot a valid booleanerror rather than an opaque rollback message.
0.3.6 (2026-07-04)¶
- Fix: an invalid range (two-handle)
select_slidervalue is now rejected instead of silently reverting (#33). The single-value form was already validated, but_validate_choiceexplicitly skipped list/tuple values, so a range set with a handle that isn't an offered option (e.g.["xl", "NOPE"]) fell through all three safety nets — AppTest reverts the bad handle to the default without raising, soset_widgetreported success (exit 0/isError=false) while discarding the requested value and clobbering any prior valid range.select_slidernow validates every handle againstoptions(likemultiselect), raising a clear error up front and leaving the prior value untouched (atomic, CLI + MCP). Closes the last known gap in the silent-revert class (#10/#12/#31).
0.3.5 (2026-07-03)¶
- Fix: an invalid
color_pickervalue is now rejected instead of silently reverting (#31).color_pickerbecame a supported widget in 0.3.4, but a bad value ("notacolor", a CSS name, a wrong-length hex) fell through both validation nets: AppTest normalizes it back to the widget default without raising, soset_widgetreported success (exit 0/isError=false) while discarding the requested value — and clobbering any prior valid one in a long-lived session. This is the same silent-revert class as the out-of-range fix in #12, now closed forcolor_picker:set_widgetvalidates up front that the value is a#RGB/#RRGGBBhex string and raises a clear error otherwise, leaving the prior value untouched (atomic, on both the CLI and MCP).
0.3.4 (2026-07-02)¶
- Widgets no longer silently dropped (#29). Any input widget that was neither in the
supported set nor the unsupported list vanished from
widgetsandunsupportedon every surface, breaking the "reported explicitly, never silently dropped" guarantee. Now: time_input,toggle,select_slider, andcolor_pickerare supported — introspected and drivable viaset_widget(AppTest drives them;time_inputaccepts"HH:MM"). This also resolves the inconsistency wherelive()syncedtime_inputbutinspectshowed nothing.- the remaining input widgets streamlit-mcp can't drive (
pills,segmented_control,feedback,link_button,page_link,form_submit_button, plus the existing file/camera/audio/chat/data_editor/download_button) are reported inunsupported.
0.3.3 (2026-07-01)¶
- Fix: an uncaught app exception no longer corrupts stdout (#27). Streamlit prints a rich
traceback to stdout when a served app raises; that made
--jsonunparseable and put non-protocol bytes on the stdio MCP JSON-RPC channel. The app's stdout is now redirected to stderr for the duration of each run, so stdout carries only the JSON payload / MCP messages — the error is still reported in the structuredexceptionfield. - Fix (security):
--read-onlyand--allownow cover@mcp_toolsemantic tools on both the CLI and MCP (#26). Previously a semantic tool ran with full side effects despite--read-only, returning success. It now fails closed:--read-onlyblocks any tool;--allowgates tool names too (--allow <tool>opts one back in). - Robustness: the AppTest run timeout is raised from its 3s default so a slow app or a loaded CI box doesn't spuriously fail a run.
0.3.2 (2026-06-29)¶
- Fix:
live()'s polling fragment is now reliably skipped under headless AppTest (the agent driving over MCP, or tests). The previousst.runtime.exists()gate wasTrueunder AppTest too, so therun_everyfragment could install and intermittently hang a headless run for apps usingst.columns. AppTest mocks the runtime, so a genuineRuntimeinstance is now the gate; the live browser still polls as before. - Docs: new "Dynamic / agent-driven layout" guide +
examples/dynamic_app.py— the agent adds components and rearranges the layout by driving state (the app's structure is a function of state it controls), with the human watching live.
0.3.1 (2026-06-29)¶
- Fix:
live()now syncsdate_input/time_inputvalues.FileStoreused a plainjson.dumps, so a synceddatetime.date(a documented supported widget) raisedTypeError: Object of type date is not JSON serializable— crashing the rerun, never persisting the store, yet reporting success.FileStorenow uses a symmetric date/datetime/time codec so those values round-trip back as real objects (#23). - Fix (parity):
@mcp_toolsemantic tools are now reachable from the CLI, restoring the "Human ↔ agent parity" guarantee.inspectlists them (text,--json,--layout) andcall --tool <name> [--arg k=v ...]invokes one — both via the same registry the MCP server uses. Previously they were callable over MCP but invisible/uncallable from the CLI (#21).
0.3.0 (2026-06-28)¶
- New:
streamlit_mcp.live— opt-in live human-in-the-loop sync. Wrap your widgets inwith live(name, defaults={...}):and an agent's edits over MCP appear in a watching browser live (no manual refresh, no browser automation). It bridges Streamlit's isolated sessions through a shared, versioned store the app re-reads — re-seeding widgetsession_statebefore widgets are created, publishing local edits on exit, and polling viast.fragment(run_every=...)in a live browser (skipped under headless AppTest). Ships aFileStore(atomic writes) by default and aStoreprotocol so a custom backend (e.g. Redis) can be passed for multi-node. Purely app-side — no new MCP tools, no engine/server changes. See the docs "Live / human-in-the-loop" page andexamples/live_app.py.
0.2.3 (2026-06-27)¶
- Fix: a
@mcp_tooldefined in the served app file is now actually exposed overserve(#14). The decorator only fires when the app module executes, but sessions run the app lazily — after the tool list was already built — so app-file semantic tools were silently never registered.servenow loads the app once at startup before building the tool list, and@mcp_toolregistration is idempotent so per-session re-runs don't error. Documented in the README. - Fix:
inspecton a missing/unloadable app file now prints a clean one-line error and exits 1, matchingcall, instead of dumping a raw Python traceback (#15).
0.2.2 (2026-06-26)¶
- Fix: an out-of-range
number_input/slider/date_inputset_widgetis now rejected up front with a clear error, instead of silently reverting the widget to its default and reporting success (#12). 0.2.1 made option widgets atomic; range-constrained widgets fell through both safety nets because AppTest doesn't raise on an out-of-range value — it resets to the default — so a bad value reportedisError=Falsewhile discarding the prior valid value.set_widgetnow range-checks against the widget'smin/maxbefore writing, matching the option path.
0.2.1 (2026-06-25)¶
- Fix: a failed
set_widgetno longer poisons a long-lived MCP session (#10). Setting a selectbox/radio/multiselect to an option that isn't offered is now rejected up front with a clear error (it lists the valid options) before any state changes — previously the bad value was left pending in the AppTest runtime, so every laterset_widget/clickon any widget re-raised the stale error and the failing call could silently apply its own mutation. Any other failed run is now rolled back to the prior value so the session stays usable, and the error is attributed to the call that caused it.
0.2.0 (2026-06-25)¶
- Bearer auth is now enforced on HTTP/SSE (#7).
serve --transport http|sse --bearer-token <T>wires a FastMCP token verifier, so every request must carryAuthorization: Bearer <T>— a missing or wrong token gets 401 before any tool runs. stdio stays local/unauthenticated. - Non-loopback hosts are now allowed when a token is set (auth gates access); without a
token,
servestill refuses a non-loopback host (fail closed). - Removes the 0.1.2 "token is set but not enforced" startup warning — it's no longer true.
- CI: bump
actions/checkout(v4→v7) andastral-sh/setup-uv(v5→v7) to clear the Node-20 deprecation (#8).
0.1.2 (2026-06-24)¶
- Fix (security UX):
serve --transport http/ssenow prints a prominent stderr warning when--bearer-tokenis set, because bearer auth is not yet enforced on the transport — the server accepts unauthenticated loopback requests. Previously the flag was silently accepted with no effect while--helpclaimed it was "required", implying a protection that did not exist. The--helptext now states the flag is reserved/not-yet-enforced (#4). (RealFastMCP(auth=…)enforcement remains the documented top follow-up.)
0.1.1 (2026-06-23)¶
Fixes from a clean-room dogfood of the published 0.1.0.
- Fix (parity):
inspect --layouttext output now lists unsupported elements. Theunsupportedsection was present in--jsonand the MCPget_layouttool but silently dropped from the default human/text view, contradicting the "reported explicitly, never silently dropped" guarantee (#1). - Add: a top-level
--versionflag (streamlit-mcp --version) (#2). - Fix: silence Streamlit's explicitly-ignorable "missing ScriptRunContext!" bare-mode
warning that leaked to stderr on every
inspect/call/serve(#2). - Packaging/docs: declare Python 3.13 support (trove classifier + CI matrix); 0.1.0 is marked released below (#2).
0.1.0 (2026-06-20)¶
First release. Serve an existing Streamlit app as an MCP server, driven headlessly via
streamlit.testing.v1.AppTest — no browser automation.
- Auto-introspect all ten v1 widget kinds (text_input, number_input, text_area, slider, selectbox, multiselect, checkbox, radio, button, date_input) into MCP tools.
- Core MCP tools:
list_widgets,get_layout,set_widget,click,read_output,get_state. Unsupported elements are reported explicitly. - Transports: stdio and HTTP/SSE (see Known issues for HTTP auth status).
- Human-first CLI (
serve/inspect/call) with parity to the MCP tools. @mcp_tooldecorator for opt-in semantic tools.- Guardrails: read-only mode and widget allow-list (enforced on both CLI and MCP).
Known issues / immediate follow-ups¶
Snapshot as of 0.1.0. Later releases resolved some of these (see the entries above); the
README's "Known limitations" tracks what's still open today.
- HTTP bearer auth is not yet enforced on the transport. The token primitive
(Guardrails.require_bearer) is implemented and tested, but is not yet bound to the
FastMCP HTTP/SSE request path. As a safeguard, serve refuses to start an HTTP/SSE
server on a non-loopback host. Wiring FastMCP(auth=...) with a token verifier is the
top follow-up before networked HTTP is supported.
→ Resolved in 0.2.0 — bearer auth is enforced (401 without a valid token), and a
non-loopback host is allowed when a token is set.
- Sessions are not yet disposed. Per-client isolation works, but there is no
session-close hook, so long-running HTTP servers accumulate runtimes. Single-client and
stdio use are unaffected.
- No concurrency locking. Concurrent requests sharing one session are not serialized;
AppTest is not known to be re-entrant. Use one in-flight request per session for now.
- Output capture covers headings/markdown/caption/text; st.write/st.error/etc. are a
planned coverage expansion.