Skip to main content
Glama

Scribe MCP Server

by paxocial
repo_config.py11 kB
"""Repository discovery and configuration management for global Scribe deployment. This module enables Scribe to automatically detect the current repository root and load per-repository configuration, making it a true drop-in MCP solution. """ from __future__ import annotations import logging import os import yaml from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, Optional, Tuple # Setup structured logging for repository configuration operations repo_config_logger = logging.getLogger(__name__) @dataclass class RepoConfig: """Per-repository configuration for Scribe.""" # Core repository identification repo_slug: str repo_root: Path # Documentation structure dev_plans_dir: Path = field(default_factory=lambda: Path("docs/dev_plans")) progress_log_name: str = "PROGRESS_LOG.md" # Template and customization templates_pack: str = "default" custom_templates_dir: Optional[Path] = None # Permissions and constraints permissions: Dict[str, bool] = field(default_factory=dict) # Plugin configuration plugins_dir: Optional[Path] = None plugin_config: Dict[str, Any] = field(default_factory=dict) # Project defaults default_emoji: str = "📋" default_agent: str = "Agent" reminder_config: Dict[str, Any] = field(default_factory=dict) # Hooks configuration hooks: Dict[str, Optional[str]] = field(default_factory=dict) # Scribe MCP specific settings mcp_server_name: str = "scribe.mcp" storage_backend: str = "sqlite" # sqlite or postgres db_path: Optional[Path] = None # for sqlite @classmethod def from_dict(cls, data: Dict[str, Any], repo_root: Path) -> "RepoConfig": """Create RepoConfig from dictionary data.""" # Resolve path fields relative to repo root dev_plans_dir = repo_root / Path(data.get("dev_plans_dir", "docs/dev_plans")) custom_templates_dir = None if data.get("custom_templates_dir"): custom_templates_dir = repo_root / Path(data["custom_templates_dir"]) plugins_dir = None if data.get("plugins_dir"): plugins_dir = repo_root / Path(data["plugins_dir"]) db_path = None if data.get("db_path"): db_path = repo_root / Path(data["db_path"]) return cls( repo_slug=data.get("repo_slug", repo_root.name), repo_root=repo_root, dev_plans_dir=dev_plans_dir, progress_log_name=data.get("progress_log_name", "PROGRESS_LOG.md"), templates_pack=data.get("templates_pack", "default"), custom_templates_dir=custom_templates_dir, permissions=data.get("permissions", {}), plugins_dir=plugins_dir, plugin_config=data.get("plugin_config", {}), default_emoji=data.get("default_emoji", "📋"), default_agent=data.get("default_agent", "Agent"), reminder_config=data.get("reminder_config", {}), hooks=data.get("hooks", {}), mcp_server_name=data.get("mcp_server_name", "scribe.mcp"), storage_backend=data.get("storage_backend", "sqlite"), db_path=db_path, ) @classmethod def defaults_for_repo(cls, repo_root: Path) -> "RepoConfig": """Create default RepoConfig for a repository.""" return cls( repo_slug=repo_root.name, repo_root=repo_root, dev_plans_dir=repo_root / "docs/dev_plans", ) def to_dict(self) -> Dict[str, Any]: """Convert RepoConfig to dictionary for serialization.""" result = { "repo_slug": self.repo_slug, "repo_root": str(self.repo_root), "dev_plans_dir": str(self.dev_plans_dir.relative_to(self.repo_root)), "progress_log_name": self.progress_log_name, "templates_pack": self.templates_pack, "permissions": self.permissions, "plugin_config": self.plugin_config, "default_emoji": self.default_emoji, "default_agent": self.default_agent, "reminder_config": self.reminder_config, "hooks": self.hooks, "mcp_server_name": self.mcp_server_name, "storage_backend": self.storage_backend, } if self.custom_templates_dir: result["custom_templates_dir"] = str(self.custom_templates_dir.relative_to(self.repo_root)) if self.plugins_dir: result["plugins_dir"] = str(self.plugins_dir.relative_to(self.repo_root)) if self.db_path: result["db_path"] = str(self.db_path.relative_to(self.repo_root)) return result def get_progress_log_path(self, project_name: Optional[str] = None) -> Path: """Get the full path to the progress log for a project.""" if project_name: return self.dev_plans_dir / project_name / self.progress_log_name return self.dev_plans_dir / self.repo_slug / self.progress_log_name def get_project_docs_dir(self, project_name: str) -> Path: """Get the full path to a project's documentation directory.""" return self.dev_plans_dir / project_name class RepoDiscovery: """Repository discovery and configuration loading.""" @staticmethod def find_repo_root(start_path: Optional[Path] = None) -> Optional[Path]: """ Find the repository root by searching up from start_path. Looks for: - .git directory - .scribe directory (Scribe-specific marker) - pyproject.toml (Python project marker) - package.json (Node.js project marker) Args: start_path: Path to start searching from (defaults to current working directory) Returns: Repository root path or None if not found """ if start_path is None: start_path = Path.cwd() current = start_path.resolve() # Walk up the directory tree while current != current.parent: # Check for repository markers markers = [ ".git", ".scribe", # Scribe-specific marker "pyproject.toml", "package.json", "Cargo.toml", "go.mod", ] for marker in markers: if (current / marker).exists(): return current # Check for scribe config file directly if (current / ".scribe" / "scribe.yaml").exists(): return current current = current.parent # Check root directory as last resort for marker in [".git", ".scribe", "pyproject.toml", "package.json"]: if (current / marker).exists(): return current return None @staticmethod def load_config(repo_root: Path) -> RepoConfig: """ Load Scribe configuration for a repository. Search order: 1. .scribe/scribe.yaml 2. .scribe/scribe.yml 3. docs/dev_plans/scribe.yaml 4. .scribe/config.json 5. Create default config Args: repo_root: Repository root path Returns: Loaded or default RepoConfig """ config_paths = [ repo_root / ".scribe" / "scribe.yaml", repo_root / ".scribe" / "scribe.yml", repo_root / "docs" / "dev_plans" / "scribe.yaml", repo_root / ".scribe" / "config.json", ] for config_path in config_paths: if config_path.exists(): try: if config_path.suffix in ['.yaml', '.yml']: with open(config_path, 'r') as f: data = yaml.safe_load(f) or {} else: # JSON import json with open(config_path, 'r') as f: data = json.load(f) repo_config_logger.info(f"Successfully loaded config from {config_path}") return RepoConfig.from_dict(data, repo_root) except Exception as e: repo_config_logger.warning(f"Failed to load config from {config_path}: {e}") continue # No config found, return defaults return RepoConfig.defaults_for_repo(repo_root) @staticmethod def ensure_config(repo_root: Path, config: RepoConfig) -> None: """ Ensure Scribe configuration exists in repository. Creates .scribe directory and scribe.yaml if they don't exist. Args: repo_root: Repository root path config: Configuration to save """ scribe_dir = repo_root / ".scribe" scribe_dir.mkdir(parents=True, exist_ok=True) config_file = scribe_dir / "scribe.yaml" if not config_file.exists(): try: with open(config_file, 'w') as f: yaml.dump(config.to_dict(), f, default_flow_style=False, indent=2) repo_config_logger.info(f"Successfully created Scribe config at {config_file}") except Exception as e: repo_config_logger.error(f"Failed to create config file: {e}") raise @staticmethod def discover_or_create(start_path: Optional[Path] = None) -> Tuple[Path, RepoConfig]: """ Discover repository and load or create configuration. Args: start_path: Path to start discovery from (defaults to cwd) Returns: Tuple of (repo_root, config) Raises: RuntimeError: If no repository root can be found """ repo_root = RepoDiscovery.find_repo_root(start_path) if not repo_root: raise RuntimeError( f"Could not find repository root starting from {start_path or Path.cwd()}. " "Create a .git repository or add a .scribe directory to mark this as a project." ) config = RepoDiscovery.load_config(repo_root) # Ensure basic structure exists config.dev_plans_dir.mkdir(parents=True, exist_ok=True) return repo_root, config # Global cache for discovered configuration _current_repo_config: Optional[Tuple[Path, RepoConfig]] = None def get_current_repo_config(refresh: bool = False) -> Tuple[Path, RepoConfig]: """ Get the current repository configuration, with caching. Args: refresh: Force rediscovery even if cached Returns: Tuple of (repo_root, config) """ global _current_repo_config if refresh or _current_repo_config is None: _current_repo_config = RepoDiscovery.discover_or_create() return _current_repo_config def reload_repo_config() -> Tuple[Path, RepoConfig]: """Force reload of repository configuration.""" return get_current_repo_config(refresh=True)

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/paxocial/scribe_mcp'

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