Skip to content

Commit ac0cb49

Browse files
author
andypalmi
committed
feat(mcp): add application read tools
Wrap the read-only application platform routes as MCP tools with unit and tool-vs-route equivalence tests. Entity ids and query-string building use the shared schema fragments.
1 parent 103f9ae commit ac0cb49

2 files changed

Lines changed: 284 additions & 50 deletions

File tree

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
const { z } = require('zod')
22

3+
const {
4+
teamId,
5+
applicationId,
6+
basePagination,
7+
basePaginationKeys,
8+
pageParam,
9+
pageParamKeys,
10+
searchQuery,
11+
searchQueryKeys,
12+
auditLogFilters,
13+
auditLogFilterKeys,
14+
appendQuery
15+
} = require('../schemas')
16+
317
module.exports = [
418
{
519
name: 'platform_list_applications',
@@ -10,7 +24,7 @@ module.exports = [
1024
Call platform_get_remote_instance to get details of a specific remote instance or platform_get_hosted_instance to get details of a specific hosted instance.`,
1125
annotations: { readOnlyHint: true, destructiveHint: false },
1226
inputSchema: {
13-
teamId: z.string().describe('The ID or hashid of the team')
27+
teamId
1428
},
1529
handler: async (args, { inject }) => {
1630
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/applications?includeInstances=false&includeApplicationDevices=false` })
@@ -23,7 +37,7 @@ module.exports = [
2337
description: 'FlowFuse platform automation tool: Use this tool to retrieve application metadata (name, description, link, team createdAt and updatedAt)',
2438
annotations: { readOnlyHint: true, destructiveHint: false },
2539
inputSchema: {
26-
applicationId: z.string().describe('The ID or hashid of the application')
40+
applicationId
2741
},
2842
handler: async (args, { inject }) => {
2943
const response = await inject({ method: 'GET', url: `/api/v1/applications/${args.applicationId}` })
@@ -41,7 +55,7 @@ module.exports = [
4155
To check if hosted instances are currently running or stopped, call platform_get_application_instances_status instead.`,
4256
annotations: { readOnlyHint: true, destructiveHint: false },
4357
inputSchema: {
44-
applicationId: z.string().describe('The ID or hashid of the application')
58+
applicationId
4559
},
4660
handler: async (args, { inject }) => {
4761
const response = await inject({ method: 'GET', url: `/api/v1/applications/${args.applicationId}/instances` })
@@ -55,30 +69,17 @@ module.exports = [
5569
Gets all the remote instances (devices) that live inside an application.
5670
A remote instance is a Node-RED that runs on the user's own hardware (like a Raspberry Pi or a server) rather than on the same environment as the FlowFuse platform.
5771
Use this to see which remote instances are connected to an application, check if they are online or offline, or find one by name.
58-
You can search by name using the query parameter and page through results using cursor or limit.
72+
You can search by name using the query parameter and page through results using cursor, limit or page.
5973
To get the full details of one specific remote instance, call platform_get_remote_instance with its ID.`,
6074
annotations: { readOnlyHint: true, destructiveHint: false },
6175
inputSchema: {
62-
applicationId: z.string().describe('The ID or hashid of the application'),
63-
query: z.string().optional().describe('Search remote instances by name'),
64-
cursor: z.string().optional().describe('Cursor for pagination (the hashid of the last item from the previous page)'),
65-
limit: z.number().min(1).max(10).describe('How many results to return per page')
76+
applicationId,
77+
...basePagination,
78+
...pageParam,
79+
...searchQuery
6680
},
6781
handler: async (args, { inject }) => {
68-
let url = `/api/v1/applications/${args.applicationId}/devices`
69-
const params = []
70-
if (args.query) {
71-
params.push(`query=${args.query}`)
72-
}
73-
if (args.cursor) {
74-
params.push(`cursor=${args.cursor}`)
75-
}
76-
if (args.limit) {
77-
params.push(`limit=${args.limit}`)
78-
}
79-
if (params.length > 0) {
80-
url += '?' + params.join('&')
81-
}
82+
const url = appendQuery(`/api/v1/applications/${args.applicationId}/devices`, args, [...basePaginationKeys, ...pageParamKeys, ...searchQueryKeys])
8283
const response = await inject({ method: 'GET', url })
8384
return response
8485
}
@@ -93,7 +94,7 @@ module.exports = [
9394
this tool tells you what is happening right now (is it running? is it deploying? when were the flows last updated?).`,
9495
annotations: { readOnlyHint: true, destructiveHint: false },
9596
inputSchema: {
96-
applicationId: z.string().describe('The ID or hashid of the application')
97+
applicationId
9798
},
9899
handler: async (args, { inject }) => {
99100
const response = await inject({ method: 'GET', url: `/api/v1/applications/${args.applicationId}/instances/status` })
@@ -110,34 +111,14 @@ module.exports = [
110111
You can narrow down results by event type, username, or scope (application, project, or device).`,
111112
annotations: { readOnlyHint: true, destructiveHint: false },
112113
inputSchema: {
113-
applicationId: z.string().describe('The ID or hashid of the application'),
114-
cursor: z.string().optional().describe('Cursor for pagination (the hashid of the last entry from the previous page)'),
115-
limit: z.number().min(1).max(100).describe('How many entries to return'),
116-
event: z.string().optional().describe('Filter by event type (e.g. "application.created", "project.snapshot.device-target-set")'),
117-
username: z.string().optional().describe('Filter by the username of whoever triggered the event'),
118-
scope: z.string().optional().describe('What level of entries to include: "application", "project", or "device" (default "application")')
114+
applicationId,
115+
...basePagination,
116+
...searchQuery,
117+
...auditLogFilters,
118+
scope: z.enum(['application', 'project', 'device']).optional().describe('Which entries to include by scope (default application)')
119119
},
120120
handler: async (args, { inject }) => {
121-
let url = `/api/v1/applications/${args.applicationId}/audit-log`
122-
const params = []
123-
if (args.cursor) {
124-
params.push(`cursor=${args.cursor}`)
125-
}
126-
if (args.limit) {
127-
params.push(`limit=${args.limit}`)
128-
}
129-
if (args.event) {
130-
params.push(`event=${args.event}`)
131-
}
132-
if (args.username) {
133-
params.push(`username=${args.username}`)
134-
}
135-
if (args.scope) {
136-
params.push(`scope=${args.scope}`)
137-
}
138-
if (params.length > 0) {
139-
url += '?' + params.join('&')
140-
}
121+
const url = appendQuery(`/api/v1/applications/${args.applicationId}/audit-log`, args, [...basePaginationKeys, ...searchQueryKeys, ...auditLogFilterKeys, 'scope'])
141122
const response = await inject({ method: 'GET', url })
142123
return response
143124
}
@@ -152,7 +133,7 @@ module.exports = [
152133
annotations: { readOnlyHint: false, destructiveHint: false },
153134
inputSchema: {
154135
name: z.string().describe('Name for the new application'),
155-
teamId: z.string().describe('The ID or hashid of the team to create the application in'),
136+
teamId,
156137
description: z.string().optional().describe('Optional description for the application')
157138
},
158139
handler: async (args, { inject }) => {
@@ -163,5 +144,24 @@ module.exports = [
163144
const response = await inject({ method: 'POST', url: '/api/v1/applications', payload })
164145
return response
165146
}
147+
},
148+
{
149+
name: 'platform_list_application_snapshots',
150+
title: 'List Application Snapshots',
151+
description: `FlowFuse platform automation tool:
152+
Lists the snapshots belonging to an application.
153+
A snapshot is a saved copy of an instance's flows, credentials and settings at a point in time.
154+
Use this to see what snapshots are available for the hosted instances inside an application.
155+
Use cursor or limit to page through results.`,
156+
annotations: { readOnlyHint: true, destructiveHint: false },
157+
inputSchema: {
158+
applicationId,
159+
...basePagination
160+
},
161+
handler: async (args, { inject }) => {
162+
const url = appendQuery(`/api/v1/applications/${args.applicationId}/snapshots`, args, basePaginationKeys)
163+
const response = await inject({ method: 'GET', url })
164+
return response
165+
}
166166
}
167167
]

0 commit comments

Comments
 (0)