Fix dev apps form: union types, textarea support, JSON parsing

This commit is contained in:
Jeremiah Lowin 2026-03-23 14:20:43 -04:00
commit 0cd95719b6
No known key found for this signature in database
2 changed files with 88 additions and 6 deletions

View file

@ -871,6 +871,30 @@ def _model_from_schema(tool_name: str, input_schema: dict[str, Any]) -> type[Any
field_definitions: dict[str, Any] = {}
for prop_name, prop in properties.items():
json_type = prop.get("type", "string")
# Handle anyOf / oneOf (union types like str | dict | None)
for key in ("anyOf", "oneOf"):
if key in prop:
non_null = [
t
for t in prop[key]
if isinstance(t, dict) and t.get("type") != "null"
]
if non_null:
types = [t.get("type") for t in non_null if "type" in t]
for candidate in (
"object",
"array",
"integer",
"number",
"boolean",
"string",
):
if candidate in types:
json_type = candidate
break
break
match json_type:
case "integer":
py_type: type = int
@ -878,6 +902,9 @@ def _model_from_schema(tool_name: str, input_schema: dict[str, Any]) -> type[Any
py_type = float
case "boolean":
py_type = bool
case "object" | "array":
# Render as a string textarea; api_launch parses JSON later
py_type = str
case _:
py_type = str
@ -897,10 +924,20 @@ def _model_from_schema(tool_name: str, input_schema: dict[str, Any]) -> type[Any
from typing import Literal
py_type = Literal[tuple(prop["enum"])] # type: ignore[assignment]
if prop.get("format") == "textarea" or (
isinstance(prop.get("json_schema_extra"), dict)
and prop["json_schema_extra"].get("ui", {}).get("type") == "textarea"
):
# Textarea detection:
# 1. Explicit format: "textarea" in JSON schema
# 2. UI annotation: {"ui": {"type": "textarea"}} (json_schema_extra merged flat)
# 3. Object/array types need multiline JSON editing
use_textarea = (
prop.get("format") == "textarea"
or (
isinstance(prop.get("ui"), dict)
and prop["ui"].get("type") == "textarea"
)
or json_type in ("object", "array")
)
if use_textarea:
extra["json_schema_extra"] = {"ui": {"type": "textarea"}}
field_definitions[prop_name] = (
@ -1219,8 +1256,21 @@ def _make_dev_app(
"""Picker form submits here; returns a /launch URL string for OpenLink."""
data = await request.json()
tool = data.pop("tool", "")
# Remaining keys are tool arguments; pass all including empty optionals
tool_args = dict(data)
# Remaining keys are tool arguments. Form inputs always produce
# strings, but some parameters expect dicts/lists — try to parse
# string values that look like JSON objects or arrays.
tool_args: dict[str, Any] = {}
for k, v in data.items():
if isinstance(v, str):
stripped = v.strip()
if stripped and stripped[0] in ("{", "["):
try:
parsed = json.loads(stripped)
if isinstance(parsed, (dict, list)):
v = parsed
except (json.JSONDecodeError, TypeError):
pass
tool_args[k] = v
args_json = quote(json.dumps(tool_args))
url = f"/launch?tool={tool}&args={args_json}"
return Response(

32
src/fastmcp/types.py Normal file
View file

@ -0,0 +1,32 @@
"""Reusable type annotations for FastMCP tool parameters.
These types can be used in tool function signatures to influence how
parameters are presented in UIs (e.g. ``fastmcp dev apps``) and
serialized in JSON Schema.
Example::
from fastmcp import FastMCP
from fastmcp.types import Textarea
mcp = FastMCP("demo")
@mcp.tool()
def run_query(sql: Textarea) -> str:
...
"""
from __future__ import annotations
from typing import Annotated
from pydantic import Field
Textarea = Annotated[str, Field(json_schema_extra={"format": "textarea"})]
"""A string rendered as a multiline textarea in form-based UIs.
Produces ``"format": "textarea"`` in the JSON Schema, which
``fastmcp dev apps`` picks up automatically.
"""
__all__ = ["Textarea"]