Skip to content

Improving how function docstring gets converted to tool's jsonschema for FastMCP #226

Open

Description

@salman1993

Is your feature request related to a problem? Please describe.
Tool descriptions are not parsed as expected from the function docstring when using FastMCP. This affects tool calling performance.

Currently, FastMCP does some function inspection to create the docstring here:

  • func_arg_metadata = func_metadata(
    fn,
    skip_names=[context_kwarg] if context_kwarg is not None else [],
    )
    parameters = func_arg_metadata.arg_model.model_json_schema()
  • def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadata:
    """Given a function, return metadata including a pydantic model representing its
    signature.
    The use case for this is
    ```
    meta = func_to_pyd(func)
    validated_args = meta.arg_model.model_validate(some_raw_data_dict)
    return func(**validated_args.model_dump_one_level())
    ```
    **critically** it also provides pre-parse helper to attempt to parse things from
    JSON.
    Args:
    func: The function to convert to a pydantic model
    skip_names: A list of parameter names to skip. These will not be included in
    the model.
    Returns:
    A pydantic model representing the function's signature.
    """
    sig = _get_typed_signature(func)
    params = sig.parameters
    dynamic_pydantic_model_params: dict[str, Any] = {}
    globalns = getattr(func, "__globals__", {})
    for param in params.values():
    if param.name.startswith("_"):
    raise InvalidSignature(
    f"Parameter {param.name} of {func.__name__} cannot start with '_'"
    )
    if param.name in skip_names:
    continue
    annotation = param.annotation
    # `x: None` / `x: None = None`
    if annotation is None:
    annotation = Annotated[
    None,
    Field(
    default=param.default
    if param.default is not inspect.Parameter.empty
    else PydanticUndefined
    ),
    ]
    # Untyped field
    if annotation is inspect.Parameter.empty:
    annotation = Annotated[
    Any,
    Field(),
    # 🤷
    WithJsonSchema({"title": param.name, "type": "string"}),
    ]
    field_info = FieldInfo.from_annotated_attribute(
    _get_typed_annotation(annotation, globalns),
    param.default
    if param.default is not inspect.Parameter.empty
    else PydanticUndefined,
    )
    dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
    continue
    arguments_model = create_model(
    f"{func.__name__}Arguments",
    **dynamic_pydantic_model_params,
    __base__=ArgModelBase,
    )
    resp = FuncMetadata(arg_model=arguments_model)
    return resp

From my understanding, it creates a FuncMetadata model in pydantic which then gets converted to jsonschema.

Current behaviour:
If we have a tool such as:

def add_numbers(a: float, b: float) -> float:
    """
    Adds two numbers and returns the result.

    Args:
        a (float): The first number.
        b (float): The second number.

    Returns:
        float: The sum of a and b.
    """
    return a + b

it gets parsed into:

>>> func_arg_metadata = func_metadata(add_numbers)
>>> parameters = func_arg_metadata.arg_model.model_json_schema()
>>> parameters
{'properties': {'a': {'title': 'A', 'type': 'number'}, 'b': {'title': 'B', 'type': 'number'}}, 'required': ['a', 'b'], 'title': 'add_numbersArguments', 'type': 'object'}

>>> add_numbers.__doc__  
'\nAdds two numbers and returns the result.\n\nArgs:\n    a (float): The first number.\n    b (float): The second number.\n\nReturns:\n    float: The sum of a and b.\n'

Describe the solution you'd like
It'd be nicer to follow one of the python docstring styles and parse out the argument descriptions from the docstring.

{
  "name": "add_numbers",
  "description": "Adds two numbers and returns the sum.",
  "parameters": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number",
        "description": "The first number to add."
      },
      "b": {
        "type": "number",
        "description": "The second number to add."
      }
    },
    "required": ["a", "b"]
  }
}

Describe alternatives you've considered
we used to do this in a previous python version of goose: https://github.com/block/goose/blob/eccb1b22614f39b751db4e5efd73d728d9ca40fc/packages/exchange/src/exchange/utils.py#L82-L107

here are some test examples: https://github.com/block/goose/blob/eccb1b22614f39b751db4e5efd73d728d9ca40fc/packages/exchange/tests/test_utils.py#L32-L136

Additional context
I am happy to add this in - wanted to post this first to check that you're okay with enforcing a docstring style ("google", "numpy", "sphinx") & adding griffe as a dependency.

Activity

  1. changed the title [-]Improving the jsonschema created from the docstring in FastMCP[/-] [+]Improving how function docstring gets converted to jsonschema when using FastMCP[/+] on Feb 21, 2025
  2. changed the title [-]Improving how function docstring gets converted to jsonschema when using FastMCP[/-] [+]Improving how function docstring gets converted to tool's jsonschema when using FastMCP[/+] on Feb 21, 2025
  3. changed the title [-]Improving how function docstring gets converted to tool's jsonschema when using FastMCP[/-] [+]Improving how function docstring gets converted to tool's jsonschema for FastMCP[/+] on Feb 21, 2025
  4. rohit-ganguly commented on Mar 24, 2025

    @rohit-ganguly

    +1, ran into this issue while making client/server just now. I'm happy to provide a PR updating our examples to use properly formatted docstrings if this gets merged.

    Oh, and I'm team Sphinx :)

  5. Kludex commented on Mar 24, 2025

    @Kludex
    Member

    It should infer the docstring format. PR welcome.

    Implementation can be copied from pydantic AI.

  6. rohit-ganguly commented on Mar 24, 2025

    @rohit-ganguly

    Hi @Kludex, thanks for the reply, I'd love to make a PR. Would you prefer me:

    1. Update func_metadata to fit the JSONSchema for tool calling?
    2. Make a utility function to convert the existing func_metadata model into to the tool calling JSONSchema?
    3. Another approach?
  7. kaneg commented on Sep 7, 2025

    @kaneg

    Have there been any updates on this topic? Although we can add the parameter's description in the tools' description, it would be better to give AIs the accurate hint for each parameter.

  8. added
    enhancementRequest for a new feature that's not currently supported
    ready for workEnough information for someone to start working on
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Oct 6, 2025
  9. 14 remaining items

  10. added 2 commits that reference this issue on Apr 3, 2026
    9b99474
    f0946ae
  11. added
    needs decisionIssue is actionable, needs maintainer decision on whether to implement
    and removed
    ready for workEnough information for someone to start working on
    on Apr 17, 2026
  12. Gaurangwad commented on Jun 15, 2026

    @Gaurangwad

    I'd like to take this. Plan: parse Google/NumPy/Sphinx docstrings via griffe (which is already in the dependency tree) and populate each parameter's description in the generated tool schema. An explicit Annotated[T, Field(description=...)] would still take precedence, and functions without docstrings are unaffected.
    One open question from the original issue, are you okay adding griffe as a runtime dependency, or would you prefer a dependency-free parser? Happy to go either way. I have a branch with tests ready to open as a PR.

  13. added 3 commits that reference this issue on Jun 15, 2026
    18fb07c
    06659a4
    9d07720
  14. Gaurangwad commented on Jun 15, 2026

    @Gaurangwad

    I've opened #2876 to implement this. Approach: parse Google/NumPy/Sphinx docstrings via griffe (already in the dependency tree) and populate each parameter's description in the generated tool schema. An explicit Annotated[T, Field(description=...)] still takes precedence, and functions without docstrings are unaffected. Tests cover all three styles plus edge cases, and the change is at 100% coverage.

    One open question from this issue: I added griffe as a runtime dependency. Happy to switch to a dependency-free parser if maintainers would prefer to avoid that...let me know.

  15. 1Ash0 commented on Nov 6, 2026

    @1Ash0

    I understand parsing is on hold until Pydantic's standalone package exists. Two data points for when it's decided:

    1. Does parsing change which tool a model picks? Not in my test.
    I wrote 18 look-alike tools, each with the same docs in Google, NumPy, reST and plain style, plus a "parsed" version (parameter docs moved into the schema). I sent 36 requests × 2 tool orders to gpt-oss-20b (temperature 0).

    version right tool chosen
    any of the 4 raw styles (main today) 69/72 each
    parsed 69/72
    summary line only, no parameter docs 65/72

    It never chose a wrong tool. Every miss was the model asking a clarifying question instead of calling a tool. Selection is at ceiling here, so this shows no benefit and doesn't rule one out for weaker models or bigger servers. Argument quality wasn't measured.

    2. The inspect.getdoc half (#1134) doesn't need a parser, and it's visible today.
    Tool.from_function sends fn.__doc__ as-is. On Python 3.11 and 3.12, both supported, the description clients receive keeps the source indentation:

    3.12: 'Add one.\n\n    Args:\n        x: The number.\n    '
    3.13: 'Add one.\n\nArgs:\n    x: The number.\n'
    

    So the same server sends different tool text depending on the interpreter. inspect.getdoc fixes that and the inherited-docstring case without a new dependency.

    Scripts, every request/response, and the analysis: https://github.com/1Ash0/mcp-226-docstring-eval. Prepared with AI assistance; I ran and checked everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featureenhancementRequest for a new feature that's not currently supportedgood first issueGood for newcomersneeds decisionIssue is actionable, needs maintainer decision on whether to implement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions