Skip to main content
Glama
tool_capabilities.py20.7 kB
"""Tool Capabilities Framework - Define and query tool capabilities""" import json from dataclasses import dataclass, field from enum import Enum from typing import Any, Callable, Dict, List, Optional, Set import FreeCAD class CapabilityType(Enum): """Types of tool capabilities""" CREATION = "creation" MODIFICATION = "modification" ANALYSIS = "analysis" SELECTION = "selection" VISUALIZATION = "visualization" IMPORT_EXPORT = "import_export" UTILITY = "utility" class RequirementType(Enum): """Types of requirements for tools""" DOCUMENT = "document" SELECTION = "selection" WORKBENCH = "workbench" OBJECT_TYPE = "object_type" PROPERTY = "property" PERMISSION = "permission" @dataclass class Parameter: """Defines a tool parameter""" name: str type: str description: str required: bool = True default: Optional[Any] = None constraints: Optional[Dict[str, Any]] = None units: Optional[str] = None examples: List[Any] = field(default_factory=list) @dataclass class Requirement: """Defines a tool requirement""" type: RequirementType description: str validator: Optional[Callable] = None error_message: Optional[str] = None @dataclass class Example: """Defines a usage example""" description: str input_text: str parameters: Dict[str, Any] expected_output: Optional[str] = None explanation: Optional[str] = None @dataclass class ToolCapability: """Complete capability definition for a tool""" tool_id: str name: str category: CapabilityType description: str detailed_description: str parameters: List[Parameter] requirements: List[Requirement] examples: List[Example] keywords: List[str] related_tools: List[str] = field(default_factory=list) produces: List[str] = field(default_factory=list) modifies: List[str] = field(default_factory=list) version: str = "1.0" def to_dict(self) -> Dict[str, Any]: """Convert to dictionary representation""" return { "tool_id": self.tool_id, "name": self.name, "category": self.category.value, "description": self.description, "detailed_description": self.detailed_description, "parameters": [ { "name": p.name, "type": p.type, "description": p.description, "required": p.required, "default": p.default, "constraints": p.constraints, "units": p.units, "examples": p.examples, } for p in self.parameters ], "requirements": [ { "type": r.type.value, "description": r.description, "error_message": r.error_message, } for r in self.requirements ], "examples": [ { "description": e.description, "input_text": e.input_text, "parameters": e.parameters, "expected_output": e.expected_output, "explanation": e.explanation, } for e in self.examples ], "keywords": self.keywords, "related_tools": self.related_tools, "produces": self.produces, "modifies": self.modifies, "version": self.version, } class ToolCapabilityRegistry: """Registry for managing tool capabilities""" def __init__(self): """Initialize the capability registry""" self.capabilities: Dict[str, ToolCapability] = {} self.capability_index: Dict[CapabilityType, Set[str]] = { cap_type: set() for cap_type in CapabilityType } self.keyword_index: Dict[str, Set[str]] = {} self.produces_index: Dict[str, Set[str]] = {} self.modifies_index: Dict[str, Set[str]] = {} # Register built-in capabilities self._register_builtin_capabilities() def register(self, capability: ToolCapability): """Register a tool capability""" self.capabilities[capability.tool_id] = capability # Update indices self.capability_index[capability.category].add(capability.tool_id) # Update keyword index for keyword in capability.keywords: if keyword not in self.keyword_index: self.keyword_index[keyword] = set() self.keyword_index[keyword].add(capability.tool_id) # Update produces index for produces in capability.produces: if produces not in self.produces_index: self.produces_index[produces] = set() self.produces_index[produces].add(capability.tool_id) # Update modifies index for modifies in capability.modifies: if modifies not in self.modifies_index: self.modifies_index[modifies] = set() self.modifies_index[modifies].add(capability.tool_id) def get(self, tool_id: str) -> Optional[ToolCapability]: """Get capability by tool ID""" return self.capabilities.get(tool_id) def query( self, category: Optional[CapabilityType] = None, keywords: Optional[List[str]] = None, produces: Optional[str] = None, modifies: Optional[str] = None, has_all_keywords: bool = False, ) -> List[ToolCapability]: """ Query capabilities based on criteria Args: category: Filter by capability type keywords: Filter by keywords produces: Filter by what the tool produces modifies: Filter by what the tool modifies has_all_keywords: If True, must have all keywords; if False, any keyword Returns: List of matching capabilities """ # Start with all tools tool_ids = set(self.capabilities.keys()) # Filter by category if category: tool_ids &= self.capability_index.get(category, set()) # Filter by keywords if keywords: keyword_tools = set() for keyword in keywords: if keyword in self.keyword_index: if has_all_keywords and not keyword_tools: keyword_tools = self.keyword_index[keyword].copy() elif has_all_keywords: keyword_tools &= self.keyword_index[keyword] else: keyword_tools |= self.keyword_index[keyword] tool_ids &= keyword_tools # Filter by produces if produces and produces in self.produces_index: tool_ids &= self.produces_index[produces] # Filter by modifies if modifies and modifies in self.modifies_index: tool_ids &= self.modifies_index[modifies] # Return capabilities return [self.capabilities[tool_id] for tool_id in tool_ids] def validate_parameters( self, tool_id: str, parameters: Dict[str, Any] ) -> Tuple[bool, List[str]]: """ Validate parameters for a tool Returns: Tuple of (is_valid, error_messages) """ capability = self.get(tool_id) if not capability: return False, [f"Unknown tool: {tool_id}"] errors = [] # Check required parameters for param in capability.parameters: if param.required and param.name not in parameters: errors.append(f"Missing required parameter: {param.name}") continue if param.name in parameters: value = parameters[param.name] # Type validation if not self._validate_type(value, param.type): errors.append( f"Parameter '{param.name}' expects type {param.type}, " f"got {type(value).__name__}" ) # Constraint validation if param.constraints: constraint_errors = self._validate_constraints( param.name, value, param.constraints ) errors.extend(constraint_errors) return len(errors) == 0, errors def check_requirements(self, tool_id: str) -> Tuple[bool, List[str]]: """ Check if tool requirements are met Returns: Tuple of (requirements_met, error_messages) """ capability = self.get(tool_id) if not capability: return False, [f"Unknown tool: {tool_id}"] errors = [] for requirement in capability.requirements: if requirement.validator: # Use custom validator if not requirement.validator(): errors.append( requirement.error_message or f"Requirement not met: {requirement.description}" ) else: # Use built-in validators if not self._check_requirement(requirement): errors.append( requirement.error_message or f"Requirement not met: {requirement.description}" ) return len(errors) == 0, errors def _validate_type(self, value: Any, expected_type: str) -> bool: """Validate value type""" type_map = { "string": str, "number": (int, float), "integer": int, "float": float, "boolean": bool, "array": list, "object": dict, } if expected_type in type_map: return isinstance(value, type_map[expected_type]) # Special types if expected_type == "vector3": return ( isinstance(value, (list, tuple)) and len(value) == 3 and all(isinstance(v, (int, float)) for v in value) ) if expected_type == "color": # Accept various color formats if isinstance(value, str): return value.startswith("#") or value in [ "red", "green", "blue", "yellow", ] if isinstance(value, (list, tuple)): return len(value) in [3, 4] and all(0 <= v <= 1 for v in value) return True # Unknown type, allow def _validate_constraints( self, name: str, value: Any, constraints: Dict[str, Any] ) -> List[str]: """Validate parameter constraints""" errors = [] # Min/max constraints if "min" in constraints and value < constraints["min"]: errors.append(f"{name} must be >= {constraints['min']}") if "max" in constraints and value > constraints["max"]: errors.append(f"{name} must be <= {constraints['max']}") # Enum constraint if "enum" in constraints and value not in constraints["enum"]: errors.append(f"{name} must be one of: {constraints['enum']}") # Pattern constraint (for strings) if "pattern" in constraints and isinstance(value, str): import re if not re.match(constraints["pattern"], value): errors.append(f"{name} must match pattern: {constraints['pattern']}") # Custom validator if "validator" in constraints: validator = constraints["validator"] if callable(validator) and not validator(value): errors.append(f"{name} validation failed") return errors def _check_requirement(self, requirement: Requirement) -> bool: """Check a single requirement""" if requirement.type == RequirementType.DOCUMENT: return FreeCAD.ActiveDocument is not None elif requirement.type == RequirementType.SELECTION: import FreeCADGui return len(FreeCADGui.Selection.getSelection()) > 0 elif requirement.type == RequirementType.WORKBENCH: # Would need to check active workbench return True elif requirement.type == RequirementType.OBJECT_TYPE: # Would need to check selected object types return True return True def _register_builtin_capabilities(self): """Register built-in tool capabilities""" # Example: Box creation tool self.register( ToolCapability( tool_id="primitives.create_box", name="Create Box", category=CapabilityType.CREATION, description="Create a box/cube primitive", detailed_description=""" Creates a parametric box (rectangular prism) in the active document. The box can be positioned and sized according to the provided parameters. """, parameters=[ Parameter( name="length", type="number", description="Length of the box (X direction)", required=False, default=10.0, units="mm", constraints={"min": 0.1, "max": 10000}, examples=[10, 50, 100], ), Parameter( name="width", type="number", description="Width of the box (Y direction)", required=False, default=10.0, units="mm", constraints={"min": 0.1, "max": 10000}, examples=[10, 50, 100], ), Parameter( name="height", type="number", description="Height of the box (Z direction)", required=False, default=10.0, units="mm", constraints={"min": 0.1, "max": 10000}, examples=[10, 50, 100], ), Parameter( name="position", type="vector3", description="Position of the box center", required=False, default=[0, 0, 0], units="mm", examples=[[0, 0, 0], [100, 50, 0]], ), Parameter( name="name", type="string", description="Name for the created box", required=False, default="Box", constraints={"pattern": r"^[A-Za-z][A-Za-z0-9_]*$"}, examples=["Box", "MyBox", "Container"], ), ], requirements=[ Requirement( type=RequirementType.DOCUMENT, description="Active document required", error_message="Please create or open a document first", ) ], examples=[ Example( description="Create a simple cube", input_text="create a cube", parameters={"length": 10, "width": 10, "height": 10}, expected_output="Box created with dimensions 10x10x10mm", ), Example( description="Create a box with specific dimensions", input_text="create a box 50mm x 30mm x 20mm", parameters={"length": 50, "width": 30, "height": 20}, expected_output="Box created with dimensions 50x30x20mm", ), Example( description="Create a box at specific position", input_text="create a box at position (100, 50, 0)", parameters={ "length": 10, "width": 10, "height": 10, "position": [100, 50, 0], }, expected_output="Box created at position (100, 50, 0)", ), ], keywords=[ "box", "cube", "rectangular", "prism", "block", "create", "make", "build", "primitive", ], related_tools=[ "primitives.create_cylinder", "primitives.create_sphere", ], produces=["Part::Box", "solid", "shape"], modifies=["document"], ) ) # Add more built-in capabilities as needed... def export_capabilities(self, file_path: str): """Export all capabilities to JSON file""" data = {tool_id: cap.to_dict() for tool_id, cap in self.capabilities.items()} with open(file_path, "w") as f: json.dump(data, f, indent=2) def import_capabilities(self, file_path: str): """Import capabilities from JSON file""" with open(file_path, "r") as f: data = json.load(f) for tool_id, cap_data in data.items(): # Reconstruct capability object # This is simplified - would need proper deserialization pass def generate_documentation(self, tool_id: str) -> str: """Generate documentation for a tool""" capability = self.get(tool_id) if not capability: return f"No documentation available for {tool_id}" doc = f"# {capability.name}\n\n" doc += f"**Category:** {capability.category.value}\n\n" doc += f"**Description:** {capability.description}\n\n" doc += capability.detailed_description.strip() + "\n\n" # Parameters if capability.parameters: doc += "## Parameters\n\n" for param in capability.parameters: req = "Required" if param.required else "Optional" doc += ( f"- **{param.name}** ({param.type}, {req}): {param.description}\n" ) if param.default is not None: doc += f" - Default: {param.default}\n" if param.units: doc += f" - Units: {param.units}\n" if param.constraints: doc += f" - Constraints: {param.constraints}\n" if param.examples: doc += f" - Examples: {', '.join(map(str, param.examples))}\n" # Requirements if capability.requirements: doc += "\n## Requirements\n\n" for req in capability.requirements: doc += f"- {req.description}\n" # Examples if capability.examples: doc += "\n## Examples\n\n" for i, example in enumerate(capability.examples, 1): doc += f"### Example {i}: {example.description}\n\n" doc += f"**Input:** `{example.input_text}`\n\n" doc += f"**Parameters:** `{example.parameters}`\n\n" if example.expected_output: doc += f"**Expected Output:** {example.expected_output}\n\n" if example.explanation: doc += f"**Explanation:** {example.explanation}\n\n" # Related tools if capability.related_tools: doc += f"\n## Related Tools\n\n" doc += ", ".join(capability.related_tools) + "\n" return doc # Global registry instance _capability_registry = None def get_capability_registry() -> ToolCapabilityRegistry: """Get the global capability registry""" global _capability_registry if _capability_registry is None: _capability_registry = ToolCapabilityRegistry() return _capability_registry

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/jango-blockchained/mcp-freecad'

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