From 0cd95719b6a8bc9a174067d204909f4905eefe6e Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:20:43 -0400 Subject: [PATCH] Fix dev apps form: union types, textarea support, JSON parsing --- src/fastmcp/cli/apps_dev.py | 62 +++++++++++++++++++++++++++++++++---- src/fastmcp/types.py | 32 +++++++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 src/fastmcp/types.py diff --git a/src/fastmcp/cli/apps_dev.py b/src/fastmcp/cli/apps_dev.py index ac15b9c4c..bb17a235f 100644 --- a/src/fastmcp/cli/apps_dev.py +++ b/src/fastmcp/cli/apps_dev.py @@ -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( diff --git a/src/fastmcp/types.py b/src/fastmcp/types.py new file mode 100644 index 000000000..f078ceb6b --- /dev/null +++ b/src/fastmcp/types.py @@ -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"]