MCP Server

Baserow ships a built-in Model Context Protocol (MCP) server that exposes database operations as tools so that AI assistants can read and write Baserow tables directly.

Architecture

LLM / MCP client
      │ POST /mcp (Streamable HTTP, bearer token)   GET /mcp/{key}/sse (legacy SSE)
      ▼
Starlette app, ASGI only           ← backend/src/baserow/core/mcp/__init__.py
      │
BaserowMCPServer                   ← backend/src/baserow/core/mcp/__init__.py
      │
MCPToolRegistry                    ← backend/src/baserow/core/mcp/registries.py
      │
services.py                        ← backend/src/baserow/contrib/database/mcp/services.py
      │
ActionTypes → Django ORM
  • MCPTool (registries.py) — Base class for all tools. Subclasses define a type, a Pydantic input_schema, and implement _sync_call(). Tools with enabled = False are registered but hidden from MCP clients.
  • Services layer (services.py) — Workspace-scoped database operations. Shared by both MCP tools and the enterprise AI assistant.
  • Action types — All mutations go through Baserow’s action-type layer, so operations are undoable and audit-logged.
  • Workspace isolation — Every service function enforces workspace-scoped access. Tools cannot touch data outside the endpoint’s workspace.
  • SSE transport (backend/src/baserow/core/mcp/sse.py) — DjangoChannelsSseServerTransport serves the legacy GET /mcp/{key}/sse stream and POST /mcp/messages/.
  • OAuth (backend/src/baserow/core/mcp/oauth/) — Authorization server, client registration and token views for POST /mcp. The consent and connected-apps APIs used by the frontend are in backend/src/baserow/api/mcp/oauth_views.py.

Endpoint model

An MCPEndpoint links a user to one workspace and to the tools they allowed (allowed_tools, null allows every enabled tool). There are two kinds:

  • OAuth grants: created when a client signs in through the consent page (see OAuth sign-in). The client calls POST /mcp with an access token. They are listed, and can be disconnected, on the OAuth tab of My settings > MCP server.
  • Endpoint keys: a 32-character secret key, created under My settings > MCP server (on the Endpoint tab when OAuth is on). Keys allow every enabled tool. Older clients use it in the SSE URL, GET /mcp/{key}/sse. POST /mcp also accepts it as Authorization: Bearer <key>.

Users install the client side, the MCP server config plus a skill, from baserow/baserow-mcp. Its README has the setup steps for each agent.

Running the server

The MCP server requires ASGI mode (Django Channels).

# Start dependencies
just dcd up -d db redis

# Run in dev or ASGI mode
cd backend
# dev mode
just run-dev-server
# or ASGI mode
just run-asgi

OAuth sign-in

MCP clients can sign in with OAuth instead of a pasted key. See ADR 009 for the design.

Set BASEROW_MCP_OAUTH_ENABLED=true to turn it on. It is off by default.

All clients share one URL, <PUBLIC_BACKEND_URL>/mcp. The user picks a workspace and the allowed tools on the consent screen.

Each grant is stored as an MCPEndpoint row with oauth_client_id set. Its tokens carry the scope mcp endpoint:<id>, which ties them to that row. Deleting the row revokes the tokens. Grants are also revoked when the user leaves the workspace. A password change or reset deletes the user’s tokens but keeps the grants, so the apps have to sign in again.

Client registration

  • CIMD: the client URL is the client id. On by default; set BASEROW_MCP_OAUTH_CIMD_ENABLED=false to turn it off. BASEROW_MCP_OAUTH_CIMD_ALLOWED_HOSTS is a comma-separated host list and defaults to *. Its redirect URIs must also be https or loopback.
  • DCR: on by default. Set BASEROW_MCP_OAUTH_DCR_ENABLED=false to turn it off. It only accepts public clients that use the authorization code grant, with at most 10 https or loopback redirect URIs and a body of at most 16 KB. Other redirect URIs are dropped; the registration fails only when none is left.

A Celery task deletes clients that have no grant: DCR clients once they have not been used for 30 days (issuing or refreshing a token counts as use), CIMD clients one day after they were first fetched, since they are fetched again when needed. An app whose client was deleted gets a 400 asking the user to remove the Baserow server from the app and add it again.

Rate limits and tokens

The limits are per IP, as comma-separated lists. The consent API also applies the authorize limits per user, as it can fetch CIMD documents too. Only failed token calls count, since hosted apps refresh every user’s token from a few IPs. Token revocation has its own limit with the token values, because it loads the client and so can fetch a CIMD document.

Variable Default
BASEROW_MCP_OAUTH_AUTHORIZE_RATE_LIMITS 30/m,300/h
BASEROW_MCP_OAUTH_REGISTRATION_RATE_LIMITS 10/m,60/h
BASEROW_MCP_OAUTH_TOKEN_RATE_LIMITS 30/m,300/h

Access tokens last one hour. Clients always get a refresh token, whether or not they ask for offline_access; refresh tokens rotate and expire after 30 days. Replaying a used refresh token revokes the whole family.

Running it locally

BASEROW_MCP_OAUTH_ENABLED=true just run-dev-server
claude mcp add --transport http baserow http://localhost:8000/mcp

ALLOW_LOCALHOST_LOOPBACK is already on in the settings, so clients can use http://localhost redirect URIs.

See the test plan for other clients, the plugin and the skill.

ChatGPT app domain verification

OpenAI verifies the domain of a submitted ChatGPT app by fetching /.well-known/openai-apps-challenge. Set BASEROW_MCP_OPENAI_APPS_CHALLENGE to the token shown in OpenAI’s portal and the backend returns it as plain text. When it’s empty the route returns a 404. Like the other OAuth routes, it only exists with BASEROW_MCP_OAUTH_ENABLED=true.

Tools

Some tools are currently disabled (enabled = False) and hidden from MCP clients. The consent page does not list them.

Tool Status Description
list_databases enabled List all databases in the workspace
list_tables enabled List tables, optionally filtered by database
get_table_schema enabled Get field definitions for one or more tables
list_table_rows enabled List rows with optional search and pagination
create_rows enabled Create one or more rows using field names
update_rows enabled Update rows by ID using field names
delete_rows enabled Delete rows by ID
create_database disabled Create a new database
create_table disabled Create a table with optional initial fields
update_table disabled Rename a table
delete_table disabled Delete (trash) a table
create_fields disabled Add fields to an existing table
update_fields disabled Update existing fields
delete_fields disabled Delete (trash) fields

Typical LLM workflow

list_databases
  └─ list_tables(database_id)
       └─ get_table_schema([table_id])   ← learn field names and types
            ├─ create_rows(table_id, [{Name: "Alice"}])
            ├─ update_rows(table_id, [{id: 1, Name: "Bob"}])
            └─ delete_rows(table_id, [1, 2])

Adding a new tool

  1. Create a class extending MCPTool in the appropriate mcp/*/tools.py file.
  2. Define a Pydantic model for the input and assign it to input_schema.
  3. Implement _sync_call(self, endpoint, args).
  4. Register it in backend/src/baserow/contrib/database/apps.py.
  5. Add tests in backend/tests/baserow/contrib/database/mcp/.

Set enabled = False on the class if the tool should not be exposed to MCP clients yet.

Testing

See docs/testing/mcp-test-plan.md for manual testing instructions.

# Server, transports and OAuth
just b test tests/baserow/core/mcp/

# Tools and services
just b test tests/baserow/contrib/database/mcp/

# Endpoint API
just b test tests/baserow/api/test_mcp_views.py

# OAuth audit log (enterprise)
just b test ../enterprise/backend/tests/baserow_enterprise_tests/audit_log/test_mcp_oauth_audit_log.py

# Service layer only
just b test tests/baserow/contrib/database/mcp/test_mcp_services.py