Skip to main content
Glama
Barrow1990

authentik-mcp-server

by Barrow1990

authentik-mcp-server

A minimal Model Context Protocol server that connects to Authentik, packaged for Docker.

It runs as a standing network service (streamable-http transport, not stdio), so any MCP client on your internal network can connect to http://<host>:<port>/mcp — the container isn't spawned per-client, and container lifecycle/updates can be handed off to a tool like Dockhand.

Two modes, one image

This image can run as either of two containers, chosen by AUTHENTIK_MCP_MODE:

Mode

Port (default)

Tools

read (default)

8937

Everything except set_user_active

write

8942

Everything read has, plus set_user_active

This is not a config toggle guarding one shared codebase — in read mode, set_user_active is never defined at all, so it can't appear in a tool listing or be called under any circumstances. docker-compose.yml runs read always-on and puts write behind a Compose profile, so a plain docker compose up -d only ever starts something that structurally cannot write to your identity provider. Starting the write container is a deliberate, separate action: docker compose --profile write up -d.

Related MCP server: Remote MCP AuthKit

Tools

Tool

Mode

Description

list_users

read, write

List users, optionally filtered by search term and/or active status

user_details

read, write

Full details for one user by ID

list_groups

read, write

List groups, optionally filtered by name

list_applications

read, write

List configured SSO applications

recent_events

read, write

Recent audit/auth events (logins, failures, config changes, ...)

system_status

read, write

Authentik version and runtime info

set_user_active

write only

Enable or disable a user account

Why no destructive tools

Authentik is an identity provider — it holds real user accounts, group membership, and SSO/application configuration for everything else behind it. A wrong or malicious tool call here has a much bigger blast radius than one against a media manager, so this server is deliberately read-mostly, with writes isolated behind several independent layers rather than one flag:

  1. Mode split (structural). set_user_active only exists in write-mode containers. Point a client at the read-mode server and the tool is genuinely absent — not hidden, not permission-denied, just not there.

  2. AUTHENTIK_ALLOW_WRITES=true — even in a write-mode container, this must be explicitly set (default false). An operator has to turn writes on for that specific deployment.

  3. confirm=True on the call itself — an assistant has to explicitly ask for the write every time; it can't happen from a misread instruction or a config left at its default.

  4. Separate credentials recommended (see Configuration below): give the read and write containers different, separately-scoped Authentik API tokens and different MCP_AUTH_TOKENs, so a compromise of the read container's environment doesn't hand over write-capable credentials too.

Beyond set_user_active, there is no create/delete tool for users, groups, applications, providers, or flows, and no tool that touches password hashes, tokens, or recovery links, even though Authentik's API supports all of that (set_password, recovery, impersonate, etc. on the users endpoint). If you need those, use Authentik's own admin UI.

recent_events is read-only but still sensitive — audit events can include IPs and usernames tied to real login activity. Scope who can reach either server's /mcp endpoint accordingly (see Authentication, below).

If you want more capability than this, treat it as a deliberate decision to widen scope, not a missing feature — add tools individually, decide which mode they belong in, and mind the permission each one needs.

Health endpoints

Two plain HTTP endpoints per container, reachable without MCP_AUTH_TOKEN (so Docker's HEALTHCHECK, Dockhand, or any other monitor can poll them without the secret):

Endpoint

Checks

Healthy

Unhealthy

GET /health

The process is up and serving HTTP. Does not call Authentik.

200 {"status": "ok"}

(doesn't respond)

GET /ready

AUTHENTIK_URL is reachable and the mode's API token is valid (via GET /core/users/me/, which any valid token can call regardless of its other permissions).

200 {"status": "ok", "reachable": true, "authenticated": true, "mode": "read"|"write", "authentik": {...}}

503 {"status": "error", "reachable": ..., "authenticated": ..., "error": "..."}

They're split deliberately: /health is what each container's own HEALTHCHECK uses (so a transient Authentik outage doesn't get the container itself restarted in a loop), while /ready is for verifying config — curl http://<host>:8937/ready (read) or curl http://<host>:8942/ready (write) tells you plainly whether that container's host is reachable, its token is valid, or both. The response's mode field confirms which container answered.

/ready deliberately checks token validity, not the specific permissions individual tools need. system_status needs the authentik_rbac.view_system_info permission and set_user_active needs write access to users; a 403 from either of those is a permission-scope problem with that container's token, not a readiness failure — /ready will still report healthy as long as the token can authenticate at all.

Authentication

Set MCP_AUTH_TOKEN_READ and MCP_AUTH_TOKEN_WRITE (random shared secrets — openssl rand -hex 32 each) and every request to the corresponding container must carry Authorization: Bearer <that container's token> or it returns 401. Use two different values — that's what stops a leaked read-side bearer token from being replayed against the write container's port. This is checked by a small Starlette middleware in front of the MCP app, not the mcp SDK's built-in OAuth support (mcp.server.auth) — that machinery expects a full OAuth authorization server (issuer/resource metadata, RFC 8414/8707/9068 discovery), which is unnecessary complexity for a secret shared by trusted LAN clients.

Leave a container's token unset and that container runs with no auth — anything that can reach its /mcp endpoint can call every tool it exposes. For the write container that includes set_user_active (still behind AUTHENTIK_ALLOW_WRITES + confirm=True, but still — set the token). Each container logs a warning on startup when it's running without one. Either way, the trust boundary is still the network:

  • Do not publish either port through any reverse proxy, port-forward, or anything else reachable from outside your LAN/VLAN.

  • Bind the compose ports: mapping to a specific internal interface (e.g. 192.168.1.50:8937:8937) rather than all interfaces, if you want to be stricter about which hosts on your network can reach it at all.

Authentik API token scope — use two, not one

Create two separate service accounts in Authentik (Directory > Users > Create Service Account) rather than reusing your own admin user, or reusing one account for both modes. Create each one's API token under Directory > Tokens and App passwords (Intent: API Token):

  • Read token (AUTHENTIK_API_TOKEN_READ): grant only view_user, view_group, view_application, view_event, and authentik_rbac.view_system_info — whatever the read tools you actually use need. This account should have no write permissions on anything.

  • Write token (AUTHENTIK_API_TOKEN_WRITE): the above, plus authentik_core.change_user for set_user_active.

Don't rely on assumed defaults for what a fresh service account or API token can and can't do — Authentik's permission model has changed across versions (see the RBAC/permissions docs for your installed version), and what a token can do depends on the role/group it's actually assigned, not just on it being newly created. Check the token's effective permissions directly in Authentik's admin UI after creating it, for both accounts.

This is the layer that actually matters most: even a full compromise of the read container's process — its environment, its memory, everything — can't yield a credential capable of writing anything, because Authentik's own RBAC enforces that, not just this server's tool registry. A single AUTHENTIK_API_TOKEN (no _READ/_WRITE suffix) is supported as a fallback if you'd rather use one token for both, but that gives up this guarantee.

Configuration

Environment variables (see .env.example):

Variable

Required

Default

Description

AUTHENTIK_MCP_MODE

no

read

read or write — see "Two modes, one image" above

AUTHENTIK_URL

yes

e.g. http://192.168.1.50:9000 (or your reverse-proxied HTTPS URL)

AUTHENTIK_API_TOKEN_READ

yes, in read mode

Falls back to plain AUTHENTIK_API_TOKEN if unset

AUTHENTIK_API_TOKEN_WRITE

yes, in write mode

Falls back to plain AUTHENTIK_API_TOKEN if unset

AUTHENTIK_ALLOW_WRITES

no

false

Write-mode-only master switch for set_user_active. Must be true and the call must pass confirm=True

MCP_HOST

no

0.0.0.0

Interface the server binds to inside the container

MCP_PORT

no

8937 (read) / 8942 (write)

Port the server listens on

MCP_AUTH_TOKEN_READ / MCP_AUTH_TOKEN_WRITE

no

Per-mode bearer secret. Falls back to plain MCP_AUTH_TOKEN if unset. Unset entirely = no auth on that container (see above)

Authentik's REST API is a fixed path prefix (/api/v3/...) rather than content-negotiated or runtime-discoverable the way the Servarr apps (Sonarr/Radarr, also in this family of MCP servers) expose a GET /api endpoint reporting current/deprecated versions. There's no AUTHENTIK_API_VERSION setting here because there's nothing to point it at — v3 is hardcoded into server.py's HTTP client, and a future Authentik v4 would need a code change here. Unlike the Servarr-family servers, /ready can't detect that drift automatically; if tool calls start 404ing after an Authentik upgrade, check Authentik's release notes for an API version bump.

Image

Built and pushed to ghcr.io/barrow1990/authentik-mcp-server by .github/workflows/ci.yml on every push to main that passes tests, tagged :latest and :<commit-sha>. The same image serves both modes — docker-compose.yml runs it twice with different AUTHENTIK_MCP_MODE values. Swap in build: . there instead if you'd rather build locally from the Dockerfile.

The image is a three-stage build: builder compiles dependencies into --target=/deps (all of them, including cryptography's compiled cffi extension, ship musllinux wheels, so this needs no compiler even on alpine); prep starts fresh from python:3.12-alpine, drops pip/setuptools/wheel, strips stdlib pieces this headless server never touches (tkinter, idlelib, lib2to3, ensurepip, ...), adds the non-root app user, and copies in /deps and server.py; runtime then does a single COPY --from=prep / / onto a scratch base. That last step matters more than it looks — a plain RUN rm -rf only hides files still physically present in the base image's own layers underneath, so it doesn't shrink a normal layered image at all; copying the already-trimmed filesystem onto scratch is what actually drops those bytes from what gets pushed.

That takes the published image to roughly ~98MB, the same floor as the other servers in this family — mcp's own dependency graph pulls in cryptography's AES-GCM/HKDF regardless. Dependencies in requirements.txt are pinned to exact versions rather than >= ranges, so a routine docker build can't silently pull in a heavier resolution than the one that was actually tested.

Running with Docker Compose

cp .env.example .env   # fill in AUTHENTIK_URL and both API tokens
docker compose up -d --pull always              # read container only
docker compose --profile write up -d            # also starts the write container

The read server is reachable at http://<docker-host>:8937/mcp; the write server, once started, at http://<docker-host>:8942/mcp.

One .env, but no container sees more than its own variables

docker-compose.yml deliberately does not use env_file: (which would dump the whole .env into every container). Instead each service's environment: block names its own variables explicitly via ${VAR} substitution — a variable never referenced in a service's block simply never reaches that container. Verify this yourself any time after editing .env:

docker compose --profile write config

authentik-mcp-read's resolved environment should show AUTHENTIK_API_TOKEN_READ but never AUTHENTIK_API_TOKEN_WRITE or MCP_AUTH_TOKEN_WRITE — confirmed during development of this compose file, but re-check it after any edit, since a typo in a service's environment: block would silently reintroduce exactly the leak this design exists to prevent.

Does this fit how Dockhand expects to be used?

Only if Dockhand feeds docker compose a plain .env. ${VAR} substitution reads exclusively from a literal .env in the project directory (or --env-file <path> / real shell environment variables) — never from an arbitrarily-named file. This is a change from how earlier servers in this family (sonarr, radarr, ...) load config — those use env_file: [.env, .env.dockhand] on the understanding that Dockhand writes its UI-configured values to a separate .env.dockhand, which env_file: (unlike substitution) can load regardless of filename.

This repo hasn't been verified against a live Dockhand deployment. Before relying on it under Dockhand:

  1. Deploy this stack in Dockhand and check whether the values you set in its UI actually reach the containers — docker exec authentik-mcp-read env.

  2. If they don't (Dockhand is writing to .env.dockhand, substitution never sees it, and the :? markers in docker-compose.yml make docker compose up fail outright rather than start with empty credentials): edit .env directly in this repo's checkout instead of through Dockhand's UI, or check whether Dockhand's stack settings let you point it at .env instead of .env.dockhand.

Make the GHCR package public, or every pull will need docker login ghcr.io with a PAT on each deploy host — a private package by default requires auth even to docker pull, which most homelab boxes won't have configured.

Set a restart policy of unless-stopped (already in docker-compose.yml) so Dockhand-driven restarts and host reboots bring the read container back up without manual intervention. The write container, being profile-gated, needs its profile invoked again after a host reboot too — that's intentional, not a bug to fix; it's meant to require a deliberate action to bring back.

Connecting a client

Claude Code

claude mcp add authentik -s user --transport http http://<docker-host>:8937/mcp \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN_READ>"

Add the write server as a separate, explicitly-named connection only when you actually want it available, rather than always-on alongside the read one:

claude mcp add authentik-write -s user --transport http http://<docker-host>:8942/mcp \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN_WRITE>"

(Drop the --header flag on either if you're running that container with its token unset.)

Claude Desktop

Claude Desktop's built-in config expects a locally-spawned command, so for a network server like this you'll need an HTTP-to-stdio bridge such as mcp-remote:

{
  "mcpServers": {
    "authentik": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://<docker-host>:8937/mcp",
        "--header", "Authorization: Bearer <MCP_AUTH_TOKEN_READ>"
      ]
    }
  }
}

Running without Docker

pip install -r requirements.txt
AUTHENTIK_URL=http://192.168.1.50:9000 AUTHENTIK_MCP_MODE=read \
AUTHENTIK_API_TOKEN_READ=your-read-scoped-token \
MCP_AUTH_TOKEN_READ=your-shared-secret python server.py

Testing

pip install -r requirements-dev.txt
python -m pytest tests/ -v
  • tests/test_tools.py — each tool's logic against a mocked Authentik (httpx.MockTransport, no extra mocking library needed). Runs with AUTHENTIK_MCP_MODE=write (the superset) so set_user_active is covered alongside everything read mode has.

  • tests/test_http.py/health, /ready, and the bearer-auth middleware, via server.build_app() (the exact app __main__ runs) through Starlette's TestClient.

  • tests/test_mode.py — the mode split itself, via subprocess (this behavior only shows up at import time, so it can't be exercised by monkeypatching an already-imported module): read mode genuinely lacking set_user_active, write mode having it, per-mode port defaults, invalid AUTHENTIK_MCP_MODE values exiting non-zero, and the _READ/_WRITE credential fallback logic.

  • tests/test_live_authentik.pyopt-in contract tests against a real Authentik instance, to catch drift if an Authentik upgrade renames/removes a field these tools depend on (pk, username, is_active, action, ...). Skipped by default (no Authentik in CI); run with:

    RUN_LIVE_AUTHENTIK_TESTS=1 AUTHENTIK_URL=https://authentik.example.com \
    AUTHENTIK_API_TOKEN=<real token> python -m pytest tests/test_live_authentik.py -v

CI (.github/workflows/ci.yml) runs the mocked suite on every push/PR; the GHCR build only runs after it passes.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Authentik MCP provides seamless integration with Authentik's API, supporting both full-featured and diagnostic modes. These enable secure, automated user, group, and system management through MCP-compatible tools.
    17
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables remote MCP server connections with WorkOS AuthKit authentication and user management. Supports organization-centric authentication and permission-based tool access control.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP server for Authentik identity management, enabling natural language management of users, groups, applications, flows, policies, providers, and more.
    128 npm
    11
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server for Authentik identity provider enabling management of users, groups, apps, tokens, flows, and policy bindings through natural language.
    7
    MIT