A local Model Context Protocol (MCP) server written in Python that connects Claude Desktop (or any MCP-compatible client) directly to a Microsoft SQL Server database.
Claude can explore your schema, execute safe read-only queries, and answer natural-language questions about your data — all without leaving the chat.
run_query— execute any read-onlySELECTorEXEC <proc>statementget_schema— introspect tables, views, columns, stored procedures, and triggersdescribe_object— drill into a single table, view, or stored procedurenl_to_sql— convert a natural-language question into SQL (LLM-pluggable, no API keys hardcoded)- Safety guard — blocks
DROP,DELETE,TRUNCATE,ALTER,INSERT,CREATE, dynamic SQL,xp_*procs,OPENROWSET, and multi-statement batches - Schema cache — introspection runs once on startup and is held in memory; reload on demand
- Structured error responses — every failure returns a clean MCP-format JSON error
mcp_sqlserver/
├── server.py # JSON-RPC 2.0 loop over stdio (MCP entry point)
├── tool_handler.py # Tool dispatcher and implementations
├── db_service.py # pyodbc connection, schema introspection, query execution
├── query_validator.py # SQL safety guard — keyword scanner + statement-type enforcer
├── config.py # All configuration in one place
└── requirements.txt # Python dependencies
| Requirement | Notes |
|---|---|
| Python 3.10+ | |
| ODBC Driver 17 or 18 for SQL Server | Must match the driver value in config.py |
pyodbc |
Installed via requirements.txt |
| Claude Desktop | Download — or any MCP-compatible client |
git clone https://github.com/YashUkhare/mcp_sqlserver.git
cd mcp_sqlserverpip install -r requirements.txtOpen config.py and edit the DB_CONFIG block:
DB_CONFIG = {
"driver": "{ODBC Driver 17 for SQL Server}",
"server": "localhost", # or "192.168.1.10" / "myserver\\SQLEXPRESS"
"port": 1433,
"database": "YourDatabaseName",
"username": "sa",
"password": "YourPassword",
...
}Other tuneable settings in config.py:
| Setting | Default | Description |
|---|---|---|
INCLUDED_SCHEMAS |
[] (all) |
Limit introspection to specific schemas |
EXCLUDED_SCHEMAS |
system schemas | Always excluded from introspection |
MAX_ROWS_RETURNED |
500 |
Cap on result rows per query |
QUERY_TIMEOUT_SEC |
30 |
Per-query execution timeout |
python server.pyYou should see on stderr:
[INFO] mcp_server: MCP SQL Server started. Waiting for messages on stdin …
Press Ctrl+C to stop.
- Open (or create)
~/.claude/claude_desktop_config.json - Add the
sqlserverentry undermcpServers:
{
"mcpServers": {
"sqlserver": {
"command": "python",
"args": ["/absolute/path/to/mcp-sqlserver/server.py"]
}
}
}Windows example:
"args": ["C:\\Users\\YourName\\projects\\mcp-sqlserver\\server.py"]
- Restart Claude Desktop.
- Click the Tools (plug) icon in the chat input bar — you should see
sqlserverlisted.
Execute a read-only T-SQL query directly.
Input:
{ "sql": "SELECT TOP 10 * FROM dbo.Orders WHERE status = 'pending'" }Output:
{
"tool": "run_query",
"executed_sql": "SELECT TOP 10 ...",
"explanation": "Read-only SELECT query executed successfully.",
"columns": ["OrderId", "CustomerId", "Status", "CreatedAt"],
"rows": [[1, 42, "pending", "2024-01-15T10:30:00"]],
"row_count": 1,
"truncated": false
}Return the cached database schema.
Input:
{ "format": "summary", "reload": false }format:"full"(structured JSON) or"summary"(compact text for LLMs)reload:trueto force a fresh introspection from the database
Describe a single table, view, or stored procedure.
Input:
{ "object_name": "dbo.Orders" }Accepts fully qualified (dbo.Orders) or bare (Orders) names, case-insensitive.
Translate a natural-language question into SQL.
Step 1 — get the prompt (no generated_sql provided):
{ "question": "Which customers placed more than 5 orders last month?" }Returns nl_prompt — a schema-aware prompt ready to send to any LLM.
Step 2 — execute after your LLM responds:
{
"question": "Which customers placed more than 5 orders last month?",
"generated_sql": "SELECT c.CustomerName, COUNT(*) AS OrderCount FROM dbo.Orders o JOIN dbo.Customers c ON o.CustomerId = c.Id WHERE o.OrderDate >= DATEADD(month, -1, GETDATE()) GROUP BY c.CustomerName HAVING COUNT(*) > 5"
}The generated SQL is fully validated by the safety guard before execution, regardless of source.
The validator blocks the following before any query touches the database:
| Category | Blocked patterns |
|---|---|
| DDL | DROP, CREATE, ALTER, TRUNCATE |
| DML writes | DELETE, INSERT, UPDATE |
| Privilege control | GRANT, REVOKE, DENY |
| Dangerous procs | xp_cmdshell, all xp_* extended procs, sp_configure, sp_executesql |
| Dynamic SQL | EXEC('...'), EXECUTE('...') string patterns |
| External access | OPENROWSET, OPENDATASOURCE, BULK INSERT |
| Multi-statement | Semicolon-separated batches |
| Comment smuggling | Comments stripped before keyword scanning |
nl_to_sql is intentionally LLM-agnostic. When called without generated_sql, it returns a filled prompt string (nl_prompt) that you can forward to any model.
Example using the Anthropic SDK on your side:
import anthropic
# 1. Call nl_to_sql to get the prompt
prompt = mcp_response["nl_prompt"]
# 2. Send to Claude
client = anthropic.Anthropic(api_key="...")
message = client.messages.create(
model="claude-opus-4-5",
max_tokens=512,
messages=[{"role": "user", "content": prompt}]
)
generated_sql = message.content[0].text.strip()
# 3. Call nl_to_sql again with the generated SQL
mcp_call("nl_to_sql", {"question": "...", "generated_sql": generated_sql})The prompt template itself lives in config.py under NL_PROMPT_TEMPLATE and can be freely customised.
pyodbc.Error: ('01000', ...) — ODBC driver version mismatch.
Update the driver field in config.py to match your installed driver (run odbcinst -q -d on Linux/macOS or check ODBC Data Sources on Windows).
Login failed for user — verify username / password in config.py and ensure the SQL Server login has at least db_datareader permission on the target database.
Claude Desktop doesn't show the tool — make sure the path in claude_desktop_config.json is an absolute path, and restart Claude Desktop after any config change.
Schema is stale — call get_schema with reload: true to force a fresh introspection.
MIT