update_card
Update a saved Metabase card's properties: name, description, SQL query, database, display type, collection, visualization settings, and archive status. Preserve template tags on query change or replace them atomically with new tags.
Instructions
Update properties of a saved question/card in Metabase.
When query is updated, existing template-tags on the card are preserved
by default (re-sent alongside the new SQL) so filters wired to the card
aren't silently wiped. Pass template_tags to set/replace them atomically
with the SQL update.
Args:
card_id: The ID of the card to update.
name: New name for the card.
description: New description for the card.
query: New SQL query for the card.
database_id: New database ID (required if changing query).
display: Display type (e.g. "table", "bar", "line", "pie", "scalar", "row", "area", "combo", "pivot", "smartscalar", "funnel", "waterfall", "map").
collection_id: Move the card to a different collection.
visualization_settings: Visualization settings to apply.
archived: Set to true to archive the card, false to unarchive.
template_tags: Replacement template-tags map (tag-name -> config). If
provided, replaces the card's template-tags. If omitted while
query is set, existing template-tags are preserved unchanged.
Returns: The updated card object.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| card_id | Yes | ||
| name | No | ||
| description | No | ||
| query | No | ||
| database_id | No | ||
| display | No | ||
| collection_id | No | ||
| visualization_settings | No | ||
| archived | No | ||
| template_tags | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- server.py:597-696 (handler)The main handler for the 'update_card' tool. It accepts card_id, name, description, query, database_id, display, collection_id, visualization_settings, archived, and template_tags. It builds a payload, optionally fetches the current card to merge dataset_query changes (preserving existing template-tags if not explicitly replaced), and sends a PUT request to /api/card/{card_id}.
@mcp.tool async def update_card( card_id: int, ctx: Context, name: str | None = None, description: str | None = None, query: str | None = None, database_id: int | None = None, display: str | None = None, collection_id: int | None = None, visualization_settings: dict[str, Any] | None = None, archived: bool | None = None, template_tags: dict[str, dict[str, Any]] | None = None, ) -> dict[str, Any]: """ Update properties of a saved question/card in Metabase. When `query` is updated, existing template-tags on the card are preserved by default (re-sent alongside the new SQL) so filters wired to the card aren't silently wiped. Pass `template_tags` to set/replace them atomically with the SQL update. Args: card_id: The ID of the card to update. name: New name for the card. description: New description for the card. query: New SQL query for the card. database_id: New database ID (required if changing query). display: Display type (e.g. "table", "bar", "line", "pie", "scalar", "row", "area", "combo", "pivot", "smartscalar", "funnel", "waterfall", "map"). collection_id: Move the card to a different collection. visualization_settings: Visualization settings to apply. archived: Set to true to archive the card, false to unarchive. template_tags: Replacement template-tags map (tag-name -> config). If provided, replaces the card's template-tags. If omitted while `query` is set, existing template-tags are preserved unchanged. Returns: The updated card object. """ try: await ctx.info(f"Updating card {card_id}") payload: dict[str, Any] = {} if name is not None: payload["name"] = name if description is not None: payload["description"] = description if display is not None: payload["display"] = display if collection_id is not None: payload["collection_id"] = collection_id if visualization_settings is not None: payload["visualization_settings"] = visualization_settings if archived is not None: payload["archived"] = archived if query is not None or template_tags is not None: current_card = await metabase_client.request("GET", f"/card/{card_id}") current_dq = current_card.get("dataset_query") or {} current_sql, _ = _read_native_stage(current_dq) new_sql = query if query is not None else current_sql if not new_sql: raise ToolError( f"Cannot update card {card_id} dataset_query: no SQL text available " "(neither `query` provided nor existing SQL found on the card)." ) new_tags_arg: dict[str, Any] | None = None if template_tags is not None: new_tags_arg = {} for tag_name, cfg in template_tags.items(): entry = dict(cfg) entry.setdefault("name", tag_name) new_tags_arg[tag_name] = entry new_dq = _merge_native_stage( current_dq, sql=new_sql, template_tags=new_tags_arg ) if database_id is not None: new_dq["database"] = database_id elif "database" not in new_dq: new_dq["database"] = current_card.get("database_id") new_dq.setdefault("type", "native") payload["dataset_query"] = new_dq if not payload: raise ToolError("No update fields provided. Specify at least one field to update.") result = await metabase_client.request("PUT", f"/card/{card_id}", json=payload) await ctx.info(f"Successfully updated card {card_id}") return result except ToolError: raise except Exception as e: error_msg = f"Error updating card {card_id}: {e}" await ctx.error(error_msg) raise ToolError(error_msg) from e - server.py:597-597 (registration)The @mcp.tool decorator on the async function registers 'update_card' as a tool with the FastMCP server.
@mcp.tool - server.py:610-610 (schema)The template_tags parameter type: dict[str, dict[str, Any]] defines the schema for optional template-tag replacement.
) -> dict[str, Any]: - server.py:263-278 (helper)Helper function _read_native_stage used by update_card to extract current SQL and template-tags from a card's dataset_query (handling both stages and legacy native shapes).
def _read_native_stage( dataset_query: dict[str, Any], ) -> tuple[str | None, dict[str, Any]]: """ Return (sql, template_tags) from a card's dataset_query. Handles both Metabase shapes: - stages shape: dataset_query.stages[0] = {"native": "<SQL>", "template-tags": {...}, ...} - legacy shape: dataset_query.native = {"query": "<SQL>", "template-tags": {...}} """ stages = dataset_query.get("stages") if isinstance(stages, list) and stages: stage = stages[0] or {} return stage.get("native"), dict(stage.get("template-tags") or {}) native = dataset_query.get("native") or {} return native.get("query"), dict(native.get("template-tags") or {}) - server.py:281-315 (helper)Helper function _merge_native_stage used by update_card to merge new SQL and/or template-tags into a dataset_query while preserving sibling fields.
def _merge_native_stage( dataset_query: dict[str, Any], *, sql: str | None = None, template_tags: dict[str, Any] | None = None, ) -> dict[str, Any]: """ Return a shallow-copy of `dataset_query` with `sql` and/or `template-tags` applied to the native stage, preserving all sibling fields (native/stages, lib/type, collection, etc.). Leaves root-level keys like database/type untouched — callers override those explicitly if needed. Pass only the fields you want to change: unset args keep their current values. """ new_dq = dict(dataset_query) stages = dataset_query.get("stages") if isinstance(stages, list) and stages: new_stages = [dict(s) if isinstance(s, dict) else s for s in stages] stage0 = new_stages[0] if isinstance(new_stages[0], dict) else {} if sql is not None: stage0["native"] = sql if template_tags is not None: stage0["template-tags"] = template_tags new_stages[0] = stage0 new_dq["stages"] = new_stages return new_dq new_native = dict(dataset_query.get("native") or {}) if sql is not None: new_native["query"] = sql if template_tags is not None: new_native["template-tags"] = template_tags new_dq["native"] = new_native return new_dq