Skip to main content
Glama

metabase-mcp

A self-hosted MCP server that puts Metabase behind a static bearer token instead of OAuth.

Metabase v0.63+ ships its own MCP server at /api/metabase-mcp, but it is OAuth-only. On the Claude client side the token exchange (POST /oauth/token) is currently never performed: discovery, dynamic client registration and user authorization all succeed, and the fourth leg never fires, so the connector can never authenticate (metabase/metabase #75084, #74962, #80389).

This server sidesteps that entirely: it speaks Metabase's ordinary REST API using an API key held server-side, and exposes those capabilities over MCP behind a single static token — the same pattern that already works for self-hosted Firefly III, Obsidian and n8n MCP servers.

Claude  ──Authorization: Bearer <MCP_AUTH_TOKEN>──▶  metabase-mcp  ──X-API-KEY: <METABASE_API_KEY>──▶  Metabase
  • Transport: streamable HTTP at POST /mcp, listening on 0.0.0.0:3000

  • Inbound auth: Authorization: Bearer <MCP_AUTH_TOKEN>, compared in constant time

  • Outbound auth: X-API-KEY: <METABASE_API_KEY> against METABASE_URL

  • Health check: GET /health (unauthenticated, returns service name, version and tool count)

Environment variables

Variable

Required

Default

Purpose

METABASE_URL

yes

Base URL of the Metabase instance, e.g. https://insights.example.com. Trailing slashes are trimmed.

METABASE_API_KEY

yes

Metabase API key, sent outbound as X-API-KEY.

MCP_AUTH_TOKEN

yes

Static token MCP clients must send as Authorization: Bearer …. MCP_TOKEN is accepted as an alias.

PORT

no

3000

Listen port.

METABASE_TIMEOUT_MS

no

60000

Per-request timeout against Metabase. 60s is deliberate: some queries scan thousands of rows.

MAX_RESPONSE_ROWS

no

2000

Ceiling on rows returned by the query tools, so one query cannot flood the conversation.

If METABASE_URL, METABASE_API_KEY or MCP_AUTH_TOKEN is missing the server logs a single line naming every missing variable and exits with status 1 — it never starts half-configured:

[metabase-mcp] FATAL: Missing required environment variable(s): METABASE_API_KEY. ...

Related MCP server: Metabase MCP Server

docker-compose

services:
  metabase-mcp:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      METABASE_URL: ${METABASE_URL}
      METABASE_API_KEY: ${METABASE_API_KEY}
      MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN}
      PORT: 3000
      METABASE_TIMEOUT_MS: ${METABASE_TIMEOUT_MS:-60000}
      MAX_RESPONSE_ROWS: ${MAX_RESPONSE_ROWS:-2000}
    expose:
      - "3000"
    healthcheck:
      test:
        - CMD
        - node
        - -e
        - "fetch('http://127.0.0.1:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    networks:
      - dokploy-network

networks:
  dokploy-network:
    external: true

The container only needs expose: 3000; Dokploy's Traefik adds the router and the certificate when a domain is attached to the service, so no ports are published to the host.

Running it anywhere else is the same minus the external network:

docker build -t metabase-mcp .
docker run --rm -p 3000:3000 \
  -e METABASE_URL=https://insights.example.com \
  -e METABASE_API_KEY=mb_xxx \
  -e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
  metabase-mcp

Connecting Claude

Settings → Connectors → Add custom connector:

  • URL: https://metabase-mcp.example.com/mcp

  • Authentication: None

  • Custom header: name Authorization, value Bearer <MCP_AUTH_TOKEN>

Claude reads the tool list when a conversation starts, so start a fresh chat after connecting or after changing the tools.

Tools

All 27 tools use JSON Schema draft-07 with additionalProperties: false. Metabase's own error body is passed through verbatim on any non-2xx — its validation messages are the useful part.

Databases and schema

Tool

Metabase endpoint

list_databases

GET /api/database

get_database

GET /api/database/:id

list_tables

GET /api/database/:id/metadata

get_table

GET /api/table/:id/query_metadata

get_table_field_values

GET /api/field/:id/values

Querying

Tool

Metabase endpoint

execute_query

POST /api/dataset — native SQL or MBQL, returns rows

execute_saved_question

POST /api/card/:id/query

construct_query

none — builds an MBQL dataset_query from a structured description

visualize_query

none, or PUT /api/card/:id when question_id is given

Questions (cards)

Tool

Metabase endpoint

create_question

POST /api/card

update_question

PUT /api/card/:id

get_question

GET /api/card/:id

list_questions

GET /api/card

archive_question

PUT /api/card/:id with {archived: true}

Dashboards

Tool

Metabase endpoint

create_dashboard

POST /api/dashboard

update_dashboard

PUT /api/dashboard/:id

get_dashboard

GET /api/dashboard/:id

list_dashboards

GET /api/dashboard

add_card_to_dashboard

GET /api/dashboard/:id then PUT /api/dashboard/:id

archive_dashboard

PUT /api/dashboard/:id with {archived: true}

Collections, search and metrics

Tool

Metabase endpoint

create_collection

POST /api/collection

list_collections

GET /api/collection

get_collection_items

GET /api/collection/:id/items

search

GET /api/search

list_metrics

GET /api/metric

get_metric

GET /api/metric/:id

get_metric_field_values

GET /api/metric/:id/dimension/:key/values

Charts in one call

create_question takes display and visualization_settings as first-class parameters, so a chart is created correctly without a follow-up edit:

{
  "name": "Spend by month",
  "database_id": 2,
  "sql": "SELECT DATE_FORMAT(date, '%Y-%m') AS month, SUM(amount) AS total FROM transactions GROUP BY 1",
  "display": "line",
  "visualization_settings": {
    "graph.dimensions": ["month"],
    "graph.metrics": ["total"],
    "card.title": "Spend by month"
  }
}

visualize_query builds that visualization_settings object for you from display, dimension_column and metric_columns, and applies it to an existing question if you pass question_id.

Dashboard layout

Metabase lays dashboards out on a 24-column grid. add_card_to_dashboard defaults to a half-width card (size_x: 12, size_y: 8) placed in the first free slot — scanning rows top to bottom, columns left to right — so two cards land side by side and the third starts a new row. Pass row, col, size_x and size_y to place a card yourself.

Because Metabase replaces a dashboard's entire card set on update, the tool first reads the dashboard, re-sends every existing card unchanged and appends the new one with a negative id (Metabase's convention for "create this"). Existing positions are preserved.

Notes on the Metabase API

Verified against the release-x.63.x branch of metabase/metabase (src/metabase/api_routes/routes.clj and the API namespaces it references). Differences worth knowing:

  • POST /api/card has no top-level database_id. Its schema requires name, dataset_query, display and visualization_settings; the database belongs inside dataset_query.database. This server accepts the friendlier database_id argument and folds it into the query for you.

  • PUT /api/dashboard/:id/cards is deprecated in 0.63 in favour of PUT /api/dashboard/:id with a dashcards array, and it is a full-set replacement rather than an append. add_card_to_dashboard uses the current endpoint.

  • GET /api/dashboard is marked deprecated but still served; it only supports the filter modes all, mine and archived. For anything richer use search with models: ["dashboard"].

  • GET /api/card has no collection filter. Its only filters are f (all, mine, bookmarked, database, table, using_model, using_segment, archived) plus model_id. To list one collection use get_collection_items or search.

  • /api/metric is the modern metrics API. A metric is a card with type: "metric"; GET /api/metric returns {total, limit, offset, data}. Field values for a metric come from GET /api/metric/:id/dimension/:dimension-key/values, where the key is a UUID from get_metric — not from /api/field/:id/values.

  • POST /api/collection names the parent parent_id, not collection_id.

  • A broken query is not an HTTP error. POST /api/dataset answers with HTTP 202 and status: "failed" in the body. The query tools detect that and raise it as a tool error carrying Metabase's message, rather than reporting zero rows.

get_database and list_tables strip the details object from responses: it holds warehouse connection settings, which an admin API key can read and which have no business reaching a model.

Development

npm install
npm run typecheck   # tsc --noEmit, strict mode
npm test            # compiles, then runs the offline unit tests
npm run build       # compile to dist/
npm start           # run the compiled server

The tests are fully offline: HTTP is mocked by injecting a fetch implementation into the Metabase client, and they assert on schema shape, argument validation, URL and body construction, error bubbling, the MBQL builder and the dashboard layout packer. No Metabase instance is needed.

Security

  • The Metabase API key never leaves the server; clients only ever hold MCP_AUTH_TOKEN.

  • The token is compared with timingSafeEqual, and /mcp returns 401 before any MCP handling.

  • Anything the API key can do, a connected client can do. Create the key against a Metabase user with only the permissions you want exposed.

  • Terminate TLS in front of the server (Traefik/Dokploy does this); the token is a bearer credential and must not travel over plain HTTP.

Licence

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to interact with Metabase through its API, allowing them to list, read, execute, and manage databases, tables, dashboards, cards, collections, and queries with support for multiple export formats.
    -
  • F
    license
    A
    quality
    D
    maintenance
    Enables LLMs to interact with Metabase instances through 33 tools for querying cards, dashboards, databases, collections, and executing SQL queries against your Metabase installation.
    23
    -
  • F
    license
    Not graded
    quality
    Not graded
    maintenance
    Enables AI assistants to interact with Metabase by providing access to dashboards, questions, and databases through the Metabase API. It allows users to list resources, execute existing cards, and run custom SQL queries to retrieve data through natural language.
    15 npm
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    A safety-gated Python MCP server for Metabase that lets MCP clients and agents inspect Metabase, run governed queries, and create or maintain dashboards, cards/questions, collections, snippets, permissions, and other Metabase assets through the Metabase REST API.
    GPL 3.0