diff --git a/mcp-servers/viz-platform/mcp_server/__init__.py b/mcp-servers/viz-platform/mcp_server/__init__.py new file mode 100644 index 0000000..46123ee --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/__init__.py @@ -0,0 +1,7 @@ +""" +viz-platform MCP Server package. + +Provides Dash Mantine Components validation and visualization tools to Claude Code. +""" + +__version__ = "1.0.0" diff --git a/mcp-servers/viz-platform/mcp_server/config.py b/mcp-servers/viz-platform/mcp_server/config.py new file mode 100644 index 0000000..e552d02 --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/config.py @@ -0,0 +1,172 @@ +""" +Configuration loader for viz-platform MCP Server. + +Implements hybrid configuration system: +- System-level: ~/.config/claude/viz-platform.env (theme preferences) +- Project-level: .env (DMC version overrides) +- Auto-detection: DMC package version from installed package +""" +from pathlib import Path +from dotenv import load_dotenv +import os +import logging +from typing import Dict, Optional + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class VizPlatformConfig: + """Hybrid configuration loader for viz-platform tools""" + + def __init__(self): + self.dmc_version: Optional[str] = None + self.theme_dir_user: Path = Path.home() / '.config' / 'claude' / 'themes' + self.theme_dir_project: Optional[Path] = None + self.default_theme: Optional[str] = None + + def load(self) -> Dict[str, any]: + """ + Load configuration from system and project levels. + + Returns: + Dict containing dmc_version, theme directories, and availability flags + """ + # Load system config + system_config = Path.home() / '.config' / 'claude' / 'viz-platform.env' + if system_config.exists(): + load_dotenv(system_config) + logger.info(f"Loaded system configuration from {system_config}") + + # Find project directory + project_dir = self._find_project_directory() + + # Load project config (overrides system) + if project_dir: + project_config = project_dir / '.env' + if project_config.exists(): + load_dotenv(project_config, override=True) + logger.info(f"Loaded project configuration from {project_config}") + + # Set project theme directory + self.theme_dir_project = project_dir / '.viz-platform' / 'themes' + + # Get DMC version (from env or auto-detect) + self.dmc_version = os.getenv('DMC_VERSION') or self._detect_dmc_version() + self.default_theme = os.getenv('VIZ_DEFAULT_THEME') + + # Ensure user theme directory exists + self.theme_dir_user.mkdir(parents=True, exist_ok=True) + + return { + 'dmc_version': self.dmc_version, + 'dmc_available': self.dmc_version is not None, + 'theme_dir_user': str(self.theme_dir_user), + 'theme_dir_project': str(self.theme_dir_project) if self.theme_dir_project else None, + 'default_theme': self.default_theme, + 'project_dir': str(project_dir) if project_dir else None + } + + def _detect_dmc_version(self) -> Optional[str]: + """ + Auto-detect installed Dash Mantine Components version. + + Returns: + Version string (e.g., "0.14.7") or None if not installed + """ + try: + from importlib.metadata import version + dmc_version = version('dash-mantine-components') + logger.info(f"Detected DMC version: {dmc_version}") + return dmc_version + except ImportError: + logger.warning("dash-mantine-components not installed - using registry fallback") + return None + except Exception as e: + logger.warning(f"Could not detect DMC version: {e}") + return None + + def _find_project_directory(self) -> Optional[Path]: + """ + Find the user's project directory. + + Returns: + Path to project directory, or None if not found + """ + # Strategy 1: Check CLAUDE_PROJECT_DIR environment variable + project_dir = os.getenv('CLAUDE_PROJECT_DIR') + if project_dir: + path = Path(project_dir) + if path.exists(): + logger.info(f"Found project directory from CLAUDE_PROJECT_DIR: {path}") + return path + + # Strategy 2: Check PWD + pwd = os.getenv('PWD') + if pwd: + path = Path(pwd) + if path.exists() and ( + (path / '.git').exists() or + (path / '.env').exists() or + (path / '.viz-platform').exists() + ): + logger.info(f"Found project directory from PWD: {path}") + return path + + # Strategy 3: Check current working directory + cwd = Path.cwd() + if (cwd / '.git').exists() or (cwd / '.env').exists() or (cwd / '.viz-platform').exists(): + logger.info(f"Found project directory from cwd: {cwd}") + return cwd + + logger.debug("Could not determine project directory") + return None + + +def load_config() -> Dict[str, any]: + """ + Convenience function to load configuration. + + Returns: + Configuration dictionary + """ + config = VizPlatformConfig() + return config.load() + + +def check_dmc_version() -> Dict[str, any]: + """ + Check DMC installation status for SessionStart hook. + + Returns: + Dict with installation status and version info + """ + config = load_config() + + if not config.get('dmc_available'): + return { + 'installed': False, + 'message': 'dash-mantine-components not installed. Run: pip install dash-mantine-components' + } + + version = config.get('dmc_version', 'unknown') + + # Check for registry compatibility + registry_path = Path(__file__).parent.parent / 'registry' + major_minor = '.'.join(version.split('.')[:2]) if version else None + registry_file = registry_path / f'dmc_{major_minor.replace(".", "_")}.json' if major_minor else None + + if registry_file and registry_file.exists(): + return { + 'installed': True, + 'version': version, + 'registry_available': True, + 'message': f'DMC {version} ready with component registry' + } + else: + return { + 'installed': True, + 'version': version, + 'registry_available': False, + 'message': f'DMC {version} installed but no matching registry. Validation may be limited.' + } diff --git a/mcp-servers/viz-platform/mcp_server/dmc_tools.py b/mcp-servers/viz-platform/mcp_server/dmc_tools.py new file mode 100644 index 0000000..ca8f589 --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/dmc_tools.py @@ -0,0 +1,72 @@ +""" +DMC (Dash Mantine Components) validation tools. + +Provides component constraint layer to prevent Claude from hallucinating invalid props. +Tools implemented in Issue #172. +""" +import logging +from typing import Dict, List, Optional, Any + +logger = logging.getLogger(__name__) + + +class DMCTools: + """ + DMC component validation tools. + + These tools provide the "constraint layer" that validates component usage + against a version-locked registry of DMC components. + """ + + def __init__(self, registry=None): + """ + Initialize DMC tools with component registry. + + Args: + registry: ComponentRegistry instance (from Issue #171) + """ + self.registry = registry + + async def list_components(self, category: Optional[str] = None) -> Dict[str, Any]: + """ + List available DMC components, optionally filtered by category. + + Args: + category: Optional category filter (e.g., "inputs", "buttons", "navigation") + + Returns: + Dict with components grouped by category + """ + # Implementation in Issue #172 + raise NotImplementedError("Implemented in Issue #172") + + async def get_component_props(self, component: str) -> Dict[str, Any]: + """ + Get props schema for a specific component. + + Args: + component: Component name (e.g., "Button", "TextInput") + + Returns: + Dict with props, types, defaults, and enum values + """ + # Implementation in Issue #172 + raise NotImplementedError("Implemented in Issue #172") + + async def validate_component( + self, + component: str, + props: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Validate component props against registry. + + Args: + component: Component name + props: Props to validate + + Returns: + Dict with valid: bool, errors: [], warnings: [] + """ + # Implementation in Issue #172 + raise NotImplementedError("Implemented in Issue #172") diff --git a/mcp-servers/viz-platform/mcp_server/server.py b/mcp-servers/viz-platform/mcp_server/server.py new file mode 100644 index 0000000..778440a --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/server.py @@ -0,0 +1,129 @@ +""" +MCP Server entry point for viz-platform integration. + +Provides Dash Mantine Components validation, charting, layout, theming, and page tools +to Claude Code via JSON-RPC 2.0 over stdio. +""" +import asyncio +import logging +import json +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import Tool, TextContent + +from .config import VizPlatformConfig + +# Suppress noisy MCP validation warnings on stderr +logging.basicConfig(level=logging.INFO) +logging.getLogger("root").setLevel(logging.ERROR) +logging.getLogger("mcp").setLevel(logging.ERROR) +logger = logging.getLogger(__name__) + + +class VizPlatformMCPServer: + """MCP Server for visualization platform integration""" + + def __init__(self): + self.server = Server("viz-platform-mcp") + self.config = None + # Tool handlers will be added in subsequent issues + # self.dmc_tools = None + # self.chart_tools = None + # self.layout_tools = None + # self.theme_tools = None + # self.page_tools = None + + async def initialize(self): + """Initialize server and load configuration.""" + try: + config_loader = VizPlatformConfig() + self.config = config_loader.load() + + # Log available capabilities + caps = [] + if self.config.get('dmc_available'): + caps.append(f"DMC {self.config.get('dmc_version')}") + else: + caps.append("DMC (not installed)") + + logger.info(f"viz-platform MCP Server initialized with: {', '.join(caps)}") + + except Exception as e: + logger.error(f"Failed to initialize: {e}") + raise + + def setup_tools(self): + """Register all available tools with the MCP server""" + + @self.server.list_tools() + async def list_tools() -> list[Tool]: + """Return list of available tools""" + tools = [] + + # DMC validation tools (Issue #172) + # - list_components + # - get_component_props + # - validate_component + + # Chart tools (Issue #173) + # - chart_create + # - chart_configure_interaction + + # Layout tools (Issue #174) + # - layout_create + # - layout_add_filter + # - layout_set_grid + + # Theme tools (Issue #175) + # - theme_create + # - theme_extend + # - theme_validate + # - theme_export_css + + # Page tools (Issue #176) + # - page_create + # - page_add_navbar + # - page_set_auth + + return tools + + @self.server.call_tool() + async def call_tool(name: str, arguments: dict) -> list[TextContent]: + """Handle tool invocation.""" + try: + # Tool routing will be added as tools are implemented + # DMC tools + # if name == "list_components": + # result = await self.dmc_tools.list_components(**arguments) + # ... + + raise ValueError(f"Unknown tool: {name}") + + except Exception as e: + logger.error(f"Tool {name} failed: {e}") + return [TextContent( + type="text", + text=json.dumps({"error": str(e)}, indent=2) + )] + + async def run(self): + """Run the MCP server""" + await self.initialize() + self.setup_tools() + + async with stdio_server() as (read_stream, write_stream): + await self.server.run( + read_stream, + write_stream, + self.server.create_initialization_options() + ) + + +async def main(): + """Main entry point""" + server = VizPlatformMCPServer() + await server.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/mcp-servers/viz-platform/pyproject.toml b/mcp-servers/viz-platform/pyproject.toml new file mode 100644 index 0000000..32b670f --- /dev/null +++ b/mcp-servers/viz-platform/pyproject.toml @@ -0,0 +1,45 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "viz-platform-mcp" +version = "1.0.0" +description = "MCP Server for visualization with Dash Mantine Components validation and theming" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.10" +authors = [ + {name = "Leo Miranda"} +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "mcp>=0.9.0", + "plotly>=5.18.0", + "dash>=2.14.0", + "dash-mantine-components>=0.14.0", + "python-dotenv>=1.0.0", + "pydantic>=2.5.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.3", + "pytest-asyncio>=0.23.0", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["mcp_server*"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/mcp-servers/viz-platform/requirements.txt b/mcp-servers/viz-platform/requirements.txt new file mode 100644 index 0000000..93859ca --- /dev/null +++ b/mcp-servers/viz-platform/requirements.txt @@ -0,0 +1,15 @@ +# MCP SDK +mcp>=0.9.0 + +# Visualization +plotly>=5.18.0 +dash>=2.14.0 +dash-mantine-components>=0.14.0 + +# Utilities +python-dotenv>=1.0.0 +pydantic>=2.5.0 + +# Testing +pytest>=7.4.3 +pytest-asyncio>=0.23.0 diff --git a/mcp-servers/viz-platform/tests/__init__.py b/mcp-servers/viz-platform/tests/__init__.py new file mode 100644 index 0000000..6eeed7d --- /dev/null +++ b/mcp-servers/viz-platform/tests/__init__.py @@ -0,0 +1 @@ +"""viz-platform MCP Server tests."""