Model Context Protocol server for Qlik Sense Enterprise. Exposes Qlik's Repository (HTTP) and Engine (WebSocket) APIs as 28 MCP tools so an LLM client can discover apps, inspect data models, query data, and manage reload tasks through a single uniform interface. In JWT mode the 14 reload-task tools are hidden, since QRS task administration needs certificate auth.
| Area | Tools | Used for |
|---|---|---|
| Repository (apps & metadata) | get_about, get_apps, get_app_details |
Discover apps, list tables and fields with cardinalities |
| Engine (data & script) | engine_query, engine_create_hypercube, get_app_script, get_app_variables, get_app_sheets, get_app_sheet_objects, get_app_object, search_app, get_app_field, engine_get_field_range, get_app_field_statistics |
Query data, read load script, list visualizations, inspect field values |
| Reload tasks (certificate mode only) | get_tasks, get_task_details, get_task_dependencies, get_task_schedule, get_task_executions, get_task_script_log, get_failed_tasks_with_logs, start_task, create_task, update_task, delete_task, create_task_schedule, update_task_schedule, delete_task_schedule |
Inspect, trigger and manage reload tasks |
Full list with descriptions: docs/tools.md.
The main analysis call takes the question, not the Qlik syntax for it:
The server writes the set analysis, checks that the filter selects
something, and answers with period_check — the earliest and latest
date actually in the result — so a filter that failed to apply is
visible instead of hiding behind a plausible number. Independent
questions go in one call as queries and share three round-trips.
engine_create_hypercube takes the same shape with the expressions
written by hand, for calculations the typed form cannot state.
uvx qlik-sense-mcp-serverThe server starts in Streamable HTTP
mode on http://127.0.0.1:8000/mcp. Configure it via environment
variables — see docs/configuration.md.
For stdio mode (legacy MCP transport), pass --stdio.
Two authentication modes are supported: client certificate (legacy,
full QRS access) and JWT via virtual proxy (per-analyst, no on-disk
secrets). See docs/AUTH_JWT.md for the JWT setup.
| Document | What's inside |
|---|---|
docs/installation.md |
Requirements, install via uvx / pip / source, certificate setup |
docs/configuration.md |
All QLIK_* environment variables, sample .env, MCP client config snippet |
docs/AUTH_JWT.md |
JWT authentication via virtual proxy: key generation, virtual proxy setup, QLIK_JWT_TOKEN usage |
docs/usage.md |
Transports, server start commands, recommended call order, hard limits enforced by this server |
docs/tools.md |
Inventory of all 27 tools, response/error envelope, error categories |
docs/architecture.md |
Project layout, components, connection caching, strict id-matching, two-tier timeout |
docs/development.md |
make targets, tests, versioning, how to add a new tool |
docs/troubleshooting.md |
Common errors, hypercube planning failures, verbose logging, configuration self-test |
docs/llm-behaviour.md |
What models actually do with this server, measured: calls per question, where they go wrong, session limits, how to benchmark honestly |
CHANGELOG.md |
Release notes |
-
A wrong query is refused, not answered. Qlik evaluates an unknown field name as an expression worth 0, so a hypercube grouped by a typo came back as a single row holding the grand total — a plausible number with nothing to mark it as wrong. Every query is checked by Engine before it runs:
ExpandExpressionresolves variables,CheckExpressionreports syntax and unknown names,GetFieldsFromExpressionreports the fields a set modifier actually filters on. The four checks cost about 4ms in one batch, against 75ms for the smallest hypercube. -
A period filter is measured, not assumed. Comparison inside a set modifier runs against the text Qlik displays for a value, so a serial number range returns 0 on a field displayed as
01.01.2024and works on one displayed as45292— with no error either way. State the period as a filter and the server tries the cheap numeric form against a reference count, falls back to the form that always works, and reports the period the result actually covers. -
One value, one writing. A date in a query result reads as the text Qlik displays for it, the same as the sample values in
get_app_detailsand the bounds fromengine_get_field_range. -
Objects say which fields they use.
get_app_sheet_objectsreturnsfields_used, including fields reached through master measures and the ones inside a filter pane's listboxes — so "what does this sheet work with" is one call. -
Paging is done by Qlik, not after it. App and task listings read
/{entity}/tablewithskip/takeand take the total from/{entity}/count, so nothing past the QRS record limit goes missing andtotal_foundis the real total. Field search and field paging likewise happen in Engine — verified on a field with 200,000 distinct values, where the old local scan simply could not see a match. -
A failure is never an empty answer. A QRS 500, a refused connection or an Engine error used to arrive as
[],""or "no schedule", which reads as a tidy, empty Qlik. Every such path now returns anerror_categoryand the original cause. -
Column meanings, not just column names. Fields and tables commented in the load script (
COMMENT FIELD/COMMENT TABLE) carry that text intoget_app_detailsascomment, and intoget_app_fieldasfield_comment, so the model reads what a column means instead of guessing from its name. Added in 1.7.2. -
Runs on both MCP SDK lines. SDK 2.0 dropped
FastMCP; the server now picksMCPServer(2.x) orFastMCP(1.x) at import time, somcp>=1.1.0,<3.0.0all work. Both lines are covered by the test suite and were verified end to end against a live Qlik app. -
Ranked queries (top-N) in one call.
engine_create_hypercubetakessort_by(a measure label, a measure expression or a dimension field),sort_order(desc/asc) andlimit, so "the 10 clients with the highest GGR" is a single request. Before v1.6.0 sorting by a measure silently did nothing —qInterColumnSortOrderwas hard-coded to the dimensions, so the server returned the alphabetically first rows instead of the largest ones. -
NULL groups stay out of rankings. Facts with no value for the grouping field collapse into Qlik's
"-"row, which often holds a large total and would otherwise take first place in a top-N. It is dropped by default; passexclude_null_dimensions=falseto measure how much data is unattributed. -
Compact, LLM-friendly results. The hypercube response is
columns+rowswith real numbers, plusgrand_totaland per-steptimings. Passinclude_raw_layout=truefor the full Qlik layout. -
Failures name the query that failed. Every error reply, timeouts included, echoes
toolandrequestwith the exact arguments sent. -
Fewer useless tools in JWT mode. Reload-task administration needs QRS admin rights, so those 14 tools are registered only in certificate mode: 27 tools with a certificate, 13 with a JWT.
-
One Qlik session per server. Qlik's per-user limit (5 by default) counts proxy sessions, and in JWT mode one is created by the session bootstrap itself — before any WebSocket. The server therefore bootstraps once and reuses that session for every call; restarting it in a loop is what exhausts the quota, not the number of queries.
-
JWT authentication via virtual proxy. Set
QLIK_JWT_TOKENinstead of certificate paths and the server will authenticate every Repository and Engine call as the analyst encoded in the token. No certificates or private keys live on the host. The legacy certificate mode is unchanged and still required for full QRS access. Setup guide:docs/AUTH_JWT.md. -
Cached Engine WebSocket connections. Once an app is opened, every subsequent tool call against the same
app_idreuses the same WebSocket and the same open document. Switchingapp_idcloses the old document and opens the new one on the same socket. Dropped connections are reopened transparently. Implementation:engine_api.pyanddocs/architecture.md. -
Streamable HTTP transport by default. The server is a long-lived process; multiple MCP clients can talk to it in parallel. The legacy stdio mode still works behind
--stdio. -
tool_call_secondsis injected as the first key of every tool response — wall-clock time of the call in milliseconds. Use it to spot slow tools. -
Hard hypercube limits.
engine_create_hypercuberejects requests withmax_rows > 5000orcolumns * max_rows > 9900immediately, with a structured error and a hint pointing at set-analysis or top-N patterns. Qlik Engine itself returns error 7009calc-pages-too-largefor any single page over 10000 cells. -
Single timeout knob.
QLIK_WS_TIMEOUT(default180.0seconds) controls both the WebSocket handshake and every Engine API call.
- Python 3.12 (the package is built and tested against this version; see
pyproject.toml) - Qlik Sense Enterprise (Repository on port 4242, Engine on port 4747 — the standard ports)
- Client certificate, private key and root CA from the Qlik Sense node
- Network access from the host running this server to Qlik
This project is an independent, community-built integration. It is NOT affiliated with, endorsed by, sponsored by, or supported by Qlik Technologies Inc., QlikTech International AB, or any other Qlik entity. "Qlik", "Qlik Sense", "QlikView" and all related product names are trademarks of their respective owners.
All information about Qlik Sense APIs, port allocations, error codes, protocol behavior and usage patterns used in this project was obtained exclusively from publicly available sources — the Qlik Developer Portal (help.qlik.com, qlik.dev), the Qlik Community forums, and other public documentation. No proprietary, confidential or reverse-engineered material is used.
MIT © 2025-2026 Stanislav Chernov