create_metabase_card
Create visual charts and tables in Metabase using SQL or MBQL queries to display data insights through various visualization types.
Instructions
Create a new card (chart or table) in Metabase via the /api/card endpoint.
This function creates a visual card using either SQL or MBQL queries and supports all chart types including pie, donut, bar, table, and KPI-style metrics.
Args: name (str): Display name of the card in Metabase.
dataset_query (dict):
Defines the query behind the chart.
Required structure:
- "type": "native" or "query"
- "native": { "query": "..." }, for SQL
- "query": {...}, for MBQL
- "database": database ID
display (str):
Visualization type. Common values:
- "table", "bar", "line", "pie", "area", "scatter", "funnel", "pivot-table", "map"
type (str, optional):
Card type, defaults to "question".
- "question": general chart or table
- "metric": for KPI display
- "model": reserved/legacy
visualization_settings (dict, optional):
Controls chart appearance and formatting. Structure varies by chart type.
ββ π Bar / Line / Area ββ
{
"graph": {
"x_axis": "destination",
"y_axis": ["seatsSold"],
"series": "flightType",
"metrics": ["seatsSold"],
"x_axis_label": "Destination",
"y_axis_label": "Seats Sold",
"x_axis_formatting": {
"scale": "ordinal",
"label_rotation": 45
},
"y_axis_formatting": {
"number_style": "decimal",
"suffix": " pax"
}
},
"show_legend": true,
"legend_position": "bottom"
}
ββ π₯§ Pie / Donut Charts ββ
{
"pie": {
"category": "destination", # Label or group for slices
"metric": "seatsSold", # Size of each slice
"labels": true, # Show category names
"show_values": true, # Show numeric values inside slices
"inner_radius": 0.6, # Enables donut (0 = full pie)
"outer_radius": 0.95, # Size scaling (0.0 to 1.0)
"outer_ring": true # Enables dual-ring charts
},
"show_legend": true,
"legend_position": "right"
}
Notes on ring options:
- `inner_radius` creates a donut shape. Recommended: 0.5β0.8.
- `outer_radius` controls the size of the entire chart area.
- `outer_ring` enables comparison across rings, useful when the query returns multiple groupings/metrics.
ββ π Table ββ
{
"table.pivot_column": "flightType",
"column_settings": {
"seatsSold": {
"number_style": "decimal",
"suffix": " pax"
}
}
}
collection_id (int, optional):
Save card into a specific Metabase collection (folder).
description (str, optional):
Description or help text for the card.
parameter_mappings (list, optional):
Used when linking dashboard filters to this card.
Example:
[
{
"parameter_id": "flightType",
"card_id": 123,
"target": ["dimension", ["template-tag", "flightType"]]
}
]
collection_position (int, optional):
Optional order in the collection.
result_metadata (list, optional):
Optional field metadata describing result set.
cache_ttl (int, optional):
Cache duration (in seconds). 0 disables caching.
parameters (list, optional):
List of query parameters for SQL or MBQL filters.
Example: [{"name": "region", "type": "category", "slug": "region"}]
dashboard_id (int, optional):
Adds this card to an existing dashboard.
dashboard_tab_id (int, optional):
If the dashboard has tabs, specify the tab ID to attach the card to.
entity_id (str, optional):
External or custom ID for embedding/syncing cards.Returns: Dict[str, Any]: A dictionary representing the created card including: - id (int) - name (str) - dataset_query (dict) - visualization_settings (dict) - created_at, updated_at, etc.
Example: >>> await create_metabase_card( name="Seats Sold by Destination (Donut with Outer Ring)", display="pie", dataset_query={ "type": "native", "native": { "query": "SELECT destination, SUM("seatsSold") AS total_seats_sold FROM "Flight" GROUP BY destination" }, "database": 2 }, visualization_settings={ "pie": { "category": "destination", "metric": "total_seats_sold", "labels": true, "inner_radius": 0.6, "outer_radius": 0.95, "show_values": true, "outer_ring": true }, "show_legend": true, "legend_position": "right" }, collection_id=3 )
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | ||
| dataset_query | Yes | ||
| display | Yes | ||
| type | No | question | |
| visualization_settings | No | ||
| collection_id | No | ||
| description | No | ||
| parameter_mappings | No | ||
| collection_position | No | ||
| result_metadata | No | ||
| cache_ttl | No | ||
| parameters | No | ||
| dashboard_id | No | ||
| dashboard_tab_id | No | ||
| entity_id | No |
Implementation Reference
- src/metabase_mcp_server.py:349-570 (handler)The main handler function for 'create_metabase_card' tool. It creates a new card (chart or table) in Metabase via the /api/card endpoint. Accepts parameters for name, dataset_query, display type, visualization_settings, and other optional card properties. It builds a payload and calls make_metabase_request() to POST to the Metabase API.
@mcp.tool() async def create_metabase_card( name: str, dataset_query: Dict[str, Any], display: str, type: str = "question", visualization_settings: Optional[Union[Dict[str, Any], str]] = None, collection_id: Optional[int] = None, description: Optional[str] = None, parameter_mappings: Optional[List] = None, collection_position: Optional[int] = None, result_metadata: Optional[List] = None, cache_ttl: Optional[int] = None, parameters: Optional[List] = None, dashboard_id: Optional[int] = None, dashboard_tab_id: Optional[int] = None, entity_id: Optional[str] = None ) -> Dict[str, Any]: """ Create a new card (chart or table) in Metabase via the /api/card endpoint. This function creates a visual card using either SQL or MBQL queries and supports all chart types including pie, donut, bar, table, and KPI-style metrics. Args: name (str): Display name of the card in Metabase. dataset_query (dict): Defines the query behind the chart. Required structure: - "type": "native" or "query" - "native": { "query": "..." }, for SQL - "query": {...}, for MBQL - "database": database ID display (str): Visualization type. Common values: - "table", "bar", "line", "pie", "area", "scatter", "funnel", "pivot-table", "map" type (str, optional): Card type, defaults to "question". - "question": general chart or table - "metric": for KPI display - "model": reserved/legacy visualization_settings (dict, optional): Controls chart appearance and formatting. Structure varies by chart type. ββ π Bar / Line / Area ββ { "graph": { "x_axis": "destination", "y_axis": ["seatsSold"], "series": "flightType", "metrics": ["seatsSold"], "x_axis_label": "Destination", "y_axis_label": "Seats Sold", "x_axis_formatting": { "scale": "ordinal", "label_rotation": 45 }, "y_axis_formatting": { "number_style": "decimal", "suffix": " pax" } }, "show_legend": true, "legend_position": "bottom" } ββ π₯§ Pie / Donut Charts ββ { "pie": { "category": "destination", # Label or group for slices "metric": "seatsSold", # Size of each slice "labels": true, # Show category names "show_values": true, # Show numeric values inside slices "inner_radius": 0.6, # Enables donut (0 = full pie) "outer_radius": 0.95, # Size scaling (0.0 to 1.0) "outer_ring": true # Enables dual-ring charts }, "show_legend": true, "legend_position": "right" } Notes on ring options: - `inner_radius` creates a donut shape. Recommended: 0.5β0.8. - `outer_radius` controls the size of the entire chart area. - `outer_ring` enables comparison across rings, useful when the query returns multiple groupings/metrics. ββ π Table ββ { "table.pivot_column": "flightType", "column_settings": { "seatsSold": { "number_style": "decimal", "suffix": " pax" } } } collection_id (int, optional): Save card into a specific Metabase collection (folder). description (str, optional): Description or help text for the card. parameter_mappings (list, optional): Used when linking dashboard filters to this card. Example: [ { "parameter_id": "flightType", "card_id": 123, "target": ["dimension", ["template-tag", "flightType"]] } ] collection_position (int, optional): Optional order in the collection. result_metadata (list, optional): Optional field metadata describing result set. cache_ttl (int, optional): Cache duration (in seconds). 0 disables caching. parameters (list, optional): List of query parameters for SQL or MBQL filters. Example: [{"name": "region", "type": "category", "slug": "region"}] dashboard_id (int, optional): Adds this card to an existing dashboard. dashboard_tab_id (int, optional): If the dashboard has tabs, specify the tab ID to attach the card to. entity_id (str, optional): External or custom ID for embedding/syncing cards. Returns: Dict[str, Any]: A dictionary representing the created card including: - id (int) - name (str) - dataset_query (dict) - visualization_settings (dict) - created_at, updated_at, etc. Example: >>> await create_metabase_card( name="Seats Sold by Destination (Donut with Outer Ring)", display="pie", dataset_query={ "type": "native", "native": { "query": "SELECT destination, SUM(\"seatsSold\") AS total_seats_sold FROM \"Flight\" GROUP BY destination" }, "database": 2 }, visualization_settings={ "pie": { "category": "destination", "metric": "total_seats_sold", "labels": true, "inner_radius": 0.6, "outer_radius": 0.95, "show_values": true, "outer_ring": true }, "show_legend": true, "legend_position": "right" }, collection_id=3 ) """ payload = { "name": name, "dataset_query": dataset_query, "display": display, "type": type, } # Ensure visualization_settings is a proper dict and not a string if visualization_settings is not None: if isinstance(visualization_settings, str): try: import json visualization_settings = json.loads(visualization_settings) except json.JSONDecodeError: logger.error("Invalid JSON in visualization_settings") raise ValueError("visualization_settings must be a valid JSON object") payload["visualization_settings"] = visualization_settings else: payload["visualization_settings"] = {} if collection_id is not None: payload["collection_id"] = collection_id if description is not None: payload["description"] = description if parameter_mappings is not None: payload["parameter_mappings"] = parameter_mappings if collection_position is not None: payload["collection_position"] = collection_position if result_metadata is not None: payload["result_metadata"] = result_metadata if cache_ttl is not None: payload["cache_ttl"] = cache_ttl if parameters is not None: payload["parameters"] = parameters if dashboard_id is not None: payload["dashboard_id"] = dashboard_id if dashboard_tab_id is not None: payload["dashboard_tab_id"] = dashboard_tab_id if entity_id is not None: payload["entity_id"] = entity_id logger.info(f"Creating card '{name}'") return await make_metabase_request(RequestMethod.POST, "/api/card", json=payload) - src/metabase_mcp_server.py:349-349 (registration)The @mcp.tool() decorator registers the create_metabase_card function as an MCP tool. The 'mcp' object is a FastMCP instance initialized at line 160.
@mcp.tool() - src/metabase_mcp_server.py:160-160 (registration)FastMCP instance initialization: mcp = FastMCP("metabase", lifespan=app_lifespan). This creates the MCP server object that the @mcp.tool() decorator uses to register tools.
mcp = FastMCP("metabase", lifespan=app_lifespan) - src/metabase_mcp_server.py:162-240 (helper)Helper function make_metabase_request() used by create_metabase_card to make HTTP requests to the Metabase API. Handles authentication, error handling, and response processing.
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)}")