feat(viz-platform): implement DMC validation tools (#172)
- Add list_components tool: list DMC components by category - Add get_component_props tool: get props schema with types/defaults/enums - Add validate_component tool: validate props with error/warning messages - Includes typo detection and common mistake warnings - All tools registered in MCP server with proper JSON schemas Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,11 +2,12 @@
|
|||||||
DMC (Dash Mantine Components) validation tools.
|
DMC (Dash Mantine Components) validation tools.
|
||||||
|
|
||||||
Provides component constraint layer to prevent Claude from hallucinating invalid props.
|
Provides component constraint layer to prevent Claude from hallucinating invalid props.
|
||||||
Tools implemented in Issue #172.
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
|
|
||||||
|
from .component_registry import ComponentRegistry
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -18,16 +19,39 @@ class DMCTools:
|
|||||||
against a version-locked registry of DMC components.
|
against a version-locked registry of DMC components.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, registry=None):
|
def __init__(self, registry: Optional[ComponentRegistry] = None):
|
||||||
"""
|
"""
|
||||||
Initialize DMC tools with component registry.
|
Initialize DMC tools with component registry.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
registry: ComponentRegistry instance (from Issue #171)
|
registry: ComponentRegistry instance. If None, creates one.
|
||||||
"""
|
"""
|
||||||
self.registry = registry
|
self.registry = registry
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
async def list_components(self, category: Optional[str] = None) -> Dict[str, Any]:
|
def initialize(self, dmc_version: Optional[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Initialize the registry if not already provided.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dmc_version: DMC version to load registry for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if initialized successfully
|
||||||
|
"""
|
||||||
|
if self.registry is None:
|
||||||
|
self.registry = ComponentRegistry(dmc_version)
|
||||||
|
|
||||||
|
if not self.registry.is_loaded():
|
||||||
|
self.registry.load()
|
||||||
|
|
||||||
|
self._initialized = self.registry.is_loaded()
|
||||||
|
return self._initialized
|
||||||
|
|
||||||
|
async def list_components(
|
||||||
|
self,
|
||||||
|
category: Optional[str] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
List available DMC components, optionally filtered by category.
|
List available DMC components, optionally filtered by category.
|
||||||
|
|
||||||
@@ -35,10 +59,33 @@ class DMCTools:
|
|||||||
category: Optional category filter (e.g., "inputs", "buttons", "navigation")
|
category: Optional category filter (e.g., "inputs", "buttons", "navigation")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with components grouped by category
|
Dict with:
|
||||||
|
- components: Dict[category -> [component names]]
|
||||||
|
- categories: List of available categories
|
||||||
|
- version: Loaded DMC registry version
|
||||||
|
- total_count: Total number of components
|
||||||
"""
|
"""
|
||||||
# Implementation in Issue #172
|
if not self._initialized:
|
||||||
raise NotImplementedError("Implemented in Issue #172")
|
return {
|
||||||
|
"error": "Registry not initialized",
|
||||||
|
"components": {},
|
||||||
|
"categories": [],
|
||||||
|
"version": None,
|
||||||
|
"total_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
components = self.registry.list_components(category)
|
||||||
|
all_categories = self.registry.get_categories()
|
||||||
|
|
||||||
|
# Count total components
|
||||||
|
total = sum(len(comps) for comps in components.values())
|
||||||
|
|
||||||
|
return {
|
||||||
|
"components": components,
|
||||||
|
"categories": all_categories if not category else [category],
|
||||||
|
"version": self.registry.loaded_version,
|
||||||
|
"total_count": total
|
||||||
|
}
|
||||||
|
|
||||||
async def get_component_props(self, component: str) -> Dict[str, Any]:
|
async def get_component_props(self, component: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -48,10 +95,53 @@ class DMCTools:
|
|||||||
component: Component name (e.g., "Button", "TextInput")
|
component: Component name (e.g., "Button", "TextInput")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with props, types, defaults, and enum values
|
Dict with:
|
||||||
|
- component: Component name
|
||||||
|
- description: Component description
|
||||||
|
- props: Dict of prop name -> {type, default, enum, description}
|
||||||
|
- prop_count: Number of props
|
||||||
|
- required: List of required prop names
|
||||||
|
Or error dict if component not found
|
||||||
"""
|
"""
|
||||||
# Implementation in Issue #172
|
if not self._initialized:
|
||||||
raise NotImplementedError("Implemented in Issue #172")
|
return {
|
||||||
|
"error": "Registry not initialized",
|
||||||
|
"component": component,
|
||||||
|
"props": {},
|
||||||
|
"prop_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
comp_def = self.registry.get_component(component)
|
||||||
|
if not comp_def:
|
||||||
|
# Try to suggest similar component name
|
||||||
|
similar = self._find_similar_component(component)
|
||||||
|
error_msg = f"Component '{component}' not found in registry"
|
||||||
|
if similar:
|
||||||
|
error_msg += f". Did you mean '{similar}'?"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"error": error_msg,
|
||||||
|
"component": component,
|
||||||
|
"props": {},
|
||||||
|
"prop_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
props = comp_def.get('props', {})
|
||||||
|
|
||||||
|
# Extract required props
|
||||||
|
required = [
|
||||||
|
name for name, schema in props.items()
|
||||||
|
if schema.get('required', False)
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"component": component,
|
||||||
|
"description": comp_def.get('description', ''),
|
||||||
|
"props": props,
|
||||||
|
"prop_count": len(props),
|
||||||
|
"required": required,
|
||||||
|
"version": self.registry.loaded_version
|
||||||
|
}
|
||||||
|
|
||||||
async def validate_component(
|
async def validate_component(
|
||||||
self,
|
self,
|
||||||
@@ -63,10 +153,154 @@ class DMCTools:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
component: Component name
|
component: Component name
|
||||||
props: Props to validate
|
props: Props dict to validate
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with valid: bool, errors: [], warnings: []
|
Dict with:
|
||||||
|
- valid: bool - True if all props are valid
|
||||||
|
- errors: List of error messages
|
||||||
|
- warnings: List of warning messages
|
||||||
|
- validated_props: Number of props validated
|
||||||
|
- component: Component name for reference
|
||||||
"""
|
"""
|
||||||
# Implementation in Issue #172
|
if not self._initialized:
|
||||||
raise NotImplementedError("Implemented in Issue #172")
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"errors": ["Registry not initialized"],
|
||||||
|
"warnings": [],
|
||||||
|
"validated_props": 0,
|
||||||
|
"component": component
|
||||||
|
}
|
||||||
|
|
||||||
|
errors: List[str] = []
|
||||||
|
warnings: List[str] = []
|
||||||
|
|
||||||
|
# Check if component exists
|
||||||
|
comp_def = self.registry.get_component(component)
|
||||||
|
if not comp_def:
|
||||||
|
similar = self._find_similar_component(component)
|
||||||
|
error_msg = f"Unknown component: {component}"
|
||||||
|
if similar:
|
||||||
|
error_msg += f". Did you mean '{similar}'?"
|
||||||
|
errors.append(error_msg)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"errors": errors,
|
||||||
|
"warnings": warnings,
|
||||||
|
"validated_props": 0,
|
||||||
|
"component": component
|
||||||
|
}
|
||||||
|
|
||||||
|
comp_props = comp_def.get('props', {})
|
||||||
|
|
||||||
|
# Check for required props
|
||||||
|
for prop_name, prop_schema in comp_props.items():
|
||||||
|
if prop_schema.get('required', False) and prop_name not in props:
|
||||||
|
errors.append(f"Missing required prop: '{prop_name}'")
|
||||||
|
|
||||||
|
# Validate each provided prop
|
||||||
|
for prop_name, prop_value in props.items():
|
||||||
|
# Skip special props that are always allowed
|
||||||
|
if prop_name in ('id', 'children', 'className', 'style', 'key'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = self.registry.validate_prop(component, prop_name, prop_value)
|
||||||
|
|
||||||
|
if not result.get('valid', True):
|
||||||
|
error = result.get('error', f"Invalid prop: {prop_name}")
|
||||||
|
# Distinguish between typos/unknown props and type errors
|
||||||
|
if "Unknown prop" in error:
|
||||||
|
errors.append(f"❌ {error}")
|
||||||
|
elif "expects one of" in error:
|
||||||
|
errors.append(f"❌ {error}")
|
||||||
|
elif "expects type" in error:
|
||||||
|
warnings.append(f"⚠️ {error}")
|
||||||
|
else:
|
||||||
|
errors.append(f"❌ {error}")
|
||||||
|
|
||||||
|
# Check for props that exist but might have common mistakes
|
||||||
|
self._check_common_mistakes(component, props, warnings)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid": len(errors) == 0,
|
||||||
|
"errors": errors,
|
||||||
|
"warnings": warnings,
|
||||||
|
"validated_props": len(props),
|
||||||
|
"component": component,
|
||||||
|
"version": self.registry.loaded_version
|
||||||
|
}
|
||||||
|
|
||||||
|
def _find_similar_component(self, component: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Find a similar component name for suggestions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component: The (possibly misspelled) component name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Similar component name, or None if no close match
|
||||||
|
"""
|
||||||
|
if not self.registry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
comp_lower = component.lower()
|
||||||
|
all_components = []
|
||||||
|
for comps in self.registry.categories.values():
|
||||||
|
all_components.extend(comps)
|
||||||
|
|
||||||
|
for comp in all_components:
|
||||||
|
# Exact match after lowercase
|
||||||
|
if comp.lower() == comp_lower:
|
||||||
|
return comp
|
||||||
|
# Check if it's a prefix match
|
||||||
|
if comp.lower().startswith(comp_lower) or comp_lower.startswith(comp.lower()):
|
||||||
|
return comp
|
||||||
|
# Check for common typos
|
||||||
|
if abs(len(comp) - len(component)) <= 2:
|
||||||
|
if comp_lower[:4] == comp.lower()[:4]:
|
||||||
|
return comp
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _check_common_mistakes(
|
||||||
|
self,
|
||||||
|
component: str,
|
||||||
|
props: Dict[str, Any],
|
||||||
|
warnings: List[str]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Check for common prop usage mistakes and add warnings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component: Component name
|
||||||
|
props: Props being used
|
||||||
|
warnings: List to append warnings to
|
||||||
|
"""
|
||||||
|
# Common mistake: using 'onclick' instead of callback pattern
|
||||||
|
if 'onclick' in [p.lower() for p in props.keys()]:
|
||||||
|
warnings.append(
|
||||||
|
"⚠️ Dash uses callback patterns, not inline event handlers. "
|
||||||
|
"Use 'n_clicks' prop with a callback instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Common mistake: using 'class' instead of 'className'
|
||||||
|
if 'class' in props:
|
||||||
|
warnings.append(
|
||||||
|
"⚠️ Use 'className' instead of 'class' for CSS classes."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Button-specific checks
|
||||||
|
if component == 'Button':
|
||||||
|
if 'href' in props and 'component' not in props:
|
||||||
|
warnings.append(
|
||||||
|
"⚠️ Button with 'href' should also set 'component=\"a\"' for proper anchor behavior."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Input-specific checks
|
||||||
|
if 'Input' in component:
|
||||||
|
if 'value' in props and 'onChange' in [p for p in props.keys()]:
|
||||||
|
warnings.append(
|
||||||
|
"⚠️ Dash uses 'value' prop with callbacks, not 'onChange'. "
|
||||||
|
"The value updates automatically through Dash callbacks."
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from mcp.server.stdio import stdio_server
|
|||||||
from mcp.types import Tool, TextContent
|
from mcp.types import Tool, TextContent
|
||||||
|
|
||||||
from .config import VizPlatformConfig
|
from .config import VizPlatformConfig
|
||||||
|
from .dmc_tools import DMCTools
|
||||||
|
|
||||||
# Suppress noisy MCP validation warnings on stderr
|
# Suppress noisy MCP validation warnings on stderr
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
@@ -26,8 +27,8 @@ class VizPlatformMCPServer:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.server = Server("viz-platform-mcp")
|
self.server = Server("viz-platform-mcp")
|
||||||
self.config = None
|
self.config = None
|
||||||
|
self.dmc_tools = DMCTools()
|
||||||
# Tool handlers will be added in subsequent issues
|
# Tool handlers will be added in subsequent issues
|
||||||
# self.dmc_tools = None
|
|
||||||
# self.chart_tools = None
|
# self.chart_tools = None
|
||||||
# self.layout_tools = None
|
# self.layout_tools = None
|
||||||
# self.theme_tools = None
|
# self.theme_tools = None
|
||||||
@@ -39,10 +40,16 @@ class VizPlatformMCPServer:
|
|||||||
config_loader = VizPlatformConfig()
|
config_loader = VizPlatformConfig()
|
||||||
self.config = config_loader.load()
|
self.config = config_loader.load()
|
||||||
|
|
||||||
|
# Initialize DMC tools with detected version
|
||||||
|
dmc_version = self.config.get('dmc_version')
|
||||||
|
self.dmc_tools.initialize(dmc_version)
|
||||||
|
|
||||||
# Log available capabilities
|
# Log available capabilities
|
||||||
caps = []
|
caps = []
|
||||||
if self.config.get('dmc_available'):
|
if self.config.get('dmc_available'):
|
||||||
caps.append(f"DMC {self.config.get('dmc_version')}")
|
caps.append(f"DMC {dmc_version}")
|
||||||
|
if self.dmc_tools._initialized:
|
||||||
|
caps.append(f"Registry loaded ({self.dmc_tools.registry.loaded_version})")
|
||||||
else:
|
else:
|
||||||
caps.append("DMC (not installed)")
|
caps.append("DMC (not installed)")
|
||||||
|
|
||||||
@@ -61,9 +68,70 @@ class VizPlatformMCPServer:
|
|||||||
tools = []
|
tools = []
|
||||||
|
|
||||||
# DMC validation tools (Issue #172)
|
# DMC validation tools (Issue #172)
|
||||||
# - list_components
|
tools.append(Tool(
|
||||||
# - get_component_props
|
name="list_components",
|
||||||
# - validate_component
|
description=(
|
||||||
|
"List available Dash Mantine Components. "
|
||||||
|
"Returns components grouped by category with version info. "
|
||||||
|
"Use this to discover what components are available before building UI."
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Optional category filter. Available categories: "
|
||||||
|
"buttons, inputs, navigation, feedback, overlays, "
|
||||||
|
"typography, layout, data_display, charts, dates"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
tools.append(Tool(
|
||||||
|
name="get_component_props",
|
||||||
|
description=(
|
||||||
|
"Get the props schema for a specific DMC component. "
|
||||||
|
"Returns all available props with types, defaults, and allowed values. "
|
||||||
|
"ALWAYS use this before creating a component to ensure valid props."
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"component": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Component name (e.g., 'Button', 'TextInput', 'Select')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["component"]
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
tools.append(Tool(
|
||||||
|
name="validate_component",
|
||||||
|
description=(
|
||||||
|
"Validate component props before use. "
|
||||||
|
"Checks for invalid props, type mismatches, and common mistakes. "
|
||||||
|
"Returns errors and warnings with suggestions for fixes."
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"component": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Component name to validate"
|
||||||
|
},
|
||||||
|
"props": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Props object to validate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["component", "props"]
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
# Chart tools (Issue #173)
|
# Chart tools (Issue #173)
|
||||||
# - chart_create
|
# - chart_create
|
||||||
@@ -91,11 +159,47 @@ class VizPlatformMCPServer:
|
|||||||
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||||
"""Handle tool invocation."""
|
"""Handle tool invocation."""
|
||||||
try:
|
try:
|
||||||
# Tool routing will be added as tools are implemented
|
# DMC validation tools
|
||||||
# DMC tools
|
if name == "list_components":
|
||||||
# if name == "list_components":
|
result = await self.dmc_tools.list_components(
|
||||||
# result = await self.dmc_tools.list_components(**arguments)
|
category=arguments.get('category')
|
||||||
# ...
|
)
|
||||||
|
return [TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(result, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
elif name == "get_component_props":
|
||||||
|
component = arguments.get('component')
|
||||||
|
if not component:
|
||||||
|
return [TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps({"error": "component is required"}, indent=2)
|
||||||
|
)]
|
||||||
|
result = await self.dmc_tools.get_component_props(component)
|
||||||
|
return [TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(result, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
elif name == "validate_component":
|
||||||
|
component = arguments.get('component')
|
||||||
|
props = arguments.get('props', {})
|
||||||
|
if not component:
|
||||||
|
return [TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps({"error": "component is required"}, indent=2)
|
||||||
|
)]
|
||||||
|
result = await self.dmc_tools.validate_component(component, props)
|
||||||
|
return [TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(result, indent=2)
|
||||||
|
)]
|
||||||
|
|
||||||
|
# Chart tools (Issue #173)
|
||||||
|
# Layout tools (Issue #174)
|
||||||
|
# Theme tools (Issue #175)
|
||||||
|
# Page tools (Issue #176)
|
||||||
|
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user