PlainQuery
Ask your database questions in plain English. PlainQuery connects PostgreSQL databases to Claude, Cursor, or VS Code — and answers with real, structured data instead of guesses.
  !Python
<p align="center"> <img src="docs/PlainQuery_GIF.gif" alt="PlainQuery demo — querying a database in plain English from an MCP client" width="800"> </p>
---
What it does
You connect a database once. Then, from any MCP client, you ask questions like:
_"How many orders did we ship in March?"_ _"Which 5 customers spent the most last year?"_ _"What's the average order value by month?"_
PlainQuery introspects your schema, generates SQL with an LLM, validates it for safety, runs it, and returns structured JSON. If a query fails, it reads the error and retries automatically.
Why PlainQuery
- Natural language, real answers — no SQL required; results come from your actual data, not a hallucination.
- Read-only and safe by design — every query is checked before it runs: writes (
INSERT/UPDATE/DELETE/DDL) are blocked, dangerous functions and patterns are scanned out, and aLIMITis injected automatically. - Self-correcting — when a generated query errors, the agent feeds the error back to the LLM and retries (up to a configurable limit).
- Schema-aware — automatically introspects and caches your schema, so questions map to the right tables and columns.
- Works with any MCP client — Claude Desktop, Cursor, VS Code Copilot, or anything that speaks MCP.
- Secure multi-tenant hosting — database URLs and LLM keys are Fernet-encrypted at rest; user-supplied connection strings are SSRF-guarded; every request is tenant-scoped.
- Hosted or self-hosted — use the managed service, or run the whole stack yourself with Docker.
- Bring your own LLM — Anthropic Claude or Groq.
How it works
Your question
→ Schema introspection (reads tables/columns, cached)
→ SQL generation (schema + question → LLM → SQL)
→ Safety validation (blocks writes, scans dangerous patterns, injects LIMIT)
→ Execution (runs read-only, with a timeout)
→ Self-correction retry (on error: feed it back to the LLM, fix, re-run)
→ Structured JSON result
Architecture
<p align="center"> <img src="docs/diagram/plainquery-architecture.png" alt="PlainQuery architecture diagram showing clients, Next.js frontend, Starlette/FastAPI backend, FastMCP endpoint, query pipeline, LLM provider, and customer database" width="900"> </p>
---
Quick start
Option 1 — Use the hosted service (no install)
- Sign up at
https://plainquery.inand verify your email. - Connect your PostgreSQL database and create an API key.
- Add the server to your MCP client (see Connect your MCP client).
That's it — start asking questions.
Option 2 — Run it yourself
See Self-hosting & local development below.
Connect your MCP client
PlainQuery is published on the official MCP Registry as io.github.Jarvis-27/mcp-db-agent, so registry-aware clients can discover it directly.
To configure a client manually, point it at the MCP endpoint and pass your API key:
{
"mcpServers": {
"plainquery": {
"url": "https://plainquery.in/mcp",
"headers": { "X-API-Key": "mdbk_your_key_here" }
}
}
}
The backend can also generate ready-to-paste config for VS Code, Cursor, and generic HTTP clients — call POST /api/v1/account/setup-payloads (see Setup payloads). MCP clients can authenticate with an OAuth 2.1 bearer token or an API key, depending on MCP_AUTH_MODE.
---
Self-hosting & local development
Prerequisites
- Python 3.12+
uv- Node.js 20+ and
pnpmfor the frontend - At least one LLM API key (
ANTHROPIC_API_KEYorGROQ_API_KEY)
Backend
uv sync
cp .env.example .env
Edit .env with at least:
CREDENTIAL_ENCRYPTION_KEYSREGISTRATION_OPEN=true- one LLM provider key plus
LLM_PROVIDER
Run the backend:
uv run uvicorn src.app:app --reload --host 0.0.0.0 --port 8000
The backend mounts:
- REST API at
http://localhost:8000/api - MCP endpoint at
http://localhost:8000/mcp
Frontend
cd frontend
pnpm install
pnpm dev
Open http://localhost:3000, sign up, complete setup, link your OAuth identity via account settings, then connect an MCP client at http://localhost:8000/mcp. The client will complete the OAuth flow automatically, or you can use an API key in api_key_only / hybrid mode.
Runtime model
- Backend API: FastAPI/Starlette at
src.app:app - Frontend: Next.js app in
frontend/ - Auth: passwordless email verification and login links
- MCP auth: OAuth 2.1 bearer tokens (
oauth_only), API keys (api_key_only), or both (hybrid) — set viaMCP_AUTH_MODE - Setup payloads:
POST /api/v1/account/setup-payloads - Billing: Stripe Checkout, Customer Portal, and webhook-confirmed Free/Pro entitlements
The product model is single-account and user-scoped: signup → verify email → connect database → link OAuth identity → use /mcp → upgrade with Stripe
Security
<p align="center"> <img src="docs/diagram/plainquery-security.png" alt="PlainQuery security diagram showing URL validation, SSRF protection, encrypted credential storage, MCP authentication, per-request isolation, SQL validation, rate limits, and controlled execution" width="900"> </p>
- User-supplied database URLs are validated against SSRF, path traversal, private IPs, and DNS rebinding before any connection attempt.
- Database URLs and LLM keys are Fernet-encrypted at rest, with support for key rotation.
- Generated SQL is validated before execution: single-statement guard, forbidden-function scan, dangerous-pattern scan, write/DDL block, table-existence check, and automatic
LIMITinjection. - Per-request context scoping prevents cross-tenant data leaks.
- Per-user rate limits and fallback-LLM quotas limit cost abuse.
Important environment variables
| Variable | Description | |---|---| | AUTH_DATABASE_URL | Auth/account database used by the hosted product | | CREDENTIAL_ENCRYPTION_KEYS | Encrypts stored database URLs and other secrets | | REGISTRATION_OPEN | Enables or disables public signup | | ANTHROPIC_API_KEY / GROQ_API_KEY | LLM credentials for SQL generation | | LLM_PROVIDER | Active provider name | | APP_BASE_URL | Base URL used in setup payloads | | FRONTEND_BASE_URL | Base URL used in email links | | STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET | Stripe API and webhook credentials | | STRIPE_PRO_PRICE_ID | Stripe Price ID that maps to the Pro plan | | STRIPE_CHECKOUT_SUCCESS_URL / STRIPE_CHECKOUT_CANCEL_URL | Optional Checkout redirect overrides | | STRIPE_CUSTOMER_PORTAL_RETURN_URL | Optional Customer Portal return URL override |
See .env.example for the current full set.
API surface
Auth
POST /api/v1/auth/signupGET /api/v1/auth/verify-emailPOST /api/v1/auth/request-login-linkGET /api/v1/auth/exchange-login-linkPOST /api/v1/auth/logout
Account (session-authenticated)
All account routes use session token auth (x-session-token: <session-token> or Authorization: Bearer <session-token>):
GET /api/v1/accountGET /api/v1/account/statusPUT /api/v1/account/databaseGET /api/v1/account/api-keysPOST /api/v1/account/api-keysDELETE /api/v1/account/api-keys/{id}POST /api/v1/account/api-keys/{id}/rotatePOST /api/v1/account/setup-payloadsGET /api/v1/account/dashboardGET /api/v1/account/usage/recent
Billing
GET /api/v1/account/billingPOST /api/v1/account/billing/checkout-sessionPOST /api/v1/account/billing/portal-sessionPOST /api/v1/billing/webhook
Stripe webhooks are the source of truth for plan transitions. Checkout or subscription activation moves a user to plan_code=pro; canceled, unpaid, or past-due states restrict paid entitlements without deleting database setup.
OAuth MCP account linking (session-authenticated)
GET /api/v1/account/mcp-oauth/statusPOST /api/v1/account/mcp-oauth/startGET /api/v1/account/mcp-oauth/callbackDELETE /api/v1/account/mcp-oauth/link
MCP
POST /mcp- Auth: OAuth 2.1 bearer token, API key, or both — controlled by
MCP_AUTH_MODE
Setup payloads
POST /api/v1/account/setup-payloads returns client configuration material for VS Code, Cursor, generic HTTP MCP clients, and the current ChatGPT placeholder.
curl -X POST http://localhost:8000/api/v1/account/setup-payloads \
-H "Authorization: Bearer <session-token>" \
-H "Content-Type: application/json" \
-d '{"raw_api_key":"mdbk_..."}'
The backend never stores raw API keys after creation. A raw key is only embedded in setup payloads when you explicitly send it in the request.
Tests
uv run pytest tests/ -m "not integration"
uv run pytest tests/ -m integration
uv run ruff check .
uv run mypy src --ignore-missing-imports
Deployment smoke test
After each hosted deploy, verify the public MCP OAuth discovery surface:
uv run python scripts/smoke_mcp_deployment.py \
--mcp-url https://mcp.example.com/mcp \
--issuer-url https://YOUR_DOMAIN.auth0.com/
The smoke test checks the unauthenticated /mcp challenge, protected resource metadata, ChatGPT/Claude well-known discovery routes, and issuer metadata. To also verify authenticated MCP initialize, tools/list, and resources/list, pass a linked user's access token with --access-token or MCP_SMOKE_ACCESS_TOKEN.
Docker
Run the hosted HTTP stack:
docker compose up --build
The container image serves only the hosted HTTP runtime. The MCP endpoint remains http://localhost:8000/mcp.
License
MIT © 2026 PlainQuery











