Changelog¶
0.7.1 (2026-07-19)¶
Documentation fix. README.md is the PyPI long description, so this correction is the point of
the release — the text was wrong on the project page.
-
Fix: the README listed
st.pills,st.segmented_controlandst.feedbackas supported and, six lines further down, among the input widgets streamlit-mcp "can't drive" (#72). The 0.7.0 promotion updated the same sentence indocs/usage.mdand missed the README's, so a reader going top-to-bottom hit a direct contradiction on a headline 0.7.0 capability — and the second claim was simply false. The undrivable list now readsfile_uploader,camera_input,audio_input,chat_input,data_editor, which matches what the code reports. -
Internal: CI now guards the two classes the nightly dogfood routine keeps finding first. The routine installs the published package clean-room and follows the docs verbatim, so it is a doc-conformance tester by construction and every doc/behaviour gap is a guaranteed find. The docs' widget lists are now asserted against
SUPPORTED_KINDS/UNSUPPORTED_ELEMENTS(in both docs — checking only one is how #72 escaped), and text↔--jsonsurface parity is asserted across the whole command matrix rather than one surface at a time, which is what let #1 → #58 → #64 keep re-opening.
0.7.0 (2026-07-19)¶
Widens what an agent can see and drive, plus one crash-reporting false positive. The supported list turned out to be stale rather than accurate — three widgets it called undrivable are drivable, and the read surface had a larger gap than any of them.
-
New: an agent can now see the status and data outputs an app renders, not just its prose. Previously only
title/header/subheader/markdown/caption/textwere reported, so an agent that filled a form and clicked submit could not tell whether the app answeredst.success("Saved")orst.error("Name is required")— the outcome of its own action was invisible, leaving it to re-read widget values and guess. Adds the status kinds (success,error,warning,info) and the data kinds (metric,code,json,dataframe,table).A
metricis assembled rather than stringified — itsvalueis the bare number, so a dashboard of four metrics read as four anonymous numbers; it now reports asTotal: $4,210 (+8%). Output text is capped, becausest.jsonserializes a whole structure (~11 KB for a 2000-element list) where a DataFrame self-truncates, and one output should not crowd out the rest of the app. -
New:
st.pills,st.segmented_controlandst.feedbackare supported widgets, no longer reported as undrivable.pillsandsegmented_controlshare thebutton_groupprotobuf name in the element tree — which is why they were written off — but Streamlit's typed accessors tell them apart, so they are driven under the names you write in the app. Afeedbackwidget's scale (thumbs0-1,faces/stars0-4) is read off the widget proto by enum name.All three corrupt silently when driven naively, so each is guarded up front, in the tradition of #12/#31/#55: a bad option on a single-select reverts with no exception; a bad member of a multi-select is silently dropped (
['y','NOPE']lands as['y']— a partial write reported as success, #33); and an out-of-rangefeedbackrating is neither rejected nor reverted but stored —5on a 5-star widget,99,-1all stuck. Aselection_modeis not exposed by Streamlit, so the value's shape is the signal (a list means multi), the same signal a range widget gives. -
Fix: a deliberate
st.exception(e)is no longer reported as an app crash (#69). It is the documented way to show a handled error, but every exception element was read as the app's uncaught exception — so an app that handles errors correctly looked broken on every surface, and--strictfailed CI for it. Streamlit offers nothing that separates the two directly (there is noSCRIPT_STOPPED_WITH_EXCEPTIONevent, and a caught exception displayed on purpose carries a real stack trace just like a crash), but an uncaught exception halts the script — so anything rendered after an exception element proves it was deliberate. Deliberately partial: a lonest.exception(e)as an app's final statement is indistinguishable from a crash and keeps reporting, because missing a real crash would break the guarantee #27/#58/#64 exist to give.
Behavior changes: read_output/get_layout now report more output kinds, so an agent reading
outputs sees entries it did not before (additive; existing kinds are unchanged, and exception
is deliberately excluded so a crash is not duplicated into outputs). pills,
segmented_control and feedback move out of unsupported and into widgets, and are settable.
An app that only ever displayed a handled exception no longer reports an exception field and no
longer exits non-zero under --strict.
0.6.0 (2026-07-18)¶
Two self-consistency fixes from the nightly dogfood run, both instances of the same theme the 0.5.x line has been working through: the tool must not contradict itself across its own surfaces. Also carries the 0.5.1 fix below, which was never published separately.
-
Fix: a widget built from non-string options (
st.selectbox("Year", [2023, 2024, 2025])) no longer reports avaluethat violates theschemathe same call advertises (#62). It read backvalue: 2023(int) against{"type": "string", "enum": ["2023","2024","2025"]}— a value that is neither a member of nor the type of its own schema, so a schema-validating agent balked at output the tool itself emitted. #51 fixed only the write path (set_widgetmatches on string form); the advertised model stayed self-inconsistent. Theenumnow carries both forms —[2023, 2024, 2025, "2023", "2024", "2025"]— so the reported value is always a member, and any option can be set in either form, not just the one currently selected.Streamlit hands over options already stringified, so the current value's type is the only evidence of the real option type available at runtime. It's used to recover every option's typed form, guarded by a round-trip check so a typed member always denotes the option it came from: a mixed list (
[1, "two", 3.0]) keeps the unrecoverable options in string form, andboolgets its own mapping (bool("False")isTrue). A widget with nothing selected (an untouchedst.multiselect("Nums", [1,2,3])) offers no such evidence and keeps the string form — still correct and settable, just less informative. -
Fix: a command's
--jsonform now carries the app-levelexceptionits own text form prints (#64). 0.5.0 (#58) made all four text CLI forms report a crashed app, but bareinspect --jsonandcall --state --jsonstill dropped it — so within the same command the human sawexception: boomand a script doinginspect --json | jq .exceptiongotnull. Those two payloads come fromlist_widgets/get_state, which legitimately don't carry the field (they mirror MCP tools that don't), whileget_layout/read_outputdo. The value is now injected once per command wherever it's missing, rather than being plumbed surface by surface — the pattern that let this family (#27 → #58 → #64) keep re-opening. The MCP contract is unchanged; this is a CLI text↔--jsonparity fix, and it closes a gap against the documented guarantee that the exception is surfaced on every read surface.
Behavior changes: a non-string-option selectbox/radio/select_slider/multiselect now
advertises a mixed-type enum (both the typed and string form of each option) and no longer
carries "type": "string", since a mixed enum has no single JSON-Schema type; pure-string-option
widgets are untouched. inspect --json and call --state --json gain an "exception" field when
the served app raised — a healthy app's payload is byte-identical to before. No previously-correct
set stops working.
Thanks to @Sanjays2402 for #63, which found and fixed the non-string-option schema inconsistency.
0.5.1 (2026-07-16 — never published separately; shipped in 0.6.0)¶
Completes the null-placeholder fix from 0.5.0 (#57), which covered only selectbox/radio and
left the other widgets that legitimately hold value: None broken the same way (#60). The whole
class is now handled by one shared path — schema nullability keyed on the value, and a single
kind-agnostic write path — so it can't re-open widget-by-widget.
-
Fix: a
text_input/text_areabuilt withvalue=None(the "empty field" placeholder) no longer corrupts its value on write-back. Echoing its reportedvalue: nullback over MCP —set_widget("name", null)— stored the literal string"None"and returnedisError=False: the value an agent just read couldn't be sent back, state was silently mutated to a wrong value, and the failed round-trip was reported as success. Thestr(value)coercion added for #43 (so a JSON-typed value on a text field becomes its string —True→"True") wrongly caughtNone, the field's own no-value sentinel.Nonenow passes through and round-trips asnull. (Non-None values still stringify, as #43 intends.) -
Fix: the
None-placeholder schema is now nullable for every widget that reportsvalue: null, not justselectbox/radio. Atext_input/text_area/number_input(anddate_input/time_input) built withvalue=Noneadvertisedvalue: nullagainst a non-nullable schema ({"type": "string"}/{"type": "number"}), so a schema-validating agent balked at a value the tool itself emitted — the (b)-half of #57, left unfixed for non-option widgets.tool_schema_fornow widens the schema to allow null whenever the reported value is null, uniformly ({"type": ["string","null"]},{"type": ["number","null"], …}), so the reported value always satisfies its own schema. -
Under the hood,
set_widget(id, null)is now one kind-agnostic operation ("return the widget to its no-value/no-selection state"): avalue=None/index=Noneplaceholder of any kind accepts null and round-trips it; a regular widget (aselectboxwith a default, a plaintext_input, anumber_inputwith a value) has no no-value state and rejects null atomically — with its prior value intact — instead of silently keeping it (text previously stored"None"; a regularnumber_input/selectboxsilently no-op'd or reset to default). One documented rule per the issue's suggestion, so the next placeholder-capable widget is covered for free.
Behavior changes: a value=None text_input/text_area/number_input/date_input/
time_input now advertises a nullable schema (["…","null"]) instead of the bare type, and
set_widget(id, null) on such a widget round-trips (text no longer stores "None"); a regular
widget of those kinds now rejects null where text used to store "None" and number silently
ignored it. No previously-correct set stops working.
0.5.0 (2026-07-15)¶
Two more fixes from the nightly dogfood routine — both restoring fidelity between what a surface reports and what it accepts, the same through-line as 0.4.0.
-
Fix: a placeholder
selectbox/radio(built withindex=None— the ubiquitous "please select…" pattern) now round-trips its no-selection value (#57). Its value is reported asnull, but thatnull(a) violated the widget's own advertisedschema—list_widgets/inspect --jsonemittedenum: [...options]with nonull, while reportingvalue: null— and (b) was rejected on write-back:set_widget("choose", null)failed with "None is not a valid option", even thoughnullis exactly the value the tool just reported. So the round-trip was broken for the whole placeholder class: an agent readvalue: nulland couldn't send it back. Two coordinated changes: the schema is now nullable when the value is null ({"type": ["string","null"], "enum": [...options, null]}), so the reported value satisfies its own schema; andset_widget(id, null)clears the selection back to the placeholder (a real "reset this filter" operation AppTest supports). A regular (index=0) selectbox/radio, which genuinely has no no-selection state, still rejectsnull— with the prior value rolled back, so the set stays atomic. (That path can't be pre-validated: AppTest exposes no "is nullable" flag and silently keeps or default-resets a non-nullable widget set tonull, so it's caught by settingnulland verifying the selection actually cleared — sound becausenullis never a value-corrupting write to attempt.) -
Fix: the human text CLI now surfaces a served app's uncaught exception, matching
--jsonand MCP (#58). When an app raises, the exception is captured in the structuredexceptionfield and reported on--json(call/inspect) and over MCP (read_output/get_layout) — but the default text CLI dropped it, printing only the partial render withexit 0and no error line, so a crashed app looked like a clean, successful run (and only Streamlit's raw stderr traceback hinted otherwise — the very dump #27 keeps off the protocol channel). This broke the headline human↔agent parity guarantee on the error surface.call --read,call --state, andinspect --layout/bareinspectnow print anexception:line matching what--json/MCP carry. -
New: a
--strictflag oncallandinspectmakes them exit non-zero when the served app raised an uncaught exception, so a crashed app is detectable in CI/scripts without parsing--jsonfor a non-nullexception(streamlit-mcp call app.py --read --strict || …). Default behavior is unchanged: an app-level exception is a reported field, not a failure — over MCP it returns withisError=False, and the CLI mirrors that withexit 0unless--strictis set. (A guardrail/load error is a real failure and still exits non-zero regardless.)
Behavior changes (why this is a minor bump): a placeholder selectbox/radio advertises a
nullable schema (["string","null"]) instead of the bare string enum, and set_widget(id, null)
on such a widget now succeeds where it used to error; the text CLI prints an exception: line for a
crashed app where it previously printed nothing. No previously-working set stops working, and the
default exit codes are unchanged.
0.4.0 (2026-07-13)¶
Five fixes from the nightly dogfood routine, all of the same family: the widget model lost type, arity, or placement fidelity, so a value an agent read back could not be sent back.
-
Fix: a widget built from non-string options (
st.selectbox("Pick", [1, 2, 3])— a very common pattern) can now be set to its natural typed value (#51). AppTest stringifies a widget's options but reports its value in the real type, so such a widget advertisedoptions: ["1","2","3"]while reading backvalue: 1— and_validate_choicecompared the incoming value against the stringified options, rejecting2(a genuine option, and the same type as the value the tool had just advertised) as "not a valid option". The whole class of numeric/non-string-option widgets was undrivable on the CLI and misrepresented over MCP, breaking thelist_widgets→set_widgetround-trip. Membership is now compared on the string form of both sides, so both the typed value (2) and the option's string form ("2") are accepted — AppTest resolves either to the real option. A value that genuinely isn't an option is still rejected. (Also: a fractional value on an integernumber_input—Score=30.5, which AppTest silently truncated to30while reporting success — is now rejected up front.) -
Fix: unsupported widgets placed through a container accessor are no longer silently dropped (#52).
detect_unsupportedregex-scanned the source for the literalst.<name>(form, sost.sidebar.file_uploader(...),col.camera_input(...),container.download_button(...)andtab.data_editor(...)— the sidebar and columns idioms most real apps are built from — matched nothing and were reported nowhere, on any surface. That is exactly the failure the "never silently dropped" guarantee exists to prevent: an agent introspecting such an app got no indication the widget existed. The scan now parses the source as an AST and matches the call node's attribute, so any receiver is caught (including an aliasedimport streamlit as sl), and occurrences in comments and string literals — previously reported as real — no longer are. (The runtime element tree can't serve as the detector: AppTest names nodes after their protobuf type, sost.data_editorarrives asdataframe, indistinguishable from a plainst.dataframeoutput, and pills/segmented_control/feedback all collapse intobutton_group.) -
Fix:
st.formis now a supported flow, andform_submit_buttonis no longer double-reported (#53). It was listed both as a supported, clickablebuttonand asunsupportedwith the reason "drive it another way" — which was simply false: clicking it submits the form and runs its body, over the CLI and MCP alike. An agent readingget_layoutreasonably concluded it could not submit the form, and gave up on an interaction it was fully capable of performing.form_submit_buttonis dropped fromUNSUPPORTED_ELEMENTS; a form is driven the way a human drives it — set the fields, then click the submit button. -
Fix: the CLI no longer JSON-pre-parses a
--setvalue that is one of the widget's own options (#54). An option widget whose options are genuinely strings that look like JSON tokens (["true","false"], a["1","2","3"]version picker) could not be set from the CLI at all:--set "Env=true"was pre-parsed to the booleanTrue, matched no option, and was rejected — while the identical string sent over MCP was accepted. A CLI-only failure on a common widget class, and a human↔agent parity break; the only way through was to guess the--set 'Env="true"'quoting trick. This is the #43 class (CLI JSON pre-parse corrupting a value that should be a literal string), which #43 fixed only fortext_input/text_area.--setnow prefers an exact match against the target's advertised options over the JSON parse (Rng=[5,95],Tags=[]andAge=41keep parsing as before). -
Fix: two-handle range widgets (
st.slider("P", 0, 100, (20, 80)),st.select_slider(..., value=("s","l")),st.date_input(..., value=(d1, d2))) now advertise a range, and a wrong-arity value is rejected instead of silently discarded (#55). They hold a 2-element list but advertised the scalar schema of their single-handle form, so nothing in the model said the widget was a range — and a schema-following agent that sent the scalar it was told to send had its write silently thrown away (slider/select_slider reverted to the prior value; a date range degraded to a one-element range) whileset_widgetreported success. The schema is now range-aware ({"type": "array", "items": …, "minItems": 2, "maxItems": 2}), andset_widgetvalidates arity up front, in both directions — a single value sent to a range widget, and a list sent to a single-handle one, are both rejected with a clear error, leaving the prior value untouched. This closes the silent-revert class (#10/#12/#31/#33) for the arity case: those validators guarded a value's content, leaving its shape unchecked.
Behavior changes (why this is a minor bump, not a patch): range widgets advertise an array
schema instead of a scalar one; form_submit_button moves from unsupported to a supported
button; set_widget now rejects two inputs it previously accepted and then discarded (a
wrong-arity value, and a fractional value on an integer number_input); and an unsupported element
that appears only in a comment or a string literal is no longer reported. Every new rejection
replaces a silent failure — nothing that used to apply cleanly stops applying.
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.