Skip to main content
Glama
gmines

SQL MCP Server

by gmines

SQL MCP Server

A Python MCP server that lets you query a SQL Server database in plain English through Claude. You ask a question, Claude figures out the SQL, runs it, and gives you an answer. No query writing, no copy-pasting schemas into chat.


Design Concept

Why not just paste the schema?

The obvious approach is to dump your schema into the conversation and ask Claude to write queries. It works, but it falls apart on any real database: large schemas eat the context window, you repeat the process every session, and you still have to copy the generated SQL somewhere to actually run it.

MCP fixes all of that. The Model Context Protocol is an open standard from Anthropic that lets Claude connect to external tools and data sources during a conversation. The server exposes what it can do; Claude decides when to call it. The user just asks questions.

What this server exposes

Three tools and one resource:

  • list_tables -- lists every user table in the database grouped by schema. Claude uses this when it hits a table name that isn't in the schema context file.

  • describe_table -- returns full column definitions plus three sample rows for a given table. Claude uses this when it needs precise type information before writing a query.

  • run_query -- executes a SELECT statement and returns the results as a Markdown table. This is the main event.

  • schema://context (resource) -- serves the contents of schema-context.md, a file you maintain that describes your key tables in business terms. Claude reads this before generating any query. A well-written schema context eliminates most of the dynamic tool calls and gets the SQL right on the first try.

How a query actually flows

You: "What were my top 10 equity positions by market value last Friday?"

  1. Claude reads schema://context
       -> understands Holdings, Instruments, the YYYYMMDD date format, etc.

  2. Claude calls run_query with the SQL it composed

  3. run_query executes against SQL Server, returns a Markdown table

  4. Claude answers in plain English with the data inline

For tables not covered in schema-context.md, Claude falls back to list_tables and describe_table and explores dynamically before writing the query. It's slower but it works.

Safety

The server uses a read-only database login as the primary control. run_query also rejects anything that doesn't start with SELECT as a secondary check. Neither replaces a properly restricted database user -- always connect with a login that only has SELECT grants on the schemas you care about.


Related MCP server: MS SQL MCP Server

MCP Implementation

Transport

The server uses stdio transport -- it reads JSON-RPC messages on stdin and writes responses to stdout. Claude Code and Claude Desktop launch it as a child process. There's no network port, no listening socket, nothing to expose.

FastMCP

The server is built on FastMCP from the mcp package. It turns decorated Python functions directly into MCP tools and resources. The alternative is the lower-level mcp.server.Server class which gives you full protocol control, but for a server this simple it's just boilerplate. FastMCP removes all of it:

mcp = FastMCP("sql", instructions="...")

@mcp.tool()
def my_tool(arg: str) -> str:
    ...

@mcp.resource("scheme://uri")
def my_resource() -> str:
    ...

mcp.run()

Tools vs Resources

Tools are functions Claude calls explicitly, with arguments, in the tool-use loop. Every call shows up in the conversation.

Resources are read-only data at a URI. Claude requests them via the resource protocol rather than as a tool call -- better suited for reference data like a schema description that you want Claude to read once per session, not call repeatedly.

Server instructions

FastMCP accepts an instructions string that gets sent to Claude when the connection is established. That's how Claude knows to read schema://context before writing SQL -- without you having to say so in every message. It's part of the MCP spec, separate from the system prompt, and scoped to this server's capabilities.


Code Breakdown

sql-mcp/
├── server.py            Everything -- MCP server, tools, resource
├── schema-context.md    Your schema reference (the thing you maintain)
├── pyproject.toml       Dependencies
├── .env                 Connection string (not committed)
└── .env.example         Template

server.py

Startup

load_dotenv()
CONN_STR = os.environ["MSSQL_CONN"]

python-dotenv loads .env at import time. MSSQL_CONN is required -- missing it raises KeyError immediately, which shows up as a startup failure in Claude's MCP panel rather than a confusing runtime error mid-conversation.

_connect()

def _connect() -> pyodbc.Connection:
    conn = pyodbc.connect(CONN_STR, timeout=QUERY_TIMEOUT)
    conn.timeout = QUERY_TIMEOUT
    return conn

Opens a fresh connection per call. timeout gets set twice -- once on connect() for the handshake, once on the connection object for statement execution. For a single-user local tool this is fine. If you need to support concurrent sessions, see the connection pool extension below.

_rows_to_markdown()

Converts column names and row tuples into a Markdown table. Database NULLs become the string "NULL" rather than Python's None -- it reads better when Claude includes it in a response.

schema_context (resource)

@mcp.resource("schema://context")
def schema_context() -> str:
    if not SCHEMA_CONTEXT_PATH.exists():
        return "No schema-context.md found. ..."
    return SCHEMA_CONTEXT_PATH.read_text()

Reads schema-context.md from disk on every request, so you can edit the file without restarting the server. If the file is missing, Claude falls back to list_tables and describe_table automatically.

list_tables (tool)

Queries INFORMATION_SCHEMA.TABLES for all BASE TABLE entries -- no views. Returns results grouped by schema.

describe_table (tool)

Queries INFORMATION_SCHEMA.COLUMNS for column metadata, then runs SELECT TOP 3 to pull representative rows. The sample data matters more than the column types for getting SQL right -- especially for code columns and non-obvious date formats.

run_query (tool)

Normalises the SQL with .strip().upper() and checks it starts with SELECT. Fetches MAX_ROWS + 1 rows, then truncates to MAX_ROWS and appends a notice if the extra row was there. Avoids a separate COUNT(*) round-trip while still detecting truncation.


Extending the Server

Add a new tool

Decorate a function with @mcp.tool(), give it typed arguments and a clear docstring, and it becomes available to Claude immediately on next server start. Argument names and the docstring go directly into Claude's tool description, so write them like you'd write a comment for a colleague.

Example -- row counts across all tables:

@mcp.tool()
def table_row_counts() -> str:
    """Return the approximate row count for every user table."""
    sql = """
        SELECT s.name AS schema_name, t.name AS table_name,
               p.rows AS row_count
        FROM sys.tables t
        JOIN sys.schemas s ON s.schema_id = t.schema_id
        JOIN sys.partitions p ON p.object_id = t.object_id
            AND p.index_id IN (0, 1)
        ORDER BY p.rows DESC
    """
    with _connect() as conn:
        cursor = conn.cursor()
        cursor.execute(sql)
        rows = cursor.fetchall()
    return _rows_to_markdown(["Schema", "Table", "Rows"], rows)

Add a connection pool

For concurrent sessions or an HTTP deployment, swap the per-call _connect() for SQLAlchemy:

# pyproject.toml: add "sqlalchemy>=2.0"
from sqlalchemy import create_engine, text

engine = create_engine(
    "mssql+pyodbc:///?odbc_connect=" + CONN_STR,
    pool_size=5,
    max_overflow=10,
)

def _connect():
    return engine.connect()

Switch cursor-based queries to conn.execute(text(sql)) and adjust result handling.

Switch to HTTP transport

stdio works for local use. To share the server across a team, switch to SSE:

mcp.run(transport="sse", host="0.0.0.0", port=8080)

In the Claude config, replace command/args with a url pointing at the server.

Multiple databases

Pass a database argument to the tools and route to the right connection string:

CONNECTIONS = {
    "trading": os.environ["MSSQL_CONN_TRADING"],
    "risk":    os.environ["MSSQL_CONN_RISK"],
}

@mcp.tool()
def run_query(sql: str, database: str = "trading") -> str:
    """Execute a SELECT against the named database (trading or risk)."""
    if database not in CONNECTIONS:
        return f"Unknown database '{database}'. Options: {', '.join(CONNECTIONS)}"
    ...

The schema-context.md File

Why it matters

This file is the most important thing you can do for query quality. Column names and data types tell Claude what exists; schema-context.md tells it what things mean. A good file eliminates schema-exploration round-trips and gets the SQL right on the first attempt. A stale or missing file makes Claude guess, and it will guess wrong on anything domain-specific.

Structure

Plain Markdown. The server passes the full text to Claude as-is. No required format, but this layout works well:

# Database Schema Context

## Core Tables

### dbo.TableName
One sentence: what does this table represent in business terms?

| Column | Type | Notes |
|--------|------|-------|
| pk_col | INT PK | |
| fk_col | INT FK->OtherTable | |

**Common joins:**
<SQL snippet>

---

## Common Patterns
<Named SQL snippets for your most frequent query shapes>

What to put in the Notes column

This is where most of the value lives:

  • PK / FK -- mark primary and foreign keys explicitly. Claude uses these to build joins.

  • Code values -- if a column contains codes, list them. `B` = buy, `S` = sell. Claude cannot infer these from the data.

  • Units and currency -- for monetary columns, note the currency and scale (units, thousands, millions).

  • Date format quirks -- if dates are stored as integers in YYYYMMDD format, say so and include the conversion expression. This is the single most common source of bad SQL in financial databases.

  • Default filters -- if nearly every query should include a filter (WHERE is_active = 1, WHERE status <> 'CANCELLED'), call it out. Claude will include it without being asked.

What to leave out

  • Don't document every column. Focus on the ones that matter for analysis. If a table has 80 columns and 10 are relevant, document the 10.

  • Skip infrastructure tables, audit logs, ETL staging, and anything users won't query directly.

  • Skip indexes, triggers, and stored procedures unless they directly affect how SELECT queries should be written.

Common patterns section

Pre-written SQL snippets for your most frequent query shapes are worth the time to write. They give Claude a template to adapt rather than generating from scratch, which means more consistent SQL that matches your existing conventions.

Keeping it current

The server reads schema-context.md on every resource request, so edits take effect immediately without a restart. Treat it like any other internal documentation -- update it when tables are renamed or columns change meaning. An outdated file is actively worse than no file.


Prerequisites

Python and uv

Requires Python 3.11+. Dependencies are managed with uv:

curl -LsSf https://astral.sh/uv/install.sh | sh

Microsoft ODBC Driver for SQL Server

pyodbc needs the ODBC driver installed at the system level.

Fedora / RHEL:

curl https://packages.microsoft.com/config/rhel/9/prod.repo \
  | sudo tee /etc/yum.repos.d/mssql-release.repo

sudo ACCEPT_EULA=Y dnf install -y msodbcsql18 unixODBC-devel

Ubuntu / Debian: Use the equivalent Microsoft Ubuntu repo and apt-get install msodbcsql18 unixodbc-dev.

macOS:

brew tap microsoft/mssql-release https://github.com/Microsoft/homebrew-mssql-release
brew install msodbcsql18

Read-only database user

Create a login with SELECT-only access before connecting:

CREATE LOGIN sql_mcp WITH PASSWORD = 'StrongPassword123!';
USE YourDatabase;
CREATE USER sql_mcp FOR LOGIN sql_mcp;
GRANT SELECT ON SCHEMA::dbo TO sql_mcp;

Build and Run

1. Install dependencies

cd ~/sql-mcp
uv sync

Creates .venv inside the project. You don't need to activate it -- uv run handles that.

2. Configure the connection

cp .env.example .env

Edit .env:

MSSQL_CONN=DRIVER={ODBC Driver 18 for SQL Server};SERVER=192.168.1.50;DATABASE=TradingDB;UID=sql_mcp;PWD=StrongPassword123!

For Windows Authentication:

MSSQL_CONN=DRIVER={ODBC Driver 18 for SQL Server};SERVER=myserver;DATABASE=TradingDB;Trusted_Connection=yes

3. Edit schema-context.md

Replace the sample content with documentation for your actual tables. See the Schema Context section above. You can skip this for initial testing, but don't skip it for regular use.

4. Test the server

uv run server.py

It blocks waiting for MCP messages on stdin. Ctrl-C to stop. If it fails with ImportError: libodbc.so.2, the ODBC driver installation didn't complete.

5. Register with Claude Code

Open ~/.claude/settings.json and confirm this block is present (it should already be there from initial setup):

{
  "mcpServers": {
    "sql": {
      "command": "uv",
      "args": ["--directory", "/home/pr0f1t/sql-mcp", "run", "server.py"]
    }
  }
}

Run /mcp in a Claude Code session to confirm the sql server is connected and the three tools and schema://context resource are listed.

6. Register with Claude Desktop

Open the Claude Desktop config file:

  • Linux: ~/.config/Claude/claude_desktop_config.json

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add the same mcpServers block as above and restart Claude Desktop.

7. Ask questions

What are my top 10 positions by market value?
Show me all equity trades from last week.
Which accounts have more than 5% concentration in a single name?
What was the total notional traded per broker this month?

Claude reads the schema context, writes the SQL, runs it, and answers. If the result is wrong, correct it in plain English and Claude will adjust and re-run.


Troubleshooting

Server doesn't appear in /mcp Check that uv is on the PATH Claude Code uses. Run which uv -- if it's under ~/.local/bin, make sure that's in your shell profile's PATH.

ImportError: libodbc.so.2 The unixODBC system library isn't installed. Run the dnf install command from the prerequisites section.

login rejected (code 2) Wrong credentials in .env, or the server isn't reachable. Test with sqlcmd -S <server> -U <user> -P <password> -Q "SELECT 1".

KeyError: 'MSSQL_CONN' .env is missing or in the wrong directory. It needs to be at /home/pr0f1t/sql-mcp/.env.

Results are truncated The default cap is 500 rows. Raise MAX_ROWS in server.py if you need more -- just keep in mind that large result sets consume context window space and slow the response.

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gmines/sql-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server