Skip to content

Add experimental branch commands and MCP tools - #265

Closed
nathanjcochran wants to merge 30 commits into
mainfrom
nathan/branches
Closed

nathanjcochran wants to merge 30 commits into
mainfrom
nathan/branches

Conversation

@nathanjcochran

@nathanjcochran nathanjcochran commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

First pass at branches for Fluid Services, in the CLI and MCP. Branch commands and tools are behind TIGER_EXPERIMENTAL. It's built on the API's service-nested branch endpoints (/projects/{project_id}/services/{service_id}/branches/...) and its new resolve endpoint (/projects/{project_id}/resolve), both synced into openapi.yaml. Neither is deployed yet, so this has only been tested against mocks.

Resolving refs. The branch and db commands resolve refs (an ID or a name) through the resolve endpoint, via common.Resolve, restricted by its type parameter:

  • tiger branch commands resolve only branches. That one call also gives the parent service ID the nested branch endpoints need.
  • tiger db commands resolve anything they can connect to: a service, a read replica set, or (behind the experimental gate) a branch.

tiger service commands still resolve through the ref filter on the service list, unchanged. The resolve endpoint's SERVICE type doesn't match read replica set IDs, which those commands accept today.

CLI. tiger branch has create, list, get, delete, start/resume, stop/pause, resize, rename, and update-password. tiger service branch is the same command as tiger branch create, built by one shared builder.

  • Branch commands take a branch by ID or name. There's no default branch: service_id only ever holds a service ID, and create never changes it.
  • Shell completion offers branch names, falling back to IDs, from the API's internal endpoint listing every branch in a project.
  • create and list take a service, falling back to the default service.
  • create saves the branch's initial tsdbadmin password (returned once by the API) the same way service create/fork do, and ends with "Connect with: tiger db psql ".
  • Flags and output follow the matching tiger service commands. CPU/memory are checked against the fluid combinations.
  • create and resize take --iops, matching the flag on the service commands: it's sent only when given, and resize accepts an IOPS-only change. The branch's IOPS is shown when the API returns it.
  • get, create, and list show the parent service's name with its ID (service_name in JSON/YAML). get fetches the parent for it; a failed fetch is a warning, and the ID is shown alone.
  • tiger service get shows a fluid service's branch count (branch_count), behind the gate. It lists the service's branches to count them; a failed listing is a warning, and the count is left out.

MCP. There's a tool for each branch command (branch_create, branch_list, branch_get, branch_delete, branch_start, branch_stop, branch_resize, branch_rename, branch_update_password), registered only behind the experimental gate.

  • They take IDs only. Since the resolve endpoint also matches names, a match by name is refused.
  • branch_create and branch_resize take an iops parameter, like the flag. The branch tools return a branch's IOPS in resources, as the service tools do.
  • branch_get returns the parent's service_name, and service_get returns branch_count for a fluid service behind the gate (the field is left out of its schema otherwise).
  • db_query and db_schema also go through the resolve endpoint now, so behind the gate they accept branch IDs too.

Read-only. A branch has no environment tag and always counts as DEV (common.BranchEnvironmentTag). So read_only=prod allows every branch operation, even on branches of a PROD service, and all blocks them all.

Supporting changes.

  • Some existing functions took an api.Service and now need to work with branches too:
    • Password storage (the keyring and pgpass Save/Get/Remove, SavePasswordWithResult, and GetPassword) takes a PasswordTarget: the project ID, ID, and endpoint a password is keyed by. ServicePasswordTarget and BranchPasswordTarget build one. The keyring key format is unchanged.
    • ConnectionTarget, which the db commands and the db_query/db_schema tools connect through, held two api.Service values (the one to connect to, and the one whose credentials to use). It now holds just what connecting needs: kind (service, read replica, or branch), ID, name, status, environment, endpoint, pooler, and the credential owner. There's a constructor for each kind.
    • GetConnectionDetails takes a ConnectionTarget, and CheckServiceReady became CheckReady, taking a status.
  • The db commands' help (behind the gate) says they take a branch, and their errors name a branch and the tiger branch commands (or branch_* tools) for one. db psql skips its read-replica menu for a branch, and its password recovery resets a branch's password through the branch endpoint.
  • The wait loop is shared by WaitForService and WaitForBranch.
  • sizeCompletion takes a configs func, so the branch commands can complete fluid-only sizes.
  • A few helpers moved to _helper.go files because more than one group now uses them.

Changes to existing (public) commands

  • Release ordering. The db commands now depend on the resolve endpoint (marked x-tigerdata-internal), so this can't be released before the API ships it.
  • No guessed ports. A service endpoint or connection pooler without a port is no longer shown with 5432 or 6432 filled in, in tiger service get/list or the MCP service detail. pgpass password storage, which keys entries by host and port, refuses such an endpoint rather than assuming 5432. Branches follow the same rule.
  • Empty lists stay structured. service list, service backup list, and branch list print [] for an empty list with -o json/yaml instead of nothing. Empty-result messages appear only in table output, on stderr.
  • service delete lists branches. The API deletes a fluid service's branches along with it. tiger service delete lists them before its confirmation prompt and after the delete, and service_delete names them in its PROD prompt and its result. This isn't gated, since the branches go either way; only the help text and tool description mention branches behind the gate.
  • db messages. "Not found" now reads "service or read replica set '…' not found". A read replica set that isn't active reads as "not ready" rather than "paused". Under discussion; likely to be reverted to the old behaviour.

Follow-ups

  • Move the service commands to the resolve endpoint. Once read replica sets have their own commands (tiger replica … or similar), the tiger service commands no longer need to accept replica set IDs and can move to the resolve endpoint. Until then they keep the service-list lookup, which is the only way to reach a replica set today (TODO on resolveService).
  • branch logs. The API has no branch logs endpoint, and the PRD puts branch logs at GA.
  • Missing fields. There's no storage breakdown (the API doesn't return one).
  • Branch Console URL. The branch commands don't print a Console URL, because the Console doesn't support branches yet. Its prototype puts a branch at /projects/<project>/branches/<branch>/overview, not under its parent service as the PRD has it, so add one once that's settled (TODO in prepareBranchForOutput).
  • Service Console URL. tiger service get prints /dashboard/services/<service>, a legacy path the Console redirects into the browser's active project, which may not be the CLI's. The canonical form is /projects/<project>/services/<service>.
  • MCP test harness. An unexpected mock call inside a tool handler makes the test hang until the go test timeout instead of failing, because gomock's Fatalf ends the handler goroutine without a response.

Copies the spec from the project-level branches API proposal and regenerates the API client and mocks.
Adds 'tiger branch' (create, list, get, delete, start, stop, resize, rename,
update-password) and 'tiger service branch', behind TIGER_EXPERIMENTAL and
backed by the preview project-level branch endpoints. Branch commands take
branch IDs only for now; create and list take a service by ID or name.

A branch starts with its parent's password, which the platform can't return,
so create copies the parent's saved password into a new entry for the branch.
Branches count as DEV for the read-only gate. The wait loop is shared between
services and branches.
Adds branch_create, branch_list, branch_get, branch_delete, branch_start,
branch_stop, branch_resize, branch_rename, and branch_update_password,
registered only behind TIGER_EXPERIMENTAL. They mirror the branch CLI
commands and the existing service tools, taking IDs only. Branches count
as DEV, so none of them prompt for confirmation and read_only=prod leaves
them usable.
A branch no longer becomes the default on create, so 'tiger branch create'
and 'tiger service branch' lose --no-set-default and branch_create loses
set_default. Since service_id only ever holds a service ID, the branch
commands no longer fall back to it and always take an explicit branch ID.
@nathanjcochran nathanjcochran self-assigned this Oct 19, 2026
Syncs openapi.yaml with the API's service-nested branch endpoints and its
new resolve endpoint, which turns a ref (ID or name) into the service,
branch, or read replica set it names.

Every ref now goes through the resolve endpoint, restricted by type: the
service commands resolve services, the branch commands branches, and the db
commands anything they can connect to. Branch commands now accept names,
and get the parent service ID the nested endpoints need from the same
lookup. The db commands and the db_query/db_schema tools can connect to
branches behind the experimental gate. Service commands no longer accept
read replica set IDs.

A branch now gets its own initial password on create, which is saved like
a new service's, replacing the copy of the parent's password.
The resolve endpoint's SERVICE type doesn't match read replica set IDs,
which the service commands accept today, so they go back to resolving
through the ref filter on the service list until the CLI's handling of
replica sets is reworked. The branch and db commands stay on the resolve
endpoint.
Creating a branch gives its tsdbadmin user a new password.
Matches the --iops flag on the service commands: it is sent only when
given, and resize accepts an IOPS-only change. The branch resize endpoint
requires CPU and memory, so an IOPS-only resize sends the branch's
current ones. branch_create and branch_resize take a matching iops
parameter.
A branch endpoint without a port is shown as no endpoint at all, in both
the CLI table and the MCP branch detail, rather than with 5432 filled in.
A service endpoint or connection pooler without a port is no longer shown
with 5432 or 6432 filled in, in the CLI table or the MCP service detail, and
pgpass storage, which keys entries by host and port, refuses one rather
than assuming 5432.
Password storage and database connections no longer take an api.Service.
Password storage takes a PasswordTarget (the project, ID, and endpoint a
password is keyed by), and ConnectionTarget carries just what connecting
needs (kind, ID, name, status, environment, endpoint, pooler, and whose
credentials to use) instead of two services. Services, branches, and read
replica sets each convert into these, and BranchAsService is gone.
Brings in the branch API's latest changes: optional CPU and memory on
branch resize, an internal endpoint listing every branch in a project, and
the corrected 250m / 2 GB default branch size. The resolve endpoint's
changes are kept on top.

An IOPS-only branch resize now leaves out CPU and memory, which the API
keeps, instead of sending the branch's current ones.
Uses the new endpoint listing every branch in a project. Like the service
completion, each branch is offered once, by name, falling back to its ID
when what is typed matches the ID instead.
Moves the branch ref helpers out of branch.go and into ref_helper.go
(renamed from service_ref_helper.go), next to their service counterparts,
so the two are easy to find and compare.
branch get fetches the parent service to show its name alongside its ID,
and service get counts a fluid service's branches, behind the
experimental gate. branch create and list already hold the parent, so
they show its name too. Neither extra fetch blocks the output: a failure
is a warning, and the field is left out. The branch_get and service_get
tools return the same fields.

The service and branch output functions now take an options struct
instead of a run of positional arguments, and read the output format from
the config rather than taking it separately.
The Console doesn't support branches yet, and its prototype puts them at a
different path than the one the CLI printed, so the link was likely wrong.
Leave it out until the Console's URL is settled.
service list, service backup list, and branch list returned early with a
message before checking the output format, so -o json and -o yaml printed
nothing for an empty list. They now print [], and the message is shown
only for table output. Every empty-result message (including service
backup region list and service metrics series, which printed theirs to
stdout) goes to stderr from the output function's table case.

The output*Table functions now take their writer first, matching
util.SerializeToJSON and util.SerializeToYAML.

CLAUDE.md spells out both conventions.
The API deletes a fluid service's branches when the service is deleted,
without asking. tiger service delete and the service_delete tool now list
them: the CLI before its confirmation prompt and after the delete, and the
tool in its PROD confirmation prompt and its result message. The listing
isn't gated, since the branches are deleted either way; only the help text
and tool description mention branches behind the experimental gate. A
failed listing is a warning and doesn't stop the delete.
# Conflicts:
#	internal/cmd/completion_helper.go
#	internal/cmd/completion_helper_test.go
#	internal/cmd/service.go
#	internal/cmd/service_get_test.go
#	internal/mcp/service_get_test.go
#	internal/mcp/utils.go
The branch tools returned IOPS as a top-level field because ResourceInfo
had none. Now that the service IOPS work added one, put the branch's IOPS
there to match the service tools.
Both the service and branch password endpoints answer success with a 204
and nothing else, but the CLI and MCP also accepted a 200. Check for the
one status the API sends, as the other 204 endpoints do.
Move withGenerateSecurePassword into main_test.go, with the MCP helper's
signature, and use it in the service update-password tests in place of
gomock.Any() and length checks. Add a case for a failed password
generation to every command and tool that generates one.
NewReplicaConnectionTarget only ever read the primary's credentials, so
it takes those (from the new ServiceCredentials) instead of the primary's
ConnectionTarget, and callers no longer build a target just to pass it in.
A branch's credentials now record its parent service, and the password
reset helpers pick the service or branch password endpoint from them. db
psql's recovery menu offers the reset for branches too, and branch
update-password goes through the same helpers instead of its own copy.
Behind the experimental gate, each db command's help says it also takes a
branch. A paused or unready branch is reported as a branch, not a service,
in both the CLI and MCP.
It only aliased the fluid platform's configurations, which are what a
branch takes since only fluid services can be branched.
The exported Resolve only filled in resolve's error wording, and had one
caller in its own package. Callers now pass resolve what they're looking
for directly.
@nathanjcochran

Copy link
Copy Markdown
Member Author

Split into a stack of smaller PRs: #268, #269, #270, #271, #272.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant