Skip to content

Tool schema bugs cause silent data loss in update_card, hard failure in create_card; get_dashboard_cards returns empty array #33

Description

@marcelopossa

Tool schema bugs cause silent data loss in update_card, hard failure in create_card; get_dashboard_cards returns empty array

Summary

Three reproducible bugs found in @cognitionai/metabase-mcp-server@1.0.14 against Metabase v0.60.3:

  1. update_card — accepts updates parameter, returns HTTP 200 with full card object, but nothing actually changes (silent failure). Most dangerous of the three because scripts that check status code think it succeeded.
  2. create_card — accepts dataset_query parameter, but the value is dropped before being sent to Metabase, causing the upstream API to reject the malformed request with HTTP 500.
  3. get_dashboard_cards — returns [] for dashboards that demonstrably contain cards.

Bugs 1 and 2 share the same root cause: the tool input schemas declare nested objects with properties: {} and additionalProperties: false, which strips every key the caller provides.

Environment

  • Package: @cognitionai/metabase-mcp-server 1.0.14 (via npx)
  • Metabase: v0.60.3 (c86baaf), SQL Server backend (engine sqlserver)
  • Node: 20.x
  • MCP host: Claude Code (also reproducible via any MCP client that surfaces the tool schema)
  • Auth: API key (METABASE_API_KEY)

Bug 1 — update_card silently no-ops

Tool schema (as exposed by the server)

{
  "name": "update_card",
  "parameters": {
    "card_id":   { "type": "number" },
    "updates":   { "type": "object", "properties": {}, "additionalProperties": false },
    "query_params": { "type": "object", "properties": {}, "additionalProperties": false }
  },
  "required": ["card_id", "updates"]
}

additionalProperties: false + empty properties → MCP/JSON-schema validation drops every key inside updates.

Reproduction

  1. Create a card via direct Metabase API (curl) — confirm name and dataset_query persist.
  2. Call update_card:
    { "card_id": 166, "updates": { "name": "Renamed via update_card" } }
  3. Call get_card({ "card_id": 166 }).

Expected

name should be "Renamed via update_card".

Actual

  • HTTP 200 returned by the tool, full card object in response.
  • updated_at and cache_invalidated_at change (so the request reached Metabase).
  • name is unchanged. Same result when sending dataset_query, display, or any other field inside updates.

Sending the same body via direct curl PUT /api/card/{id} works correctly, confirming Metabase itself is fine — the MCP layer is dropping the payload.

Bug 2 — create_card drops dataset_query, fails with 500

Tool schema

{
  "name": "create_card",
  "parameters": {
    "name": { "type": "string" },
    "display": { "type": "string" },
    "collection_id": { "type": "number" },
    "dataset_query": { "type": "object", "properties": {}, "additionalProperties": false },
    "visualization_settings": { "type": "object", "properties": {}, "additionalProperties": false }
  }
}

Same pattern as Bug 1.

Reproduction

{
  "name": "Test card",
  "display": "table",
  "collection_id": 9,
  "dataset_query": {
    "type": "native",
    "native": { "query": "SELECT 1", "template-tags": {} },
    "database": 2
  }
}

Expected

Card created in collection 9 with the specified native SQL query.

Actual

Tool 'create_card' execution failed: Failed to create card: Request failed with status code 500

Metabase logs (when reproducible on the maintainer's side) should show a POST /api/card with no dataset_query body — because the MCP stripped it.

visualization_settings has the same schema bug, though it's optional so it doesn't trigger the 500 on its own.

Bug 3 — get_dashboard_cards returns []

Reproduction

Pick any dashboard that has cards (verifiable via the Metabase UI or get_dashboard).

{ "dashboard_id": 10 }

Expected

Array of dashcard objects.

Actual

[]

For the same dashboard, get_dashboard({ "dashboard_id": 10 }) returns the dashboard with a non-empty dashcards field — so the data is reachable; the dedicated tool just doesn't surface it.

Likely cause: the implementation may be calling an endpoint that doesn't return the embedded cards (e.g. /api/dashboard/{id}/cards is not a standard Metabase endpoint — cards live inside GET /api/dashboard/{id} under dashcards).

Suggested fix

For Bugs 1 and 2 — replace the empty properties: {} placeholder with the actual sub-schema, or relax to additionalProperties: true so the caller can pass through arbitrary keys. The two minimal patches:

 "dataset_query": {
   "type": "object",
-  "properties": {},
-  "additionalProperties": false
+  "additionalProperties": true
 }

A typed schema is preferable, e.g.:

{
  "type": "object",
  "properties": {
    "type":     { "enum": ["native", "query"] },
    "database": { "type": "number" },
    "native":   { "type": "object", "additionalProperties": true },
    "query":    { "type": "object", "additionalProperties": true }
  },
  "required": ["type", "database"]
}

For update_card.updates, the simplest fix is additionalProperties: true since Metabase's PUT /api/card/{id} accepts an open partial-update payload.

For Bug 3, replace the implementation with GET /api/dashboard/{id} and project .dashcards in the response.

Workarounds (current)

  • create_card / update_card: bypass the MCP and call POST/PUT /api/card[/id] via raw HTTP using METABASE_API_KEY.
  • get_dashboard_cards: use get_dashboard({id}) and read .dashcards.

Why it matters

Bug 1 is the worst — it returns success on a no-op write. Anyone using these tools in an LLM-driven loop ("update this card's query, then verify") sees the tool report success and skips verification, leaving the dashboard in a stale state. Suggest prioritizing this one even ahead of the schema fixes for Bug 2.

Happy to test patches if it helps.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions