Tool reference

All nine tools registered by drill-mcp (see drill_mcp/server.py). Every tool raises a single exception type, ToolError, to the MCP client — never a raw traceback, never an unrelated Python exception. ToolError‘s message is the only thing a model sees; where the underlying failure is Drill’s own error text, that text is passed through (and truncated at 2000 characters — see client_rest.py_error_text).

Five of the nine tools (list_storage_plugins, cluster_status, list_profiles, get_profile, cancel_query) are management tools: they require the REST backend. On the JDBC backend they all raise the same ToolError:

'<method>' needs a REST connection to Drill; the JDBC backend does not
expose management endpoints

This is because JdbcClient (drill_mcp/client_jdbc.py) simply does not implement storage_plugins, cluster_status, profiles, profile, or cancel_query — there is no fallback or emulation attempted.


run_query

run_query(sql: str, max_rows: int | None = None) -> dict

Runs exactly one SQL statement against Drill and returns its rows.

Parameters

NameTypeOptionalNotes
sqlstringnoExactly one SQL statement. Anything that doesn't parse as one statement is rejected — see “Safety” below.
max_rowsintegeryes (default None)A cap on returned rows. May only lower the server's configured max_rows, never raise it (see below). A value <= 0 is treated as “not supplied” and the configured cap is used.

Returns a dict:

{
  "columns": ["id", "name"],
  "rows": [{"id": 1, "name": "alice"}],
  "query_id": "24680246-1000-abcd-8888-0123456789ab",
  "truncated": false
}

If the result was truncated, a "note" key is added:

{
  "truncated": true,
  "note": "Results were truncated at 100 rows. Narrow the query or aggregate to see the rest."
}

query_id is Drill's own query UUID on the REST backend, useful as input to get_profile/cancel_query. On the JDBC backend query_id is always nullJdbcClient.query has no way to recover it from a plain JDBC cursor, so get_profile/cancel_query are of no use for a query run through JDBC even independent of their REST-only restriction.

Errors

  • sql must be a stringsql was not a str.
  • max_rows must be an integermax_rows was supplied but not an int.
  • Any policy rejection (see “Safety” below) — message explains which rule was violated (unwritable plugin, hidden schema, multiple statements, disallowed statement type, unparseable SQL).
  • Drill's own error text (e.g. VALIDATION ERROR: no such table), passed through from DrillError unchanged, truncated at 2000 characters.

Row cap. The effective cap is min(max_rows, config.max_rows) when max_rows is a positive integer, otherwise config.max_rows. A caller cannot ask for more rows than the operator configured; it can only ask for fewer.

Safety (the SQL policy)

Every statement is parsed with sqlglot's Drill dialect — a real parser, not a regex — before it reaches Drill. The policy is deny-by-default:

  • Reads are always permitted (SELECT, set operations, subqueries), as long as no write is embedded inside them (e.g. a WITH x AS (INSERT ...) SELECT * FROM x, or a nested CREATE/DROP/INSERT/UPDATE/DELETE/ MERGE anywhere in the parse tree — checked structurally, not textually).
  • CREATE TABLE AS, CREATE VIEW, DROP TABLE, DROP VIEW are permitted only when their target is schema-qualified and that schema matches an entry in writable_plugins (see docs/configuration.md). An unqualified target (no schema. prefix) is always rejected, since there is nothing to check it against. CREATE/DROP of anything other than TABLE/VIEW (e.g. a schema or function) is rejected outright.
  • INSERT, UPDATE, DELETE, MERGE, ALTER, USE, REFRESH are always rejected — writable_plugins has no effect on them. There is no configuration path that permits them.
  • ALTER SYSTEM and storage-plugin creation/update/deletion are not implemented anywhere in this codebase. There is no flag, config key, or hidden code path that turns them on — nothing exists for such a tool call to reach.
  • SHOW and DESCRIBE are always permitted (metadata-only).
  • EXPLAIN / EXPLAIN PLAN FOR recurses. The guard strips the leading keyword and re-checks the remaining statement text against the same rules (so EXPLAIN cannot be used to peek at or run a statement that would otherwise be rejected). Recursion is bounded (5 levels), so a pathological EXPLAIN EXPLAIN EXPLAIN ... chain cannot exhaust the stack — it is rejected instead.
  • Exactly one statement per call. sql must parse to exactly one statement; stacked statements (SELECT 1; SELECT 2) are rejected.
  • Anything that fails to parse is rejected, including a tokenizer failure (pathologically deep nesting, unterminated quoting, etc.) — a parse failure is never treated as “unknown, so allow it.”

Hidden schemas and SHOW

If hidden_schemas is configured, run_query additionally:

  1. Rejects, before the query ever reaches Drill, any statement whose parsed table references resolve into a hidden schema (e.g. SELECT * FROM sys.options with hidden_schemas: [sys]). This applies to ordinary SELECT/CREATE/DROP statements, where sqlglot can identify the table references structurally.
  2. Also scans the raw text of any SHOW/DESCRIBE-style command for a hidden schema‘s name as a whole word (sqlglot has no dedicated grammar for Drill’s SHOW, so the whole command falls back to an opaque Command node with everything after the keyword left as unparsed text; this regex scan is a best-effort, word-boundary text match over that remainder, and is documented in code as a target for replacement should Drill's grammar ever expose SHOW's target as a real expression).
  3. Filters the result rows of every SHOW command — not just SHOW SCHEMAS/SHOW DATABASES — dropping any row whose first column matches a hidden-schema prefix. This is deliberate and fail-closed: three narrower attempts to detect specifically-schema-listing SHOW variants (a regex over the raw SQL, exact match against sqlglot‘s parsed literal, a comment-stripping regex over that literal) were each defeated by some spelling (an embedded comment, LIKE '...' clauses, unbalanced comment delimiters) and leaked a hidden schema name through. The trade-off: a SHOW TABLES/SHOW FILES row for a real table or file that happens to be named after a hidden-schema prefix (e.g. a table literally called sys in a workspace, under hidden_schemas: [sys]) is also dropped, even though it isn’t a schema at all. A false positive here — a real table briefly missing from a listing — is accepted in exchange for never leaking a schema name the operator asked to hide.

Hiding INFORMATION_SCHEMA does not break list_schemas, list_tables, or describe_table. All three query INFORMATION_SCHEMA internally regardless of what's hidden; they simply omit hidden entries from what they return to the caller.


list_schemas

list_schemas() -> list[dict]

Lists every schema visible on the cluster, minus any matching hidden_schemas.

Parameters: none.

Returns a list of dicts, one per schema:

[
  {"name": "dfs.tmp", "type": "file"},
  {"name": "sys", "type": "system-tables"}
]

type is Drill's INFORMATION_SCHEMA.SCHEMATA.TYPE column, unmodified.

Errors: Drill's own error text on a connection or query failure.


list_tables

list_tables(schema: str) -> list[dict]

Lists the tables (and, for file plugins, files/directories) in one schema.

Parameters

NameTypeOptionalNotes
schemastringnoA dotted schema/workspace path, e.g. dfs.tmp.

Returns a list of dicts:

[
  {"name": "orders", "type": "TABLE"},
  {"name": "sales_view", "type": "VIEW"},
  {"name": "logs", "type": "DIRECTORY"}
]

Behavior an operator should expect: for a file-based plugin (storage plugin TYPE = file, e.g. dfs, s3), list_tables uses SHOW FILES FROM <schema> instead of INFORMATION_SCHEMA.TABLES — file plugins do not register their contents in INFORMATION_SCHEMA at all, so querying TABLES for one would silently return an empty list that looks like an empty workspace. Under SHOW FILES:

  • A file named <name>.view.drill is reported as {"name": "<name>", "type": "VIEW"} — the .view.drill suffix (how Drill persists a view on a filesystem workspace) is stripped.
  • Anything Drill's directory listing marks isDirectory: true is reported as "type": "DIRECTORY", not "TABLE".
  • Results are sorted by name.

For any other plugin type, INFORMATION_SCHEMA.TABLES is queried directly and type is whatever TABLE_TYPE reports (typically TABLE or VIEW).

Errors

  • schema '<x>' is hidden by configurationschema matches a hidden_schemas entry. Refused before any query is issued.
  • Drill's own error text otherwise.

describe_table

describe_table(schema: str, table: str) -> list[dict]

Lists one table's columns, with type and (where determinable) nullability.

Parameters

NameTypeOptionalNotes
schemastringnoDotted schema/workspace path.
tablestringnoTable/view/file name. May itself contain a literal . (e.g. sales.csv on a file plugin) — treated as one identifier, not a nested path.

Returns a list of dicts:

[
  {"name": "id", "data_type": "INTEGER", "nullable": true},
  {"name": "created_at", "data_type": "TIMESTAMP", "nullable": null}
]

Behavior an operator should expect — this is the most surprising tool in the package. The strategy depends on the storage plugin's TYPE, read from INFORMATION_SCHEMA.SCHEMATA (an extra round trip on every call):

  • Static-schema plugins (anything not file, mongo, or splunk): uses DESCRIBE <schema>.<table>. Metadata-only, never reads user data. Reports real nullability (IS_NULLABLE from Drill).
  • Dynamic-schema plugins (file, mongo, splunk): DESCRIBE cannot answer for these — Drill discovers their schema at read time, not from a registry. describe_table instead probes with SELECT * FROM <schema>.<table> LIMIT 1 (or, for mongo specifically, SELECT \**` FROM . LIMIT 1, since Mongo's dynamic-field syntax differs from a plain *). Only the probe's **column names and types** are used; the sampled row's actual values are never read out of the response and never appear in the tool's output, under any failure mode. **Nullability cannot be determined from one sampled row** — a NULLin the one row says nothing about whether the column *can* be non-null, and vice versa — sonullableis reported asnull` (not guessed) on this path.
    • If the probe returns zero rows (e.g. the table is genuinely empty), describe_table raises rather than silently returning [] — an empty column list would be indistinguishable from “this table has no columns,” which is a different and misleading claim.
  • HTTP plugins (TYPE = http): Drill has no schema for an HTTP source until a real request has been made against it — there is nothing to DESCRIBE and nothing to probe. describe_table raises a ToolError that says so explicitly and suggests running SELECT * FROM <schema>.<table> LIMIT 10 and reading the column names from the result, rather than returning an empty list that would look like “no columns.”

Errors

  • schema '<x>' is hidden by configuration — refused before any query.
  • invalid identifier: '<x>'table contains characters outside the safe set (letters, digits, _, $, ., -), or an unsafe .-segment (empty, or .., which could otherwise reach a parent directory on a file plugin).
  • The explicit HTTP-plugin message above.
  • columns could not be determined for . because the probe returned no rows; the table may be empty. — dynamic-schema probe returned zero rows.
  • Drill‘s own error text for any other query failure (missing table, permissions, etc.), unmodified — Drill’s error text does not embed cell content, so nothing is redacted from it here.

list_storage_plugins

list_storage_plugins() -> list[dict]

Lists every configured storage plugin, with secrets redacted.

Parameters: none. Requires the REST backend.

Returns a list of plugin config dicts, shape defined by Drill's own /storage.json, e.g.:

[
  {
    "name": "s3",
    "config": {
      "type": "file",
      "connection": "s3a://my-bucket",
      "accessKey": "***REDACTED***",
      "secretKey": "***REDACTED***"
    }
  }
]

Redaction is always on and is not configurable off — see drill_mcp/redact.py. Any dict key matching (case insensitively, anywhere in the key) password, passwd, secret, credential, token, access[._-]?key, private[._-]?key, api[._-]?key, session[._-]?key, authorization, passphrase, keytab, or principal has its value replaced with "***REDACTED***", recursively through nested dicts, lists, and tuples. This is deliberately broad — a false-positive redaction is a cosmetic problem, a missed one is a leaked credential — so a non-secret key that happens to match (e.g. a plugin literally named tokenizer) is redacted too.

Plugins backing a hidden schema (per hidden_schemas) are omitted from the returned list entirely, matched on the plugin's name.

Errors

  • 'storage_plugins' needs a REST connection to Drill; the JDBC backend does not expose management endpoints — JDBC backend.
  • Drill's own error text otherwise.
  • A non-dict entry in Drill‘s plugin list is silently skipped rather than raising (defensive, in case Drill’s REST response is malformed).

cluster_status

cluster_status() -> dict

Reports Drillbit membership and overall cluster status.

Parameters: none. Requires the REST backend.

Returns the merge of Drill's /cluster.json and /status.json responses into one dict (/status.json's keys win on any name collision):

{
  "drillbits": [{"address": "10.0.0.5", "userPort": 31010, "state": "ONLINE"}],
  "status": "Running!",
  "version": "1.21.1"
}

Errors

  • 'cluster_status' needs a REST connection to Drill; the JDBC backend does not expose management endpoints — JDBC backend.
  • Drill's own error text otherwise.

list_profiles

list_profiles(limit: int = 20) -> list[dict]

Lists recent and currently-running query profiles, newest first.

Parameters

NameTypeOptionalNotes
limitintegeryes, default 20Caps the number of profiles returned. Combines Drill's runningQueries and finishedQueries lists (running first) and truncates to limit. A negative limit is clamped to 0 (returns []), not rejected.

Note: the default is 20, not None — check the live tool schema if a client surfaces defaults, since a caller passing no limit at all still gets at most 20 profiles back, not an unbounded list.

Returns a list of profile summary dicts, shape defined by Drill‘s /profiles.json, passed through the same secret redaction as list_storage_plugins (profiles are cluster-wide and can carry other users’ connection strings). Any entry whose query text names a hidden_schemas entry is dropped entirely, the same protection list_schemas/list_tables apply — otherwise a hidden schema‘s name would leak out as data in another user’s query text:

[
  {"queryId": "24680246-...", "state": "RUNNING", "query": "SELECT * FROM ..."},
  {"queryId": "13570246-...", "state": "COMPLETED", "query": "SELECT 1"}
]

Errors

  • 'profiles' needs a REST connection to Drill; the JDBC backend does not expose management endpoints — JDBC backend.
  • limit must be an integerlimit was supplied but not an int (a bool is explicitly rejected too, since bool is a subclass of int in Python and True/False as a row limit would be a confusing accident).
  • Drill's own error text otherwise.

get_profile

get_profile(query_id: str) -> dict

Fetches the full profile for one query id.

Parameters

NameTypeOptionalNotes
query_idstringnoDrill's query UUID, as returned in run_query's query_id field (REST backend only — always null on JDBC). Validated against [A-Za-z0-9-]+.

Returns Drill‘s full profile JSON for that query — the complete /profiles/<query_id>.json payload (fragments, operator metrics, timing, etc.), which can be large — passed through the same secret redaction as list_storage_plugins. A full profile embeds Drill’s serialized physical plan, which for JDBC and HTTP storage plugins can carry plugin configuration, so this is not returned unmodified.

Errors

  • 'profile' needs a REST connection to Drill; the JDBC backend does not expose management endpoints — JDBC backend.
  • query_id must be a string — wrong type.
  • invalid query id: '<x>'query_id contains characters outside [A-Za-z0-9-]. Rejected before any request is made.
  • profile '<id>' references a hidden schema — the profile's query text names a hidden_schemas entry.
  • Drill's own error text otherwise (e.g. no profile with that id).

cancel_query

cancel_query(query_id: str) -> str

Cancels a running query by its query id.

Parameters

NameTypeOptionalNotes
query_idstringnoSame validation as get_profile.

Returns Drill's raw response text from /profiles/cancel/<query_id> (a short human-readable string, e.g. "Cancelled query <id>"), passed through unmodified — not parsed as JSON, not restructured.

Errors

  • 'cancel_query' needs a REST connection to Drill; the JDBC backend does not expose management endpoints — JDBC backend.
  • query_id must be a string — wrong type.
  • invalid query id: '<x>' — invalid characters.
  • Drill's own error text otherwise (e.g. the query id does not exist or has already completed).