MCP SQL Server
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., "@MCP SQL Servershow me the schema of the Orders table"
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.
MCP SQL Server Tool
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.0Related 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:
the user-scoped
appsettings.json— any number of profiles;McpMssql__Profiles__<NAME>__<FIELD>environment variables — any number of profiles;flat
MCPMSSQL_<FIELD>environment variables — thedefaultprofile 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 |
|
| required | — |
|
| none | — |
|
|
| — |
|
| 500 | 1 000 |
|
| 30 | 300 |
|
| 10 000 | 50 000 |
|
| 120 | 300 |
|
| 300 | 600 |
|
| 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.jsonWindows:
%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.McpMssqlConnection 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 configured connection profiles. Call first when picking a non-default profile. Returns | — |
| Get metadata for one relation (columns, indexes, constraints, relationships) or routine (definition). |
|
| Execute read-only T-SQL SELECT; only SELECT allowed (no DML/DDL). Returns results as CSV in the |
|
| Analyze execution plan for a read-only SELECT. Returns compact JSON summary (cost, operators, cardinality, warnings, |
|
| Execute write T-SQL (DDL/DML). Advertised only when some profile sets |
|
kind—relationorroutine.includes— Array of detail sections:columns,indexes,constraints,relationships(relations only),definition(routines only).relationshipsreturns 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 |
| List configured connection profiles, including |
| Retrieve full XML execution plan by ID from |
| Retrieve full query result as CSV by ID from |
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 |
| Materializes a new table. |
| Assigns a variable, mutating session state. |
| Advances a sequence. |
| Reads through an ad-hoc external data source. |
| 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.TestsOne 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.0CI 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
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only MCP server for ClassQuill, a tutoring-business-management platform.
2,000+ MCP servers read at source level. Know what one does before you connect. Free, no key.
Read-only MCP server for The Quiet Protocol's engines, benchmarks, proof, and business data.
Draxlr's remote MCP server connects AI assistants to your SQL databases and dashboards. Explore schemas, run read-only queries, manage saved queries and dashboards, and export results, all with row-level security so each user sees only their own data.
Related MCP Servers
- FlicenseAqualityDmaintenanceAn 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-
- AlicenseNot gradedqualityDmaintenanceRead-only SQL Server MCP server enabling safe database queries, table listing, and schema inspection with built-in security protections.MIT
- AlicenseNot gradedqualityDmaintenanceA 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
- FlicenseAqualityCmaintenanceA 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-