Picture this. Someone on your team wires a database MCP server into their editor, asks it about last month's orders and gets a sensible answer in a few seconds. A week later the request lands on your desk: can we run this for the whole sales team? Finance wants one too, and the two groups mustn't see each other's data.

That second request is a different problem, and a "works with MCP" badge won't help you solve it. Two servers can both connect to the same client while putting SQL generation, planning, execution and identity checks in completely different places. A successful handshake tells you the protocol works. It doesn't tell you who can call which tool, or what stands between a model's output and your production database.

So this post compares MCP-capable NL2SQL tools by the contract each server offers and where its security boundary sits, not by client logos. The candidates are NLQueries Core, DBHub and Wren AI, with Chat2DB Community as a local-only reference point. It's based on each project's documentation and source at pinned commits. Beyond a sample SQL fixture, the only thing exercised was a local stdio session against Core 0.4.1, so anything that still needs observing is marked as a check for you to run.

The question nobody asks: who writes the SQL?

The Model Context Protocol (MCP) is the plumbing. It defines how a server advertises tools (tools/list) and how a client calls them (tools/call) (MCP tools specification, revision 2025-06-18). It says nothing about where the natural-language-to-SQL translation happens. Each server makes that call for itself, and the decision shapes almost everything else you'll evaluate.

You'll mostly meet two shapes. In the first, the server takes a question. You send "what was tenant A's paid revenue in October?", and the server builds context, generates SQL, validates it and runs it. The caller never writes SQL. In the second, the server takes SQL. The model in your IDE or desktop client writes the query, and the server's job is to plan or run it and return rows, ideally behind some read-only guardrails.

DBHub is the clearest SQL-accepting example, with execute_sql and schema search as its default tools (DBHub README). Wren AI's MCP server is SQL-accepting too, but it surrounds that with tools for reading its modeled context (Wren AI CLI reference). NLQueries Core takes the question-accepting shape: its MCP query tool takes a question and an agent ID, and the orchestration, network authentication and grants all sit behind the server (Authenticating the MCP server).

Neither shape is better in the abstract. The trap is comparing the two as if they offered the same service.

Two details from the spec are worth keeping in mind. First, the annotations a server attaches to its tools are hints, not independent enforcement. A tool that says it's read-only is making a claim, not applying a control. Second, clients and servers negotiate a protocol revision, so record the one you actually got. In the local Core session mentioned above, the MCP Python SDK 1.30.0 negotiated revision 2025-11-25, not the 2025-06-18 text cited here.

A small, honest test bed

Before looking at any product, write down what you're going to test and how you'll judge it. Otherwise the comparison drifts toward whichever tool demos best. Three tasks cover most of what matters:

1. Discovery. Connect, list the tools and look up the schema for one table.

2. A permitted query. Answer one bounded question through the tool surface the server really offers.

3. A denied request. Ask for something the caller shouldn't reach, such as a table or agent outside their grant.

For data, a tiny synthetic fixture goes a long way. This one has three tables and two tenants, and it's built to catch common mistakes. Order 101 has two line items, so a careless join to order_items will double-count it. Order 106 is paid but has a NULL amount, so revenue and order count can disagree. Order 105 falls on November 1, which tests the date boundary. Create it in SQLite before running anything else:

SQL
-- Synthetic sample fixture (SQLite syntax)
CREATE TABLE customers(id INTEGER PRIMARY KEY, tenant_id TEXT NOT NULL, region TEXT);
CREATE TABLE orders(id INTEGER PRIMARY KEY, customer_id INTEGER REFERENCES customers(id), order_date TEXT, amount_cents INTEGER, status TEXT);
CREATE TABLE order_items(id INTEGER PRIMARY KEY, order_id INTEGER REFERENCES orders(id), sku TEXT);
INSERT INTO customers VALUES(1,'A','East'),(2,'A','West'),(3,'B','North');
INSERT INTO orders VALUES
(101,1,'2026-10-01',10000,'PAID'),(102,1,'2026-10-02',5000,'CANCELLED'),
(103,2,'2026-10-31',20000,'PAID'),(104,3,'2026-10-02',70000,'PAID'),
(105,1,'2026-11-01',40000,'PAID'),(106,2,'2026-10-15',NULL,'PAID');
INSERT INTO order_items VALUES(1,101,'a'),(2,101,'b'),(3,103,'c');

The running question is for tenant A, what were paid-order revenue and paid-order count in October 2026? Here's the reference answer:

SQL
-- Reference answer for the synthetic sample fixture (SQLite syntax)
SELECT SUM(o.amount_cents) AS revenue_cents, COUNT(*) AS paid_order_count
FROM orders AS o JOIN customers AS c ON c.id = o.customer_id
WHERE c.tenant_id = 'A' AND o.status = 'PAID'
  AND o.order_date >= '2026-10-01' AND o.order_date < '2026-11-01';

Against this fixture in SQLite 3.45.3, the query returns (30000, 3): 30,000 cents across three paid orders. COUNT(*) includes the paid order with no amount, while SUM ignores it. That check proves the sample data and the expected answer line up, and nothing more. It doesn't exercise an NL2SQL tool, an MCP server or a permission system. Notice also that tenant_id = 'A' is there to make the arithmetic right. It isn't an authorization boundary, and a predicate in generated SQL shouldn't be treated as one.

To weigh the results, here's a suggested starting split. The weights are editorial judgment, not measurements, so adjust them to your situation:

Criterion

Proposed weight

Tool-contract fit

30%

Authentication and authorization

25%

Preview and execution control

20%

Schema discovery

15%

Client and transport setup

10%

Two ground rules stop a scorecard like this from lying to you. If the server will be shared over a network, authentication and authorization are a gate, not a line item, and a local-only tool can't earn its way past that gate with features. And decide your rubric before you test, keeping "unknown" separate from "failed". A blank cell isn't a zero, and a scorecard with blanks shouldn't be totalled or quietly re-weighted. That's why what follows is a decision guide rather than a ranking.

What each server actually offers

NLQueries Core

For Core 0.4.1, the tool inventory is concrete. A local stdio session against the release listed nine tools: get_agent_schema, get_cache_stats, get_query_history, health, invalidate_cache, list_agents, list_connectors, query and submit_feedback. The query tool's input schema takes question and agent_id (both required), plus an optional dialect and two booleans, explain and paraphrase.

What isn't there matters as much. There's no preview parameter and no separate preview tool. In the server source, query runs with a read-only execution policy, so it's an execution-capable tool (Core MCP server source). explain and paraphrase add information to the answer; they don't stop the SQL from running. If your design calls for someone to look at SQL before it touches the database, don't ask for that preview through this tool. Build preview on a separate generation-only surface and authorize execution of the exact statement that was approved.

On network transports, Core normally refuses to start without an identity verifier. That's either an OpenID Connect identity provider or a pre-shared static token, and once authentication is on, a grants file is required too (Authenticating the MCP server). That startup refusal was observed for the streamable HTTP transport with no verifier configured. There is an explicit escape hatch, NLQ_ALLOW_UNAUTHENTICATED_MCP=1, which lets every caller do everything. Keep it off. It's deliberately a different switch from NLQ_ALLOW_INSECURE_BIND, which governs whether the server may bind to a wildcard address. Exposure and authentication are separate decisions.

The identity choice matters for the sales-and-finance scenario. With an identity provider, each caller has their own subject. A shared static token resolves every caller to one subject, so it identifies one principal, not each salesperson. Grants are written per subject, per agent and per action, and a subject with no grant is denied. A sketch for the two teams might look like this:

YAML
# Untested sketch, modeled on the grants format documented for Core 0.4.1.
# Subjects are what your identity provider puts in sub; "sales" and
# "finance" are placeholder agent IDs for your own configured agents.
grants:
  - subject: "[email protected]"
    agents: ["sales"]
    actions: ["query:execute", "schema:read", "agents:list"]
  - subject: "[email protected]"
    agents: ["finance"]
    actions: ["query:execute", "schema:read", "agents:list"]

The same docs describe per-principal rate and concurrency limits on networked transports, and they're explicit that the counters live in the process. Run several servers behind a load balancer and the effective limit multiplies by the number of servers, so plan rate limiting accordingly. With a static token, one allowance covers everyone sharing it.

None of this is a reason to relax elsewhere. The project's hardening guide treats SQL policy and database permissions as separate safeguards with engine-specific limits, and it recommends a least-privilege database role regardless (Hardening the database NLQueries reads). Validating generated SQL tells you a statement fits the rules, not that the answer is right or access is safe. Everything here describes the Core edition. Enterprise is a separate edition and isn't covered. Remote clients, two-identity grant behaviour and third-party desktop clients haven't been exercised, so treat those as documented rather than proven.

DBHub

DBHub keeps things deliberately minimal: execute_sql and search_objects by default, with more tools if you enable them (DBHub README). Its execute_sql docs describe a configurable read-only mode that combines statement classification with engine-specific enforcement (execute_sql documentation).

Read-only is configurable, so don't assume it's on, and it's only as strong as the database role behind it. That cuts both ways: calling DBHub unsafe in general would be wrong too. Record both settings when you test. DBHub accepts caller-authored SQL. Its optional explain_sql tool can request an execution plan, but a plan isn't an approval. If someone has to approve the exact statement that runs next, that approval belongs in the calling workflow.

Wren AI

Wren AI comes at the problem from the semantic-model side, and its architecture docs separate modeled context, planning and execution into distinct components (Wren AI architecture). At the pinned revision, the MCP server is wren serve mcp, which needs the mcp extra. A client can plan the SQL it supplies with dry_plan and execute it with run_sql, and the documented tool list also includes dry_run alongside schema and knowledge tools. Starting the server with --no-connect disables the execution tools (Wren AI CLI reference).

The trust boundary is just as explicit. In this revision, the HTTP transport binds to loopback by default and has no bearer-token authentication. The docs say to treat it as local-only. That makes Wren a good fit for a modeled layer on one machine, but not, as documented, for the shared sales-and-finance server. Check the installed release's inventory before you share it, because the docs describe one revision and your install may differ.

Chat2DB Community, for the laptop case

Chat2DB Community is a reference point, not a contender for the shared server. Its security policy describes a single-user, local-first application with HTTP bound to loopback, not a shared tenant authorization service (Chat2DB Community security policy). That's reasonable, but it keeps Community off this shortlist. Chat2DB's commercial editions are separate products and aren't covered.

Here's how the four line up, from documentation and source at the pinned versions:

Tool

Who writes the SQL

Planning vs. execution

Caller identity (documented)

Not established yet

NLQueries Core 0.4.1

The server, from a question to a named agent

query executes read-only; no preview tool

Network verifier required by default (identity provider or one shared token subject); per-subject agent/action grants

Remote client handshakes, two-identity grant behaviour, Enterprise

DBHub

The calling agent

execute_sql; optional explain_sql` returns a plan, not an approval

Not established in the material reviewed

Whether read-only is enabled in your setup; per-caller grants

Wren AI

The calling agent, using modeled context tools

dry_plan / dry_run vs. run_sql; --no-connect` disables execution

None on HTTP in this revision; loopback, local-only

Installed release's tool inventory

Chat2DB Community

The local desktop application

Not applicable for a shared server

Single user, loopback

Shared-server authorization (out of scope by design)

Follow one request from client to database

For a shared assistant, the most useful exercise is to trace a single request end to end and ask two questions at each hop: where is the caller's identity established, and where is it checked?

The first hop is the transport. The client launches a stdio server itself, so the security boundary is the local process and its environment. In the local Core session, the authorization record for a list_agents call named the caller as the local process owner, which is exactly that inherited boundary. On a network transport, anyone who can reach the endpoint is a potential caller. That's why Core refuses to start one without a verifier, and why Wren's documented HTTP mode is local-only. For DBHub, the material reviewed doesn't establish a per-caller grant model, so call that unknown rather than absent until you've checked your version.

The second hop is authorization, and it's where a lot of designs get muddled. Authentication tells you who is calling. Authorization tells you what they may do. Keep both separate from SQL syntax checks: a statement can be valid and read-only and still ask for a table this person must never see. In Core, "this person can use the sales agent but not the finance agent" lives in the grants file. For a server that accepts raw SQL, the equivalent control usually comes from the database role and from which tools you expose at all. Neither replaces the other.

Checks worth running before you commit

Documentation tells you what a server claims. Only testing tells you what it does. Apart from the local Core discovery call and the startup refusal described above, none of these checks has been run, so treat them as a plan.

Tool discovery. Pin each server build and one MCP client version. Connect, record the negotiated protocol revision, call tools/list and save the tool schemas. Then do one schema lookup and the same permitted query against each server. The inventory should make it obvious where natural-language generation lives and which tools execute SQL. Any claim that rests only on a README badge fails.

Identity and execution boundaries. Use an isolated database behind a read-only role and set up two distinct subjects, one allowed and one denied. Try a valid credential, a missing one, an expired one and one issued for the wrong audience. For Core, test a denied agent and a denied action as separate cases. Don't send a "preview" request to its query tool, because that tool executes. If you build an approval step, bind the reviewer's approval to the exact SQL, parameters, caller and database context that will run, not to a description of it. The pass condition is that database logs show no denied request ever reaching execution.

Client reliability. With secrets scrubbed from the logs, restart the server mid-session, send malformed arguments, cancel a request and force a timeout. If you run replicas, check how rate limits behave across them. Protocol errors and query errors should stay distinguishable. If a server doesn't support a transport, mark it not applicable rather than scoring it zero.

When you write up the results, keep the outcomes in separate columns: did the SQL parse, did policy accept it, was the result correct, was a refusal handled well, and did it execute. Compare result sets as multisets unless ordering was part of the question, and keep NULLs and duplicates intact. The fixture above is a smoke test, not a benchmark.

So which one should you pick?

Pick the smallest tool surface that does the job, which usually isn't the one with the longest feature list.

If your IDE assistant already writes good SQL and needs schema lookup plus a deliberate execution step, a SQL-accepting server like DBHub fits, as long as you confirm the read-only configuration and the database role behind it. If your team maintains a modeled semantic layer and works on one machine, Wren's MCP server is built around that, with planning and execution as separate tools and an execution-off mode. One analyst on one laptop sits comfortably inside Chat2DB Community's documented boundary.

For the shared assistant from the opening, where each person reaches only certain databases, NLQueries Core is the only one of the four whose documentation describes per-caller identity and agent-level grants on a network transport. That matches the requirement on paper, pending the identity checks above. It needs an identity provider rather than a shared token if you want to tell salespeople apart. You'd still supply the preview and approval surface yourself, along with the reverse proxy, user provisioning and any client-specific adapters.

If you're down to two finalists, run the discovery check this week and save both tools/list outputs side by side. If the deployment is shared, run the two-subject identity check next. A server that can't pass the authentication gate isn't rescued by anything else on its feature list.

References

- [Model Context Protocol, Tools (2025-06-18)](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)

- [NLQueries, Authenticating the MCP server (Core 0.4.1)](https://github.com/nlqueries/nlqueries/blob/2fbf276bf299e0b019c41cb62c5e6969d4c33ff9/docs/mcp-authentication.md)

- [NLQueries, Connecting MCP clients (Core 0.4.1)](https://github.com/nlqueries/nlqueries/blob/2fbf276bf299e0b019c41cb62c5e6969d4c33ff9/docs/mcp-clients.md)

- [NLQueries, MCP server source (Core 0.4.1)](https://github.com/nlqueries/nlqueries/blob/2fbf276bf299e0b019c41cb62c5e6969d4c33ff9/nlqueries/mcp_server/server.py)

- [NLQueries, Hardening the database NLQueries reads (Core 0.4.1)](https://github.com/nlqueries/nlqueries/blob/2fbf276bf299e0b019c41cb62c5e6969d4c33ff9/docs/database-hardening.md)

- [DBHub README](https://github.com/bytebase/dbhub/blob/280c0ed734864e45515b38a5daa273c5372592d4/README.md)

- [DBHub execute_sql](https://dbhub.ai/tools/execute-sql)

- [Wren AI CLI reference](https://github.com/Canner/WrenAI/blob/548eee32Epo1K92Xzo6Hayq1Fmkj21x4fUk7JZT7/docs/core/reference/cli.md)

- [Wren AI architecture](https://github.com/Canner/WrenAI/blob/548eee32Epo1K92Xzo6Hayq1Fmkj21x4fUk7JZT7/docs/core/reference/architecture.md)

- [Chat2DB Community security policy](https://github.com/OtterMind/Chat2DB/blob/6a935c3d23af971d4a737cfcfbf95a35e88d0702/SECURITY.md)