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()