The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Build the MCP server as a small, policy-enforcing application between an AI host and your database. Expose narrow, typed tools such as list_tables, describe_table, search_rows and approved domain operations; use parameterized SQL, an allowlist, row and time limits, a least-privilege database role, and authorization on every request. Use stdio for a local host and Streamable HTTP for a remote service.
What an MCP SQL server actually does
Model Context Protocol (MCP) is the protocol layer between an AI application and server-side capabilities. The host discovers the server’s tools, resources and prompts, then sends validated tool calls. Your server—not the model—opens the database connection, constructs the query, applies policy and returns a controlled result.
MCP does not make arbitrary SQL safe. Safety comes from your tool schemas, query construction, database permissions, authentication, authorization, limits and monitoring. Treat the model as an untrusted caller even when the host is an internal application.
Choose an SDK and transport
Python
The official Python SDK supports servers and clients over stdio, Streamable HTTP and SSE. Current documentation requires Python 3.10 or newer. Install it with:
#1 Best Overall
python -m venv .venv
. .venv/bin/activate
pip install "mcp[cli]"
TypeScript
The TypeScript v2 SDK is the documented stable line implementing the 2026-07-28 MCP specification. Its quickstart uses @modelcontextprotocol/server, serveStdio and Zod schemas. Use TypeScript when your existing service, validation and deployment stack is JavaScript-based; use Python when your data tooling and drivers are already Python-native.
Transport decision
- stdio: best for local development and desktop hosts that launch your process directly. Credentials remain in the local process environment.
- Streamable HTTP: use for a shared or hosted service. Put TLS, authentication, authorization, rate limits, host/origin protection, logging and proxy configuration around the endpoint.
Design a deliberately small SQL surface
Start from user tasks rather than from SQL syntax. A read-only baseline can contain these tools:
list_tables()returns only approved tables.describe_table(table)returns approved column names, types and safe descriptions.search_rows(table, filters, limit)accepts structured filters, not SQL text.aggregate(table, metric, group_by, filters)accepts allowlisted metrics and fields.
If the application needs writes, expose explicit operations such as create_customer or update_order_status. Validate every field and mark destructive behavior accurately. Avoid a general execute_sql tool unless you have a compelling, separately controlled administrative use case.
Controls every query should have
- Allowlist table, column, sort and aggregate names; never interpolate caller-provided identifiers.
- Bind values as parameters through the driver.
- Impose a maximum page size, statement timeout and response size.
- Paginate deterministic results instead of returning an unbounded result set.
- Return only required columns, excluding secrets and unnecessary personal data.
- Use a database role with only the permissions the tools need.
- Keep transaction boundaries in the server process and roll back failed writes.
- Return structured, user-safe errors; do not expose stack traces, credentials or connection strings.
Build a read-only Python server
The following example uses SQLite so it can run without a separate database service. The same policy pattern applies to PostgreSQL, MySQL or SQL Server after replacing the driver and connection code.
Windows 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 reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutefrom __future__ import annotations
import os
import sqlite3
from typing import Any
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("safe-sql")
DB_PATH = os.environ.get("DB_PATH", "app.db")
ALLOWED_TABLES = {"customers", "orders"}
MAX_LIMIT = 100
def connect() -> sqlite3.Connection:
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA query_only = ON")
return connection
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 approved for read-only access."""
return sorted(ALLOWED_TABLES)
@mcp.tool()
def describe_table(table: str) -> list[dict[str, Any]]:
"""Return column metadata for an approved table."""
table = checked_table(table)
with connect() as db:
rows = db.execute(f"PRAGMA table_info({table})").fetchall()
return [
{"name": row[1], "type": row[2], "nullable": not bool(row[3])}
for row in rows
]
@mcp.tool()
def search_rows(
table: str,
customer_id: int | None = None,
status: str | None = None,
limit: int = 25,
offset: int = 0,
) -> list[dict[str, Any]]:
"""Search approved rows with optional structured filters."""
table = checked_table(table)
if not 1 <= limit <= MAX_LIMIT:
raise ValueError(f"limit must be between 1 and {MAX_LIMIT}")
if offset < 0:
raise ValueError("offset must not be negative")
clauses: list[str] = []
values: list[Any] = []
if customer_id is not None:
clauses.append("customer_id = ?")
values.append(customer_id)
if status is not None:
clauses.append("status = ?")
values.append(status)
where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
sql = f"SELECT * FROM {table}{where} ORDER BY rowid LIMIT ? OFFSET ?"
values.extend([limit, offset])
with connect() as db:
rows = db.execute(sql, values).fetchall()
return [dict(row) for row in rows]
if __name__ == "__main__":
mcp.run()
Only values are bound as parameters; the table identifier is selected from ALLOWED_TABLES before it is interpolated. For PostgreSQL, use a driver such as psycopg, a connection pool and server-side statement timeouts. For MySQL or SQL Server, use the corresponding parameter style and preserve the same allowlist and permission checks.
Run it locally
- Create the file as
server.pyand setDB_PATHif the database is notapp.db. - Start the development server with
uv run mcp dev server.py(after installing uv and the project dependencies), or runpython server.pywhen your host launches stdio directly. - Connect your MCP host to the process and verify that only the three declared tools appear.
Equivalent TypeScript server
This TypeScript example follows the v2 SDK style: Zod defines the input schema and the SDK validates a call before the handler executes. Replace the database adapter with your production driver’s pool.
import { McpServer, serveStdio } from "@modelcontextprotocol/server";
import { z } from "zod";
import Database from "better-sqlite3";
const db = new Database(process.env.DB_PATH ?? "app.db", { readonly: true });
const allowed = new Set(["customers", "orders"]);
const maxLimit = 100;
const server = new McpServer({ name: "safe-sql", version: "1.0.0" });
server.tool("list_tables", "List approved tables", {}, async () => ({
content: [{ type: "text", text: JSON.stringify([...allowed].sort()) }]
}));
server.tool(
"search_rows",
"Search approved rows with structured filters",
{
table: z.string(),
customerId: z.number().int().optional(),
status: z.string().optional(),
limit: z.number().int().min(1).max(maxLimit).default(25),
offset: z.number().int().min(0).default(0)
},
async ({ table, customerId, status, limit, offset }) => {
if (!allowed.has(table)) throw new Error("table is not available");
const clauses: string[] = [];
const values: unknown[] = [];
if (customerId !== undefined) { clauses.push("customer_id = ?"); values.push(customerId); }
if (status !== undefined) { clauses.push("status = ?"); values.push(status); }
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
const sql = `SELECT * FROM ${table}${where} ORDER BY rowid LIMIT ? OFFSET ?`;
const rows = db.prepare(sql).all(...values, limit, offset);
return { content: [{ type: "text", text: JSON.stringify(rows) }] };
}
);
await serveStdio(server);
Pin compatible package versions, keep the database object in the server process, and use a pool with bounded connections for a network database. In either language, a schema validator is not an authorization layer: the handler still has to check identity, permissions and row scope.
Authenticate and authorize every call
Authorization belongs in the MCP server and must not be delegated to the model. Authenticate the caller at the transport boundary, map its identity to a policy or database role, and apply that identity to every query. A sales user might be restricted to rows for an assigned region; an administrator may have additional write tools.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Mark read-only tools with an accurate readOnlyHint: true annotation where your SDK supports it. Destructive tools need an accurate destructive annotation and an explicit confirmation policy in the host. Log the tool name, principal, duration, row count and outcome, while redacting tokens, personal data and query values that could contain secrets.
Expose Streamable HTTP safely
For a remote deployment, serve a stable HTTPS endpoint behind your normal ingress. Require authentication, configure rate limits and preserve the authorization context through the proxy. Set explicit allowed_hosts and allowed_origins values for DNS-rebinding protection. A missing or incorrect host allowlist can produce 421 Invalid Host header. If TLS terminates at a proxy, configure forwarded headers so generated redirects remain HTTPS.
Choose infrastructure according to runtime dependencies, streaming behavior, latency, data residency, secret management and rollback requirements. Keep the database private where possible and expose only the MCP endpoint. Add health checks, structured logs, latency and error metrics, and a way to revoke credentials without redeploying the application.
Test with MCP Inspector before production
- Launch
uv run mcp dev server.pyor start MCP Inspector directly. - Confirm initialization and inspect the advertised tool names, descriptions, schemas and annotations.
- Call every tool with normal values, missing values and boundary values.
- Verify that nonexistent tables, oversized limits, negative offsets and malformed filters fail before a query runs.
- Try injection-like strings in every text field and confirm they remain bound values.
- Test empty results, database timeouts, permission failures and a write attempt through a read-only tool.
- Check that errors contain useful messages but no SQL credentials or stack traces.
- Repeat the authorization tests with two identities and confirm row-level scope is enforced.
Inspector confirms protocol behavior; it does not prove that your database policy is correct. Keep the SQL and authorization tests in your normal automated test suite.
Performance, reliability and cost controls
- Use a connection pool sized for the database, not for the number of model requests. Cap concurrent tool calls.
- Set a database statement timeout and an MCP request deadline. Cancel work when the caller disconnects.
- Return pages and aggregates instead of thousands of rows. Include a continuation token when offset pagination becomes expensive.
- Cache stable metadata such as table descriptions, but avoid caching identity-sensitive rows unless the cache key includes the authorization scope.
- Retry only transient connection failures, and never blindly retry a non-idempotent write.
- Measure query duration, returned row count, rejected calls and timeout rate. No independent performance or cost benchmark is established by the official material; size the service with your own workload.
Hand-built server or Microsoft SQL MCP Server?
| Decision axis | Hand-built SDK server | Microsoft SQL MCP Server |
|---|---|---|
| Control | Define exactly the tools, queries and policies your application needs. | Use a prebuilt SQL-focused surface based on Data API builder. |
| Database scope | Narrow domain operations for one application or workflow. | Generalized typed CRUD over configured entities. |
| Security model | You own authentication, authorization, allowlists, auditing and limits. | Provides documented RBAC capabilities through the Data API builder model. |
| Operations | You manage the runtime, observability and deployment. | Documentation includes local and Azure Container Apps deployment paths, caching and telemetry. |
| Portability | Python or TypeScript and any MCP host; choose your database driver. | Best fit for teams already centered on Microsoft SQL and Azure. |
Microsoft documents six typed DML tools with RBAC for its server. Choose it when that entity abstraction and Azure-oriented operation match your environment; choose a custom server when the model should see a smaller, domain-specific contract.
Troubleshooting
The host cannot start the server
Check that the host launches the correct virtual environment or compiled JavaScript entry point, that Python is 3.10 or newer, and that database credentials are available to the process. Run the command manually and inspect stderr before involving the host.
The host shows no tools
Confirm the process completed MCP initialization and that each tool decorator or registration executes at startup. A syntax error, import failure or database connection made too early can terminate registration. Move expensive work into the handler or a controlled startup check.
Rank #4
Every HTTP request returns 421
The hostname is not in the server’s allowed-host list, or the proxy is forwarding a different host. Add only the real service names and configure forwarded headers correctly; do not disable host protection broadly.
Queries are slow or time out
Lower the maximum page size, add indexes for approved filters, set a statement timeout and inspect the database plan. Do not solve timeouts by allowing unlimited rows or by increasing every timeout indefinitely.
A legitimate user receives a permission error
Log the principal, tool and policy decision, then verify the mapped database role and row scope. Keep the database role least-privileged and grant only the missing operation, not blanket write access.
A model attempts destructive SQL
Do not add a hidden escape hatch. Remove unrestricted SQL tools, expose an explicit domain operation with validation and confirmation, and ensure the database account cannot perform unapproved destructive statements.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If you need a clean image or PDF of your MCP documentation or an admin page, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP or PDF; consent banners are accepted and 60-plus known consent platforms, newsletter popups and chat widgets are removed before capture. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. AI agents can call its take_screenshot, get_page_info and capture_pdf tools through MCP.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsExample cURL call (see the ScreenshotNeo API documentation):
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://screenshotneo.com/docs/ -o shot.webp
There is a free plan with 1,000 screenshots per month and no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can an MCP host connect directly to my database without a server?
No. The MCP server is the boundary that owns the driver connection, query policy, authorization and result shaping; the host communicates with that server through MCP.
Should I expose database schema as MCP resources?
Only expose metadata that the caller is allowed to see. A resource can complement typed tools, but it does not replace per-call authorization or column and row filtering.
When is SSE preferable to Streamable HTTP?
Use the transport supported by your SDK and host; for new remote deployments, the documented Streamable HTTP path is the general choice, while stdio remains simpler for local processes.
How do I handle schema migrations?
Version your tool contract, deploy additive database changes first, then switch callers and remove obsolete fields after they are no longer used. Keep migrations outside model-generated tool calls.
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.

