feat(viz-platform): create MCP server foundation (#170)
Add viz-platform MCP server structure at mcp-servers/viz-platform/: - mcp_server/server.py: Main MCP server entry point with async initialization - mcp_server/config.py: Hybrid config loader with DMC version detection - mcp_server/dmc_tools.py: Placeholder for DMC validation tools - pyproject.toml and requirements.txt for dependencies - tests/ directory structure Server starts without errors with empty tool list. Config detects DMC installation status via importlib.metadata. Closes #170 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
7
mcp-servers/viz-platform/mcp_server/__init__.py
Normal file
7
mcp-servers/viz-platform/mcp_server/__init__.py
Normal 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"
|
||||||
172
mcp-servers/viz-platform/mcp_server/config.py
Normal file
172
mcp-servers/viz-platform/mcp_server/config.py
Normal 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.'
|
||||||
|
}
|
||||||
72
mcp-servers/viz-platform/mcp_server/dmc_tools.py
Normal file
72
mcp-servers/viz-platform/mcp_server/dmc_tools.py
Normal 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")
|
||||||
129
mcp-servers/viz-platform/mcp_server/server.py
Normal file
129
mcp-servers/viz-platform/mcp_server/server.py
Normal 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())
|
||||||
45
mcp-servers/viz-platform/pyproject.toml
Normal file
45
mcp-servers/viz-platform/pyproject.toml
Normal 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"]
|
||||||
15
mcp-servers/viz-platform/requirements.txt
Normal file
15
mcp-servers/viz-platform/requirements.txt
Normal 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
|
||||||
1
mcp-servers/viz-platform/tests/__init__.py
Normal file
1
mcp-servers/viz-platform/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""viz-platform MCP Server tests."""
|
||||||
Reference in New Issue
Block a user