Skip to content

Commit 3a537c8

Browse files
author
andypalmi
committed
refactor(mcp): add application audit-log export and team application statuses
1 parent ac0cb49 commit 3a537c8

2 files changed

Lines changed: 120 additions & 0 deletions

File tree

forge/ee/lib/mcp/tools/applications.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ const {
1414
appendQuery
1515
} = require('../schemas')
1616

17+
// Audit-log routes accept cursor+limit pagination, free-text query, event
18+
// (single name or array) and username. scope narrows which entity levels are
19+
// returned and its allowed values differ per route (the device route has none);
20+
// includeChildren pulls in descendant entries within the chosen scope.
21+
const includeChildren = z.boolean().optional().describe('Also include audit entries from child entities within the chosen scope')
22+
const auditLogInput = { ...basePagination, ...searchQuery, ...auditLogFilters }
23+
const auditLogKeys = [...basePaginationKeys, ...searchQueryKeys, ...auditLogFilterKeys]
24+
1725
module.exports = [
1826
{
1927
name: 'platform_list_applications',
@@ -163,5 +171,42 @@ module.exports = [
163171
const response = await inject({ method: 'GET', url })
164172
return response
165173
}
174+
},
175+
{
176+
name: 'platform_export_application_audit_log',
177+
title: 'Export Application Audit Log',
178+
description: `FlowFuse platform automation tool:
179+
Exports an application audit log as a CSV file.
180+
Use this when the user wants a downloadable or shareable copy of the application's audit history.
181+
To read audit log entries directly instead, use the application audit log read tool.`,
182+
annotations: { readOnlyHint: true, destructiveHint: false },
183+
inputSchema: {
184+
applicationId,
185+
...auditLogInput,
186+
scope: z.enum(['application', 'project', 'device']).optional().describe('Entity level to include (default application)'),
187+
includeChildren
188+
},
189+
handler: async (args, { inject }) => {
190+
const url = appendQuery(`/api/v1/applications/${args.applicationId}/audit-log/export`, args, [...auditLogKeys, 'scope', 'includeChildren'])
191+
const response = await inject({ method: 'GET', url })
192+
return response
193+
}
194+
},
195+
{
196+
name: 'platform_list_team_application_statuses',
197+
title: 'List Team Application Statuses',
198+
description: `FlowFuse platform automation tool:
199+
Lists the applications in a team along with the live status of their associated hosted instances and remote instances.
200+
Use this to get a status overview across an entire team without querying each application individually.`,
201+
annotations: { readOnlyHint: true, destructiveHint: false },
202+
inputSchema: {
203+
teamId,
204+
associationsLimit: z.number().optional().describe('Maximum number of associated instances and devices to include per application')
205+
},
206+
handler: async (args, { inject }) => {
207+
const url = appendQuery(`/api/v1/teams/${args.teamId}/applications/status`, args, ['associationsLimit'])
208+
const response = await inject({ method: 'GET', url })
209+
return response
210+
}
166211
}
167212
]

test/unit/forge/ee/lib/mcp/tools/applications_spec.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,64 @@ describe('MCP Application Tools', function () {
114114
})
115115
})
116116

117+
describe('platform_export_application_audit_log', function () {
118+
const tool = findTool('platform_export_application_audit_log')
119+
120+
it('has read-only annotations', function () {
121+
tool.annotations.readOnlyHint.should.equal(true)
122+
tool.annotations.destructiveHint.should.equal(false)
123+
})
124+
125+
it('exposes the expected input fields', function () {
126+
Object.keys(tool.inputSchema).should.eql(['applicationId', 'cursor', 'limit', 'query', 'event', 'username', 'scope', 'includeChildren'])
127+
})
128+
129+
it('requests the export endpoint with no query string when no filters are given', async function () {
130+
const { calls, inject } = recordingInject()
131+
await tool.handler({ applicationId: 'app1' }, { inject })
132+
calls.should.have.length(1)
133+
calls[0].method.should.equal('GET')
134+
calls[0].url.should.equal('/api/v1/applications/app1/audit-log/export')
135+
})
136+
137+
it('serialises cursor, limit, event, username, scope and includeChildren onto the query string', async function () {
138+
const { calls, inject } = recordingInject()
139+
await tool.handler({
140+
applicationId: 'app1',
141+
cursor: 'c1',
142+
limit: 10,
143+
event: 'application.created',
144+
username: 'alice',
145+
scope: 'application',
146+
includeChildren: true
147+
}, { inject })
148+
calls.should.have.length(1)
149+
calls[0].method.should.equal('GET')
150+
calls[0].url.should.equal('/api/v1/applications/app1/audit-log/export?cursor=c1&limit=10&event=application.created&username=alice&scope=application&includeChildren=true')
151+
})
152+
})
153+
154+
describe('platform_list_team_application_statuses', function () {
155+
const tool = findTool('platform_list_team_application_statuses')
156+
157+
it('has the expected input params', function () {
158+
Object.keys(tool.inputSchema).should.eql(['teamId', 'associationsLimit'])
159+
})
160+
161+
it('calls GET /api/v1/teams/:teamId/applications/status with associationsLimit', async function () {
162+
const { calls, inject } = recordingInject()
163+
await tool.handler({ teamId: 'team1', associationsLimit: 5 }, { inject })
164+
calls[0].method.should.equal('GET')
165+
calls[0].url.should.equal('/api/v1/teams/team1/applications/status?associationsLimit=5')
166+
})
167+
168+
it('omits the query string when associationsLimit is not set', async function () {
169+
const { calls, inject } = recordingInject()
170+
await tool.handler({ teamId: 'team1' }, { inject })
171+
calls[0].url.should.equal('/api/v1/teams/team1/applications/status')
172+
})
173+
})
174+
117175
describe('Integration smoke test', function () {
118176
let app
119177
let token
@@ -229,6 +287,23 @@ describe('MCP Application Tools', function () {
229287
url: `/api/v1/applications/${app.application.hashid}/snapshots`
230288
})
231289
})
290+
291+
it('platform_export_application_audit_log matches GET /api/v1/applications/:applicationId/audit-log/export', async function () {
292+
const tool = findTool('platform_export_application_audit_log')
293+
await expectToolMatchesRoute(inject, tool, { applicationId: app.application.hashid }, {
294+
method: 'GET',
295+
url: `/api/v1/applications/${app.application.hashid}/audit-log/export`,
296+
raw: true
297+
})
298+
})
299+
300+
it('platform_list_team_application_statuses matches GET /api/v1/teams/:teamId/applications/status', async function () {
301+
const tool = findTool('platform_list_team_application_statuses')
302+
await expectToolMatchesRoute(inject, tool, { teamId: app.team.hashid }, {
303+
method: 'GET',
304+
url: `/api/v1/teams/${app.team.hashid}/applications/status`
305+
})
306+
})
232307
})
233308
})
234309
})

0 commit comments

Comments
 (0)