Files
mcp-use__mcp-use/docs/generate_docs.py
T
pietrozullo ad42b4bb3e docs: noindex auto-generated Python API reference instead of searchable flag
The searchable: false approach on the API Reference tab was inert on
visible tabs. Switch to per-page noindex: true frontmatter, which keeps
pages in the sidebar but excludes them from search indexing (search
engines, internal docs search, and AI context) so generic queries like
"CLI" surface curated guides instead of the generated reference.

Applied to all 73 existing reference pages and emitted by the generator
on every regeneration.

Closes MCP-2398
2026-06-29 10:23:42 +02:00

1844 lines
65 KiB
Python

#!/usr/bin/env python3
"""
Script to generate Mintlify-compatible MDX API documentation files and update docs.json.
Organizes files by path structure and excludes __init__.py files.
"""
import ast
import inspect
import json
import os
import sys
from pathlib import Path
from typing import Any, Union
def detect_deprecated_items(file_path: str) -> list[str]:
"""Detect deprecated classes and functions in a Python file.
Args:
file_path: Path to the Python file to analyze
Returns:
List of deprecated class/function names
"""
deprecated_items = []
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
tree = ast.parse(content)
# Check for @deprecated decorators
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == "deprecated":
deprecated_items.append(node.name)
elif (
isinstance(decorator, ast.Call)
and isinstance(decorator.func, ast.Name)
and decorator.func.id == "deprecated"
):
deprecated_items.append(node.name)
elif isinstance(node, ast.FunctionDef):
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == "deprecated":
deprecated_items.append(node.name)
elif (
isinstance(decorator, ast.Call)
and isinstance(decorator.func, ast.Name)
and decorator.func.id == "deprecated"
):
deprecated_items.append(node.name)
except (FileNotFoundError, SyntaxError, UnicodeDecodeError) as e:
print(f"Warning: Could not analyze {file_path}: {e}")
return deprecated_items
def detect_deprecated_imports(file_path: str) -> list[dict[str, str]]:
"""Detect deprecated import paths in a Python file by analyzing @deprecated decorators.
Args:
file_path: Path to the Python file to analyze
Returns:
List of dictionaries containing deprecated import information
"""
deprecated_imports = []
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
tree = ast.parse(content)
# Extract deprecated patterns from @deprecated decorators
deprecated_patterns = {}
for node in ast.walk(tree):
if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
for decorator in node.decorator_list:
if (
isinstance(decorator, ast.Call)
and isinstance(decorator.func, ast.Name)
and decorator.func.id == "deprecated"
):
# Extract the deprecation message
if decorator.args and isinstance(
decorator.args[0], ast.Constant
):
deprecation_msg = decorator.args[0].value
if isinstance(
deprecation_msg, str
) and deprecation_msg.startswith("Use "):
# Extract the new path from "Use mcp_use.new.path"
new_path = deprecation_msg[4:] # Remove "Use "
# Determine the old path based on current file location
old_path = _get_current_module_path(file_path)
if old_path and new_path != old_path:
deprecated_patterns[old_path] = new_path
# Now check for imports that match these patterns
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name in deprecated_patterns:
deprecated_imports.append(
{
"type": "import",
"line": node.lineno,
"old_path": alias.name,
"new_path": deprecated_patterns[alias.name],
"alias": alias.asname,
}
)
elif isinstance(node, ast.ImportFrom):
if node.module and node.module in deprecated_patterns:
deprecated_imports.append(
{
"type": "from_import",
"line": node.lineno,
"old_path": node.module,
"new_path": deprecated_patterns[node.module],
"names": [alias.name for alias in node.names],
}
)
except (FileNotFoundError, SyntaxError, UnicodeDecodeError) as e:
print(f"Warning: Could not analyze {file_path}: {e}")
return deprecated_imports
def _get_current_module_path(file_path: str) -> str:
"""Extract the module path from a file path."""
# Convert file path to module path
# e.g., "../libraries/python/mcp_use/session.py" -> "mcp_use.session"
path_parts = file_path.replace("\\", "/").split("/")
# Find the mcp_use directory
try:
mcp_use_index = path_parts.index("mcp_use")
module_parts = path_parts[mcp_use_index:]
# Remove .py extension and join with dots
if module_parts[-1].endswith(".py"):
module_parts[-1] = module_parts[-1][:-3]
return ".".join(module_parts)
except ValueError:
return None
def generate_deprecation_warning(
deprecated_imports: list[dict[str, str]], file_path: str
) -> str:
"""Generate a deprecation warning callout for deprecated imports.
Args:
deprecated_imports: List of deprecated import information
file_path: Path to the file being analyzed
Returns:
MDX formatted deprecation warning
"""
if not deprecated_imports:
return ""
warning_lines = [
"<Warning>",
f"**File:** `{file_path}`",
"",
"The following import paths are deprecated and should be updated:",
"",
]
for dep_import in deprecated_imports:
if dep_import["type"] == "import":
old_line = f"import {dep_import['old_path']}"
if dep_import["alias"]:
old_line += f" as {dep_import['alias']}"
new_line = f"import {dep_import['new_path']}"
if dep_import["alias"]:
new_line += f" as {dep_import['alias']}"
warning_lines.extend(
[
f"**Line {dep_import['line']}:**",
"```python",
"# Deprecated:",
f"{old_line}",
"# Use instead:",
f"{new_line}",
"```",
"",
]
)
elif dep_import["type"] == "from_import":
names_str = ", ".join(dep_import["names"])
old_line = f"from {dep_import['old_path']} import {names_str}"
new_line = f"from {dep_import['new_path']} import {names_str}"
warning_lines.extend(
[
f"**Line {dep_import['line']}:**",
"```python",
"# Deprecated:",
f"{old_line}",
"# Use instead:",
f"{new_line}",
"```",
"",
]
)
warning_lines.append("</Warning>")
warning_lines.append("")
return "\n".join(warning_lines)
def get_docstring(obj: Any) -> str:
"""Extract docstring from an object."""
if hasattr(obj, "__doc__") and obj.__doc__:
return obj.__doc__.strip()
return ""
def format_type_annotation(annotation: Any) -> str:
"""Format type annotation for display."""
if annotation is None or annotation is type(None):
return "None"
# Handle GenericAlias (Python 3.9+)
if hasattr(annotation, "__origin__") and hasattr(annotation, "__args__"):
origin = annotation.__origin__
args = annotation.__args__
if origin is Union:
if args and args[-1] is type(None):
# Optional type
non_none_args = [arg for arg in args if arg is not type(None)]
if len(non_none_args) == 1:
return f"{format_type_annotation(non_none_args[0])} | None"
else:
return (
" | ".join(format_type_annotation(arg) for arg in non_none_args)
+ " | None"
)
else:
return " | ".join(format_type_annotation(arg) for arg in args)
elif origin is list:
if args:
return f"list[{format_type_annotation(args[0])}]"
return "list"
elif origin is dict:
if len(args) >= 2:
return f"dict[{format_type_annotation(args[0])}, {format_type_annotation(args[1])}]"
return "dict"
elif origin is tuple:
if args:
return (
f"tuple[{', '.join(format_type_annotation(arg) for arg in args)}]"
)
return "tuple"
elif origin is set:
if args:
return f"set[{format_type_annotation(args[0])}]"
return "set"
# Handle regular types - show full import path
if hasattr(annotation, "__module__") and hasattr(annotation, "__name__"):
module = annotation.__module__
name = annotation.__name__
# Skip built-in types and common stdlib types
if module in ("builtins", "typing", "collections.abc") or module.startswith(
"typing"
):
return name
# For external modules, show the full path
return f"{module}.{name}"
elif hasattr(annotation, "__name__"):
return annotation.__name__
elif isinstance(annotation, str):
return annotation
return str(annotation)
def process_docstring(docstring: str) -> str:
"""Process docstring to convert code blocks, escape problematic characters, and remove Args section."""
if not docstring:
return ""
lines = docstring.split("\n")
processed_lines = []
in_code_block = False
in_args_section = False
skip_line = False
for _, line in enumerate(lines):
stripped = line.strip()
# Skip if we're in a line that should be skipped
if skip_line:
skip_line = False
continue
# Handle Sphinx-style code blocks
if stripped.startswith(".. code-block::"):
in_code_block = True
processed_lines.append("```python wrap")
continue
elif (
in_code_block
and stripped
and not line.startswith(" ")
and not line.startswith("\t")
):
# End of code block
in_code_block = False
processed_lines.append("```")
processed_lines.append("")
processed_lines.append(line)
continue
elif in_code_block:
# Inside code block - escape curly braces in comments
if "#" in line:
line = line.replace("{", "\\{").replace("}", "\\}")
processed_lines.append(line)
else:
# Check for Args:, Parameters:, Returns:, Warns:, Raises: sections
if stripped.lower().startswith(
(
"args:",
"parameters:",
"returns:",
"return:",
"warns:",
"warn:",
"raises:",
"raise:",
)
):
in_args_section = True
skip_line = True # Skip the section header line itself
continue
# Check for end of section
elif in_args_section and stripped.lower().startswith(
("yields:", "note:", "example:", "usage:")
):
in_args_section = False
processed_lines.append(line)
# Skip lines in args section
elif in_args_section:
# Check if this line starts a new section (not indented)
if (
stripped
and not line.startswith(" ")
and not line.startswith("\t")
and ":" in stripped
):
in_args_section = False
processed_lines.append(line)
# Otherwise skip this line (it's part of args section)
continue
else:
# Outside code block and args section - escape curly braces
line = line.replace("{", "\\{").replace("}", "\\}")
processed_lines.append(line)
# Close any open code block
if in_code_block:
processed_lines.append("```")
return "\n".join(processed_lines)
def format_signature(func: Any) -> str:
"""Format function signature for display."""
try:
sig = inspect.signature(func)
params = []
for name, param in sig.parameters.items():
if name == "self":
continue
param_str = name
if param.annotation != inspect.Signature.empty:
param_str += f": {format_type_annotation(param.annotation)}"
if param.default != inspect.Signature.empty:
if isinstance(param.default, str):
param_str += f' = "{param.default}"'
else:
param_str += f" = {param.default}"
params.append(param_str)
return f"({', '.join(params)})"
except Exception:
return "()"
def extract_param_docs(docstring: str) -> dict[str, str]:
"""Extract parameter descriptions from docstring."""
param_docs = {}
if not docstring:
return param_docs
lines = docstring.split("\n")
current_param = None
current_desc = []
in_args_section = False
for line in lines:
stripped = line.strip()
# Sphinx style: :param name: description
if stripped.startswith(":param "):
if current_param and current_desc:
param_docs[current_param] = " ".join(current_desc).strip()
parts = stripped[7:].split(":", 1)
if len(parts) == 2:
current_param = parts[0].strip()
current_desc = [parts[1].strip()] if parts[1].strip() else []
else:
current_param = parts[0].strip()
current_desc = []
# Google style: Args: or Parameters:
elif stripped.lower().startswith(("args:", "parameters:")):
in_args_section = True
current_param = None
current_desc = []
# Google style: name (type): description
elif in_args_section and ":" in stripped and not stripped.startswith(" "):
if current_param and current_desc:
param_docs[current_param] = " ".join(current_desc).strip()
parts = stripped.split(":", 1)
if len(parts) == 2:
param_part = parts[0].strip()
if "(" in param_part and ")" in param_part:
param_name = param_part.split("(")[0].strip()
current_param = param_name
current_desc = [parts[1].strip()] if parts[1].strip() else []
else:
# Simple parameter name without type
current_param = param_part
current_desc = [parts[1].strip()] if parts[1].strip() else []
# Continuation of current parameter description
elif current_param and (
stripped.startswith(" ") or stripped == "" or stripped.startswith("-")
):
if stripped:
current_desc.append(stripped)
# End of args section
elif in_args_section and stripped.lower().startswith(
("returns:", "raises:", "yields:", "note:", "example:")
):
if current_param and current_desc:
param_docs[current_param] = " ".join(current_desc).strip()
in_args_section = False
current_param = None
current_desc = []
else:
if current_param and current_desc:
param_docs[current_param] = " ".join(current_desc).strip()
current_param = None
current_desc = []
# Add the last parameter
if current_param and current_desc:
param_docs[current_param] = " ".join(current_desc).strip()
return param_docs
def generate_param_field(name: str, param: inspect.Parameter, description: str) -> str:
"""Generate ParamField component for a parameter."""
param_type = ""
default_value = ""
if param.annotation != inspect.Signature.empty:
param_type = format_type_annotation(param.annotation)
if param.default != inspect.Signature.empty:
if isinstance(param.default, str):
# Use single quotes for string defaults to avoid JSX parsing issues
default_value = f"'{param.default}'"
else:
# Always quote non-string defaults to avoid JSX parsing issues
default_value = f'"{str(param.default)}"'
field_parts = [f'<ParamField body="{name}"']
if param_type:
field_parts.append(f'type="{param_type}"')
if default_value:
field_parts.append(f"default={default_value}")
else:
# Add required="True" for parameters without default values
field_parts.append('required="True"')
field_parts.append(">")
if description and description.strip():
field_parts.append(f" {description.strip()}")
else:
# Provide more helpful default descriptions based on parameter name
default_desc = get_default_param_description(name, param_type)
field_parts.append(f" {default_desc}")
field_parts.append("</ParamField>")
return " ".join(field_parts)
def get_default_param_description(name: str, param_type: str) -> str:
"""Generate a helpful default description for a parameter."""
name_lower = name.lower()
# Common parameter patterns
if "config" in name_lower:
return "Configuration object or file path"
elif "session" in name_lower:
return "MCP session instance"
elif "client" in name_lower:
return "MCP client instance"
elif "server" in name_lower:
return "Server name or configuration"
elif "query" in name_lower:
return "Query string or input"
elif "callback" in name_lower:
return "Callback function"
elif "middleware" in name_lower:
return "Middleware instance"
elif "connector" in name_lower:
return "Connector instance"
elif "tools" in name_lower:
return "List of tools"
elif "name" in name_lower:
return "Name identifier"
elif "path" in name_lower or "filepath" in name_lower:
return "File path"
elif "url" in name_lower:
return "URL string"
elif "timeout" in name_lower:
return "Timeout duration"
elif "retries" in name_lower:
return "Number of retry attempts"
elif "debug" in name_lower or "verbose" in name_lower:
return "Enable debug/verbose mode"
elif "async" in name_lower:
return "Enable asynchronous mode"
elif "auto" in name_lower:
return "Enable automatic behavior"
elif param_type and "bool" in param_type.lower():
return "Boolean flag"
elif param_type and "list" in param_type.lower():
return "List of items"
elif param_type and "dict" in param_type.lower():
return "Dictionary of key-value pairs"
elif param_type and "str" in param_type.lower():
return "String value"
elif param_type and "int" in param_type.lower():
return "Integer value"
else:
return "Parameter value"
def extract_return_docs(docstring: str) -> str:
"""Extract return value description from docstring."""
if not docstring:
return ""
lines = docstring.split("\n")
in_returns_section = False
return_desc = []
for line in lines:
stripped = line.strip()
# Sphinx style: :returns: or :return:
if stripped.startswith((":returns:", ":return:")):
return_desc.append(stripped.split(":", 1)[1].strip())
in_returns_section = True
# Google style: Returns: or Return: (handle indented)
elif stripped.lower().endswith(("returns:", "return:")):
# Extract the description part after the colon
desc_part = stripped.split(":", 1)[1].strip()
if desc_part:
return_desc.append(desc_part)
in_returns_section = True
# Continuation of return description
elif in_returns_section and (
line.startswith(" ") or stripped == "" or stripped.startswith("-")
):
# Check if this line starts a new section (like "Example:")
if stripped and stripped.lower().endswith(":"):
break
if stripped:
return_desc.append(stripped)
# End of returns section
elif in_returns_section and stripped.lower().startswith(
(
"raises:",
"yields:",
"note:",
"example:",
"args:",
"parameters:",
"warns:",
"warn:",
)
):
break
return " ".join(return_desc).strip()
def extract_warns_docs(docstring: str) -> list[str]:
"""Extract warns documentation from docstring."""
if not docstring:
return []
lines = docstring.split("\n")
in_warns_section = False
warn_desc = []
for line in lines:
stripped = line.strip()
# Sphinx style: :warns: or :warn:
if stripped.startswith((":warns:", ":warn:")):
warn_desc.append(stripped.split(":", 1)[1].strip())
in_warns_section = True
# Google style: Warns: or Warn:
elif stripped.lower().startswith(("warns:", "warn:")):
warn_desc.append(stripped.split(":", 1)[1].strip())
in_warns_section = True
# Continuation of warn description
elif in_warns_section and (
stripped.startswith(" ") or stripped == "" or stripped.startswith("-")
):
if stripped:
warn_desc.append(stripped)
# End of warns section
elif in_warns_section and stripped.lower().startswith(
(
"raises:",
"yields:",
"note:",
"example:",
"args:",
"parameters:",
"returns:",
"return:",
)
):
break
return warn_desc
def extract_raises_docs(docstring: str) -> list[str]:
"""Extract raises documentation from docstring."""
if not docstring:
return []
lines = docstring.split("\n")
in_raises_section = False
raise_desc = []
for line in lines:
stripped = line.strip()
# Sphinx style: :raises: or :raise:
if stripped.startswith((":raises:", ":raise:")):
raise_desc.append(stripped.split(":", 1)[1].strip())
in_raises_section = True
# Google style: Raises: or Raise:
elif stripped.lower().startswith(("raises:", "raise:")):
raise_desc.append(stripped.split(":", 1)[1].strip())
in_raises_section = True
# Continuation of raise description
elif in_raises_section and (
stripped.startswith(" ") or stripped == "" or stripped.startswith("-")
):
if stripped:
raise_desc.append(stripped)
# End of raises section
elif in_raises_section and stripped.lower().startswith(
(
"warns:",
"warn:",
"yields:",
"note:",
"example:",
"args:",
"parameters:",
"returns:",
"return:",
)
):
break
return raise_desc
def generate_module_description(module_name: str, module_docstring: str) -> str:
"""Generate a better description for a module."""
if module_docstring and module_docstring.strip():
# Use the first sentence of the docstring
first_sentence = module_docstring.strip().split(".")[0].strip()
if first_sentence:
return f"{first_sentence} API Documentation"
# Fallback to generic description
return f"{module_name.split('.')[-1].replace('_', ' ').title()} API Documentation"
def generate_response_field(name: str, field_type: str, description: str = "") -> str:
"""Generate ResponseField component with enhanced formatting."""
field_parts = [f'<ResponseField name="{name}"']
if field_type and field_type.strip():
field_parts.append(f'type="{field_type}"')
if description:
# Escape quotes in description
escaped_desc = description.replace('"', "&quot;")
field_parts.append(f">{escaped_desc}</ResponseField>")
else:
field_parts.append("/>")
return " ".join(field_parts)
def is_defined_in_module(obj: Any, module_name: str) -> bool:
"""Check if an object is defined in the specified module."""
if hasattr(obj, "__module__"):
return obj.__module__ == module_name
return False
def generate_class_docs(cls: type, module_name: str) -> str:
"""Generate documentation for a class."""
docs = []
# Class header
class_name = cls.__name__
docstring = get_docstring(cls)
# Class section header for sidebar
docs.append(f"## {class_name}")
docs.append("")
# Class Card with gradient background
docs.append("<div>")
docs.append(
'<RandomGradientBackground className="rounded-lg p-4 w-full h-full rounded-full">'
)
docs.append('<div className="text-black">')
docs.append(
f'<div className="text-black font-bold text-xl mb-2 mt-8">'
f'<code className="!text-black">class</code> {class_name}</div>'
)
docs.append("")
if docstring:
docs.append(process_docstring(docstring))
docs.append("")
docs.append("</div>")
docs.append("</RandomGradientBackground>")
# Add import example below gradient
docs.append("```python")
docs.append(f"from {module_name} import {class_name}")
docs.append("```")
docs.append("")
# Class attributes/fields - focus on type annotations for Pydantic models
class_attributes = []
# Check for annotations that are likely user-defined fields
if hasattr(cls, "__annotations__"):
for name, annotation in cls.__annotations__.items():
# Skip private attributes and Pydantic internal fields
if (
not name.startswith("_")
and not name.startswith("model_")
and name
not in ["computed_fields", "config", "extra", "fields", "fields_set"]
):
class_attributes.append((name, annotation))
if class_attributes:
docs.append('<Card type="info">')
docs.append("**Attributes**")
docs.append(">")
for name, annotation in class_attributes:
# Create a mock parameter object for the annotation
from inspect import Parameter
param = Parameter(
name, Parameter.POSITIONAL_OR_KEYWORD, annotation=annotation
)
param_field = generate_param_field(name, param, "")
docs.append(param_field)
docs.append("")
docs.append("</Card>")
docs.append("")
# Constructor
if hasattr(cls, "__init__"):
init_method = cls.__init__
if init_method != object.__init__:
# Constructor Card
docs.append('<Card type="info">')
docs.append("### `method` __init__")
docs.append("")
try:
init_docstring = get_docstring(init_method)
sig = inspect.signature(init_method)
param_docs = extract_param_docs(get_docstring(init_method))
params_to_show = [
(name, param)
for name, param in sig.parameters.items()
if name != "self"
]
# Add description outside callout
if init_docstring:
docs.append(process_docstring(init_docstring))
docs.append("")
# Add parameters in callout
if params_to_show:
docs.append("**Parameters**")
for name, param in params_to_show:
param_doc = param_docs.get(name, "")
param_field = generate_param_field(name, param, param_doc)
docs.append(f">{param_field}")
docs.append("")
# Constructor signature
docs.append("**Signature**")
docs.append("```python wrap")
sig_str = format_signature(init_method)
docs.append(f"def __init__{sig_str}:")
docs.append("```")
docs.append("")
docs.append("</Card>")
except Exception:
docs.append("Error generating parameters")
docs.append("")
docs.append("</Card>")
docs.append("")
# Methods - only include methods defined in this module (including async methods and properties)
for name, method in inspect.getmembers(
cls,
predicate=lambda x: inspect.isfunction(x)
or inspect.iscoroutinefunction(x)
or isinstance(x, property),
):
# For properties, check if the getter function is defined in this module
is_property_in_module = False
if isinstance(method, property):
if hasattr(method, "fget") and method.fget:
is_property_in_module = is_defined_in_module(method.fget, module_name)
else:
is_property_in_module = is_defined_in_module(method, module_name)
# Skip inherited methods/properties - only show those defined in this class
is_inherited = False
if isinstance(method, property):
if hasattr(method, "fget") and method.fget:
# Check if this property is defined in a parent class
for base_cls in cls.__bases__:
if hasattr(base_cls, name) and isinstance(
getattr(base_cls, name), property
):
is_inherited = True
break
else:
# Check if this method is defined in a parent class
for base_cls in cls.__bases__:
if hasattr(base_cls, name) and inspect.isfunction(
getattr(base_cls, name)
):
is_inherited = True
break
if (
(not name.startswith("_") or name in ["__call__", "__enter__", "__exit__"])
and is_property_in_module
and not is_inherited
):
# Method/Property Card
docs.append('<Card type="info">')
if isinstance(method, property):
docs.append(f"### `property` {name}")
else:
docs.append(f"### `method` {name}")
docs.append("")
method_docstring = get_docstring(method)
# Handle properties differently from regular methods
if isinstance(method, property):
# Add description outside callout
if method_docstring:
docs.append(process_docstring(method_docstring))
docs.append("")
else:
# Regular method handling
sig = inspect.signature(method)
param_docs = extract_param_docs(get_docstring(method))
params_to_show = [
(name_param, param)
for name_param, param in sig.parameters.items()
if name_param != "self"
]
# Add description outside callout
if method_docstring:
docs.append(process_docstring(method_docstring))
docs.append("")
docs.append("")
# Handle properties differently from regular methods
if isinstance(method, property):
# Add return type for properties
if hasattr(method, "fget") and method.fget:
sig = inspect.signature(method.fget)
return_annotation = sig.return_annotation
if return_annotation != inspect.Signature.empty:
docs.append("**Returns**")
docs.append(
f'><ResponseField name="returns" type="{format_type_annotation(return_annotation)}" />'
)
docs.append("")
# Property signature
docs.append("**Signature**")
docs.append("```python wrap")
docs.append(f"def {name}():")
docs.append("```")
docs.append("")
else:
# Add parameters in callout
if params_to_show:
docs.append("**Parameters**")
for name_param, param in params_to_show:
param_doc = param_docs.get(name_param, "")
param_field = generate_param_field(name_param, param, param_doc)
docs.append(f">{param_field}")
docs.append("")
# Generate return type and signature
try:
# Return type (skip for properties as they're handled separately)
if (
not isinstance(method, property)
and sig.return_annotation != inspect.Signature.empty
):
return_type = format_type_annotation(sig.return_annotation)
return_desc = extract_return_docs(get_docstring(method))
# Only show Returns section if there's a meaningful return type (not None)
if return_type not in ["None", "NoneType"]:
docs.append("**Returns**")
# Use ResponseField for better formatting
response_field = generate_response_field(
"returns", return_type, return_desc
)
docs.append(f">{response_field}")
docs.append("")
# Method signature (only for methods, not properties)
if not isinstance(method, property):
docs.append("**Signature**")
docs.append("```python wrap")
sig_str = format_signature(method)
# Split long signatures across multiple lines
signature_line = f"def {name}{sig_str}:"
if len(signature_line) > 100:
# Split parameters for better readability
docs.append(f"def {name}(")
if sig_str != "()":
params = sig_str[1:-1].split(", ")
for i, param in enumerate(params):
prefix = " " if i > 0 else ""
suffix = "," if i < len(params) - 1 else ""
docs.append(f"{prefix}{param}{suffix}")
docs.append("):")
else:
docs.append(signature_line)
docs.append("```")
docs.append("")
except Exception:
docs.append("Error generating parameters")
docs.append("")
docs.append("</Card>")
docs.append("")
# Close the main class Card
docs.append("</div>")
return "\n".join(docs)
def generate_function_docs(func: Any, module_name: str) -> str:
"""Generate documentation for a function."""
docs = []
func_name = func.__name__
docstring = get_docstring(func)
# Function section header for sidebar
docs.append("")
docs.append(f"## {func_name}")
# Function Card
docs.append('<Card type="info">')
docs.append(f"### `function` {func_name}")
docs.append("")
# Function description
if docstring:
docs.append(process_docstring(docstring))
docs.append("")
# Add import example before parameters
docs.append("```python")
docs.append(f"from {module_name} import {func_name}")
docs.append("```")
docs.append("")
# Parameters section
sig = inspect.signature(func)
param_docs = extract_param_docs(get_docstring(func))
if sig.parameters:
docs.append("**Parameters**")
for name, param in sig.parameters.items():
param_doc = param_docs.get(name, "")
param_field = generate_param_field(name, param, param_doc)
docs.append(f">{param_field}")
docs.append("")
# Returns section
if sig.return_annotation != inspect.Signature.empty:
return_type = format_type_annotation(sig.return_annotation)
return_desc = extract_return_docs(get_docstring(func))
# Only show Returns section if there's a meaningful return type (not None)
if return_type not in ["None", "NoneType"]:
docs.append("**Returns**")
response_field = generate_response_field(
"returns", return_type, return_desc
)
docs.append(f">{response_field}")
docs.append("")
# Warns section
warns_docs = extract_warns_docs(get_docstring(func))
if warns_docs:
docs.append("**Warns**")
for warn in warns_docs:
docs.append(f"><Warning>{warn}</Warning>")
docs.append("")
# Raises section
raises_docs = extract_raises_docs(get_docstring(func))
if raises_docs:
docs.append("**Raises**")
for raise_desc in raises_docs:
docs.append(f"><Danger>{raise_desc}</Danger>")
docs.append("")
# Signature section
docs.append("**Signature**")
docs.append("```python wrap")
try:
sig_str = format_signature(func)
# Split long signatures across multiple lines
signature_line = f"def {func_name}{sig_str}:"
if len(signature_line) > 100:
# Split parameters for better readability
docs.append(f"def {func_name}(")
if sig_str != "()":
params = sig_str[1:-1].split(", ")
for i, param in enumerate(params):
prefix = " " if i > 0 else ""
suffix = "," if i < len(params) - 1 else ""
docs.append(f"{prefix}{param}{suffix}")
docs.append("):")
else:
docs.append(signature_line)
except Exception:
docs.append(f"def {func_name}():")
docs.append("```")
docs.append("")
docs.append("</Card>")
return "\n".join(docs)
def generate_module_docs(module_name: str, output_dir: str) -> None:
"""Generate MDX documentation for a module."""
try:
module = __import__(module_name, fromlist=[""])
except ImportError as e:
print(f"Error importing module {module_name}: {e}")
return
# Detect deprecated items in the module
module_path = module_name.replace(".", "/")
source_file_path = f"../libraries/python/{module_path}.py"
deprecated_items = detect_deprecated_items(source_file_path)
# Get all members of the module
members = inspect.getmembers(module)
# Filter classes and functions (same logic as used later)
classes = [
(name, obj)
for name, obj in members
if inspect.isclass(obj)
and not name.startswith("_")
and is_defined_in_module(obj, module_name)
]
functions = [
(name, obj)
for name, obj in members
if inspect.isfunction(obj)
and not name.startswith("_")
and is_defined_in_module(obj, module_name)
]
# Check if module only contains deprecated items
if deprecated_items and (classes or functions):
# Check if all classes and functions are deprecated
all_classes_deprecated = all(name in deprecated_items for name, _ in classes)
all_functions_deprecated = all(
name in deprecated_items for name, _ in functions
)
if all_classes_deprecated and all_functions_deprecated:
print(
f" ⚠️ Skipping {module_name} - module only contains deprecated items"
)
return
# Create output directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
# Generate filename preserving module path structure
filename = f"{module_name.replace('.', '_')}.mdx"
output_path = os.path.join(output_dir, filename)
# Start building the MDX content
content = []
# Frontmatter
title = module_name.split(".")[-1].replace("_", " ").title()
module_docstring = get_docstring(module)
# Get icon from module info
module_info = get_module_info_from_filename(f"{module_name.replace('.', '_')}.mdx")
icon = module_info["icon"]
# Generate better description
description = generate_module_description(module_name, module_docstring)
# Generate GitHub URL for source code
# Handle library-specific paths
module_path = module_name.replace(".", "/")
github_url = f"https://github.com/mcp-use/mcp-use/blob/main/libraries/python/{module_path}.py"
frontmatter = {
"title": title,
"description": description,
"icon": icon,
"github": github_url,
# Keep the auto-generated API reference visible in the sidebar but out
# of search indexing (search engines, internal docs search, and AI
# context), so generic queries surface curated guides. See MCP-2398.
"noindex": True,
}
content.append("---")
for key, value in frontmatter.items():
if isinstance(value, bool):
content.append(f"{key}: {str(value).lower()}")
else:
content.append(f'{key}: "{value}"')
content.append("---")
content.append("")
content.append('import {RandomGradientBackground} from "/snippets/gradient.jsx"')
content.append("")
# GitHub source code callout
content.append('<Callout type="info" title="Source Code">')
content.append(
f"View the source code for this module on GitHub: <a href='{github_url}'"
f" target='_blank' rel='noopener noreferrer'>{github_url}</a>"
)
content.append("</Callout>")
content.append("")
# Module description
if module_docstring:
content.append(process_docstring(module_docstring))
content.append("")
# Filter out deprecated items from classes and functions
filtered_classes = [
(name, obj) for name, obj in classes if name not in deprecated_items
]
for _, cls in filtered_classes:
content.append(generate_class_docs(cls, module_name))
content.append("")
filtered_functions = [
(name, obj) for name, obj in functions if name not in deprecated_items
]
for _, func in filtered_functions:
content.append(generate_function_docs(func, module_name))
content.append("")
# Write the file
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(content))
print(f"Generated documentation for {module_name} -> {output_path}")
def find_python_modules(
package_dir: str, exclude_patterns: list[str] | None = None
) -> list[str]:
"""Find all Python modules in a package directory, excluding __init__.py files.
Args:
package_dir: The package directory to search
exclude_patterns: List of patterns to exclude (e.g., ['telemetry', 'internal'])
"""
if exclude_patterns is None:
exclude_patterns = []
modules = []
package_path = Path(package_dir)
if not package_path.exists():
return modules
# Find all submodules, excluding __init__.py files
for py_file in package_path.rglob("*.py"):
if py_file.name == "__init__.py":
# Skip __init__.py files
continue
elif not py_file.name.startswith("__"):
# This is a module
rel_path = py_file.relative_to(package_path.parent)
module_name = str(rel_path.with_suffix("")).replace("/", ".")
# Check if module matches any exclusion pattern
should_exclude = any(pattern in module_name for pattern in exclude_patterns)
if not should_exclude and module_name not in modules:
modules.append(module_name)
return sorted(modules)
def get_module_info_from_filename(filename: str) -> dict[str, str]:
"""Extract module information from filename."""
# Remove .mdx extension
name = filename.replace(".mdx", "")
# Convert snake_case to Title Case
display_name = name.replace("_", " ").title()
# Map specific modules to icons with better semantic mapping
icon_map = {
# Core modules
"mcpagent": "bot",
"mcpclient": "router",
"client": "router",
"server": "server",
"agent": "bot",
# Adapters and connectors
"adapters": "plug-2",
"connectors": "cable",
"base": "box",
"http": "globe",
"websocket": "wifi",
"stdio": "terminal",
"sandbox": "lock",
"streamable_http": "globe",
"sse": "radio",
# Authentication
"auth": "key",
"bearer": "shield",
"oauth": "key",
"oauth_callback": "key",
# Middleware and processing
"middleware": "layers",
"middleware_logging": "logs",
"middleware_metrics": "bar-chart",
# Observability and monitoring
"observability": "eye",
"telemetry": "chart-line",
"logging": "logs",
"callbacks_manager": "phone-callback",
"laminar": "chart-bar",
"langfuse": "chart-line",
"metrics": "bar-chart",
"events": "calendar",
# Configuration and management
"config": "settings",
"session": "session",
"managers": "users",
"server_manager": "server-cog",
"task_managers": "command",
# Tools and utilities
"tools": "hammer",
"base_tool": "hammer",
"connect_server": "server-plus",
"disconnect_server": "server-minus",
"get_active_server": "server",
"list_servers_tool": "list",
"search_tools": "search",
"utils": "tool",
# CLI and interfaces
"cli": "terminal",
# Types and data structures
"types": "type",
# Prompts and templates
"prompts": "volume-2",
"templates": "file-template",
"system_prompt_builder": "file-text",
# Error handling
"exceptions": "triangle-alert",
"errors": "triangle-alert",
"error_formatting": "triangle-alert",
# Remote and cloud
"remote": "cloud",
"openmcp": "server",
}
icon = icon_map.get(name, "code")
return {"module": name, "display_name": display_name, "icon": icon}
def organize_modules_by_path(
files: list[str],
) -> dict[str, dict[str, list[dict[str, str]]]]:
"""Organize modules by file path structure with nested groups."""
packages = {}
for filename in files:
if filename.endswith(".mdx"):
# Skip __init__.py files
if filename == "__init__.mdx":
continue
module_info = get_module_info_from_filename(filename)
# Extract module path from filename automatically
module_path = filename.replace(".mdx", "")
# Convert filename to module path automatically
if module_path.startswith("mcp_use_"):
# Remove mcp_use_ prefix and convert to module path
remaining = module_path[8:] # Remove 'mcp_use_'
# Handle compound package names by mapping them back
compound_mappings = {
"task_managers": "task_managers",
"error_formatting": "error_formatting",
"oauth_callback": "oauth_callback",
"callbacks_manager": "callbacks_manager",
"streamable_http": "streamable_http",
"langchain_adapter": "langchain_adapter",
"system_prompt_builder": "system_prompt_builder",
"server_manager": "server_manager",
"base_tool": "base_tool",
"connect_server": "connect_server",
"disconnect_server": "disconnect_server",
"get_active_server": "get_active_server",
"list_servers_tool": "list_servers_tool",
"search_tools": "search_tools",
}
# Check if this is a compound name we need to preserve
module_path = "mcp_use."
parts = remaining.split("_")
# Reconstruct the path, preserving compound names
i = 0
while i < len(parts):
# Check for compound names starting at this position
compound_found = False
for compound, replacement in compound_mappings.items():
compound_parts = compound.split("_")
if i + len(compound_parts) <= len(parts):
if "_".join(parts[i : i + len(compound_parts)]) == compound:
module_path += replacement + "."
i += len(compound_parts)
compound_found = True
break
if not compound_found:
module_path += parts[i] + "."
i += 1
# Remove trailing dot
module_path = module_path.rstrip(".")
else:
# Handle other cases
module_path = module_path.replace("_", ".")
# Dynamically determine package and subpackage based on module path
path_parts = module_path.split(".")
if len(path_parts) == 1:
# Root level module (e.g., 'mcp_use')
package = "core"
subpackage = "root"
elif len(path_parts) == 2:
# Top-level package (e.g., 'mcp_use.client') - these are root-level modules
package = "core"
subpackage = path_parts[1] # Use the module name as subpackage
elif len(path_parts) >= 3:
# Subpackage (e.g., 'mcp_use.task_managers.base')
# The package is the compound name (e.g., 'task_managers')
package = path_parts[1]
subpackage = path_parts[2]
else:
# Fallback
package = "other"
subpackage = "root"
if package not in packages:
packages[package] = {}
if subpackage:
if subpackage not in packages[package]:
packages[package][subpackage] = []
packages[package][subpackage].append(module_info)
else:
if "root" not in packages[package]:
packages[package]["root"] = []
packages[package]["root"].append(module_info)
return packages
def get_package_display_info(package: str) -> dict[str, str]:
"""Get display information for a package."""
# Common package icons
package_icons = {
"core": "package",
"client": "router",
"server": "server",
"agent": "bot",
"auth": "key",
"middleware": "layers",
"connectors": "cable",
"adapters": "plug-2",
"observability": "eye",
"telemetry": "chart-line",
"logging": "logs",
"cli": "terminal",
"utils": "tool",
"config": "settings",
"session": "database",
"errors": "triangle-alert",
"types": "type",
"task_managers": "command",
"managers": "users",
"prompts": "volume-2",
"remote": "cloud",
"sandbox": "shield",
"http": "globe",
"stdio": "terminal",
"websocket": "wifi",
"bearer": "key",
"oauth": "lock",
"oauth_callback": "refresh-cw",
"error_formatting": "alert-triangle",
"exceptions": "alert-circle",
"callbacks_manager": "users",
"laminar": "layers",
"langfuse": "chart-line",
"base": "layers",
"sse": "activity",
"streamable_http": "globe",
"events": "calendar",
"other": "code",
}
# Use predefined icon if available, otherwise default to 'code'
icon = package_icons.get(package, "code")
# Convert package name to display name
display_name = package.replace("_", " ").title()
return {"name": display_name, "icon": icon}
def get_subpackage_display_info(subpackage: str) -> dict[str, str]:
"""Get display information for a subpackage."""
# Common subpackage icons
subpackage_icons = {
"root": "globe",
"managers": "users",
"adapters": "plug-2",
"observability": "eye",
"prompts": "volume-2",
"auth": "key",
"connectors": "cable",
"middleware": "layers",
"task_managers": "tasks",
"errors": "alert-triangle",
"types": "type",
"tools": "tool",
"remote": "cloud",
"sandbox": "shield",
"http": "globe",
"stdio": "terminal",
"websocket": "wifi",
"bearer": "key",
"oauth": "lock",
"oauth_callback": "refresh-cw",
"error_formatting": "alert-triangle",
"exceptions": "alert-circle",
"callbacks_manager": "users",
"laminar": "layers",
"langfuse": "chart-line",
"base": "layers",
"sse": "activity",
"streamable_http": "globe",
"events": "calendar",
"telemetry": "chart-line",
"utils": "tool",
"config": "settings",
"session": "database",
"logging": "logs",
"cli": "terminal",
}
# Use predefined icon if available, otherwise default to 'code'
icon = subpackage_icons.get(subpackage, "code")
# Convert subpackage name to display name
if subpackage == "root":
display_name = "Overview"
else:
display_name = subpackage.replace("_", " ").title()
return {"name": display_name, "icon": icon}
def generate_api_reference_groups(
packages: dict[str, dict[str, list[dict[str, str]]]],
) -> list[dict[str, Any]]:
"""Generate API reference groups from organized packages with nested structure."""
groups = []
# Process packages in alphabetical order for consistent organization
for package in sorted(packages.keys()):
if package in packages and packages[package]:
package_info = get_package_display_info(package)
# Check if this package has subpackages
has_subpackages = any(key != "root" for key in packages[package].keys())
if has_subpackages:
# Create nested structure with subpackages
subpackages = []
# Add root items directly to subpackages list (no Overview section)
if "root" in packages[package] and packages[package]["root"]:
root_modules = sorted(
packages[package]["root"], key=lambda x: x["display_name"]
)
root_pages = [
f"python/api-reference/{module['module']}"
for module in root_modules
]
subpackages.extend(root_pages)
# Add other subpackages - only create subsections if they have more than 1 item
for subpackage_name in sorted(packages[package].keys()):
if subpackage_name == "root":
continue
modules = packages[package][subpackage_name]
# Only create subsection if there's more than 1 module
if len(modules) > 1:
subpackage_info = get_subpackage_display_info(subpackage_name)
sorted_modules = sorted(
modules, key=lambda x: x["display_name"]
)
pages = [
f"python/api-reference/{module['module']}"
for module in sorted_modules
]
subpackages.append(
{
"group": subpackage_info["name"],
"icon": subpackage_info["icon"],
"pages": pages,
}
)
else:
# Single module - add directly to root level
module = modules[0]
subpackages.append(f"python/api-reference/{module['module']}")
group = {
"group": package_info["name"],
"icon": package_info["icon"],
"pages": subpackages,
}
else:
# Simple structure without subpackages
modules = sorted(
packages[package]["root"], key=lambda x: x["display_name"]
)
pages = [
f"python/api-reference/{module['module']}" for module in modules
]
group = {
"group": package_info["name"],
"icon": package_info["icon"],
"pages": pages,
}
groups.append(group)
return groups
def scan_all_files_for_deprecated_items(package_dir: str) -> dict[str, list[str]]:
"""Scan all Python files in a package for deprecated items.
Args:
package_dir: The package directory to scan
Returns:
Dictionary mapping file paths to lists of deprecated item names
"""
deprecated_files = {}
package_path = Path(package_dir)
if not package_path.exists():
return deprecated_files
# Find all Python files
for py_file in package_path.rglob("*.py"):
if py_file.name == "__init__.py":
continue
deprecated_items = detect_deprecated_items(str(py_file))
if deprecated_items:
deprecated_files[str(py_file)] = deprecated_items
return deprecated_files
def update_docs_json(docs_json_path: str, api_reference_dir: str) -> None:
"""Update docs.json with new API reference structure."""
# Read existing docs.json as text to preserve formatting
with open(docs_json_path, encoding="utf-8") as f:
original_content = f.read()
# Parse JSON for updates
docs_config = json.loads(original_content)
# Get all MDX files in api-reference directory
api_ref_path = Path(api_reference_dir)
if not api_ref_path.exists():
print(f"API reference directory {api_reference_dir} does not exist")
return
mdx_files = [f.name for f in api_ref_path.glob("*.mdx")]
# Organize modules by path
packages = organize_modules_by_path(mdx_files)
# Generate API reference groups
api_groups = generate_api_reference_groups(packages)
# Update the navigation structure
# Find the Python SDK product and then the API Reference tab within it.
for product in docs_config["navigation"]["products"]:
if product.get("product") == "Python SDK":
if "tabs" in product:
for tab in product["tabs"]:
if "API Reference" in tab.get("tab", ""):
tab["groups"] = api_groups
break # Found the tab, stop searching tabs
break # Found the product, stop searching products
# Write updated docs.json
with open(docs_json_path, "w", encoding="utf-8") as f:
json.dump(docs_config, f, indent=2, ensure_ascii=False)
f.write("\n")
print(f"Updated {docs_json_path} with {len(mdx_files)} API reference files")
print(f"Organized into {len(api_groups)} groups")
def main():
"""Main function."""
if len(sys.argv) < 2:
print(
"Usage: python generate_docs.py <package_dir> [output_dir] [docs_json_path] [--scan-only]"
)
print(
"Example: python generate_docs.py ../libraries/python/mcp_use python/api-reference docs.json"
)
print(
"Example: python generate_docs.py ../libraries/python/mcp_use --scan-only"
)
sys.exit(1)
package_dir = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else "api-reference"
docs_json_path = sys.argv[3] if len(sys.argv) > 3 else "docs.json"
scan_only = "--scan-only" in sys.argv
# If scan-only mode, just scan for deprecated items and exit
if scan_only:
print("🔍 Scanning for deprecated items...")
deprecated_files = scan_all_files_for_deprecated_items(package_dir)
if not deprecated_files:
print("✅ No deprecated items found!")
return
print(f"⚠️ Found deprecated items in {len(deprecated_files)} files:")
print()
for file_path, deprecated_items in deprecated_files.items():
print(f"📁 {file_path}")
for item in deprecated_items:
print(f" - {item}")
print()
return
# Add the directory containing the package to Python path
# Convert package_dir (e.g., ../libraries/python/mcp_use) to absolute path
package_abs_path = os.path.abspath(package_dir)
# Get the parent directory that contains the package
parent_dir = os.path.dirname(package_abs_path)
sys.path.insert(0, parent_dir)
print("🔄 Generating API documentation...")
# Step 1: Generate all API docs
# Exclude telemetry and other private modules
exclude_patterns = ["telemetry"]
modules = find_python_modules(package_dir, exclude_patterns)
print(f"Found {len(modules)} modules (excluding: {', '.join(exclude_patterns)})")
# Step 1a: Remove stale documentation files
print("🔄 Checking for stale documentation files...")
existing_mdx_files = (
list(Path(output_dir).glob("*.mdx")) if os.path.exists(output_dir) else []
)
existing_module_names = {f.stem for f in existing_mdx_files}
expected_module_names = {module.replace(".", "_") for module in modules}
stale_files = existing_module_names - expected_module_names
if stale_files:
print(f"🗑️ Found {len(stale_files)} stale documentation file(s) to remove:")
for stale_name in sorted(stale_files):
stale_path = Path(output_dir) / f"{stale_name}.mdx"
if stale_path.exists():
stale_path.unlink()
print(f" - Removed {stale_path}")
else:
print("✅ No stale documentation files found")
# Generate docs for each module
success_count = 0
for module in modules:
try:
print(f"Generating docs for {module}...")
# Check for deprecated items in the module
module_path = module.replace(".", "/")
source_file_path = f"../libraries/python/{module_path}.py"
deprecated_items = detect_deprecated_items(source_file_path)
if deprecated_items:
print(
f" ⚠️ Found {len(deprecated_items)} deprecated item(s) in {module}: {', '.join(deprecated_items)}"
)
generate_module_docs(module, output_dir)
success_count += 1
except Exception as e:
print(f"Error generating docs for {module}: {e}")
continue
print(f"✅ Generated documentation for {success_count}/{len(modules)} modules")
# Step 2: Update docs.json
print("🔄 Updating docs.json...")
if os.path.exists(docs_json_path):
update_docs_json(docs_json_path, output_dir)
print("✅ Updated docs.json with new API reference structure")
else:
print(f"⚠️ docs.json not found at {docs_json_path}, skipping update")
print("🎉 API documentation generation complete!")
print(f"📁 Files generated in: {output_dir}")
print(f"📄 Navigation updated in: {docs_json_path}")
if __name__ == "__main__":
main()