Skip to content

feat: parse Google-style docstrings to populate tool parameter descriptions#2402

Open
Maanik23 wants to merge 2 commits intomodelcontextprotocol:mainfrom
Maanik23:feat/docstring-param-descriptions
Open

feat: parse Google-style docstrings to populate tool parameter descriptions#2402
Maanik23 wants to merge 2 commits intomodelcontextprotocol:mainfrom
Maanik23:feat/docstring-param-descriptions

Conversation

@Maanik23
Copy link
Copy Markdown

@Maanik23 Maanik23 commented Apr 8, 2026

Github-Issue:#226

Summary

FastMCP previously discarded the parameter descriptions in a function's docstring when generating the JSON schema for a tool. Tools showed up to LLMs with bare titles and types but no per-parameter context, which made tool calling less reliable.

This PR adds a small Google-style docstring parser (no new dependencies) that extracts the summary and per-parameter descriptions, then wires them into both the tool description and the generated JSON schema.

Before

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

    Args:
        a: The first number to add.
        b: The second number to add.
    """
    return a + b

Generated schema:

{
  "properties": {
    "a": {"title": "A", "type": "number"},
    "b": {"title": "B", "type": "number"}
  }
}

The tool description was the entire raw docstring including the Args: block.

After

{
  "properties": {
    "a": {"title": "A", "type": "number", "description": "The first number to add."},
    "b": {"title": "B", "type": "number", "description": "The second number to add."}
  }
}

And the tool description is just the summary: "Adds two numbers and returns the result."

Changes

src/mcp/server/mcpserver/utilities/docstring.py (new file)

  • parse_docstring(docstring) -> tuple[str, dict[str, str]] returns a (summary, param_descriptions) tuple
  • Hand-written parser, no new dependencies
  • Handles multi-line summaries, parameters with or without type annotations, complex annotations like Annotated[list[int], Field(min_length=1)], multi-line continuation of parameter descriptions, all common Args header aliases, and early termination at Returns/Raises/Examples sections

src/mcp/server/mcpserver/utilities/func_metadata.py

  • Calls parse_docstring(func.__doc__) once per function
  • Passes the matching description (if any) to Field(description=...) when building the dynamic Pydantic model

src/mcp/server/mcpserver/tools/base.py

  • Uses the parsed summary as the tool description when no explicit description= is provided
  • Falls back to the raw docstring if the parser cannot find a summary
  • An explicit description= argument to Tool.from_function() still wins, preserving backward compatibility

Tests

  • tests/server/mcpserver/test_docstring.py (new) — 16 unit tests covering: None/empty/whitespace input, summary-only docstrings, multi-line summaries, Google-style with and without type annotations, multi-line parameter descriptions, all Args header aliases (Args, Arguments, Parameters), section termination by Raises/Returns, blank lines inside the Args block, complex Annotated[...] type expressions with nested brackets, and unrecognized continuation lines
  • tests/server/mcpserver/test_func_metadata.py — added two integration tests verifying that descriptions land in the JSON schema and that functions without an Args section still work
  • tests/server/mcpserver/test_tool_manager.py — added an end-to-end test through ToolManager that confirms both the tool description and parameter descriptions come from the docstring

Compatibility

  • Default behaviour is preserved: existing tools without docstrings, with summary-only docstrings, or with non-Google docstring styles continue to work exactly as before
  • An explicit description= argument to Tool.from_function() still takes precedence over the parsed summary
  • No new runtime dependencies
  • Only the Google docstring style is supported. NumPy and Sphinx styles fall back to summary-only behaviour, which is still an improvement over the previous "use the entire raw docstring" behaviour

Why Google style only

The issue suggested supporting google, numpy, and sphinx styles via griffe. Adding griffe is a non-trivial dependency for a feature that, in practice, is overwhelmingly used with Google-style docstrings (the default in FastAPI, Pydantic, LangChain, and most modern Python projects). A self-contained parser keeps the dependency footprint at zero and is easy to extend later if there is real demand for the other two styles.

Maanik23 added 2 commits April 8, 2026 09:40
…ptions

FastMCP previously discarded the parameter descriptions in a function's
docstring when generating the JSON schema for a tool. The schema only
contained titles and types, which gives LLMs less context when deciding
how to call the tool.

This change adds a small Google-style docstring parser (no new
dependencies) that extracts:

- the leading summary line, used as the tool description in place of the
  full raw docstring
- per-parameter descriptions from the Args/Arguments/Parameters section,
  which are passed to Pydantic Field(description=...) so they appear in
  the generated JSON schema

The parser handles:

- multi-line summaries collapsed into one paragraph
- parameters with or without type annotations
- complex annotations like Annotated[list[int], Field(min_length=1)]
- multi-line continuation of a parameter description
- Args/Arguments/Parameters section header aliases
- early termination when a Returns/Raises/Examples section appears
- empty/None docstrings

Existing behaviour is preserved: an explicit description= argument to
Tool.from_function() still wins over the parsed summary, and tools
without an Args section keep working without any description fields on
their parameters.

Github-Issue:modelcontextprotocol#226
The MCP repo enforces 100% test coverage. Add two tests that exercise
the previously uncovered defensive branches in _parse_param_line:

- test_param_line_with_unclosed_parenthesis covers the end_idx == -1
  fallback when a type annotation has no closing paren
- test_param_line_starting_with_non_word covers the regex no-match
  fallback for lines that do not begin with a word character
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant