Skip to main content
Glama
omniwaifu

Pydantic AI Documentation Server

by omniwaifu
server.py8.38 kB
import logging from fastmcp import FastMCP from typing import List, Optional from pathlib import Path # Import actual models from .models import ( ParsedDocument, TopicItem, StatusResponse, ChangelogFile, ChangelogContent, ) # Import repository manager functions from .repository_manager import ( clone_or_pull_repository, get_repo_path, get_current_commit_hash, ) # Import parser functions from .parser import parse_markdown_file, get_pydantic_docs_path # Search index functionality (Whoosh-based) has been removed from this server. logger = logging.getLogger(__name__) app = FastMCP() @app.tool() async def update_documentation(force_clone: bool = False) -> StatusResponse: """Clones/updates the Pydantic repo, parses docs, and rebuilds the search index.""" logger.info(f"update_documentation called with force_clone={force_clone}") try: repo_update_result = clone_or_pull_repository(force_clone) logger.info(f"Repository update result: {repo_update_result}") if repo_update_result.get("status") == "error": return StatusResponse( status="error", message=repo_update_result.get("message", "Repository update failed"), data=repo_update_result, ) commit_hash = get_current_commit_hash() return StatusResponse( status="success", message=f"Documentation updated successfully from commit {commit_hash}.", data={"commit_hash": commit_hash}, ) except Exception as e: logger.error(f"Error in update_documentation: {e}", exc_info=True) return StatusResponse( status="error", message=str(e), data={"error_details": str(e)} ) @app.tool() async def get_document_by_path(path: str) -> Optional[ParsedDocument]: """ Retrieves a specific document by its path relative to the Pydantic documentation root (e.g., 'usage/models.md'). Returns the ParsedDocument if found, otherwise None. """ logger.info(f"Attempting to get document by path: {path}") try: pydantic_docs_root = get_pydantic_docs_path() full_file_path = (pydantic_docs_root / path).resolve() if not str(full_file_path).startswith(str(pydantic_docs_root.resolve())): logger.warning( f"Path traversal attempt or invalid path for get_document_by_path: {path}" ) return None if not full_file_path.is_file(): logger.info( f"Document not found or not a file at path: {path} (resolved: {full_file_path})" ) return None document = parse_markdown_file( file_path=full_file_path, docs_base_dir=pydantic_docs_root ) if document: logger.info(f"Successfully retrieved document: {document.path}") return document else: logger.warning( f"Failed to parse document at path: {path} (resolved: {full_file_path})" ) return None except Exception as e: logger.error( f"Error in get_document_by_path for path '{path}': {e}", exc_info=True ) return None @app.tool() async def list_topics(path: Optional[str] = None) -> List[TopicItem]: """ Lists files and directories non-recursively within the Pydantic documentation. The path is relative to the 'docs/' directory in the cloned Pydantic repository. """ logger.info(f"Listing topics for path: {path if path else 'root'}") try: repo_root = get_repo_path() docs_root = repo_root / "docs" if not docs_root.exists() or not docs_root.is_dir(): logger.error(f"Pydantic docs directory not found at {docs_root}") return [] current_path = docs_root if path: target_path = (docs_root / path).resolve(strict=False) if not str(target_path).startswith(str(docs_root.resolve())): logger.warning(f"Path traversal attempt or invalid path: {path}") return [] if not target_path.exists() or not target_path.is_dir(): logger.info( f"Specified path does not exist or is not a directory: {target_path}" ) return [] current_path = target_path items: List[TopicItem] = [] for item in current_path.iterdir(): if item.name.startswith("."): continue item_relative_path = item.relative_to(docs_root).as_posix() items.append( TopicItem( name=item.name, path=item_relative_path, is_directory=item.is_dir() ) ) items.sort(key=lambda x: (not x.is_directory, x.name.lower())) return items except FileNotFoundError: logger.info(f"Path not found during list_topics: {path}") return [] except Exception as e: logger.error(f"Error listing topics for path '{path}': {e}", exc_info=True) return [] @app.tool() async def list_available_changelogs() -> List[ChangelogFile]: """ Lists all available changelog files found in the Pydantic-AI documentation repository (e.g., in 'docs/history/'). """ logger.info("list_available_changelogs called") changelogs: List[ChangelogFile] = [] try: repo_root = get_repo_path() history_dir = repo_root / "docs" / "history" if not history_dir.is_dir(): logger.warning(f"Changelog directory not found: {history_dir}") return [] for file_path in history_dir.glob("*.md"): if file_path.is_file(): version_name = file_path.stem relative_path = file_path.relative_to(history_dir).as_posix() changelogs.append( ChangelogFile( name=file_path.name, path=f"history/{relative_path}", version=version_name, ) ) changelogs.sort( key=lambda cf: [ int(part) if part.isdigit() else part for part in cf.version.split(".") ], reverse=True, ) logger.info(f"Found {len(changelogs)} changelog files.") except Exception as e: logger.error(f"Error listing available changelogs: {e}", exc_info=True) return [] return changelogs @app.tool() async def get_changelog_content(path: str) -> Optional[ChangelogContent]: """ Retrieves the parsed content of a specific changelog file. The path should be relative to the Pydantic-AI documentation root's 'docs' directory (e.g., 'history/0.1.0.md'). """ logger.info(f"get_changelog_content called for path: {path}") try: repo_root = get_repo_path() docs_dir = repo_root / "docs" changelog_file_path = (docs_dir / path).resolve() if not str(changelog_file_path).startswith(str(docs_dir.resolve())): logger.warning( f"Path traversal attempt or invalid path for get_changelog_content: {path}" ) return None if not changelog_file_path.is_file(): logger.info( f"Changelog file not found or not a file at path: {path} (resolved: {changelog_file_path})" ) return None content = changelog_file_path.read_text(encoding="utf-8") version = Path(path).stem return ChangelogContent( path=path, version=version, content=content, release_date=None, ) except Exception as e: logger.error( f"Error in get_changelog_content for path '{path}': {e}", exc_info=True ) return None def main_server_logic(): logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger.info("Initializing Pydantic Docs Server...") app.run(transport="stdio") # This check is not strictly necessary if __main__.py calls main_server_logic directly, # but good practice if server.py might also be run directly. if __name__ == "__main__": main_server_logic()

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/omniwaifu/pydantic-ai-docs-server'

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