SQL MCP
Allows running parameterized read-only or read-write SQL queries against MariaDB databases via the MySQL engine, with typed parameters, result limits, and pooled connections.
Allows running parameterized read-only or read-write SQL queries against MySQL databases, with typed parameters, result limits, and pooled connections.
Allows running parameterized read-only or read-write SQL queries against PostgreSQL databases, with typed parameters, result limits, and pooled connections.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@SQL MCPCreate a tool to fetch recent orders for a customer"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
SQL MCP
A self-hosted MCP server that turns SQL queries you define into tools Claude can call. Works with MySQL / MariaDB, PostgreSQL and Microsoft SQL Server.
Define each tool as JSON: the query, its typed parameters, and a description for Claude
Manage connections and tools in a web admin UI, or edit the JSON files directly (changes reload automatically)
Values are always sent as bound parameters, never pasted into SQL
Per-tool read-only or read-write mode
One Docker image, API-key auth, Streamable HTTP transport
Claude ──HTTPS + API key──► /mcp ─┐
You ──browser + login ──► / ├─ sql-mcp ──► MySQL / PostgreSQL / SQL Server
/api ─┘ │
/data/{connections,tools}/*.json
/data/sqlmcp.db (admin users)How it works
Full workflow
From first start, through setting up connections and tools in the admin UI, to Claude calling a tool:
flowchart TD
subgraph setup["1. First start"]
A["docker run / npm start"] --> B["Run migrations on DATA_DIR/sqlmcp.db"]
B --> C{"Any users yet?"}
C -- no --> D["Seed admin / admin<br/>(password change required)"]
C -- yes --> E["Keep existing users"]
end
subgraph admin["2. Admin UI (browser → /api)"]
F["Sign in"] --> G{"Must change<br/>password?"}
G -- yes --> H["Set new password<br/>(API returns 403 until done)"]
H --> I["Admin UI"]
G -- no --> I
I --> J["Add connection<br/>(password encrypted)"]
I --> L["Create tool<br/>(SQL + typed parameters)"]
J --> K["Test connection"]
L --> M["Test run"]
end
subgraph disk["3. Config on disk (DATA_DIR)"]
N[("connections/*.json")]
O[("tools/*.json")]
end
subgraph mcp["4. Claude (→ /mcp)"]
P["Connect with Bearer API key"] --> Q["tools/list"]
Q --> R["tools/call"]
R --> S{"Valid key and<br/>arguments?"}
S -- no --> T["Error returned<br/>(database not touched)"]
S -- yes --> U["Compile :name placeholders<br/>to ? / $1 / @name"]
end
subgraph db["5. Your database"]
V["Connection pool<br/>(created on first use)"] --> W[("MySQL / PostgreSQL / SQL Server")]
W --> X["Rows, or affectedRows for write tools"]
end
D --> F
E --> F
J --> N
L --> O
N -. "reloaded within ~1s" .-> Q
O -. "list_changed sent to Claude" .-> Q
K -. "temporary connection" .-> W
M --> U
U --> VWhen is the database connection opened?
Saving a connection in the UI only writes a JSON file. The real connection is opened when it's first needed:
sequenceDiagram
participant UI as Admin UI
participant S as sql-mcp
participant F as connections/*.json
participant DB as Database
participant C as Claude
UI->>S: Test connection
S->>DB: Open temporary connection, SELECT 1
DB-->>S: OK
S-->>UI: "Connection succeeded" (connection closed)
UI->>S: Save connection
S->>F: Write JSON (password encrypted)
Note over S,DB: No connection is held open yet
C->>S: tools/call get_customer_orders
S->>DB: Create pool on first use, run query
DB-->>S: Rows
S-->>C: Result
C->>S: tools/call (again)
S->>DB: Reuse pooled connection
UI->>S: Edit or delete connection
S->>DB: Close old pool (a new one opens on the next call)Read tools run in a read-only transaction (MySQL, PostgreSQL). On SQL Server they run in a transaction that is always rolled back.
A test run in the UI goes through exactly the same path as a call from Claude.
On restart, pools are recreated lazily. Nothing connects to your databases until a tool is used.
Related MCP server: MCP Database Server
Quick start (Docker)
docker run -d --name sql-mcp -p 3000:3000 \
-v sqlmcp-data:/data \
-e API_KEY="$(openssl rand -hex 32)" \
-e SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/devimfaheem/relationaldb-mcp:latestOpen http://localhost:3000 and sign in with admin / admin. You'll be asked to choose a new password straight away.
Then add a connection and create a tool.
Run docker exec sql-mcp printenv API_KEY to get the key Claude will use.
The image is published when a
v*tag is pushed. You can also build it yourself:docker build -t relationaldb-mcp .
Try the demo stack
The repo includes a compose file with sample MySQL and PostgreSQL databases (and optionally SQL Server) plus example tools:
git clone https://github.com/devimfaheem/relationaldb-mcp.git && cd relationaldb-mcp
cp .env.example .env # fill in API_KEY and SECRET_KEY
docker compose up --build # add --profile mssql to include SQL ServerConnect Claude
The MCP endpoint is https://your-host/mcp. Clients authenticate with Authorization: Bearer <API_KEY>.
Claude Code
claude mcp add --transport http sql https://your-host/mcp --header "Authorization: Bearer YOUR_API_KEY"Claude Desktop (claude_desktop_config.json, using mcp-remote)
{
"mcpServers": {
"sql": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-host/mcp", "--header", "Authorization:${AUTH_HEADER}"],
"env": { "AUTH_HEADER": "Bearer YOUR_API_KEY" }
}
}
}claude.ai custom connectors can't send custom headers. Set ALLOW_QUERY_KEY=true and use
https://your-host/mcp?key=YOUR_API_KEY as the connector URL. The key is then part of the URL and can end up in
proxy and access logs, so only do this over HTTPS and rotate the key if it leaks.
Defining tools
Each tool is a file at /data/tools/<name>.json, and each connection is a file at /data/connections/<id>.json.
The admin UI writes these files for you. You can also create or edit them by hand, or keep them in git.
Tool
{
"name": "get_customer_orders",
"description": "Returns a customer's orders, newest first. Optionally filter by status.",
"connection": "sales_db",
"mode": "read",
"enabled": true,
"query": "SELECT id, total, status FROM orders WHERE customer_id = :customer_id AND (:status IS NULL OR status = :status) ORDER BY created_at DESC LIMIT :limit",
"parameters": [
{ "name": "customer_id", "type": "integer", "required": true, "description": "Customer ID" },
{ "name": "status", "type": "string", "enum": ["pending", "shipped", "cancelled"], "description": "Filter by status" },
{ "name": "limit", "type": "integer", "default": 50, "min": 1, "max": 500, "description": "Max rows" }
],
"limits": { "maxRows": 500, "timeoutMs": 15000 }
}Field | Required | Notes |
| yes | Lowercase letters, digits and |
| yes | Shown to Claude. Say what the tool returns and when to use it. |
| yes | The |
| yes |
|
| no | Default |
| yes | SQL with |
| no | See below. Every placeholder needs a parameter, and every parameter must be used in the query. |
| no | Default 1000 (max 10000). Results beyond this are cut off and flagged |
| no | Default 30000. |
Parameters: name, type (string, integer, number, boolean, date, datetime), description, and optionally
required, default, enum, min/max (numbers), pattern/maxLength (strings). An optional parameter that isn't
supplied and has no default is bound as NULL, which makes (:x IS NULL OR col = :x) filters work.
Placeholders are converted to each engine's native parameters (? for MySQL, $1 for PostgreSQL, @name for SQL Server).
:: casts, string literals, quoted identifiers and comments are left alone.
PostgreSQL tip: when an optional parameter is only compared with
IS NULL, PostgreSQL can't infer its type. Add a cast, for example:country::text IS NULL.
Connection
{
"id": "sales_db",
"engine": "postgres",
"host": "db.example.com",
"port": 5432,
"database": "sales",
"user": "readonly_user",
"password": "${SALES_DB_PASSWORD}",
"ssl": { "enabled": true, "rejectUnauthorized": true },
"pool": { "max": 10 },
"options": {}
}engine:mysql(also covers MariaDB),postgresormssql. The default ports are 3306, 5432 and 1433.passwordcan be:an encrypted
enc:v1:…value (the UI encrypts passwords withSECRET_KEY)a
${ENV_VAR}reference, resolved from the container's environmentplain text (not recommended)
useralso accepts${ENV_VAR}.optionsis passed through to the driver. For SQL Server, for example:{ "instanceName": "SQLEXPRESS" }.
Configuration
Variable | Required | Default | |
| yes | Key Claude uses to call | |
| yes | Encrypts stored passwords and signs sessions. At least 32 characters. If you change it, stored passwords can no longer be decrypted. | |
|
| ||
|
| Where connection and tool JSON files and the | |
|
| Accept the key as | |
|
| Set to | |
|
|
Admin login and the app database
On first start the server creates DATA_DIR/sqlmcp.db, a small SQLite database for admin users, and runs its migrations
(src/server/migrations.ts). The first migration creates the users table, and the second seeds an admin / admin
login that must be changed before anything else can be done in the UI. Migrations are tracked in a schema_migrations
table, so restarts never re-seed or overwrite your password.
Forgot the password? Reset it to admin / admin (you'll be asked to change it again on next sign-in):
docker exec sql-mcp node dist/server/reset-admin.js # Docker
npm run reset-admin # from source (uses DATA_DIR)Back up /data to keep your tools, connections and login.
Security
Use a least-privilege database user. For read-only tools, give the user
SELECTonly.Read-only mode is enforced with
START TRANSACTION READ ONLY(MySQL) andBEGIN READ ONLY(PostgreSQL). SQL Server has no read-only transaction mode, so read tools run in a transaction that is always rolled back. A read-only login is still recommended.Claude can only run the queries you define. There is no "run arbitrary SQL" tool.
Put the server behind HTTPS, for example with Caddy:
caddy reverse-proxy --from your-host --to localhost:3000. Also setTRUST_PROXY=true.Failed API key and login attempts are rate-limited per IP.
When mounting a host directory as
/dataon Linux, make sure it's writable by UID 1000, which is the container'snodeuser.
Development
npm install
cp .env.example .env # set DATA_DIR=./data for local runs
npm run build && node --env-file=.env --disable-warning=ExperimentalWarning dist/server/index.js
npm run dev:ui # UI with hot reload on :5173 (proxies /api to :3000)
npm testProject layout: src/shared holds the JSON schemas and placeholder compiler, src/server the Fastify server, MCP,
admin API and drivers, and ui/ the React admin UI.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Query 40 databases from Claude, ChatGPT, or Cursor — on any device. Read-only, encrypted, audited.
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
- dataOAuthco.thinair
PostgreSQL, MySQL, and SQL Server in one session. 26 read-only MCP tools for AI agents.
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceProvides Claude Desktop with secure access to multiple database connections, allowing users to query MySQL, PostgreSQL, SQLite, and SQL Server databases directly through natural language.-
- AlicenseNot gradedqualityDmaintenanceProvides Claude with direct access to databases including SQLite, SQL Server, PostgreSQL, and MySQL, enabling execution of SQL queries and table management through natural language.997 npm1MIT
- AlicenseNot gradedqualityDmaintenanceEnables Claude to connect to and interact with SQLite, SQL Server, PostgreSQL, and MySQL databases through natural language. Supports executing queries, managing tables, exporting data, and storing business insights with authentication options including AWS IAM.997 npmMIT
- AlicenseAqualityBmaintenanceEnables Claude Desktop to execute read-only SQL queries on MySQL databases via natural language, with dynamic connection switching and built-in security.3153 npm7MIT