Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changes/sql-server/work-resolve-issue-issue-9898-4b7e2c19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
bump: minor
---

### Added

- The `setup` skill (`/sql-server:setup check|apply --config <path>`). `check` runs `skills/setup/scripts/check-setup.mjs`, which prints a PASS, FAIL or INFO row for Node.js, the config against `config.v1.schema.json`, each environment, each credential variable (set or not, never its value) and the server copy in the plugin data directory, then a `claude mcp add` command per environment that names each credential as `--env 'NAME=${NAME}'`. It exits 1 on a FAIL row, such as a config with a literal password. With an environment already registered, `check` also lists the principal's database permissions through its `query` tool. `apply` copies the server into the plugin data directory and runs `npm ci` there, then runs each registration command at user scope once the user confirms; project scope is refused ([#9898](https://github.com/melodic-software/claude-code-plugins/issues/9898)).
- `reference/azure-sql-database.md`: a free Azure SQL Database for a trial (`--use-free-limit true --free-limit-exhaustion-behavior AutoPause`), a read-only Entra contained user in `db_datareader`, the config entry, and what differs from a local SQL Server, `READ_COMMITTED_SNAPSHOT` being on by default there.
- The README's Setup section and a list of the limits of each guarantee, from ADR 0086 and the read-safety measurement.
2 changes: 1 addition & 1 deletion docs/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ plugin manifests and kept in sync by CI. Never hand-edit it; the category vocabu
- [`source-control`](../plugins/source-control): Git and GitHub delivery: /commit (convention-checked subject, Co-authored-by trailer, surgical staging), /pull-request (prep, create, CI monitoring, review triage, merge, CI logs), /babysit-prs (safe-by-default PR fleet loop, opt-in worker and autopilot tiers), /babysit-loop (merge lane; merge is human until the repo adopts it), /worktree, /resolve-conflicts (intent-first), /check, and /setup (layered source-control.md convention config; Conventional Commits by default).
- [`implementation`](../plugins/implementation): Disciplined implementation stage: execute approved plans inline (`/implementation:implement`) or via orchestrated worker subagents (`/implementation:implement-dispatch`) with incremental validation, TDD-by-default cadence, green-checkpoint commits, scope-fence drift detection, and divergence detection that routes back to planning. Build/test/lint, testing, and outcome verification live in the companion `toolchain`, `testing`, and `verification` plugins, invoked when installed.
- [`toolchain`](../plugins/toolchain): Repo-agnostic polyglot verification toolchain: build + test + lint for changed files across .NET, Python, TypeScript, Bash, PowerShell, Markdown, Go, YAML, and cross-cutting surfaces (`/toolchain:check`, `/toolchain:lint` with format-only `--fix` and gated `--code-fix`), `/toolchain:setup` for the per-ecosystem command config, `/toolchain:ratchet` for a CI ceiling on a count, and a turn-end Stop hook that warns when the recorded check or lint result is not a pass.
- [`sql-server`](../plugins/sql-server): Read-only SQL Server and Azure SQL Database access for agents, bounded by a read-only database principal (ADR 0086). This release ships the versioned config schema, its loader, the single-statement reject list, the stdio MCP server with its fixed query, list_objects and describe_object tools, and the tedious executor with per-call transaction guards; the setup skill comes later.
- [`sql-server`](../plugins/sql-server): Read-only SQL Server and Azure SQL Database access for agents, bounded by a read-only database principal (ADR 0086). This release ships the versioned config schema, its loader, the single-statement reject list, the stdio MCP server with its fixed query, list_objects and describe_object tools, the tedious executor with per-call transaction guards, and a setup skill that checks a config and registers each environment by reference.
- [`wizard`](../plugins/wizard): Scripts that walk a human through manual steps an agent cannot do: provisioning infrastructure or credentials, CI secrets, third-party dashboards, one-off migrations. /wizard:generate scopes stages from the repo (.env key names only), builds an interactive bash wizard on a hardened library (TTY-only prompts, hidden secret entry, 0600 .env writes), and shows the stages for approval first; the human runs it. /wizard:unattended writes a PowerShell script the human launches once.
- [`developer-experience`](../plugins/developer-experience): Helps a team build and maintain its own developer tooling: setup records the team's tooling conventions, build-cli builds command-line tools to them, and audit-tools inventories a repository's scripts, CLIs, hooks and skills and reports what to fix.

Expand Down
2 changes: 1 addition & 1 deletion plugins/sql-server/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sql-server",
"version": "0.3.2",
"description": "Read-only SQL Server and Azure SQL Database access for agents, bounded by a read-only database principal (ADR 0086). This release ships the versioned config schema, its loader, the single-statement reject list, the stdio MCP server with its fixed query, list_objects and describe_object tools, and the tedious executor with per-call transaction guards; the setup skill comes later.",
"description": "Read-only SQL Server and Azure SQL Database access for agents, bounded by a read-only database principal (ADR 0086). This release ships the versioned config schema, its loader, the single-statement reject list, the stdio MCP server with its fixed query, list_objects and describe_object tools, the tedious executor with per-call transaction guards, and a setup skill that checks a config and registers each environment by reference.",
"author": {
"name": "Melodic Software",
"email": "info@melodicsoftware.com"
Expand Down
55 changes: 51 additions & 4 deletions plugins/sql-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,33 @@ Read-only access to SQL Server-family databases (a local SQL Server container, A
for agents. The design is [ADR 0086](../../docs/adr/0086-bound-the-sql-server-read-tier-by-a-read-only-principal-and-fixed-registrations.md).

This release ships the versioned config schema, its loader, the single-statement reject list, the
stdio MCP server with its three tools, and the executor that runs each call on SQL Server through
`tedious`. The setup skill comes in a later release, so the plugin is off by default.
stdio MCP server with its three tools, the executor that runs each call on SQL Server through
`tedious`, and the `setup` skill that checks a config and registers its environments. The plugin
is off by default.

## Setup

1. Create a read-only principal on the server (for Azure SQL Database, see
[reference/azure-sql-database.md](reference/azure-sql-database.md), which also covers a free
database for a trial).
2. Write a config file outside your project directory (see [Config](#config)).
3. Run `/sql-server:setup check --config <path>`, then `/sql-server:setup apply --config <path>`.
`apply` installs the server into the plugin's data directory, which keeps its path across
plugin updates, and prints one `claude mcp add` command per environment, which it runs once you
confirm. The command names each credential as `--env 'NAME=${NAME}'`, a reference Claude Code
expands when it starts the server, so the stored registration holds no secret. Project scope is
refused, because the agent can write the repository's `.mcp.json`.

The check is a script you can run on its own. From a checkout of this repository:

```bash
node plugins/sql-server/skills/setup/scripts/check-setup.mjs --config <path> [--environment <name>] [--data-dir <dir>]
```

It prints one PASS, FAIL or INFO row for Node.js, the config, each environment, each credential
variable (set or not, never its value) and, with `--data-dir`, the installed server copy, then each
environment's `claude mcp add` command. It exits 1 when a row fails, as it does for a config with a
literal password, and writes nothing.

## The server

Expand Down Expand Up @@ -82,8 +107,9 @@ its managed identity client id, when given) to `tedious`'s `token-credential` au

The executor loads `tedious` and `@azure/identity`, the dependencies in `package.json`, on the
first call, so the server starts without them and a call made before they are installed fails
with a tool error saying so. `package-lock.json` pins their whole tree, and Claude Code installs
that tree when it copies the plugin into its cache.
with a tool error saying so. `package-lock.json` pins their whole tree. Claude Code installs that
tree when it copies the plugin into its cache, and `/sql-server:setup apply` installs it with
`npm ci` into the server copy in the plugin's data directory, the copy a registration runs.

### The reject list

Expand All @@ -107,6 +133,27 @@ What the list lets through, because it is a heuristic:
`SELECT 1 AS a RECEIVE TOP(1) * FROM q`. The read-only principal, which holds no `RECEIVE` or
`ALTER` permission, is what refuses it.

### Limits of the guarantees

- **Other databases on the server.** The allow-list refuses a `database` argument outside it, but
a three-part name such as `other.dbo.t` is stopped only by what `other` lets the principal do.
Check the principal's access in every database on the server, system databases included.
- **Lock hold before the row cap fires.** The measurement shows a capped read returning promptly
with no transaction left open, for a fast consumer and no writer. How long a slow or paused
consumer holds locks before the cap fires was not measured.
- **Load per registration.** Each registration runs one process per client session, and each
process holds at most two connections, so a session with three registrations can hold six. The
measurement blocked no request with up to 2 processes and a few briefly at 8.
- **Locking reads on SQL Server.** `READ_COMMITTED_SNAPSHOT` is off by default on SQL Server, so a
`READ COMMITTED` read takes shared locks until its statement ends and the lock timeout, the
request timeout and the row cap bound how long it blocks a writer. Azure SQL Database turns it on
by default and reads row versions instead.
- **Entra tokens.** Whether a connection that stays busy past its token's expiry keeps working is
not verified.
- **Not covered.** Windows integrated and Kerberos authentication. Azure SQL Managed Instance and
other SQL Server hosts reachable over TCP are expected to work with the same two auth types, but
are not tested.

## Config

One JSON file, validated by [`schemas/config.v1.schema.json`](schemas/config.v1.schema.json)
Expand Down
43 changes: 43 additions & 0 deletions plugins/sql-server/prerequisites.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
{
"requires": [
{
"id": "node",
"kind": "runtime",
"need": "required",
"for": [
"plugin",
"skill:setup"
],
"detect": {
"any": [
"node"
]
},
"degrade": "Without node, the MCP server cannot start and /sql-server:setup cannot run its check or install scripts.",
"install": {
"docs": "https://nodejs.org/en/download",
"brew": "node",
"winget": "OpenJS.NodeJS.LTS",
"apt": "nodejs"
},
"check": "/sql-server:setup"
},
{
"id": "npm",
"kind": "cli",
"need": "optional",
"for": [
"skill:setup"
],
"detect": {
"any": [
"npm"
]
},
"degrade": "Without npm, /sql-server:setup apply cannot install the server copy's dependencies; check still runs.",
"install": {
"docs": "https://nodejs.org/en/download",
"brew": "node",
"winget": "OpenJS.NodeJS.LTS",
"apt": "npm"
},
"check": "/sql-server:setup"
},
{
"id": "docker",
"kind": "cli",
Expand Down
98 changes: 98 additions & 0 deletions plugins/sql-server/reference/azure-sql-database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Azure SQL Database

Azure SQL Database needs no code of its own: an environment in the config with an `entra-default`
or `sql-login` `auth` block reaches it. This page covers a free database for a trial, a read-only
Entra principal, the config entry and what differs from a local SQL Server.

## A free database for a trial

Each Azure subscription can hold up to 10 free General Purpose databases. Each gets 100,000 vCore
seconds, 32 GB of data and 32 GB of backup storage per month. `--use-free-limit true` makes the
database free; without it the command creates a billable serverless database.
`--free-limit-exhaustion-behavior AutoPause` pauses the database once the month's free limit runs
out, so it stops charges until the allowance resets at the start of the next month. The other
behavior, `BillOverUsage`, keeps the database online and bills the overage, and a database cannot
switch back from it.

```bash
az sql db create --name <database-name> --server <server-name> --resource-group <resource-group> --edition GeneralPurpose --family Gen5 --capacity 2 --compute-model Serverless --use-free-limit true --free-limit-exhaustion-behavior AutoPause
az sql server firewall-rule create --name AllowClientIp --server <server-name> --resource-group <resource-group> --start-ip-address <client-ip-address> --end-ip-address <client-ip-address>
```

The second command lets your machine's public IP address through the logical server's firewall.
A free database pauses after 15 minutes without activity, and an open connection keeps it awake.
The server's pool closes a connection idle for 30 seconds, so a registration left running does not
hold the database awake between calls.

## A read-only Entra principal

The read-only principal is the guarantee here as on any server: a registration on an identity that
can write is not read-only, whatever the reject list does.

1. Set a Microsoft Entra admin on the logical server (portal: the server's **Microsoft Entra ID**
page, **Set admin**; CLI: `az sql server ad-admin create`). Done when the admin's name shows in
the **Microsoft Entra admin** field.
2. Connect to the database the agent reads as an Entra identity with `ALTER ANY USER`, such as that
admin, and create a contained user that holds read access only:

```sql
CREATE USER [reader@contoso.com] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [reader@contoso.com];
```

For a managed identity or service principal, use its display name in the brackets. Grant
nothing else: `db_datareader` reads every user table and view in that database, and a contained
user exists only in the database it was created in. Done when
`SELECT name FROM sys.database_principals WHERE name = 'reader@contoso.com'` returns one row.
3. Repeat step 2 in each other database the environment lists, and in no database it does not.

## The config entry

```json
{
"schemaVersion": 1,
"environments": {
"azure-dev": {
"server": "example-dev.database.windows.net",
"databases": ["app"],
"auth": { "type": "entra-default" }
}
}
}
```

- `server` is the logical server name from the database's **Overview** page in the portal. Leave
out `port`: clients reach the gateway on 1433, the default. From inside Azure the default
connection policy is Redirect, which also needs outbound ports 11000 to 11999.
- `entra-default` signs in with `DefaultAzureCredential`, which tries, in order, environment
variables, workload identity, managed identity, Visual Studio Code, the Azure CLI, Azure
PowerShell and the Azure Developer CLI. On a workstation, `az login` as the reader identity is
enough. Set `clientId` to pick a user-assigned managed identity.
- A `sql-login` entry works too, with a contained SQL user that is a member of `db_datareader`
only. Entra is the better choice: the config then names no password variable at all.

Check the file with `/sql-server:setup check`, or from a checkout of this repository:

```bash
node plugins/sql-server/skills/setup/scripts/check-setup.mjs --config ~/.config/sql-server/config.json
```

## What differs from a local SQL Server

- **Reads do not block writers.** `READ_COMMITTED_SNAPSHOT` is on by default in Azure SQL Database
and off in SQL Server. With it on, the server's pinned `READ COMMITTED` reads row versions
instead of taking shared locks. The [measurement](read-safety-measurement.md) ran with it off,
so its lock-wait rows show the local container's case.
- **Entra token lifetime.** `tedious` asks the credential for a token on each connect, and the
pool closes idle connections. Whether a connection that stays busy past its token's expiry keeps
working is not verified.
- **Not tested here.** The integration test runs against a local container only; no test in this
plugin connects to Azure SQL Database.

Sources read on 2026-11-01:
[Azure SQL Database free tier](https://learn.microsoft.com/en-us/azure/azure-sql/database/free-tier),
[Configure Microsoft Entra authentication](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure),
[Database-level roles](https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/database-level-roles),
[Connectivity architecture](https://learn.microsoft.com/en-us/azure/azure-sql/database/connectivity-architecture),
[Transaction locking and row versioning guide](https://learn.microsoft.com/en-us/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide)
and [DefaultAzureCredential](https://learn.microsoft.com/en-us/javascript/api/@azure/identity/defaultazurecredential).
Loading
Loading