development #184

Merged
lmiranda merged 11 commits from development into main 2026-01-26 18:59:23 +00:00
7 changed files with 441 additions and 0 deletions
Showing only changes of commit bf0745cc94 - Show all commits

View File

@@ -0,0 +1,7 @@
"""
viz-platform MCP Server package.
Provides Dash Mantine Components validation and visualization tools to Claude Code.
"""
__version__ = "1.0.0"

View File

@@ -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.'
}

View File

@@ -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")

View File

@@ -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())

View File

@@ -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"]

View File

@@ -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

View File

@@ -0,0 +1 @@
"""viz-platform MCP Server tests."""