Skip to main content
Glama
ryailabs

qlik-cloud-admin

by ryailabs
README.md
# Qlik Cloud Admin MCP Server

> **Demo and experimental use only.**
> This project is provided as-is to illustrate concepts. It is not production-ready,
> not officially supported by Qlik, and carries no warranty of any kind.
> Anyone using this code does so at their own risk and is responsible for testing
> and validating it in their own environment before use.

---

## What This Is

A local [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that connects
Claude Desktop to a Qlik Cloud tenant for admin operations.

```
Claude Desktop -> MCP server (this project) -> Qlik Cloud REST API
                        ^
                        └── signed in as YOU, with your own permissions
```

It demonstrates three things:

1. **Extensibility** - Qlik Cloud exposes a rich REST API. By wrapping it in an MCP server,
   any admin operation becomes available as a natural language conversation in Claude Desktop.

2. **Governance** - MCP tools can encode business rules. This server shows a concrete example:
   deleting a resource does not hard-delete it. Instead, it moves it to a "Recycle Bin"
   shared space, giving admins a recovery window. Deleting spaces is blocked entirely and
   redirected to the Qlik Cloud Management Console.

3. **Identity** - the server acts as the person using it, never as an admin service account.

This server complements the official [Qlik MCP Server](https://help.qlik.com/en-US/cloud-services/Subsystems/Hub/Content/Sense_Hub/QlikMCP/Qlik-MCP-server-tools.htm),
which covers analytics operations. This one covers admin and governance operations that the
official server does not expose today.

---

## Permissions

**This server acts as the person using it.** On first use you sign in to Qlik Cloud in your
browser, and every request the server makes carries your own identity. If you cannot open an
app, a space, or the data load editor in the Qlik hub, you cannot do it here either.

There is no way to elevate, and that is structural rather than a policy:

- The only credential in the running process is an access token whose subject is you.
- The OAuth client is **public** - no client secret exists to leak or share, so nothing on your
  machine can mint a token for another identity.
- Only the `user_default` and `offline_access` scopes are ever requested. No `admin.*` scope is
  requestable, by configuration or otherwise.
- There is no API key, no client-credentials grant, and no impersonation grant in the codebase.
- A `403` is reported as a permission result. Nothing is retried against a different identity,
  because there is no other identity.

Consequences worth knowing: two users legitimately get different answers to "how many apps are
there", and retiring someone else's app requires an admin to be the one signed in.

Credential endpoints (`/api/v1/api-keys`, `/api/v1/oauth-clients`, `/api/v1/oauth-tokens`,
`/oauth/*`) are refused by the tools before a request is sent. Not because they would escalate
anything, but so a durable credential cannot be minted and echoed into a chat transcript.

---

## Requirements

- [uv](https://docs.astral.sh/uv/) installed on your machine
- A Qlik Cloud account on the tenant
- The tenant URL and public OAuth client id from your Qlik admin (see below)
- Claude Desktop

qlik-cli is **not** required. Earlier versions shelled out to it; this one talks to the REST
API directly.

---

## Setup

### For admins: register the OAuth client (once)

Create one **native** (desktop) public OAuth client for the whole team. If the Management
Console does not offer a native option, create it through the API:

```bash
curl -X POST "https://<tenant>/api/v1/oauth-clients" \
  -H "Authorization: Bearer <admin token>" \
  -H "Content-Type: application/json" \
  -d '{"clientName":"Qlik Cloud Admin MCP",
       "description":"Local MCP server. Each user signs in as themselves.",
       "appType":"native",
       "redirectUris":["http://localhost/callback"],
       "allowedScopes":["user_default","offline_access"]}'
```

- Do **not** send `allowedGrantTypes` or `allowedAuthMethods`: the API rejects them for
  `appType: native`, and the grants are implied.
- **Redirect URL: `http://localhost/callback`** - exactly this host and path. Qlik matches
  scheme, host and path exactly but **ignores the port**, so one entry covers every user and
  every run. `127.0.0.1` is not an alias for `localhost` here.
- Scopes: `user_default` and `offline_access` only. Granting `admin.*` or `admin_classic`
  would defeat the permission model; the server never requests them.
- Leave the consent method unset. Users then see a Qlik "authorize this application" screen on
  first sign-in, which is the authorization step this design wants.
- No client secret is issued, and none is needed. Give users the client id and the tenant URL.

Reference: [Create an OAuth client](https://qlik.dev/authenticate/oauth/create/create-oauth-client-spa)
and the [OAuth clients API](https://qlik.dev/apis/rest/oauth-clients).

### For users: install and sign in

**1. Install uv**

```powershell
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
```

**2. Download this repository**

```powershell
git clone https://github.com/mabaeyens/qlik-cloud-admin-mcp.git
cd qlik-cloud-admin-mcp
```

**3. Configure**

```powershell
copy .env.example .env
```

Fill in the two values your admin gave you. Neither is a secret:

```
QLIK_TENANT_URL=https://your-tenant.eu.qlikcloud.com
QLIK_OAUTH_CLIENT_ID=your_public_client_id
```

**4. Create the Recycle Bin space**

In Qlik Cloud, create a **shared space** named exactly `Recycle Bin`. The delete tool moves
apps, automations, and data files there instead of hard-deleting them.

Then **add every user who will retire content as a member with edit rights**. Being able to
see the space is not enough, and being a tenant admin is not enough either: moving content
into a shared space needs a contributor role in that space, so a tenant admin with no role
there gets a 403. Create the space as an admin, or grant the admin full permissions on it
afterwards.

**5. Configure Claude Desktop**

Open `%APPDATA%\Claude\claude_desktop_config.json` and add the block below inside
`mcpServers`, replacing the path with where you cloned the repository.

```json
{
  "mcpServers": {
    "qlik-cloud-admin": {
      "command": "uv",
      "args": ["--directory", "C:/path/to/qlik-cloud-admin-mcp", "run", "server.py"],
      "env": {
        "QLIK_TENANT_URL": "https://your-tenant.eu.qlikcloud.com",
        "QLIK_OAUTH_CLIENT_ID": "your_public_client_id"
      }
    }
  }
}
```

**6. Restart Claude Desktop and sign in**

Fully quit and reopen Claude Desktop. Then ask Claude to sign you in, which calls
`qlikcloud_login`. Your browser opens and Qlik asks you to **authorize this application** -
approve it. That consent screen is the moment you grant the server permission to act as your
account; it appears once per account. The tab then tells you it is safe to close.

Run `qlikcloud_health` to confirm which account you are signed in as.

To switch accounts, ask to sign in as a different account — `qlikcloud_login` with
`switch_account=True` makes Qlik ask which account to use instead of silently reusing whatever
session your browser already has. Without it, a live Qlik session in the browser signs you
straight back in as the same account.

**7. Set the system prompt (recommended if also using the official Qlik MCP Server)**

If you have both this server and the [official Qlik MCP Server](https://help.qlik.com/en-US/cloud-services/Subsystems/Hub/Content/Sense_Hub/QlikMCP/Qlik-MCP-server-tools.htm) connected, create a Claude Desktop project and add the following system prompt so Claude always prefers the official server for analytics operations:

> When both the official Qlik MCP Server and qlik-cloud-admin are connected, always prefer the official Qlik MCP tools for analytics operations (opening apps, searching fields, listing sheets, reading app content). Use qlik-cloud-admin tools only for admin and governance operations not covered by the official server.

---

## Usage

| Tool | Description |
|---|---|
| `qlikcloud_login` | Sign in to Qlik Cloud in your browser, as yourself |
| `qlikcloud_logout` | Sign out, revoke the token, and remove the stored session |
| `qlikcloud_health` | Tenant, signed-in identity, roles, storage backend, token expiry |
| `qlikcloud_get` | Read any Qlik Cloud REST endpoint (`fetch_all=True` to follow pagination) |
| `qlikcloud_delete` | DELETE with governance rules enforced (see below) |
| `qlikcloud_assistant_chat` | Send a message to a Qlik Answers assistant and get a response |

Six tools, and `qlikcloud_get` is the only way to read. When the official Qlik MCP Server covers
an operation (opening apps, searching fields, listing sheets), prefer those tools instead.

**Why there is no generic POST, PUT or PATCH.** They were removed in v0.4.0. A model holding an
unrestricted write verb against a whole tenant is a large blast radius for very little gain, and
any write worth making is worth a purpose-built tool that can say what it is about to change and
ask first — which is what `qlikcloud_delete` does. Keeping the surface small has a second
benefit: MCP clients that ration tool schemas start hiding them behind a search once enough
servers are connected, and a hidden read tool is one a model can conclude does not exist.

### Pagination

Qlik list endpoints return one page at a time. Pass `fetch_all=True` to `qlikcloud_get` when
you want a complete list or a count:

```
qlikcloud_get(path = "/api/v1/spaces", fetch_all = True, max_pages = 10)
```

The merged result reports `pageCount`, `itemCount`, and `truncated`. If a cap is reached,
`truncatedReason` says which - the list is never silently cut short. Note that "complete" means
complete for you: results are already filtered to what your account can see.

### qlikcloud_assistant_chat

Query a Qlik Answers assistant with conversation context. On the first call, omit `thread_id`
and a new thread is created automatically. Pass the returned `thread_id` on follow-up calls to
continue the conversation.

```
qlikcloud_assistant_chat(
    assistant_id = "abc123",   # from /api/v1/assistants
    message     = "What were total sales last quarter?",
    thread_id   = None         # omit on first call; reuse on follow-ups
)
```

### Governance rules in qlikcloud_delete

- **Apps** - moved to the "Recycle Bin" shared space, never hard-deleted.
- **Automations** - moved to the "Recycle Bin" shared space, never hard-deleted.
- **Data files** - moved to the "Recycle Bin" shared space, never hard-deleted.
- **Anything already in the Recycle Bin** - refused. See below.
- **Spaces** - blocked. Must be deleted manually in the Qlik Cloud Management Console.
- **All other resource types** - not supported by this tool.

**The Recycle Bin is a one-way door.** Nothing can be deleted permanently through this
server, and anything already in the Recycle Bin is refused outright - for every user, tenant
admins included. Emptying the Recycle Bin is done by hand in the Qlik Cloud Activity Center in
the hub. There is no tool, flag, or path that overrides this: `qlikcloud_delete` is the only
tool that can delete anything at all, and all it ever does is move.

These rules run with your own rights: you can only retire resources you are allowed to move,
and you must be able to see the Recycle Bin space *and* hold edit rights in it. If either is
not true the tool says so and moves nothing. Retiring another person's app is an admin action,
performed by an admin being the one signed in.

---

## Companion skill: model archaeology

`skills/qlik-model-archaeology/` is a Claude skill that turns these tools into a workflow for
making sense of a badly documented app: map the data model, work out what `DATE3` and `COS`
actually mean from glossary, lineage and script evidence, and produce a data dictionary.

Install it by copying or symlinking it where Claude looks for skills:

```bash
mkdir -p ~/.claude/skills
ln -s "$PWD/skills/qlik-model-archaeology" ~/.claude/skills/qlik-model-archaeology
```

It works with the tools listed above - no extra setup - because the evidence it relies on is all
reachable over REST. Its central rule is that a *retrieved* answer (a verified glossary term, a
source column name) is reported differently from an *inferred* one, and that "I cannot determine
this, ask this steward" is a valid and preferred answer when the evidence is not there.

## API Path Format

Paths are ordinary Qlik Cloud REST paths:

```
/api/v1/spaces
/api/v1/users/me
/api/v1/apps/{appId}
```

Reference: https://qlik.dev/apis/rest/

The older qlik-cli shorthand (`v1/spaces`, without `/api`) is still accepted, so prompts
written for v0.2.x keep working.

---

## Troubleshooting

Run `qlikcloud_health` first: it reports the tenant, the signed-in identity, where the session
is stored, and when the token refreshes.

| Symptom | Check |
|---|---|
| "Not signed in" | Run `qlikcloud_login` |
| Browser did not open | The login tool prints the sign-in URL - paste it into a browser |
| `redirect_uri is not registered` | The client needs `http://localhost/callback` registered - exact host and path, port irrelevant. `127.0.0.1` does not match `localhost` |
| Console will not accept a localhost redirect URL | Create the client via the API with `appType: "native"` (see the admin section) |
| Signed in as the wrong account | `qlikcloud_login` with `switch_account=True` - it forces Qlik to ask which account to use |
| 403 on something you expect to work | Your account lacks the permission. Check it in the hub - the server cannot elevate |
| 403 on a Management Console page even as a tenant admin | Expected. The token carries only the `user_default` scope, so admin-classified endpoints (licences, roles, encryption keys) are refused for everyone. Use the Management Console |
| Session ends sooner than expected | The client may not grant `offline_access`, so no refresh token is issued. Ask your admin to add the scope |
| Session storage says "file" not "OS keychain" | No usable keychain on this host. The refresh token is in a 0600 file instead |
| Connector shows as failed in Claude Desktop | Check the Claude Desktop MCP log; this server logs to stderr |

### Where the session is stored

The refresh token goes in the OS keychain (macOS Keychain, Windows Credential Manager, Secret
Service on Linux) when one is available, and in a `0600` file under
`~/.config/qlik-cloud-admin-mcp/` otherwise. Non-secret metadata (which account is active)
lives in `sessions.json` in that directory either way.

To reset completely: `qlikcloud_logout(all_accounts=True)`, or delete that directory and the
`qlik-cloud-admin-mcp` keychain entries.

---

## Upgrading from v0.2.x

v0.3.0 changes the identity model. Previously every user of the server acted as the tenant
admin whose API key was configured; now each user acts as themselves.

1. **Admin:** register the public OAuth client (see Setup) and hand out the tenant URL and
   client id.
2. **Each user:** replace `QLIK_API_KEY` in `.env` - and in `claude_desktop_config.json`, if it
   was set there - with `QLIK_OAUTH_CLIENT_ID`, and set `QLIK_TENANT_URL`.
3. Restart Claude Desktop, run `qlikcloud_login`, then `qlikcloud_health`.
4. **Admin: revoke the old tenant-admin API key.** Nothing uses it any more, and a live admin
   key with no consumer is pure risk.
5. Expect narrower results than v0.2.0 if you are not an admin. That is the point of the
   change, not a bug.
6. qlik-cli can stay installed; nothing here touches it any more.

---

## Development

```bash
uv sync                 # install dependencies, including the dev group
uv run pytest           # unit tests, no network access needed
uv run server.py        # run the server directly (stdio transport)
```

Design documents live in `specs/` (not published). Every behavioural rule in this README is
backed by a test in `tests/`.

---

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for release notes.

---

## License

MIT

TDQS

A4.4/5.0

Scored across 6 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: read, delete, auth lifecycle, health check, and assistant chat. The generic GET and DELETE tools are broad but non-overlapping in action, so there is no ambiguity.

Naming Consistency5/5

All tools follow the consistent qlikcloud_<action> snake_case pattern, with verbs like get, delete, login, logout, and assistant_chat. Naming is predictable and uniform.

Tool Count4/5

Six tools is a reasonable count for a focused admin client that exposes generic GET/DELETE operations plus auth and health. It is slightly lean but not inappropriately small for the stated scope.

Completeness2/5

The tool set only supports read and delete operations, with no create or update capabilities for Qlik Cloud resources. While GET covers all read endpoints, the absence of POST/PUT/PATCH leaves significant lifecycle gaps for an admin-oriented server.

Maintenance

ActivityStale
ResponsivenessNo issues