This is the abridged developer documentation for 911CPB-MCP
# 911CPB-MCP
> A hosted, read-only MCP server that lets Claude explore and query the CRM Postgres database.
Hosted on Cloudflare Runs as a Cloudflare Worker. MCP (Streamable HTTP) at `/mcp`, plus a REST API with an OpenAPI spec for agents without MCP. Read-only by design One statement per call, read-type statements only, inside a `READ ONLY` transaction, against the Aurora reader endpoint. Pooled via Hyperdrive Cloudflare Hyperdrive holds the credentials and pools connections to Aurora in `us-east-2`. OAuth or access key Sign in with OAuth 2.1 (claude.ai, ChatGPT) or send the access key as `Authorization: Bearer` / `X-API-Key`. Anything else gets a `401`. Works with any agent Cursor, VS Code, Windsurf, Gemini CLI, Codex, OpenAI, LangChain, custom GPTs. See [Connect any agent](/guides/any-agent/). Agent-readable docs are at [`/llms-full.txt`](/llms-full.txt). ## Architecture [Section titled “Architecture”](#architecture)
```plaintext
Any AI agent ──HTTPS + token──▶ Cloudflare Worker (911CPB-MCP) /mcp · /api/* · /openapi.json
│ checks token, validates SQL
▼
Hyperdrive (pooling, cache)
│ TLS (sslmode=require)
▼
Aurora Postgres reader endpoint · crmdb
```
# Connect any agent
> Use 911CPB-MCP from any MCP client, OpenAI or LangChain agents, custom GPTs, or plain HTTP.
911CPB-MCP works with any AI agent through one of two interfaces: | Interface | URL | Use when | | ------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | **MCP** (Streamable HTTP) | `https://mcp.911cpb.org/mcp` | The agent or IDE supports remote MCP servers | | **REST + OpenAPI** | `https://mcp.911cpb.org/api/*`, spec at [`/openapi.json`](https://mcp.911cpb.org/openapi.json) | The agent supports HTTP or OpenAPI tools but not MCP | Both expose the same three tools (`list_tables`, `describe_table`, `query`) with the same [query rules](/reference/query-rules/). ## Authentication [Section titled “Authentication”](#authentication) The MCP endpoint accepts two kinds of credentials: | Method | Works on | Use when | | ------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **OAuth 2.1** (sign in through a browser) | `/mcp` | The client has an “add connector” flow with no place to paste a header: claude.ai, Claude Desktop, ChatGPT, Cursor’s OAuth mode | | **Access key** in `Authorization: Bearer ` or `X-API-Key: ` | `/mcp` and `/api/*` | The client lets you set headers: IDEs, CLIs, SDKs, scripts, OpenAPI importers | The OpenAPI spec (`/openapi.json`) is public so agent platforms can import it. Every tool call needs a credential. ### How the OAuth sign-in works [Section titled “How the OAuth sign-in works”](#how-the-oauth-sign-in-works) 1. Add `https://mcp.911cpb.org/mcp` as a connector in the client. Leave client ID and secret blank; the client registers itself. 2. A browser window opens on the **Authorize** page, showing the client’s name and where it will redirect. 3. Paste the 911CPB-MCP access key and click **Approve**. 4. The client gets a 1-hour access token, which it refreshes automatically. ### claude.ai and Claude Desktop [Section titled “claude.ai and Claude Desktop”](#claudeai-and-claude-desktop) **Settings → Connectors → Add custom connector**, then enter the URL `https://mcp.911cpb.org/mcp`. Click **Connect** and approve on the Authorize page. ### ChatGPT [Section titled “ChatGPT”](#chatgpt) **Settings → Apps & Connectors → Create** (developer mode), set the MCP server URL to `https://mcp.911cpb.org/mcp` and authentication to **OAuth**, then approve on the Authorize page. ## MCP clients [Section titled “MCP clients”](#mcp-clients) Most MCP clients take the same JSON shape. Replace `` with your token. ### Cursor [Section titled “Cursor”](#cursor) `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
```json
{
"mcpServers": {
"postgres": {
"url": "https://mcp.911cpb.org/mcp",
"headers": { "Authorization": "Bearer " }
}
}
}
```
### VS Code (GitHub Copilot agent mode) [Section titled “VS Code (GitHub Copilot agent mode)”](#vs-code-github-copilot-agent-mode) `.vscode/mcp.json`. The `inputs` prompt keeps the token out of the file:
```json
{
"inputs": [{ "id": "pg-token", "type": "promptString", "description": "911CPB-MCP token", "password": true }],
"servers": {
"postgres": {
"type": "http",
"url": "https://mcp.911cpb.org/mcp",
"headers": { "Authorization": "Bearer ${input:pg-token}" }
}
}
}
```
### Windsurf [Section titled “Windsurf”](#windsurf) `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"postgres": {
"serverUrl": "https://mcp.911cpb.org/mcp",
"headers": { "Authorization": "Bearer " }
}
}
}
```
### Gemini CLI [Section titled “Gemini CLI”](#gemini-cli) `~/.gemini/settings.json`:
```json
{
"mcpServers": {
"postgres": {
"httpUrl": "https://mcp.911cpb.org/mcp",
"headers": { "Authorization": "Bearer " }
}
}
}
```
### OpenAI Codex CLI [Section titled “OpenAI Codex CLI”](#openai-codex-cli) `~/.codex/config.toml`:
```toml
[mcp_servers.postgres]
url = "https://mcp.911cpb.org/mcp"
bearer_token_env_var = "PG_MCP_TOKEN"
```
### Clients that only support stdio [Section titled “Clients that only support stdio”](#clients-that-only-support-stdio) Bridge with `mcp-remote`:
```json
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.911cpb.org/mcp", "--header", "Authorization:Bearer ${PG_MCP_TOKEN}"],
"env": { "PG_MCP_TOKEN": "" }
}
}
}
```
Client config formats change often, so check each client’s MCP docs if a field name is rejected. ## Agent SDKs and APIs [Section titled “Agent SDKs and APIs”](#agent-sdks-and-apis) ### OpenAI Responses API [Section titled “OpenAI Responses API”](#openai-responses-api)
```ts
import OpenAI from "openai";
const client = new OpenAI();
const response = await client.responses.create({
model: "gpt-5",
tools: [
{
type: "mcp",
server_label: "postgres",
server_url: "https://mcp.911cpb.org/mcp",
headers: { Authorization: `Bearer ${process.env.PG_MCP_TOKEN}` },
require_approval: "never",
},
],
input: "How many devices are there?",
});
```
### Claude API [Section titled “Claude API”](#claude-api) See [Use from a webapp](/guides/webapp/). ### Vercel AI SDK [Section titled “Vercel AI SDK”](#vercel-ai-sdk)
```ts
import { experimental_createMCPClient as createMCPClient } from "ai";
const mcp = await createMCPClient({
transport: {
type: "http",
url: "https://mcp.911cpb.org/mcp",
headers: { Authorization: `Bearer ${process.env.PG_MCP_TOKEN}` },
},
});
const tools = await mcp.tools(); // pass to generateText / streamText
```
### LangChain / LangGraph (Python) [Section titled “LangChain / LangGraph (Python)”](#langchain--langgraph-python)
```python
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"postgres": {
"transport": "streamable_http",
"url": "https://mcp.911cpb.org/mcp",
"headers": {"Authorization": f"Bearer {token}"},
}
})
tools = await client.get_tools()
```
## Agents without MCP [Section titled “Agents without MCP”](#agents-without-mcp) ### Custom GPTs and other OpenAPI importers [Section titled “Custom GPTs and other OpenAPI importers”](#custom-gpts-and-other-openapi-importers) 1. Import the schema from `https://mcp.911cpb.org/openapi.json`. 2. Set authentication to **API Key**, type **Bearer**, and paste the token. If the platform only supports a custom header, use `X-API-Key`. ### Plain function calling [Section titled “Plain function calling”](#plain-function-calling) Define three functions that POST JSON to the REST endpoints. See the [REST API reference](/reference/rest-api/). ## Docs for agents [Section titled “Docs for agents”](#docs-for-agents) This site publishes machine-readable docs following the [llms.txt](https://llmstxt.org) convention: * [`/llms.txt`](/llms.txt): index * [`/llms-full.txt`](/llms-full.txt): every page as one plain-text file * [`/llms-small.txt`](/llms-small.txt): condensed version Point an agent at `/llms-full.txt` to give it everything it needs to use 911CPB-MCP.
# Use from Claude Code
> Add 911CPB-MCP as a remote MCP server in Claude Code.
```bash
claude mcp add --transport http postgres https://mcp.911cpb.org/mcp \
--scope user \
--header "Authorization: Bearer $(cut -d= -f2 ~/projects/911cpb-mcp/.secrets)"
```
Then run `claude mcp list` to confirm it shows as connected, and `/mcp` inside a session to see the tools. Caution `--scope project` writes the token into `.mcp.json`. Don’t commit that file if you use it.
# Quickstart
> Make your first call to 911CPB-MCP.
## What you need [Section titled “What you need”](#what-you-need) * The endpoint: `https://mcp.911cpb.org/mcp` * The auth token, stored in `~/projects/911cpb-mcp/.secrets` as `MCP_AUTH_TOKEN=...` Caution The token gives read access to the whole CRM database. Keep it server-side. Never ship it to a browser or commit it. ## Check that the server is up [Section titled “Check that the server is up”](#check-that-the-server-is-up)
```bash
export MCP_AUTH_TOKEN=$(cut -d= -f2 ~/projects/911cpb-mcp/.secrets)
curl -s https://mcp.911cpb.org/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $MCP_AUTH_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
The response is a Server-Sent Events message listing `list_tables`, `describe_table` and `query`. ## Run a query [Section titled “Run a query”](#run-a-query)
```bash
curl -s https://mcp.911cpb.org/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $MCP_AUTH_TOKEN" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query","arguments":{"sql":"select count(*) from \"Note\""}}}'
```
## Next steps [Section titled “Next steps”](#next-steps) * [Use it from your webapp](/guides/webapp/) * [Use it from Claude Code](/guides/claude-code/)
# Use from a webapp
> Let Claude query the CRM database from your webapp via the Claude API MCP connector.
The Claude API can call remote MCP servers for you. Your backend sends a Messages request with `mcp_servers`, and Claude calls the 911CPB-MCP tools while answering. ## TypeScript (server-side) [Section titled “TypeScript (server-side)”](#typescript-server-side)
```ts
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.beta.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 2048,
betas: ["mcp-client-2025-04-04"],
mcp_servers: [
{
type: "url",
url: "https://mcp.911cpb.org/mcp",
name: "postgres",
authorization_token: process.env.MCP_AUTH_TOKEN,
},
],
messages: [{ role: "user", content: "How many devices were added this month?" }],
});
```
Check the [Claude API MCP connector docs](https://docs.claude.com/en/docs/agents-and-tools/mcp-connector) for the current beta header and model IDs. ## Rules for the webapp [Section titled “Rules for the webapp”](#rules-for-the-webapp) * Call the API from your **server** (API route, server action, Worker). Put `MCP_AUTH_TOKEN` in server env vars only. * Results go back to Claude as tool output. Avoid prompts that make Claude repeat sensitive rows to end users who shouldn’t see them. * Each `query` call returns at most 500 rows, so ask for aggregates rather than raw dumps. ## Calling the database without Claude [Section titled “Calling the database without Claude”](#calling-the-database-without-claude) If your webapp only needs fixed queries, skip MCP and query Postgres from a normal API route. MCP is for when Claude decides which SQL to run.
# Deploy & rotate secrets
> Build, deploy and rotate credentials.
All commands use the `cf` CLI from `~/projects/911cpb-mcp`, with `CLOUDFLARE_ACCOUNT_ID=edc1a364499054f067488201b7646f09` (ApplyLogics). ## Build and deploy [Section titled “Build and deploy”](#build-and-deploy) The Worker serves both the MCP server and these docs on `https://mcp.911cpb.org`. `npm run build` builds the Starlight site (`docs/dist`) and then the Worker, which ships the docs as static assets.
```bash
cd ~/projects/911cpb-mcp
npx tsc --noEmit # typecheck
npm run build # docs + Worker
cf deploy
```
Paths the docs don’t use (`/mcp`, `/api/*`, `/openapi.json`, `/authorize`, `/token`, `/register`, `/.well-known/*`) fall through to the Worker. ## Rotate the auth token [Section titled “Rotate the auth token”](#rotate-the-auth-token)
```bash
openssl rand -hex 32 # new token
cf workers secrets update MCP_AUTH_TOKEN # paste it when prompted
```
Then update `.secrets` and every client (webapp env var, Claude Code config). Old clients get `401` right away. ## Rotate the database password [Section titled “Rotate the database password”](#rotate-the-database-password) 1. Change the password in Aurora. 2. Update Hyperdrive with the new password. `cf cli search "update hyperdrive origin password"` finds the command; run `--help` on it for the flags. 3. Check it with any `query` call. No redeploy is needed, because the Worker reads the connection from the binding. ## Local development [Section titled “Local development”](#local-development)
```bash
cf dev
```
To run locally against a different database, set `dev.connectionString` on the `HYPERDRIVE` binding in `cloudflare.config.ts`. Don’t commit a real connection string.
# Security model
> The layers that keep 911CPB-MCP read-only and private.
| Layer | What it does | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bearer token | Requests without the exact token get `401`. The comparison runs in constant time. | | OAuth 2.1 | `/mcp` supports OAuth with PKCE (S256) and dynamic client registration. Approving a client requires the access key. Access tokens last 1 hour; refresh tokens rotate. Grants are stored in the `911cpb-mcp-oauth` KV namespace. | | Bearer token / API key | `X-API-Key: ` is accepted as an alternative header for the same token. | | Path check | Only `/mcp`, `/api/*` and the public `/openapi.json` are served; everything else returns `404`. | | Statement checks | One read-type statement per call; transaction control and `SET` are rejected ([details](/reference/query-rules/)). | | `READ ONLY` transaction | Postgres refuses writes, including writes hidden in CTEs. | | Aurora reader endpoint | Connects to a read replica, which can’t write to tables at all. | | Database permissions | The `skye` role lacks write grants on at least some tables. | | Timeout & row cap | 15 s per statement, 500 rows per response. | ## Known gaps [Section titled “Known gaps”](#known-gaps) * **Shared personal credentials.** Hyperdrive connects as `skye`. Move it to a dedicated role with `SELECT`-only grants (and no access to `AuditEvent`). * **Password exposure.** The database password was shared in plain text during setup. Rotate it, then [update Hyperdrive](/operations/deploy/#rotate-the-database-password). * **Public database.** The Aurora endpoint accepts connections from the internet. Restrict its security group to [Cloudflare IP ranges](https://www.cloudflare.com/ips/) if nothing else needs it. * **Single shared token.** Every key-based client uses the same token, and the same key approves OAuth clients. Rotate it if it leaks. Rotating it doesn’t revoke OAuth grants already issued; clear the `911cpb-mcp-oauth` KV namespace to sign out every OAuth client. * **Open client registration.** Anyone can register an OAuth client, but nobody can get a token without approving it with the access key.
# Troubleshooting
> Common errors and fixes.
| Symptom | Cause | Fix | | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------- | | `401 Unauthorized` | Missing or wrong token | Send `Authorization: Bearer ` with the current token | | `404 Not found` | Wrong path | Use `/mcp` for MCP or `/api/` for REST | | `405 Use POST` | REST call with GET | REST endpoints are POST-only | | `Unsupported Media Type` | Missing headers | Send `Content-Type: application/json` and `Accept: application/json, text/event-stream` | | `relation "device" does not exist` | Unquoted PascalCase name | Use `"Device"` | | `canceling statement due to statement timeout` | Query ran over 15 s | Add filters or `LIMIT`, or aggregate | | `truncated: true` | More than 500 rows | Aggregate or page with `LIMIT`/`OFFSET` | | `password authentication failed` | Database password rotated | [Update Hyperdrive](/operations/deploy/#rotate-the-database-password) | | Stale results | Hyperdrive cache | Wait about 60 s, or [disable caching](/reference/configuration/#caching) | | A valid query is rejected | Blocked keyword such as `set` or `lock` used as a name | Quote the identifier | | OAuth: “Wrong access key” on the Authorize page | Pasted the wrong key | Use the value of `MCP_AUTH_TOKEN` from `.secrets` | | OAuth: `invalid_target` / “resource parameter must name exactly one configured protected resource” | Client configured with a URL other than `/mcp` | Use `https://mcp.911cpb.org/mcp` exactly | | OAuth token gets `401` on `/api/*` | REST only accepts the access key | Use `X-API-Key` for REST |
# Configuration
> Bindings, secrets and constants.
The project lives in `~/projects/911cpb-mcp` and deploys as the Worker `911cpb-mcp` on the custom domain `mcp.911cpb.org`. The docs are in `docs/` and are served from the same Worker. Worker config is `cloudflare.config.ts` (the `cf` CLI format, not `wrangler.toml`). ## Bindings [Section titled “Bindings”](#bindings) | Name | Type | Value | | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------- | | `HYPERDRIVE` | Hyperdrive | `911cpb-mcp` (`3c36ffaf23d548219b7be25f348a830e`) → Aurora reader endpoint, database `crmdb`, `sslmode=require` | | `MCP_AUTH_TOKEN` | Secret | Access key: works as a bearer token / `X-API-Key`, and approves OAuth clients | | `OAUTH_KV` | KV | `911cpb-mcp-oauth` (`83cbd56e72ca4cd59078c63bfa7eaa1d`): OAuth clients, grants and tokens | The Worker uses the `nodejs_compat` compatibility flag (needed by the `postgres` driver). ## Constants in `src/index.ts` [Section titled “Constants in src/index.ts”](#constants-in-srcindexts) | Constant | Default | Purpose | | ----------------- | -------------------------------------------- | ------------------------------ | | `MAX_ROWS` | `500` | Rows returned per `query` call | | `EXCLUDED_TABLES` | `["AuditEvent"]` | Tables hidden from all tools | | `ALLOWED_START` | `select\|with\|explain\|show\|values\|table` | Allowed first keyword | The statement timeout (`15s`) is set in `readOnly()`. ## OAuth endpoints [Section titled “OAuth endpoints”](#oauth-endpoints) | Endpoint | Purpose | | ------------------------------------------- | -------------------------------------- | | `/.well-known/oauth-protected-resource/mcp` | Protected resource metadata (RFC 9728) | | `/.well-known/oauth-authorization-server` | Authorization server metadata | | `/register` | Dynamic client registration | | `/authorize` | Sign-in and approval page | | `/token` | Code and refresh-token exchange | The OAuth resource is exactly `https://mcp.911cpb.org/mcp`. OAuth sign-in only works on that domain; the old `workers.dev` address supports the access key only. Access tokens last 3600 seconds. ## Excluded tables [Section titled “Excluded tables”](#excluded-tables) `public."AuditEvent"` (about 400 GB) is excluded. It doesn’t show up in `list_tables`, and `describe_table` and `query` refuse it. To exclude more tables, add them to `EXCLUDED_TABLES` and [redeploy](/operations/deploy/). ## Caching [Section titled “Caching”](#caching) Hyperdrive caching is on. Identical read queries can return results up to about 60 seconds old. To turn it off, update the Hyperdrive config with caching disabled.
# Query rules
> What the query tool accepts and rejects.
The `query` tool checks every statement before it reaches Postgres. Comments and quoted text are ignored during the check, so `select 'a;b'` is fine. ## Allowed [Section titled “Allowed”](#allowed) * One statement per call * Starts with `SELECT`, `WITH`, `EXPLAIN`, `SHOW`, `VALUES` or `TABLE` ## Rejected [Section titled “Rejected”](#rejected) | Rule | Example | Error | | ------------------------------- | ---------------------------------- | -------------------------------------------------------- | | More than one statement | `select 1; select 2` | Only one statement per query is allowed | | Not a read statement | `delete from "Note"` | Only SELECT/WITH/EXPLAIN/SHOW queries are allowed | | Transaction control or settings | `... commit ...`, `... set ...` | Transaction control, SET and SELECT INTO are not allowed | | `SELECT INTO` | `select * into temp t from "Note"` | (same as above) | | Excluded table named | `select * from "AuditEvent"` | Queries on AuditEvent are not allowed | The keywords `commit`, `rollback`, `begin`, `set`, `reset`, `discard`, `lock` and `into` are blocked anywhere outside quotes. A query that uses a column named `set` or `lock` must quote it (`"set"`). ## Enforced by Postgres [Section titled “Enforced by Postgres”](#enforced-by-postgres) Even if a statement passes the checks, it runs in a `READ ONLY` transaction, so data-modifying CTEs such as `with d as (delete ...) select ...` still fail. ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------- | -------------------- | | Statement timeout | 15 seconds | | Rows returned | 500 (rest truncated) |
# REST API
> HTTP endpoints for agents that don't speak MCP.
Base URL: `https://mcp.911cpb.org` OpenAPI 3.1 spec: [`/openapi.json`](https://mcp.911cpb.org/openapi.json) (public) Every endpoint is a `POST` with a JSON body and needs `Authorization: Bearer ` or `X-API-Key: `. ## `POST /api/list_tables` [Section titled “POST /api/list_tables”](#post-apilist_tables) Body: none, or `{}`
```bash
curl -X POST https://mcp.911cpb.org/api/list_tables -H "X-API-Key: $PG_MCP_TOKEN"
```
Returns `[{ "table_schema", "table_name", "table_type" }]`. ## `POST /api/describe_table` [Section titled “POST /api/describe_table”](#post-apidescribe_table)
```json
{ "table": "Device", "schema": "public" }
```
Returns `[{ "column_name", "data_type", "is_nullable", "column_default" }]`. ## `POST /api/query` [Section titled “POST /api/query”](#post-apiquery)
```json
{ "sql": "select count(*) as n from \"Device\"" }
```
Returns:
```json
{ "rowCount": 1, "rows": [{ "n": "92313" }], "truncated": false }
```
## Status codes [Section titled “Status codes”](#status-codes) | Code | Meaning | Body | | ----- | ------------------------------------------------------------------------------------------------- | -------------------------- | | `200` | Success | Result JSON | | `400` | Body isn’t JSON, or a required field is missing | `{ "error": "..." }` | | `401` | Missing or wrong token | `Unauthorized` | | `404` | Unknown path | `{ "error": "Not found" }` | | `405` | Not a POST | `{ "error": "Use POST" }` | | `422` | Query rejected by the [query rules](/reference/query-rules/), or Postgres error (syntax, timeout) | `{ "error": "..." }` |
# Tools
> The three tools 911CPB-MCP exposes.
All tools return JSON as a text content block. Failures come back with `isError: true` and the error message as text. ## `list_tables` [Section titled “list_tables”](#list_tables) Lists tables and views in every user schema (skips `pg_catalog`, `information_schema`, and [excluded tables](/reference/configuration/#excluded-tables)). **Arguments:** none **Returns:** `[{ table_schema, table_name, table_type }]` ## `describe_table` [Section titled “describe_table”](#describe_table) Shows the columns of one table. | Argument | Type | Default | Notes | | -------- | ------ | -------- | --------------------------------------------------- | | `table` | string | required | Exact name, case-sensitive (`Device`, not `device`) | | `schema` | string | `public` | | **Returns:** `[{ column_name, data_type, is_nullable, column_default }]`. An empty array means the table wasn’t found. ## `query` [Section titled “query”](#query) Runs one read-only SQL statement. See [Query rules](/reference/query-rules/) for what’s allowed. | Argument | Type | Notes | | -------- | ------ | -------------------------------------- | | `sql` | string | One statement. A trailing `;` is fine. | **Returns:**
```json
{ "rowCount": 2000, "rows": [ ... up to 500 ... ], "truncated": true }
```
`rowCount` is the total the database returned; `rows` is capped at 500.