Skip to content
8

Pulse operations ​

Run Pulse in production: the Docker image, its security model, environment variables, toolsets, health routes and CI.

Deployment artifact ​

The server ships as a single multi-arch Docker image on the GitHub Container Registry, ghcr.io/miragon/pulse-server (:<version> or :latest):

bash
docker run --rm -p 127.0.0.1:8400:8400 \
  -e CAMUNDA_BASE_URL=... \
  -e PROMETHEUS_URL=... \
  ghcr.io/miragon/pulse-server:latest

release-please tags v<version>; after manual approval publish-to-docker.yml builds the root Dockerfile and pushes both tags. The HEALTHCHECK polls /health/ready. SIGTERM stops accepting, lets in-flight requests finish (≤ 4 s; readiness answers them 503 draining, so add a Kubernetes preStop sleep), then closes the database pool. playground/docker/docker-compose.yml binds every port to 127.0.0.1 (--profile full adds the server); Fly.io: playground/README.md.

Security ​

Without MCP_OAUTH the endpoint is unauthenticated and every module without a toolset suffix runs read-only; with it the server is an OAuth resource server (Keycloak or Auth0: bearer tokens validated on /mcp, 401 otherwise, .well-known metadata served) and the default rises to operations / standard (see Module activation). Tokens must carry the canonical MCP URL (MCP_URL, then required) in aud (RFC 8707); any other provider or config key fails the boot. Every request's Host (and a POST's Origin) must be localhost-class, MCP_URL's or allow-listed below, else 403 (DNS rebinding).

A gateway or reverse proxy that terminates auth in front of the server is invisible to it: such a deployment runs read-only until MCP_ACTIVE_MODULES names its toolsets, and saves no settings. Identity reaches the server only as a bearer token it validates itself (MCP_OAUTH), never from a header. admin is only reached by naming it, never on a server anyone can reach unauthenticated. Deploying a BPMN/DMN is code execution inside the engine JVM (JUEL expressions, scripts), so camunda7_create_deployment also needs CAMUNDA_ALLOW_DEPLOYMENTS=true.

CAMUNDA_AUTH_TYPE=passthrough forwards each caller's token to the engine (never to Prometheus), which enforces the caller's permissions if its REST auth is on; only for a trusted single-audience setup, since the engine could replay that token here. With auth, profiles and saved dashboards scope to the user (sub); without it there is no caller identity: settings saves refuse, and the dashboard builder and the language save are absent (they also need every module above read-only). docker compose --profile auth up -d adds a local Keycloak on :8480 (realm miragon, demo/demo; see .env.example). The public Fly playground is unauthenticated and pinned to camunda7:read-only,analytics:read-only.

Environment variables ​

VariableDefaultNotes
PORT8400HTTP port the MCP server listens on
MCP_URLunsetPublic base URL: advertised URLs, the OAuth token audience, the Host/Origin the server accepts, and the origin the views load Geist from: the server serves it at /fonts/geist-<version>/ (woff2, cached immutable, no CDN) and declares the origin in every view's CSP resourceDomains; unset, a view uses the origin its host read it from. A host that blocks the origin, or a gateway that does not route /fonts/, shows the host's font
MCP_ALLOWED_HOSTS / MCP_ALLOWED_ORIGINSlocalhost-classFurther hostnames / origins (comma lists) admitted next to MCP_URL's; an origin without a scheme matches any scheme and port, * turns the check off (only behind an edge that validates Host itself)
MCP_MAX_BODY_BYTES4194304Request-body cap (4 MiB): larger bodies get 413 before they are buffered, ahead of the OAuth gate. At most 4× the cap is held across in-flight requests (503 beyond) and a request must arrive within 30 s. An unauthenticated client can still spend that budget, so rate-limit a public server at its proxy (pnpm dev is uncapped)
MCP_OAUTHunsetJSON OAuth resource-server config, the only caller identity; providers keycloak, auth0 (anything else fails the boot). Full field lists: .env.example
MCP_ACTIVE_MODULESall, read-onlyComma-separated module or module:toolset entries, e.g. camunda7:operations,analytics:standard; no suffix = read-only, under OAuth the standard toolset (see Module activation)
DATABASE_URLunsetPostgres for saved dashboards + user profiles (both stores; migrations run at boot). Beats MCP_*_DIR; the Compose stack ships an instance on host port 8440
MCP_DASHBOARD_DIR / MCP_PROFILE_DIRin-memoryDirectories persisting saved dashboards / user profiles across restarts (the file-based alternative when no DATABASE_URL is set); one server process per directory. A NODE_ENV=production boot (the Docker image) that keeps a store in memory logs a loud warning, but still starts
CAMUNDA_ENGINES_FILE / CAMUNDA_ENGINES_JSONunsetEngine list as a file path / inline JSON (the file wins; highest precedence): [{id, baseUrl, cockpitUrl?, environment?, flavor?, auth?}, ...] or the environment map {"<environment>": [engines…]}. environment groups engines in pickers and view heads; flavor (cibseven | operaton | camunda7, default camunda7) selects the vendor's cockpit-link routes and product name; mixed fleets are fine.
CAMUNDA_BASE_URLhttp://localhost:8410/engine-restSingle-engine REST endpoint (registered as id CAMUNDA_ENGINE_ID); ignored when CAMUNDA_ENGINES_* is set. The server warns at boot when no engine source is set at all: the silent fallback works against the Compose engine but breaks the engine_id join
CAMUNDA_ENGINE_ID / CAMUNDA_ENGINE_FLAVORdefault / camunda7Id and vendor (cibseven | operaton | camunda7) of the CAMUNDA_BASE_URL shorthand. The id must match the engine container's ENGINE_ID (= the engine_id metric label): analytics reads only the configured engine ids, so a mismatch empties ALL analytics for that engine and its health reads unknown. A CIB seven engine sets cibseven, or its links open the Camunda 7 cockpit
CAMUNDA_COCKPIT_URLderivedUsed for jump-out links to Cockpit; multi-engine setups use per-engine cockpitUrl instead. Unset, links derive from the REST base URL and carry its host. Set a public URL when that host is internal
CAMUNDA_AUTH_TYPEnonebasic, bearer, passthrough, or none (fallback for engines without a per-engine auth)
CAMUNDA_USERNAME / CAMUNDA_PASSWORD / CAMUNDA_TOKENunsetRequired for basic (username + password) / bearer (token), enforced at boot
CAMUNDA_ALLOW_DEPLOYMENTSfalsetrue registers camunda7_create_deployment (also needs camunda7:admin). Deploying runs code inside the engine JVM. Only true/false (empty = unset); any other value fails the boot
CAMUNDA_INCIDENT_ISSUE_REPOunsetThe one owner/repo incident-ticket drafts target with a prefilled GitHub URL (no tool argument redirects it); unset → no URL
CAMUNDA_HEALTH_CRITICAL_*50 / 25…_INCIDENTS / …_CLUSTER_SIZE: thresholds for the engine-health critical verdict
CAMUNDA_REQUEST_TIMEOUT_MS / PROMETHEUS_TIMEOUT_MS30000Per-request deadline (ms) for engine / Prometheus calls: a hung upstream fails the tool call with a timeout error naming it. Whole milliseconds only; anything else fails the boot
PROMETHEUS_URLhttp://localhost:9090Prometheus HTTP API, the analytics data source (the repo's Compose stack publishes :8460; the server warns at boot when unset). Auth: PROMETHEUS_BEARER_TOKEN, or PROMETHEUS_USERNAME + PROMETHEUS_PASSWORD (URL userinfo moves there); PROMETHEUS_HEADERS adds a JSON object of headers (tenant ids). Ambiguous auth fails the boot
ANALYTICS_ENGINE_IDSunsetComma-separated engine_ids analytics may read, only for a boot without camunda7; with camunda7 active its configured engine ids scope analytics and this is ignored. With neither, every analytics tool refuses (fail-closed), so a shared Prometheus never exposes other teams' engines

Unknown CAMUNDA_*/MCP_* variables warn at boot; mcp-use telemetry is off (MCP_USE_ANONYMIZED_TELEMETRY=true opts in). The engine container takes METRICS_ENABLED, ENGINE_ID (must match the server's engine id, or that engine's analytics come back empty), OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_SERVICE_NAME and, per node of a clustered engine, OTEL_RESOURCE_ATTRIBUTES=service.instance.id=<node>.

Module activation ​

MCP_ACTIVE_MODULES lists the modules (unset/all = every module), e.g. camunda7 alone where no Prometheus exists (the cockpit drops its cross-engine view). Each module runs one toolset per boot:

ToolsetSurfaceNo-suffix default
camunda7:read-onlyreadOnlyHint tools only: queries incl. camunda7_list_engineswithout OAuth
camunda7:operations+ start, claim/assign/complete tasks, variables, job + external-task retries, resolve incidents, messages, default enginewith OAuth
camunda7:admin+ delete/modify/suspension, migrations, batch retries, signals, the external-task worker protocol, deployments (with the flag)never
analytics:read-onlyevery analytics tool, no settings savewithout OAuth
analytics:standard+ analytics_save_settingswith OAuth

An empty (camunda7:) or unknown suffix warns and falls back to read-only, even under OAuth. Widgets hide action buttons whose tool the toolset drops. The full surface of earlier releases is camunda7:admin,analytics:standard plus CAMUNDA_ALLOW_DEPLOYMENTS=true, with MCP_OAUTH for the dashboard builder.

Observability ​

Three routes sit next to /mcp, outside the OAuth gate and the Host check:

RoutePurpose
/health/liveLiveness: 200 as soon as the process serves HTTP
/health/readyReadiness: 200 once the server's own dependencies respond (the Postgres store under DATABASE_URL), else 503 naming the failing check. Never probes engines or Prometheus: their outages surface as tool errors, not as an unroutable server. /health aliases it (Docker HEALTHCHECK, Compose, Fly)
/metricsPrometheus text: mcp_tool_calls_total{tool,outcome}, mcp_tool_call_duration_seconds{tool}, mcp_http_requests_total{method,route,status}, mcp_http_request_duration_seconds{method,route} plus the standard process_*/nodejs_* collectors. Labels are bounded by construction (tool catalogue, known routes), never users, sessions or arguments. The tool counters see calls that reached a handler; an unknown tool or invalid arguments show only in the request log. With MCP_METRICS_TOKEN set it requires that bearer token (401 otherwise)

mcp-use logs one line per request to stdout (MCP_USE_LOG_LEVEL=debug/trace adds payloads; the boot warns). The server expects Camunda 7 / CIB seven and Prometheus; Grafana is optional (:8470). Engine metrics come from the Kotlin plugin via any Micrometer export (see Architecture); alert rules ship in playground/docker/prometheus/alerts.yml, and analytics_engine_health surfaces the same gauges and firing alerts in one call.

CI/CD ​

.github/workflows/ci.yml runs on public npm dependencies only: TypeScript (build, test, lint, format), Kotlin engine plugins, the CIB seven example, the brand checks, the CLI and the VS Code extension against Camunda 7, CIB seven and Operaton, the CLI's binaries, image and plugin, the extension in VS Code and as a .vsix. A push to main runs every job, a pull request those its changes need (scripts/ci-scopes.mjs); mutation testing runs nightly. docs.yml checks the docs kit, builds the site on pull requests touching docs/ (a dead link fails it) and publishes it from main.