copy_metabase_dashboard
Duplicate an existing Metabase dashboard to create a new version with custom name, description, and collection placement. Optionally copy linked cards for comprehensive replication.
Instructions
Copy a dashboard.
Args: from_dashboard_id (int): ID of the source dashboard to copy. name (str): Name for the new dashboard. description (str, optional): Description for the new dashboard. collection_id (int, optional): Collection ID for the new dashboard. is_deep_copy (bool, optional): Whether to perform a deep copy (copy linked cards too). collection_position (int, optional): Position in the collection.
Returns: Dict[str, Any]: New dashboard metadata.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| from_dashboard_id | Yes | ||
| name | Yes | ||
| description | No | ||
| collection_id | No | ||
| is_deep_copy | No | ||
| collection_position | No |
Implementation Reference
- src/metabase_mcp_server.py:966-1006 (handler)The main handler function for 'copy_metabase_dashboard' tool. It accepts dashboard copy parameters (from_dashboard_id, name, description, collection_id, is_deep_copy, collection_position), builds a payload, and makes a POST request to Metabase's /api/dashboard/{from_dashboard_id}/copy endpoint. Registered as an MCP tool via @mcp.tool() decorator.
@mcp.tool() async def copy_metabase_dashboard( from_dashboard_id: int, name: str, description: Optional[str] = None, collection_id: Optional[int] = None, is_deep_copy: bool = False, collection_position: Optional[int] = None ) -> Dict[str, Any]: """ Copy a dashboard. Args: from_dashboard_id (int): ID of the source dashboard to copy. name (str): Name for the new dashboard. description (str, optional): Description for the new dashboard. collection_id (int, optional): Collection ID for the new dashboard. is_deep_copy (bool, optional): Whether to perform a deep copy (copy linked cards too). collection_position (int, optional): Position in the collection. Returns: Dict[str, Any]: New dashboard metadata. """ payload = { "name": name, } if description is not None: payload["description"] = description if collection_id is not None: payload["collection_id"] = collection_id if is_deep_copy is not None: payload["is_deep_copy"] = is_deep_copy if collection_position is not None: payload["collection_position"] = collection_position logger.info(f"Copying dashboard {from_dashboard_id} to '{name}'") return await make_metabase_request( RequestMethod.POST, f"/api/dashboard/{from_dashboard_id}/copy", json=payload ) - src/metabase_mcp_server.py:162-247 (helper)Helper function 'make_metabase_request' that handles all HTTP communication with the Metabase API. It creates authenticated requests with proper headers, handles errors (connection errors, 5xx responses, non-JSON responses), and returns parsed JSON responses.
async def make_metabase_request( method: RequestMethod, endpoint: str, data: Optional[Dict[str, Any] | bytes] = None, params: Optional[Dict[str, Any]] = None, json: Any = None, headers: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: """ Make a request to the Metabase API. Args: method: HTTP method to use (GET, POST, PUT, DELETE) endpoint: API endpoint path data: Request data (for form data) params: URL parameters json: JSON request body headers: Additional headers Returns: Dict[str, Any]: Response data Raises: MetabaseConnectionError: When the Metabase server is unreachable MetabaseResponseError: When Metabase returns a non-2xx status code RuntimeError: For other errors """ if not METABASE_URL or not METABASE_API_KEY: raise RuntimeError("METABASE_URL or METABASE_API_KEY environment variable is not set. Metabase API requests will fail.") if session is None: raise RuntimeError("HTTP session is not initialized. Ensure app_lifespan was called.") try: request_headers = headers or {} logger.debug(f"Making {method.name} request to {METABASE_URL}{endpoint}") # Log request payload for debugging (omit sensitive info) if json and logger.level <= logging.DEBUG: sanitized_json = {**json} if 'password' in sanitized_json: sanitized_json['password'] = '********' logger.debug(f"Request payload: {sanitized_json}") response = await session.request( method=method.name, url=endpoint, timeout=aiohttp.ClientTimeout(total=30), headers=request_headers, data=data, params=params, json=json, ) try: # Handle 500 errors with more detailed info if response.status >= 500: error_text = await response.text() logger.error(f"Server error {response.status}: {error_text[:200]}") raise MetabaseResponseError(response.status, f"Server Error: {error_text[:200]}", endpoint) response.raise_for_status() response_data = await response.json() # Ensure the response is a dictionary for FastMCP compatibility return ensure_dict_response(response_data) except aiohttp.ContentTypeError: # Handle empty responses or non-JSON responses content = await response.text() if not content: return {"data": {}} logger.warning(f"Received non-JSON response: {content}") return {"data": content} except aiohttp.ClientConnectionError as e: logger.error(f"Connection error: {str(e)}") raise MetabaseConnectionError("Metabase is unreachable. Is the Metabase server running?") from e except aiohttp.ClientResponseError as e: logger.error(f"Response error: {e.status}, {e.message}, {e.request_info.url}") raise MetabaseResponseError(e.status, e.message, str(e.request_info.url)) from e except Exception as e: logger.error(f"Request error: {str(e)}") raise RuntimeError(f"Request error: {str(e)}") from e - src/metabase_mcp_server.py:159-160 (registration)FastMCP instance initialization. The 'mcp' object is created with the name 'metabase' and a lifespan context manager. This object is used to register tools via the @mcp.tool() decorator.
# Initialize FastMCP agent mcp = FastMCP("metabase", lifespan=app_lifespan) - src/enums/request_enum.py:1-11 (schema)RequestMethod enum defining HTTP methods (GET, POST, PUT, DELETE) used for API requests. The copy_metabase_dashboard handler uses RequestMethod.POST for the copy operation.
from enum import Enum, auto class RequestMethod(Enum): GET = auto() POST = auto() PUT = auto() DELETE = auto() def __str__(self): return self.name