Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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

  1. Start the development server with uv run mcp dev server.py, or launch MCP Inspector directly for your SDK.
  2. Confirm initialization completes and inspect the advertised tool names, descriptions, input schemas and annotations.
  3. Call each tool with valid values, then test missing fields, wrong types, unknown tables and oversized limits.
  4. Verify that permission failures and database timeouts become controlled tool errors without stack traces.
  5. Attempt injection-like strings in every text field, nonexistent tables, empty results and pagination boundaries.
  6. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Protect the HTTP endpoint

  • Configure explicit allowed_hosts and allowed_origins to prevent DNS-rebinding attacks. An incorrect host allowlist can produce 421 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.