5.2-b Write tools, non-destructive (phase 2)
- Dominant language
- JavaScript
- Stars
- 400
- Forks
- 89
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 149
Description
**Parent:** #7671 (5.2 Snapshot lifecycle)
**Tool file:** `forge/ee/lib/mcp/tools/snapshots.js` (extend)
`readOnlyHint: false`, `destructiveHint: false`.
| Tool | Endpoint | Scope | Annotation |
|---|---|---|---|
| `platform_export_snapshot` | `POST /snapshots/:id/export` | `snapshot:export` | write |
| `platform_import_snapshot` | `POST /snapshots/import` | `snapshot:import` | write |
| `platform_update_snapshot` | `PUT /snapshots/:id` | `snapshot:edit` | write |
| `platform_set_instance_device_target` | `POST /projects/:id/devices/settings` | `project:snapshot:set-target` | write |
**Consolidation notes:**
- Export is a single tool: `platform_export_snapshot` takes a snapshot id and the generic route resolves the owner (instance or device) from the snapshot, so a separate instance-scoped export tool is not needed. The generic route also accepts an optional `components` selection.
**Design notes:**
- Export requires a `credentialSecret` in the payload (**400** if missing). Import requires `credentialSecret` when the snapshot has encrypted credentials. Model `credentialSecret` as a required input where the route demands it, and surface the validation error descriptively.
- Export is treated as a write even though it "reads" data (it decrypts/exports credentials), so it is annotated `readOnlyHint: false`.
- The generic `POST /snapshots/:id/export` accepts an optional `components` object (`{flows, credentials, envVars}`).
**Tool definitions (description + zod inputSchema):**
```js
platform_export_snapshot: {
description: 'Export a snapshot by snapshot id; the owner (instance or device) is resolved from the snapshot. credentialSecret is mandatory; the export fails with 400 if it is missing.',
inputSchema: z.object({
id: z.string().describe('ProjectSnapshot id (opaque hashid), not a project UUID'),
credentialSecret: z.string().describe('Secret used to re-encrypt exported credentials'),
components: z.object({
flows: z.boolean().optional().describe('Include flows in the export (default true)'),
credentials: z.boolean().optional().describe('Include credentials in the export (default true)'),
envVars: z.union([z.enum(['all', 'keys']), z.literal(false)]).optional().describe("Env vars to include: 'all' values, 'keys' only, or false to exclude")
}).optional().describe('Optional selection of which snapshot components to export')
})
}
platform_import_snapshot: {
description: 'Import a snapshot into an instance or device. ownerId is an instance (project) UUID when ownerType is instance, or a device hashid when ownerType is device. The snapshot object requires name, flows and settings. credentialSecret is required only when the snapshot includes encrypted credentials.',
inputSchema: z.object({
ownerId: z.string().describe('Target owner id: instance (project) UUID or device hashid, matching ownerType'),
ownerType: z.enum(['device', 'instance']).describe('Type of resource that will own the imported snapshot'),
snapshot: z.object({
name: z.string().describe('Snapshot name'),
description: z.string().optional().describe('Snapshot description'),
flows: z.object({
flows: z.array(z.any()).describe('Node-RED flows array'),
credentials: z.record(z.any()).optional().describe('Encrypted credentials object')
}).describe('Flows payload'),
settings: z.object({
settings: z.record(z.any()).optional().describe('Runtime settings'),
env: z.record(z.any()).optional().describe('Environment variables'),
modules: z.record(z.any()).optional().describe('Installed module versions')
}).describe('Settings payload')
}).describe('Snapshot to import'),
credentialSecret: z.string().optional().describe('Secret to decrypt the snapshot credentials'),
components: z.object({
flows: z.boolean().optional().describe('Import flows (default true)'),
credentials: z.boolean().optional().describe('Import credentials (default true)'),
envVars: z.union([z.enum(['all', 'keys']), z.literal(false)]).optional().describe("Env vars to import: 'all' values, 'keys' only, or false to exclude")
}).optional().describe('Optional selection of which snapshot components to import')
})
}
platform_update_snapshot: {
description: 'Update a snapshot name and/or description by snapshot id.',
inputSchema: z.object({
id: z.string().describe('ProjectSnapshot id (opaque hashid), not a project UUID'),
name: z.string().optional().describe('New snapshot name'),
description: z.string().optional().describe('New snapshot description')
})
}
platform_set_instance_device_target: {
description: 'Set the device target snapshot for a hosted instance. The target snapshot is validated to belong to the instance.',
inputSchema: z.object({
instanceId: z.string().uuid().describe('Hosted instance (project) UUID whose device target to set'),
targetSnapshot: z.string().describe('Snapshot id (opaque hashid) to set as the device target')
})
}
```
**Tests:**
- Export/import/edit and set-target rejected for read-only PAT.
- Export without `credentialSecret` returns the descriptive validation error.
- Export resolves the owner from the snapshot for both instance- and device-owned snapshots.
- Set-target validates the snapshot belongs to the instance.
---
Contributor guide
Research direction
Extend forge/ee/lib/mcp/tools/snapshots.js with the four listed snapshot tools and their Zod schemas. Start by reading the existing snapshot tools and run the relevant tests for export/import/edit and set-target behavior. Done means the tools use the specified endpoints, annotations, validation, owner resolution, and read-only PAT rejection cases.
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