Ask the AI apps you already use about your data. QueryAPIGate turns the SQL you've saved into tools they can call, and checks every call in the server: who's asking, what they may see, and what they may change.
http://<host>:5001/mcpX-API-Key: <the agent's own key>{"customer": "acme"}| id | title | |
|---|---|---|
| 2 | Monitor flickers | a•••••@acme.com |
| 5 | Invoice PDF is blank | j•••@acme.com |
An illustration. Every call above passes the same checks as a REST request.
Three conversations, three outcomes. The agent asks; the server decides.
{"customer": "acme"}{}{"id": 2}rows_affected: 1 · once · in a transactionWrite the SQL once in the Console and publish it. Its declared parameters become the tool's typed input schema, and every value is bound, never spliced into the SQL.
orders_by_customerpublished ✓GET /q/orders_by_customerIts grant names exactly the queries, connections and tables it needs, and tools/list shows it nothing else. A hosted assistant signs in through your identity provider and acts as that person, their rows and masking included.
tools/list · key support-bot3 toolstools/call salaries_by_personunauthorizedThe same code as a REST call to the same query, in the same order. The prompt is never one of them.
Only through queries you marked for agents. A person sees the agent, the SQL and the exact arguments, and approves or rejects. It runs once, in a transaction capped at the rows you allow, and the key that asked can never approve its own write.
close_ticket {"id": 2}● awaiting a personrows_affected: 1Every tool call lands in run history marked "transport": "mcp": which agent, which query, the arguments' names, how many rows. Changes to who may do what go to a hash-chained audit log that streams to your SIEM.
run · support-bot · orders_by_customer12 rows · mcp#a91f · approve_write · maria→ SIEMOne URL and a key, or sign-in through your identity provider.
X-API-Key header. Each client's menu for this differs; look for "remote MCP server" or "custom connector" in its documentation.$ pip install "queryapigate[server,mcp]" $ queryapigate serve & $ queryapigate mcp
URL http://<host>:5001/mcp Header X-API-Key: <the agent's key>
The key travels as a header, never in the URL, where it would end up in access logs. Guide 24, step by step →
The official MCP SDK, verified end to end against a running server:
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
KEY = "your-api-key"
async def main():
async with streamablehttp_client("http://127.0.0.1:5001/mcp", headers={"X-API-Key": KEY}) as (r, w, _):
async with ClientSession(r, w) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool("example_top_films", {"top_n": 3})
print(result.structuredContent)
asyncio.run(main())Results come back as structuredContent with an outputSchema, so your code reads rows, not prose. Guide 24 →
/.well-known/oauth-protected-resource.QUERYAPIGATE_MCP_RESOURCE_URL=https://data.example.com/mcp QUERYAPIGATE_MCP_AUTH_SERVER=https://idp.example.com/realms/acme QUERYAPIGATE_MCP_ROLE=analyst
A token issued for the REST API is never accepted here, or the reverse. Stable since 0.30. Guide 46, with Keycloak →
tools/list returns in this workspace, and a connect snippet shows the URL and header a client needs.Hide a query from agents with one toggle on its MCP tab while it keeps serving over REST. Guide 27 →
The servers above give an agent your data. A separate, read-only documentation server lets it search QueryAPIGate's docs, guides and blog, and read any page. No sign-in. In Claude Code:
claude mcp add --transport http queryapigate-docs https://queryapigate-docs.queryapigate-docs-mcp.workers.dev/mcp --scope user
list_tables and describe_table to explore, masking-aware.execute_sql, always read-only, inside the tables you named.embed parameter and pgvector Exp.execute_sql.QUERYAPIGATE_MCP_MAX_ROWS), with a note to narrow the query.| What | Goes to | Kept | What the agent gets |
|---|---|---|---|
| Rows a tool returns | The agent's client | Not kept, unless you turn on the response cache | After masking; up to 200 rows a call |
| Each call: which agent, which query, the arguments' names, how many rows | Run history, in your store, marked mcp | For your history retention | Nothing |
| Changes to keys, roles and approvals | The audit log, hash-chained, streamed to your SIEM | In your audit log | Nothing |
| Database credentials | Stay in QueryAPIGate, encrypted | With the connection | Never |
| The agent's key or token | Checked, never passed on to a database | Keys as hashes | Its own, only |
Self-hosted: none of this leaves your infrastructure unless you point it somewhere. Where data goes →
Any MCP client that speaks Streamable HTTP and can send a header (for an API key) or follow MCP's OAuth (for sign-in): agent frameworks, IDE assistants and hosted assistants' connectors.
No, it's the same query. The tool you publish for agents is also GET /q/<name>, with an OpenAPI description, and can be exported on a schedule.
Through your identity provider, with MCP's OAuth 2.1. QueryAPIGate checks the tokens and issues none, so it never sees a password, and a token for the REST API is never accepted here.
No. Agents authenticate to QueryAPIGate; QueryAPIGate reaches your databases with each connection's own credentials, stored encrypted, and never passes a token or password on.
Its question doesn't matter; its grant does. It can call only the tools its key or role allows; execute_sql, if granted at all, is read-only and limited to the tables you named; masked columns stay masked however the SQL is written, or the query is refused.
It can ask for anything. It gets only what its grant allows, because the boundary is the server, not the model.
Its rate limit refuses it, its quota stops it for the day or month, and its concurrency cap keeps it from tying up your database, each a clear, machine-readable error it can act on.
Masking isn't available on MongoDB, H2 or generic JDBC connections; a caller with mask rules is refused there. Approved writes run on PostgreSQL, MySQL, SQLite and DuckDB. OAuth needs JWT access tokens signed with an asymmetric key. Semantic search is experimental and PostgreSQL only. And there's no natural-language-to-SQL, by design: it serves the queries you curated.