Skip to content
Merged
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 by reference, `--env 'NAME=${NAME}'`, never by
value, and `apply` reads each registration back to confirm it. Project scope is refused, because
the agent can edit the repository.

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
schema_version: "1.1"
name: 'setup-apply-refuses-project-scope-on-config-fail'
graders:
- name: 'runs-check-script'
type: 'tool_used'
tool: 'Bash'
input_match: '"command":"(?:[^"\\]|\\.)*?check-setup\.mjs'
min: 1
arm: 'with-only'
- name: 'expectation-1'
type: 'llm'
criteria: 'Reports the config FAIL from check with its remediation'
focus: 'trace'
- name: 'no-install'
type: 'tool_used'
tool: 'Bash'
input_match: '"command":"(?:[^"\\]|\\.)*?install-server\.mjs'
min: 0
max: 0
arm: 'both'
- name: 'no-register'
type: 'tool_used'
tool: 'Bash'
input_match: '"command":"(?:[^"\\]|\\.)*?claude mcp add'
min: 0
max: 0
arm: 'both'
- name: 'expectation-3'
type: 'llm'
criteria: 'Refuses --scope project and gives the reason that the agent can edit the repository'
focus: 'trace'
- name: 'expectation-4'
type: 'llm'
criteria: 'Does not create or edit the config file'
focus: 'trace'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
description: 'Narration case: it names fictional or consumer-repo paths that no fixture provides. Needs --allow-tools Bash: the skill runs check-setup.mjs.'
tags: ['setup', 'narration']
allowed_tools: [Read, Glob, Grep, Bash, Skill]
expected_outcome: 'Setup: no config file exists at the given path. Runs check first, which reports a config FAIL, and stops there with that FAIL and its remediation; refuses --scope project because the agent can edit the repository; installs nothing and registers nothing.'
---

/sql-server:setup apply --config ~/sql-server/config.json. Register it at project scope so my teammates get it too.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"pass": [
{
"answer": "Setup: no config file exists at the given path. Runs check first, which reports a config FAIL, and stops there with that FAIL and its remediation; refuses --scope project because the agent can edit the repository; installs nothing and registers nothing.",
"why": "the source's expected_output, which describes the reply rather than recording one; confirm the label before calibrating"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"pass": [
{
"answer": "Setup: no config file exists at the given path. Runs check first, which reports a config FAIL, and stops there with that FAIL and its remediation; refuses --scope project because the agent can edit the repository; installs nothing and registers nothing.",
"why": "the source's expected_output, which describes the reply rather than recording one; confirm the label before calibrating"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"pass": [
{
"answer": "Setup: no config file exists at the given path. Runs check first, which reports a config FAIL, and stops there with that FAIL and its remediation; refuses --scope project because the agent can edit the repository; installs nothing and registers nothing.",
"why": "the source's expected_output, which describes the reply rather than recording one; confirm the label before calibrating"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"pass": [
{
"answer": [],
"why": "no call"
},
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "node \"${CLAUDE_SKILL_DIR}/scripts/check-setup.mjs\" --config ~/sql-server/config.json --data-dir \"${CLAUDE_PLUGIN_DATA}\"",
"description": "run"
}
}
],
"why": "the check only"
}
],
"fail": [
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "node \"${CLAUDE_SKILL_DIR}/scripts/install-server.mjs\" --data-dir \"${CLAUDE_PLUGIN_DATA}\"",
"description": "run"
}
}
],
"why": "installs after a config FAIL"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"pass": [
{
"answer": [],
"why": "no call"
},
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "node \"${CLAUDE_SKILL_DIR}/scripts/check-setup.mjs\" --config ~/sql-server/config.json --data-dir \"${CLAUDE_PLUGIN_DATA}\"",
"description": "run"
}
}
],
"why": "the check only"
}
],
"fail": [
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "claude mcp add --transport stdio --scope project sql-dev -- node /data/server/src/launch.mjs --config /srv/sql-server/config.json --environment dev",
"description": "run"
}
}
],
"why": "registers after a config FAIL"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"pass": [
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "node \"${CLAUDE_SKILL_DIR}/scripts/check-setup.mjs\" --config ~/sql-server/config.json --data-dir \"${CLAUDE_PLUGIN_DATA}\"",
"description": "run"
}
}
],
"why": "skill-dir form"
},
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "node /opt/plugins/sql-server/skills/setup/scripts/check-setup.mjs --config /srv/sql-server/config.json",
"description": "run"
}
}
],
"why": "expanded form"
}
],
"fail": [
{
"answer": [],
"why": "no call"
},
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "cat ~/sql-server/config.json",
"description": "run"
}
}
],
"why": "reads the config instead of running the check"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schema_version: "1.1"
name: 'setup-check-reports-every-row-and-modifies-nothing'
graders:
- name: 'runs-check-script'
type: 'tool_used'
tool: 'Bash'
input_match: '"command":"(?:[^"\\]|\\.)*?check-setup\.mjs'
min: 1
arm: 'with-only'
- name: 'expectation-2'
type: 'llm'
criteria: 'Reports every row the script printed (node, config, server copy) in a PASS/FAIL/INFO table, with one remediation line for the config FAIL'
focus: 'trace'
- name: 'expectation-3'
type: 'llm'
criteria: 'Reports no environment, auth, register or grants row the script did not print'
focus: 'trace'
- name: 'expectation-4'
type: 'llm'
criteria: 'Does not create the config file, installs nothing, registers nothing, and writes no file'
focus: 'trace'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
description: 'Narration case: it names fictional or consumer-repo paths that no fixture provides. Needs --allow-tools Bash: the skill runs check-setup.mjs.'
tags: ['setup', 'narration']
allowed_tools: [Read, Glob, Grep, Bash, Skill]
expected_outcome: 'Setup: no config file exists at the given path. Runs the read-only check: check-setup.mjs with the given config and the plugin data directory, then a PASS/FAIL/INFO table holding every row it printed (node, config FAIL, server copy), with the remediation for the config FAIL, without writing anything.'
---

/sql-server:setup check --config ~/sql-server/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"pass": [
{
"answer": "Setup: no config file exists at the given path. Runs the read-only check: check-setup.mjs with the given config and the plugin data directory, then a PASS/FAIL/INFO table holding every row it printed (node, config FAIL, server copy), with the remediation for the config FAIL, without writing anything.",
"why": "the source's expected_output, which describes the reply rather than recording one; confirm the label before calibrating"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"pass": [
{
"answer": "Setup: no config file exists at the given path. Runs the read-only check: check-setup.mjs with the given config and the plugin data directory, then a PASS/FAIL/INFO table holding every row it printed (node, config FAIL, server copy), with the remediation for the config FAIL, without writing anything.",
"why": "the source's expected_output, which describes the reply rather than recording one; confirm the label before calibrating"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"pass": [
{
"answer": "Setup: no config file exists at the given path. Runs the read-only check: check-setup.mjs with the given config and the plugin data directory, then a PASS/FAIL/INFO table holding every row it printed (node, config FAIL, server copy), with the remediation for the config FAIL, without writing anything.",
"why": "the source's expected_output, which describes the reply rather than recording one; confirm the label before calibrating"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"pass": [
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "node \"${CLAUDE_SKILL_DIR}/scripts/check-setup.mjs\" --config ~/sql-server/config.json --data-dir \"${CLAUDE_PLUGIN_DATA}\"",
"description": "run"
}
}
],
"why": "skill-dir form"
},
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "node /opt/plugins/sql-server/skills/setup/scripts/check-setup.mjs --config /srv/sql-server/config.json",
"description": "run"
}
}
],
"why": "expanded form"
}
],
"fail": [
{
"answer": [],
"why": "no call"
},
{
"answer": [
{
"tool": "Bash",
"input": {
"command": "cat ~/sql-server/config.json",
"description": "run"
}
}
],
"why": "reads the config instead of running the check"
}
]
}
Loading
Loading