Skip to main content
Glama
cli.py19.1 kB
"""CLI interface for the Typst MCP Server.""" import pathlib import anyio import click import uvicorn from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from mcp.server.stdio import stdio_server from starlette.applications import Starlette from starlette.routing import Mount from .server import TypstDocumentationServer TYPST_VERSION = "v0.13.1" def get_default_docs_path() -> pathlib.Path: """Get the default documentation path based on TYPST_VERSION.""" return pathlib.Path(__file__).parent.parent.parent / TYPST_VERSION @click.group() @click.version_option() def cli() -> None: """Typst MCP Server - A Model Context Protocol server for Typst documentation. This CLI provides commands to interact with Typst documentation, including search, browse, and read operations. """ pass @cli.command() @click.argument("transport", type=click.Choice(["stdio", "http"]), default="stdio") @click.option( "--port", type=int, default=8000, help="Port to bind the HTTP server to (default: 8000, only used with 'http' transport)", ) @click.option( "--host", default="127.0.0.1", help="Host to bind the HTTP server to (default: 127.0.0.1, only used with 'http' transport)", ) @click.option( "--docs-path", type=click.Path( exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path ), help=f"Path to Typst documentation directory (default: {TYPST_VERSION} in project root)", ) @click.option( "--debug", is_flag=True, help="Enable debug mode with additional logging", ) def serve( transport: str, port: int, host: str, docs_path: pathlib.Path | None, debug: bool, ) -> None: """Start the MCP server with specified transport. TRANSPORT: Choose between 'stdio' (default, MCP standard) or 'http' (Streamable HTTP) - stdio: Standard MCP communication via stdin/stdout - http: HTTP server with Streamable HTTP for web-based clients """ if debug: click.echo("Debug mode enabled") actual_docs_path = docs_path if docs_path else get_default_docs_path() server_instance = TypstDocumentationServer(actual_docs_path) if docs_path: click.echo(f"Using custom documentation path: {docs_path}") else: click.echo(f"Using default documentation path: {TYPST_VERSION}/") click.echo("\nAvailable tools:") click.echo(" • typst_search - Search through documentation") click.echo(" • typst_browse - Browse directory structure") click.echo(" • typst_read - Read specific documentation files") if transport == "http": click.echo(f"\nStarting HTTP server on http://{host}:{port}") click.echo("Available endpoints:") click.echo(f" • HTTP http://{host}:{port}/mcp - MCP over HTTP endpoint") session_manager = StreamableHTTPSessionManager( app=server_instance.server, event_store=None, json_response=True, stateless=True, ) async def handle_streamable_http(scope, receive, send): await session_manager.handle_request(scope, receive, send) starlette_app = Starlette( debug=debug, routes=[ Mount("/mcp", app=handle_streamable_http), ], ) uvicorn.run(starlette_app, host=host, port=port) else: # stdio click.echo("\nStarting Typst MCP Server...") click.echo("Server will communicate via stdio (MCP standard)") click.echo("Server ready. Waiting for MCP client connection...") async def async_main() -> None: async with stdio_server() as (read_stream, write_stream): await server_instance.server.run( read_stream, write_stream, server_instance.server.create_initialization_options(), ) anyio.run(async_main) @click.group() def tools() -> None: """MCP tools for interacting with Typst documentation.""" pass @tools.command("list") @click.option( "--format", "output_format", type=click.Choice(["text", "json", "table"]), default="table", help="Output format (default: table)", ) @click.option( "--verbose", "-v", is_flag=True, help="Show detailed information about each tool including schemas and examples", ) def list_tools(output_format: str, verbose: bool) -> None: """List all available MCP tools provided by the server. Shows the tools that can be used by MCP clients when connecting to this server, including their descriptions, input/output schemas, and usage examples. """ async def run_tools() -> None: # Get tools from the server (simulate the list_tools call) tools_data = [ { "name": "typst_search", "description": f"Search through Typst {TYPST_VERSION} documentation for specific topics, functions, or syntax", "input_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "Search term or phrase to find in Typst documentation", } }, "required": ["query"], }, "output_schema": { "type": "object", "description": "Returns search results with file locations and contextual matches", "properties": { "type": {"type": "string", "enum": ["text"]}, "text": { "type": "string", "description": "Formatted search results with file names, line numbers, and context", }, }, }, "examples": [ { "description": "Search for function definitions", "input": {"query": "function"}, "usage": "Find all function-related documentation", }, { "description": "Search for specific syntax", "input": {"query": "let variable"}, "usage": "Find variable declaration syntax", }, ], }, { "name": "typst_browse", "description": "Browse the Typst documentation structure as a hierarchical tree", "input_schema": { "type": "object", "properties": { "depth": { "type": "integer", "description": "Maximum depth to traverse (default: 0 for full depth)", "default": 0, }, "sub_directory": { "type": "string", "description": "Subdirectory to explore (default: '.' for root)", "default": ".", }, }, }, "output_schema": { "type": "object", "description": "Returns hierarchical directory structure with files and folders", "properties": { "type": {"type": "string", "enum": ["text"]}, "text": { "type": "string", "description": "Tree-structured directory listing with emojis for files and folders", }, }, }, "examples": [ { "description": "Browse root directory", "input": {}, "usage": "Get overview of entire documentation structure", }, { "description": "Browse specific subdirectory with depth limit", "input": {"sub_directory": "reference", "depth": 2}, "usage": "Explore reference section with limited depth", }, ], }, { "name": "typst_read", "description": "Read the content of a specific Typst documentation file", "input_schema": { "type": "object", "properties": { "path": { "type": "string", "description": "Relative path to the documentation file", } }, "required": ["path"], }, "output_schema": { "type": "object", "description": "Returns the complete content of the specified documentation file", "properties": { "type": {"type": "string", "enum": ["text"]}, "text": { "type": "string", "description": "Full file content with filename header", }, }, }, "examples": [ { "description": "Read a specific function documentation", "input": {"path": "reference/foundations/calc.md"}, "usage": "Get detailed documentation for calc module", }, { "description": "Read tutorial file", "input": {"path": "tutorial/writing-in-typst.md"}, "usage": "Learn about writing content in Typst", }, ], }, ] if output_format == "json": import json click.echo(json.dumps(tools_data, indent=2)) elif output_format == "table": click.echo("Available MCP Tools") click.echo("=" * 80) click.echo() for tool in tools_data: click.echo(f"🔧 {tool['name']}") click.echo(f" {tool['description']}") click.echo() if verbose: # Input Schema click.echo(" 📥 Input Schema:") props = tool["input_schema"].get("properties", {}) required = tool["input_schema"].get("required", []) if props: for param_name, param_info in props.items(): required_mark = ( " (required)" if param_name in required else " (optional)" ) param_type = param_info.get("type", "unknown") param_desc = param_info.get("description", "No description") default = param_info.get("default") default_text = ( f" [default: {default}]" if default is not None else "" ) click.echo( f" • {param_name} ({param_type}){required_mark}: {param_desc}{default_text}" ) else: click.echo(" No parameters required") click.echo() # Output Schema if "output_schema" in tool: click.echo(" 📤 Output Schema:") output_desc = tool["output_schema"].get( "description", "No description" ) click.echo(f" {output_desc}") output_props = tool["output_schema"].get("properties", {}) if output_props: for prop_name, prop_info in output_props.items(): prop_desc = prop_info.get( "description", "No description" ) click.echo(f" • {prop_name}: {prop_desc}") click.echo() # Examples if "examples" in tool: click.echo(" 💡 Usage Examples:") for i, example in enumerate(tool["examples"], 1): click.echo(f" {i}. {example['description']}") click.echo(f" Input: {example['input']}") click.echo(f" Usage: {example['usage']}") if i < len(tool["examples"]): click.echo() click.echo() click.echo("-" * 80) click.echo() else: # text format click.echo("Available MCP Tools:") click.echo() for tool in tools_data: click.echo(f"• {tool['name']}: {tool['description']}") if verbose: # Parameters props = tool["input_schema"].get("properties", {}) required = tool["input_schema"].get("required", []) if props: click.echo(" Parameters:") for param_name, param_info in props.items(): required_mark = ( " (required)" if param_name in required else " (optional)" ) param_desc = param_info.get("description", "No description") default = param_info.get("default") default_text = ( f" [default: {default}]" if default is not None else "" ) click.echo( f" - {param_name}{required_mark}: {param_desc}{default_text}" ) # Examples if "examples" in tool: click.echo(" Examples:") for example in tool["examples"]: click.echo( f" - {example['description']}: {example['usage']}" ) click.echo(f" Input: {example['input']}") click.echo() anyio.run(run_tools) def _get_server_instance(docs_path: pathlib.Path | None) -> TypstDocumentationServer: return TypstDocumentationServer(docs_path if docs_path else get_default_docs_path()) @tools.command("typst_search") @click.argument("query", required=True) @click.option( "--docs-path", type=click.Path( exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path ), help=f"Path to Typst documentation directory (default: {TYPST_VERSION} in project root)", ) def search(query: str, docs_path: pathlib.Path | None) -> None: """Search through Typst documentation for specific topics, functions, or syntax. QUERY: The search term or phrase to find in the documentation. """ async def run_search() -> None: server = _get_server_instance(docs_path) results = await server._handle_search(query) for result in results: click.echo(result.text) anyio.run(run_search) @tools.command("typst_browse") @click.option( "--depth", type=int, default=0, help="Maximum depth to traverse (0 for unlimited, default: 0)", ) @click.option( "--sub-directory", "--dir", default=".", help="Subdirectory to explore (default: root)", ) @click.option( "--docs-path", type=click.Path( exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path ), help=f"Path to Typst documentation directory (default: {TYPST_VERSION} in project root)", ) def browse(depth: int, sub_directory: str, docs_path: pathlib.Path | None) -> None: """Browse the Typst documentation structure as a hierarchical tree.""" async def run_browse() -> None: server = _get_server_instance(docs_path) results = await server._handle_browse(depth, sub_directory) for result in results: click.echo(result.text) anyio.run(run_browse) @tools.command("typst_read") @click.argument("path", required=True) @click.option( "--docs-path", type=click.Path( exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path ), help=f"Path to Typst documentation directory (default: {TYPST_VERSION} in project root)", ) def read(path: str, docs_path: pathlib.Path | None) -> None: """Read the content of a specific Typst documentation file. PATH: Relative path to the documentation file (e.g., 'reference/library/foundations/calc.md'). """ async def run_read() -> None: server = _get_server_instance(docs_path) results = await server._handle_read(path) for result in results: click.echo(result.text) anyio.run(run_read) @cli.command() @click.argument("pattern", required=True) @click.option( "--docs-path", type=click.Path( exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path ), help=f"Path to Typst documentation directory (default: {TYPST_VERSION} in project root)", ) def grep(pattern: str, docs_path: pathlib.Path | None) -> None: """Search for text patterns in documentation files.""" server = _get_server_instance(docs_path) files = server._find_markdown_files() total_matches = 0 for file_path in files: try: content = file_path.read_text(encoding="utf-8") lines = content.split("\n") matches = [] for i, line in enumerate(lines): if pattern.lower() in line.lower(): matches.append((i + 1, line)) if matches: relative_path = file_path.relative_to(server.docs_path) click.echo(f"📄 {relative_path}") for line_num, line in matches: click.echo(f"Line {line_num}: {line}") click.echo() total_matches += len(matches) except Exception: continue click.echo(f"Total matches: {total_matches}") # Add the tools group to the main CLI cli.add_command(tools) def main() -> None: """Entry point for the CLI application.""" cli() 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/FujishigeTemma/typst-mcp'

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