MCP Server
Kvery ships a built-in MCP (Model Context Protocol) server, so AI assistants that speak MCP can discover and run your queries as tools — letting an assistant answer questions straight from your databases through Kvery.
What MCP exposes
Through the MCP server an assistant can:
- List queries available to it (
kvery_list_queries). - List groups (
kvery_list_groups). - Search queries by description, internal note or SQL (
kvery_search; SQL only for queries you can edit, and not with a token that hides the SQL). - Get a query's details and parameters (
kvery_get_query). - Execute a query that reads data (with parameters) and read the result
(
kvery_execute_query). A query that modifies data is refused with a message that points tokvery_execute_write_query. - Execute any query, including one that modifies data
(
kvery_execute_write_query) — not offered to read-only tokens.
Every tool except kvery_execute_write_query is marked as read-only for MCP
clients, so a client can let reads run without asking and ask for confirmation
before a query that modifies data. Lists, search results and query details tell
whether a query modifies data (isWrite). The SQL, the connection name, the
average runtime and the time of the last run are only shown for queries you can
edit, as in the web app — and a token can hide the SQL even then (see
Hiding the SQL).
Access is scoped by the token you issue (see below) — an assistant only ever sees what its token is allowed to see. Basic Auth with your e-mail address and password is also accepted, but prefer tokens: Basic Auth has the full access of your account, cannot be limited with scope tags, made read-only or set to hide the SQL, sends your password with every request and does not ask for the two-factor code.
Connecting
The server offers the MCP protocol and a REST API on the same address:
| Transport | Endpoint | Use |
|---|---|---|
| MCP (Streamable HTTP) | POST /mcp | JSON-RPC 2.0 calls from MCP clients, answered with plain JSON |
| REST | GET /mcp/queries, /mcp/groups, /mcp/search, GET /mcp/queries/{id}, POST /mcp/queries/{id}/execute | direct integration |
GET /mcp only announces the POST /mcp endpoint (in an endpoint event) and
then closes the stream. MCP clients that can only use the HTTP+SSE transport of
MCP protocol version 2024-11-05, which expects the answers on that event stream,
are not supported.
Point your MCP-capable client at the /mcp endpoint and authenticate with a Kvery
MCP token. The token dialog in Kvery gives you a ready-to-paste client
configuration:
{
"mcpServers": {
"kvery": {
"url": "https://app.kvery.io/mcp",
"headers": {
"Authorization": "Bearer kvery_mcp_..."
}
}
}
}
The same token works on the REST endpoints, for scripts, CI pipelines or any HTTP client — see Using the REST API.
MCP tokens
You manage access with MCP tokens, which you create and revoke from your profile menu → MCP / API Keys:
- Every token is prefixed with
kvery_mcp_, so it's easy to recognise. It is shown only once, when you create it. - Tokens expire 90 days after they are created. Expired tokens stay in the list, marked Expired, and are deleted automatically 30 days later.
- Each token can carry optional scope tags (it then only reaches queries with
at least one of those tags), so you can hand different assistants different,
least-privilege access. Only tags you may use can be chosen, and tags starting
with
_cannot be used. - A token can be read-only and can hide the SQL of your queries (see below).
- Revoke a token at any time to immediately cut off access.
- A token name can be up to 100 characters long, a token can have up to 100 scope tags, and you can create up to 5 tokens per hour.
- The token list is paged and searchable, and can be exported to CSV or Excel (with times in your local time; the number of queries a token reaches is the last column).
- The list shows how many queries each token reaches (your access when the list is loaded, narrowed by the token's scope tags). Click the number to see these queries with their description and group, execute a query in a new browser tab, or edit it if you can. A query of another group opens for editing in the same window, because the editor switches to the query's group. A token that reaches no query shows a highlighted 0; an expired token reaches none and shows a dash.
- Changing or resetting your password revokes all of your tokens — create new ones afterwards. Your profile warns you before the change and tells you if the tokens could not be revoked.
An MCP token grants an assistant the ability to run your queries. Store it like a password, scope it tightly, and revoke tokens you don't use.
Read-only tokens
Tick Read-only when you create a token, and the token can only run queries that read data:
- MCP clients using it don't get
kvery_execute_write_query, so they can only usekvery_execute_query, which runs queries that read data. - On the REST API, executing a query that modifies data answers
403. - Listing, searching and query details work as usual.
Read-only is enforced at the database, not only by checking the SQL text. Every
read-only run (kvery_execute_query with any token, and any run with a read-only
token) executes inside a transaction that is always rolled back:
- On MySQL / MariaDB and PostgreSQL the transaction is
READ ONLY, so the database itself rejects any data change — including one made by a function or procedure called from aSELECT— and nothing is ever committed. A schema change, which MySQL / MariaDB commit implicitly, is stopped by the SQL text check: it counts as a write, so a read-only run never sends it. - On SQL Server (no read-only transaction mode) a normal transaction is used and
the rollback discards anything the query changed. A
COMMITorROLLBACKin the query's own SQL ends this transaction early, so whatever runs after it is only covered by the SQL text check. - Agent connections can't be enforced this way, so a read-only run of an
agent-connection query is refused; use a full-access (non read-only) token with
kvery_execute_write_queryfor those. - A value inserted into a statement while the query runs (a query variable,
:name = …) can't add another statement to it: a read-only run refuses such a statement.
So on MySQL / MariaDB and PostgreSQL a read-only token can't change the data of your database's tables even if the SQL text check misjudges a query.
Hiding the SQL
The SQL of a query is only shown to people who can edit it. Tick Hide SQL when you create a token, and the token doesn't reveal the SQL even when you are an editor — as far as the SQL is concerned, it behaves as if you couldn't edit the queries:
- Query details (
kvery_get_query) contain no SQL, and search (kvery_search) doesn't look into the SQL. - When a query is refused because it modifies data, only the kind of cause is named, never the keyword that triggered it.
- When a query fails, the generic error message is returned instead of the
database's error text, also on agent connections (a message the query defines with
ON ERROR RETURNis still returned, and for database errors the full error stays in the query log).
Everything else works as usual: the token reaches and runs the same queries, and still sees whether a query modifies data, its parameters, connection name and run statistics.
Read and write queries
A query counts as reading only if every statement in it is a SELECT, WITH,
SHOW, EXPLAIN, DESCRIBE / DESC, VALUES or TABLE statement (or starts with
an opening parenthesis) without any data or schema changing command; anything Kvery
can't classify safely counts as a write.
A query with a webhook directive (such as
ON SUCCESS POST '…' TO '…') or an inline HTTP fetch directive (GET('…'),
POST('…'), PUT('…'), PATCH('…'), RPOST('…'), RPUT('…'), RPATCH('…'),
RDELETE('…')) also counts as a write, because it makes an outbound HTTP request
when the query runs. So does a construct whose effect a rollback can't undo:
sequence steps (NEXT VALUE FOR, nextval(), setval()), statements sent over
another connection or to another server (PostgreSQL dblink…(), SQL Server
OPENQUERY(), OPENROWSET(), OPENDATASOURCE()), server-side files (lo_export(),
lo_import()) and ending other sessions (pg_terminate_backend(),
pg_cancel_backend()). The check uses the SQL that actually runs (the published
version, snippets, form-field SQL and the parameter values).
Database functions called from a SELECT are not inspected by the SQL text check,
but a read-only run still can't change your tables: it runs in an always-rolled-back
transaction, so on MySQL / MariaDB and PostgreSQL the database rejects the change
and on SQL Server it is discarded. This does not apply to agent connections
(a read-only run of one is refused). A function or extension of your database that
changes something outside the transaction (for example on another server or in a
file) can't be recognised from the SQL, so for complete protection connect through
a database user that only has read permissions.
Running queries
- Parameters are passed by name —
kvery_get_querylists them with their field type and label (the field's display name). A display name that is SQL or contains{% %}/[% %]blocks or a$variableis never shown: the label is then the parameter name. A value can be a string, number, boolean or a list (for multi-select fields). - Results come back as the query ID plus column names and rows:
{"queryId": "…", "result": {"columns": ["id", "name"], "data": [["1", "Acme Ltd"], ...]}}. - If the query fails (for example, an SQL error), the tool reports an error instead
of an empty result. If you can edit the query, the message is the database's
error text (also the one an agent connection relays), as in the editor (not with
a token that hides the SQL). Otherwise it is a generic message,
unless the query sets its own message with
ON ERROR RETURN, which everyone gets. - Runs are recorded in the query's logs with their real result, so log levels that record only errors or only successes, and webhooks, work as for runs in the app.
- The MCP server and its REST API always run the published version of a query: an unpublished draft doesn't change what they return until you publish it. Query details and search show the SQL of that version as well. Drafts are kept only while a query is shared, so for a query that is not shared this is its latest saved version.
- Each request runs at most one query: when an MCP client sends several execute calls in one batch, only the first one that passes its checks runs, and the others get an error asking to send them in separate requests. A call refused by its checks (for example for a missing parameter) does not count; a call refused by the group's IP whitelist, or because the query's group no longer exists, does.
- Queries run with the settings of their own group (tags, snippets, package limits). The group's IP whitelist always applies to running queries through the API — also for the group you last opened in the web app, because a token can be used from any address; listing, searching and query details are not restricted by it.
Using the REST API
Send the token in the Authorization header of every request (the word Bearer
is not case-sensitive):
Authorization: Bearer kvery_mcp_...
Every response is JSON in the same envelope:
{ "responseCode": "OK", "response": { ... } }
{ "responseCode": "ERROR", "response": "error message" }
To run a query, send its parameters in a parameters object to
POST /mcp/queries/{id}/execute:
curl -X POST 'https://app.kvery.io/mcp/queries/1727775300aB3dE5fG7hJ9kL1mN3pQ5r/execute' \
-H 'Authorization: Bearer kvery_mcp_...' \
-H 'Content-Type: application/json' \
-d '{"parameters":{"status":"active"}}'
{
"responseCode": "OK",
"response": {
"queryId": "1727775300aB3dE5fG7hJ9kL1mN3pQ5r",
"result": {
"columns": ["id", "name", "status"],
"data": [
["1", "Acme Ltd", "active"],
["2", "Globex Inc", "active"]
]
}
}
}
The main status codes:
| Status | Meaning |
|---|---|
400 | Invalid request (for example a missing required parameter), or the query failed |
401 | Missing, invalid or expired token (or wrong Basic Auth credentials) |
403 | No access to the query, a read-only token tried to run a query that modifies data, a read-only run was attempted on an agent-connection query, or the group's IP whitelist refused the call |
404 | The query is hidden from you in its group (also for a short time after its group has been deleted) |
429 | Rate limit reached — retry after the number of seconds in the Retry-After header |
503 | Temporary server problem (for example the rate limiter is unavailable) — try again later |
Limits and security
- Rate limit: 60 requests per minute per user; every REST request and every tool call counts, also inside a batch. Users behind the same IP address (e.g. an office network) can send up to 600 requests per minute together. A batch can contain up to 20 messages. The group and query rate limit applies to the public query API only, not to the MCP server.
- Basic Auth: repeated failed attempts temporarily block Basic Auth. Failures
are counted for an IP address and account together, for an IP address (blocking
every account from it) and for an account (blocking it from every address), each
with its own limit. While blocked, the API answers
429with aRetry-Afterheader until 15 minutes have passed since the last failed attempt (tokens are not affected). - Sessions: every request is authenticated on its own. The API never creates a web session and does not accept the session cookie of a browser signed in to Kvery.
- Browser requests: browser apps can send a token in the
Authorizationheader. app.kvery.io accepts requests from every origin. The server configuration can restrict browser origins; then browser requests are accepted only from Kvery itself and the allowed origins (servers, scripts and desktop MCP clients are not affected). - Activity log: every tool call and every REST call made with valid
credentials is logged, including calls refused by the rate limit, with the
token, query, parameters (the values of password fields are stored as
***), result size and IP address. Entries are kept for 90 days. Connection set-up messages (such as listing the tools), token management, failed sign-ins and requests refused before sign-in are not logged. - Your activity: MCP / API Keys → Activity lists your own calls, newest
first, with times in your local time; calls made with a token that has been
revoked or deleted show Deleted token. Each call shows the description of
its query (hover over it to see the query ID); a query that has been deleted or
that you cannot access is shown by its ID. The list is paged and
searchable — the search covers the action, token (or Basic Auth), query
description and ID, parameters, error message, IP address and the time in UTC
(
YYYY-MM-DD HH:MM:SS) — and can be exported to CSV or Excel (up to 10,000 entries, newest first; the Query column holds the query ID, and the query description is the last column). Other users' activity is never shown. - Live activity: while the Activity tab is open, it refreshes itself every 10 seconds and highlights the new calls for a moment. Switch Auto-refresh off to stop it (your browser remembers the choice), or use the refresh button. The automatic refresh pauses while you look at an older page.
Tips
- Publish and name queries clearly so an assistant can find the right one via search.
- Use tags to group the queries a token should expose.
- Use read-only tokens for assistants that only need to read data.
- Tick Hide SQL for assistants that should run your queries without seeing how they are written.