SQL Server 数据库 MCP 服务

一款让 AI 助手通过 Model Context Protocol 连接和操作 Microsoft SQL Server 数据库的 MCP 服务,支持 SQL 查询、表结构探查、视图/函数/存储过程管理等能力,适用于 Claude 等支持 MCP 的客户端场景。

  • by odeciojunior
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 odeciojunior/mcp-sql-server

使用说明

MCP SQL Server

A Python Model Context Protocol (MCP) server that enables AI assistants to interact with Microsoft SQL Server databases. Built on the MCP Python SDK (MCPServer, SDK 2.x), it provides tools for executing queries, exploring schemas, retrieving object definitions, and managing stored procedures -- with multi-database support, connection pooling, caching, audit logging, and security validation.

Features

  • 10 MCP Tools for query execution, schema discovery, object inspection, and stored procedure management
  • 5 MCP Resources for browsing database metadata, functions, pool statistics, and configured connections
  • Multi-Database Support via a DatabaseRegistry pattern -- connect to multiple SQL Server instances from a single server
  • Thread-Safe Connection Pooling with health checks, idle/stale retirement, and configurable pool sizing
  • TTL-Based Metadata Caching (60-second default) for schema discovery queries
  • SQL Security Validation blocking DDL, DCL, admin commands, and system procedures
  • Audit Logging with SQL hashing, execution timing, and sensitive data masking
  • Structured Logging in JSON or text format with request correlation IDs
  • Error Sanitization that redacts IPs, credentials, and connection details from error messages
  • Strict Type Safety with full mypy strict mode compliance
  • Comprehensive test suite at 95%+ code coverage (no live database required)

Architecture Overview

                    +------------------+
                    |   MCP Client     |
                    |  (Claude, etc.)  |
                    +--------+---------+
                             | stdio
                    +--------v---------+
                    |    server.py     |
                    |   MCP Server     |
                    |  (tool & resource|
                    |   registration)  |
                    +--------+---------+
                             |
              +--------------+--------------+
              |                             |
    +---------v----------+       +----------v---------+
    |   tools/           |       |   resources/       |
    | - query_execution  |       | - database_info    |
    | - schema_discovery |       |   (tables, funcs,  |
    | - object_defs      |       |    pool stats,     |
    | - stored_procs     |       |    db info,        |
    | - registry_tools   |       |    databases)      |
    +--------+-----------+       +----------+----------+
             |                              |
             +----------- utils.py ---------+
                             |
                    +--------v---------+
                    |   registry.py    |
                    | DatabaseRegistry |
                    | (named DB mgmt)  |
                    +--------+---------+
                             |
                    +--------v---------+
                    |   database.py    |
                    | DatabaseManager  |
                    | (cursor mgmt,   |
                    |  query/stmt exec)|
                    +--------+---------+
                             |
                    +--------v---------+
                    |     pool.py      |
                    |  ConnectionPool  |
                    | (thread-safe,    |
                    |  health checks)  |
                    +--------+---------+
                             |
                    +--------v---------+
                    |    pyodbc        |
                    |  (ODBC Driver)   |
                    +------------------+

Cross-Cutting Concerns:
  config.py ........... Pydantic models, .env loading, multi-DB config
  security.py ......... SQL validation, keyword blocking, identifier checks
  cache.py ............ TTL cache with @cached decorator
  audit.py ............ Query hashing, execution timing, audit events
  errors.py ........... Error sanitization and error responses
  logging_config.py ... Structured JSON/text logging, request correlation
  sql_lexer.py ........ Dependency-free T-SQL tokenizer used by security/audit
  utils.py ............ Lazy accessors (breaks tools/resources <-> server import cycle)

Module Responsibilities

Module Responsibility
server.py MCPServer initialization, tool/resource registration, lifecycle management
registry.py Named database management with lazy initialization and thread-safe access
database.py Connection management, cursor context managers, query/statement execution
pool.py Thread-safe connection pooling with Queue, health checks, stale/idle retirement
config.py Pydantic DatabaseConfig and PoolConfig models, .env loading, multi-DB env parsing
security.py SQL validation, blocked keyword detection, identifier sanitization, bracket quoting
sql_lexer.py Dependency-free T-SQL tokenizer (words, strings, quoted identifiers, numbers, paren depth) used by validation and audit masking
cache.py TTLCache class with thread-safe get/set, @cached decorator, global metadata cache
audit.py AuditLogger for queries/statements/procedures, SQL hashing, timed_operation() context manager
errors.py Error sanitization (credentials, IPs, echoed data values), simplification of common SQL Server errors, error-response builder
logging_config.py StructuredFormatter (JSON), StandardFormatter (text), request ID correlation via ContextVar
utils.py Lazy import helpers to break circular dependencies between tools/resources and server
tools/ Tool implementations organized by domain (query, schema, objects, procedures, registry)
resources/ MCP resource handlers returning formatted markdown strings

Installation

Prerequisites

Setup

# Create virtual environment
python3 -m venv .venv

# Install package with dev dependencies
.venv/bin/pip install -e ".[dev]"

MCP Client Integration

The server uses stdio transport and works with any MCP-compatible client. Below are setup instructions for the most common clients.

Prerequisites for all clients:

  1. Clone and install the project (see Setup above)
  2. Copy .env.example to .env and configure your database credentials
  3. Note the absolute path to the project's Python interpreter: /path/to/mcp-sql-server/.venv/bin/python
  4. On WSL2 with SQL Server running on Windows, use 127.0.0.1 rather than localhost for the host: under mirrored networking localhost logins can hang until the login timeout.

Claude Code (CLI)

Register the server using claude mcp add. All flags go before the server name, and -- separates the command.

Basic (reads .env from the server's working directory):

claude mcp add --transport stdio mcp-sql-server -- \
  /path/to/mcp-sql-server/.venv/bin/python -m mcp_sql_server.server

With inline environment variables (no .env file needed):

claude mcp add --transport stdio \
  -e DB_HOST=sqlserver.example.com \
  -e DB_USER=myuser \
  -e DB_PASSWORD=mypassword \
  -e DB_NAME=MyDatabase \
  -e DB_DRIVER="ODBC Driver 18 for SQL Server" \
  -e DB_TRUST_CERT=true \
  mcp-sql-server -- \
  /path/to/mcp-sql-server/.venv/bin/python -m mcp_sql_server.server

Project-scoped (shared via .mcp.json in your repo):

claude mcp add --transport stdio --scope project mcp-sql-server -- \
  /path/to/mcp-sql-server/.venv/bin/python -m mcp_sql_server.server

Manage registered servers:

claude mcp list                    # List all registered servers
claude mcp get mcp-sql-server      # Show server details
claude mcp remove mcp-sql-server   # Remove the server

Claude Desktop

Edit your claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

Open Claude Desktop → Settings → Developer → Edit Config, then add:

{
  "mcpServers": {
    "mcp-sql-server": {
      "command": "/path/to/mcp-sql-server/.venv/bin/python",
      "args": ["-m", "mcp_sql_server.server"],
      "env": {
        "DB_HOST": "sqlserver.example.com",
        "DB_USER": "myuser",
        "DB_PASSWORD": "mypassword",
        "DB_NAME": "MyDatabase",
        "DB_DRIVER": "ODBC Driver 18 for SQL Server",
        "DB_TRUST_CERT": "true"
      }
    }
  }
}

Restart Claude Desktop after saving. A hammer icon should appear in the input area confirming the tools are loaded.

Note: All env values must be strings. The .env file is looked for in the server's working directory; if the client starts the server somewhere else, set MCP_SQL_SERVER_ENV_FILE to the file's full path, or pass the variables via the env block.

Cursor

Go to Settings → Tools & Integrations → MCP and add a new server, or edit the config file directly at .cursor/mcp.json in your project root:

{
  "mcpServers": {
    "mcp-sql-server": {
      "command": "/path/to/mcp-sql-server/.venv/bin/python",
      "args": ["-m", "mcp_sql_server.server"],
      "env": {
        "DB_HOST": "sqlserver.example.com",
        "DB_USER": "myuser",
        "DB_PASSWORD": "mypassword",
        "DB_NAME": "MyDatabase",
        "DB_DRIVER": "ODBC Driver 18 for SQL Server",
        "DB_TRUST_CERT": "true"
      }
    }
  }
}

Windsurf

Open Settings → Plugins (MCP servers) under Cascade, or edit the config file directly:

  • macOS: ~/.codeium/windsurf/mcp_config.json
  • Windows: %USERPROFILE%\.codeium\windsurf\mcp_config.json
{
  "mcpServers": {
    "mcp-sql-server": {
      "command": "/path/to/mcp-sql-server/.venv/bin/python",
      "args": ["-m", "mcp_sql_server.server"],
      "env": {
        "DB_HOST": "sqlserver.example.com",
        "DB_USER": "myuser",
        "DB_PASSWORD": "mypassword",
        "DB_NAME": "MyDatabase",
        "DB_DRIVER": "ODBC Driver 18 for SQL Server",
        "DB_TRUST_CERT": "true"
      }
    }
  }
}

VS Code

Add to your VS Code settings.json (or workspace .vscode/settings.json):

{
  "mcp": {
    "servers": {
      "mcp-sql-server": {
        "command": "/path/to/mcp-sql-server/.venv/bin/python",
        "args": ["-m", "mcp_sql_server.server"],
        "env": {
          "DB_HOST": "sqlserver.example.com",
          "DB_USER": "myuser",
          "DB_PASSWORD": "mypassword",
          "DB_NAME": "MyDatabase",
          "DB_DRIVER": "ODBC Driver 18 for SQL Server",
          "DB_TRUST_CERT": "true"
        }
      }
    }
  }
}

Generic MCP Client (JSON)

Any client supporting the MCP stdio transport can use this server. The JSON configuration pattern is the same across clients:

{
  "command": "/path/to/mcp-sql-server/.venv/bin/python",
  "args": ["-m", "mcp_sql_server.server"],
  "env": {
    "DB_HOST": "sqlserver.example.com",
    "DB_USER": "myuser",
    "DB_PASSWORD": "mypassword",
    "DB_NAME": "MyDatabase",
    "DB_DRIVER": "ODBC Driver 18 for SQL Server",
    "DB_TRUST_CERT": "true"
  }
}

Note: The code default for DB_DRIVER is ODBC Driver 17 for SQL Server. If you have only Driver 18 installed, you must set DB_DRIVER explicitly.

Environment Variables vs .env File

There are two ways to provide database credentials to the server:

Method Best For How It Works
env block in JSON config Claude Desktop, Cursor, Windsurf, VS Code Client injects variables into the server process at startup
.env file Claude Code CLI, local development Server reads it via python-dotenv on startup, without modifying the process environment

When using the env block: The MCP client spawns the server as a subprocess and injects the variables directly. This is the most reliable method since it doesn't depend on the working directory.

When using the .env file: the server looks in this order, taking the first that applies:

  1. $MCP_SQL_SERVER_ENV_FILE, if set -- the full path to the file
  2. .env in the server's working directory
  3. .env beside the installed package, which exists only for an editable checkout (pip install -e .)

Only the working directory is checked, never its parents: climbing ancestors would let an unrelated directory's .env supply database credentials without anyone choosing it. If the client starts the server outside your project, set MCP_SQL_SERVER_ENV_FILE.

Multi-database variables (e.g., DB_ANALYTICS_HOST) work with both methods. See Multi-Database Support for details.

Configuration

Copy .env.example to .env in the directory the server runs from and configure your database connection. To keep it elsewhere, point MCP_SQL_SERVER_ENV_FILE at it.

Configuration Sources and Precedence

The default database can be configured two ways:

  • DB_* variables, from the MCP client's env block or the .env file
  • SQL_SERVER_* variables (SQL_SERVER_HOST, SQL_SERVER_PORT, SQL_SERVER_USER, SQL_SERVER_PASSWORD, SQL_SERVER_DATABASE, SQL_SERVER_DRIVER, SQL_SERVER_ENCRYPT, SQL_SERVER_TRUST_CERT), typically set in .claude/settings.local.json for Claude Code

Precedence, highest first:

  1. DB_* set in the process environment (e.g. by the MCP client)
  2. SQL_SERVER_*
  3. DB_* from .env

Named databases (DB_{ALIAS}_*) do not read SQL_SERVER_*.

Database Connection

Variable Default Required Description
DB_HOST - Yes SQL Server hostname or IP
DB_PORT 1433 No SQL Server port
DB_USER - Yes Database username
DB_PASSWORD - Yes Database password
DB_NAME - Yes Database name
DB_DRIVER ODBC Driver 17 for SQL Server No ODBC driver name
DB_TIMEOUT 30 No Connection timeout (seconds)
DB_QUERY_TIMEOUT 120 No Query timeout (seconds)
DB_ENCRYPT false No Enable encrypted connection
DB_TRUST_CERT false No Trust server certificate

Connection Pool

Variable Default Description
DB_POOL_MIN_SIZE 1 Minimum connections pre-created
DB_POOL_MAX_SIZE 5 Maximum connections allowed
DB_POOL_IDLE_TIMEOUT 300 Idle connection retirement (seconds)
DB_POOL_HEALTH_CHECK_INTERVAL 30 Health check frequency (seconds)
DB_POOL_ACQUIRE_TIMEOUT 10.0 Timeout waiting for a connection (seconds)
DB_POOL_MAX_LIFETIME 3600 Maximum connection age before retirement (seconds)

Logging

Variable Default Description
LOG_LEVEL INFO Log level (DEBUG, INFO, WARNING, ERROR)
LOG_FORMAT text Output format (text or json)

LOG_LEVEL and LOG_FORMAT are read from the process environment first, then from .env.

Query Files

Variable Default Description
QUERY_DIR <repo-root>/query/ Directory containing .sql files for execute_query_file

Configuration File

Variable Default Description
MCP_SQL_SERVER_ENV_FILE unset Full path to the .env file. Overrides the working-directory lookup.

Example .env

DB_HOST=sqlserver.example.com
DB_PORT=1433
DB_USER=myuser
DB_PASSWORD=mypassword
DB_NAME=MyDatabase
DB_DRIVER=ODBC Driver 18 for SQL Server
DB_ENCRYPT=true
DB_TRUST_CERT=true
DB_POOL_MIN_SIZE=2
DB_POOL_MAX_SIZE=10
LOG_LEVEL=INFO
LOG_FORMAT=text

Startup Validation

On startup the server checks every database's configuration and logs problems at ERROR (alias, field, and error type only; never values). The server still starts: a misconfigured database returns its configuration error when a tool targets it, and the other databases keep working.

Security tip: connect with a login that has only the db_datareader role unless you need execute_statement. The server's SQL validation is defense in depth, not a substitute for database permissions.

Multi-Database Support

The server supports connecting to multiple SQL Server databases simultaneously via the DatabaseRegistry pattern. Every tool accepts an optional database parameter (defaults to "default").

How It Works

  1. The default database reads from standard DB_* environment variables.
  2. Additional databases are declared in DB_DATABASES as a comma-separated list of aliases.
  3. Each alias reads from prefixed environment variables: DB_{ALIAS}_HOST, DB_{ALIAS}_USER, etc.
  4. Each database gets its own DatabaseManager and ConnectionPool, lazily initialized on first use.

Configuration Pattern

# Default database (always required)
DB_HOST=primary.example.com
DB_USER=user1
DB_PASSWORD=pass1
DB_NAME=PrimaryDB

# Declare additional databases
DB_DATABASES=analytics,archive

# Analytics database
DB_ANALYTICS_HOST=analytics.example.com
DB_ANALYTICS_USER=analyst
DB_ANALYTICS_PASSWORD=pass2
DB_ANALYTICS_NAME=AnalyticsDB
DB_ANALYTICS_POOL_MAX_SIZE=3

# Archive database
DB_ARCHIVE_HOST=archive.example.com
DB_ARCHIVE_USER=reader
DB_ARCHIVE_PASSWORD=pass3
DB_ARCHIVE_NAME=ArchiveDB
DB_ARCHIVE_TIMEOUT=60

Usage in Tools

# Query the default database
execute_query(sql="SELECT TOP 10 * FROM Users")

# Query a named database
execute_query(sql="SELECT TOP 10 * FROM Events", database="analytics")

# List all configured databases, checking each is actually reachable
list_databases()
# Returns: {"success": True, "databases": [{"name": "default", ..., "status": "ok"}, ...]}

# Skip the connection check when you only want the configured names
list_databases(probe=False)

Alias Rules

  • Must match ^[a-zA-Z][a-zA-Z0-9_]{0,63}$ (start with letter, max 64 chars)
  • "default" is reserved and always present
  • Each alias has independent pool configuration via DB_{ALIAS}_POOL_* variables

Available Tools

Breaking change: tool names no longer have a leading underscore (_execute_query → execute_query, _list_tables → list_tables, etc.). Update MCP clients and any saved permission rules to match — e.g. mcp__<server>__execute_query instead of mcp__<server>___execute_query.

execute_query

Execute a read-only SELECT query against the database.

Parameter Type Default Description
sql str required SQL query (must start with SELECT or WITH)
params list[str] None Positional parameter values for ? placeholders
limit int 1000 Max rows to return (1--10,000)
database str "default" Target database alias

The SQL runs unmodified (CTEs, ORDER BY, and UNION all work). Rows are capped on the server with SET ROWCOUNT (limit+1) and read with fetchmany; the session is reset afterwards and any connection whose state cannot be restored is retired. If more than limit rows exist, truncated is set to True.

Response:

{
  "success": true,
  "columns": ["id", "name"],
  "rows": [{"id": 1, "name": "Alice"}],
  "row_count": 1,
  "truncated": false
}

execute_statement

Execute a data modification statement (INSERT, UPDATE, DELETE).

Parameter Type Default Description
sql str required SQL statement (must start with INSERT, UPDATE, or DELETE)
params list[str] None Positional parameter values for ? placeholders
database str "default" Target database alias

Only one statement is allowed per call; see SQL Validation.

Response:

{
  "success": true,
  "affected_rows": 5
}

execute_query_file

Execute a SQL query from a .sql file in the configured query directory.

Parameter Type Default Description
filename str required Filename (e.g., "my_query.sql" or "my_query")
database str "default" Target database alias

Filename must match ^[a-zA-Z0-9_-]+\.sql$. Path traversal is prevented.

list_tables

List all tables in the database, optionally filtered by schema.

Parameter Type Default Description
schema str None Schema name to filter by
database str "default" Target database alias

Response:

{
  "success": true,
  "tables": [{"schema": "dbo", "name": "Users", "type": "BASE TABLE"}],
  "count": 1
}

describe_table

Get detailed column information for a table.

Parameter Type Default Description
table_name str required Table name
schema str "dbo" Schema name
database str "default" Target database alias

Response:

{
  "success": true,
  "table": "dbo.Users",
  "columns": [
    {"name": "id", "type": "int", "max_length": null, "precision": 10, "nullable": "NO", "default_value": null}
  ]
}

get_view_definition

Get the SQL source code of a database view.

Parameter Type Default Description
view_name str required View name
schema str "dbo" Schema name
database str "default" Target database alias

get_function_definition

Get the SQL source code of a user-defined function.

Parameter Type Default Description
function_name str required Function name
schema str "dbo" Schema name
database str "default" Target database alias

list_procedures

List all stored procedures in the database.

Parameter Type Default Description
schema str None Schema name to filter by
database str "default" Target database alias

Response:

{
  "success": true,
  "procedures": [{"schema": "dbo", "name": "usp_GetUsers", "created": "...", "modified": "..."}],
  "count": 1
}

execute_procedure

Execute a stored procedure with optional named parameters.

Parameter Type Default Description
proc_name str required Procedure name
schema str "dbo" Schema name
params dict[str, Any] None Parameter name-value pairs
database str "default" Target database alias

System procedures (xp_*, sp_*) are blocked. Parameter names are validated as safe identifiers.

At most 10,000 rows are returned; the response includes "truncated": true when more rows existed. Only the first result set is read.

Runs read-only: any data changes made by the procedure are rolled back when the connection is returned to the pool.

Example:

execute_procedure(
    proc_name="GetUsersByDepartment",
    params={"DeptId": 5, "Status": "active"}
)

list_databases

List all configured database connections and whether each one is reachable.

Parameter Type Default Description
probe bool true Open a real connection per database to check reachability. false returns the configured names without connecting.

Response:

{
  "success": true,
  "databases": [
    {"name": "default", "host": "server1", "port": 1433, "database": "MyDB", "status": "ok"},
    {"name": "reports", "host": "server1", "port": 1433, "database": "Reports",
     "status": "unreachable", "error": "Cannot open database \"Reports\" requested by the login"},
    {"name": "archive", "status": "misconfigured", "error": "password: string_too_short"}
  ],
  "count": 3
}

status is one of:

Value Meaning
ok A connection opened and SELECT 1 succeeded.
unreachable The probe ran and failed; error gives a value-free reason.
misconfigured The configuration is invalid, so no probe was attempted. Calls that target it return that error; other databases keep working.
unknown No probe was requested (probe=false, or the sqlserver://databases resource).

status never claims reachability that was not measured. A database that is OFFLINE on the server, or that the login cannot open, reports unreachable rather than ok.

Probes run in parallel with a 5-second timeout each, separate from DB_TIMEOUT. The sqlserver://databases resource never probes -- resources are read speculatively and must not open connections as a side effect -- so it reports unknown.

Available Resources

Resources return formatted markdown strings for browsing database metadata.

URI Description
sqlserver://tables All tables grouped by schema with type indicators
sqlserver://database/info Database version, edition, collation, and name
sqlserver://functions All user-defined functions grouped by schema with return types
sqlserver://pool/stats Connection pool metrics (acquisitions, health checks, peak usage, etc.)
sqlserver://databases All configured database connections in a markdown table

Security

SQL Validation

All queries and statements are tokenized before execution. Keywords inside string literals, quoted identifiers ([...], "..."), and comments are ignored, so WHERE note = 'DROP' and leading -- comments are fine.

Blocked Keywords (DDL/DCL/Admin):

Category Keywords
DDL DROP, TRUNCATE, ALTER, CREATE
DCL GRANT, REVOKE, DENY
Admin SHUTDOWN, BACKUP, RESTORE, DBCC, KILL, RECONFIGURE, CHECKPOINT
External Access OPENROWSET, OPENQUERY, OPENDATASOURCE, BULK
Dynamic SQL / control flow EXEC, EXECUTE, DECLARE, USE, WAITFOR, BEGIN, COMMIT, ROLLBACK, SAVE, IF, WHILE, GOTO, RETURN, PRINT, RAISERROR, THROW, OPEN, CLOSE, DEALLOCATE, SETUSER, REVERT, RECEIVE, SEND, ADD
Legacy text/image WRITETEXT, UPDATETEXT, READTEXT
Side effects NEXT VALUE (as in NEXT VALUE FOR), ENABLE/DISABLE TRIGGER, GET/MOVE/END CONVERSATION
File readers fn_xe_file_target_read_file, fn_trace_gettable, fn_get_audit_file, fn_get_audit_file_v2, fn_dump_dblog, fn_dump_dblog_xtp, fn_xe_telemetry_blob_target_read_file, dm_os_file_exists, dm_os_enumerate_filesystem

Blocked Prefixes: xp_*, sp_* (system stored procedures)

One statement per call. T-SQL does not need ; between statements, so the validator allows only one statement at the top level (outside parentheses). A single trailing ; is fine. Unbalanced parentheses are rejected.

  • execute_query accepts only SELECT or WITH first, rejects INSERT, UPDATE, DELETE, MERGE, INTO, and SET anywhere, and allows a second top-level SELECT only after UNION, EXCEPT, or INTERSECT.
  • execute_statement accepts only INSERT, UPDATE, or DELETE first. SET is allowed once in an UPDATE, and must come before any top-level FROM, WHERE, OUTPUT, or OPTION; a top-level SELECT only in INSERT ... SELECT; INTO only in INSERT INTO or OUTPUT ... INTO. UPDATE STATISTICS is rejected outright. Nested (composable) DML is not allowed.
  • CTE-prefixed DML (WITH c AS (...) DELETE ...) is not supported by either tool.

Unbracketed column names that match a blocked word (for example Send, Receive, Open) must be written in brackets: [Send].

Query hints are allowed: join/union hints such as INNER MERGE JOIN and OPTION (MERGE JOIN), and OPTION (USE HINT(...)) / OPTION (USE PLAN ...).

FETCH and statement labels after DML are accepted; they are harmless because tools never open cursors.

Use a read-only login. The validator is defense in depth. For read-only use, connect with a login that only has db_datareader.

Identifier Validation

Table, column, schema, and parameter names are validated against ^[a-zA-Z_][a-zA-Z0-9_]*$ and bracket-quoted to prevent SQL injection.

Query File Security

The execute_query_file tool validates filenames against ^[a-zA-Z0-9_-]+\.sql$ and uses Path.relative_to() to prevent path traversal attacks.

Error Sanitization

Error messages are automatically scrubbed to remove:

  • IP addresses
  • Usernames and passwords
  • Connection string details (SERVER, UID, PWD)
  • Data values SQL Server echoes in errors: duplicate key values (2627/2601), truncated values (2628), values in conversion failures (245), and values in conversion overflows (248)

Object names (e.g. Invalid object name 'dbo.Req') are kept for debugging.

Connection Pooling

The ConnectionPool class provides thread-safe connection management using Python's queue.Queue.

Lifecycle

  1. Initialization: Pre-creates min_size connections on first database access
  2. Acquisition: Returns a healthy connection from the pool, or creates a new one (up to max_size)
  3. Health Check: Executes SELECT 1 before returning connections that exceed the health_check_interval
  4. Release: Rolls back any pending transaction, checks staleness, and returns to pool
  5. Retirement: Connections are closed when they exceed max_lifetime, idle_timeout, or fail health checks
  6. Invalid connections: A connection whose session state could not be reset after a query (e.g. SET ROWCOUNT or database changed) is closed on release instead of being reused
  7. Login failures: If a new connection can't be created and the pool holds none, acquire() raises the login error immediately; if others are checked out it waits for one to be released, without retrying the login

Pool Statistics

Available via the sqlserver://pool/stats resource:

Metric Description
total_connections Total connections currently created
in_use Connections currently checked out
available Connections waiting in the pool
peak_usage Maximum concurrent connections observed
total_acquisitions Cumulative connection checkouts
total_releases Cumulative connection returns
failed_acquisitions Timeouts or creation failures
health_checks Total health checks performed
transaction_resets Rollbacks performed on release
pool_size / max_size Configured maximum pool size
min_size Minimum connections pre-created
closed Whether the pool has been shut down

Caching

Schema metadata queries (list_tables, describe_table, list_procedures) are cached using a thread-safe TTLCache with a 60-second default TTL.

Features

  • Thread-safe get/set/invalidate/clear operations via threading.Lock
  • Automatic expiration on access (lazy cleanup)
  • @cached decorator for transparent function-level caching; keys bind arguments to the function signature, so positional and keyword calls share an entry
  • Global invalidate_metadata_cache() to clear all cached entries
  • cleanup_expired() for bulk removal of stale entries
  • stats() method returning total, valid, and expired entry counts and the default TTL

Observability

Audit Logging

The AuditLogger tracks all database operations:

  • Queries: SQL hash, masked preview (first 100 chars), duration, row count, truncation status, target database
  • Statements: SQL hash, masked preview, statement type (INSERT/UPDATE/DELETE), duration, affected rows, target database
  • Procedures: Procedure name, schema, duration, row count, target database
  • Validation Failures: SQL hash, short masked preview, error, target database

Previews replace every string and numeric literal (and "double-quoted" tokens, which are strings under QUOTED_IDENTIFIER OFF) with ? and drop comments (WHERE cpf = '123' → WHERE cpf = ?). SQL that cannot be tokenized is logged as <unparseable> and hashed as a constant, never raw. sql_hash is the first 16 hex chars of the SHA-256 of the masked SQL: a query-shape fingerprint, so queries that differ only in literal values share a hash and the values cannot be recovered from it.

Structured Logging

Two output formats controlled by LOG_FORMAT:

  • text (default): 2024-01-15 10:30:00 - module.name - INFO - [3f2a9c1b7d4e] message
  • json: {"timestamp": "...", "level": "INFO", "logger": "...", "message": "...", "request_id": "3f2a9c1b7d4e"}

Every tool call gets a 12-character request_id, so all log lines from one call (including audit events) share it. Lines outside a tool call show - (text) or omit the field (JSON).

Error Handling

Tools never raise to the client. Every failure is returned as a sanitized error dictionary:

{
  "success": false,
  "error": "Simplified message",
  "error_context": "query"
}

Common SQL Server errors are automatically simplified (e.g., "Invalid object name 'Foo'" becomes "Object not found: Foo").

Testing

The test suite covers all modules with mocked database connections; run it to see the current count.

# Run all tests
.venv/bin/pytest tests/ -v

# Run with coverage report
.venv/bin/pytest tests/ -v --cov=mcp_sql_server --cov-report=term-missing

# Run a specific test file
.venv/bin/pytest tests/test_server.py -v

# Run tests matching a pattern
.venv/bin/pytest tests/ -v -k "test_execute_query"

Test Files

File Tests Description
test_server.py Tool and resource registration, end-to-end tool behavior
test_database.py DatabaseManager query/statement execution, cursor management
test_pool.py Connection pool lifecycle, health checks, retirement, metrics
test_config.py Configuration loading, validation, connection string generation
test_config_multi.py Multi-database config, alias parsing, prefixed env vars
test_registry.py DatabaseRegistry lazy init, get/close, from_env()
test_security.py SQL validation, blocked keywords, identifier checks
test_cache.py TTL cache operations, expiry, @cached decorator, thread safety
test_server_registration.py MCP SDK registration surface (tool/resource names, schemas)
test_concurrency.py Parallel pool/registry access and per-call request IDs
test_sql_lexer.py T-SQL tokenizer: strings, identifiers, comments, depth
test_audit.py Masked SQL previews and query-shape hashes
test_errors.py Error sanitization and value redaction
test_logging_config.py Text/JSON formats, request ID propagation
test_utils.py Lazy server accessors
test_query_dir.py Query directory resolution and file loading
test_entrypoint.py python -m mcp_sql_server.server over stdio: single registry, clean shutdown (no DB connection)
conftest.py Shared pytest fixtures (mock database, config objects)

Type Safety

The codebase uses strict type annotations validated with mypy in strict mode.

# Run type checker
.venv/bin/python -m mypy src/mcp_sql_server/

Mypy configuration (from pyproject.toml):

  • disallow_untyped_defs, disallow_incomplete_defs, disallow_untyped_decorators, check_untyped_defs
  • disallow_any_generics (all generics must be parameterized)
  • warn_return_any, warn_unused_configs, warn_redundant_casts, warn_unused_ignores, warn_no_return
  • strict_equality and strict_concatenate
  • no_implicit_reexport

Project Structure

pyproject.toml                             # Package metadata, dependencies, mypy config
+-- src/
|   +-- mcp_sql_server/
|       +-- __init__.py                    # Package version
|       +-- server.py                      # MCPServer, tool/resource registration
|       +-- database.py                    # DatabaseManager with pooled/non-pooled modes
|       +-- pool.py                        # Thread-safe ConnectionPool
|       +-- config.py                      # Pydantic configs, .env loading, multi-DB support
|       +-- registry.py                    # DatabaseRegistry for named database management
|       +-- security.py                    # SQL validation, keyword blocking
|       +-- sql_lexer.py                   # T-SQL tokenizer for validation and masking
|       +-- cache.py                       # TTLCache and @cached decorator
|       +-- audit.py                       # AuditLogger, SQL hashing, timed_operation
|       +-- errors.py                      # Exception hierarchy, error sanitization
|       +-- logging_config.py              # Structured/standard formatters, request IDs
|       +-- utils.py                       # Lazy import helpers (circular dep avoidance)
|       +-- tools/
|       |   +-- __init__.py                # Tool exports
|       |   +-- query_execution.py         # execute_query, execute_statement, execute_query_file
|       |   +-- schema_discovery.py        # list_tables, describe_table
|       |   +-- object_definitions.py      # get_view_definition, get_function_definition
|       |   +-- stored_procedures.py       # list_procedures, execute_procedure
|       |   +-- registry_tools.py          # list_databases
|       +-- resources/
|           +-- __init__.py                # Resource exports
|           +-- database_info.py           # tables, db info, functions, pool stats, databases
+-- tests/
    +-- conftest.py                        # Shared fixtures
    +-- test_server.py                     # Server integration tests
    +-- test_database.py                   # DatabaseManager tests
    +-- test_pool.py                       # ConnectionPool tests
    +-- test_config.py                     # Config loading tests
    +-- test_config_multi.py               # Multi-database config tests
    +-- test_registry.py                   # DatabaseRegistry tests
    +-- test_security.py                   # Security validation tests
    +-- test_cache.py                      # TTLCache tests
    +-- test_server_registration.py        # MCP registration surface tests
    +-- test_concurrency.py                # Thread-safety tests
    +-- test_sql_lexer.py                  # Tokenizer tests
    +-- test_audit.py                      # Audit masking tests
    +-- test_errors.py                     # Error sanitization tests
    +-- test_logging_config.py             # Logging tests
    +-- test_utils.py                      # Lazy accessor tests
    +-- test_query_dir.py                  # Query directory tests
    +-- test_entrypoint.py                 # -m entrypoint / registry lifecycle test

Development Commands

# Install with dev dependencies
.venv/bin/pip install -e ".[dev]"

# Run the MCP server (stdio transport)
.venv/bin/python -m mcp_sql_server.server

# Run tests
.venv/bin/pytest tests/ -v

# Run tests with coverage
.venv/bin/pytest tests/ -v --cov=mcp_sql_server --cov-report=term-missing

# Type check
.venv/bin/python -m mypy src/mcp_sql_server/

# Run via entry point (after install)
.venv/bin/mcp-sql-server

Dependencies

Runtime

Package Version Purpose
mcp >=2.0,<3 Model Context Protocol SDK (MCPServer)
pyodbc >=5.0.0 ODBC database connectivity
python-dotenv >=1.0.0 Environment variable loading from .env
pydantic >=2.12 Configuration validation and modeling

Development

Package Version Purpose
pytest >=7.0.0 Test framework
pytest-asyncio >=0.21.0 Async test support
pytest-cov >=4.0.0 Coverage reporting
mypy >=1.0.0 Static type checking

License

MIT — see LICENSE.