streamlit-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streamlit_mcp-0.1.0/.gitattributes +5 -0
- streamlit_mcp-0.1.0/.github/workflows/release.yml +27 -0
- streamlit_mcp-0.1.0/.github/workflows/test.yml +22 -0
- streamlit_mcp-0.1.0/.gitignore +16 -0
- streamlit_mcp-0.1.0/CHANGELOG.md +29 -0
- streamlit_mcp-0.1.0/LICENSE +21 -0
- streamlit_mcp-0.1.0/PKG-INFO +103 -0
- streamlit_mcp-0.1.0/README.md +57 -0
- streamlit_mcp-0.1.0/pyproject.toml +45 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/__init__.py +29 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/cli.py +180 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/decorator.py +70 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/elements.py +133 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/engine.py +102 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/guardrails.py +67 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/runtime.py +209 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/server.py +159 -0
- streamlit_mcp-0.1.0/src/streamlit_mcp/session.py +39 -0
- streamlit_mcp-0.1.0/tests/apps/sample_app.py +24 -0
- streamlit_mcp-0.1.0/tests/test_cli.py +82 -0
- streamlit_mcp-0.1.0/tests/test_decorator.py +91 -0
- streamlit_mcp-0.1.0/tests/test_elements.py +104 -0
- streamlit_mcp-0.1.0/tests/test_engine.py +88 -0
- streamlit_mcp-0.1.0/tests/test_guardrails.py +86 -0
- streamlit_mcp-0.1.0/tests/test_release_fixes.py +84 -0
- streamlit_mcp-0.1.0/tests/test_runtime.py +106 -0
- streamlit_mcp-0.1.0/tests/test_server.py +61 -0
- streamlit_mcp-0.1.0/tests/test_session.py +54 -0
- streamlit_mcp-0.1.0/uv.lock +2712 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Tag a release (e.g. `git tag v0.1.0 && git push --tags`) to build and publish to PyPI.
|
|
4
|
+
# Publishing uses PyPI Trusted Publishing (OIDC) — no API token stored in the repo.
|
|
5
|
+
# One-time setup on pypi.org: add a Trusted Publisher for project `streamlit-mcp` with
|
|
6
|
+
# owner `dkedar7`, repository `streamlit-mcp`, workflow `release.yml`, environment `pypi`.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write # required for Trusted Publishing (OIDC)
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v5
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: uv run --with pytest pytest -q
|
|
24
|
+
- name: Build
|
|
25
|
+
run: uv build
|
|
26
|
+
- name: Publish to PyPI (Trusted Publishing)
|
|
27
|
+
run: uv publish
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Run tests
|
|
22
|
+
run: uv run --with pytest pytest -q
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
First release. Serve an existing Streamlit app as an MCP server, driven headlessly via
|
|
6
|
+
`streamlit.testing.v1.AppTest` — no browser automation.
|
|
7
|
+
|
|
8
|
+
- Auto-introspect all ten v1 widget kinds (text_input, number_input, text_area, slider,
|
|
9
|
+
selectbox, multiselect, checkbox, radio, button, date_input) into MCP tools.
|
|
10
|
+
- Core MCP tools: `list_widgets`, `get_layout`, `set_widget`, `click`, `read_output`,
|
|
11
|
+
`get_state`. Unsupported elements are reported explicitly.
|
|
12
|
+
- Transports: stdio and HTTP/SSE (see Known issues for HTTP auth status).
|
|
13
|
+
- Human-first CLI (`serve`/`inspect`/`call`) with parity to the MCP tools.
|
|
14
|
+
- `@mcp_tool` decorator for opt-in semantic tools.
|
|
15
|
+
- Guardrails: read-only mode and widget allow-list (enforced on both CLI and MCP).
|
|
16
|
+
|
|
17
|
+
### Known issues / immediate follow-ups
|
|
18
|
+
- **HTTP bearer auth is not yet enforced on the transport.** The token primitive
|
|
19
|
+
(`Guardrails.require_bearer`) is implemented and tested, but is not yet bound to the
|
|
20
|
+
FastMCP HTTP/SSE request path. As a safeguard, `serve` refuses to start an HTTP/SSE
|
|
21
|
+
server on a non-loopback host. Wiring `FastMCP(auth=...)` with a token verifier is the
|
|
22
|
+
top follow-up before networked HTTP is supported.
|
|
23
|
+
- **Sessions are not yet disposed.** Per-client isolation works, but there is no
|
|
24
|
+
session-close hook, so long-running HTTP servers accumulate runtimes. Single-client and
|
|
25
|
+
stdio use are unaffected.
|
|
26
|
+
- **No concurrency locking.** Concurrent requests sharing one session are not serialized;
|
|
27
|
+
AppTest is not known to be re-entrant. Use one in-flight request per session for now.
|
|
28
|
+
- Output capture covers headings/markdown/caption/text; `st.write`/`st.error`/etc. are a
|
|
29
|
+
planned coverage expansion.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kedar Dabhadkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: streamlit-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Serve an existing Streamlit app as an MCP server — agents drive it natively, no browser.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dkedar7/streamlit-mcp
|
|
6
|
+
Project-URL: Source, https://github.com/dkedar7/streamlit-mcp
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/dkedar7/streamlit-mcp/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/dkedar7/streamlit-mcp/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Kedar Dabhadkar
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Kedar Dabhadkar
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: agent,automation,llm,mcp,model-context-protocol,streamlit
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Requires-Dist: fastmcp<4,>=3
|
|
42
|
+
Requires-Dist: streamlit>=1.58
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# streamlit-mcp
|
|
48
|
+
|
|
49
|
+
**Serve any Streamlit app as an MCP server.** Agents introspect your app's widgets,
|
|
50
|
+
set values, click buttons, and read the rendered output and `session_state` — natively,
|
|
51
|
+
over MCP, with **no browser automation**.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv tool install . # or: pip install .
|
|
55
|
+
|
|
56
|
+
# serve an app over MCP (stdio for local clients, or HTTP/SSE for networked agents)
|
|
57
|
+
streamlit-mcp serve app.py
|
|
58
|
+
streamlit-mcp serve app.py --transport http --host 127.0.0.1 --port 8000
|
|
59
|
+
|
|
60
|
+
# drive it yourself from the terminal (same engine the agent uses)
|
|
61
|
+
streamlit-mcp inspect app.py
|
|
62
|
+
streamlit-mcp call app.py --set "Name=agent" --click "Save" --read
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Streamlit has no callback graph — it reruns the whole script per interaction — so
|
|
66
|
+
streamlit-mcp drives the app headlessly through Streamlit's own test runtime
|
|
67
|
+
(`streamlit.testing.v1.AppTest`) and returns the **semantic element tree**, not pixels.
|
|
68
|
+
Gradio and Dash already shipped native app-as-MCP; this fills the Streamlit gap.
|
|
69
|
+
|
|
70
|
+
## Tools exposed to agents
|
|
71
|
+
|
|
72
|
+
| Tool | What it does |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `list_widgets` / `get_layout` | introspect widgets (kind, label, value, constraints) |
|
|
75
|
+
| `set_widget(identifier, value)` | set a widget and rerun |
|
|
76
|
+
| `click(identifier)` | click a button and rerun |
|
|
77
|
+
| `read_output()` | the rendered element tree, agent-readable |
|
|
78
|
+
| `get_state()` | the app's `session_state` |
|
|
79
|
+
|
|
80
|
+
Supported widgets (v1): text_input, number_input, text_area, slider, selectbox,
|
|
81
|
+
multiselect, checkbox, radio, button, date_input. Unsupported elements (file_uploader,
|
|
82
|
+
custom components, `st.chat`, fragments) are reported explicitly, never silently dropped.
|
|
83
|
+
|
|
84
|
+
## Human ↔ agent parity
|
|
85
|
+
|
|
86
|
+
Everything an agent can do over MCP, a human can do via the CLI — both call the same
|
|
87
|
+
engine. The read-only mode and widget allow-list guardrails apply identically to both
|
|
88
|
+
surfaces.
|
|
89
|
+
|
|
90
|
+
## Security / trust model
|
|
91
|
+
|
|
92
|
+
- **`app_path` is executed as trusted code** in the server process (that's how AppTest
|
|
93
|
+
runs it). Only serve apps you trust.
|
|
94
|
+
- **`get_state` / `read_output` expose the app's `session_state`** to the caller — do not
|
|
95
|
+
put secrets there.
|
|
96
|
+
- **HTTP/SSE is loopback-only in v1.** A bearer-token primitive exists, but it is **not yet
|
|
97
|
+
enforced** on the transport, so `serve` refuses to bind HTTP/SSE to a non-loopback host.
|
|
98
|
+
Use stdio for local clients, or HTTP/SSE on `127.0.0.1`. Enforced networked auth is the
|
|
99
|
+
top follow-up (see `CHANGELOG.md`).
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# streamlit-mcp
|
|
2
|
+
|
|
3
|
+
**Serve any Streamlit app as an MCP server.** Agents introspect your app's widgets,
|
|
4
|
+
set values, click buttons, and read the rendered output and `session_state` — natively,
|
|
5
|
+
over MCP, with **no browser automation**.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install . # or: pip install .
|
|
9
|
+
|
|
10
|
+
# serve an app over MCP (stdio for local clients, or HTTP/SSE for networked agents)
|
|
11
|
+
streamlit-mcp serve app.py
|
|
12
|
+
streamlit-mcp serve app.py --transport http --host 127.0.0.1 --port 8000
|
|
13
|
+
|
|
14
|
+
# drive it yourself from the terminal (same engine the agent uses)
|
|
15
|
+
streamlit-mcp inspect app.py
|
|
16
|
+
streamlit-mcp call app.py --set "Name=agent" --click "Save" --read
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Streamlit has no callback graph — it reruns the whole script per interaction — so
|
|
20
|
+
streamlit-mcp drives the app headlessly through Streamlit's own test runtime
|
|
21
|
+
(`streamlit.testing.v1.AppTest`) and returns the **semantic element tree**, not pixels.
|
|
22
|
+
Gradio and Dash already shipped native app-as-MCP; this fills the Streamlit gap.
|
|
23
|
+
|
|
24
|
+
## Tools exposed to agents
|
|
25
|
+
|
|
26
|
+
| Tool | What it does |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `list_widgets` / `get_layout` | introspect widgets (kind, label, value, constraints) |
|
|
29
|
+
| `set_widget(identifier, value)` | set a widget and rerun |
|
|
30
|
+
| `click(identifier)` | click a button and rerun |
|
|
31
|
+
| `read_output()` | the rendered element tree, agent-readable |
|
|
32
|
+
| `get_state()` | the app's `session_state` |
|
|
33
|
+
|
|
34
|
+
Supported widgets (v1): text_input, number_input, text_area, slider, selectbox,
|
|
35
|
+
multiselect, checkbox, radio, button, date_input. Unsupported elements (file_uploader,
|
|
36
|
+
custom components, `st.chat`, fragments) are reported explicitly, never silently dropped.
|
|
37
|
+
|
|
38
|
+
## Human ↔ agent parity
|
|
39
|
+
|
|
40
|
+
Everything an agent can do over MCP, a human can do via the CLI — both call the same
|
|
41
|
+
engine. The read-only mode and widget allow-list guardrails apply identically to both
|
|
42
|
+
surfaces.
|
|
43
|
+
|
|
44
|
+
## Security / trust model
|
|
45
|
+
|
|
46
|
+
- **`app_path` is executed as trusted code** in the server process (that's how AppTest
|
|
47
|
+
runs it). Only serve apps you trust.
|
|
48
|
+
- **`get_state` / `read_output` expose the app's `session_state`** to the caller — do not
|
|
49
|
+
put secrets there.
|
|
50
|
+
- **HTTP/SSE is loopback-only in v1.** A bearer-token primitive exists, but it is **not yet
|
|
51
|
+
enforced** on the transport, so `serve` refuses to bind HTTP/SSE to a non-loopback host.
|
|
52
|
+
Use stdio for local clients, or HTTP/SSE on `127.0.0.1`. Enforced networked auth is the
|
|
53
|
+
top follow-up (see `CHANGELOG.md`).
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "streamlit-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Serve an existing Streamlit app as an MCP server — agents drive it natively, no browser."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [{ name = "Kedar Dabhadkar" }]
|
|
9
|
+
keywords = ["streamlit", "mcp", "model-context-protocol", "agent", "llm", "automation"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Software Development :: Libraries",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"streamlit>=1.58",
|
|
21
|
+
"fastmcp>=3,<4",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/dkedar7/streamlit-mcp"
|
|
26
|
+
Source = "https://github.com/dkedar7/streamlit-mcp"
|
|
27
|
+
"Bug Tracker" = "https://github.com/dkedar7/streamlit-mcp/issues"
|
|
28
|
+
Changelog = "https://github.com/dkedar7/streamlit-mcp/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
streamlit-mcp = "streamlit_mcp.cli:_cli"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["pytest>=7"]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/streamlit_mcp"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
pythonpath = ["src"]
|
|
45
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""streamlit-mcp — serve a Streamlit app as an MCP server, no browser.
|
|
2
|
+
|
|
3
|
+
The app is driven headlessly through Streamlit's AppTest runtime (behind a Runtime
|
|
4
|
+
interface); widgets auto-map to MCP tools, and a human-first CLI exercises the same
|
|
5
|
+
engine an agent uses (parity).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
# Public API
|
|
11
|
+
from .decorator import mcp_tool # noqa: E402
|
|
12
|
+
from .engine import Engine, PermissionDenied # noqa: E402
|
|
13
|
+
from .guardrails import Guardrails # noqa: E402
|
|
14
|
+
from .runtime import AppTestRuntime, Runtime, RuntimeError_, WidgetNotFound # noqa: E402
|
|
15
|
+
from .server import build_server, serve # noqa: E402
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"mcp_tool",
|
|
20
|
+
"Engine",
|
|
21
|
+
"PermissionDenied",
|
|
22
|
+
"Guardrails",
|
|
23
|
+
"AppTestRuntime",
|
|
24
|
+
"Runtime",
|
|
25
|
+
"RuntimeError_",
|
|
26
|
+
"WidgetNotFound",
|
|
27
|
+
"build_server",
|
|
28
|
+
"serve",
|
|
29
|
+
]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""streamlit-mcp CLI — the human-first surface.
|
|
2
|
+
|
|
3
|
+
`serve` launches the MCP server; `inspect`/`call` let a human drive the app from the
|
|
4
|
+
terminal. All three go through the same engine an agent uses over MCP, so parity (R2)
|
|
5
|
+
holds by construction. Guardrail flags apply identically to the CLI and the server.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from .engine import Engine
|
|
16
|
+
from .guardrails import Guardrails
|
|
17
|
+
from .runtime import AppTestRuntime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _force_utf8_output() -> None:
|
|
21
|
+
for stream in (sys.stdout, sys.stderr):
|
|
22
|
+
try:
|
|
23
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_guardrails(args: argparse.Namespace) -> Optional[Guardrails]:
|
|
29
|
+
read_only = getattr(args, "read_only", False)
|
|
30
|
+
allow = getattr(args, "allow", None)
|
|
31
|
+
bearer = getattr(args, "bearer_token", None)
|
|
32
|
+
if not read_only and not allow and not bearer:
|
|
33
|
+
return None
|
|
34
|
+
return Guardrails(
|
|
35
|
+
read_only=read_only,
|
|
36
|
+
allow_list=set(allow) if allow else None,
|
|
37
|
+
bearer_token=bearer,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _engine(args: argparse.Namespace) -> Engine:
|
|
42
|
+
rt = AppTestRuntime(args.app)
|
|
43
|
+
rt.run()
|
|
44
|
+
return Engine(rt, guard=build_guardrails(args), app_path=args.app)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_value(raw: str) -> Any:
|
|
48
|
+
"""Parse a --set value: JSON when possible (true/41/["a"]), else a plain string."""
|
|
49
|
+
try:
|
|
50
|
+
return json.loads(raw)
|
|
51
|
+
except (json.JSONDecodeError, ValueError):
|
|
52
|
+
return raw
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _split_assignment(item: str) -> tuple[str, Any]:
|
|
56
|
+
if "=" not in item:
|
|
57
|
+
raise ValueError(f"--set expects 'identifier=value', got {item!r}")
|
|
58
|
+
identifier, raw = item.split("=", 1)
|
|
59
|
+
return identifier.strip(), _parse_value(raw)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# --------------------------------------------------------------------- commands
|
|
63
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
64
|
+
from .server import serve
|
|
65
|
+
try:
|
|
66
|
+
serve(
|
|
67
|
+
args.app,
|
|
68
|
+
transport=args.transport,
|
|
69
|
+
host=args.host,
|
|
70
|
+
port=args.port,
|
|
71
|
+
guard=build_guardrails(args),
|
|
72
|
+
)
|
|
73
|
+
except ValueError as e: # fail-closed / bad transport -> clean message, not a traceback
|
|
74
|
+
print(str(e), file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cmd_inspect(args: argparse.Namespace) -> int:
|
|
80
|
+
eng = _engine(args)
|
|
81
|
+
out = eng.get_layout() if args.layout else eng.list_widgets()
|
|
82
|
+
if args.json:
|
|
83
|
+
print(json.dumps(out, indent=2, default=str))
|
|
84
|
+
return 0
|
|
85
|
+
for w in out["widgets"]:
|
|
86
|
+
flag = " [action]" if w.get("action") else ""
|
|
87
|
+
print(f" {w['kind']:<13} {w['identifier']:<14} = {w['value']!r}{flag}")
|
|
88
|
+
if args.layout:
|
|
89
|
+
for o in out.get("outputs", []):
|
|
90
|
+
print(f" [{o['kind']}] {o['text']}")
|
|
91
|
+
state = out.get("session_state") or {}
|
|
92
|
+
if state:
|
|
93
|
+
print(" session_state:")
|
|
94
|
+
for k, v in state.items():
|
|
95
|
+
print(f" {k} = {v!r}")
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cmd_call(args: argparse.Namespace) -> int:
|
|
100
|
+
try:
|
|
101
|
+
eng = _engine(args)
|
|
102
|
+
for item in args.set or []:
|
|
103
|
+
ident, value = _split_assignment(item)
|
|
104
|
+
eng.set_widget(ident, value)
|
|
105
|
+
for ident in args.click or []:
|
|
106
|
+
eng.click(ident)
|
|
107
|
+
result = eng.get_state() if args.state else eng.read_output()
|
|
108
|
+
except Exception as e: # CLI: surface any failure as a clean message + exit 1
|
|
109
|
+
print(str(e), file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
if args.json:
|
|
112
|
+
print(json.dumps(result, indent=2, default=str))
|
|
113
|
+
return 0
|
|
114
|
+
if args.state:
|
|
115
|
+
for k, v in result.items():
|
|
116
|
+
print(f" {k} = {v!r}")
|
|
117
|
+
else:
|
|
118
|
+
for o in result.get("outputs", []):
|
|
119
|
+
print(f" [{o['kind']}] {o['text']}")
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# --------------------------------------------------------------------- parser
|
|
124
|
+
def _add_guard_flags(p: argparse.ArgumentParser, *, bearer: bool) -> None:
|
|
125
|
+
p.add_argument("--read-only", dest="read_only", action="store_true",
|
|
126
|
+
help="block state-changing actions")
|
|
127
|
+
p.add_argument("--allow", action="append",
|
|
128
|
+
help="allow-list a widget identifier (repeatable)")
|
|
129
|
+
if bearer:
|
|
130
|
+
p.add_argument("--bearer-token", dest="bearer_token",
|
|
131
|
+
help="require this bearer token on HTTP/SSE")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
135
|
+
parser = argparse.ArgumentParser(prog="streamlit-mcp",
|
|
136
|
+
description="Serve a Streamlit app as an MCP server.")
|
|
137
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
138
|
+
|
|
139
|
+
p_serve = sub.add_parser("serve", help="serve an app over MCP")
|
|
140
|
+
p_serve.add_argument("app")
|
|
141
|
+
p_serve.add_argument("--transport", choices=["stdio", "http", "sse"], default="stdio")
|
|
142
|
+
p_serve.add_argument("--host", default="127.0.0.1")
|
|
143
|
+
p_serve.add_argument("--port", type=int, default=8000)
|
|
144
|
+
_add_guard_flags(p_serve, bearer=True)
|
|
145
|
+
p_serve.set_defaults(func=cmd_serve)
|
|
146
|
+
|
|
147
|
+
p_inspect = sub.add_parser("inspect", help="print the app's widgets")
|
|
148
|
+
p_inspect.add_argument("app")
|
|
149
|
+
p_inspect.add_argument("--layout", action="store_true", help="full layout (outputs, state, unsupported)")
|
|
150
|
+
p_inspect.add_argument("--json", action="store_true")
|
|
151
|
+
_add_guard_flags(p_inspect, bearer=False)
|
|
152
|
+
p_inspect.set_defaults(func=cmd_inspect)
|
|
153
|
+
|
|
154
|
+
p_call = sub.add_parser("call", help="drive the app (same engine agents use)")
|
|
155
|
+
p_call.add_argument("app")
|
|
156
|
+
p_call.add_argument("--set", action="append", help="identifier=value (repeatable)")
|
|
157
|
+
p_call.add_argument("--click", action="append", help="button identifier (repeatable)")
|
|
158
|
+
p_call.add_argument("--read", action="store_true", help="print rendered output (default)")
|
|
159
|
+
p_call.add_argument("--state", action="store_true", help="print session_state instead")
|
|
160
|
+
p_call.add_argument("--json", action="store_true")
|
|
161
|
+
_add_guard_flags(p_call, bearer=False)
|
|
162
|
+
p_call.set_defaults(func=cmd_call)
|
|
163
|
+
|
|
164
|
+
return parser
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
168
|
+
_force_utf8_output()
|
|
169
|
+
args = build_parser().parse_args(argv)
|
|
170
|
+
return args.func(args)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _cli() -> None:
|
|
174
|
+
"""Console-script entry point. Propagates the exit code — entry-point shims call the
|
|
175
|
+
target and ignore its return value, so returning from main() would always exit 0."""
|
|
176
|
+
raise SystemExit(main())
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
_cli()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Opt-in semantic tools: expose a developer-chosen function as a clean MCP tool.
|
|
2
|
+
|
|
3
|
+
The auto path (widget tools) needs no app changes. When a developer wants higher-level,
|
|
4
|
+
named actions, they decorate a function with ``@mcp_tool`` and it is registered alongside
|
|
5
|
+
the auto-generated widget tools (origin R8). Names must not collide with the core tools or
|
|
6
|
+
with each other (AE5 reporting).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
|
|
14
|
+
# Mirror of server.TOOL_NAMES, kept here to avoid a server<->decorator import cycle.
|
|
15
|
+
RESERVED_NAMES = (
|
|
16
|
+
"list_widgets",
|
|
17
|
+
"get_layout",
|
|
18
|
+
"set_widget",
|
|
19
|
+
"click",
|
|
20
|
+
"read_output",
|
|
21
|
+
"get_state",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class SemanticToolSpec:
|
|
27
|
+
name: str
|
|
28
|
+
description: str
|
|
29
|
+
func: Callable
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_REGISTRY: dict[str, SemanticToolSpec] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def mcp_tool(func: Optional[Callable] = None, *, name: Optional[str] = None,
|
|
36
|
+
description: Optional[str] = None):
|
|
37
|
+
"""Register ``func`` as a semantic MCP tool. Usable bare or with arguments::
|
|
38
|
+
|
|
39
|
+
@mcp_tool
|
|
40
|
+
def reset(): ...
|
|
41
|
+
|
|
42
|
+
@mcp_tool(name="reset_all", description="Reset everything")
|
|
43
|
+
def reset(): ...
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def register(fn: Callable) -> Callable:
|
|
47
|
+
tool_name = name or fn.__name__
|
|
48
|
+
if tool_name in RESERVED_NAMES:
|
|
49
|
+
raise ValueError(f"semantic tool name {tool_name!r} collides with a core tool")
|
|
50
|
+
if tool_name in _REGISTRY:
|
|
51
|
+
raise ValueError(f"semantic tool name {tool_name!r} is already registered")
|
|
52
|
+
_REGISTRY[tool_name] = SemanticToolSpec(
|
|
53
|
+
name=tool_name,
|
|
54
|
+
description=description or (fn.__doc__ or "").strip(),
|
|
55
|
+
func=fn,
|
|
56
|
+
)
|
|
57
|
+
return fn
|
|
58
|
+
|
|
59
|
+
if func is not None: # used as @mcp_tool
|
|
60
|
+
return register(func)
|
|
61
|
+
return register # used as @mcp_tool(...)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def registered_semantic_tools() -> list[SemanticToolSpec]:
|
|
65
|
+
return list(_REGISTRY.values())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def clear_registry() -> None:
|
|
69
|
+
"""Reset the registry (test isolation)."""
|
|
70
|
+
_REGISTRY.clear()
|