init docforge lib
This commit is contained in:
5
docforge/exporters/__init__.py
Normal file
5
docforge/exporters/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Exporters package for doc-forge output formats."""
|
||||
|
||||
from .mcp import MCPExporter
|
||||
|
||||
__all__ = ["MCPExporter"]
|
||||
18
docforge/exporters/__init__.pyi
Normal file
18
docforge/exporters/__init__.pyi
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Type stubs for doc-forge exporters package."""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from pathlib import Path
|
||||
from docforge.model import Project
|
||||
|
||||
class MCPExporter:
|
||||
"""Exports documentation model to MCP JSON format."""
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def export(self, project: Project, out_dir: Path) -> None: ...
|
||||
|
||||
def export_index(self, project: Project, out_dir: Path) -> None: ...
|
||||
|
||||
def export_nav(self, project: Project, out_dir: Path) -> None: ...
|
||||
|
||||
def export_modules(self, project: Project, out_dir: Path) -> None: ...
|
||||
281
docforge/exporters/mcp.py
Normal file
281
docforge/exporters/mcp.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""MCP exporter for doc-forge static documentation bundles.
|
||||
|
||||
The MCP exporter creates static JSON bundles that can be consumed by
|
||||
MCP (Model Context Protocol) clients. This follows the ADS specification
|
||||
by providing a machine-consumable representation of the documentation
|
||||
model.
|
||||
|
||||
The exporter bypasses renderers entirely and works directly with the
|
||||
documentation model to create alias-safe, deterministic output.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from docforge.model import Project, Module, DocObject
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MCPExporter:
|
||||
"""Exports documentation model to MCP JSON format.
|
||||
|
||||
The MCPExporter creates static JSON bundles that represent the
|
||||
documentation model in a machine-consumable format. These bundles
|
||||
can be loaded by MCP clients to provide structured access to
|
||||
documentation.
|
||||
|
||||
Output structure:
|
||||
mcp/
|
||||
├── index.json # Project metadata
|
||||
├── nav.json # Navigation structure
|
||||
└── modules/
|
||||
├── package.module.json # Individual module data
|
||||
└── ...
|
||||
|
||||
The exported data is:
|
||||
- Alias-safe: Uses canonical paths for all references
|
||||
- Deterministic: Same input always produces same output
|
||||
- Self-contained: No external dependencies
|
||||
- Queryable: Structured for easy machine consumption
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the MCP exporter."""
|
||||
self._version = "1.0.0"
|
||||
|
||||
def export(self, project: Project, out_dir: Path) -> None:
|
||||
"""Export the complete project to MCP format.
|
||||
|
||||
This is the main entry point for exporting. It creates the full
|
||||
MCP bundle structure including index, navigation, and all module
|
||||
files.
|
||||
|
||||
Args:
|
||||
project: The documentation project to export
|
||||
out_dir: Directory where MCP bundle should be written
|
||||
"""
|
||||
if project.is_empty():
|
||||
raise ValueError("Cannot export empty project")
|
||||
|
||||
# Create output directory structure
|
||||
modules_dir = out_dir / "modules"
|
||||
modules_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"Exporting MCP bundle to {out_dir}")
|
||||
|
||||
# Export components
|
||||
self.export_index(project, out_dir)
|
||||
self.export_nav(project, out_dir)
|
||||
self.export_modules(project, modules_dir)
|
||||
|
||||
logger.info(f"MCP export completed: {len(project.get_all_modules())} modules")
|
||||
|
||||
def export_index(self, project: Project, out_dir: Path) -> None:
|
||||
"""Export the project index metadata.
|
||||
|
||||
The index.json file contains project-level metadata and
|
||||
serves as the entry point for the MCP bundle.
|
||||
|
||||
Args:
|
||||
project: The documentation project
|
||||
out_dir: Output directory
|
||||
"""
|
||||
index_path = out_dir / "index.json"
|
||||
|
||||
index_data = {
|
||||
"name": project.name,
|
||||
"version": project.version,
|
||||
"docforge_version": self._version,
|
||||
"export_format": "mcp",
|
||||
"export_timestamp": self._get_timestamp(),
|
||||
"module_count": len(project.get_all_modules()),
|
||||
"total_objects": project.get_total_object_count(),
|
||||
}
|
||||
|
||||
with open(index_path, "w", encoding="utf-8") as f:
|
||||
json.dump(index_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.debug(f"Exported index: {index_path}")
|
||||
|
||||
def export_nav(self, project: Project, out_dir: Path) -> None:
|
||||
"""Export the navigation structure.
|
||||
|
||||
The nav.json file contains the hierarchical navigation structure
|
||||
for browsing the documentation.
|
||||
|
||||
Args:
|
||||
project: The documentation project
|
||||
out_dir: Output directory
|
||||
"""
|
||||
nav_path = out_dir / "nav.json"
|
||||
|
||||
nav_data = {
|
||||
"entries": [
|
||||
{
|
||||
"title": entry.title,
|
||||
"module": entry.module,
|
||||
"path": f"modules/{entry.module.replace('.', '/')}.json",
|
||||
}
|
||||
for entry in project.nav.entries
|
||||
]
|
||||
}
|
||||
|
||||
with open(nav_path, "w", encoding="utf-8") as f:
|
||||
json.dump(nav_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.debug(f"Exported navigation: {nav_path}")
|
||||
|
||||
def export_modules(self, project: Project, modules_dir: Path) -> None:
|
||||
"""Export all modules to individual JSON files.
|
||||
|
||||
Each module is exported to its own JSON file in the modules/
|
||||
directory. The filename follows the pattern: package.module.json
|
||||
|
||||
Args:
|
||||
project: The documentation project
|
||||
modules_dir: Directory for module files
|
||||
"""
|
||||
for module in project.get_all_modules():
|
||||
self._export_module(module, modules_dir)
|
||||
|
||||
def _export_module(self, module: Module, modules_dir: Path) -> None:
|
||||
"""Export a single module to JSON.
|
||||
|
||||
Args:
|
||||
module: The module to export
|
||||
modules_dir: Directory for module files
|
||||
"""
|
||||
# Create subdirectories for nested packages
|
||||
module_file_path = modules_dir / f"{module.path}.json"
|
||||
module_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
module_data = {
|
||||
"path": module.path,
|
||||
"docstring": module.docstring,
|
||||
"objects": [self._serialize_object(obj) for obj in module.get_public_objects()],
|
||||
}
|
||||
|
||||
with open(module_file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(module_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.debug(f"Exported module: {module_file_path}")
|
||||
|
||||
def _serialize_object(self, obj: DocObject) -> Dict[str, Any]:
|
||||
"""Serialize a DocObject to MCP format.
|
||||
|
||||
Args:
|
||||
obj: The DocObject to serialize
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the object
|
||||
"""
|
||||
data = {
|
||||
"name": obj.name,
|
||||
"kind": obj.kind,
|
||||
"path": obj.path,
|
||||
"docstring": obj.docstring,
|
||||
}
|
||||
|
||||
if obj.signature:
|
||||
data["signature"] = obj.signature
|
||||
|
||||
if obj.members:
|
||||
data["members"] = [
|
||||
self._serialize_object(member)
|
||||
for member in obj.get_public_members()
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
def export_module(self, module: Module, out_dir: Path) -> None:
|
||||
"""Export a single module (convenience method).
|
||||
|
||||
Args:
|
||||
module: The module to export
|
||||
out_dir: Output directory
|
||||
"""
|
||||
modules_dir = out_dir / "modules"
|
||||
modules_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._export_module(module, modules_dir)
|
||||
|
||||
def validate_export(self, out_dir: Path) -> bool:
|
||||
"""Validate that the MCP export is complete and valid.
|
||||
|
||||
Args:
|
||||
out_dir: Directory containing the exported bundle
|
||||
|
||||
Returns:
|
||||
True if export is valid, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check required files exist
|
||||
index_path = out_dir / "index.json"
|
||||
nav_path = out_dir / "nav.json"
|
||||
modules_dir = out_dir / "modules"
|
||||
|
||||
if not (index_path.exists() and nav_path.exists() and modules_dir.exists()):
|
||||
return False
|
||||
|
||||
# Load and validate index
|
||||
with open(index_path, "r", encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
|
||||
required_fields = ["name", "docforge_version", "export_format"]
|
||||
if not all(field in index for field in required_fields):
|
||||
return False
|
||||
|
||||
# Load and validate nav
|
||||
with open(nav_path, "r", encoding="utf-8") as f:
|
||||
nav = json.load(f)
|
||||
|
||||
if "entries" not in nav:
|
||||
return False
|
||||
|
||||
# Check that all nav entries have corresponding module files
|
||||
for entry in nav["entries"]:
|
||||
module_path = modules_dir / f"{entry['module']}.json"
|
||||
if not module_path.exists():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Export validation failed: {e}")
|
||||
return False
|
||||
|
||||
def _get_timestamp(self) -> str:
|
||||
"""Get current timestamp in ISO format.
|
||||
|
||||
Returns:
|
||||
ISO format timestamp string
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def get_export_info(self, out_dir: Path) -> Optional[Dict[str, Any]]:
|
||||
"""Get information about an existing export.
|
||||
|
||||
Args:
|
||||
out_dir: Directory containing the exported bundle
|
||||
|
||||
Returns:
|
||||
Export information dictionary or None if invalid
|
||||
"""
|
||||
index_path = out_dir / "index.json"
|
||||
|
||||
if not index_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(index_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user