You can let an AI assistant answer questions from your company data through Metabase’s MCP server. Assistants such as Claude, ChatGPT, Cursor and Codex connect to it directly, and an assistant you build into your own app connects to it too. Every connection acts as one person who signed in, so the assistant sees only what that person can see in Metabase. The MCP server is part of every Metabase plan, including the free Open Source edition.
Metabase already holds what an assistant needs to give the right answer: your database connections, the tables people use, your metrics and their definitions, and your permissions. So you don’t need to write a SQL tool or a metrics catalog for the assistant. It uses the connections, metrics and permissions you already keep in Metabase.
Which path fits your AI assistant
- Your assistant is an MCP client, such as Claude, Claude Code, ChatGPT, Cursor or Codex. Connect it to the Metabase MCP server. Each person signs in to Metabase, and the assistant sees only what that person can see.
- Your assistant runs in your own app, such as a chat page or an internal support tool that calls a model with tool calling. Your app connects to the same MCP server as the person who uses it. On Pro and Enterprise, your backend gets that person’s token from a signed JWT, with no sign-in page.
| Your assistant | Connect through | Who it acts as |
|---|---|---|
| Claude, Claude Code, ChatGPT, Cursor, Codex and other MCP clients | MCP server at /api/metabase-mcp, with an OAuth sign-in | The person who signed in |
| Your own app with tool calling | MCP server at /api/metabase-mcp, with an OAuth sign-in on any plan, or a token from a signed JWT on Pro and Enterprise | The person signed in to your app |
The MCP server can also create questions, dashboards and alerts, so the same connection works for building as well as asking. To have a coding agent build content in a development Metabase and version it in Git, see agent-driven development, which pairs the Metabase CLI with Remote Sync on Pro and Enterprise.
Connect an MCP client to the Metabase MCP server
Admins turn the MCP server on or off in Admin > AI > MCP with the MCP server toggle. It needs AI features to be on. It doesn’t need an AI provider in Metabase: your assistant uses its own model and calls Metabase for the data.
Point your client at your Metabase’s MCP address:
https://metabase.example.com/api/metabase-mcp
To connect Claude Code for all your projects, run:
claude mcp add --scope user --transport http metabase https://metabase.example.com/api/metabase-mcp
Then start claude, run /mcp, select the Metabase server and click Authenticate. On Claude on the web or Claude Desktop, open Claude’s connector directory and enter the same address, which other MCP clients use too.
The client signs in with OAuth 2.0. Metabase runs its own OAuth server, so you don’t need an external one. The client receives an access token scoped to the permissions of the person who signed in. The consent page lists the scopes the client asks for, such as agent:query:run to run queries and agent:sql:run to run raw SQL. A scope never grants more than the person’s Metabase permissions.
Once connected, the assistant can search your content, read table fields and metric definitions, and run queries. It uses execute_query for anything Metabase’s query language can express, such as counts, sums, grouping, filters and joins. execute_sql runs raw SQL, and only for people with native query permission on that database. Both return up to 100 rows per call by default and up to 2,000 at most, and the assistant can ask for the next page. In Claude Desktop (in Chat), Claude on the web, ChatGPT, Cursor and VS Code, the assistant can also show the answer as a chart, once an admin turns on that client under Show inline charts in these MCP clients. Every chart has an Explore in Metabase button that opens the question behind it, so people can check how the assistant got the number.
Build an assistant into your own app
If your app has its own chat page or support tool, its model can use the same MCP server. Your app connects as the person who uses it, so each question runs with that person’s Metabase permissions. Your app reads the tool list from the MCP server, gives it to the model, and runs each tool call the model makes.
On any plan, your app can send each person through Metabase’s sign-in and consent page, as any MCP client does. On Pro and Enterprise, your backend can skip that page with headless access: it signs a JWT for the person and exchanges it for an MCP access token.
Get an MCP token for each person with a signed JWT
- Turn on JWT authentication in Admin > Settings > Authentication > JWT: fill in the JWT Identity Provider URI, such as your app’s sign-in page, and set up the signing key. Your backend signs every JWT with the same key.
- Map the groups in your JWT to Metabase groups, so each person gets the data permissions of their team.
- For each person, sign a JWT with their email, an
iat(issued at) claim and anexp(expiration) claim. First and last name are optional. Post it to/oauth/token, and Metabase accepts it for at most three minutes after itsiat.
Metabase checks the JWT the same way it does at SSO login, and it creates or updates the person’s account. Then it returns an access token that lasts one hour. Ask only for the scopes the assistant needs: with agent:content:read and agent:query:run, the token can search, read and run queries, but it can’t write content or run raw SQL.
Save this Python function as metabase_token.py:
import os
import time
import httpx
import jwt # PyJWT
METABASE_URL = os.environ["METABASE_URL"] # for example https://metabase.example.com
JWT_SECRET = os.environ["METABASE_JWT_SECRET"] # the JWT signing key from Admin > Settings > Authentication > JWT
def mcp_token(person):
"""Exchange a JWT for the person signed in to your app for an MCP access token that lasts one hour."""
now = int(time.time())
assertion = jwt.encode(
{
"email": person["email"],
"first_name": person["first_name"],
"last_name": person["last_name"],
"iat": now,
"exp": now + 60,
},
JWT_SECRET,
algorithm="HS256",
)
response = httpx.post(
f"{METABASE_URL}/oauth/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
"scope": "agent:content:read agent:query:run",
},
)
response.raise_for_status()
return response.json()["access_token"]
Give the model the Metabase tools
This Python example, assistant.py, connects to the MCP server with the token, gives the model the read-only tools, and runs the tool loop with the Anthropic Python SDK and version 2 of the MCP Python SDK. Other model providers follow the same steps: send the question with the tools, run each tool call through the MCP session, send the results back, and stop when the model answers.
import asyncio
import anthropic
from mcp import ClientSession
from mcp.client.streamable_http import create_mcp_http_client, streamable_http_client
from metabase_token import METABASE_URL, mcp_token
client = anthropic.AsyncAnthropic()
SYSTEM = """You answer questions from company data with the Metabase tools.
Prefer a verified metric when one matches the question.
Say which metric or table each number comes from."""
async def ask(person, question):
headers = {"Authorization": f"Bearer {mcp_token(person)}"}
async with create_mcp_http_client(headers=headers) as http_client:
async with streamable_http_client(f"{METABASE_URL}/api/metabase-mcp", http_client=http_client) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
listed = await session.list_tools()
# Give the model only the read-only tools: search, browse, read, glossary and queries.
tools = [
{"name": tool.name, "description": tool.description or "", "input_schema": tool.input_schema}
for tool in listed.tools
if tool.annotations and tool.annotations.read_only_hint
]
messages = [{"role": "user", "content": question}]
while True:
reply = await client.messages.create(
model="claude-opus-5-5", max_tokens=16000, system=SYSTEM, tools=tools, messages=messages
)
messages.append({"role": "assistant", "content": reply.content})
if reply.stop_reason != "tool_use":
return "".join(block.text for block in reply.content if block.type == "text")
results = []
for block in reply.content:
if block.type == "tool_use":
result = await session.call_tool(block.name, block.input)
text = "\n".join(item.text for item in result.content if item.type == "text")
results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": text, "is_error": bool(result.is_error)}
)
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
person = {"email": "ana@example.com", "first_name": "Ana", "last_name": "Ruiz"}
print(asyncio.run(ask(person, "How many orders did we get each year?")))
The MCP server doesn’t filter its tool list by the token’s scopes, and a call outside those scopes fails. So the example keeps only the tools that Metabase marks as read-only. For a client without inline charts, like this one, those are search, browse_data, browse_collection, get_content, get_parameter_values, glossary, learn, execute_query and run_saved_question.
The token lasts one hour, so get a new one when a conversation runs longer. Each token from a signed JWT appears in the Activity log with the topic mcp-jwt-token-issued, and its calls appear in MCP analytics.
Give the assistant your definitions, not just tables
An assistant gives the same answer as your dashboards when it uses the same definitions. So define the numbers people ask about once, as metrics in Data Studio, such as revenue, active customers or average order value, and publish them to the Library. A metric is a saved aggregation that the assistant can filter and group by its dimensions, without writing the formula again. Add descriptions to tables, fields and metrics too, because the search and read tools return them to the model. Define your business terms in the glossary as well, since the MCP server tells assistants to read it before they answer.
Keep the assistant read-only
- Permissions decide what it reads. Every MCP tool runs with the permissions of the person who signed in, so the assistant sees the same data as that person.
- Grant only the scopes it needs. An assistant that answers questions needs
agent:content:readandagent:query:run. Most MCP clients ask for every scope, so uncheck the others on Metabase’s consent page. In your own app, send only those two in thescopeparameter. - SQL needs native query permission.
execute_sqlonly works for people with native query permission on that database, so set Create queries to Query builder only for the groups that shouldn’t send raw SQL. - Connect with a read-only database user. The Metabase docs recommend one, so the assistant’s queries can’t change your data.
- On Pro and Enterprise, admins choose the tools for each group. In Admin > AI > Usage controls > MCP tools access, an admin can give a group only the tools that read content. Metabase then leaves the blocked tools out of the tool list a client gets.
- Admins see who connected. Admin > AI > MCP > Authorizations lists the MCP clients that registered, and who approved or denied them. Tokens from a signed JWT appear in the Activity log instead.
- Results go to your model provider. Query results go to the MCP client, or to your app, which may send them to its AI provider. See AI privacy.
The MCP server’s tools that create questions, dashboards and alerts need the agent:content:write or agent:delivery:write scope. To keep an MCP client read-only, grant only the two read scopes, or block the write tools in your client. On Pro and Enterprise, an admin can also block them for a group in MCP tools access.
Plans
You don’t need a paid plan or an AI provider in Metabase to use the MCP server, because your assistant brings its own model. Headless access with JWT for assistants in your own app, MCP tools access for each group, and MCP analytics in Monitor > AI Auditing > MCP analytics are part of the Pro and Enterprise plans.