Remote Mode: CLI and MCP against VisionStudio Cloud¶
By default every visionstudio command reads and writes the local Dolt
database. Remote mode points the same commands, and the same MCP server,
at a VisionStudio Cloud tenant instead. It is not a second implementation. The
CLI and pkg/mcpserver talk to pkg/service, which talks to the store.Store
interface. Remote mode swaps the local Dolt store for pkg/remote, an HTTP
implementation of that interface that calls the cloud tenant API at
/o/<tenant>/api/v1/....
Remote mode and local mode are separate systems. Nothing syncs between them, and remote mode is opt-in per invocation.
Quick start¶
# 1. Store an API key (create one in VisionStudio Cloud). The key is
# verified against the tenant before it is saved.
visionstudio cloud login --remote https://cloud.example.com --remote-tenant acme
# 2. Check what remote mode will use.
visionstudio cloud status --remote https://cloud.example.com
# 3. Run commands against the tenant.
visionstudio initiative list --remote https://cloud.example.com
visionstudio initiative create --remote https://cloud.example.com \
--id INIT-ACME-001 --title "Launch" --workflow pbhq-lite
Settings and precedence¶
| Setting | Resolved from (first match wins) |
|---|---|
| Cloud URL (turns remote mode on) | --remote <url> → $VISIONSTUDIO_REMOTE_URL |
| Tenant | --remote-tenant → $VISIONSTUDIO_REMOTE_TENANT → URL ending in /o/<tenant> → tenant saved by cloud login |
| Credential | $VISIONSTUDIO_REMOTE_TOKEN → key saved by cloud login for that URL |
Notes:
--remote https://cloud.example.com/o/acmeis equivalent to--remote https://cloud.example.com --remote-tenant acme.- The tenant is the organization's ID or its slug. A bearer token names
organizations by ID only, so use the ID with one. A URL ending in the
former
/t/<tenant>still works; the client calls/o/. - The tenant flag is
--remote-tenant, not--tenant, becausesyncandpullalready use a local--tenantflag with a different meaning. --remotecannot be combined with--dsnor--data-dir.cloud loginremembers the URL forcloud statusandcloud logout. It never switches other commands to remote mode. Those still need--remoteor$VISIONSTUDIO_REMOTE_URL.- Remote mode applies to commands that open the store through the shared
connection path, which covers the entity commands (
initiative,rmi,phase,program,work, ...) andvisionstudio mcp. Commands that manage the local database or UI directly (db,app,dashboard,ui,sync,pull) ignore--remote.
Credentials¶
visionstudio cloud login reads a VisionStudio Cloud API key (vsc_live_...)
or a session token from one of these sources:
- standard input, with
--with-token(for exampleop read ... | visionstudio cloud login --with-token) $VISIONSTUDIO_REMOTE_TOKEN- a hidden interactive prompt
The key is stored per cloud URL in
~/.productbuildershq/visionstudio/credentials.json. The file is kept separate
from config.json and is always written with owner-only (0600) permissions.
visionstudio cloud logout deletes the local copy, and --all deletes every
stored key. Revoke a key in VisionStudio Cloud to invalidate it on the server
side. cloud status shows only a masked form of the key, never the full value.
A browser-based device login flow is planned. Until it ships, paste an API key.
MCP with Claude Code¶
Run the MCP server locally in remote mode, so agent sessions read and write the cloud tenant:
{
"mcpServers": {
"visionstudio": {
"command": "visionstudio",
"args": ["mcp", "--remote", "https://cloud.example.com/o/acme"]
}
}
}
Run visionstudio cloud login first, or pass the key through the server's
environment:
{
"mcpServers": {
"visionstudio": {
"command": "visionstudio",
"args": ["mcp"],
"env": {
"VISIONSTUDIO_REMOTE_URL": "https://cloud.example.com/o/acme",
"VISIONSTUDIO_REMOTE_TOKEN": "${VISIONSTUDIO_REMOTE_TOKEN}"
}
}
}
}
Or register it from the command line:
What works today¶
The cloud tenant API serves the full agent-work surface. Store operations
that have no cloud endpoint return a ... is not supported in remote mode
error that names the area. They never return empty or invented data.
| Area | Remote support |
|---|---|
| Programs | create, get, list, update |
| Initiatives | create, get, list, update (including lifecycle transitions) |
| Phases | create, list, delete (refused while the phase has RMIs) |
| RMIs | create, get, list (by initiative, repository, status, or all), update (status changes, moves, origin, context spec) |
| RMI dependencies | create, list (per RMI, all) |
| Assignments (work leases) | claim, get, get active, list (active, all), renew, hand off, release, complete |
| Delivery evidence | create, list (by RMI, by initiative, all) |
| Releases | get, list (by repository, by initiative, all) — read-only |
| Repositories | get, list (by organization, all) — read-only |
| Spec workflows | get, list, select for an initiative, get an initiative's selection |
| Spec documents | create, get, list (by initiative, by repository, all), update, delete |
| Judge results | create, list by initiative |
MCP tools that work end to end include initiative_list, initiative_get,
initiative_create, rmi_create, program_list, program_create,
work_ready, task_claim, task_update (status, evidence, handoff,
completion), task_release, workflow_list and workflow_select. On the
CLI, initiative, rmi (including update, move and dep), phase,
program and work commands run against the tenant; rmi create validates
--repo against the tenant's repository registry.
The server enforces the lease rules a shared database needs. A claim while
another session holds a live lease on the RMI, or an update to an assignment
that is no longer active (released, completed, expired, or taken over), fails
with 409 LEASE_CONFLICT, which errors.Is matches as both
remote.ErrLeaseConflict and remote.ErrConflict.
Creates that set fields the cloud create endpoint cannot carry (an
initiative's status, home repo, program, specs or lifecycle timestamps; an
RMI's status, origin, context_spec or completed_at) are sent as a create
followed by a full update. The two requests are not atomic: if the update
fails, the entity exists with only its create fields and the error says so.
Remaining gaps¶
These areas have no cloud endpoint yet and fail with a not-supported error:
- initiative dependencies
- repository writes (register, update, archive, remove) and repository dependencies
- release writes (
release recordand friends) - organizations and people
- spec workflow writes (workflows come from the embedded catalog)
- capability models and maturity assessments, DevX period reports, and PRISM roadmaps, goals and documents
Embedding: custom HTTP clients and request signing¶
pkg/remote is a library. A program can embed the MCP server against a cloud
tenant with its own HTTP stack, for example an agent launcher that signs every
request:
rs, err := remote.New("https://cloud.example.com", "acme",
remote.WithTransport(signingRoundTripper), // or remote.WithHTTPClient(hc)
)
if err != nil {
return err
}
return mcpserver.Run(ctx, service.New(rs))
Bearer auth is optional. remote.WithToken adds Authorization: Bearer <key>,
and when no token is set the store sends no Authorization header, so the
transport can authenticate however it needs to. The stock binary uses Go's
default proxy settings (HTTPS_PROXY), so a local signing proxy also works
without code changes.
Errors are typed. *remote.APIError carries the HTTP status, the server's
machine-readable Code (for example INVALID, INVALID_REFERENCE,
NOT_FOUND, CONFLICT, LEASE_CONFLICT) and its message. errors.Is
matches remote.ErrUnauthorized (401), remote.ErrForbidden (403, not a
member of the tenant), remote.ErrInvalid (400), remote.ErrInvalidReference
(400 INVALID_REFERENCE: the body names an entity that does not exist),
remote.ErrNotFound (404), remote.ErrConflict (409),
remote.ErrLeaseConflict (409 LEASE_CONFLICT) and remote.ErrNotSupported.
Store.Ping checks the URL, credential and tenant membership with
GET /o/<tenant>/api/v1/me. Store.Me returns the credential's principal and
every tenant membership (GET /api/v1/me), which visionstudio cloud status
prints after verifying access.
For tests, pkg/remote/remotetest provides an in-process fake of the tenant
API. It mirrors the cloud contract (strict request decoding, [] for empty
lists, the {"assignment": null} and {"workflow": null} wrappers, the error
codes, and both lease-conflict rules) and serves requests through the real
service layer over an in-memory store.