Skip to main content
Glama
cli.py9.22 kB
#!/usr/bin/env python3 """Command-line interface for MCP documentation server.""" import argparse import json import os import signal import sys import asyncio from typing import List, Dict import yaml from mcpdoc._version import __version__ from mcpdoc.main import create_server, DocSource from mcpdoc.splash import SPLASH class CustomFormatter( argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter ): # Custom formatter to preserve epilog formatting while showing default values pass EPILOG = """ Examples: # Directly specifying documentation files with optional names mcpdoc --urls LangGraph:https://esakrissa.github.io/mcpdoc/docs/langgraph.txt # Using a local file (absolute or relative path) mcpdoc --urls MCP:/path/to/mcp.txt --allowed-domains '*' # Using a YAML config file mcpdoc --yaml sample_config.yaml # Using a JSON config file mcpdoc --json sample_config.json # Combining multiple documentation sources mcpdoc --yaml sample_config.yaml --json sample_config.json --urls LangGraph:https://esakrissa.github.io/mcpdoc/docs/langgraph.txt # Using SSE transport with default host (127.0.0.1) and port (8000) mcpdoc --yaml sample_config.yaml --transport sse # Using SSE transport with custom host and port mcpdoc --yaml sample_config.yaml --transport sse --host 0.0.0.0 --port 9000 # Using SSE transport with additional HTTP options mcpdoc --yaml sample_config.yaml --follow-redirects --timeout 15 --transport sse --host localhost --port 8080 # Allow fetching from additional domains. The domains hosting the documentation files are always allowed. mcpdoc --yaml sample_config.yaml --allowed-domains https://example.com/ https://another-example.com/ # Allow fetching from any domain mcpdoc --yaml sample_config.yaml --allowed-domains '*' """ def parse_args() -> argparse.Namespace: """Parse command-line arguments.""" # Custom formatter to preserve epilog formatting parser = argparse.ArgumentParser( description="MCP Documentation Server", formatter_class=CustomFormatter, epilog=EPILOG, ) # Allow combining multiple doc source methods parser.add_argument( "--yaml", "-y", type=str, help="Path to YAML config file with doc sources" ) parser.add_argument( "--json", "-j", type=str, help="Path to JSON config file with doc sources" ) parser.add_argument( "--urls", "-u", type=str, nargs="+", help="List of documentation file URLs or file paths with optional names (format: 'url_or_path' or 'name:url_or_path')", ) parser.add_argument( "--follow-redirects", action="store_true", help="Whether to follow HTTP redirects", ) parser.add_argument( "--allowed-domains", type=str, nargs="*", help="Additional allowed domains to fetch documentation from. Use '*' to allow all domains.", ) parser.add_argument( "--timeout", type=float, default=10.0, help="HTTP request timeout in seconds" ) parser.add_argument( "--transport", type=str, default="stdio", choices=["stdio", "sse"], help="Transport protocol for MCP server", ) parser.add_argument( "--log-level", type=str, default="INFO", help=( "Log level for the server. Use one on the following: DEBUG, INFO, " "WARNING, ERROR." " (only used with --transport sse)" ), ) # SSE-specific options parser.add_argument( "--host", type=str, default="127.0.0.1", help="Host to bind the server to (only used with --transport sse)", ) parser.add_argument( "--port", type=int, default=8000, help="Port to bind the server to (only used with --transport sse)", ) # Version information parser.add_argument( "--version", "-V", action="version", version=f"mcpdoc {__version__}", help="Show version information and exit", ) return parser.parse_args() def load_config_file(file_path: str, file_format: str) -> List[Dict[str, str]]: """Load configuration from a file. Args: file_path: Path to the config file file_format: Format of the config file ("yaml" or "json") Returns: List of doc source configurations """ try: with open(file_path, "r", encoding="utf-8") as file: if file_format.lower() == "yaml": config = yaml.safe_load(file) elif file_format.lower() == "json": config = json.load(file) else: raise ValueError(f"Unsupported file format: {file_format}") if not isinstance(config, list): raise ValueError("Config file must contain a list of doc sources") return config except (FileNotFoundError, yaml.YAMLError, json.JSONDecodeError) as e: print(f"Error loading config file: {e}", file=sys.stderr) sys.exit(1) def create_doc_sources_from_urls(urls: List[str]) -> List[DocSource]: """Create doc sources from a list of URLs or file paths with optional names. Args: urls: List of documentation file URLs or file paths with optional names (format: 'url_or_path' or 'name:url_or_path') Returns: List of DocSource objects """ doc_sources = [] for entry in urls: if not entry.strip(): continue if ":" in entry and not entry.startswith(("http:", "https:")): # Format is name:url name, url = entry.split(":", 1) doc_sources.append({"name": name, "doc_file": url}) else: # Format is just url doc_sources.append({"doc_file": entry}) return doc_sources def main() -> None: """Main entry point for the CLI.""" # Custom exception hook to handle asyncio.CancelledError and KeyboardInterrupt gracefully original_excepthook = sys.excepthook def custom_excepthook(exc_type, exc_value, exc_traceback): # Suppress KeyboardInterrupt and asyncio.CancelledError if exc_type in (KeyboardInterrupt, asyncio.exceptions.CancelledError): print("\nShutting down gracefully... Goodbye!") return # For all other exceptions, use the original excepthook return original_excepthook(exc_type, exc_value, exc_traceback) # Set up signal handlers for graceful shutdown def signal_handler(sig, frame): print("\nShutting down gracefully... Goodbye!") sys.exit(0) # Register signal handlers for graceful shutdown signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) # Replace the default exception hook sys.excepthook = custom_excepthook # Check if --help or -h is in arguments if "--help" in sys.argv or "-h" in sys.argv: help_parser = argparse.ArgumentParser( prog="mcpdoc", description="MCP Documentation Server", formatter_class=CustomFormatter, epilog=EPILOG, ) # Add version to help parser too help_parser.add_argument( "--version", "-V", action="version", version=f"mcpdoc {__version__}", help="Show version information and exit", ) help_parser.print_help() sys.exit(0) args = parse_args() # Load doc sources based on command-line arguments doc_sources: List[DocSource] = [] # Check if any source options were provided if not (args.yaml or args.json or args.urls): print( "Error: At least one source option (--yaml, --json, or --urls) is required", file=sys.stderr, ) sys.exit(1) # Merge doc sources from all provided methods if args.yaml: doc_sources.extend(load_config_file(args.yaml, "yaml")) if args.json: doc_sources.extend(load_config_file(args.json, "json")) if args.urls: doc_sources.extend(create_doc_sources_from_urls(args.urls)) # Only used with SSE transport settings = { "host": args.host, "port": args.port, "log_level": "INFO", } # Create and run the server server = create_server( doc_sources, follow_redirects=args.follow_redirects, timeout=args.timeout, settings=settings, allowed_domains=args.allowed_domains, ) # Print startup banner and configuration info if args.transport == "sse": print(SPLASH) print("Starting MCP Documentation Server...") print(f"Version: {__version__}") print(f"Launching MCP-DOC server with {len(doc_sources)} doc sources") # Pass transport-specific options try: server.run(transport=args.transport) except KeyboardInterrupt: print("\nShutting down gracefully... Goodbye!") except Exception as e: print(f"\nError: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()

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/esakrissa/mcp-doc'

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