Skip to main content
Glama
williamamed

mcp-polizei

by williamamed
README.md
# 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

```bash
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](#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).

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

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

```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`):

```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)

```bash
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:

```bash
# 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):

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

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

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

### Docker Compose

```yaml
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](./deploy.md) for a production setup with Caddy and automatic TLS.

### Development mode

```bash
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](#authentication).

**Session tenant**: `list_available_tenants`, `set_tenant`, `get_active_tenant` — `list_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`.

TDQS

C2.5/5.0

Scored across 55 tools

Disambiguation2/5

Many tools have overlapping purposes, such as list_available_tenants vs list_tenants, list_oauth_scopes vs list_oauth_client_scopes, delete_scope vs delete_scope_own, and add_user_to_scope vs create_user. Even with descriptive notes, the high number of similarly named tools creates significant ambiguity.

Naming Consistency2/5

Naming mixes conventions: list_* vs get_*, create_* vs save_*, update_* vs set_* vs change_*, plus import, assign, remove, reject. Also, 'scope' and 'tenant' are used interchangeably, further breaking consistency.

Tool Count1/5

With 55 tools, the surface is overwhelming and far beyond typical scopes. The excessive count makes it impractical to browse and increases selection errors.

Completeness2/5

While covering many identity operations, there are notable gaps: no get_user (only list/search), no update_invitation, no direct user-permission assignment, and no get_role/get_permission by id. The large tool count does not correspond to complete lifecycle coverage.

Maintenance

ActivityMaintained
ResponsivenessNo issues