Skip to main content
Glama
consolidated-action-pattern.md19.1 kB
# FastMCP Consolidated Action-Parameter Pattern Guide ## Overview The Consolidated Action-Parameter Pattern is an architectural approach for organizing FastMCP tools that groups related operations under a single tool with an `action` parameter, rather than creating separate tools for each operation. This pattern significantly reduces tool count, improves context efficiency, and provides better user experience. ## Benefits ### Context Efficiency - **Token Reduction**: Our 3 consolidated tools (27 actions) use ~5k tokens vs Playwright's 21 individual tools using ~9.7k tokens - **2.6x more efficient**: 181 tokens per function vs 464 tokens per function - **Better scaling**: Adding new actions to existing tools is more efficient than creating new tools ### User Experience - **Cleaner interface**: 3 logical groups instead of 27 separate tools - **Easier discovery**: Related operations are grouped together - **Consistent parameters**: Shared parameters like `host_id` defined once per group - **Logical organization**: Operations naturally group by domain (hosts, containers, stacks) ### Maintainability - **Reduced duplication**: Common validation and error handling - **Centralized routing**: Single dispatch point per domain - **Consistent patterns**: Uniform approach across all operations ## Implementation Pattern ### 1. Basic Structure ```python from typing import Annotated, Literal from pydantic import Field from fastmcp import FastMCP mcp = FastMCP(name="ConsolidatedServer") @mcp.tool async def domain_manager( # Action parameter - defines what operation to perform action: Annotated[Literal["list", "create", "update", "delete"], Field(description="Action to perform")], # Required parameters - used by multiple actions resource_id: Annotated[str, Field(description="Resource identifier", min_length=1)], # Optional parameters - provide defaults for all actions name: Annotated[str, Field(default="", description="Resource name")] = "", description: Annotated[str, Field(default="", description="Resource description")] = "", tags: Annotated[list[str], Field(default_factory=list, description="Resource tags")] = [], # Action-specific parameters - only used by certain actions force: Annotated[bool, Field(default=False, description="Force the operation")] = False, dry_run: Annotated[bool, Field(default=False, description="Perform a dry run")] = False ) -> dict[str, Any]: """Consolidated resource management tool. Actions: - list: List all resources - create: Create a new resource (requires: name) - update: Update an existing resource (requires: resource_id) - delete: Delete a resource (requires: resource_id; optional: force) """ # Validate action valid_actions = ["list", "create", "update", "delete"] if action not in valid_actions: return { "success": False, "error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}" } # Route to appropriate handler try: if action == "list": return await handle_list_resources() elif action == "create": if not name: return {"success": False, "error": "name is required for create action"} return await handle_create_resource(name, description, tags) elif action == "update": if not resource_id: return {"success": False, "error": "resource_id is required for update action"} return await handle_update_resource(resource_id, name, description, tags) elif action == "delete": if not resource_id: return {"success": False, "error": "resource_id is required for delete action"} return await handle_delete_resource(resource_id, force, dry_run) except Exception as e: return { "success": False, "error": f"Action '{action}' failed: {str(e)}" } ``` ### 2. Action Parameter Design #### Use Literal Types for Actions ```python # ✅ Good - Explicit list of valid actions action: Annotated[Literal["start", "stop", "restart", "status"], Field(description="Container action")] # ❌ Avoid - Too generic action: Annotated[str, Field(description="Action to perform")] ``` #### Group Related Operations ```python # ✅ Good groupings by domain docker_hosts_actions = ["list", "add", "remove", "ports", "cleanup"] docker_container_actions = ["list", "info", "start", "stop", "restart", "logs"] docker_compose_actions = ["list", "deploy", "up", "down", "build", "logs", "migrate"] # ❌ Avoid mixing unrelated domains mixed_actions = ["list_hosts", "start_container", "deploy_stack"] # Too broad ``` ### 3. Parameter Strategies #### Required vs Optional Parameters ```python @mcp.tool async def resource_manager( # Required for all actions action: Annotated[Literal["list", "create", "update"], Field(description="Action to perform")], # Conditionally required (validate in function body) resource_id: Annotated[str, Field(default="", description="Resource identifier")] = "", # Always optional with sensible defaults timeout: Annotated[int, Field(default=30, ge=1, le=300, description="Operation timeout")] = 30, force: Annotated[bool, Field(default=False, description="Force the operation")] = False ) -> dict: # Validate conditionally required parameters if action in ["update", "delete"] and not resource_id: return {"success": False, "error": f"resource_id is required for {action} action"} ``` #### Parameter Grouping by Usage ```python @mcp.tool async def container_manager( action: Annotated[Literal["list", "start", "stop", "logs"], Field(description="Action to perform")], # Core identification parameters host_id: Annotated[str, Field(default="", description="Docker host identifier")] = "", container_id: Annotated[str, Field(default="", description="Container identifier")] = "", # List-specific parameters all_containers: Annotated[bool, Field(default=False, description="Include stopped containers")] = False, limit: Annotated[int, Field(default=20, ge=1, le=1000, description="Max results")] = 20, # Logs-specific parameters follow: Annotated[bool, Field(default=False, description="Follow log output")] = False, lines: Annotated[int, Field(default=100, ge=1, le=10000, description="Number of log lines")] = 100, # Operation parameters force: Annotated[bool, Field(default=False, description="Force the operation")] = False, timeout: Annotated[int, Field(default=10, ge=1, le=300, description="Timeout in seconds")] = 10 ): pass ``` ### 4. Validation and Error Handling #### Action Validation Pattern ```python def validate_action(action: str, valid_actions: list[str]) -> tuple[bool, str]: """Validate action parameter.""" if action not in valid_actions: return False, f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}" return True, "" # Usage in tool valid_actions = ["list", "create", "update", "delete"] is_valid, error_msg = validate_action(action, valid_actions) if not is_valid: return {"success": False, "error": error_msg} ``` #### Parameter Validation by Action ```python def validate_parameters_for_action(action: str, **params) -> tuple[bool, str]: """Validate required parameters for specific actions.""" if action == "create": if not params.get("name"): return False, "name is required for create action" elif action in ["update", "delete"]: if not params.get("resource_id"): return False, f"resource_id is required for {action} action" elif action == "deploy": if not params.get("compose_content"): return False, "compose_content is required for deploy action" # Add range validations if params.get("port") and not (1 <= params["port"] <= 65535): return False, "port must be between 1 and 65535" return True, "" ``` ### 5. Routing and Dispatch #### Service Layer Pattern ```python class ResourceService: async def list_resources(self) -> dict: """List all resources.""" pass async def create_resource(self, name: str, **kwargs) -> dict: """Create a new resource.""" pass async def update_resource(self, resource_id: str, **kwargs) -> dict: """Update existing resource.""" pass @mcp.tool async def resource_manager(action: str, **params) -> dict: service = ResourceService() # Route to appropriate service method if action == "list": return await service.list_resources() elif action == "create": return await service.create_resource(**params) elif action == "update": return await service.update_resource(**params) # ... etc ``` #### Direct Dispatch Pattern ```python @mcp.tool async def container_manager(action: str, host_id: str, container_id: str, **kwargs) -> dict: """Direct dispatch to handler functions.""" # Route to handler functions handlers = { "list": handle_list_containers, "info": handle_container_info, "start": handle_start_container, "stop": handle_stop_container, "logs": handle_container_logs } handler = handlers.get(action) if not handler: return {"success": False, "error": f"Unknown action: {action}"} try: return await handler(host_id, container_id, **kwargs) except Exception as e: return {"success": False, "error": f"Action '{action}' failed: {str(e)}"} async def handle_list_containers(host_id: str, **kwargs) -> dict: """Handle container listing.""" all_containers = kwargs.get("all_containers", False) limit = kwargs.get("limit", 20) # Implementation... return {"success": True, "containers": []} ``` ## Real-World Example: Docker MCP Server ### Tool Organization ```python # Before: 27 separate tools @mcp.tool async def list_docker_hosts(): pass @mcp.tool async def add_docker_host(): pass @mcp.tool async def list_host_ports(): pass # ... 24 more tools # After: 3 consolidated tools @mcp.tool async def docker_hosts(action: Literal["list", "add", "ports", ...], ...): pass @mcp.tool async def docker_container(action: Literal["list", "info", "start", ...], ...): pass @mcp.tool async def docker_compose(action: Literal["list", "deploy", "up", ...], ...): pass ``` ### Complete Implementation ```python @mcp.tool async def docker_hosts( action: Annotated[Literal[ "list", "add", "ports", "compose_path", "import_ssh", "cleanup", "schedule", "reserve_port", "release_port" ], Field(description="Action to perform")], # Core parameters host_id: Annotated[str, Field(default="", description="Host identifier")] = "", ssh_host: Annotated[str, Field(default="", description="SSH hostname")] = "", ssh_user: Annotated[str, Field(default="", description="SSH username")] = "", # Optional parameters with proper defaults ssh_port: Annotated[int, Field(default=22, ge=1, le=65535, description="SSH port")] = 22, ssh_key_path: Annotated[str, Field(default="", description="SSH private key path")] = "", description: Annotated[str, Field(default="", description="Host description")] = "", tags: Annotated[list[str], Field(default_factory=list, description="Host tags")] = [], # Action-specific parameters port: Annotated[int, Field(default=0, ge=1, le=65535, description="Port number")] = 0, cleanup_type: Annotated[str, Field(default="", description="Type of cleanup")] = "", export_format: Annotated[str, Field(default="", description="Export format")] = "" ) -> dict[str, Any]: """Consolidated Docker hosts management tool.""" # Validation valid_actions = ["list", "add", "ports", "compose_path", "import_ssh", "cleanup", "schedule", "reserve_port", "release_port"] if action not in valid_actions: return {"success": False, "error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"} try: # Route to appropriate handler if action == "list": return await self.list_docker_hosts() elif action == "add": # Validate required parameters if not all([host_id, ssh_host, ssh_user]): return {"success": False, "error": "host_id, ssh_host, and ssh_user are required for add action"} return await self.add_docker_host(host_id, ssh_host, ssh_user, ssh_port, ssh_key_path, description, tags) elif action == "ports": if not host_id: return {"success": False, "error": "host_id is required for ports action"} return await self.list_host_ports(host_id, export_format, ...) # ... handle other actions except Exception as e: return {"success": False, "error": f"Action '{action}' failed: {str(e)}"} ``` ## Best Practices ### 1. Action Naming ```python # ✅ Good - Verb-based, clear actions ["list", "create", "update", "delete", "start", "stop", "restart"] # ✅ Good - Domain-specific actions ["deploy", "scale", "migrate", "backup", "restore"] # ❌ Avoid - Ambiguous or too generic ["manage", "handle", "process", "execute"] # ❌ Avoid - Mixing abstraction levels ["list", "get_detailed_info", "quick_start"] ``` ### 2. Parameter Organization ```python @mcp.tool async def consolidated_tool( # 1. Action parameter (always first) action: Annotated[Literal[...], Field(description="Action to perform")], # 2. Core required parameters (used by most actions) resource_id: Annotated[str, Field(description="Resource identifier")], # 3. Core optional parameters (used by most actions) name: Annotated[str, Field(default="", description="Resource name")] = "", # 4. Action-specific parameters (grouped by usage) # List parameters limit: Annotated[int, Field(default=20, description="Max results")] = 20, # Deploy parameters compose_content: Annotated[str, Field(default="", description="Compose file")] = "", # Operation parameters force: Annotated[bool, Field(default=False, description="Force operation")] = False ): pass ``` ### 3. Documentation Patterns ```python @mcp.tool async def domain_manager(action, ...): """Consolidated domain management tool. Actions: - list: List all resources - create: Create a new resource (requires: name; optional: description, tags) - update: Update resource (requires: resource_id; optional: name, description) - delete: Delete resource (requires: resource_id; optional: force) - deploy: Deploy resource (requires: resource_id, config; optional: dry_run) Examples: - List all: action="list" - Create new: action="create", name="my-resource", description="My resource" - Update existing: action="update", resource_id="123", name="new-name" """ ``` ### 4. Error Handling ```python async def consolidated_tool(action: str, **params) -> dict: # Always return consistent error format def error_response(message: str) -> dict: return { "success": False, "error": message, "action": action # Include action for debugging } # Validate action if action not in VALID_ACTIONS: return error_response(f"Invalid action '{action}'") # Validate parameters validation_result = validate_params_for_action(action, **params) if not validation_result.success: return error_response(validation_result.error) try: # Execute action result = await execute_action(action, **params) return { "success": True, "action": action, **result } except Exception as e: logger.error(f"Action '{action}' failed", exc_info=True) return error_response(f"Execution failed: {str(e)}") ``` ## Migration Guide ### Converting Individual Tools to Consolidated #### Step 1: Identify Tool Groups ```python # Group related tools by domain container_tools = ["list_containers", "start_container", "stop_container", "get_container_logs"] host_tools = ["list_hosts", "add_host", "remove_host", "test_host_connection"] ``` #### Step 2: Design Action Parameter ```python # Extract action from tool names container_actions = ["list", "start", "stop", "logs"] # Remove "container" prefix host_actions = ["list", "add", "remove", "test"] # Remove "host" prefix ``` #### Step 3: Consolidate Parameters ```python # Before - separate tools with duplicate parameters async def start_container(host_id: str, container_id: str, timeout: int = 30): pass async def stop_container(host_id: str, container_id: str, timeout: int = 30, force: bool = False): pass # After - consolidated with shared parameters async def container_manager( action: Literal["start", "stop"], host_id: str, container_id: str, timeout: int = 30, force: bool = False # Used by stop, ignored by start ): pass ``` #### Step 4: Implement Routing ```python async def container_manager(action: str, **params): # Map actions to original functions action_map = { "list": original_list_containers, "start": original_start_container, "stop": original_stop_container, "logs": original_get_container_logs } handler = action_map.get(action) return await handler(**params) ``` ## Troubleshooting ### Common Issues 1. **Too Many Parameters**: If you have >15 parameters, consider splitting into multiple consolidated tools 2. **Parameter Conflicts**: When actions need mutually exclusive parameters, use validation in the function body 3. **Action Overlap**: Avoid having similar actions across different consolidated tools 4. **Validation Complexity**: If validation logic becomes too complex, consider separate validation functions ### Performance Considerations - **Token Efficiency**: Consolidated tools use significantly fewer tokens than individual tools - **Context Overhead**: Each tool adds ~400-500 tokens to context - consolidation reduces this multiplicatively - **Parameter Parsing**: More parameters per tool, but FastMCP handles this efficiently - **Route Dispatch**: Minimal overhead compared to context token savings ## Summary The Consolidated Action-Parameter Pattern provides: - **Massive token efficiency**: 2.6x more efficient than individual tools - **Better organization**: Logical grouping of related operations - **Cleaner interfaces**: Fewer tools in MCP client listings - **Easier maintenance**: Centralized validation and error handling - **Consistent UX**: Uniform parameter patterns across actions This pattern is especially valuable for domain-rich APIs where you have many related operations that share common parameters and validation logic.

Latest Blog Posts

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/jmagar/docker-mcp'

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