Skip to content

MCP operate verbs behind a human-in-the-loop gate#262

Merged
George-Payne merged 12 commits into
mainfrom
ui-1731/mcp-operate-verbs
Jul 6, 2026
Merged

MCP operate verbs behind a human-in-the-loop gate#262
George-Payne merged 12 commits into
mainfrom
ui-1731/mcp-operate-verbs

Conversation

@George-Payne

@George-Payne George-Payne commented Jul 6, 2026

Copy link
Copy Markdown
Member

Adds the operate verbs to the gaffer mcp server behind a human-in-the-loop production gate: deploy_pause, deploy_resume, deploy_abort, deploy_recreate, deploy_rollback, and deploy_delete, each mirroring its CLI command's semantics.

  • cli/internal/stamp (new): the ledger-stamping core (toolLedger's revision/actor resolution) moved out of cmd so mcpserver stamps the same attribution; one deliberate change documented in the commit - an env with no connection yields an empty actor rather than scraping .env credentials.
  • remote gains the rollback hash resolution (FindVersionByHash, neutral VersionMatch, NormalizeHashPrefix, RollbackRefusal), each page bounded by the RPC timeout as before.
  • deploy_gate.go: the confirmWrite gate. Production writes (and always-tier writes) require an MCP elicitation confirm answered by the human through the client mid-call - the model cannot fabricate it. Accept is the only proceed token; decline/cancel/error/no-capability/nil-session all refuse without writing and name the CLI command as the next step. The elicit schema is raw {"type":"object","properties":{}} (spec requires properties; the typed schema's omitempty drops an empty map and pulling the package in downgraded the SDK via MVS). Projection names are %q-quoted in the question so crafted names can't read as part of the sentence the human decides on.
  • The verbs: a shared runOperateVerb/operateSetup core (project gate, system-projection refusal, connect, existence check, $server-info prod detection matching the CLI). Tiers: pause/resume/abort elicit on production only; recreate and delete always elicit (both destroy state with no undo); rollback elicits on production, naming the exact hash movement. Recreate runs the compile and diagnostics preflight with no bypass - review caught that skipping the diagnostics gate could destroy a healthy projection and rebuild it into a faulting state. Recreate/rollback stamp operation: recreate/rollback; lifecycle verbs stamp nothing, like the CLI.
  • Tests: gate unit tests over real in-memory MCP transports (accept, decline/cancel/empty allowlist, elicit failure, missing capability, nil session, always tier); verb validation unit tests; an integration lifecycle against live KurrentDB (all six verbs, declined elicits leave the projection untouched, no-capability delete refused, recreate/rollback classified and attributed in deploy_history).
  • Docs + changeset: the MCP docs page documents the verbs and the elicitation gate; patch changeset.

non-production destructive
image

production non-destructive
image

production destructive

image

The MCP operate verbs (and later the deploy tool) need to stamp the
same tool metadata and resolve the same rollback targets as the CLI,
but both lived unexported in package cmd. Move them:

- cli/internal/stamp (new): stamp.Ledger builds the remote.Ledger from
  an already-resolved env plus operation/version/root, with the
  GAFFER_REVISION/git and GAFFER_ACTOR/principal resolution moved
  along. cmd's toolLedger stays as a thin resolveLiveEnv wrapper. One
  deliberate behaviour change: an env with no connection now yields an
  empty actor instead of letting engine.Principal dig credentials out
  of .env for an env gaffer never connected with - unreachable from
  cmd today (connectResolved refuses an empty connection before any
  write), and the safer contract for new callers.
- remote gains the rollback hash resolution: FindVersionByHash (each
  page bounded by the RPC timeout, same as cmd's rpc() did),
  VersionMatch (neutral name - hash-to-version resolution isn't
  rollback-specific), NormalizeHashPrefix, and RollbackRefusal.

Pure-logic tests move with their code; a new cmd test locks the
toolLedger wrapper's unresolvable-env contract (actor dropped,
everything else stamped).
confirmWrite is the production gate the operate verbs (and later the
deploy tool) sit behind: a write that targets a production server - or
one flagged Always, for verbs with no undo - requires an MCP
elicitation confirm, answered by the human through the client's UI
mid-call, so the model cannot fabricate it. Accept is the only proceed
token; decline, cancel, an unknown or empty action, an elicit failure,
a client without the (form) elicitation capability, and a call with no
session all refuse without writing, naming the CLI equivalent as the
next step. Non-production writes don't elicit: every MCP tool call
already passes the client's tool-approval layer, and double-prompting
every resume trains click-through.

The elicit schema is raw {"type":"object","properties":{}} - the spec
requires the properties member and jsonschema.Schema's omitempty drops
an empty map (adding the package as a direct dep also downgraded the
MCP SDK via MVS; review caught it). The projection name is %q-quoted in
the question and refusals so a crafted name can't read as part of the
sentence the human decides on.

Tests run over real in-memory MCP transports with a fake human on the
client side: accept, decline/cancel/empty (locking the accept-only
allowlist), elicit failure, missing capability, nil session, the
always-tier off production, and the prodWhere phrasing table.
Six write tools behind the human-in-the-loop gate, wrapping the same
remote.Client calls as the CLI operate commands and echoing
{name, outcome, env, target, production}:

- deploy_pause / deploy_resume / deploy_abort mirror disable / enable /
  disable --abort; prod-gated, no ledger (lifecycle writes are
  metadata-less by design, like the CLI)
- deploy_delete mirrors gaffer delete: disable first, state and
  checkpoint streams included, deleteEmitted optional - and it always
  elicits, production or not, because delete is the one verb with no
  undo story at all; a client without elicitation cannot delete via MCP
- deploy_recreate mirrors gaffer recreate: config + compile gate before
  the destructive Disable -> Delete -> Create sequence (no validation
  bypass exists here - the CLI's --no-validate is an operator's flag,
  not an agent's), the emit re-emission caution folded into the elicit
  wording, stamped operation: recreate
- deploy_rollback mirrors gaffer rollback: hash-prefix resolution via
  remote.FindVersionByHash, the unchanged short-circuit, the in-place
  refusal for create-time dimensions, stamped operation: rollback, full
  target hash in the result

A shared runOperateVerb/operateSetup core does the project gate, the
system-projection refusal, connect, existence check, and target/
production resolution (operateTarget - the CLI's resolveOperateTarget
semantics); every RPC gets its own timeout budget like the CLI's rpc
helper. These are the first handlers to consume the request (the gate
needs req.Session); annotations mark pause/resume non-destructive and
the rest destructive.
- deploy_recreate now runs the diagnostics preflight the CLI runs by
  default (engine.Preflight): a source that compiles can still carry
  error-severity diagnostics known to fault on the server, and Delete
  runs before Create - without the gate a healthy projection could be
  destroyed and rebuilt into a faulting one, a state the CLI can only
  reach via an explicit --no-validate
- recreate joins delete's always-elicit tier: it embeds a delete
  (state wiped; deleteEmitted destroys emitted-stream data), and the
  CLI's recreate confirm is unconditional too. Rollback deliberately
  stays production-only: in-place and roll-forward-able, the one
  recorded deviation from the CLI's confirm-always
- the rollback elicit names the hash movement (current -> target), so
  the human confirms a specific change, not an abstract one
- the CLI hand-off strings carry --delete-emitted when set, and the
  delete error messages match the CLI's could-not-delete phrasing
- recreate's step closures drop their redundant timeout wrappers -
  deploy.Recreate bounds every step itself
- resume and abort join the system-projection refusal test table
One lifecycle against a live KurrentDB: pause/resume/abort off
production (no elicit, nil request), recreate declined-then-accepted
(declined leaves the projection in place; accepted stamps a
gaffer-attributed recreate in history), rollback to the recreated
content by hash prefix plus the unchanged short-circuit, and delete
refused for a no-elicitation client, refused on decline, then accepted
and verified gone. The gate harness from the unit tests composes with
the live fixture - the fake human is a real in-memory MCP client.
- The MCP docs page gains the verbs bullet, explaining the elicitation
  gate: production writes confirm through the client mid-call, recreate
  and delete ask every time, and a client without elicitation is
  pointed at the CLI
- The server's initialize instructions mention the lifecycle verbs
- Changeset for the release notes
Docs-review pass: split the gate sentence, add the per-verb nuances the
tool descriptions carry (abort's skipped checkpoint, recreate's
preflight, rollback's in-place refusal, delete's deleteEmitted), and
fix the changeset's hyphen-as-dash.
@George-Payne George-Payne requested a review from a team as a code owner July 6, 2026 08:22
@George-Payne George-Payne added enhancement New feature or request cli Gaffer Go binary (CLI commands, TUI, DAP server, deploy, plugin invocation) mcp MCP server - tools, resources, prompts labels Jul 6, 2026
@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

UI-1731

@George-Payne George-Payne self-assigned this Jul 6, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add MCP deploy operate verbs with human-in-the-loop production write gate

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add MCP deploy operate verbs mirroring CLI lifecycle commands, with safe defaults.
• Gate production and destructive writes via MCP elicitation confirmation (fail-closed).
• Share CLI ledger stamping and rollback hash resolution across CLI and MCP server.
Diagram

graph TD
  client{{"MCP Client"}} --> server(["gaffer MCP server"]) --> ops(["Operate verbs"]) --> gate(["confirmWrite gate"]) --> rclient(["remote.Client"]) --> kdb[("KurrentDB")]
  ops --> stamp(["stamp.Ledger"]) 
  ops --> rb(["rollback resolver"]) 
  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _svc(["Service"]) ~~~ _mod(["Module"]) ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Two-phase tools (request + execute with confirmation token)
  • ➕ Avoids relying on client elicitation support
  • ➕ Can enforce explicit human-provided token in args
  • ➖ More complex UX and state handling (token issuance, expiry, replay protection)
  • ➖ Higher implementation surface area than leveraging MCP elicitation
2. Server-side policy (RBAC/allowlist) instead of elicitation
  • ➕ Works for headless clients; no interactive UI required
  • ➕ Centralized authorization independent of client features
  • ➖ Harder to guarantee “human-in-the-loop” per action
  • ➖ Requires additional identity/auth plumbing beyond current scope

Recommendation: Keep the current elicitation-based gate: it directly enforces human confirmation mid-call, fails closed (nil session / missing capability / non-accept action), and preserves parity with the CLI by reusing shared stamping and rollback resolution. The chosen approach minimizes new auth/state complexity while still preventing model-fabricated approvals.

Files changed (19) +1477 / -400

Enhancement (5) +601 / -2
deploy_gate.goAdd confirmWrite elicitation gate for production/destructive MCP writes +96/-0

Add confirmWrite elicitation gate for production/destructive MCP writes

• Introduces writeGate and confirmWrite to require a human confirmation via MCP elicitation for production writes and always-gated destructive verbs. Fails closed on nil session, missing elicitation capability, errors, or non-accept actions; refusal messages point to the equivalent CLI command.

cli/internal/mcpserver/deploy_gate.go

deploy_operate.goAdd deploy_pause/resume/abort/delete MCP tools with shared operate core +269/-0

Add deploy_pause/resume/abort/delete MCP tools with shared operate core

• Implements shared operate setup (project gate, system-projection refusal, connect, existence check, $server-info production detection) and RPC budgeting. Adds pause/resume/abort/delete tools with appropriate destructive hints and confirmWrite gating tiers; delete runs disable+delete with per-step timeouts.

cli/internal/mcpserver/deploy_operate.go

deploy_recreate.goAdd deploy_recreate tool with compile + diagnostics preflight and stamping +130/-0

Add deploy_recreate tool with compile + diagnostics preflight and stamping

• Implements recreate from local config with strict preflight: projection must exist in gaffer.toml, config must be valid, source must compile, and diagnostics must be clean (no bypass). Always requires confirmWrite and stamps operation: recreate in the deploy ledger; executes disable->delete->create via deploy.Recreate steps.

cli/internal/mcpserver/deploy_recreate.go

deploy_rollback.goAdd deploy_rollback tool with hash-prefix resolution, refusal checks, and stamping +96/-0

Add deploy_rollback tool with hash-prefix resolution, refusal checks, and stamping

• Implements rollback by resolving a content-hash prefix from deploy_history and updating the live query in place, returning unchanged when already at target. Refuses incompatible targets (engine version / emitted-stream tracking) via remote.RollbackRefusal, gates production via confirmWrite, and stamps operation: rollback.

cli/internal/mcpserver/deploy_rollback.go

server.goRegister new deploy_* operate tools and update server instructions +10/-2

Register new deploy_* operate tools and update server instructions

• Adds deploy_pause/resume/abort/delete/recreate/rollback tools to the MCP server tool registry. Updates user-facing instructions to describe lifecycle management and the human confirmation gate for destructive/production writes.

cli/internal/mcpserver/server.go

Refactor (5) +239 / -194
history_rollback_modal.goReuse exported rollback refusal message from remote package +1/-1

Reuse exported rollback refusal message from remote package

• Switches rollback modal messaging to use remote.RollbackRefusal after moving rollback logic into the remote package. Keeps CLI UX consistent while sharing logic with MCP server.

cli/cmd/history_rollback_modal.go

ledger.goDelegate tool ledger stamping to shared stamp package +6/-64

Delegate tool ledger stamping to shared stamp package

• Replaces in-cmd revision/actor resolution with stamp.Ledger to share attribution logic with MCP server. Preserves tool/version/operation stamping even when env resolution fails, dropping only the actor.

cli/cmd/ledger.go

rollback.goMove rollback hash resolution and refusal logic into remote package +9/-129

Move rollback hash resolution and refusal logic into remote package

• Updates rollback command to call remote.NormalizeHashPrefix, Client.FindVersionByHash, and remote.RollbackRefusal. Removes the local history scan, hash matching, and validation helpers.

cli/cmd/rollback.go

rollback.goAdd shared rollback hash resolver and validation helpers in remote client +143/-0

Add shared rollback hash resolver and validation helpers in remote client

• Introduces hash-prefix normalization, history scanning with per-page RPC deadlines, ambiguity/not-found handling, and a shared RollbackRefusal check. Exposes Client.FindVersionByHash returning a VersionMatch used by both CLI rollback and MCP deploy_rollback.

cli/internal/remote/rollback.go

stamp.goExtract tool ledger stamping into reusable cli/internal/stamp package +80/-0

Extract tool ledger stamping into reusable cli/internal/stamp package

• Moves ledger stamping logic (tool/version/operation plus GAFFER_REVISION git HEAD resolution and GAFFER_ACTOR/derived principal resolution) out of cmd. Tightens behavior so zero/unresolved env yields an empty actor rather than scraping credentials.

cli/internal/stamp/stamp.go

Tests (7) +623 / -203
ledger_test.goUpdate ledger tests to lock unresolvable-env stamping behavior +15/-80

Update ledger tests to lock unresolvable-env stamping behavior

• Removes git/actor unit tests that moved to cli/internal/stamp. Adds a focused test ensuring tool/version/operation stamp remains when env resolution fails and actor is empty.

cli/cmd/ledger_test.go

rollback_test.goDrop rollback helper tests moved to remote/rollback_test.go +0/-123

Drop rollback helper tests moved to remote/rollback_test.go

• Removes tests for normalizeHashPrefix, hash matching/scan settling, and rollback refusal after moving that logic into cli/internal/remote. Retains rendering tests for rollback output formatting.

cli/cmd/rollback_test.go

deploy_gate_test.goUnit-test confirmWrite gating over in-memory MCP transports +158/-0

Unit-test confirmWrite gating over in-memory MCP transports

• Adds tests covering accepted/declined/cancelled/empty actions, elicitation errors, missing capability, nil session fail-closed behavior, and Always gating off production. Uses a real in-memory client/server MCP session to exercise Elicit end-to-end.

cli/internal/mcpserver/deploy_gate_test.go

deploy_operate_integration_test.goIntegration test covering all operate verbs against live KurrentDB +144/-0

Integration test covering all operate verbs against live KurrentDB

• Adds an integration test verifying pause/resume/abort behavior, recreate/rollback ledger classification, rollback outcomes (unchanged vs rolled-back), and delete gating semantics. Confirms declined elicits do not change state and missing elicitation capability refuses delete.

cli/internal/mcpserver/deploy_operate_integration_test.go

deploy_operate_test.goUnit-test operate verb validation and early refusals +80/-0

Unit-test operate verb validation and early refusals

• Adds tests ensuring all operate verbs fail cleanly without a project, require a name, refuse system projections before connecting, validate unknown envs, and validate rollback hash before env resolution/connection.

cli/internal/mcpserver/deploy_operate_test.go

rollback_test.goAdd unit tests for remote rollback resolution utilities +136/-0

Add unit tests for remote rollback resolution utilities

• Adds tests for NormalizeHashPrefix validation, hash matching and scan settling behavior, and RollbackRefusal messaging. Includes a helper for representing deleted (tombstone) history entries.

cli/internal/remote/rollback_test.go

stamp_test.goMove git revision and actor override tests into stamp package +90/-0

Move git revision and actor override tests into stamp package

• Adds isolated git-repo tests validating clean/dirty revision detection and GAFFER_REVISION override behavior. Verifies GAFFER_ACTOR override for actor resolution independent of connection-derived principal logic.

cli/internal/stamp/stamp_test.go

Documentation (2) +14 / -1
mcp-operate-verbs.mdAdd patch changeset documenting new MCP operate verbs and gating +12/-0

Add patch changeset documenting new MCP operate verbs and gating

• Introduces a patch changeset describing the new deploy_* operate verbs and their CLI parity. Documents the human-in-the-loop elicitation requirement for production and always-gated destructive operations.

.changeset/mcp-operate-verbs.md

mcp.mdDocument new MCP operate verbs and elicitation gating behavior +2/-1

Document new MCP operate verbs and elicitation gating behavior

• Updates MCP documentation to include deploy_pause/resume/abort/recreate/rollback/delete and explains their semantics and safety constraints. Documents elicitation-based human confirmation for production and always-gated destructive operations, including behavior for clients without elicitation support.

docs/src/content/docs/cli/mcp.md

CI's KurrentDB is a release image that ignores ledger metadata, so the
recreate's stamped create reads back metadata-less and history
classifies it as an external edit. Assert the gaffer-stamped recreate
classification only when GAFFER_TEST_LEDGER says the server keeps
metadata - the same gate remote's ledger integration tests use. The
rest of the lifecycle (outcomes, refusals, rollback by content hash)
holds on both server generations and stays asserted unconditionally.
@qodo-code-review

qodo-code-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Unescaped CLI suggestion ✓ Resolved 🐞 Bug ⛨ Security
Description
Refusal messages show a CLI command assembled by concatenating the (user-controlled) projection name
without quoting/escaping, so a crafted name can make the suggested command misleading or unsafe to
copy/paste. This is an operator-facing output-sanitization gap that undermines the intent of the
refusal’s “run this in CLI instead” guidance.
Code

cli/internal/mcpserver/deploy_operate.go[R247-252]

+	if r := confirmWrite(ctx, req, writeGate{
+		Verb: spec.verb, Name: in.Name,
+		Target: conn.target, Production: conn.production,
+		Always: spec.always, Warning: spec.warning,
+		CLI: fmt.Sprintf("%s %s", spec.cli, in.Name),
+	}); r != nil {
Evidence
The MCP server constructs the CLI fallback command by concatenating spec.cli and in.Name without
quoting, and confirmWrite embeds that command in refusal strings. operateSetup only validates
non-empty and not $-prefixed names, so other characters are not excluded before becoming part of
the suggested CLI command.

cli/internal/mcpserver/deploy_operate.go[53-65]
cli/internal/mcpserver/deploy_operate.go[247-252]
cli/internal/mcpserver/deploy_gate.go[45-47]
cli/internal/mcpserver/deploy_gate.go[52-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When gated writes are refused, the fallback CLI command is built via string concatenation with the projection name (and sometimes other args) and then embedded in messages. Because the name is not quoted/escaped, unexpected characters (spaces, quotes, shell metacharacters, backticks) can change how the command reads and how an operator might copy/paste it.

### Issue Context
This does not execute server-side, but it is still an operator safety concern: the refusal message is specifically instructing the human what to run next.

### Fix Focus Areas
- cli/internal/mcpserver/deploy_operate.go[247-252]
- cli/internal/mcpserver/deploy_recreate.go[87-97]
- cli/internal/mcpserver/deploy_rollback.go[78-83]
- cli/internal/mcpserver/deploy_gate.go[46-47]

### Suggested fix
- Introduce a small helper to safely render CLI arguments (e.g., POSIX single-quote escaping) and apply it to projection names and other user-controlled args.
- Alternatively (simpler), render the *entire* suggested command using `%q` (or `strconv.Quote`) and avoid wrapping it in backticks, so formatting can’t be broken by backticks in the name.
- Ensure all refusal paths that mention `g.CLI` use the sanitized/quoted version consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Elicitation mode check bug 🐞 Bug ☼ Reliability
Description
confirmWrite’s capability predicate only rejects URL-only elicitation clients, but it does not
reject the case where elicitation is present yet neither Form nor URL is provided, even though the
implementation unconditionally calls Session.Elicit with a form-style request. That can lead to
non-deterministic failures (SDK-dependent) instead of a clear “client doesn’t support elicitation”
refusal.
Code

cli/internal/mcpserver/deploy_gate.go[R52-55]

+	if p := req.Session.InitializeParams(); p == nil || p.Capabilities == nil || p.Capabilities.Elicitation == nil ||
+		(p.Capabilities.Elicitation.Form == nil && p.Capabilities.Elicitation.URL != nil) {
+		return toolError("%s %q on %s needs a human confirmation, and this MCP client doesn't support elicitation; run `%s` instead", g.Verb, g.Name, prodWhere(g.Target, g.Production), g.CLI)
+	}
Evidence
The capability check currently only treats (Form == nil && URL != nil) as unsupported, and then
always calls req.Session.Elicit with a schema that assumes form support. This leaves a gap where
the code proceeds without confirming that a usable elicitation mode exists.

cli/internal/mcpserver/deploy_gate.go[48-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`confirmWrite` intends to fail fast when the client cannot perform elicitation. The current check rejects URL-only clients, but still allows `Elicitation != nil` with both `Form == nil` and `URL == nil`, then proceeds to call `Session.Elicit`.

### Issue Context
This tool path assumes a form-based confirm (it sends a JSON-schema `RequestedSchema`). If the client cannot actually handle a form elicitation, the server should refuse deterministically with the “doesn’t support elicitation” message.

### Fix Focus Areas
- cli/internal/mcpserver/deploy_gate.go[48-67]

### Suggested fix
- Tighten the predicate to require a form capability explicitly (e.g., `p.Capabilities.Elicitation.Form != nil`) since the server sends a form schema.
- Optionally, if URL-only elicitation is meant to be supported, implement the URL flow (if the SDK/spec supports it) rather than rejecting it; otherwise, reject any `Form == nil` consistently.
- Add/adjust a unit test to cover the `Elicitation != nil` but no usable method scenario, ensuring the refusal is deterministic and uses the intended message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread cli/internal/mcpserver/deploy_operate.go
Comment thread cli/internal/mcpserver/deploy_gate.go
The gate's refusals tell the human which command to run instead, and
the projection name in that string is model-controlled - a crafted
name could smuggle shell syntax into a command someone copies and
runs. Single-quote it (POSIX quoting disables all expansion; embedded
single quotes close-escape-reopen). Rollback's hash prefix stays bare:
it is validated hex before anything else runs.
An agent test-drove the verbs against a production-flagged database and
found the prompt under-signalled: "Pause "orders" on production
Navigator testing?" buries the risk mid-sentence, reads like a server
named "production Navigator testing", and never shows the env key the
operator actually chose. The new format front-loads it, plain text and
single-line throughout (Claude Code renders elicit messages without
markdown):

  PRODUCTION [env.production]: pause projection "orders" on
    "Navigator testing"? Stops after a final checkpoint; resume with
    deploy_resume.
  Delete projection "orders" on "local" [env.local]? Removes the
    projection, its state, and checkpoints. No undo.

Each verb now supplies its complete lowercase action phrase (rollback
splits its verb around the name, so the gate can't assemble verb+name)
and a required consequence stating what the human is agreeing to -
notably abort's skipped checkpoint, the distinction status can't show.

The no-undo tier (delete, recreate) escalates on production: the
confirm requires typing the projection name into the form, checked
against the input; off production it stays accept/decline - friction
proportional to blast radius. The SDK's schema validation already
rejects a missing or mistyped field before the gate's value check.

deploy_status and deploy_plan now echo the resolved target and
production flag alongside env, so an agent reading status alone can
tell it is pointed at a production database before deciding to mutate.
Claude Code renders the field description at the input, so carrying
"Type the projection name ... to confirm" in both the message and the
field showed it twice. It now lives on the field alone, and the name
reads "orders" rather than ("orders") - the parens added nothing over
the quotes.
The confirm field was free text, so a client had nothing to validate
against and the human could Accept a wrong name only to learn
afterwards that nothing happened. The schema now carries
pattern: ^<name>$ (regexp-escaped), so a client that enforces JSON
Schema on elicitation input can gate its Accept inline. Claude Code
currently doesn't (tested; it checks type and presence only), but the
constraint is strictly better: enforcing clients close the seam,
ignoring clients lose nothing, and the SDK validates the result against
it in the round-trip either way - which the gate now reports as "the
typed name must match ... exactly; nothing was changed" instead of a
raw schema-validation error.

pattern is an extension over the MCP elicitation string-schema subset
(a one-item enum, the subset's only exact-match shape, would render as
a picker and remove the friction). The server-side equality check
stays as the real guard.
@George-Payne George-Payne merged commit 15cc888 into main Jul 6, 2026
3 checks passed
@George-Payne George-Payne deleted the ui-1731/mcp-operate-verbs branch July 6, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Gaffer Go binary (CLI commands, TUI, DAP server, deploy, plugin invocation) enhancement New feature or request mcp MCP server - tools, resources, prompts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant