list_versions
Retrieve a list of all Python documentation versions available in this documentation server.
Instructions
List Python documentation versions available in this index.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| versions | Yes | Available 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", ) - src/mcp_server_python_docs/server.py:273-383 (registration)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