Skip to main content
Glama
williamamed

mcp-polizei

by williamamed

mcp-polizei

MCP server for the Polizei (Polisafe) Admin/RBAC API — the IAM & OAuth 2.0 / OIDC server behind srv-polizei.

It exposes user, role, permission, scope (tenant), invitation, OAuth client, and profile management as MCP tools. It is standalone: an independent Node/TypeScript process that talks to the API over HTTP.

Prerequisites

  1. An OAuth client with the password grant enabled (used by auth_login and, unless a dedicated one is configured, by the browser login flow).

  2. For stdio or service sessions: an admin user with RBAC privileges over the scopes it will operate on.

  3. The srv-polizei API running and reachable.

Installation

cd apps/services/mcp-polizei
npm install
npm run build

Configuration (.env)

Copy .env.example to .env and fill it in:

Variable

Description

PLS_PUBLIC_URL

Public URL of Polizei (e.g. http://localhost:3000)

APP_PREFIX

Global API prefix (e.g. /api/v4/security)

PLS_MCP_CLIENT_ID / PLS_MCP_CLIENT_SECRET

OAuth client used for logins (grant password)

PLS_MCP_ADMIN_USER / PLS_MCP_ADMIN_PASS

Admin user — required only for stdio or when service sessions are enabled

PLS_MCP_PUBLIC_URL

(optional) Public URL of this MCP server (e.g. https://mcp.polizei.yourdomain.com). Needed only for the browser login flow (auth_login_start)

PLS_MCP_LOGIN_CLIENT_ID / PLS_MCP_LOGIN_CLIENT_SECRET

(optional) Dedicated OAuth client for the browser (authorization_code) flow. Defaults to PLS_MCP_CLIENT_*

PLS_MCP_AUTH_MODE

auto (default) | user | service — see Authentication

PLS_MCP_SCOPE

Scopes requested for the token (default openid profile)

PLS_MCP_TOKEN_MIN_TTL

Renew the token when fewer than <N> seconds remain (default 120)

PLS_MCP_TRANSPORT

stdio (local) or http (remote)

PLS_MCP_HTTP_PORT / PLS_MCP_HTTP_HOST

Port/host for the HTTP transport

PLS_MCP_HTTP_AUTH_TOKEN

(optional) Service bearer token — requests carrying it create service sessions (env admin identity) when PLS_MCP_AUTH_MODE=auto

PLS_MCP_MAX_SESSIONS

(optional) Max concurrent HTTP sessions (default 100)

PLS_MCP_SESSION_IDLE_TTL_MIN

(optional) Idle timeout per session (default 720)

Authentication

Each MCP connection over HTTP gets its own session (Mcp-Session-Id) with an isolated identity, token and active tenant. Two ways to authenticate as a real Polizei user:

1. Chat login — auth_login(username, password)

The user (or the agent on their behalf) calls the auth_login tool with their Polizei credentials. The server exchanges them once (OAuth password grant against the configured client) and keeps only the resulting token in memory for that session. Passwords are never stored or logged.

2. Provider login page — auth_login_start

  1. Call auth_login_start — the server creates a pending login (PKCE) and returns a URL.

  2. Open the URL in a browser: Polizei shows its own login form.

  3. After login, Polizei redirects to {PLS_MCP_PUBLIC_URL}/oauth/callback, the server exchanges the code and binds the tokens to the session that started the flow.

  4. Back in the chat, auth_status / auth_poll confirm the session is authenticated.

Requirements: PLS_MCP_PUBLIC_URL set, and an OAuth client in Polizei with the authorization_code grant whose redirectUris include {PLS_MCP_PUBLIC_URL}/oauth/callback.

Service sessions (legacy / automation, optional)

If PLS_MCP_HTTP_AUTH_TOKEN is set and PLS_MCP_AUTH_MODE=auto, an initialize request carrying Authorization: Bearer <token> creates a service session that acts as the env admin — the previous single-identity behavior. Without a bearer, sessions are per-user (anonymous until login). With PLS_MCP_AUTH_MODE=service, every session is a service session (and the bearer is enforced if configured). With PLS_MCP_AUTH_MODE=user, only per-user sessions are allowed.

Session lifecycle

  • auth_status → current identity/mode, active tenant, token expiry.

  • auth_logout → clears the identity and tenant of the session.

  • Tokens are renewed automatically before expiry when Polizei returns a refresh_token; otherwise the session expires and a new auth_login is requested.

  • Sessions are evicted after PLS_MCP_SESSION_IDLE_TTL_MIN of inactivity or when PLS_MCP_MAX_SESSIONS is reached — the client then re-initializes and logs in again.

Accessing protected tools without an identity returns a readable error instructing auth_login / auth_login_start. Run over HTTPS in production: credentials and tokens travel in the MCP tool payloads and upstream requests.

Usage

stdio transport (Claude Desktop, Cursor, opencode)

Local mode: the single process uses the env admin identity (as before).

npm run start        # or: node dist/index.js

opencode configuration (~/.config/opencode/opencode.jsonc):

{
  "mcp": {
    "mcp-polizei": {
      "type": "local",
      "command": ["node", "/absolute/path/to/mcp-polizei/dist/index.js"],
      "enabled": true,
      "environment": {
        "PLS_PUBLIC_URL": "http://localhost:3000",
        "APP_PREFIX": "/api/v4/security",
        "PLS_MCP_CLIENT_ID": "{env:PLS_MCP_CLIENT_ID}",
        "PLS_MCP_CLIENT_SECRET": "{env:PLS_MCP_CLIENT_SECRET}",
        "PLS_MCP_ADMIN_USER": "{env:PLS_MCP_ADMIN_USER}",
        "PLS_MCP_ADMIN_PASS": "{env:PLS_MCP_ADMIN_PASS}",
        "PLS_MCP_TRANSPORT": "stdio"
      }
    }
  }
}

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "mcp-polizei": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-polizei/dist/index.js"],
      "env": {
        "PLS_PUBLIC_URL": "http://localhost:3000",
        "APP_PREFIX": "/api/v4/security",
        "PLS_MCP_CLIENT_ID": "your-client-id",
        "PLS_MCP_CLIENT_SECRET": "your-client-secret",
        "PLS_MCP_ADMIN_USER": "admin",
        "PLS_MCP_ADMIN_PASS": "admin-password",
        "PLS_MCP_TRANSPORT": "stdio"
      }
    }
  }
}

HTTP transport (remote / Streamable HTTP)

PLS_MCP_TRANSPORT=http PLS_MCP_HTTP_PORT=3100 npm run start

The HTTP endpoint is session-based (stateful): after the initialize handshake the server returns a Mcp-Session-Id header that the client must echo in every later request. MCP clients built on the official SDK do this automatically (opencode, Claude Code/Desktop, Cursor). Routes:

  • GET /health{"ok":true} (public)

  • GET|POST|DELETE / → MCP endpoint (Streamable HTTP)

  • GET /oauth/callback → browser-login callback (public, only completes pending states)

Full flow with curl:

# 1. initialize -> capture the Mcp-Session-Id header
curl -i -X POST http://localhost:3100/ \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

# 2. tool calls with the session id (no Authorization needed in user mode)
curl -X POST http://localhost:3100/ \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: <id-from-step-1>" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"auth_status","arguments":{}}}'

opencode remote configuration (per-user mode — no header):

{
  "mcp": {
    "mcp-polizei": {
      "type": "remote",
      "url": "http://localhost:3100/",
      "enabled": true
    }
  }
}

Service mode (single admin identity, previous behavior — requires PLS_MCP_HTTP_AUTH_TOKEN):

{
  "mcp": {
    "mcp-polizei": {
      "type": "remote",
      "url": "http://localhost:3100/",
      "headers": { "Authorization": "Bearer <PLS_MCP_HTTP_AUTH_TOKEN>" },
      "enabled": true
    }
  }
}

Docker Compose

services:
  mcp-polizei:
    build: .
    restart: unless-stopped
    ports:
      - "3100:3100"          # remove this if you expose it through a reverse proxy instead
    environment:
      PLS_PUBLIC_URL: "http://<polizei-host>:3000"   # URL of the Polizei API as seen from the container
      APP_PREFIX: "/api/v4/security"
      PLS_MCP_CLIENT_ID: "${PLS_MCP_CLIENT_ID}"
      PLS_MCP_CLIENT_SECRET: "${PLS_MCP_CLIENT_SECRET}"
      # PLS_MCP_PUBLIC_URL: "https://mcp.polizei.yourdomain.com"   # enables auth_login_start
      PLS_MCP_SCOPE: "openid profile"
      PLS_MCP_TRANSPORT: "http"
      PLS_MCP_HTTP_PORT: "3100"
      PLS_MCP_HTTP_HOST: "0.0.0.0"
      # PLS_MCP_HTTP_AUTH_TOKEN: "${PLS_MCP_HTTP_AUTH_TOKEN}"      # optional service sessions
      # PLS_MCP_ADMIN_USER/PASS are only needed for service sessions

The ${PLS_MCP_*} variables come from a .env next to the compose file or from the server environment. Do not use localhost in PLS_PUBLIC_URL if Polizei runs on another host — see deploy.md for a production setup with Caddy and automatic TLS.

Development mode

npm run dev      # tsx watch src/index.ts
npm run typecheck

Exposed tools

Auth (per session): auth_login, auth_login_start, auth_poll, auth_status, auth_logout — see Authentication.

Session tenant: list_available_tenants, set_tenant, get_active_tenantlist_available_tenants lists the tenants the current identity has access to and the active one is chosen with set_tenant (by name); that tenant is sent as the X-Tenant header on every request and can be changed at any time.

Users: list_users, create_user, update_user, delete_user, assign_user_roles, remove_user_from_scope Roles: list_roles, list_assignable_roles, create_role, update_role, delete_role, list_role_permissions, assign_role_permissions Permissions: list_permissions, create_permission, update_permission, delete_permission, import_permissions Scopes (tenants): list_tenants (tree of the active tenant), list_child_scopes, create_scope, update_scope, delete_scope, get_scope_review, add_user_to_scope Invitations: list_invitations, create_invitation, delete_invitation, list_invitation_roles, list_invitation_users, reject_invitation_user, assign_invitation_roles OAuth clients: list_oauth_clients, create_oauth_client, update_oauth_client, delete_oauth_client, list_oauth_client_scopes OAuth scopes: list_oauth_scopes, create_oauth_scope, update_oauth_scope, delete_oauth_scope Profile/system: get_my_profile, get_my_permissions, get_my_scopes, search_users, update_my_profile, change_my_password, save_scope, delete_scope_own, get_tenant_settings, set_tenant_settings, get_dashboard

Resources

  • polizei://openapi — OpenAPI specification ({APP_PREFIX}/docs-json, public)

  • polizei://oidc/discovery.well-known/openid-configuration (public)

  • polizei://token/claims — claims of the current session's token (requires login)

Notes

  • Per-user isolation over HTTP: each session owns its identity, tokens and active tenant; one user's operations never use another user's privileges.

  • Scope/tenant IDs passed to the tools must belong to the current identity's tenants; otherwise the API returns 403 (reported as a readable MCP error).

  • If Polizei does not return a refresh_token, user sessions must call auth_login again after the access token expires (default ~15 min); service sessions renew automatically with the env admin credentials.

  • The create_user, update_*, etc. tools accept an extra object for additional model fields (admin bodies are Record<string, any>).

  • The OpenAPI spec is generated with @nestjs/swagger; any new endpoint in the API shows up in polizei://openapi.