diff --git a/fast_dash/Components.py b/fast_dash/Components.py index edb260d..c4cb39a 100644 --- a/fast_dash/Components.py +++ b/fast_dash/Components.py @@ -1343,6 +1343,9 @@ def _get_component_from_input(hint, default_value=None): # Resolve typing generics (Literal, Enum, Optional, Annotated, list[str], etc.) resolved = _resolve_typing_hint(hint, default_value) if resolved.component is not None: + resolved.component.tag = {"Literal": "Select", "Enum": "Select"}.get( + resolved.component.tag, resolved.component.tag + ) return resolved.component hint = resolved.base_type @@ -1396,7 +1399,7 @@ def _get_component_from_input(hint, default_value=None): elif _default_value_type == "Sequence": component = Fastify( - dmc.Select(data=default_value), "value", tag=_hint_type + dmc.Select(data=default_value), "value", tag="Select" ) elif _default_value_type == "Dictionary": @@ -1428,12 +1431,12 @@ def _get_component_from_input(hint, default_value=None): component = Fastify( dmc.NumberInput(value=hint(default_value)), "value", - tag=_hint_type, + tag="NumberInput", ) elif _default_value_type == "Numeric": component = Fastify( - dmc.NumberInput(value=default_value), "value", tag=_hint_type + dmc.NumberInput(value=default_value), "value", tag="NumberInput" ) elif _default_value_type == "Sequence": @@ -1445,7 +1448,7 @@ def _get_component_from_input(hint, default_value=None): ) component = Fastify( - dmc.Slider(min=start, max=stop, step=step), "value", tag=_hint_type + dmc.Slider(min=start, max=stop, step=step), "value", tag="Slider" ) else: @@ -1462,11 +1465,11 @@ def _get_component_from_input(hint, default_value=None): tooltip={"placement": "bottom", "always_visible": True}, ), "value", - tag=_hint_type, + tag="Slider", ) else: - component = Fastify(dmc.NumberInput(), "value", tag=_hint_type) + component = Fastify(dmc.NumberInput(), "value", tag="NumberInput") elif _hint_type == "Sequence": if _default_value_type == "Text": @@ -1519,11 +1522,11 @@ def _get_component_from_input(hint, default_value=None): # old dbc.Checkbox rendered with the Bootswatch accent (off-theme). if _default_value_type == "Boolean": component = Fastify( - dmc.Checkbox(checked=default_value), "checked", tag=_hint_type + dmc.Checkbox(checked=default_value), "checked", tag="Switch" ) else: - component = Fastify(dmc.Checkbox(), "checked", tag=_hint_type) + component = Fastify(dmc.Checkbox(), "checked", tag="Switch") elif _hint_type == "Image": if _default_value_type == "Image": @@ -1579,7 +1582,7 @@ def _get_component_from_input(hint, default_value=None): style={"width": "100%"}, ), "date", - tag=_hint_type, + tag="DateInput", ) else: @@ -1590,7 +1593,7 @@ def _get_component_from_input(hint, default_value=None): style={"width": "100%"}, ), "date", - tag=_hint_type, + tag="DateInput", ) else: diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 3cf6b7e..c7c46a6 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -17,7 +17,12 @@ import importlib.util import json import warnings -from typing import Annotated, Optional, Tuple # module-level for get_type_hints (#119, #132) +from typing import ( # module-level for get_type_hints (#119, #132) + Annotated, + Literal, + Optional, + Tuple, +) import pandas as pd # noqa: F401 (module-level for any -> pd.DataFrame hints) import plotly.graph_objects as go @@ -657,6 +662,37 @@ def style(color: str = "#1c7ed6", assert by_id["bio"]["tag"] == "TextArea" assert by_id["name"]["tag"] == "Text" + def test_static_input_tags_match_component_types(self): + # #158: expose component types, not internal hint names such as Numeric + # or Boolean, so agents can cross-reference list_component_types(). + def form( + plain: str = "hi", + choice: str = ["a", "b"], + literal: Literal["x", "y"] = "x", + flavor: Flavor = Flavor.VANILLA, + count: int = 1, + ratio: float = 0.5, + enabled: bool = True, + day: datetime.date = datetime.date(2024, 1, 1), + ) -> str: + return plain + + c = _client_for(FastDash(callback_fn=form, mcp_server=True)) + legal = set(_call(c, "list_component_types")["types"]) + by_id = {i["id"]: i for i in _call(c, "describe_app")["inputs"]} + + assert {name: item["tag"] for name, item in by_id.items()} == { + "plain": "Text", + "choice": "Select", + "literal": "Select", + "flavor": "Select", + "count": "NumberInput", + "ratio": "NumberInput", + "enabled": "Switch", + "day": "DateInput", + } + assert {item["tag"] for item in by_id.values()} <= legal + def test_password_value_goes_in_but_never_comes_back(self): # #151: the browser masks a PasswordInput; the unauthenticated /mcp route # must not undo that by reading the credential back out in plain text.