metabase-mcp
Provides tools for interacting with a Metabase instance's REST API behind a static bearer token, including listing databases, tables and fields, executing native SQL or MBQL queries and saved questions, building and visualizing queries, and managing questions (cards), dashboards (including 24-column grid layout and adding cards), collections, search, and metrics.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@metabase-mcprun a SQL query joining orders and customers for the last month"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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>──▶ MetabaseTransport: streamable HTTP at
POST /mcp, listening on0.0.0.0:3000Inbound auth:
Authorization: Bearer <MCP_AUTH_TOKEN>, compared in constant timeOutbound auth:
X-API-KEY: <METABASE_API_KEY>againstMETABASE_URLHealth check:
GET /health(unauthenticated, returns service name, version and tool count)
Environment variables
Variable | Required | Default | Purpose |
| yes | — | Base URL of the Metabase instance, e.g. |
| yes | — | Metabase API key, sent outbound as |
| yes | — | Static token MCP clients must send as |
| no |
| Listen port. |
| no |
| Per-request timeout against Metabase. 60s is deliberate: some queries scan thousands of rows. |
| no |
| 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: trueThe 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-mcpConnecting Claude
Settings → Connectors → Add custom connector:
URL:
https://metabase-mcp.example.com/mcpAuthentication: None
Custom header: name
Authorization, valueBearer <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 |
|
|
|
|
|
|
|
|
|
|
Querying
Tool | Metabase endpoint |
|
|
|
|
| none — builds an MBQL |
| none, or |
Questions (cards)
Tool | Metabase endpoint |
|
|
|
|
|
|
|
|
|
|
Dashboards
Tool | Metabase endpoint |
|
|
|
|
|
|
|
|
|
|
|
|
Collections, search and metrics
Tool | Metabase endpoint |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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/cardhas no top-leveldatabase_id. Its schema requiresname,dataset_query,displayandvisualization_settings; the database belongs insidedataset_query.database. This server accepts the friendlierdatabase_idargument and folds it into the query for you.PUT /api/dashboard/:id/cardsis deprecated in 0.63 in favour ofPUT /api/dashboard/:idwith adashcardsarray, and it is a full-set replacement rather than an append.add_card_to_dashboarduses the current endpoint.GET /api/dashboardis marked deprecated but still served; it only supports the filter modesall,mineandarchived. For anything richer usesearchwithmodels: ["dashboard"].GET /api/cardhas no collection filter. Its only filters aref(all,mine,bookmarked,database,table,using_model,using_segment,archived) plusmodel_id. To list one collection useget_collection_itemsorsearch./api/metricis the modern metrics API. A metric is a card withtype: "metric";GET /api/metricreturns{total, limit, offset, data}. Field values for a metric come fromGET /api/metric/:id/dimension/:dimension-key/values, where the key is a UUID fromget_metric— not from/api/field/:id/values.POST /api/collectionnames the parentparent_id, notcollection_id.A broken query is not an HTTP error.
POST /api/datasetanswers with HTTP 202 andstatus: "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 serverThe 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/mcpreturns 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
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Query, browse, and automate OmegaAI workspaces from any MCP client. Streamable HTTP with OAuth 2.0.
n8n MCP — query your own n8n instance (BYO).
Monday.com MCP — wraps the Monday.com GraphQL API (BYO API key)
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables 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.-
- FlicenseAqualityDmaintenanceEnables LLMs to interact with Metabase instances through 33 tools for querying cards, dashboards, databases, collections, and executing SQL queries against your Metabase installation.23-
- FlicenseNot gradedqualityNot gradedmaintenanceEnables 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-
- AlicenseNot gradedqualityDmaintenanceA 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