Skip to main content
Glama
alyiox

MCP SQL Server

MCP SQL Server Tool

Build Status NuGet Version License: MIT

A read-only-by-default Model Context Protocol (MCP) server for Microsoft SQL Server with schema discovery, parameterized SELECT queries, execution-plan analysis, and opt-in writes per profile. Profile-based configuration serves multiple databases and servers from one toolset deployment.

Requirements: .NET 8.0 or later runtime (the tool targets net8.0 and net10.0), SQL Server, and a connection string. Building from source requires the .NET 10.0 SDK.

Quick start

Set MCPMSSQL_CONNECTION_STRING and run the server in one of these ways:

# Option 1: Run from NuGet package (e.g. with MCP Inspector)
export MCPMSSQL_CONNECTION_STRING="Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"
npx -y @modelcontextprotocol/inspector@latest dotnet dnx Alyio.McpMssql --prerelease
# Option 2: Install and run as a global tool
dotnet tool install --global Alyio.McpMssql --prerelease
export MCPMSSQL_CONNECTION_STRING="Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"
npx -y @modelcontextprotocol/inspector@latest mcp-mssql
# Option 3: Run from source (clone repo, then)
export MCPMSSQL_CONNECTION_STRING="Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"
npx -y @modelcontextprotocol/inspector@latest dotnet run --project src/Alyio.McpMssql -f net10.0

Related MCP server: mcp-sqlserver-readonly

Configuration

A profile is one SQL Server connection: a connection string, the row and timeout caps that apply to it, and whether writes are allowed. A profile named default always exists; every tool takes an optional profile to reach another, and list_profiles reports what is configured.

Settings come from three sources, merged field by field, later winning:

  1. the user-scoped appsettings.json — any number of profiles;

  2. McpMssql__Profiles__<NAME>__<FIELD> environment variables — any number of profiles;

  3. flat MCPMSSQL_<FIELD> environment variables — the default profile only.

Because the merge is per field rather than per profile, an appsettings.json can carry the full set while a flat MCPMSSQL_CONNECTION_STRING repoints the default profile at a local server, leaving its other fields intact. There is no fallback connection string: a profile without one — including a default that nothing configured — fails startup.

Each setting has one field name, spelled three ways — the JSON path under McpMssql:Profiles:<NAME>, that same path with : replaced by __ as an environment variable, or the flat form:

Field

Flat variable

Default

Hard ceiling

ConnectionString

MCPMSSQL_CONNECTION_STRING

required

Description

MCPMSSQL_DESCRIPTION

none

AllowWrite

MCPMSSQL_ALLOW_WRITE

false

Query:MaxRows

MCPMSSQL_QUERY_MAX_ROWS

500

1 000

Query:CommandTimeoutSeconds

MCPMSSQL_QUERY_COMMAND_TIMEOUT_SECONDS

30

300

Query:SnapshotMaxRows

MCPMSSQL_QUERY_SNAPSHOT_MAX_ROWS

10 000

50 000

Query:SnapshotCommandTimeoutSeconds

MCPMSSQL_QUERY_SNAPSHOT_COMMAND_TIMEOUT_SECONDS

120

300

Analyze:CommandTimeoutSeconds

MCPMSSQL_ANALYZE_COMMAND_TIMEOUT_SECONDS

300

600

Write:CommandTimeoutSeconds

MCPMSSQL_WRITE_COMMAND_TIMEOUT_SECONDS

60

600

Caps are per profile. A value above its ceiling — or below 1 — is clamped at startup and the adjustment is logged as a warning on stderr; a flat value that is not an integer, or not a boolean for AllowWrite, is ignored, leaving whatever the other sources set.

Single connection: flat environment variables are the shortest path.

# Connection string (required).
export MCPMSSQL_CONNECTION_STRING="Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"

# Optional description for the default profile (tooling/AI discovery).
export MCPMSSQL_DESCRIPTION="Primary connection"

# Optional caps, defaults shown.
export MCPMSSQL_QUERY_MAX_ROWS="500"
export MCPMSSQL_QUERY_COMMAND_TIMEOUT_SECONDS="30"
export MCPMSSQL_QUERY_SNAPSHOT_MAX_ROWS="10000"
export MCPMSSQL_QUERY_SNAPSHOT_COMMAND_TIMEOUT_SECONDS="120"
export MCPMSSQL_ANALYZE_COMMAND_TIMEOUT_SECONDS="300"

# Optional write access, off by default; also controls whether run_command
# is advertised at all. A soft guard, not a database permission — prefer a
# db_datareader login for a hard read-only guarantee.
export MCPMSSQL_ALLOW_WRITE="false"
export MCPMSSQL_WRITE_COMMAND_TIMEOUT_SECONDS="60"

Multiple connections: use the user-scoped appsettings.json, which keeps credentials out of the host's process environment.

  • Unix-like: ~/.config/mcp-mssql/appsettings.json

  • Windows: %USERPROFILE%\.config\mcp-mssql\appsettings.json

{
  "McpMssql": {
    "Profiles": {
      "default": {
        "ConnectionString": "Server=...;User ID=...;Password=...;",
        "Description": "Primary connection",
        "Query": {
          "MaxRows": 500,
          "CommandTimeoutSeconds": 60,
          "SnapshotMaxRows": 10000,
          "SnapshotCommandTimeoutSeconds": 120
        },
        "Analyze": {
          "CommandTimeoutSeconds": 300
        }
      },
      "warehouse": {
        "ConnectionString": "Server=warehouse.example.com;...",
        "Description": "Warehouse read-only"
      },
      "migrations": {
        "ConnectionString": "Server=...;User ID=...;Password=...;",
        "Description": "Write-enabled profile for schema changes",
        "AllowWrite": true,
        "Write": {
          "CommandTimeoutSeconds": 60
        }
      }
    }
  }
}

Profile names are case-insensitive, and the structured environment form splits on __, so McpMssql__Profiles__WAREHOUSE__ConnectionString is profile warehouse, field ConnectionString. A missing appsettings.json is fine — the server starts on whatever sources remain — but one that is not valid JSON fails startup.

Local development: store the connection string in user-secrets, then run with DOTNET_ENVIRONMENT=Development so secrets and a working-directory appsettings.json load as extra sources.

dotnet user-secrets set "MCPMSSQL_CONNECTION_STRING" "..." --project src/Alyio.McpMssql
npx -y @modelcontextprotocol/inspector -e DOTNET_ENVIRONMENT=Development dotnet run --project src/Alyio.McpMssql

Connection string syntax: the usual Server=host,port;Database=db;User ID=...;Password=...;Encrypt=True; keywords of Microsoft.Data.SqlClient, which also supports Microsoft Entra (Azure AD) authentication: set Authentication to a supported mode (e.g. Active Directory Default, Active Directory Managed Identity, or Active Directory Interactive) when connecting to Azure SQL. See Connect to Azure SQL with Microsoft Entra authentication and SqlClient for all modes and details.

Tools and resources

All tools accept an optional profile; when omitted, the default profile is used.

Tools

Tool

Description

Key params

list_profiles

List configured connection profiles. Call first when picking a non-default profile. Returns name, description and allow_write per profile.

get_object

Get metadata for one relation (columns, indexes, constraints, relationships) or routine (definition). name accepts Users, dbo.Users or [dbo].[Users]. includes omitted → columns. Relations also carry an approximate row_count.

kind, name, profile, catalog, schema, includes

run_query

Execute read-only T-SQL SELECT; only SELECT allowed (no DML/DDL). Returns results as CSV in the data field (inline) or a snapshot resource URI when snapshot=true. Inline limit: 500 rows (hard ceiling 1000). Snapshot limit: 10 000 rows (hard ceiling 50 000). Prefer analyze_query for plan tuning.

sql, profile, catalog, parameters, snapshot

analyze_query

Analyze execution plan for a read-only SELECT. Returns compact JSON summary (cost, operators, cardinality, warnings, missing_indexes, waits, stats); no result rows, full XML at plan_uri.

sql, profile, catalog, parameters, estimated

run_command

Execute write T-SQL (DDL/DML). Advertised only when some profile sets AllowWrite=true (off by default); still rejected at call time when the target profile is locked. Caller manages transactions. Returns rows_affected (−1 for DDL) and server messages. Marked destructive; intended for human-supervised use.

sql, profile, catalog, parameters

  • kindrelation or routine.

  • includes — Array of detail sections: columns, indexes, constraints, relationships (relations only), definition (routines only). relationships returns foreign keys in both directions.

Catalog browsing is left to run_query over sys.objects, sys.schemas and sys.databases. get_object accepts analyze_query's missing_indexes[].table as-is.

Resources

URI template

Description

mssql://profiles

List configured connection profiles, including allow_write. Same data as list_profiles.

mssql://plans/{id}

Retrieve full XML execution plan by ID from analyze_query; entries expire after 7 days.

mssql://snapshots/{id}

Retrieve full query result as CSV by ID from run_query (snapshot=true); entries expire after 7 days.

Plans and snapshots are written to disk, under ~/.cache/mcp-mssql/plans/ and ~/.cache/mcp-mssql/snapshots/ (%USERPROFILE%\.cache\mcp-mssql\ on Windows). Expired files are swept the first time the server touches the store.

Security

The query tools (run_query, analyze_query) are read-only (SELECT only) and use parameterized @paramName binding. Use environment variables, config file or user-secrets for connection strings—never commit secrets.

What counts as read-only. The SQL is parsed with ScriptDom and must be exactly one SELECT statement in a single batch — not merely text that begins with SELECT. Multi-statement and GO-separated scripts are rejected, and so are these, despite being syntactically SELECTs:

Rejected

Reason

SELECT ... INTO

Materializes a new table.

SELECT @v = ...

Assigns a variable, mutating session state.

NEXT VALUE FOR

Advances a sequence.

OPENQUERY, OPENDATASOURCE, OPENROWSET, OPENROWSET(BULK ...)

Reads through an ad-hoc external data source.

UPDLOCK, XLOCK, TABLOCK, TABLOCKX, HOLDLOCK, SERIALIZABLE, REPEATABLEREAD

Take locks that impede concurrent writers.

Hints that acquire no extra locks, such as NOLOCK, ROWLOCK and READPAST, stay allowed. Input longer than 64 KB or nested more than 100 parentheses deep is also refused, which keeps the recursive-descent parser clear of a stack overflow.

Like AllowWrite below, this constrains what this server will send — it is not a database permission.

Writes are opt-in, and invisible until then. The run_command tool executes arbitrary T-SQL. Unless at least one configured profile sets AllowWrite=true (default false), the tool is not registered at all — it never appears in tools/list, so a read-only deployment spends no context on it and offers no write surface for an agent to be talked into. Once any profile opts in, the tool is advertised server-wide and still rejects at call time on profiles that remain locked; list_profiles reports allow_write per profile so an agent can pick a writable one.

AllowWrite is a soft, application-level guard, not a security boundary — it constrains this server, not the database. For a genuine read-only guarantee, connect with a login restricted to db_datareader, and keep write-enabled profiles pointed at credentials scoped to only what they need. run_command is marked destructive via MCP tool annotations so hosts can gate it behind confirmation, but honor those annotations at the host's discretion.

MCP host examples

Replace the connection string with your own; ensure dotnet is on your PATH. The env block is unnecessary when the connection string already comes from appsettings.json or the environment.

Claude Code, Cursor and Gemini all read the same mcpServers shape:

{
  "mcpServers": {
    "mssql": {
      "command": "dotnet",
      "args": ["dnx", "Alyio.McpMssql", "--prerelease", "--yes"],
      "env": {
        "MCPMSSQL_CONNECTION_STRING": "Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"
      }
    }
  }
}

Codex (TOML):

[mcp_servers.mssql]
command = "dotnet"
args = ["dnx", "Alyio.McpMssql", "--prerelease", "--yes"]
[mcp_servers.mssql.env]
MCPMSSQL_CONNECTION_STRING = "Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"

Open Code:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "mssql": {
      "type": "local",
      "enabled": true,
      "command": ["dotnet", "dnx", "Alyio.McpMssql", "--prerelease", "--yes"],
      "environment": {
        "MCPMSSQL_CONNECTION_STRING": "Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"
      }
    }
  }
}

GitHub Copilot:

{
  "inputs": [],
  "servers": {
    "mssql": {
      "type": "stdio",
      "command": "dotnet",
      "args": ["dnx", "Alyio.McpMssql", "--prerelease", "--yes"],
      "env": {
        "MCPMSSQL_CONNECTION_STRING": "Server=127.0.0.1;User ID=sa;Password=<YourStrong@Passw0rd>;Encrypt=True;TrustServerCertificate=True;"
      }
    }
  }
}

Integration tests

Tests use a real SQL Server and the default profile (MCPMSSQL_CONNECTION_STRING from environment variables or user-secrets). The suite expects a database named McpMssqlTest: the connection string must include Initial Catalog=McpMssqlTest. The test infrastructure creates, seeds, and drops this database. Set the secret for the test project:

dotnet user-secrets set "MCPMSSQL_CONNECTION_STRING" \
  "Server=localhost,1433;User ID=sa;Password=...;TrustServerCertificate=True;Encrypt=True;Initial Catalog=McpMssqlTest;" \
  --project test/Alyio.McpMssql.Tests

One framework at a time. The single McpMssqlTest database is shared by every test, and the fixtures drop and recreate it on initialization. Within one test process this is safe — the SqlServer collection disables parallelization. Across processes it is not: the test project targets both net8.0 and net10.0, and dotnet test runs the two framework modules in parallel, so they race on that one database. There is no cross-process locking, so run a single framework at a time:

dotnet test --framework net8.0
dotnet test --framework net10.0

CI does the same, iterating over TARGET_FRAMEWORKS sequentially.

Why this instead of Data API Builder?

Data API Builder (DAB) is a full REST/GraphQL API with CRUD and auth. This project is a small, read-only MCP server for agents: stdio, parameterized SELECT only, minimal surface. Choose this for agent workflows and low operational overhead; choose DAB for CRUD, REST/GraphQL, and rich policies.

Roadmap

MCP Tasks extension (SEP-2663). Snapshot queries and execution-plan analysis run under long timeouts (120 s and 300 s by default), which is the shape the Tasks extension exists for: the server returns a durable task handle instead of blocking, and the client polls tasks/get until the work reaches a terminal state.

The fit is good; adoption is the blocker. Tasks is an opt-in extension (io.modelcontextprotocol/tasks) that a server may only use when the client declares support in its per-request capabilities, and no client currently lists it in the extension support matrix. Deferred until clients ship support.

Schema compatibility

Nullable members emit a JSON Schema union type — "type": ["string", "null"] — because that is what System.Text.Json produces for string? and friends. It is legal JSON Schema 2020-12 and permitted by the MCP spec. MCP Inspector warns on the form, on the grounds that some MCP clients read type as a single string; whether that rule still has evidence behind it is under review upstream. Rewriting to anyOf is not a clear win: OpenAI documents the union form for optional parameters, Anthropic supports anyOf and not type arrays, and Cursor, Gemini, and Azure AI Foundry reject anyOf.

Nothing is lost by ignoring the null branch. This server never serializes null — absent members are omitted rather than sent as null — and no nullable member appears in a required list, so a client that reads only the first type in the union gets the exact contract. Left as the SDK emits it; revisit if the SDK changes or the rule settles.

Contributing

Open issues or PRs; follow existing style and add tests where appropriate.

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    D
    maintenance
    An MCP server for Microsoft SQL Server that enables executing read-only queries, listing tables, and describing database schemas. It offers specialized support for custom ports and multiple authentication methods including SQL credentials, NTLM, and Windows Integrated Auth.
    3
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Read-only SQL Server MCP server enabling safe database queries, table listing, and schema inspection with built-in security protections.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A read-only MCP server for exploring on-premises, multi-instance Microsoft SQL Server estates from AI clients, with read-only enforcement and Windows authentication support.
    Apache 2.0
  • F
    license
    A
    quality
    C
    maintenance
    A read-only MCP server for browsing and querying SQL Server databases, providing tools to list schemas, tables, describe columns, and execute safe SELECT queries with validated parameters.
    15
    -