Repository navigation
Description
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:
python-sdk/src/mcp/server/fastmcp/tools/base.py
Lines 55 to 59 in 775f879
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() python-sdk/src/mcp/server/fastmcp/utilities/func_metadata.py
Lines 105 to 174 in 775f879
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
[-]Improving the jsonschema created from the docstring in FastMCP[/-][+]Improving how function docstring gets converted to jsonschema when using FastMCP[/+][-]Improving how function docstring gets converted to jsonschema when using FastMCP[/-][+]Improving how function docstring gets converted to tool's jsonschema when using FastMCP[/+][-]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[/+]rohit-ganguly commentedon Mar 24, 2025
+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 :)
Kludex commentedon Mar 24, 2025
It should infer the docstring format. PR welcome.
Implementation can be copied from pydantic AI.
rohit-ganguly commentedon Mar 24, 2025
Hi @Kludex, thanks for the reply, I'd love to make a PR. Would you prefer me:
kaneg commentedon Sep 7, 2025
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.
14 remaining items
feat: extract parameter descriptions from docstrings into JSON schema
feat: extract parameter descriptions from docstrings into JSON schema
feat: parse Google-style docstrings to populate tool parameter descri…
feat: parse Google-style docstrings to populate tool parameter descri…
Gaurangwad commentedon Jun 15, 2026
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.
Extract tool parameter descriptions from docstrings
Extract tool parameter descriptions from docstrings
Extract tool parameter descriptions from docstrings
Gaurangwad commentedon Jun 15, 2026
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.
feat: propagate docstring parameter descriptions to JSON Schema
feat(anti-pattern): add multi-parser-silent-fallback lesson
feat(lesson): add lesson-22 multi-parser-silent-fallback
1Ash0 commentedon Nov 6, 2026
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).
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.getdochalf (#1134) doesn't need a parser, and it's visible today.Tool.from_functionsendsfn.__doc__as-is. On Python 3.11 and 3.12, both supported, the description clients receive keeps the source indentation:So the same server sends different tool text depending on the interpreter.
inspect.getdocfixes 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.