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:
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.
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.
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
- Create a card via direct Metabase API (curl) — confirm name and dataset_query persist.
- Call
update_card:
{ "card_id": 166, "updates": { "name": "Renamed via update_card" } }
- 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).
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.
Tool schema bugs cause silent data loss in
update_card, hard failure increate_card;get_dashboard_cardsreturns empty arraySummary
Three reproducible bugs found in
@cognitionai/metabase-mcp-server@1.0.14against Metabasev0.60.3:update_card— acceptsupdatesparameter, 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.create_card— acceptsdataset_queryparameter, but the value is dropped before being sent to Metabase, causing the upstream API to reject the malformed request with HTTP 500.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: {}andadditionalProperties: false, which strips every key the caller provides.Environment
@cognitionai/metabase-mcp-server1.0.14(vianpx)v0.60.3 (c86baaf), SQL Server backend (enginesqlserver)METABASE_API_KEY)Bug 1 —
update_cardsilently no-opsTool 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+ emptyproperties→ MCP/JSON-schema validation drops every key insideupdates.Reproduction
update_card:{ "card_id": 166, "updates": { "name": "Renamed via update_card" } }get_card({ "card_id": 166 }).Expected
nameshould be"Renamed via update_card".Actual
updated_atandcache_invalidated_atchange (so the request reached Metabase).nameis unchanged. Same result when sendingdataset_query,display, or any other field insideupdates.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_carddropsdataset_query, fails with 500Tool 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
Metabase logs (when reproducible on the maintainer's side) should show a
POST /api/cardwith nodataset_querybody — because the MCP stripped it.visualization_settingshas the same schema bug, though it's optional so it doesn't trigger the 500 on its own.Bug 3 —
get_dashboard_cardsreturns[]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-emptydashcardsfield — 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}/cardsis not a standard Metabase endpoint — cards live insideGET /api/dashboard/{id}underdashcards).Suggested fix
For Bugs 1 and 2 — replace the empty
properties: {}placeholder with the actual sub-schema, or relax toadditionalProperties: trueso 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 isadditionalProperties: truesince Metabase'sPUT /api/card/{id}accepts an open partial-update payload.For Bug 3, replace the implementation with
GET /api/dashboard/{id}and project.dashcardsin the response.Workarounds (current)
create_card/update_card: bypass the MCP and callPOST/PUT /api/card[/id]via raw HTTP usingMETABASE_API_KEY.get_dashboard_cards: useget_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.