Skip to main content
Glama

Mode Manager MCP

chatmode_manager.py13.2 kB
""" Mode Manager for VS Code .chatmode.md files. This module handles chatmode files which define custom chat behaviors, tools, and instructions for VS Code Copilot. """ import json import logging import urllib.error import urllib.request from pathlib import Path from typing import Any, Dict, List, Optional, Union from .path_utils import get_vscode_prompts_directory from .simple_file_ops import ( FileOperationError, parse_frontmatter_file, safe_delete_file, write_frontmatter_file, ) logger = logging.getLogger(__name__) class ChatModeManager: """Manages VS Code .chatmode.md files in the prompts directory.""" def __init__(self, prompts_dir: Optional[Union[str, Path]] = None): """ Initialize chatmode manager. Args: prompts_dir: Custom prompts directory (default: VS Code user dir + prompts) """ if prompts_dir: self.prompts_dir = Path(prompts_dir) else: self.prompts_dir = get_vscode_prompts_directory() # Ensure prompts directory exists self.prompts_dir.mkdir(parents=True, exist_ok=True) logger.info(f"ChatMode manager initialized with prompts directory: {self.prompts_dir}") def list_chatmodes(self) -> List[Dict[str, Any]]: """ List all .chatmode.md files in the prompts directory. Returns: List of chatmode file information """ chatmodes: List[Dict[str, Any]] = [] if not self.prompts_dir.exists(): return chatmodes for file_path in self.prompts_dir.glob("*.chatmode.md"): try: frontmatter, content = parse_frontmatter_file(file_path) # Get preview of content (first 100 chars) content_preview = content.strip()[:100] if content.strip() else "" chatmode_info = { "filename": file_path.name, "name": file_path.stem.replace(".chatmode", ""), "path": str(file_path), "description": frontmatter.get("description", ""), "tools": frontmatter.get("tools", []), "frontmatter": frontmatter, "content_preview": content_preview, "size": file_path.stat().st_size, "modified": file_path.stat().st_mtime, } chatmodes.append(chatmode_info) except Exception as e: logger.warning(f"Error reading chatmode file {file_path}: {e}") continue # Sort by name chatmodes.sort(key=lambda x: x["name"].lower()) return chatmodes def get_chatmode(self, filename: str) -> Dict[str, Any]: """ Get content and metadata of a specific chatmode file. Args: filename: Name of the .chatmode.md file Returns: Chatmode data including frontmatter and content Raises: FileOperationError: If file cannot be read """ # Ensure filename has correct extension if not filename.endswith(".chatmode.md"): filename += ".chatmode.md" file_path = self.prompts_dir / filename if not file_path.exists(): raise FileOperationError(f"Chatmode file not found: {filename}") try: frontmatter, content = parse_frontmatter_file(file_path) return { "filename": filename, "name": file_path.stem.replace(".chatmode", ""), "path": str(file_path), "description": frontmatter.get("description", ""), "tools": frontmatter.get("tools", []), "frontmatter": frontmatter, "content": content, "size": file_path.stat().st_size, "modified": file_path.stat().st_mtime, } except Exception as e: raise FileOperationError(f"Error reading chatmode file {filename}: {e}") def get_raw_chatmode(self, filename: str) -> str: """ Get the raw file content of a specific chatmode file without any processing. Args: filename: Name of the .chatmode.md file Returns: Raw file content as string Raises: FileOperationError: If file cannot be read """ # Ensure filename has correct extension if not filename.endswith(".chatmode.md"): filename += ".chatmode.md" file_path = self.prompts_dir / filename if not file_path.exists(): raise FileOperationError(f"Chatmode file not found: {filename}") try: with open(file_path, "r", encoding="utf-8") as f: return f.read() except Exception as e: raise FileOperationError(f"Error reading raw chatmode file {filename}: {e}") def create_chatmode( self, filename: str, description: str, content: str, tools: Optional[List[str]] = None, ) -> bool: """ Create a new chatmode file. Args: filename: Name for the new .chatmode.md file description: Description of the chatmode content: Chatmode content/instructions tools: List of tools (optional) Returns: True if successful Raises: FileOperationError: If file cannot be created """ # Ensure filename has correct extension if not filename.endswith(".chatmode.md"): filename += ".chatmode.md" file_path = self.prompts_dir / filename if file_path.exists(): raise FileOperationError(f"Chatmode file already exists: {filename}") # Create frontmatter frontmatter: Dict[str, Any] = {"description": description} if tools: frontmatter["tools"] = tools try: success = write_frontmatter_file(file_path, frontmatter, content, create_backup=False) if success: logger.info(f"Created chatmode file: {filename}") return success except Exception as e: raise FileOperationError(f"Error creating chatmode file {filename}: {e}") def update_chatmode( self, filename: str, frontmatter: Optional[Dict[str, Any]] = None, content: Optional[str] = None, ) -> bool: """ Update an existing chatmode file. Args: filename: Name of the .chatmode.md file frontmatter: New frontmatter (optional) content: New content (optional) Returns: True if successful Raises: FileOperationError: If file cannot be updated """ # Ensure filename has correct extension if not filename.endswith(".chatmode.md"): filename += ".chatmode.md" file_path = self.prompts_dir / filename if not file_path.exists(): raise FileOperationError(f"Chatmode file not found: {filename}") try: # Read current content current_frontmatter, current_content = parse_frontmatter_file(file_path) # Use provided values or keep current ones new_frontmatter = frontmatter if frontmatter is not None else current_frontmatter new_content = content if content is not None else current_content success = write_frontmatter_file(file_path, new_frontmatter, new_content, create_backup=True) if success: logger.info(f"Updated chatmode file with backup: {filename}") return success except Exception as e: raise FileOperationError(f"Error updating chatmode file {filename}: {e}") def delete_chatmode(self, filename: str) -> bool: """ Delete a chatmode file with automatic backup. Args: filename: Name of the .chatmode.md file Returns: True if successful Raises: FileOperationError: If file cannot be deleted """ # Ensure filename has correct extension if not filename.endswith(".chatmode.md"): filename += ".chatmode.md" file_path = self.prompts_dir / filename if not file_path.exists(): raise FileOperationError(f"Chatmode file not found: {filename}") try: # Use safe delete which creates backup automatically safe_delete_file(file_path, create_backup=True) logger.info(f"Deleted chatmode file with backup: {filename}") return True except Exception as e: raise FileOperationError(f"Error deleting chatmode file {filename}: {e}") def update_from_source(self, filename: str) -> Dict[str, Any]: """ Update a chatmode file from its source URL. This method fetches the latest version from the source_url in the frontmatter, while preserving any local tool customizations. Args: filename: Name of the .chatmode.md file Returns: Dict with update status and details Raises: FileOperationError: If file cannot be updated """ # Ensure filename has correct extension if not filename.endswith(".chatmode.md"): filename += ".chatmode.md" file_path = self.prompts_dir / filename if not file_path.exists(): raise FileOperationError(f"Chatmode file not found: {filename}") try: # Read current file current_frontmatter, current_content = parse_frontmatter_file(file_path) # Check if source_url exists source_url = current_frontmatter.get("source_url") if not source_url: raise FileOperationError(f"No source_url found in {filename}") # Fetch content from source logger.info(f"Fetching chatmode update from: {source_url}") try: with urllib.request.urlopen(source_url) as response: raw_content = response.read() # Try to decode with utf-8, fallback to other encodings if needed try: source_content = raw_content.decode("utf-8") except UnicodeDecodeError: try: source_content = raw_content.decode("cp1252") # Windows encoding except UnicodeDecodeError: source_content = raw_content.decode("latin1") # Fallback except urllib.error.URLError as e: raise FileOperationError(f"Failed to fetch from {source_url}: {e}") # Parse source content try: # For GitHub Gist raw URLs, the content is just the file content if "gist.github.com" in source_url or "gist.githubusercontent.com" in source_url: # Parse the source content as a frontmatter file import tempfile # Write to temp file to parse frontmatter with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False, encoding="utf-8") as temp_file: temp_file.write(source_content) temp_path = Path(temp_file.name) try: source_frontmatter, source_body = parse_frontmatter_file(temp_path) finally: temp_path.unlink() # Clean up temp file else: # For other sources, assume it's already in the right format raise FileOperationError(f"Unsupported source URL format: {source_url}") except Exception as e: raise FileOperationError(f"Failed to parse source content: {e}") # Preserve local tool settings if they exist local_tools = current_frontmatter.get("tools") # Create updated frontmatter by merging source with local customizations updated_frontmatter = source_frontmatter.copy() updated_frontmatter["source_url"] = source_url # Ensure source_url is preserved if local_tools: updated_frontmatter["tools"] = local_tools logger.info(f"Preserved local tools setting: {local_tools}") # Write updated file with backup write_frontmatter_file(file_path, updated_frontmatter, source_body, create_backup=True) logger.info(f"Successfully updated chatmode from source with backup: {filename}") return { "status": "success", "filename": filename, "source_url": source_url, "preserved_tools": local_tools is not None, "message": f"Updated {filename} from source", } except FileOperationError: raise except Exception as e: raise FileOperationError(f"Error updating chatmode from source {filename}: {e}")

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/NiclasOlofsson/mode-manager-mcp'

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