Skip to main content
Glama
EngIcaro
by EngIcaro
README.md
# Qlik Lineage MCP

A read-only [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server
that exposes data-file lineage analyses for [Qlik Cloud](https://www.qlik.com/us/products/qlik-cloud)
tenants. Built especially for tenants on the **capacity** pricing model
(daily peak GB), where dropping unused columns / ghost files yields direct savings.

## Tools

| Tool | What it answers |
|---|---|
| `unused_columns` | Given a data file (QVD or Parquet) and its space, which columns are not consumed by any app in the tenant? |
| `ghost_files`    | Given a space, which data files are not consumed by any app — including transitive chains (file -> file -> app)? |

Both tools are **read-only**. They recommend, they never delete.

## Quick start

```powershell
# 1. Install deps with uv (or pip)
uv sync

# 2. Copy and fill the env file
copy .env.example .env
# edit QLIK_TENANT_URL and QLIK_API_KEY

# 3. Run the MCP server (stdio transport)
uv run qlik-lineage-mcp
```

Wire the server into Claude Desktop / Claude Code / VS Code:

```json
{
  "mcpServers": {
    "qlik-lineage": {
      "command": "uv",
      "args": ["run", "qlik-lineage-mcp"],
      "cwd": "C:/path/to/qlik-lineage-mcp"
    }
  }
}
```

## Architecture

```
src/qlik_lineage_mcp/
├── server.py       # FastMCP entry point — auto-registers everything in tools/
├── config.py       # env-var loader (.env fallback)
├── qlik_client.py  # all Qlik Cloud HTTP calls live here
├── models.py       # format-agnostic Pydantic models (DataFile = QVD or Parquet)
└── tools/
    ├── __init__.py        # auto-discovers and calls register(mcp) on each module
    ├── unused_columns.py
    └── ghost_files.py
```

**Adding a new tool:** drop a file in `tools/` that exports `register(mcp: FastMCP)`.
`server.py` does not need to be edited.

## How `unused_columns` works

A 3-phase pipeline that uses Qlik's field-level lineage (no script parsing):

1. **Enumerate columns** — `GET /lineage-graphs/nodes/{file_qri}?level=field`
   exposes every column of the target file as a node whose QRI starts with
   the file QRI.
2. **Find consumer apps** — iterate every app in the tenant and read its
   `data/lineage`. Apps whose discriminators include a `lib://...{file_name}`
   LOAD reference are the consumers.
3. **Detect renames** — for each consumer app, fetch
   `GET /lineage-graphs/nodes/{app_qri}?level=field`. Edges whose source is
   a field of the target file map the original column to the alias used by
   the app. Qlik decomposes composite expressions automatically, so
   `LOAD A_COD & '\' & A_LOJA AS KEY FROM file` produces two edges
   (`A_COD->KEY`, `A_LOJA->KEY`) — no script parser needed.

Cost: one file-side call + N `data/lineage` calls (one per app, also paid
by `ghost_files`) + M field-level lineage calls (one per consumer app).

## Known limitations

- Rename detection requires the consumer app to have been **reloaded
  since field-level lineage was activated in the tenant**. Apps that have
  not been reloaded show no edges in their field-level graph, so renames
  in those apps are invisible. The output lists which consumer apps could
  not be inspected so the verdict is auditable.
- `ghost_files` walks every app's `data/lineage` and builds an app/file
  graph, then runs a fixpoint to mark useful chains. Dependencies hidden
  inside `SUB` / `CALL` / `$(include)` or dynamic file paths are missed.
- Apps whose `data/metadata` cannot be fetched (permissions, errors) are
  surfaced as a top-level `metadata_unavailable_apps` caveat — verdicts
  are conditional on those apps being checkable.
- Parquet support is implemented format-agnostically but until a real
  Parquet item-endpoint fixture is captured, surfacing of Parquet files
  is best-effort. The tools flag this in their output.

## Testing

```powershell
uv run pytest
```

All tests run against captured JSON fixtures in `tests/fixtures/` —
no live tenant calls.

TDQS

A3.5/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: ghost_files identifies unused data files, while unused_columns identifies unused columns within a file. No overlap.

Naming Consistency5/5

Both tool names follow a consistent snake_case verb_noun pattern (ghost_files, unused_columns), making them predictable.

Tool Count4/5

With only 2 tools, the server is focused on a narrow domain (identifying unused resources). While minimal, the scope is appropriately scoped for its specific purpose.

Completeness3/5

The tools cover file-level and column-level unused resources, but missing capabilities such as listing spaces, retrieving lineage graphs, or identifying unused apps, which may be needed for full lineage analysis.

Maintenance

ActivityInactive
ResponsivenessNo issues