QuestLLens
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@QuestLLenscheck WAL lag and partition health on the trades table"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
Getting Started · Tools · Configuration · Docker · Security · Domain Context
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), 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 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 BYpatterns, 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.
Related MCP server: django-mcp-sql
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
SELECTprivileges (read-only recommended; see Security)
Quick Start (npm)
# 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 startQuestLLens is now running at http://localhost:3000 with the MCP endpoint at /mcp.
Quick Start (Docker)
docker run -p 3000:3000 \
-e QUESTDB_URL="postgresql://admin:quest@host:8812/qdb" \
ghcr.io/dmdufresne/questllens:1.0.0Connect to Claude Desktop
Add QuestLLens to your Claude Desktop configuration:
{
"mcpServers": {
"questllens": {
"url": "http://localhost:3000/mcp"
}
}
}With OAuth enabled:
{
"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
{
"mcpServers": {
"questllens": {
"type": "url",
"url": "http://localhost:3000/mcp"
}
}
}Skills
skills/ bundles four Claude skills
that teach Claude how to drive QuestLLens instead of guessing at tool names:
Skill | Use for |
| 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. |
| Getting oriented in an unfamiliar instance: inventory, meaning, time coverage, cardinality, partitions, MV graph. |
| Triage-ordered ingestion sweep: WAL lag vs staleness, suspended tables, storage, running queries. |
| The |
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 |
| Execute read-only SQL SELECT queries. Results returned as markdown tables with row counts and truncation warnings. |
| QuestDB execution plan for a SELECT. Use after a slow |
| Recommend a SAMPLE BY interval given table, range, and target bucket count. Stops agents from picking |
Schema Discovery
Tool | Description |
| Every table with designated timestamp, partition unit, WAL flag, dedup keys, and column count. Materialized views surface here too. |
| One-stop description for a table or materialized view: columns, dedup keys, partition unit. Optional flags add time range ( |
| Find columns by name pattern across every table. Case-insensitive substring match. |
| Round-trippable |
| Per-table ingestion knobs: |
| Manually force a schema-cache reload. Normally unnecessary — |
Data Exploration
Tool | Description |
| 1–20 sample rows. Pass |
| Row counts, null %, distinct counts per column — batched into one SQL. Pass |
Storage & Partitions
Tool | Description |
| Per-partition listing with parquet/active/read-only flags. Pass |
| Top-N tables by disk with parquet vs native split. Single call to find disk hotspots without fanning |
Operations
Tool | Description |
| Per-table WAL apply state: sequencer txn, writer txn, lag, suspended flag. |
| Composite ingestion diagnostic: WAL lag + suspended state + latest-timestamp staleness in one call. First stop for "why is data not arriving?". |
| Currently executing queries via |
| 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 |
| Version, build, edition, uptime, plus feature detection for |
Configuration
QuestLLens is configured via environment variables. Create a .env file or pass them directly.
Required
Variable | Description | Example |
| QuestDB connection string (PostgreSQL wire protocol) |
|
QuestDB's default PG-wire credentials are
admin/queston port8812. Change them, and create a read-only user — see Security.
Optional
Variable | Default | Description |
|
| HTTP server port |
|
| Maximum query execution time (ms) |
|
| Maximum rows returned per query |
|
| Schema cache refresh interval (ms) |
| — | Path to a markdown file with business context |
| — | Inline domain context string (alternative to file) |
OAuth Options (when running with --oauth)
Variable | Default | Description |
| — | Password for OAuth login form |
|
| Public URL (for running behind a proxy) |
| empty — all origins allowed | Comma-separated CORS allowlist for browser origins (e.g. |
|
| Token lifetime in seconds (default: 7 days) |
|
| Max login attempts per window |
|
| Rate limit window (ms, default: 15 min) |
|
| Derive the client IP from |
Docker
Pull
docker pull ghcr.io/dmdufresne/questllens:1.0.0Build
docker build -t questllens .Run
# 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" \
questllensDocker Compose
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-stoppedImage Details
Base:
node:20-alpine(multi-stage build)Size: ~80MB
User: Non-root (
nodejs:1001)Health check: Built-in via
/healthendpoint
Security
Vulnerability reporting, and what is and isn't in scope, are in 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):
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:
Change the default
admin/questcredentials immediately.Network-isolate the PG-wire port (
8812) so only QuestLLens can reach it. Do not expose it to operator workstations or other services.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, orTABLESare 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 comparisonAuthorization code binding — the code is bound to its
client_idandredirect_uri; mismatch on redemption is rejectedRedirect URI validation — only registered URIs are accepted; scheme is restricted to
https(orhttp://localhost/127.0.0.1for 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 restrictedRate 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'), plusX-Content-Type-Options,X-Frame-Options: DENY,Referrer-Policy: no-referrer, andCross-Origin-Opener-Policy. Noform-action: the consent form's 302 goes to the client's registeredredirect_uri, which for a native client is a loopback port — a different origin that browsers block underform-action 'self'. The redirect target is constrained server-side instead, against the client's registered URIs1 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:
cp context.md.example context.mdcontext.md is gitignored — it is where your proprietary domain knowledge lives, so it never gets committed.
# 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
# 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 rowsWhat Gets Enriched
Domain context is woven into:
The
querytool description (so AI writes better SQL)The
get_partitionsanddescribe_table --with_time_rangeresults (so AI understands data lifecycle)The
describe_table --with_symbol_statsoutput (so AI respects cardinality constraints)Schema discovery responses (so AI asks better follow-up questions)
API Reference
Health Check
GET /healthReturns server status and version:
{
"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 terminationAll MCP communication uses Streamable HTTP Transport 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 exchangeDevelopment
# 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 buildProject 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).
Use Cases
Use Case | How QuestLLens Helps |
AI-powered time-series analysis | Let Claude write |
Capacity planning | Combine |
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?". |
Query optimization | Use |
Ingestion debugging |
|
Data retention auditing | Use |
Schema portability |
|
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_dependenciesand the materialized-view branch ofdescribe_table/get_create_tablerequire QuestDB 8.x.get_running_queriesrequires a QuestDB version that exposesquery_activity(). Runserver_infoto 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:
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 for the terms and 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.
Built by Abelara
QuestLLens is part of the Abelara toolkit for industrial AI and edge computing, alongside PgLLens for PostgreSQL.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to securely query VAST Data databases for schema, metadata, and sample data via read-only SQL and MCP resources.MIT
- AlicenseNot gradedqualityAmaintenanceProvides a read-only PostgreSQL SQL surface for LLM agents via MCP, with defense-in-depth security layers for safe database queries.3MIT
- AlicenseNot gradedqualityBmaintenanceProvides read-only access to databases for MCP-compatible AI tools, allowing schema exploration and SELECT queries without exposing credentials or risking data changes.923MIT
- AlicenseNot gradedqualityBmaintenanceProvides read-only access to PostgreSQL databases via MCP, enforcing least-privilege roles, row-level security, masked views, and SQL AST guardrails to prevent data leakage and unauthorized operations, enabling AI agents to safely query sensitive production data.MIT
Related MCP Connectors
Read-only MCP access to sessions, funnels, campaigns, errors, live visitors, and anomalies.
Analytical memory for AI agents: a real Postgres queried in plain English over MCP. One command.
Read-only Dant3 MCP for public rooms, agents, jobs and provisional machine onboarding.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/DMDuFresne/questllens'
If you have feedback or need assistance with the MCP directory API, please join our Discord server