使用说明
mcp-mssql
MCP tool server providing SQL Server database access for AI agents.
Tools
| Tool | Description |
|---|---|
mssql_query |
Execute SQL queries (read-only by default) |
mssql_list_tables |
List all tables in a schema |
mssql_describe_table |
Get table structure (columns, types, constraints) |
Quick Start
cp .env.example .env
# Edit .env with your MSSQL connection details
make install
make server
Environment Variables
| Variable | Default | Description |
|---|---|---|
MSSQL_HOST |
localhost |
SQL Server host |
MSSQL_PORT |
1433 |
SQL Server port |
MSSQL_USER |
sa |
Database user |
MSSQL_PASSWORD |
Database password | |
MSSQL_DB |
master |
Database name |
MSSQL_READONLY |
true |
Enforce read-only queries |
MSSQL_MAX_ROWS |
1000 |
Row ceiling per query |
MCP_PORT |
8080 |
Server port |
Set
MSSQL_READONLY=falseto enable read/write mode (INSERT, UPDATE, DELETE, etc.).
MSSQL_MAX_ROWS is a resource bound, not an access control — an allowed table stays fully
readable, one page at a time. Without it a bare SELECT * on a large table materialises
every row into the pod and then into the model's context, and neither survives that.
Access control
Per-caller authorization is provided by
mcp-policy-guard, configured entirely
through the MCP_* variables the rgis-workers platform injects — see that package's README
for the full table. Two behaviours are specific to this worker:
Queries are authorized against the tables they actually read. mssql_query parses the
T-SQL with sqlglot, enumerates every referenced table, and submits the whole set for a
decision. Every table must be allowed, so a query joining dbo.Orders and hr.Payroll is
a payroll read and fails as a whole — a join cannot launder access. The check runs on the
emitted SQL, after any prompt injection has had its say, which is why it holds where a
prompt instruction would not. When the read set cannot be established with certainty —
four-part names, table-valued functions, OPENROWSET, a parse failure — the query is
refused, not guessed at. See src/mcp_mssql/table_extraction.py and its test suite.
Discovery hides rather than refuses. mssql_list_tables silently omits tables the
caller may not see, and mssql_describe_table returns the same "not found" string for a
denied table as for an absent one. Distinguishing the two would confirm which tables exist,
turning every denial into an enumeration oracle. The real reason is recorded in the audit
trail instead. mssql_query is the deliberate exception: the model already named the table,
so an explicit denial reveals nothing and stops it retrying.
With no policy URL configured the guard authenticates and then allows, which is exactly how this tool behaved before access control existed.
Endpoints
| Path | Method | Description |
|---|---|---|
/healthz |
GET | Health check for K8s probes |
/ |
GET | Server info (name, version, transports) |
/mcp |
POST | MCP Streamable-HTTP transport |
/sse |
GET | MCP SSE transport (legacy, see below) |
/sse is not mounted when MCP_REQUIRE_AUTH=true. Under SSE the long-lived connection
that carried the Authorization header is not the request that carries a tool call, so the
caller cannot be attributed to the call — mounting it anyway would leave a second,
unauthenticated door onto the same tools.
Docker
make docker-build
make docker-run