Skip to content

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 — omit table to list every table's columns; pass it to see one table's columns.
  • preview_source, filter_source, aggregate_source — table is 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:

Authorization: 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:

WWW-Authenticate: Bearer realm="TDB", resource_metadata="/.well-known/oauth-protected-resource"

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:

{
  "jsonrpc": "2.0",
  "id": <integer or string>,
  "method": "<method>",
  "params": { ... }
}

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/list also carries a title, annotations (readOnlyHint: true, openWorldHint: false: every TDB tool reads a registered source and changes nothing) and an outputSchema;
  • a successful tools/call also carries structuredContent, 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;
  • initialize adds serverInfo.title, serverInfo.description and capabilities.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) returns supportedVersions, capabilities, ttlMs and cacheScope: "public".
  • Every request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in params._meta. Missing either is HTTP 400, -32602.
  • The MCP-Protocol-Version, Mcp-Method and (on tools/call) Mcp-Name headers must repeat the body. A missing, mismatched or repeated header is HTTP 400, -32020.
  • An unsupported version is HTTP 400, -32022, with data: {"supported": [...], "requested": "..."}.
  • An unknown method, including initialize and ping, is HTTP 404, -32601. It is answered before authentication, so a client can tell this server from one that predates 2026-07-28.
  • An unknown tool is -32602.
  • Every result carries resultType: "complete" and _meta["io.modelcontextprotocol/serverInfo"]. tools/list also carries ttlMs and cacheScope: "private", because it sits behind authentication.
  • Titles, annotations, outputSchema and structuredContent are 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_source and filter_source do it without the agent writing SQL.
  • A ceiling and an honest flag. A response stops at the row ceiling and says truncated: true when 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_tools sees only those tools in tools/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 sql argument of query_source is 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. structuredContent is 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.