MCP Endpoint¶
TDB exposes a Model Context Protocol endpoint
at POST /v1/mcp. MCP uses JSON-RPC 2.0 over HTTP.
This endpoint is how Claude Desktop, Cursor, and other AI tools query your data sources directly — without building a custom integration.
Setting up a client
This page is the protocol reference. For step-by-step config (Claude Desktop, VS Code, Cursor, JetBrains, Windsurf, Cline) and example queries to run once connected, see Connect an IDE / AI Tool →.
Supported methods¶
| JSON-RPC method | Auth required | Description |
|---|---|---|
initialize |
No | MCP handshake — returns protocol version and capabilities |
ping |
No | Liveness — returns an empty result (from community 0.10.0 / enterprise 0.15.0) |
tools/list |
Yes | Lists available tools |
tools/call |
Yes | Executes a tool |
server/discover |
No | 2026-07-28 only: supported versions and capabilities (from community 0.12.0 / enterprise 0.18.0) |
Only initialize and ping are unauthenticated. This is intentional — MCP clients must
complete the handshake before presenting credentials, per the MCP spec.
Notifications such as notifications/initialized (no id) are accepted with HTTP
202 and an empty body. From community 0.10.0 / enterprise 0.15.0; earlier releases
answered them with "Method not found", which clients ignored.
Available tools¶
TDB Enterprise exposes seven tools, and an eighth, run_approved, to callers a
query approval rule can hold (from 0.32.0):
| Tool | What it does | Works with |
|---|---|---|
query_source |
Run a SQL SELECT against a source | All sources, including database-wide |
schema_source |
Column names and types, no SQL required | All sources — lists every table for database-wide sources |
preview_source |
First N rows, no SQL required | All sources — table argument required for database-wide |
filter_source |
Rows matching one column condition | All sources — table argument required for database-wide |
aggregate_source |
COUNT / SUM / AVG / MIN / MAX with optional GROUP BY | All sources — table argument required for database-wide |
list_views |
List the available YAML views | Requires TDB_VIEWS_DIR |
run_view |
Execute a named view with typed parameters | Requires TDB_VIEWS_DIR |
run_approved |
Run an approved request by its id | Listed only when an approval rule applies to the caller |
Database-wide sources and the no-SQL tools
For a database-wide source (registered without table), the four no-SQL
convenience tools (schema_source, preview_source, filter_source,
aggregate_source) accept an optional table argument to pick which table
to target, validated against the source's actual tables:
schema_source— omittableto list every table's columns; pass it to see one table's columns.preview_source,filter_source,aggregate_source—tableis required. Omitting it returns a tool-level error listing the available tables, so the AI tool can retry with a valid one.
Column/value validation and the generated SQL are scoped to whichever
table you name. query_source remains the only tool that can JOIN across
tables in a single call.
From 0.36.1 a table, column or group_by name matches in any case
when only one name matches (Snowflake lists CUSTOMERS for the table SQL calls
customers), and the generated SQL uses the database's own spelling. Names that
differ only in case need the exact one. A "not found" answer lists only the
tables and columns the caller's policy shows; a
caller under a rule naming a table outside it is refused
(policy_table_not_permitted), as SQL is. Before 0.36.1 that list was the whole
database.
Tools that return rows pass their output through the prompt-injection filter before the result reaches the MCP client. API keys can be restricted to a subset of tools — see Tool allow-lists per key. To make results smaller for an agent, see Saving tokens (Lean Mode).
Authentication¶
All MCP methods except initialize and ping require a Bearer token:
Any valid TDB credential works: static env key, DB-managed key, or JWT.
If the token is missing or invalid, TDB returns HTTP 401 with a WWW-Authenticate
header that MCP-aware clients use to discover the OAuth authorization server:
OAuth clients such as Cursor and Claude custom connectors use this header to start the OAuth 2.1 PKCE flow. See OAuth 2.1 →.
Browser origins¶
From community 0.10.0 / enterprise 0.15.0, a request carrying an Origin header is
served only if that origin is the TDB server itself or, in enterprise, is listed
in TDB_CORS_ORIGINS. Anything else gets HTTP 403 and is audited as
invalid_origin. MCP clients outside a browser send no Origin and are not
affected. The MCP spec requires this check.
Request format¶
All requests are JSON-RPC 2.0 objects sent to POST /v1/mcp:
Send one object per request: batches (a JSON array) are not supported. params,
and the arguments of a tools/call, must be objects.
In enterprise, each tool argument is checked against the type the tool declares
(from 0.15.0). A wrong type is a tool error (isError: true) that names the
argument. A number sent as a digit string ("10") is accepted, and null means
the argument was not given.
Protocol versions¶
From community 0.11.0 / enterprise 0.17.0, TDB speaks MCP 2025-11-25,
2025-06-18 and 2024-11-05. Earlier releases always answered 2024-11-05.
initialize answers with the protocolVersion the client asked for if TDB
supports it. Otherwise it answers with the newest supported version older than
the request, or 2024-11-05. A client is never handed a version newer than it
asked for. A client asking for 2025-03-26 gets 2024-11-05, as before: that
revision requires servers to accept JSON-RPC batches, which TDB does not.
After the handshake, clients send the negotiated version in the
MCP-Protocol-Version header. A request without the header is served as
2024-11-05. 2025-03-26 in the header is refused with HTTP 400. From
community 0.12.0 / enterprise 0.18.0, any other value that is not a handshake
version is judged by the 2026-07-28 rules below.
When the header names 2025-06-18 or later:
- every tool in
tools/listalso carries atitle,annotations(readOnlyHint: true,openWorldHint: false: every TDB tool reads a registered source and changes nothing) and anoutputSchema; - a successful
tools/callalso carriesstructuredContent, the same object as the JSON in the text block, which is still sent. Values in both follow the Query API rules, the same as REST; initializeaddsserverInfo.title,serverInfo.descriptionandcapabilities.tools.listChanged: false.
A client on 2024-11-05 receives exactly what it did before.
2026-07-28¶
From community 0.12.0 / enterprise 0.18.0, the same endpoint also serves
2026-07-28, the stateless revision. It has no initialize handshake. A
request is served this way when its MCP-Protocol-Version header, or its
params._meta, names 2026-07-28. A handshake-era header always selects the
handshake era, so existing clients are unaffected. The official MCP Python SDK
connects this way in its default mode, and so does Claude Code (tested with 2.1.300).
server/discover(no auth) returnssupportedVersions,capabilities,ttlMsandcacheScope: "public".- Every request carries
io.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilitiesinparams._meta. Missing either is HTTP 400,-32602. - The
MCP-Protocol-Version,Mcp-Methodand (ontools/call)Mcp-Nameheaders must repeat the body. A missing, mismatched or repeated header is HTTP 400,-32020. - An unsupported version is HTTP 400,
-32022, withdata: {"supported": [...], "requested": "..."}. - An unknown method, including
initializeandping, is HTTP 404,-32601. It is answered before authentication, so a client can tell this server from one that predates2026-07-28. - An unknown tool is
-32602. - Every result carries
resultType: "complete"and_meta["io.modelcontextprotocol/serverInfo"].tools/listalso carriesttlMsandcacheScope: "private", because it sits behind authentication. - Titles, annotations,
outputSchemaandstructuredContentare always sent.
Method reference¶
initialize¶
Completes the MCP handshake. No auth required.
curl -X POST http://localhost:8000/v1/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "tdb-enterprise", "version": "0.36.1"}
}
}
tools/list¶
Returns the list of available tools. Requires auth.
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
Response (abbreviated — one entry per tool, seven in total):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{ "name": "query_source", "description": "...", "inputSchema": { ... } },
{ "name": "schema_source", "description": "...", "inputSchema": { ... } },
{ "name": "preview_source", "description": "...", "inputSchema": { ... } },
{ "name": "filter_source", "description": "...", "inputSchema": { ... } },
{ "name": "aggregate_source", "description": "...", "inputSchema": { ... } },
{ "name": "list_views", "description": "...", "inputSchema": { ... } },
{ "name": "run_view", "description": "...", "inputSchema": { ... } }
]
}
}
tools/list advertises the tools the calling key may use. A key with a
tool allow-list is listed only those tools (from
0.19.0; earlier releases listed all seven and refused the rest at call time).
Every other caller is listed all seven.
tools/call — query_source¶
Executes a SQL query against a registered source. Rows are capped at 1,000 in
community and at TDB_MAX_ROWS in enterprise; truncated is true when more matched.
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
sql |
string | Yes | — | SQL SELECT statement |
source_name |
string | No | First registered source | The source's name in any case, or its UUID (exact name before 0.18.0 / 0.34.0) |
lean |
boolean | No | off | Rows as arrays — see Lean Mode (from 0.13.0 / 0.21.0) |
max_cell_chars |
integer | No | off | Cut longer text cells, marked — see Lean Mode |
limit |
integer | No | TDB_MAX_ROWS; 100 when paging |
Enterprise, from 0.31.0. At most TDB_MAX_ROWS |
page_key, cursor |
array of strings, string | No | — | Enterprise, from 0.31.0. Read further, with next_cursor |
The other row-returning tools take lean and max_cell_chars too, and
schema_source takes detail.
Use the table name that matches the source: CSV sources are queried as data;
database sources use real table names. Database-wide sources can JOIN across
any tables in the database.
Always pass source_name when more than one source is registered
Without it, the query runs against the first registered source, which may not be the one the AI tool intended.
Single-table source:
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "query_source",
"arguments": {
"sql": "SELECT country, COUNT(*) AS n FROM customers GROUP BY country ORDER BY n DESC LIMIT 10",
"source_name": "customers"
}
}
}'
Database-wide source (cross-table JOIN):
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "query_source",
"arguments": {
"sql": "SELECT c.name, SUM(o.total) AS spend FROM customers c JOIN orders o ON o.customer_id = c.id GROUP BY c.name ORDER BY spend DESC LIMIT 10",
"source_name": "production_db"
}
}
}'
Successful response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"source\":\"customers\",\"columns\":[\"country\",\"n\"],\"rows\":[{\"country\":\"US\",\"n\":1420},{\"country\":\"GB\",\"n\":380}],\"rows_returned\":2,\"truncated\":false}"
}
]
}
}
truncated is true when the source had more rows than were returned, so a
result of exactly the row cap is not mistaken for a complete one. Every tool that
returns rows carries it: community always has; enterprise from 0.15.1, on
query_source, preview_source, filter_source, aggregate_source and
run_view.
Reading further¶
Enterprise, from 0.31.0
A cut result gains a hint. With page_key naming columns that uniquely order the
result, the result gains next_cursor while there is more; pass it back as cursor,
with the same sql and page_key. The rules and refusals are those of
REST paging, and each refusal is a tool error.
filter_source and run_view accept the same two arguments. Only query_source
advertises them in tools/list, because every tool's schema is sent to the model on
every request; advertising them there costs 76 tokens per request. preview_source and
aggregate_source do not page.
Reading a lot this way is expensive. With Lean Mode 100 rows are about 6,900 tokens, so 50,000 rows would be about 3.4 million. An agent should aggregate, or read a little. A whole result is a job for a REST stream.
The text field contains a JSON-serialised result object. AI tools receive this
and can present it as a table or process it programmatically.
Error response (tool-level error, still HTTP 200):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{"type": "text", "text": "SQL validation error: Blocked keyword: DROP"}],
"isError": true
}
}
tools/call — schema_source¶
Returns column names and data types for a source — the tool an AI assistant calls before writing a query. No SQL required.
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
source_name |
string | No | First registered source | The source's name in any case, or its UUID (exact name before 0.18.0 / 0.34.0) |
table |
string | No | — | Table to inspect. Ignored for single-table/CSV sources. For database-wide sources: omit to list every table, or name one to see just its columns. |
Single-table or CSV source:
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {"name": "schema_source", "arguments": {"source_name": "customers"}}
}'
Result payload (inside content[0].text):
{
"source": "customers",
"columns": [
{"name": "id", "type": "integer"},
{"name": "country", "type": "text"}
]
}
Database-wide source, no table (lists every table):
{
"source": "production_db",
"tables": {
"customers": [{"name": "id", "type": "integer"}, {"name": "country", "type": "text"}],
"orders": [{"name": "id", "type": "integer"}, {"name": "customer_id", "type": "integer"}]
}
}
Database-wide source with table: "orders":
{
"source": "production_db",
"table": "orders",
"columns": [
{"name": "id", "type": "integer"},
{"name": "customer_id", "type": "integer"}
]
}
An unrecognized table returns a tool error listing the actual table names.
Schema results are served from the schema cache when caching is enabled.
Annotations: telling the model what a column means¶
Enterprise, from 0.4.0
Real schemas are full of names like cust_stat_cd_01. A model that cannot tell
what a column holds will guess, and a plausible guess produces a query that runs
and returns the wrong answer. Annotate the schema and schema_source carries
those descriptions into the model's context:
curl -X PUT http://localhost:8000/v1/sources/customers/annotations \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"annotations": [
{"description": "One row per retail customer account."},
{"column": "cust_stat_cd_01",
"description": "Account status. A=active, C=closed, S=suspended."},
{"column": "amt_usd_cts",
"description": "Balance in US cents. Divide by 100 for dollars."}
]
}'
schema_source then returns:
{
"source": "customers",
"columns": [
{"name": "cust_stat_cd_01", "type": "VARCHAR",
"description": "Account status. A=active, C=closed, S=suspended."},
{"name": "amt_usd_cts", "type": "BIGINT",
"description": "Balance in US cents. Divide by 100 for dollars."}
],
"source_description": "Retail customer master",
"description": "One row per retail customer account."
}
| Field | Comes from |
|---|---|
columns[].description |
An annotation with a column |
description |
An annotation with no column — describes the table |
source_description |
The source's own description set at registration |
table_descriptions |
Table-level annotations, on database-wide list-all responses |
Every one of these keys is omitted when unset, so an un-annotated deployment
gets exactly the response it got before. Manage annotations with
PUT/GET/DELETE /v1/sources/{ref}/annotations.
tools/call — preview_source¶
Returns the first N rows of a source's table. No SQL required.
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
source_name |
string | No | First registered source | The source's name in any case, or its UUID (exact name before 0.18.0 / 0.34.0) |
table |
string | No | — | Table to preview. Required for database-wide sources (registered without a fixed table) — ignored for single-table/CSV sources. |
limit |
integer | No | 10 | Rows to return (1–100; at most TDB_MAX_ROWS, and a larger value is a tool error (from 0.16.0)) |
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {"name": "preview_source", "arguments": {"source_name": "customers", "limit": 5}}
}'
Database-wide source (table required):
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {"name": "preview_source", "arguments": {"source_name": "production_db", "table": "orders", "limit": 5}}
}'
Result payload: {"source": ..., "columns": [...], "rows": [...], "rows_returned": N, "truncated": bool}.
Omitting table on a database-wide source returns a tool error listing the
available tables.
tools/call — filter_source¶
Returns rows matching a single column condition, without the AI tool writing SQL.
The column name is validated against the source schema and the operator against a
fixed allow-list, and from 0.31.0 value is escaped for the source's engine, so the
model cannot inject SQL through this tool.
Before 0.31.0 it could, on MySQL and Snowflake.
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
column |
string | Yes | — | Column to filter on (must exist in the schema; any case from 0.36.1) |
value |
string | Yes | — | Comparison value (interpreted by column type) |
operator |
string | No | = |
One of =, !=, >, <, >=, <=, LIKE |
source_name |
string | No | First registered source | The source's name in any case, or its UUID (exact name before 0.18.0 / 0.34.0) |
table |
string | No | — | Table to filter. Required for database-wide sources — ignored for single-table/CSV sources. column is validated against this table's schema. |
limit |
integer | No | 100 | Max rows to return. At most TDB_MAX_ROWS, and a larger value is a tool error (from 0.16.0) |
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "filter_source",
"arguments": {"source_name": "customers", "column": "country", "value": "US", "limit": 50}
}
}'
Result payload: {"source": ..., "columns": [...], "rows": [...], "rows_returned": N, "truncated": bool}.
An unknown column returns a tool error listing the valid column names.
tools/call — aggregate_source¶
Runs a single aggregate over a column, optionally grouped. Column names are validated against the source schema; the function is restricted to the five listed below.
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
function |
string | Yes | — | One of COUNT, SUM, AVG, MIN, MAX |
column |
string | Yes | — | Column to aggregate; * is allowed for COUNT(*). Any case from 0.36.1 |
group_by |
string | No | — | Column to group results by |
source_name |
string | No | First registered source | The source's name in any case, or its UUID (exact name before 0.18.0 / 0.34.0) |
table |
string | No | — | Table to aggregate. Required for database-wide sources — ignored for single-table/CSV sources. column/group_by are validated against this table's schema. |
limit |
integer | No | 100 | Max groups to return. At most TDB_MAX_ROWS, and a larger value is a tool error (from 0.16.0) |
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "aggregate_source",
"arguments": {"source_name": "customers", "function": "COUNT", "column": "*", "group_by": "country"}
}
}'
Result payload: {"source": ..., "function": "COUNT", "column": "*", "group_by": "country",
"columns": [...], "rows": [...], "rows_returned": N, "truncated": bool}.
tools/call — list_views and run_view¶
Expose YAML named views to AI tools. Views are administrator-approved,
pre-defined queries — the safest way to give an AI assistant multi-table access,
because the SQL is fixed and only typed parameters vary. Both tools require views
to be configured (TDB_VIEWS_DIR); with no views loaded, list_views returns an
empty list.
list_views takes no arguments and returns every view with its description,
source, and parameter definitions.
run_view:
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
view_name |
string | Yes | — | Name of the view to execute |
parameters |
object | No | {} |
Parameter values required by the view |
limit |
integer | No | 1,000, or TDB_MAX_ROWS if lower |
Max rows to return. At most TDB_MAX_ROWS, and a larger value is a tool error (from 0.16.0) |
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "run_view",
"arguments": {"view_name": "orders_by_status", "parameters": {"status": "shipped"}}
}
}'
Result payload: {"view": ..., "source": ..., "columns": [...], "rows": [...], "rows_returned": N, "truncated": bool}.
Tool allow-lists per API key¶
DB-managed API keys can carry an allowed_tools list that restricts which MCP
tools the key may call — for example, a reporting agent's key limited to
["schema_source", "run_view", "list_views"]. Keys without an allow-list can call
every tool.
From 0.19.0, tools/list shows a scoped key only its allowed tools, so an agent is
not offered tools it cannot use. Keys without an allow-list, TDB_API_KEYS keys and
OAuth callers are listed all seven; an empty list ([]) is listed none. On protocol
2026-07-28 a client may cache the list for up to five minutes, so after you change
a key's scope its client can briefly show the old list. Calls are always checked
against the current scope.
A disallowed call returns a tool-level error (HTTP 200, isError: true) so MCP
clients handle it gracefully:
{
"content": [{"type": "text", "text": "Tool 'query_source' is not permitted for this API key."}],
"isError": true
}
See API Keys → tool allow-lists for how to set
allowed_tools when creating or updating a key.
Saving tokens (Lean Mode)¶
Every MCP result an AI agent reads is paid for in its context window. TDB is built to keep that bill small, and most of it needs no setting:
- Ask for the answer, not the rows. SQL aggregates run in the database, and in
enterprise
aggregate_sourceandfilter_sourcedo it without the agent writing SQL. - A ceiling and an honest flag. A response stops at the row ceiling and says
truncated: truewhen it did, so an agent never pulls a whole table by accident and never re-fetches to check that it has everything. - Only the tools a key may use. In enterprise, a key with
allowed_toolssees only those tools intools/list(from 0.19.0), and every tool's schema is sent on every request — so a narrow key is also a cheaper one. Tool descriptions are kept short for the same reason. - Lean Mode, below, for the results themselves.
Lean Mode is from community 0.13.0 / enterprise 0.21.0, and opt-in at every level: with nothing set, every response is exactly what it was before.
| Setting | Effect | Lossless? | Tools |
|---|---|---|---|
lean: true |
Rows are arrays in columns order instead of objects that repeat every column name; schema_source returns {column: type} |
Yes | All tools that return rows; schema_source (enterprise) |
format |
objects (default) or table, to set the row shape explicitly |
Yes | Same |
max_cell_chars |
Text cells longer than this are cut, and each cut is marked …[+N chars]. Minimum 50; 0 turns it off |
No — never part of lean: true |
Same |
detail |
schema_source only: full (default), types ({column: type}), names (column names only), tables (table names only) |
types yes; names, tables no |
schema_source (enterprise) |
Measured with Claude Code (Claude Opus 5.5): a 100-row result went from 10,412 to
6,868 tokens with lean: true, and a 30-table, 292-column database schema from
5,235 tokens to 3,025 (types), 1,400 (names) or 113 (tables).
On an answer-quality check of 21 questions, Lean results gave the same answers as
today's.
Where a setting comes from. A tool call's own arguments win, then the API key's
stored setting (enterprise — see API Keys),
then the server default TDB_LEAN_MODE, then
off. Layers merge field by field, and lean: false in a layer clears everything
below it — so an agent can always ask for today's format.
What the agent sees. A Lean result says what was applied, so the model never has to guess:
{"source":"orders","columns":["id","note"],"rows":[[1,"Customer called about the delayed courier and aske…[+2700 chars]"]],
"rows_returned":1,"truncated":false,
"lean":{"format":"table","max_cell_chars":50,"trimmed_cells":1}}
trimmed_cells counts cut values; it is separate from truncated, which still means
rows were withheld. A marked cell tells the agent to query that value on its own if
it needs all of it. structuredContent carries the same Lean payload as the text.
Safety is unchanged. Results are shaped after the
prompt-injection filter has run, so a Lean format or a
cut can never bring back a value the filter redacted, and if the filter fails the
call is still refused. A bad value (format: "csv", max_cell_chars: 10) is refused
with a tool error naming the allowed values, never ignored.
Audit. The query entry records the settings served, as lean (see
Audit log).
Prompt-injection filtering¶
Two filters protect the MCP path:
- Input: the
sqlargument ofquery_sourceis screened before validation. A flagged input is rejected with a tool error (Input rejected: potential prompt injection detected.) and logged. - Output: rows returned by any tool are screened before the response is
serialised. Cells that contain injection patterns (e.g. instructions embedded in
data that try to steer the AI model) are redacted, and the redaction count is
written to the server log.
structuredContentis built from the filtered text, so it never carries a redacted value. In enterprise from 0.17.0, if the output filter itself fails, the call is a tool error and the rows are withheld. Earlier releases returned them unfiltered.
The filters run server-side on every call — there is nothing to configure on the MCP client.
Error codes¶
| HTTP status | JSON-RPC error code | Meaning |
|---|---|---|
| 200 | — | Success (check isError for tool-level errors) |
| 200 | -32700 | Parse error — invalid JSON |
| 200 | -32600 | Invalid JSON-RPC version |
| 400 | -32600 | Batch (array) or non-object body (from community 0.10.0 / enterprise 0.15.0) |
| 400 | -32600 | Unsupported MCP-Protocol-Version header (from community 0.11.0 / enterprise 0.17.0) |
| 400 | -32602 | 2026-07-28: params._meta is missing the version or client capabilities |
| 400 | -32020 | 2026-07-28: a routing header is missing, repeated or disagrees with the body |
| 400 | -32022 | 2026-07-28: unsupported protocol version (data.supported lists the served ones) |
| 404 | -32601 | 2026-07-28: unknown method |
| 200 | -32601 | Method not found / unknown tool |
| 200 | -32602 | params or arguments is not an object, or a tools/call without a string name |
| 401 | -32001 | Unauthorized (missing/invalid token) |
| 403 | -32600 | Origin not allowed (see Browser origins) |
| 429 | -32000 | Rate limit exceeded |
Protocol-level errors (parse, version, unknown method, invalid params) are returned
with HTTP 200 and an error object. A body TDB cannot accept at all (a batch or a
non-object), or an unsupported MCP-Protocol-Version header, is HTTP 400. Auth, origin and rate-limit failures use their HTTP status
codes, so MCP clients can react to them at the transport layer.
Rate limiting on MCP¶
DB-managed API keys are rate-limited on the MCP path in the same way as the REST
API. The rate limit check runs after authentication and before the tool call.
HTTP 429 is returned with X-RateLimit-* headers when the limit is exceeded.
Audit log¶
Every successful tools/call that touches data (query_source, preview_source,
filter_source, aggregate_source, run_view) writes a line to tdb_audit.jsonl,
same format as the REST query endpoint — including the SQL that the no-SQL tools
generated on the caller's behalf (run_view entries record <view:name>).
Refused calls are audited, not just logged: a blocked write, a prompt-injection
rejection or an auth failure writes an "event": "denied" entry with an action
and machine-readable reason, signed into the same hash chain as successful
queries — deleting one breaks /v1/audit/verify.
From community 0.12.0 / enterprise 0.18.0, every entry written while serving
/v1/mcp also names the caller: mcp_client and mcp_protocol. A
2026-07-28 client is named by the clientInfo it sends with each request. A
handshake-era client is named by its User-Agent, because it names itself only
at initialize, which a stateless server cannot tie to later calls. Both are
self-reported by the client: use them to tell AI tools apart, not as proof of
identity. The key that made the call is still key_hint.
From community 0.13.0 / enterprise 0.21.0, an entry for a call served in
Lean Mode also carries lean: the settings applied, for
example {"format": "table", "max_cell_chars": 500}. It is absent when nothing was
applied.