Skip to content

Commit 802da78

Browse files
author
andypalmi
committed
feat(mcp): add instance configuration read tools
Add read-only MCP tools for per-instance configuration surfaces: - platform_get_instance_ha - platform_get_instance_custom_hostname - platform_get_instance_custom_hostname_status - platform_get_instance_protection - platform_get_instance_auto_update_stack - platform_list_instance_files - platform_list_instance_http_tokens - platform_list_remote_instance_http_tokens Several of these surfaces are plan-gated per team (High Availability, custom hostnames, protected instance, static file storage, HTTP bearer tokens); the tools surface a descriptive message when the backing route returns not-found instead of a bare 404. Allow-list the project:edit and project:files:list scopes for the user:expert-mcp token type; project:read and device:edit were already present. Closes #7705
1 parent 482fad4 commit 802da78

2 files changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
const { z } = require('zod')
2+
3+
module.exports = [
4+
{
5+
name: 'platform_get_instance_ha',
6+
title: 'Get Instance High Availability',
7+
description: `FlowFuse platform automation tool:
8+
Returns the High Availability configuration for a hosted instance.
9+
High Availability runs an instance across multiple replicas so it stays up if one replica fails.
10+
High Availability is a plan-gated feature: if it is not enabled for the team, this call
11+
returns a message saying the feature is not available rather than the configuration.`,
12+
annotations: { readOnlyHint: true, destructiveHint: false },
13+
inputSchema: {
14+
instanceId: z.string().uuid().describe('UUID of the hosted instance')
15+
},
16+
handler: async (args, { inject }) => {
17+
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/ha` })
18+
if (response.statusCode === 404) {
19+
return { error: 'High Availability configuration is not available for this instance. This can mean the instance does not exist or the High Availability feature is not enabled for the team.' }
20+
}
21+
return response
22+
}
23+
},
24+
{
25+
name: 'platform_get_instance_custom_hostname',
26+
title: 'Get Instance Custom Hostname',
27+
description: `FlowFuse platform automation tool:
28+
Returns the custom hostname configured for a hosted instance.
29+
Custom hostnames are a plan-gated feature: if it is not enabled for the team, this call
30+
returns a message saying the feature is not available rather than the configuration.
31+
Use platform_get_instance_custom_hostname_status to check whether the hostname is verified and routable.`,
32+
annotations: { readOnlyHint: true, destructiveHint: false },
33+
inputSchema: {
34+
instanceId: z.string().uuid().describe('UUID of the hosted instance')
35+
},
36+
handler: async (args, { inject }) => {
37+
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/customHostname` })
38+
if (response.statusCode === 404) {
39+
return { error: 'Custom hostname configuration is not available for this instance. This can mean the instance does not exist or the custom hostname feature is not enabled for the team.' }
40+
}
41+
return response
42+
}
43+
},
44+
{
45+
name: 'platform_get_instance_custom_hostname_status',
46+
title: 'Get Instance Custom Hostname Status',
47+
description: `FlowFuse platform automation tool:
48+
Returns the status of the custom hostname for a hosted instance, i.e. whether the DNS
49+
CNAME record has been set up correctly and points at the platform.
50+
A 200 response means the hostname is verified. A 410 response means a hostname is set
51+
but its CNAME record does not resolve to the platform yet. A 404 response can mean no
52+
custom hostname is configured, the platform does not support hostname verification, or
53+
the custom hostname feature is not enabled for the team.`,
54+
annotations: { readOnlyHint: true, destructiveHint: false },
55+
inputSchema: {
56+
instanceId: z.string().uuid().describe('UUID of the hosted instance')
57+
},
58+
handler: async (args, { inject }) => {
59+
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/customHostname/status` })
60+
if (response.statusCode === 404) {
61+
return { error: 'No custom hostname is configured for this instance, hostname verification is not supported by this platform, or the custom hostname feature is not enabled for the team.' }
62+
}
63+
return response
64+
}
65+
},
66+
{
67+
name: 'platform_get_instance_protection',
68+
title: 'Get Instance Protection',
69+
description: `FlowFuse platform automation tool:
70+
Returns the protected-instance configuration for a hosted instance.
71+
A protected instance requires extra confirmation before destructive actions such as
72+
suspension or deletion can be performed against it.
73+
Protected instance is a plan-gated feature: if it is not enabled for the team, this call
74+
returns a message saying the feature is not available rather than the configuration.`,
75+
annotations: { readOnlyHint: true, destructiveHint: false },
76+
inputSchema: {
77+
instanceId: z.string().uuid().describe('UUID of the hosted instance')
78+
},
79+
handler: async (args, { inject }) => {
80+
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/protectInstance` })
81+
if (response.statusCode === 404) {
82+
return { error: 'Protected-instance configuration is not available for this instance. This can mean the instance does not exist or the protected-instance feature is not enabled for the team.' }
83+
}
84+
return response
85+
}
86+
},
87+
{
88+
name: 'platform_get_instance_auto_update_stack',
89+
title: 'Get Instance Auto-Update Stack Schedule',
90+
description: `FlowFuse platform automation tool:
91+
Returns the auto-update stack (weekly restart) schedule for a hosted instance.
92+
This schedule controls the windows in which the platform is allowed to automatically
93+
restart the instance to apply a stack update.
94+
This surface has no plan gate: a 404 response means the instance does not exist.`,
95+
annotations: { readOnlyHint: true, destructiveHint: false },
96+
inputSchema: {
97+
instanceId: z.string().uuid().describe('UUID of the hosted instance')
98+
},
99+
handler: async (args, { inject }) => {
100+
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/autoUpdateStack` })
101+
return response
102+
}
103+
},
104+
{
105+
name: 'platform_list_instance_files',
106+
title: 'List Instance Files',
107+
description: `FlowFuse platform automation tool:
108+
Lists files and directories within a hosted instance file store at the given path.
109+
Use an empty string for the path to list the root of the file store.
110+
Static file storage is a plan-gated feature: if it is not enabled for the team, this call
111+
returns a message saying the feature is not available rather than a listing.`,
112+
annotations: { readOnlyHint: true, destructiveHint: false },
113+
inputSchema: {
114+
instanceId: z.string().uuid().describe('UUID of the hosted instance'),
115+
path: z.string().describe('Directory path within the instance file store to list')
116+
},
117+
handler: async (args, { inject }) => {
118+
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/files/_/${encodeURIComponent(args.path)}` })
119+
if (response.statusCode === 404) {
120+
return { error: 'File storage is not available for this instance. This can mean the instance does not exist, the requested path was not found, or the static file storage feature is not enabled for the team.' }
121+
}
122+
return response
123+
}
124+
},
125+
{
126+
name: 'platform_list_instance_http_tokens',
127+
title: 'List Instance HTTP Tokens',
128+
description: `FlowFuse platform automation tool:
129+
Lists the HTTP bearer tokens configured for a hosted instance.
130+
These tokens are used by external callers to authenticate HTTP requests handled by the
131+
instance's Node-RED flows.
132+
HTTP bearer tokens are a plan-gated feature: if it is not enabled for the team, this call
133+
returns a message saying the feature is not available rather than a listing.`,
134+
annotations: { readOnlyHint: true, destructiveHint: false },
135+
inputSchema: {
136+
instanceId: z.string().uuid().describe('UUID of the hosted instance')
137+
},
138+
handler: async (args, { inject }) => {
139+
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/httpTokens` })
140+
if (response.statusCode === 404) {
141+
return { error: 'HTTP bearer tokens are not available for this instance. This can mean the instance does not exist or the HTTP bearer tokens feature is not enabled for the team.' }
142+
}
143+
return response
144+
}
145+
},
146+
{
147+
name: 'platform_list_remote_instance_http_tokens',
148+
title: 'List Remote Instance HTTP Tokens',
149+
description: `FlowFuse platform automation tool:
150+
Lists the HTTP bearer tokens configured for a remote instance (device).
151+
These tokens are used by external callers to authenticate HTTP requests handled by the
152+
remote instance's Node-RED flows.
153+
HTTP bearer tokens are a plan-gated feature: if it is not enabled for the team, this call
154+
returns a message saying the feature is not available rather than a listing.`,
155+
annotations: { readOnlyHint: true, destructiveHint: false },
156+
inputSchema: {
157+
deviceId: z.string().describe('Opaque hashid of the remote instance (device)')
158+
},
159+
handler: async (args, { inject }) => {
160+
const response = await inject({ method: 'GET', url: `/api/v1/devices/${args.deviceId}/httpTokens` })
161+
if (response.statusCode === 404) {
162+
return { error: 'HTTP bearer tokens are not available for this remote instance. This can mean the remote instance does not exist or the HTTP bearer tokens feature is not enabled for the team.' }
163+
}
164+
return response
165+
}
166+
}
167+
]

forge/routes/auth/permissions.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ const IMPLICIT_TOKEN_SCOPES = {
4545
'project:snapshot:create', // create hosted instance snapshot
4646
'device:snapshot:list', // list remote instance snapshots
4747
'device:snapshot:create', // create remote instance snapshot
48+
// instance configuration
49+
'project:edit', // get instance custom hostname, custom hostname status and HTTP tokens
50+
'project:files:list', // list instance files
4851
// teams
4952
'user:team:list', // list teams
5053
'team:read', // get team details

0 commit comments

Comments
 (0)