🔒 Security
ssh_db_query could execute arbitrary shell commands on the remote host (#44 — thanks @technophile77)
The query builders interpolated the raw query into a double-quoted shell string (mysql -e "${query}", psql -c "${query}", mongo --eval "${query}"), so the remote shell evaluated backticks `…` and $(…)/$VAR before the database engine ever saw the query. The "SELECT-only" tool could therefore run shell commands on the target server (e.g. SELECT `id` / SELECT '$(id)').
The query is now delivered to mysql/psql/mongo on stdin via a single-quoted heredoc (<<'__MCP_SQL_EOF__'), so the remote shell never parses it — only the database engine interprets the SQL/JS. A shared buildHeredoc() helper keeps the existing awk JSON-wrapper pipe on the heredoc opening line (MySQL JSON output is byte-for-byte unchanged) and defensively rejects a delimiter collision.
🐛 Fixed
- Backtick-quoted identifiers were silently corrupted (#44) — the same shell-evaluation bug rewrote any query using backtick identifiers (hyphenated DB names, cross-database joins, reserved-word columns), returning empty/incorrect results with no error. The stdin heredoc restores them verbatim.
row_countwas off by one (#45 — thanks @technophile77) — the handler counted cosmetic output lines (the leading[of the MySQL JSON wrapper, psql header/separator lines): 1 row →2, 13 rows →14, 0 rows →2. A newcountQueryRows()derives the count from each engine's actual output structure (MySQL JSON entries, MySQL--batchlines, psql's(N rows)footer with a header/separator fallback, MongoDBprintjsondocuments).
✅ Tests
tests/test-database-query-quoting.js— query text carried verbatim on stdin for all three builders, theawkpipe stays on the heredoc opening line, delimiter-collision guard, plus a stochastic round-trip of 500 random shell-metacharacter queries.tests/test-database-row-count.js— the exact issue cases, a look-alike{"row":value inside a cell, MySQL text format, psql(N rows)/(1 row)/(0 rows)footers and the no-footer fallback, MongoDB single/multi-document counting, and empty/whitespace →0.
Full changelog: CHANGELOG.md