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
41 changes: 41 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,47 @@ Use `superRefine` (or a discriminated union) to encode cross-field invariants

When migrating existing tools from `tool()` to `registerTool()`: the description and annotations move into the config object as named fields (`description`, `annotations`); the input schema becomes `inputSchema`. The handler signature is unchanged.

#### Write gating: use `requireConfirmation`

Tools that mutate state (deploy, create, run, submit, delete, non-GET `execute`) must call `requireConfirmation` from `src/helpers/requireConfirmation.ts` before performing the write, and return early when `confirmed` is false. The helper handles three branches transparently:

1. `OCTOPUS_SKIP_ELICITATION=true` env var → bypass (automation/CI). Strict string equality with `"true"`.
2. Client advertises elicitation capability → SDK emits `elicitation/create` and the helper returns `{ confirmed: result.action === "accept" }`. `decline` and `cancel` both map to `false`.
3. Client does not advertise elicitation → helper falls back to the `confirm` arg the tool surfaced in its own input schema.

Branch 3 requires every write tool's input schema to include a `confirm` field:

```typescript
confirm: z
.boolean()
.optional()
.describe(
"Required only when the MCP client does not support elicitation. " +
"Set to true to confirm the write; otherwise the tool aborts.",
),
```

Pass it through as `fallbackConfirm`. The helper returns a discriminated `ConfirmationResult` so callers can distinguish "user said no" from "user was never asked" — surface the latter as a hard error so the LLM stops and asks the user instead of treating the response as a real cancellation. The `unconfirmedResponse` builder produces the standard tool response for both branches; pass it the result and a lowercase noun phrase for the gated action:

```typescript
import {
requireConfirmation,
unconfirmedResponse,
} from "../helpers/requireConfirmation.js";

const confirmation = await requireConfirmation(server, {
message: `Deploy release ${version} to ${environment}?`,
fallbackConfirm: args.confirm,
});
if (!confirmation.confirmed) {
return unconfirmedResponse(confirmation, { action: "deployment" });
}
```

`reason` values: `accepted` / `envSkip` / `fallbackConfirm` (confirmed); `declined` / `cancelled` / `confirmationRequired` (not confirmed). `confirmationRequired` is the one `unconfirmedResponse` flags with `isError: true` — it means the gate is unreachable for this client+args combination, not that the user objected.

The handler closure captures `server` from the outer `register*Tool(server)` function — handlers do not receive `server` as an argument from the SDK. Place the gate after argument validation but before any expensive work (API client construction, network calls), so users don't spend an elicitation round-trip on a call that would have failed validation anyway.

### Resource System

Resources are addressable bodies fetched by URI (e.g. `octopus://spaces/Default/releases/Releases-1`). Each Resource is a single descriptor record in `RESOURCE_REGISTRY` with a URI template, mimeType, toolset, and an async `read` callback. Both the SDK Resource Template registration and the `read_resource` Tool backstop iterate the same registry, so adding a new resource type is one record — no edits to a central dispatcher.
Expand Down
Loading