Skip to main content
Glama
ayhammouda

python-docs-mcp-server

list_versions

Read-onlyIdempotent

Retrieve a list of all Python documentation versions available in this documentation server.

Instructions

List Python documentation versions available in this index.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
versionsYesAvailable documentation versions

Implementation Reference

  • MCP tool handler function for 'list_versions'. It is registered via @mcp.tool decorator, retrieves the VersionService from the app context, and calls version_service.list_versions(). Error handling wraps DocsServerError and generic exceptions into ToolError.
    @mcp.tool(annotations=_TOOL_ANNOTATIONS)
    def list_versions(
        ctx: Context = None,  # type: ignore[assignment]
    ) -> ListVersionsResult:
        """List Python documentation versions available in this index."""
        app_ctx: AppContext = ctx.request_context.lifespan_context
        try:
            return app_ctx.version_service.list_versions()
        except DocsServerError as e:
            raise ToolError(str(e))
        except Exception as e:
            logger.exception("Unexpected error in list_versions")
            raise ToolError(f"Internal error: {type(e).__name__}")
  • VersionService class containing the core business logic for list_versions. The list_versions() method queries the doc_sets SQLite table, constructs VersionInfo objects, and returns a ListVersionsResult. Decorated with @log_tool_call for observability.
    class VersionService:
        """Version listing service for list_versions tool.
    
        Trivial service that queries doc_sets table. Kept as a class
        for symmetry with SearchService and ContentService.
        """
    
        def __init__(self, db: sqlite3.Connection) -> None:
            self._db = db
    
        @log_tool_call("list_versions")
        def list_versions(self) -> ListVersionsResult:
            """List all available documentation versions from doc_sets table."""
            rows = self._db.execute(
                """
                SELECT version, language, label, is_default, built_at
                FROM doc_sets
                ORDER BY version DESC
                """
            ).fetchall()
    
            versions = [
                VersionInfo(
                    version=row["version"],
                    language=row["language"],
                    label=row["label"],
                    is_default=bool(row["is_default"]),
                    built_at=row["built_at"] or "",
                )
                for row in rows
            ]
    
            return ListVersionsResult(versions=versions)
  • Pydantic schema models for the list_versions tool: VersionInfo (output item with version, language, label, is_default, built_at fields) and ListVersionsResult (output envelope with a list of VersionInfo).
    # --- list_versions models ---
    
    
    class VersionInfo(BaseModel):
        """Information about an available Python version."""
    
        version: str = Field(description="Python version string (e.g. '3.13')")
        language: str = Field(default="en", description="Documentation language")
        label: str = Field(description="Display label")
        is_default: bool = Field(description="Whether this is the default version")
        built_at: str = Field(description="When this version's index was built")
    
    
    class ListVersionsResult(BaseModel):
        """Output from list_versions tool."""
    
        versions: list[VersionInfo] = Field(
            description="Available documentation versions",
        )
  • The create_server() function uses FastMCP to register the list_versions tool via @mcp.tool(annotations=_TOOL_ANNOTATIONS) decorator on line 341. This is where the tool is registered with the MCP server, along with readOnly/ idempotent annotations.
    def create_server() -> FastMCP:
        """Create and configure the FastMCP server."""
        mcp = FastMCP(
            "python-docs-mcp-server",
            lifespan=app_lifespan,
        )
    
        @mcp.tool(annotations=_TOOL_ANNOTATIONS)
        def search_docs(
            query: SearchQueryParam,
            version: VersionParam = None,
            kind: SearchKindParam = "auto",
            max_results: MaxResultsParam = 5,
            ctx: Context = None,  # type: ignore[assignment]
        ) -> SearchDocsResult:
            """Search Python documentation. Use kind='symbol' for API lookups
            (asyncio.TaskGroup), kind='example' for code samples, kind='auto' otherwise.
            When version is omitted, searches across all versions. Pass the version
            from each hit's version field to get_docs for consistent results."""
            app_ctx: AppContext = ctx.request_context.lifespan_context
            try:
                return app_ctx.search_service.search(query, version, kind, max_results)
            except DocsServerError as e:
                raise ToolError(str(e))
            except Exception as e:
                logger.exception("Unexpected error in search_docs")
                raise ToolError(f"Internal error: {type(e).__name__}")
    
        @mcp.tool(annotations=_TOOL_ANNOTATIONS)
        def get_docs(
            slug: SlugParam,
            version: VersionParam = None,
            anchor: AnchorParam = None,
            max_chars: MaxCharsParam = 8000,
            start_index: StartIndexParam = 0,
            ctx: Context = None,  # type: ignore[assignment]
        ) -> GetDocsResult:
            """Retrieve a documentation page or specific section. Provide anchor for
            section-only retrieval (much cheaper). Pagination via start_index."""
            app_ctx: AppContext = ctx.request_context.lifespan_context
            # Auto-default to detected Python version when no version specified
            if version is None and app_ctx.detected_python_version:
                version = app_ctx.detected_python_version
            try:
                return app_ctx.content_service.get_docs(slug, version, anchor, max_chars, start_index)
            except DocsServerError as e:
                raise ToolError(str(e))
            except Exception as e:
                logger.exception("Unexpected error in get_docs")
                raise ToolError(f"Internal error: {type(e).__name__}")
    
        @mcp.tool(annotations=_PYPI_TOOL_ANNOTATIONS)
        async def lookup_package_docs(
            package: PackageParam,
            ctx: Context = None,  # type: ignore[assignment]
        ) -> PackageDocsResult:
            """Look up package-declared docs/homepage/source URLs via official PyPI metadata.
    
            This is not generic web search: it only queries PyPI's JSON API and
            returns official PyPI metadata plus package-declared project URLs.
            """
            app_ctx: AppContext = ctx.request_context.lifespan_context
            try:
                return await asyncio.to_thread(app_ctx.package_docs_service.lookup, package)
            except Exception as e:
                logger.exception("Unexpected error in lookup_package_docs")
                raise ToolError(f"Internal error: {type(e).__name__}")
    
        @mcp.tool(annotations=_TOOL_ANNOTATIONS)
        def list_versions(
            ctx: Context = None,  # type: ignore[assignment]
        ) -> ListVersionsResult:
            """List Python documentation versions available in this index."""
            app_ctx: AppContext = ctx.request_context.lifespan_context
            try:
                return app_ctx.version_service.list_versions()
            except DocsServerError as e:
                raise ToolError(str(e))
            except Exception as e:
                logger.exception("Unexpected error in list_versions")
                raise ToolError(f"Internal error: {type(e).__name__}")
    
        @mcp.tool(annotations=_TOOL_ANNOTATIONS)
        def detect_python_version(
            ctx: Context = None,  # type: ignore[assignment]
        ) -> DetectPythonVersionResult:
            """Detect the Python version in the user's environment.
            Returns the detected version, how it was found, and whether it
            matches an indexed documentation set."""
            app_ctx: AppContext = ctx.request_context.lifespan_context
            detected_ver = app_ctx.detected_python_version
    
            # Re-run detection to get the raw version even if it didn't match
            from mcp_server_python_docs.detection import detect_python_version as _detect
    
            raw_ver, raw_src = _detect()
    
            return DetectPythonVersionResult(
                detected_version=raw_ver,
                source=raw_src,
                matched_index_version=detected_ver,
                is_default=detected_ver is not None,
            )
    
        # SRVR-07: _meta hint for get_docs tool.
        # FastMCP 1.27 does not expose a public API for setting _meta on tool
        # definitions. Deferred until the mcp SDK adds _meta support to the
        # decorator API or tool manager. The hint is advisory — clients work
        # correctly without it.
    
        return mcp
  • @log_tool_call decorator used on VersionService.list_versions(). Provides structured logging (logfmt) for the tool call, extracting result_count from ListVersionsResult.versions.
    def log_tool_call(tool_name: str) -> Callable:
        """Decorator that logs structured info for every service method call.
    
        Extracts version, result_count, truncated, resolution path, and
        synonym_expansion from the method arguments and return value.
    
        Note: This decorator only works with synchronous service methods.
        If a method becomes async, the wrapper must be updated to detect
        coroutines and await the result, otherwise timing will be wrong.
    
        Args:
            tool_name: Name of the MCP tool (search_docs, get_docs, list_versions).
        """
    
        def decorator(fn: Callable) -> Callable:
            @functools.wraps(fn)
            def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
                start = time.monotonic()
                result: Any = None
                error: Exception | None = None
                error_tb: TracebackType | None = None
    
                try:
                    result = fn(self, *args, **kwargs)
                except Exception as exc:
                    error = exc
                    error_tb = exc.__traceback__
    
                elapsed_ms = (time.monotonic() - start) * 1000
    
                # Extract structured fields from args/kwargs and result
                fields: dict[str, Any] = {
                    "tool": tool_name,
                    "latency_ms": round(elapsed_ms, 1),
                }
    
                # Extract version by name via inspect (WR-05: avoids fragile positional indexing)
                try:
                    sig = inspect.signature(fn)
                    bound = sig.bind(self, *args, **kwargs)
                    bound.apply_defaults()
                    version_val = bound.arguments.get("version")
                except (TypeError, ValueError):
                    version_val = kwargs.get("version")
                fields["version"] = version_val or "default"
    
                if error is None:
                    # Extract result-specific fields
                    if hasattr(result, "hits"):
                        # SearchDocsResult
                        fields["result_count"] = len(result.hits)
                        # Resolution path from service state
                        if hasattr(self, "_last_resolution"):
                            fields["resolution"] = self._last_resolution
                        else:
                            fields["resolution"] = "fts"
                        fields["truncated"] = False
                    elif hasattr(result, "truncated"):
                        # GetDocsResult
                        fields["result_count"] = 1 if result.content else 0
                        fields["truncated"] = result.truncated
                        fields["resolution"] = "exact"
                    elif hasattr(result, "versions"):
                        # ListVersionsResult
                        fields["result_count"] = len(result.versions)
                        fields["truncated"] = False
                        fields["resolution"] = "exact"
    
                    # Synonym expansion detection from service state
                    if hasattr(self, "_last_synonym_expanded"):
                        fields["synonym_expansion"] = (
                            "yes" if self._last_synonym_expanded else "no"
                        )
                else:
                    fields["error"] = type(error).__name__
    
                # Write logfmt line to stderr (HYGN-01 safe — stderr only)
                log_line = _format_logfmt(**fields)
                print(log_line, file=sys.stderr)
    
                if error is not None:
                    raise error.with_traceback(error_tb)
    
                return result
    
            return wrapper
    
        return decorator
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and idempotentHint=true, making the tool's safety clear. The description adds the scope 'in this index' but does not elaborate on behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence that conveys the complete purpose with no wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given zero parameters and the presence of an output schema, the description is mostly complete, though it could briefly mention the return format for full clarity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With zero parameters, the description does not need to explain parameters. The purpose statement is sufficient, and the baseline of 4 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('List') and resource ('Python documentation versions'), and clearly distinguishes from sibling tools like 'get_docs' and 'detect_python_version'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for checking available versions, but provides no explicit guidance on when to use this tool versus alternatives or when not to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ayhammouda/python-docs-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server