Skip to main content
Glama

Quarkdown MCP Server

by Lillard01
preview.py20 kB
"""Preview server tool for Quarkdown MCP server. This module provides the preview_server tool that starts a local development server for previewing Quarkdown documents in real-time. """ import asyncio import os import tempfile from typing import Any, Dict, List from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource from .base import BaseTool class PreviewServerTool(BaseTool): """Tool for starting a preview server for Quarkdown documents. This tool creates a local development server that allows users to preview their Quarkdown documents in a web browser with live updates. """ def get_tool_definition(self) -> Tool: """Get the MCP tool definition for preview server. Returns: Tool definition object for MCP registration """ return Tool( name="preview_server", description="Start a local preview server for Quarkdown documents with live reload", inputSchema={ "type": "object", "properties": { "source_content": { "type": "string", "description": "The Quarkdown source content to preview" }, "port": { "type": "integer", "default": 8080, "minimum": 1024, "maximum": 65535, "description": "Port number for the preview server" }, "auto_reload": { "type": "boolean", "default": True, "description": "Enable automatic reload when source content changes" }, "theme": { "type": "string", "default": "default", "enum": ["default", "dark", "light", "academic", "minimal"], "description": "Theme for the preview interface" }, "watch_files": { "type": "array", "items": {"type": "string"}, "description": "Additional files to watch for changes (optional)" }, "open_browser": { "type": "boolean", "default": False, "description": "Automatically open the preview in default browser" } }, "required": ["source_content"] } ) async def execute(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent | EmbeddedResource]: """Execute the preview server tool. Args: arguments: Tool arguments from MCP client Returns: List of content items with execution results """ try: # Clean and validate parameters cleaned_kwargs = self._clean_and_validate_params(arguments) # Extract parameters with defaults source_content = cleaned_kwargs.get('source_content') port = cleaned_kwargs.get('port', 8080) auto_reload = cleaned_kwargs.get('auto_reload', True) theme = cleaned_kwargs.get('theme', 'default') open_browser = cleaned_kwargs.get('open_browser', False) # Validate required parameters if not source_content: return [TextContent( type="text", text="Error: Missing required parameter 'source_content'" )] # Validate parameter types and values validation_result = self._validate_parameters( source_content, port, auto_reload, theme, open_browser ) if not validation_result["valid"]: return [TextContent( type="text", text=f"Error: {validation_result['error']}" )] # Execute the preview server result = await self.wrapper.start_preview_server( content=source_content, port=port, auto_reload=auto_reload, theme=theme, open_browser=open_browser ) # Format response based on result if result.get('success'): success_message = f"Preview server started successfully!\n\n" success_message += f"🌐 URL: {result.get('url', f'http://localhost:{port}')}\n" success_message += f"📁 Compiled output: {result.get('compiled_output', 'N/A')}\n" success_message += f"🔧 Process ID: {result.get('process_id', 'N/A')}\n\n" if result.get('instructions'): success_message += "📋 Instructions:\n" for instruction in result['instructions']: success_message += f" • {instruction}\n" return [TextContent( type="text", text=success_message )] else: error_message = f"Failed to start preview server\n\n" error_message += f"❌ Error: {result.get('error', 'Unknown error')}\n" if result.get('return_code'): error_message += f"🔢 Return code: {result['return_code']}\n" if result.get('exception_type'): error_message += f"🐛 Exception type: {result['exception_type']}\n" return [TextContent( type="text", text=error_message )] except Exception as e: logger.error(f"Preview server execution failed: {e}", exc_info=True) return [TextContent( type="text", text=f"Unexpected error starting preview server: {str(e)}" )] def _clean_and_validate_params(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Clean and validate input parameters. Args: kwargs: Raw input parameters Returns: Cleaned parameters dictionary """ # Remove any unexpected parameters that might be added by MCP framework unwanted_params = ['source_file', 'input_file', 'file_path'] cleaned = {k: v for k, v in kwargs.items() if k not in unwanted_params} # Normalize parameter names if 'content' in cleaned and 'source_content' not in cleaned: cleaned['source_content'] = cleaned.pop('content') return cleaned def _validate_parameters(self, source_content: str, port: int, auto_reload: bool, theme: str, open_browser: bool) -> Dict[str, Any]: """Validate parameter types and values. Args: source_content: Document content port: Server port auto_reload: Auto-reload flag theme: Theme name open_browser: Open browser flag Returns: Validation result dictionary """ # Validate source_content if not isinstance(source_content, str): return {"valid": False, "error": "source_content must be a string"} if len(source_content.strip()) == 0: return {"valid": False, "error": "source_content cannot be empty"} # Validate port if not isinstance(port, int): try: port = int(port) except (ValueError, TypeError): return {"valid": False, "error": "port must be an integer"} if not (1024 <= port <= 65535): return {"valid": False, "error": "port must be between 1024 and 65535"} # Validate boolean parameters if not isinstance(auto_reload, bool): return {"valid": False, "error": "auto_reload must be a boolean"} if not isinstance(open_browser, bool): return {"valid": False, "error": "open_browser must be a boolean"} # Validate theme valid_themes = ['default', 'dark', 'light', 'academic', 'minimal'] if theme not in valid_themes: return {"valid": False, "error": f"theme must be one of: {', '.join(valid_themes)}"} return {"valid": True} def _get_current_timestamp(self) -> str: """Get current timestamp in ISO format. Returns: ISO formatted timestamp string """ from datetime import datetime return datetime.now().isoformat() async def execute(self, arguments: Dict[str, Any]) -> List[TextContent | ImageContent | EmbeddedResource]: """Execute the preview server startup. Args: arguments: Tool execution arguments containing source content and server options Returns: List containing the server startup results and access information """ try: # Debug: Print all received arguments print(f"DEBUG preview.py: execute called with arguments:") print(f" arguments keys: {list(arguments.keys())}") print(f" arguments: {arguments}") # Remove any unwanted parameters that might be added by MCP framework if 'source_file' in arguments: print(f"DEBUG: Removing unwanted 'source_file' parameter: {arguments['source_file']}") arguments = {k: v for k, v in arguments.items() if k != 'source_file'} print(f"DEBUG: Filtered arguments: {arguments}") # Validate required arguments self._validate_required_args(arguments, ["source_content"]) source_content = arguments["source_content"] port = arguments.get("port", 8080) auto_reload = arguments.get("auto_reload", True) theme = arguments.get("theme", "default") watch_files = arguments.get("watch_files", []) open_browser = arguments.get("open_browser", False) # Create temporary directory for preview files temp_dir = tempfile.mkdtemp(prefix="quarkdown_preview_") source_file = os.path.join(temp_dir, "document.qmd") # Write source content to temporary file with open(source_file, 'w', encoding='utf-8') as f: f.write(source_content) # Check if port is available if not await self._is_port_available(port): # Try to find an available port port = await self._find_available_port(port) # Debug: Print the actual arguments being passed print(f"DEBUG: About to call start_preview_server with:") print(f" content type: {type(source_content)}") print(f" content length: {len(source_content)}") print(f" port: {port}") print(f" port type: {type(port)}") # Start the preview server try: # Filter out any unwanted parameters that might be passed by MCP framework server_params = { 'content': source_content, 'port': port } print(f"DEBUG: Calling start_preview_server with filtered params: {server_params}") server_result = await self.wrapper.start_preview_server(**server_params) except Exception as e: print(f"DEBUG: Exception details: {e}") print(f"DEBUG: Exception type: {type(e)}") import traceback print(f"DEBUG: Traceback: {traceback.format_exc()}") raise # Debug: Log the server result for troubleshooting logger.debug(f"Preview server result: {server_result}") # Check if server_result is None or empty if server_result is None: return [ self._create_error_content( "Preview server returned null result. This may indicate a configuration issue with the Quarkdown JAR file or Java environment." ) ] # Check if server started successfully if server_result.get("success"): server_url = server_result.get("url", f"http://localhost:{port}") # Optionally open browser if open_browser: await self._open_browser(server_url) response_parts = [ self._create_success_content( f"Preview server started successfully on port {port}" ), self._create_text_content( f"**Server Information**:\n" f"- **URL**: {server_url}\n" f"- **Port**: {port}\n" f"- **Auto-reload**: {'Enabled' if auto_reload else 'Disabled'}\n" f"- **Theme**: {theme}\n" f"- **Source content**: Provided content" ) ] # Add watch files information if any if watch_files: watch_info = "\n".join([f" - {file}" for file in watch_files]) response_parts.append(self._create_text_content( f"**Watching additional files**:\n{watch_info}" )) return response_parts else: error_msg = server_result.get('error', 'Unknown error') if error_msg == 'Unknown error' and not server_result.get('success'): error_msg = "Failed to start preview server. Please check that Java is installed and the Quarkdown JAR file is accessible." return [ self._create_error_content( f"Failed to start preview server: {error_msg}" ) ] except Exception as e: return [self._create_error_content(f"Error starting preview server: {str(e)}")] async def _is_port_available(self, port: int) -> bool: """Check if a port is available for use. Args: port: Port number to check Returns: True if port is available """ import socket try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(('localhost', port)) return True except OSError: return False async def _find_available_port(self, start_port: int) -> int: """Find an available port starting from the given port. Args: start_port: Starting port number Returns: Available port number """ for port in range(start_port, start_port + 100): if await self._is_port_available(port): return port # If no port found in range, try random high port import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(('localhost', 0)) return sock.getsockname()[1] async def _open_browser(self, url: str) -> None: """Open the preview URL in the default browser. Args: url: URL to open """ import webbrowser try: webbrowser.open(url) except Exception: # Silently fail if browser can't be opened pass def _generate_usage_instructions(self, server_url: str, temp_dir: str) -> str: """Generate usage instructions for the preview server. Args: server_url: Server URL temp_dir: Temporary directory path Returns: Formatted instructions string """ instructions = [ f"1. **Open your browser** and navigate to: {server_url}", "2. **Edit your document** by modifying the source content", "3. **Save changes** to see live updates (if auto-reload is enabled)", f"4. **Source file location**: {temp_dir}", "5. **Stop the server** using the management commands below" ] return "\n".join(instructions) def _generate_management_info(self, process_id: int) -> str: """Generate server management information. Args: process_id: Server process ID Returns: Formatted management information """ management_info = [ f"- **Process ID**: {process_id}", f"- **Stop server**: `kill {process_id}` (Unix/macOS) or use Task Manager (Windows)", "- **Check status**: Use `ps aux | grep quarkdown` (Unix/macOS)", "- **View logs**: Check the terminal where the server was started", "- **Restart**: Stop the current server and run this tool again" ] return "\n".join(management_info) async def stop_server(self, process_id: int) -> bool: """Stop a running preview server. Args: process_id: Process ID of the server to stop Returns: True if server was stopped successfully """ try: import os import signal # Send SIGTERM to gracefully stop the server os.kill(process_id, signal.SIGTERM) # Wait a moment for graceful shutdown await asyncio.sleep(1) # Check if process is still running try: os.kill(process_id, 0) # Check if process exists # If we reach here, process is still running, force kill os.kill(process_id, signal.SIGKILL) except ProcessLookupError: # Process already terminated pass return True except Exception: return False async def list_running_servers(self) -> List[Dict[str, Any]]: """List all running Quarkdown preview servers. Returns: List of server information dictionaries """ try: import psutil servers = [] for proc in psutil.process_iter(['pid', 'name', 'cmdline']): try: if 'quarkdown' in proc.info['name'].lower(): cmdline = ' '.join(proc.info['cmdline']) if 'preview' in cmdline or 'server' in cmdline: servers.append({ 'pid': proc.info['pid'], 'name': proc.info['name'], 'cmdline': cmdline }) except (psutil.NoSuchProcess, psutil.AccessDenied): continue return servers except ImportError: # psutil not available, return empty list return [] except Exception: return []

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/Lillard01/quarkdown-mcp'

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