FlowFuse / FlowFuse/flowfuse

5.2-b Write tools, non-destructive (phase 2)

Open
#7,688 0 comments 0 reactions 0 assignees View on GitHub
story
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.