Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Build the server as a narrow, typed API over your database—not as a chatbot-controlled execute_sql endpoint. An MCP server publishes tools, resources and prompts; an MCP host discovers those capabilities and calls them. For a local integration, use stdio. For a shared service, use Streamable HTTP with authentication, authorization, host/origin checks, rate limits and logging.
This guide builds a read-only SQL surface, shows Python and TypeScript implementations, and covers permissions, testing, deployment and failure handling. The examples use SQLite so they run without assuming a particular commercial database; replace the driver and SQL dialect only after checking the behavior of your target engine.
What an MCP SQL server actually does
MCP is the protocol layer between an AI host and server-side capabilities. The model does not receive a database connection. Instead, the host initializes your server, discovers its declared tools and sends validated tool calls. Your process then authenticates the caller, checks authorization, constructs a bounded query and returns structured data.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
A safe design has four boundaries:
- Protocol boundary: MCP framing, initialization and tool schemas.
- Application boundary: allowlists, validation, pagination and business rules.
- Database boundary: a least-privilege role, statement timeouts and transaction policy.
- Operations boundary: authentication, logs, metrics, secret management and network controls.
MCP itself does not make arbitrary SQL safe. Safety comes from the code and database policy around the protocol.
#1 Best Overall
Choose an SDK and transport
| Choice | When it fits | Important details |
|---|---|---|
| Python SDK | Fast development, data-heavy services and teams already using Python | Current documentation requires Python 3.10+. The package can be installed with pip install "mcp[cli]" or uv add "mcp[cli]". |
| TypeScript SDK v2 | Node.js services and strongly typed JavaScript stacks | The documented stable line implements the 2026-07-28 MCP specification and uses Zod schemas for validation. |
| stdio | Desktop hosts and local development | The host launches your process directly. Keep protocol output on stdout; send diagnostics to stderr. |
| Streamable HTTP | Shared, containerized or hosted deployments | Add authentication, authorization, rate limits, TLS, host/origin protection and proxy configuration. |
Design a narrow SQL tool surface
Start with the questions users need answered, then expose one tool per safe operation. A useful read-only baseline is:
list_tables()— returns only approved tables.describe_table(table)— returns columns and safe descriptions.search_rows(table, filters, limit)— accepts structured filters, not SQL text.aggregate(table, metric, group_by, filters)— allows only approved metrics and fields.
If writes are necessary, use domain operations such as create_customer or update_order_status. Validate every field, enforce the caller’s authorization and label destructive behavior accurately. Do not hide an update or delete behind a tool named “search.”
Controls every query should have
- Allowlisted table and column names. Identifiers cannot be parameterized, so select them from server-owned maps.
- Parameterized values for every user-provided value.
- A hard maximum row count, pagination and a statement timeout.
- A database account with only the required permissions.
- Redaction of credentials, connection strings and unnecessary sensitive columns.
- Structured results and controlled errors instead of stack traces.
Build it in Python
1. Create the project
mkdir sql-mcp
cd sql-mcp
python -m venv .venv
. .venv/bin/activate
pip install "mcp[cli]"
Python 3.10 or newer is required by the current SDK documentation. Set DATABASE_PATH to an existing SQLite file, or change the connection function to use your database driver’s pool.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minute2. Implement typed, read-only tools
from __future__ import annotations
import os
import sqlite3
from typing import Any
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("safe-sql")
# Keep this map in application code, not in model-provided input.
ALLOWED_TABLES = {"customers", "orders"}
def connect() -> sqlite3.Connection:
path = os.environ.get("DATABASE_PATH", "app.db")
db = sqlite3.connect(path)
db.row_factory = sqlite3.Row
return db
def checked_table(table: str) -> str:
if table not in ALLOWED_TABLES:
raise ValueError("Table is not available through this server")
return table
@mcp.tool()
def list_tables() -> list[str]:
"""List tables that this MCP server intentionally exposes."""
with connect() as db:
existing = {
row[0]
for row in db.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
)
}
return sorted(ALLOWED_TABLES & existing)
@mcp.tool()
def describe_table(table: str) -> dict[str, Any]:
"""Return column metadata for one approved table."""
table = checked_table(table)
with connect() as db:
columns = [dict(row) for row in db.execute(f"PRAGMA table_info({table})")]
return {"table": table, "columns": columns}
@mcp.tool()
def search_rows(
table: str,
customer_id: int | None = None,
limit: int = 50,
offset: int = 0,
) -> dict[str, Any]:
"""Read bounded rows using structured filters; never accepts SQL text."""
table = checked_table(table)
if not 1 <= limit <= 100:
raise ValueError("limit must be between 1 and 100")
if offset < 0:
raise ValueError("offset cannot be negative")
where = ""
params: list[Any] = []
if customer_id is not None:
where = " WHERE customer_id = ?"
params.append(customer_id)
sql = f"SELECT * FROM {table}{where} LIMIT ? OFFSET ?"
params.extend([limit, offset])
with connect() as db:
rows = [dict(row) for row in db.execute(sql, params)]
return {"table": table, "rows": rows, "limit": limit, "offset": offset}
if __name__ == "__main__":
# stdio is the right starting transport for a local MCP host.
mcp.run()
The table name is interpolated only after an allowlist check; values remain bound parameters. For PostgreSQL, MySQL or SQL Server, replace SQLite’s connection and placeholder syntax with the selected driver’s parameterized API. Do not assume identical timeout, quoting or transaction behavior across engines.
3. Add production policies
Move credentials to a secret manager or environment injection, create a role that can read only the approved tables and columns, and set a database-side statement timeout. Add pagination tokens when offsets become expensive. Keep connection pooling in the server process and define transaction boundaries explicitly. A read tool should not silently open a write transaction.
Rank #2
Build the same surface in TypeScript
The TypeScript v2 quickstart uses @modelcontextprotocol/server, serveStdio and Zod. Install the SDK, Zod and a database driver appropriate for your engine. This example uses better-sqlite3 for a local file.
npm init -y
npm install @modelcontextprotocol/server zod better-sqlite3
npm install -D typescript tsx @types/better-sqlite3
import Database from "better-sqlite3";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
const db = new Database(process.env.DATABASE_PATH ?? "app.db", { readonly: true });
const allowed = new Set(["customers", "orders"]);
const tableName = z.enum(["customers", "orders"]);
const server = new McpServer({ name: "safe-sql", version: "1.0.0" });
server.registerTool(
"list_tables",
{
title: "List approved tables",
description: "List tables exposed by this server",
inputSchema: {},
annotations: { readOnlyHint: true }
},
async () => {
const rows = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table'"
).all() as Array<{ name: string }>;
const names = rows.map(r => r.name).filter(name => allowed.has(name));
return { content: [{ type: "text", text: JSON.stringify(names.sort()) }] };
}
);
server.registerTool(
"search_rows",
{
title: "Search rows",
description: "Return bounded rows from an approved table",
inputSchema: {
table: tableName,
customer_id: z.number().int().optional(),
limit: z.number().int().min(1).max(100).default(50),
offset: z.number().int().min(0).default(0)
},
annotations: { readOnlyHint: true }
},
async ({ table, customer_id, limit = 50, offset = 0 }) => {
const sql = customer_id === undefined
? `SELECT * FROM ${table} LIMIT ? OFFSET ?`
: `SELECT * FROM ${table} WHERE customer_id = ? LIMIT ? OFFSET ?`;
const params = customer_id === undefined
? [limit, offset]
: [customer_id, limit, offset];
const rows = db.prepare(sql).all(...params);
return { content: [{ type: "text", text: JSON.stringify({ table, rows }) }] };
}
);
await serveStdio(server);
Pin the SDK version you deploy and follow that version’s API reference. The important properties are the explicit Zod schema, allowlisted identifiers, bound values and read-only annotation; the SDK validates a call before the handler runs.
Authenticate and authorize every call
Authorization belongs in the server, never in the model’s instructions. For each request, authenticate the caller, map the identity to a database role or policy and apply that identity’s row and column scope in every query. A tool that returns orders should not rely on the model to remember a tenant filter.
For write tools, check permission immediately before the operation, validate state transitions and return an idempotency result where retries are possible. Log the tool name, principal, duration, row count and outcome while redacting tokens, personal data and query values that could contain secrets.
Run and test with MCP Inspector
- Start the development server with
uv run mcp dev server.py, or launch MCP Inspector directly for your SDK. - Confirm initialization completes and inspect the advertised tool names, descriptions, input schemas and annotations.
- Call each tool with valid values, then test missing fields, wrong types, unknown tables and oversized limits.
- Verify that permission failures and database timeouts become controlled tool errors without stack traces.
- Attempt injection-like strings in every text field, nonexistent tables, empty results and pagination boundaries.
- Try a write through a read-only tool and confirm that no mutation is possible.
Inspector confirms protocol behavior; it does not replace database integration tests, migration tests or an authorization review.
Deploy over Streamable HTTP
Use stdio when a local host launches the process. A remote service should expose a stable HTTPS Streamable HTTP endpoint behind a proxy. Require authentication, enforce per-identity authorization and rate limits, and collect request and database metrics.
Protect the HTTP endpoint
- Configure explicit
allowed_hostsandallowed_originsto prevent DNS-rebinding attacks. An incorrect host allowlist can produce421 Invalid Host header. - At a TLS-terminating proxy, forward the original scheme and host so redirects remain HTTPS.
- Keep the MCP endpoint off the public network unless the authentication and authorization path is complete.
- Set request, database and response-size limits independently.
- Plan secret rotation, backups, rollback and data residency before production launch.
Or skip the browser setup
If you need clean screenshots of an MCP dashboard, schema page or documentation example, ScreenshotNeo provides a one-request capture API. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result.
Use the API details in the ScreenshotNeo documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://freedom251.com -o shot.webp
It also offers an MCP server with take_screenshot, get_page_info and capture_pdf tools, so Claude, Cursor and other MCP clients can request captures. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Hand-built server versus Microsoft SQL MCP Server
| Axis | Custom Python or TypeScript server | Microsoft SQL MCP Server |
|---|---|---|
| Control | You define every tool, field, query policy and response. | Prebuilt entity-oriented operations through Data API builder. |
| Database scope | A narrowly designed surface for one application or domain. | Generalized typed CRUD for supported SQL scenarios. |
| Security model | You own authentication, authorization, allowlists, auditing and limits. | Uses Data API builder capabilities and role-based access control. |
| Operations | You manage runtime, upgrades, logs and deployment. | Documentation includes local and Azure Container Apps deployment paths. |
| Portability | Python or TypeScript can run with any MCP host and your chosen infrastructure. | Best fit for teams already centered on Microsoft and Azure tooling. |
Choose the custom route when business-specific operations and least-privilege control matter more than setup speed. Consider Microsoft’s documented server when typed CRUD, RBAC, caching, telemetry and Azure-oriented deployment match your requirements.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallTroubleshooting common failures
The host cannot initialize the server
Check that the command points to the active virtual environment or compiled Node entry point. For stdio, remove logging from stdout; protocol messages must be the only stdout output. Send diagnostics to stderr.
A tool is missing or has the wrong schema
Restart the host after changing registrations, inspect the tool list in Inspector and verify that type hints or Zod definitions match the handler. A schema change requires updating callers and tests.
“Table is not available” appears for a real table
The allowlist intentionally controls exposure. Add the table in server code, confirm the database identity can read it and restart the process. Never accept an arbitrary table name just to remove this error.
Queries time out or return too many rows
Lower the server-side maximum, require pagination, add indexes for approved filters and configure a database statement timeout. Do not solve latency by allowing larger unbounded queries.
HTTP returns 421 Invalid Host header
Add the public DNS name to the server’s allowed_hosts configuration and ensure the proxy forwards the original host. Keep the list explicit rather than allowing every host.
A write happened when a read was expected
Separate read and write tools, use a read-only database role for read tools and mark annotations accurately. Review the query builder and transaction code; never depend on a prompt warning to prevent mutation.
Best Value
FAQ
Can I expose one unrestricted execute_sql tool for flexibility?
That design gives the model control over identifiers, joins, mutations and resource usage. Prefer allowlisted, typed tools; if a controlled query language is unavoidable, parse it into an approved abstract syntax tree and enforce the same limits.
Does the server need to keep a database connection open?
It needs a connection strategy, not necessarily one permanent connection. A pool managed by the server process is typical; close or recycle connections according to your driver’s guidance and define transaction scope per operation.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →How should I handle tenant isolation?
Derive the tenant from the authenticated principal, then apply it in server-side query construction or database row-level security. Never accept a tenant identifier from the model as the authority.
Which transport should a desktop AI client use?
Use stdio when the client launches the server locally. Use authenticated Streamable HTTP when multiple users or machines must reach one service.
Frequently Asked Questions
Can I expose one unrestricted execute_sql tool?
It is unsafe because it gives the model control over identifiers, mutations and resource usage. Use narrow, typed tools with allowlists and limits.
Does MCP provide database security automatically?
No. Security comes from your schemas, authorization code, database permissions, parameterization, limits and monitoring.
Recommended Free Tools
How do I isolate tenants?
Derive tenant scope from the authenticated identity and enforce it server-side or with database row-level security.
Which transport is appropriate for a desktop host?
Use stdio when the host launches the server locally; use authenticated Streamable HTTP for shared deployments.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

