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. maxisbey commented on Dec 8, 2025

    @maxisbey
    Contributor

    Note: This issue now consolidates:

    Important implementation detail from #1134: Using inspect.getdoc(fn) instead of fn.__doc__ provides two benefits:

    1. Searches parent methods for docstrings (important when concrete methods inherit from abstract classes)
    2. Automatically cleans docstring indentation

    This should be incorporated as part of the docstring parsing implementation.

    AI Disclaimer

  2. Ilan-kayesar commented on Jan 1, 2026

    @Ilan-kayesar

    I can try working on this

  3. added a commit that references this issue on Jan 30, 2026
    bdffc88
  4. heath-hunnicutt-ruach-tov commented on Feb 2, 2026

    @heath-hunnicutt-ruach-tov

    I have submitted PR #1979 implementing this feature. The approach follows the maintainer guidance to infer docstring format automatically, adapted from pydantic-ai's implementation using griffe. Google, NumPy, and Sphinx formats are supported with auto-detection. Field(description=...) annotations take precedence over docstring descriptions for backward compatibility.

    Apologies for not commenting here before starting — I should have signaled intent first per the contributing guide.

  5. Kludex commented on Feb 2, 2026

    @Kludex
    Member

    Pydantic will create a separate package from the logic we have in PydanticAI so we can use it here.

  6. ameenalkhaldi commented on Mar 15, 2026

    @ameenalkhaldi

    I've opened a PR addressing this: #2295

    It parses Google, NumPy, and Sphinx-style docstrings using griffe (as suggested by @Kludex) to automatically extract parameter descriptions into the generated tool JSON schemas. Explicit Field(description=...) annotations always take priority.

  7. m13v commented on Mar 18, 2026

    @m13v
  8. m13v commented on Mar 18, 2026

    @m13v
  9. added 5 commits that reference this issue on Apr 3, 2026
    8336296
    287c8ca
    25d12a4
    9b99474
    f0946ae
  10. 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
  11. 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.

  12. added 3 commits that reference this issue on Jun 15, 2026
    18fb07c
    06659a4
    9d07720
  13. 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.

  14. 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