Skip to content

Usage

Use it with an MCP client

Claude Desktop / Cursor — add to your MCP config (claude_desktop_config.json or .cursor/mcp.json):

{
  "mcpServers": {
    "streamlit-mcp": {
      "command": "uvx",
      "args": ["streamlit-mcp", "serve", "/absolute/path/to/your/app.py"]
    }
  }
}

Claude Code:

claude mcp add streamlit-mcp -- uvx streamlit-mcp serve /absolute/path/to/your/app.py

uvx runs the published package with no prior install. stdio (the default) is the right transport for local clients.

Tools exposed to agents

Tool What it does
list_widgets / get_layout introspect widgets (kind, label, value, constraints)
set_widget(identifier, value) set a widget and rerun
click(identifier) click a button and rerun
read_output() the rendered element tree, agent-readable
get_state() the app's session_state

Widgets and outputs are returned in document/render order (the order the app renders them, recursing through the sidebar and columns), so the layout an agent reads back matches the app.

Supported widgets: text_input, number_input, text_area, slider, select_slider, selectbox, multiselect, checkbox, toggle, radio, button, date_input, time_input, color_picker. Input widgets streamlit-mcp can't drive (file_uploader, camera_input, chat_input, pills, segmented_control, feedback, …) are reported explicitly on every surface (text --layout, --json, and MCP get_layout), never silently dropped.

Atomic writes

Setting a selectbox/radio/select_slider/multiselect to an option that isn't offered (including every handle of a two-handle select_slider range), a number_input/slider/date_input outside its min/max (including either end of a two-date range), or a color_picker to anything but a #RGB/#RRGGBB hex string, is rejected before any state changes — with a clear error listing the valid choices/range/format. An unparseable number, date, or time value is likewise rejected with a clear message. A failed set_widget leaves the session usable and never silently mutates state.

Custom semantic tools

Beyond the per-widget tools, expose a higher-level named action by decorating a function with @mcp_tool in your app file:

import streamlit as st
from streamlit_mcp import mcp_tool

@mcp_tool
def reset_all():
    """Reset everything to defaults."""
    return {"ok": True}

st.text_input("Name")

streamlit-mcp serve app.py loads the app and exposes reset_all over MCP alongside the widget tools. The decorated function is called directly (it isn't a widget), so keep it self-contained.

The CLI reaches it too (human ↔ agent parity): inspect lists registered tools, and call --tool invokes one:

streamlit-mcp inspect app.py                 # ...lists reset_all under "tools:"
streamlit-mcp call app.py --tool reset_all   # invoke it; --arg key=value passes arguments

Drive it from the terminal

The CLI calls the same engine an agent uses over MCP, so behavior matches exactly:

streamlit-mcp inspect app.py                 # print the widgets
streamlit-mcp inspect app.py --layout --json # full layout (widgets, outputs, state, unsupported)
streamlit-mcp call app.py --set "Name=agent" --click "Save" --read
streamlit-mcp call app.py --read-only --set "Name=x"   # guardrail: blocked, exit 1
streamlit-mcp call app.py --allow "Name" --set "Age=5"  # allow-list: 'Age' blocked, exit 1
streamlit-mcp --version

A --set value is JSON-parsed for typed widgets (so Age=5 is a number, Tags=["a","b"] a list, Agree=true a boolean), but for a text_input/text_area it's stored literally — so Comment=true is the string "true", not the boolean — matching what the same value stores over MCP.