Skip to main content
Glama
README.md
<div align="center">

<img src="images/Abelara_Icons_RGB-06.png" alt="QuestLLens" width="120">

# QuestLLens

### Give AI Eyes on Your Time-Series

**The self-documenting QuestDB MCP server that turns any QuestDB instance into a rich, queryable knowledge source for AI agents — with first-class awareness of partitions, symbols, dedup keys, WAL state, ingestion health, and storage layout.**

[![MCP SDK](https://img.shields.io/badge/MCP_SDK-1.25+-blue?style=flat-square)](https://modelcontextprotocol.io)
[![QuestDB](https://img.shields.io/badge/QuestDB-7.4+-D14671?style=flat-square&logo=questdb&logoColor=white)](https://questdb.io)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.6+-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
[![Node.js](https://img.shields.io/badge/Node.js-20+-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?style=flat-square&logo=docker&logoColor=white)](https://www.docker.com)
[![License](https://img.shields.io/badge/License-Apache_2.0-green?style=flat-square)](LICENSE)

[Getting Started](#getting-started) · [Tools](#tools) · [Configuration](#configuration) · [Docker](#docker) · [Security](#security) · [Domain Context](#domain-context)

</div>

---

## Why QuestLLens?

AI models are powerful — but they're blind to your time-series database. They don't know your designated timestamp, your partition strategy, your symbol cardinality, or which tables are running behind on WAL apply.

**QuestLLens fixes that.** It connects any QuestDB instance to AI assistants via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), giving them 18 purpose-built tools to discover, understand, and query your data — safely, in read-only mode, with zero risk of accidental writes.

### How this relates to QuestDB's built-in MCP server

QuestDB ships [its own MCP server](https://questdb.com/docs/getting-started/web-console/mcp-server/) in the Web Console, and for interactive work at your desk it is the better tool — it has notebooks, charts, SQL/function documentation lookup, and two-way handoff with the console you already have open. Use it for that.

It solves a different problem than this one:

| | QuestDB Web Console MCP | QuestLLens |
|---|---|---|
| Transport | WebSocket, **loopback only** | HTTP/SSE, remote-reachable |
| Needs a live browser session | Yes — pairing and consent happen in the console | No |
| Write access | **Yes** — DDL/DML at the Write permission level | No — read-only enforced in-process |
| Auth for remote clients | Console session / Enterprise SSO | OAuth 2.1 + PKCE, or none for local use |
| Notebooks, charts, docs lookup | Yes | No |
| Partition, WAL, dedup, symbol, ingestion-health tools | No | Yes |
| Domain-context injection into tool descriptions | No | Yes |

Reach for QuestLLens when the agent isn't sitting at your browser: a headless assistant, a container behind a tunnel, a shared team endpoint — or anywhere you need a hard read-only guarantee rather than a permission setting.

### What makes QuestLLens different

- **Time-series native** — Unlike generic SQL MCP servers, QuestLLens speaks QuestDB. Designated timestamps, time partitions, symbol capacity, dedup keys, and WAL state are all first-class concepts your AI assistant can reason about.
- **Self-documenting** — Automatically extracts table metadata, column types, partitions, indexes, and materialized view definitions. Your AI assistant understands your schema the way your team does.
- **Domain-aware** — Inject a simple markdown file with business context (what tables mean, common `SAMPLE BY` patterns, gotchas) and QuestLLens weaves it into every tool response.
- **Non-invasive** — Plugs into any QuestDB instance over the standard PostgreSQL wire protocol. No agents, no extensions, no QuestDB config changes. Just a read-only user.
- **Security-first** — Defense-in-depth: SQL keyword blocking tuned for QuestDB's full DDL surface, statement timeouts, row limits, and optional OAuth with rate limiting. Your data stays safe.

---

## Getting Started

### Prerequisites

- **Node.js** 20+
- **QuestDB** 7.4+ (any hosted or self-managed instance — WAL tables became the default in 7.4)
- A QuestDB user with `SELECT` privileges (read-only recommended; see [Security](#security))

### Quick Start (npm)

```bash
# Clone and install
git clone https://github.com/DMDuFresne/questllens.git
cd questllens
npm install

# Configure
cp .env.example .env
cp context.md.example context.md
# Edit .env with your QUESTDB_URL

# Build and run
npm run build
npm start
```

QuestLLens is now running at `http://localhost:3000` with the MCP endpoint at `/mcp`.

### Quick Start (Docker)

```bash
docker run -p 3000:3000 \
  -e QUESTDB_URL="postgresql://admin:quest@host:8812/qdb" \
  ghcr.io/dmdufresne/questllens:1.0.0
```

### Connect to Claude Desktop

Add QuestLLens to your Claude Desktop configuration:

```json
{
  "mcpServers": {
    "questllens": {
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

With OAuth enabled:

```json
{
  "mcpServers": {
    "questllens": {
      "url": "http://localhost:3000/mcp",
      "authorizationUrl": "http://localhost:3000/oauth/authorize",
      "tokenUrl": "http://localhost:3000/oauth/token",
      "registrationUrl": "http://localhost:3000/oauth/register"
    }
  }
}
```

### Connect to Claude Code

```json
{
  "mcpServers": {
    "questllens": {
      "type": "url",
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

---

## Skills

`skills/` bundles four [Claude skills](https://docs.claude.com/en/docs/claude-code/skills)
that teach Claude how to drive QuestLLens instead of guessing at tool names:

| Skill | Use for |
|-------|---------|
| `questllens-using` | The orienting skill — read-only posture, the four time-series concepts that change every query (designated timestamp, partitions, SYMBOL, WAL), discovery-first workflow, and routing to the other three. Start here. |
| `questllens-explore-a-database` | Getting oriented in an unfamiliar instance: inventory, meaning, time coverage, cardinality, partitions, MV graph. |
| `questllens-health-check` | Triage-ordered ingestion sweep: WAL lag vs staleness, suspended tables, storage, running queries. |
| `questllens-tune-a-query` | The `suggest_sample_by` → `explain_query` → `query` loop, partition pruning, and QuestDB-specific rewrites. |

Copy the four directories under `skills/` into your project's `.claude/skills/`
(or wherever your client loads skills from) to make them available; Claude Code
will surface the right one automatically based on the trigger phrases in each
skill's frontmatter.

## Tools

QuestLLens exposes **18 MCP tools** organized into six categories. Tools were designed for AI agents first — markdown output for token density, descriptions that explain *when* to use each one, and composite diagnostics that answer questions in one round-trip instead of three.

### Query

| Tool | Description |
|------|-------------|
| **`query`** | Execute read-only SQL SELECT queries. Results returned as markdown tables with row counts and truncation warnings. |
| **`explain_query`** | QuestDB execution plan for a SELECT. Use after a slow `query` — reveals SAMPLE BY / LATEST ON / ASOF JOIN behavior and chosen join algorithms. |
| **`suggest_sample_by`** | Recommend a SAMPLE BY interval given table, range, and target bucket count. Stops agents from picking `1m` on a year of data. |

### Schema Discovery

| Tool | Description |
|------|-------------|
| **`list_tables`** | Every table with designated timestamp, partition unit, WAL flag, dedup keys, and column count. Materialized views surface here too. |
| **`describe_table`** | One-stop description for a table or materialized view: columns, dedup keys, partition unit. Optional flags add time range (`with_time_range`) and per-symbol-column distinct counts (`with_symbol_stats`). |
| **`search_columns`** | Find columns by name pattern across every table. Case-insensitive substring match. |
| **`get_create_table`** | Round-trippable `CREATE TABLE` (or `CREATE MATERIALIZED VIEW`) DDL. Use when mirroring schema in code or diffing against a desired state. |
| **`get_table_params`** | Per-table ingestion knobs: `o3MaxLag`, `maxUncommittedRows`, `commitLag`, TTL, dedup state. Critical for ingestion troubleshooting. |
| **`refresh_schema`** | Manually force a schema-cache reload. Normally unnecessary — `describe_table` and friends auto-refresh on cache miss. |

### Data Exploration

| Tool | Description |
|------|-------------|
| **`get_sample_data`** | 1–20 sample rows. Pass `latest=true` for most-recent rows by designated timestamp; pass `columns` to project a subset on wide tables; pass `where` for a read-only filter. |
| **`get_table_stats`** | Row counts, null %, distinct counts per column — batched into one SQL. **Pass `sample_rows` on large tables** — a full scan can take minutes. |

### Storage & Partitions

| Tool | Description |
|------|-------------|
| **`get_partitions`** | Per-partition listing with parquet/active/read-only flags. Pass `from`/`to` to bound long-retention tables, or `summary=true` for a roll-up (counts, first/last, native vs parquet split). |
| **`get_storage_summary`** | Top-N tables by disk with parquet vs native split. Single call to find disk hotspots without fanning `get_partitions` per table. |

### Operations

| Tool | Description |
|------|-------------|
| **`get_wal_status`** | Per-table WAL apply state: sequencer txn, writer txn, lag, suspended flag. |
| **`get_ingestion_health`** | Composite ingestion diagnostic: WAL lag + suspended state + latest-timestamp staleness in one call. First stop for "why is data not arriving?". |
| **`get_running_queries`** | Currently executing queries via `query_activity()`. Optional `min_duration_ms` filter. Use when "the system feels slow." |
| **`get_mv_dependencies`** | Materialized view dependency graph with reverse index ("which views depend on table X?"). Drill into a single view with its SQL definition. Requires QuestDB 8.x. |

### Server

| Tool | Description |
|------|-------------|
| **`server_info`** | Version, build, edition, uptime, plus feature detection for `materialized_views()` and `query_activity()`. Call early in a session — it tells the agent which optional features exist without trial-and-error. |

---

## Configuration

QuestLLens is configured via environment variables. Create a `.env` file or pass them directly.

### Required

| Variable | Description | Example |
|----------|-------------|---------|
| `QUESTDB_URL` | QuestDB connection string (PostgreSQL wire protocol) | `postgresql://admin:quest@host:8812/qdb` |

> QuestDB's default PG-wire credentials are `admin` / `quest` on port `8812`. Change them, and create a read-only user — see [Security](#security).

### Optional

| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_PORT` | `3000` | HTTP server port |
| `QUERY_TIMEOUT_MS` | `30000` | Maximum query execution time (ms) |
| `MAX_ROWS` | `1000` | Maximum rows returned per query |
| `SCHEMA_REFRESH_INTERVAL_MS` | `300000` | Schema cache refresh interval (ms) |
| `DOMAIN_CONTEXT_FILE` | — | Path to a markdown file with business context |
| `DOMAIN_CONTEXT` | — | Inline domain context string (alternative to file) |

### OAuth Options (when running with `--oauth`)

| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_AUTH_PASSWORD` | — | Password for OAuth login form |
| `EXTERNAL_BASE_URL` | `http://localhost:3000` | Public URL (for running behind a proxy) |
| `MCP_ALLOWED_ORIGINS` | _empty — all origins allowed_ | Comma-separated CORS allowlist for browser origins (e.g. `https://claude.ai`). Empty or `*` allows any origin; tool routes still require a Bearer token when `--oauth` is on. Native MCP clients send `Origin: null` and are always allowed, so you never need to list them. |
| `MCP_OAUTH_TOKEN_EXPIRES_IN` | `604800` | Token lifetime in seconds (default: 7 days) |
| `MCP_RATE_LIMIT_ATTEMPTS` | `5` | Max login attempts per window |
| `MCP_RATE_LIMIT_WINDOW_MS` | `900000` | Rate limit window (ms, default: 15 min) |
| `TRUST_PROXY_HEADERS` | `false` | Derive the client IP from `X-Forwarded-For` so the rate limiter counts real clients instead of the proxy. Set `true` **only** when a proxy you control is the sole path to this server — otherwise the header is caller-supplied and spoofable. |

---

## Docker

### Pull

```bash
docker pull ghcr.io/dmdufresne/questllens:1.0.0
```

### Build

```bash
docker build -t questllens .
```

### Run

```bash
# Without OAuth (local development, trusted networks)
docker run -p 3000:3000 \
  -e QUESTDB_URL="postgresql://readonly:password@host:8812/qdb" \
  questllens

# With OAuth (production, Claude Desktop)
docker run -p 3000:3000 \
  -e QUESTDB_URL="postgresql://readonly:password@host:8812/qdb" \
  -e MCP_AUTH_PASSWORD="your-secure-password" \
  questllens node dist/index.js --oauth

# With custom domain context
docker run -p 3000:3000 \
  -e QUESTDB_URL="postgresql://readonly:password@host:8812/qdb" \
  -v ./my-context.md:/app/context.md \
  -e DOMAIN_CONTEXT_FILE="context.md" \
  questllens
```

### Docker Compose

```yaml
services:
  questllens:
    image: ghcr.io/dmdufresne/questllens:1.0.0
    ports:
      - "3000:3000"
    environment:
      QUESTDB_URL: postgresql://readonly:password@questdb:8812/qdb
      MAX_ROWS: 500
    volumes:
      - ./context.md:/app/context.md
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped
```

### Image Details

- **Base:** `node:20-alpine` (multi-stage build)
- **Size:** ~80MB
- **User:** Non-root (`nodejs:1001`)
- **Health check:** Built-in via `/health` endpoint

---

## Security

Vulnerability reporting, and what is and isn't in scope, are in
[SECURITY.md](SECURITY.md). The short version: the read-only guarantee and the OAuth
flow are in scope; anything reachable with legitimately granted read access is not —
use a least-privilege database role.

QuestLLens is read-only by design and uses defense-in-depth. The application layer is **not** sufficient on its own — a read-only database role and network isolation are **required**, not optional. The sections below describe each layer.

### Required: Read-Only Database Role

QuestDB does not honor PostgreSQL's `BEGIN READ ONLY`. The database is your only enforceable write barrier. Run QuestLLens with a read-only user:

**QuestDB Enterprise** (per-user RBAC):

```sql
CREATE USER questllens_readonly WITH PASSWORD 'your-secure-password';
GRANT SELECT ON ALL TABLES TO questllens_readonly;
```

**QuestDB Open Source** (no per-user RBAC yet):

OSS lacks per-user RBAC, so the application layer cannot fully isolate writes. Required mitigations:

1. **Change the default `admin`/`quest` credentials immediately.**
2. **Network-isolate** the PG-wire port (`8812`) so only QuestLLens can reach it. Do not expose it to operator workstations or other services.
3. Run QuestLLens behind OAuth (`--oauth`) so MCP clients are also gated at the application layer.

If you cannot meet (1) and (2), do not run QuestLLens against a production OSS instance.

### Application-Layer Read-Only Path (defense-in-depth)

Every user-supplied SQL statement passes through a real tokenizer (handles `'…'` with `''` escapes, `$tag$…$tag$`, `--`, `/* */`) and is checked against:

- **Allowlist of leading verb** — only `SELECT`, `WITH`, `EXPLAIN`, `SHOW`, or `TABLES` are accepted.
- **Multi-statement reject** — anything after a `;` is refused. The pg-wire simple-query path executes multiple statements; the safety check makes that impossible to reach.
- **Forbidden-keyword scan on tokenized input** — `INSERT` · `UPDATE` · `DELETE` · `DROP` · `CREATE` · `ALTER` · `TRUNCATE` · `RENAME` · `REINDEX` · `VACUUM` · `BACKUP` · `SNAPSHOT` · `COPY` · `ATTACH` · `DETACH` · `GRANT` · `REVOKE` · `SET` · `RESET` · `RESUME` · `SUSPEND` · `CHECKPOINT` · `CANCEL` · `KILL` · `SQUASH` · `CONVERT` · `DEDUP` · `REFRESH` · `CALL` · `EXECUTE` · `PREPARE` · `DEALLOCATE`. Because the input is tokenized, `WHERE message LIKE '%DROP%'` does not trip the scan.

Internal introspection queries (`tables()`, `wal_tables()`, `SHOW CREATE TABLE`, …) bypass the safety check via an explicit `internal: true` flag in the database client. Every internal call site is an audit point; user input never reaches that path.

### Statement Timeouts

Per-query `statement_timeout` is re-applied on every connection checkout (QuestDB has no `SET LOCAL`), so a previous internal call cannot leave a stale value on a pooled connection. Default 30 seconds.

### Row Limits

Results are capped at a configurable maximum (default: 1,000 rows) with a truncation warning.

### OAuth (when `--oauth` is enabled)

When running with `--oauth`, QuestLLens provides:

- **RFC 7591** Dynamic Client Registration
- **PKCE S256** — required when the client sends a `code_challenge`; verifier checked at the token endpoint with constant-time comparison
- **Authorization code binding** — the code is bound to its `client_id` and `redirect_uri`; mismatch on redemption is rejected
- **Redirect URI validation** — only registered URIs are accepted; scheme is restricted to `https` (or `http://localhost`/`127.0.0.1` for development)
- **CORS allowlist** — `MCP_ALLOWED_ORIGINS` (comma-separated) pins which *browser* origins may call the server. Left empty it allows any origin, which is safe here because every tool route requires a Bearer token rather than a cookie — a cross-origin page has no ambient credential to use. Set it when you want browser origins restricted
- **Rate limiting** on password attempts (5 per 15 minutes by default)
- **Timing-safe** password comparison
- **Bearer token** validation on all MCP endpoints; expired tokens are dropped from the in-memory store
- **Self-contained consent page** — the login page loads no third-party fonts, scripts, or assets, so an authentication prompt never leaks a request to a CDN
- **Security headers** on every response — `Content-Security-Policy: default-src 'none'` (inline styles only, `frame-ancestors 'none'`, `base-uri 'none'`), plus `X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`, and `Cross-Origin-Opener-Policy`. No `form-action`: the consent form's 302 goes to the client's registered `redirect_uri`, which for a native client is a loopback port — a different origin that browsers block under `form-action 'self'`. The redirect target is constrained server-side instead, against the client's registered URIs
- **1 MB request body cap** on both JSON and form-encoded bodies

> Tokens and authorization codes are kept in memory; they do not survive a server restart. Persist them externally if you need long-lived sessions across restarts.

> **Behind a proxy or tunnel:** set `TRUST_PROXY_HEADERS=true`, or the rate limiter sees every request as coming from the proxy's single address and one attacker's failed logins lock out every client. Only set it when that proxy is the sole route to the server.

---

## Domain Context

This is QuestLLens's secret weapon. While schema introspection tells the AI **what** your tables look like, domain context tells it **what they mean** — and, for time-series data, **how to query them well**.

### How It Works

Copy the template and describe your database's business logic, then point QuestLLens at it:

```bash
cp context.md.example context.md
```

`context.md` is gitignored — it is where your proprietary domain knowledge lives, so it never gets committed.

```bash
# Via environment variable
DOMAIN_CONTEXT_FILE=context.md

# Or inline
DOMAIN_CONTEXT="This database stores sensor telemetry from industrial PLCs. Use SAMPLE BY for downsampled queries; never SELECT * across more than 1 hour of raw data."
```

QuestLLens injects this context into tool descriptions, so your AI assistant understands your domain from the very first interaction.

### Example `context.md`

```markdown
# Industrial Telemetry Database

## Key Concepts
- Every table is partitioned by **DAY** with designated timestamp `ts`
- The `device_id` column is a SYMBOL — always filter on it before time ranges
- We use `LATEST ON ts PARTITION BY device_id` to get the most recent reading per device
- Hot data lives in the last 7 days; older partitions are detached to cold storage

## Common Queries
- 1-minute downsample: `SELECT ts, avg(value) FROM readings SAMPLE BY 1m`
- Latest per device: `SELECT * FROM readings LATEST ON ts PARTITION BY device_id`
- Aligned multi-sensor: `ASOF JOIN` on `ts`

## Gotchas
- The `value` column is in raw ADC counts, not engineering units — multiply by `scale` from `device_config`
- `ts` is always UTC; the device-local time is in `local_ts`
- Never run `SELECT *` on the `raw_packets` table — it's billions of rows
```

### What Gets Enriched

Domain context is woven into:

- The `query` tool description (so AI writes better SQL)
- The `get_partitions` and `describe_table --with_time_range` results (so AI understands data lifecycle)
- The `describe_table --with_symbol_stats` output (so AI respects cardinality constraints)
- Schema discovery responses (so AI asks better follow-up questions)

---

## API Reference

### Health Check

```
GET /health
```

Returns server status and version:

```json
{
  "status": "healthy",
  "server": "questllens",
  "version": "1.0.0"
}
```

### MCP Endpoint

```
POST /mcp          → JSON-RPC 2.0 request
GET  /mcp          → Server-Sent Events (SSE) stream
DELETE /mcp        → Session termination
```

All MCP communication uses [Streamable HTTP Transport](https://modelcontextprotocol.io/docs/concepts/transports#streamable-http) with session management via the `mcp-session-id` header.

### OAuth Endpoints (when `--oauth` enabled)

```
GET  /.well-known/oauth-protected-resource  → Resource metadata
GET  /.well-known/oauth-authorization-server → Server metadata
POST /oauth/register                         → Dynamic client registration
GET  /oauth/authorize                        → Login form
POST /oauth/authorize                        → Authenticate
POST /oauth/token                            → Token exchange
```

---

## Development

```bash
# Install dependencies
npm install

# Run in dev mode (hot reload)
npm run dev

# Run with OAuth in dev mode
npm run dev:oauth

# Type check
npm run typecheck

# Run tests (read-only SQL boundary, config validation, identifier quoting)
npm test

# Build for production
npm run build
```

### Project Structure

```
src/
├── index.ts                       # Entry point
├── config.ts                      # Environment config with Zod validation
├── server.ts                      # Express + MCP server, OAuth, session management
├── database/
│   ├── client.ts                  # PG-wire connection pool, query execution
│   ├── schema-loader.ts           # QuestDB introspection + cache (auto-refresh on miss)
│   └── sql-safety.ts              # Lexer + allowlist enforcing the read-only path
├── tools/
│   ├── index.ts                   # Executor re-exports
│   ├── _util.ts                   # Shared identifier quoting
│   ├── query.ts                   # Execute SELECT queries (markdown output)
│   ├── explain-query.ts           # QuestDB EXPLAIN
│   ├── suggest-sample-by.ts       # Pick a SAMPLE BY interval for a target bucket count
│   ├── list-tables.ts             # Tables with TS / partitioning / WAL flags
│   ├── describe-table.ts          # Table or MV detail (with optional time range / symbol stats)
│   ├── search-columns.ts          # Cross-table column search
│   ├── get-create-table.ts        # Round-trippable CREATE TABLE / CREATE MATERIALIZED VIEW
│   ├── get-table-params.ts        # Per-table ingestion knobs (o3MaxLag, maxUncommittedRows, ttl)
│   ├── refresh-schema.ts          # Manual cache reload (auto-refresh on miss is the default)
│   ├── get-partitions.ts          # Partition list with from/to filter and summary mode
│   ├── get-storage-summary.ts     # Top-N tables by disk (parquet vs native)
│   ├── get-sample-data.ts         # Sample rows with optional columns/where projection
│   ├── get-table-stats.ts         # Per-column null % + distinct (single batched SQL)
│   ├── get-wal-status.ts          # WAL apply state, lag, suspended tables
│   ├── get-ingestion-health.ts    # Composite WAL lag + latest-row staleness diagnostic
│   ├── get-running-queries.ts     # query_activity() wrapper
│   ├── get-mv-dependencies.ts     # Materialized view graph (forward + reverse)
│   └── server-info.ts             # Version, build, feature detection
├── descriptions/
│   ├── generator.ts               # Dynamic description builder
│   └── static.ts                  # Static description blocks
├── types/
│   └── index.ts                   # TypeScript interfaces
└── ...

tests/
├── sql-safety.test.ts             # Read-only boundary: verbs, literals, injection shapes
├── config.test.ts                 # Env parsing, limits, domain-context loading
└── identifiers.test.ts            # quoteIdent breakout attempts

skills/                            # Claude skills — copy into .claude/skills/
├── questllens-using/
├── questllens-explore-a-database/
├── questllens-health-check/
└── questllens-tune-a-query/
```

CI runs typecheck, tests, and the build on Node 20 and 22, then builds the image and
asserts the read-only boundary still holds against a live QuestDB container
(see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)).

---

## Use Cases

| Use Case | How QuestLLens Helps |
|----------|---------------------|
| **AI-powered time-series analysis** | Let Claude write `SAMPLE BY`, `LATEST ON`, and `ASOF JOIN` queries against your live data — safely in read-only mode. Use `suggest_sample_by` first so the agent picks a sane bucket size. |
| **Capacity planning** | Combine `get_storage_summary`, `get_partitions --summary`, and `describe_table --with_symbol_stats` to identify hot partitions, undersized symbol capacities, and disk-heavy tables in one pass. |
| **Onboarding to time-series** | Point an AI at QuestDB with domain context and let it explain "what does designated timestamp mean for this table?" or "why is this query slow?". `server_info` tells the agent which features are available. |
| **Query optimization** | Use `explain_query` plus `describe_table --with_symbol_stats` to surface missing indexes, low-capacity symbols, and inefficient time predicates. Use `get_running_queries` when "the system feels slow." |
| **Ingestion debugging** | `get_ingestion_health` is a single-call composite of WAL lag, suspended state, and latest-row staleness. Pair with `get_table_params` (o3MaxLag, maxUncommittedRows) to diagnose stuttering writes. |
| **Data retention auditing** | Use `get_partitions` (with `from`/`to` filters) and `describe_table --with_time_range` to confirm retention policies are working and that detached/parquet partitions match the expected schedule. |
| **Schema portability** | `get_create_table` returns round-trippable DDL — useful for mirroring schemas in code, diffing against desired state, or bootstrapping a sister environment. |

---

## Compatibility

QuestLLens works with any MCP-compatible client:

- **Claude Desktop** (with or without OAuth)
- **Claude Code** (CLI)
- **Cursor** / **Windsurf** / **VS Code** (via MCP extensions)
- **Custom MCP clients** (any client implementing the MCP specification)

And any QuestDB deployment:

- QuestDB **Open Source** 7.4+
- QuestDB **Enterprise** (recommended — enables per-user RBAC)
- QuestDB **Cloud**
- Self-managed Docker, Kubernetes, or bare-metal

> `get_mv_dependencies` and the materialized-view branch of `describe_table` / `get_create_table` require QuestDB 8.x. `get_running_queries` requires a QuestDB version that exposes `query_activity()`. Run `server_info` to see what the connected instance supports. All other tools are compatible with 7.4+.

---

## Troubleshooting

### "Connection refused" on port 8812

QuestLLens connects via the PostgreSQL wire protocol on port `8812`, not the HTTP API on `9000`. Make sure the PG-wire listener is enabled (`pg.enabled=true` in `server.conf`) and reachable.

### "Permission denied" on schema introspection

QuestLLens uses QuestDB's system functions (`tables()`, `table_columns()`, `wal_tables()`, `table_partitions()`, `materialized_views()`). On QuestDB OSS these are available to any authenticated user. On QuestDB Enterprise, ensure your role has been granted the necessary read privileges:

```sql
GRANT SELECT ON ALL TABLES TO questllens_readonly;
```

### `get_mv_dependencies` returns empty

Materialized views require QuestDB 8.x. If you're on 7.x, this tool will return an empty result with a notice — upgrade to 8.0+ to use MVs.

### `get_wal_status` shows "WAL not enabled"

WAL tables became the default in QuestDB 7.4. Tables created on older versions may still be non-WAL; they will appear in `list_tables` with `wal_enabled = false` and will not be included in `get_wal_status`.

### Schema changes not reflected

QuestLLens caches schema metadata. Either wait for the next refresh cycle (default: 5 minutes) or call `refresh_schema` to update the MCP cache immediately.

### OAuth login fails

Check `MCP_AUTH_PASSWORD` is set and the rate limiter hasn't kicked in (5 attempts per 15 minutes by default). Check server logs for details.

---

## License

Apache-2.0. Free to use, modify, and distribute with attribution; includes an explicit patent grant. See [LICENSE](LICENSE) for the terms and [NOTICE](NOTICE) for third-party dependency licenses, the QuestDB trademark disclaimer, and the Abelara brand-asset carve-out — the logos and brand artwork are **not** covered by Apache-2.0.

Provided "as is" with no warranty of any kind — use at your own risk.

<br><br>

<div align="center">

<a href="https://abelara.com">
  <img src="images/abelara-logo.svg" alt="Abelara" width="200">
</a>

**Built by [Abelara](https://abelara.com)**

QuestLLens is part of the Abelara toolkit for industrial AI and edge computing, alongside [PgLLens](https://github.com/DMDuFresne/pgllens) for PostgreSQL.

[Report Bug](https://github.com/DMDuFresne/questllens/issues) · [Request Feature](https://github.com/DMDuFresne/questllens/issues) · [Learn More](https://abelara.com)

</div>

Maintenance

ActivityMaintained
ResponsivenessNo issues