5.11-b Write tools, non-destructive (phase 2)
- Dominant language
- JavaScript
- Stars
- 400
- Forks
- 89
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 146
Description
**Parent:** #7680 (5.11 Instance configuration)
**Tool file:** new `forge/ee/lib/mcp/tools/instanceConfig.js`
`readOnlyHint: false`, `destructiveHint: false`. Sibling actions that share one concept are consolidated into a single tool with a discriminator, so one tool covers several transitions of a config surface.
| Tool | Endpoint(s) | Scope | Annotation |
|---|---|---|---|
| `platform_set_instance_config` (`surface`: `ha`\|`customHostname`\|`protection`\|`autoUpdateStack`; `action`: `enable`\|`disable`\|`set`\|`clear`) | `PUT`/`DELETE /projects/:id/{ha,customHostname,protectInstance,autoUpdateStack}` | `project:edit` | write |
| `platform_update_instance_file` (rename/move a file, or set directory sharing) | `PUT /projects/:id/files/_/:path` | `project:files:edit` | write |
| `platform_upload_instance_file` (upload a file, or create a directory) | `POST /projects/:id/files/_/:path` | `project:files:create` | write |
| `platform_create_instance_http_token` (`instanceType`: `hosted`\|`remote`) | hosted: `POST /projects/:id/httpTokens`; remote: `POST /devices/:id/httpTokens` | `project:edit` (hosted), `device:edit` (remote) | write |
| `platform_update_instance_http_token` (`instanceType`: `hosted`\|`remote`) | hosted: `PUT /projects/:id/httpTokens/:tid`; remote: `PUT /devices/:id/httpTokens/:tid` | `project:edit` (hosted), `device:edit` (remote) | write |
**Consolidation notes:**
- `platform_set_instance_config` folds the four config surfaces (`ha`, `customHostname`, `protection`, `autoUpdateStack`) into one tool. `surface` selects the config and `action` selects the transition: `ha`/`protection` use `enable`/`disable`, `customHostname`/`autoUpdateStack` use `set`/`clear`. The handler maps `enable`/`set` to `PUT` and `disable`/`clear` to `DELETE` on the corresponding route, and validates the companion field required by the surface (`replicas` for `ha`, `hostname` for `customHostname`, `schedule` for `autoUpdateStack`).
- The http-token create/update tools cover both hosted and remote instances through an `instanceType` discriminator, mirroring the read tools. They route to `/projects/:id/httpTokens` (`project:edit`) or `/devices/:id/httpTokens` (`device:edit`) based on `instanceType`.
**Design notes:**
- The `ha`/`customHostname`/`protection` config DELETE routes disable a feature rather than destroy data, so they are the `disable`/`clear` action of a non-destructive tool alongside their PUT counterpart.
- **auto-update-stack is set/clear, not enable/disable:** `PUT /autoUpdateStack` sets a weekly restart schedule (an array of `{hour, day, restart}` entries), and `DELETE /autoUpdateStack` clears all schedule entries. There is no boolean toggle, hence the `set`/`clear` action names. These routes have no feature-flag gate.
- **The two file tools are distinct routes, not a rename:** `PUT /files/_/:path` (`project:files:edit`) updates file *properties* (rename/move via body `path`, or set directory sharing via body `share`); it does not upload content. `POST /files/_/:path` (`project:files:create`) is the actual upload path (multipart body) and also creates a directory (body `path`). Do not model the PUT as an upload.
- Plan-gated (enabled per team type/plan, not per user role): HA, custom hostnames, protected instance, files. If the team's plan does not include the feature the route returns a 404, independent of the PAT's permissions; surface this as a clear "feature not enabled for this team" message rather than a bare "not found". auto-update-stack has no feature gate.
- httpTokens PUT uses the `needsPermission('project:edit', true)` two-arg variant; the route blocks modifying Expert-MCP tokens. The tool need not replicate this, only rely on the route.
**Tool definitions (description + zod inputSchema):**
```js
platform_set_instance_config: {
description: 'Enables, disables, sets, or clears a configuration surface on a hosted instance. surface selects the config; action semantics and the required companion field depend on surface: ha uses enable/disable (replicas required when enabling, must be 2); customHostname uses set/clear (hostname required when setting); protection uses enable/disable; autoUpdateStack uses set/clear (schedule required when setting). ha, customHostname, and protection are plan-gated and return a descriptive "feature not enabled for this team" error when the plan does not include them; autoUpdateStack has no plan gate.',
inputSchema: z.object({
instanceId: z.string().uuid().describe('UUID of the hosted instance'),
surface: z.enum(['ha', 'customHostname', 'protection', 'autoUpdateStack']).describe('Configuration surface to change'),
action: z.enum(['enable', 'disable', 'set', 'clear']).describe('Transition to apply: ha/protection use enable/disable, customHostname/autoUpdateStack use set/clear'),
replicas: z.literal(2).optional().describe('HA replica count; required when enabling ha, must be 2'),
hostname: z.string().optional().describe('Custom hostname; required when setting customHostname'),
schedule: z.array(z.object({
hour: z.number().describe('Hour of the day for the allowed restart window'),
day: z.number().describe('Day-of-week index (0-6) the entry applies to'),
restart: z.boolean().describe('Whether an automatic stack restart is permitted in this window')
})).optional().describe('Weekly restart schedule as {hour, day, restart} entries; required when setting autoUpdateStack')
})
}
platform_update_instance_file: {
description: 'Updates properties of an existing file or directory on a hosted instance: renames/moves it (newPath, sent as body.path) or sets directory sharing (share). It does not upload content. newPath and share are mutually exclusive.',
inputSchema: z.object({
instanceId: z.string().uuid().describe('UUID of the hosted instance'),
path: z.string().describe('Path of the existing file or directory to update'),
newPath: z.string().optional().describe('New path for a rename or move'),
share: z.record(z.any()).optional().describe('Directory sharing config')
})
}
platform_upload_instance_file: {
description: 'Uploads a file (multipart body) or creates a directory (directoryName, sent as body.path) at the target path on a hosted instance.',
inputSchema: z.object({
instanceId: z.string().uuid().describe('UUID of the hosted instance'),
path: z.string().describe('Target path where the file or directory is created'),
directoryName: z.string().optional().describe('Name of the directory to create')
})
}
platform_create_instance_http_token: {
description: 'Creates an HTTP bearer token for an instance. instanceType selects whether instanceId is a hosted instance UUID or a remote instance (device) hashid.',
inputSchema: z.object({
instanceId: z.string().describe('Instance id: hosted instance UUID, or remote instance (device) hashid'),
instanceType: z.enum(['hosted', 'remote']).describe('Whether instanceId refers to a hosted instance or a remote instance (device)'),
name: z.string().describe('Human-readable name for the HTTP bearer token'),
expiresAt: z.string().optional().describe('Token expiry as an ISO 8601 timestamp; omit for no expiry'),
scope: z.string().optional().describe('Scope string limiting what the token can access')
})
}
platform_update_instance_http_token: {
description: 'Updates the expiry or scope of an HTTP bearer token on an instance. instanceType selects whether instanceId is a hosted instance UUID or a remote instance (device) hashid.',
inputSchema: z.object({
instanceId: z.string().describe('Instance id: hosted instance UUID, or remote instance (device) hashid'),
instanceType: z.enum(['hosted', 'remote']).describe('Whether instanceId refers to a hosted instance or a remote instance (device)'),
tokenId: z.string().describe('Opaque hashid of the HTTP bearer token to update'),
expiresAt: z.string().optional().describe('New expiry as an ISO 8601 timestamp'),
scope: z.string().optional().describe('New scope string for the token')
})
}
```
**Tests:**
- `platform_set_instance_config` routes each (surface, action) pair to the correct PUT or DELETE route and enforces the required companion field.
- The http-token tools route by `instanceType` to the project or device route.
- Read-only PAT is rejected for every tool here.
- Feature-disabled instance returns the descriptive gate error (not applicable to autoUpdateStack, which has no feature gate).
---
Contributor guide
Research direction
Start with the new forge/ee/lib/mcp/tools/instanceConfig.js entry point and the listed project and device routes. Implement the five tools and add tests covering surface/action routing, instanceType routing, required fields, read-only PAT rejection, and the plan-gate error; done means all listed scenarios pass while autoUpdateStack remains ungated.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100