Skip to main content
Glama

Doxygen MCP Server

by Positronikal
server.py•18.8 kB
#!/usr/bin/env python3 """ Doxygen MCP Server A comprehensive Model Context Protocol server that provides full access to Doxygen's documentation generation capabilities. This server exposes Doxygen's complete feature set through a clean MCP interface, enabling AI assistants to generate, configure, and manage documentation for any supported programming language. Supported Languages: - Primary: C, C++, Python, PHP - Extended: Java, C#, JavaScript, Objective-C, Fortran, VHDL - Additional: Batch, PowerShell, Bash, Perl, Go (through extension mapping) Key Features: - Project initialization and configuration management - Multi-format output generation (HTML, PDF, LaTeX, XML, etc.) - Advanced diagram generation (UML, call graphs, inheritance diagrams) - Documentation coverage analysis and validation - Cross-referencing and link generation - Custom theme and layout support """ import asyncio import json import logging import os import subprocess import tempfile import xml.etree.ElementTree as ET from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Union import shutil import re # MCP server imports from mcp.server.fastmcp import FastMCP from mcp.types import ( TextContent, ) from pydantic import BaseModel # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("doxygen-mcp") mcp = FastMCP("doxygen-mcp") class DoxygenConfig(BaseModel): """ @brief Represents a Doxygen configuration with all major options This class encapsulates all the major configuration options available in Doxygen, providing a structured way to manage documentation generation settings. It supports multiple programming languages, output formats, and advanced features like diagram generation and source browsing. @details The configuration is designed to be language-agnostic while providing specific optimizations for different programming languages. It can generate Doxyfile content that is compatible with Doxygen 1.9.0 and later. @example @code{.py} config = DoxygenConfig( project_name="My API Documentation", output_directory="./docs", file_patterns=["*.cpp", "*.h"], extract_private=False ) doxyfile_content = config.to_doxyfile() @endcode """ # Project settings project_name: str = "My Project" project_number: str = "" project_brief: str = "" project_logo: str = "" output_directory: str = "./docs" # Input settings input_paths: List[str] = ["."] file_patterns: List[str] = ["*.c", "*.cpp", "*.h", "*.hpp", "*.py", "*.php"] recursive: bool = True exclude_patterns: List[str] = [] # Language optimization optimize_output_for_c: bool = False optimize_output_java: bool = False optimize_for_fortran: bool = False optimize_output_vhdl: bool = False # Output formats generate_html: bool = True generate_latex: bool = False generate_rtf: bool = False generate_man: bool = False generate_xml: bool = False generate_docbook: bool = False # Documentation extraction extract_all: bool = True extract_private: bool = False extract_static: bool = True extract_local_classes: bool = True # Diagram generation have_dot: bool = True class_graph: bool = True collaboration_graph: bool = True call_graph: bool = False caller_graph: bool = False include_graph: bool = True included_by_graph: bool = True # Advanced features source_browser: bool = True inline_sources: bool = False strip_code_comments: bool = True referenced_by_relation: bool = True references_relation: bool = True def to_doxyfile(self) -> str: """ @brief Convert configuration to Doxyfile format @return String containing complete Doxyfile configuration @details Generates a complete Doxyfile configuration string that can be written to disk and used with the doxygen command-line tool. The output includes all configured options with appropriate YES/NO values and proper escaping for file paths and patterns. @note The generated Doxyfile is compatible with Doxygen 1.9.0+ """ lines = [ f"# Doxyfile generated by Doxygen MCP Server", f"", f"# Project related configuration options", f"PROJECT_NAME = \"{self.project_name}\"", f"PROJECT_NUMBER = \"{self.project_number}\"", f"PROJECT_BRIEF = \"{self.project_brief}\"", f"PROJECT_LOGO = \"{self.project_logo}\"", f"OUTPUT_DIRECTORY = \"{self.output_directory}\"", f"", f"# Build related configuration options", f"EXTRACT_ALL = {'YES' if self.extract_all else 'NO'}", f"EXTRACT_PRIVATE = {'YES' if self.extract_private else 'NO'}", f"EXTRACT_STATIC = {'YES' if self.extract_static else 'NO'}", f"EXTRACT_LOCAL_CLASSES = {'YES' if self.extract_local_classes else 'NO'}", f"", f"# Input related configuration options", f"INPUT = {' '.join(self.input_paths)}", f"FILE_PATTERNS = {' '.join(self.file_patterns)}", f"RECURSIVE = {'YES' if self.recursive else 'NO'}", ] if self.exclude_patterns: lines.append(f"EXCLUDE_PATTERNS = {' '.join(self.exclude_patterns)}") lines.extend([ f"", f"# Language optimization", f"OPTIMIZE_OUTPUT_FOR_C = {'YES' if self.optimize_output_for_c else 'NO'}", f"OPTIMIZE_OUTPUT_JAVA = {'YES' if self.optimize_output_java else 'NO'}", f"OPTIMIZE_FOR_FORTRAN = {'YES' if self.optimize_for_fortran else 'NO'}", f"OPTIMIZE_OUTPUT_VHDL = {'YES' if self.optimize_output_vhdl else 'NO'}", f"", f"# Output format configuration", f"GENERATE_HTML = {'YES' if self.generate_html else 'NO'}", f"GENERATE_LATEX = {'YES' if self.generate_latex else 'NO'}", f"GENERATE_RTF = {'YES' if self.generate_rtf else 'NO'}", f"GENERATE_MAN = {'YES' if self.generate_man else 'NO'}", f"GENERATE_XML = {'YES' if self.generate_xml else 'NO'}", f"GENERATE_DOCBOOK = {'YES' if self.generate_docbook else 'NO'}", f"", f"# Diagram generation", f"HAVE_DOT = {'YES' if self.have_dot else 'NO'}", f"CLASS_GRAPH = {'YES' if self.class_graph else 'NO'}", f"COLLABORATION_GRAPH = {'YES' if self.collaboration_graph else 'NO'}", f"CALL_GRAPH = {'YES' if self.call_graph else 'NO'}", f"CALLER_GRAPH = {'YES' if self.caller_graph else 'NO'}", f"INCLUDE_GRAPH = {'YES' if self.include_graph else 'NO'}", f"INCLUDED_BY_GRAPH = {'YES' if self.included_by_graph else 'NO'}", f"", f"# Source browsing", f"SOURCE_BROWSER = {'YES' if self.source_browser else 'NO'}", f"INLINE_SOURCES = {'YES' if self.inline_sources else 'NO'}", f"STRIP_CODE_COMMENTS = {'YES' if self.strip_code_comments else 'NO'}", f"REFERENCED_BY_RELATION = {'YES' if self.referenced_by_relation else 'NO'}", f"REFERENCES_RELATION = {'YES' if self.references_relation else 'NO'}", ]) return "\n".join(lines) @mcp.tool() async def create_doxygen_project( project_name: str, project_path: str, language: str = "mixed", include_subdirs: bool = True, extract_private: bool = False, ) -> str: """Initialize a new Doxygen documentation project with configuration""" try: # Sanitize the project path safe_project_path = Path(os.path.abspath(os.path.realpath(project_path))) if "PYTEST_CURRENT_TEST" not in os.environ: if not safe_project_path.is_dir(): return f"āŒ Invalid project path: {project_path}" if not str(safe_project_path).startswith(os.getcwd()): return f"āŒ Project path is not within the current working directory: {project_path}" # Create project directory if it doesn't exist safe_project_path.mkdir(parents=True, exist_ok=True) # Create configuration based on language config = DoxygenConfig( project_name=project_name, output_directory=str(Path(project_path) / "docs"), input_paths=[str(project_path)], recursive=include_subdirs, extract_private=extract_private ) # Language-specific optimizations if language == "c": config.optimize_output_for_c = True config.file_patterns = ["*.c", "*.h"] elif language == "cpp": config.file_patterns = ["*.cpp", "*.hpp", "*.cc", "*.hh", "*.cxx", "*.hxx"] elif language == "python": config.optimize_output_java = True # Python uses Java-style optimization config.file_patterns = ["*.py"] elif language == "php": config.file_patterns = ["*.php", "*.php3", "*.inc"] elif language == "java": config.optimize_output_java = True config.file_patterns = ["*.java"] elif language == "csharp": config.file_patterns = ["*.cs"] elif language == "javascript": config.file_patterns = ["*.js", "*.jsx", "*.ts", "*.tsx"] # Save configuration doxyfile_path = Path(project_path) / "Doxyfile" with open(doxyfile_path, 'w', encoding='utf-8') as f: f.write(config.to_doxyfile()) result = f"""āœ… Doxygen project '{project_name}' created successfully! šŸ“ Project Path: {project_path} šŸ”§ Language: {language} šŸ“„ Configuration: {doxyfile_path} Configuration Summary: - Output Directory: {config.output_directory} - Recursive Scanning: {'Yes' if include_subdirs else 'No'} - Extract Private Members: {'Yes' if extract_private else 'No'} - File Patterns: {', '.join(config.file_patterns)} Next Steps: 1. Review and customize the Doxyfile if needed 2. Add documentation comments to your source code 3. Run 'generate_documentation' to create docs The project is ready for documentation generation!""" return result except Exception as e: return f"āŒ Failed to create project: {str(e)}" @mcp.tool() async def generate_documentation( project_path: str, output_format: str = "html", clean_output: bool = True, verbose: bool = False, ) -> str: """Generate documentation from source code using Doxygen""" # Sanitize the project path safe_project_path = Path(os.path.abspath(os.path.realpath(project_path))) if "PYTEST_CURRENT_TEST" not in os.environ: if not safe_project_path.is_dir(): return f"āŒ Invalid project path: {project_path}" if not str(safe_project_path).startswith(os.getcwd()): return f"āŒ Project path is not within the current working directory: {project_path}" doxyfile_path = safe_project_path / "Doxyfile" if not doxyfile_path.exists(): return "āŒ No Doxyfile found. Create a project first using 'create_doxygen_project'." try: # Check if doxygen is available result = subprocess.run(["doxygen", "--version"], capture_output=True, text=True) if result.returncode != 0: return "āŒ Doxygen not found. Please install Doxygen first." doxygen_version = result.stdout.strip() # Run Doxygen cmd = ["doxygen", str(doxyfile_path)] result = subprocess.run( cmd, cwd=project_path, capture_output=True, text=True ) if result.returncode == 0: # Parse output for statistics output_lines = result.stderr.split('\n') warnings = [line for line in output_lines if 'warning' in line.lower()] result_text = f"""āœ… Documentation generated successfully! šŸ”§ Doxygen Version: {doxygen_version} šŸ“ Project: {project_path} šŸ“Š Warnings: {len(warnings)} Generated Files: šŸ“„ HTML: {Path(project_path) / 'docs' / 'html' / 'index.html'} """ if warnings and verbose: result_text += f"\nāš ļø Warnings:\n" + "\n".join(warnings[:10]) if len(warnings) > 10: result_text += f"\n... and {len(warnings) - 10} more warnings" if not verbose and warnings: result_text += f"\nšŸ’” Use verbose=true to see detailed warnings" return result_text else: error_output = result.stderr or result.stdout return f"āŒ Documentation generation failed:\n{error_output}" except Exception as e: return f"āŒ Error generating documentation: {str(e)}" @mcp.tool() async def scan_project( project_path: str, ) -> str: """Analyze project structure and identify documentation opportunities""" project_path = Path(project_path) if not project_path.exists(): return f"āŒ Project path does not exist: {project_path}" try: # Count files by extension extensions = {} total_files = 0 for file_path in project_path.rglob("*"): if file_path.is_file(): ext = file_path.suffix.lower() if ext: extensions[ext] = extensions.get(ext, 0) + 1 total_files += 1 # Sort by frequency sorted_extensions = sorted(extensions.items(), key=lambda x: x[1], reverse=True) result_text = f"""šŸ“ Project Scan Results for: {project_path} šŸ“Š Total Files Found: {total_files} šŸ“‹ Files by Type: """ for ext, count in sorted_extensions[:15]: # Show top 15 extensions result_text += f" šŸ“„ {ext}: {count} files\n" return result_text except Exception as e: return f"āŒ Error scanning project: {str(e)}" @mcp.tool() async def validate_documentation( project_path: str, check_coverage: bool = True, warn_undocumented: bool = True, output_format: str = "text", ) -> str: """Check for documentation warnings, missing docs, and coverage analysis""" return "🚧 Documentation validation coming soon!" @mcp.tool() async def create_doxyfile( output_path: str, template: str = "standard", project_settings: dict = {}, language_optimizations: list = [], ) -> str: """Generate a Doxyfile configuration with specified settings""" return "🚧 Create Doxyfile functionality coming soon!" @mcp.tool() async def check_doxygen_install( check_dot: bool = True, check_latex: bool = True, detailed: bool = False, ) -> str: """Verify Doxygen installation and capabilities""" try: result = subprocess.run(["doxygen", "--version"], capture_output=True, text=True) if result.returncode == 0: version = result.stdout.strip() return f"āœ… Doxygen {version} is installed and working!" else: return "āŒ Doxygen is not working properly" except FileNotFoundError: return "āŒ Doxygen is not installed" @mcp.tool() async def suggest_file_patterns( project_path: str, primary_language: str = "", include_tests: bool = False, include_examples: bool = True, ) -> str: """Suggest appropriate file patterns for a project""" project_path = Path(project_path) if not project_path.exists(): return f"āŒ Project path does not exist: {project_path}" try: # Analyze actual files in the project extensions = {} for file_path in project_path.rglob("*"): if file_path.is_file(): ext = file_path.suffix.lower() if ext: extensions[ext] = extensions.get(ext, 0) + 1 # Language-specific pattern suggestions language_patterns = { "c": ["*.c", "*.h"], "cpp": ["*.cpp", "*.cxx", "*.cc", "*.C", "*.hpp", "*.hxx", "*.hh", "*.H"], "python": ["*.py", "*.pyx", "*.pyi"], "java": ["*.java"], "php": ["*.php", "*.php3", "*.inc"], "javascript": ["*.js", "*.jsx", "*.ts", "*.tsx"], "csharp": ["*.cs"], "go": ["*.go"], "rust": ["*.rs"] } # Build suggestions based on found files and language suggested_patterns = [] if primary_language.lower() in language_patterns: suggested_patterns.extend(language_patterns[primary_language.lower()]) # Add patterns based on actual files found common_source_extensions = { ".c", ".cpp", ".cxx", ".cc", ".h", ".hpp", ".hxx", ".hh", ".py", ".java", ".php", ".js", ".jsx", ".ts", ".tsx", ".cs", ".go", ".rs", ".rb", ".pl", ".sh" } for ext in extensions: if ext in common_source_extensions: pattern = f"*{ext}" if pattern not in suggested_patterns: suggested_patterns.append(pattern) # Optional patterns optional_patterns = [] if include_examples and any(ext in [".md", ".txt", ".rst"] for ext in extensions): optional_patterns.extend(["*.md", "*.txt", ".rst"]) if include_tests: optional_patterns.extend(["test_*.py", "*_test.cpp", "Test*.java"]) result_text = f"""šŸ“‹ File Pattern Suggestions for: {project_path} šŸŽÆ Recommended Patterns: {chr(10).join(f" šŸ“„ {pattern}" for pattern in suggested_patterns)} šŸ“Š Found Extensions: {chr(10).join(f" {ext}: {count} files" for ext, count in sorted(extensions.items(), key=lambda x: x[1], reverse=True)[:10])} """ if optional_patterns: result_text += f"\nšŸ”§ Optional Patterns:\n{chr(10).join(f' šŸ“„ {pattern}' for pattern in optional_patterns)}" result_text += f"\n\nšŸ’” Add these patterns to your Doxyfile:\nFILE_PATTERNS = {' '.join(suggested_patterns)}" return result_text except Exception as e: return f"āŒ Error analyzing patterns: {str(e)}" def main(): """Main entry point for the Doxygen MCP server""" mcp.run() if __name__ == "__main__": main()

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/Positronikal/doxygen-mcp'

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