Summary: ctx_database_query hands the caller's SQL to Drupal's
Connection::prefixTables() and Connection::quoteIdentifiers(), which are
blind str_replace() passes over the whole statement. Every [,
], { and } inside a quoted string literal is rewritten
into an identifier quote before the statement reaches the database.

Sometimes that is a syntax error. Often it is a valid query that answers a different
question
, and the tool returns the wrong answer as a normal result — no error, no warning,
and a result with exactly the shape a correct answer would have. An MCP client has no way to
notice.

Steps to reproduce

Any Drupal 11 site with ctx enabled. All statements below are self-contained.

-- 1. What the substitution actually does
SELECT '[]' AS a, '[abc]' AS b, '[' AS c, '{tbl}' AS d;
--   tool: a = ""   b = "abc"   c = "   d = "tbl"
--   psql: a = []   b = [abc]   c = [   d = {tbl}

-- 2. Silent wrong answer: regex character class and quantifier
SELECT 'x' ~ '^[a-z]{1}$' AS m;        -- tool: false   psql: true
SELECT 'abc123' ~ '[0-9]+' AS m;       -- tool: false   psql: true
SELECT 'abc123' ~ '[[:digit:]]' AS m;  -- tool: false   psql: true

-- 3. Silent wrong answer: JSON literal
SELECT '{"a":1}' AS j;                 -- tool: "a":1"  psql: {"a":1}

-- 4. Noisy failure, and the clearest evidence: the error echoes the rewritten SQL
SELECT 'abc123' ~ '[0-9]+' AS c, (ARRAY[1,2,3])[2] AS s;
-- ERROR: syntax error at or near ""1,2,3""
-- LINE 1: SELECT 'abc123' ~ '"0-9"+' AS c, (ARRAY"1,2,3")"2"...

Case 1 shows this is not bracket-pair matching but a character-for-character replacement: a
lone [ becomes a lone ".

Root cause

QueryTool::__invoke() calls the database layer with default options:

// ctx/src/Plugin/McpTool/Database/QueryTool.php:94, :97
$statement = $this->connection->query($query, $normalized);
// ...
$statement = $this->connection->prepareStatement($query, [], TRUE);

Connection::prepareStatement() then runs both rewrites
(core/lib/Drupal/Core/Database/Connection.php:488-491):

// Resolve {tables} and [identifiers] to the platform specific syntax.
$query = $this->prefixTables($query);
if (!($options['allow_square_brackets'] ?? FALSE)) {
  $query = $this->quoteIdentifiers($query);
}

Both are unconditional string replacements over the entire statement
(Connection.php:364 and :388):

return str_replace(['{', '}'], $this->tablePlaceholderReplacements, $sql);
return str_replace(['[', ']'], $this->identifierQuotes, $sql);

That is correct for Drupal's own query builders, which own the SQL they generate and never put
a brace or a bracket in a literal. It is wrong for a tool whose purpose is to execute SQL somebody
else wrote.

Two independent rewrites are involved, and only one can be switched off:

  • [ ] — suppressible via the allow_square_brackets option.
  • { } — not suppressible. prefixTables()
    has no opt-out, and {table} expansion is an advertised feature of this tool.

Affected SQL

SQL Rewritten to Outcome
'[]', '[{"a":1}]' — JSON array literals '""' silent wrong answer
'{"a":1}' — JSON object literals '"a":1"' silent wrong answer
'[0-9]+', '[[:digit:]]' — regex classes '"0-9"+' silent wrong answer
'x{2,3}' — regex quantifiers 'x"2,3"' silent wrong answer
'{1,2,3}'::int[] — array literals '"1,2,3"'::int"" error
ARRAY[1,2], col[1] — constructors, subscripts ARRAY"1,2" error
int[], text[] — array type casts int"" error
'$[0].month' — jsonpath '$"0".month' error (read as a jsonpath variable)

Every row was run through the tool; "silent wrong answer" means the tool returned a normal
result that disagrees with psql on the same statement. The error rows are harmless —
they are loud, and the message echoes the rewritten SQL. The silent rows are the bug.

Workaround

Never put a literal containing [ ] { } in the
SQL text. Bind it as a parameter — placeholders are substituted by PDO after the rewrites,
so their values are untouched:

query:     SELECT 'x' ~ :re AS m
arguments: {"re": "^[a-z]{1}$"}
→ true    (correct; the same regex inline returns false)

query:     SELECT :v AS bound
arguments: {"v": "[]"}
→ []      (correct; the same literal inline returns "")

This does not help with array subscripts, ARRAY[…] constructors or
type[] casts, which are syntax and cannot be parameterised.

Proposed fix

Pass allow_square_brackets from the tool:

$statement = $this->connection->query($query, $normalized, ['allow_square_brackets' => TRUE]);
// ...
$statement = $this->connection->prepareStatement($query, ['allow_square_brackets' => TRUE], TRUE);

That removes the entire [ ] half, which is the larger and more
dangerous one — JSON literals, regex classes, array syntax, jsonpath. The cost is that Drupal's
portable [identifier] quoting stops working inside this tool, which seems acceptable:
the tool's own description advertises only :name placeholders and {table}
expansion and never mentions [identifier], a caller who wants a quoted identifier can
write the database's own syntax, and silent data corruption is a strictly worse failure than
losing an undocumented convenience. If the portable syntax is worth keeping, exposing it as a tool
parameter defaulting to true would work as well.

The { } behaviour cannot be fixed the same way and should be
documented instead: braces are reserved for table-prefix expansion, so a literal brace must be
passed as a bound argument. Both the tool description and the query
property description are currently silent about this, as they are about brackets.

Environment

  • Drupal core 11.3.14
  • drupal/ctx 1.0.0-beta7, drupal/mcp_core 1.0.0-beta12, mcp/sdk v0.8.1
  • PHP 8.4.22, PostgreSQL 17.7, empty table prefix

Not PostgreSQL-specific: identifierQuotes is " on every ANSI-quoting
driver, and the MySQL driver uses backticks outside ANSI_QUOTES mode, which produces the same class
of corruption.

Comments

chi created an issue. See original summary.

  • niklan committed 56cd59dd on 3623439-query-tool
    fix: #3623439 ctx_database_query silently rewrites [ ] { } inside SQL...

  • niklan committed 5d6aece3 on 1.x
    fix: #3623439 ctx_database_query silently rewrites [ ] { } inside SQL...
niklan’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.