Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 196 additions & 1 deletion forge/ee/lib/mcp/tools/teams.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
const { z } = require('zod')

const { teamId, basePagination, basePaginationKeys, searchQuery, searchQueryKeys, auditLogFilters, auditLogFilterKeys, appendQuery } = require('../schemas')

// Audit-log routes accept cursor+limit pagination, free-text query, event
// (single name or array) and username. scope narrows which entity levels are
// returned; includeChildren pulls in descendant entries within the chosen scope.
const includeChildren = z.boolean().optional().describe('Also include audit entries from child entities within the chosen scope')
const auditLogInput = { ...basePagination, ...searchQuery, ...auditLogFilters }
const auditLogKeys = [...basePaginationKeys, ...searchQueryKeys, ...auditLogFilterKeys]

module.exports = [
{
name: 'platform_list_teams',
Expand All @@ -18,11 +27,197 @@ module.exports = [
description: 'FlowFuse platform automation tool: Get details of a specific team by its ID, including team type, member count, hosted instance and remote instance counts.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId: z.string().describe('The ID or hashid of the team')
teamId
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}` })
return response
}
},
{
name: 'platform_get_team_by_slug',
title: 'Get Team By Slug',
description: `FlowFuse platform automation tool:
Gets details of a specific team using its slug (URL identifier) instead of its hashid.
Use this when you only know the team's slug, for example from a URL, and need the same details as platform_get_team.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamSlug: z.string().regex(/^[a-z0-9-_]+$/i).describe('Team slug (URL identifier; lowercase letters, digits, hyphen and underscore)')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/slug/${args.teamSlug}` })
return response
}
},
{
name: 'platform_get_team_instance_counts',
title: 'Get Team Instance Counts',
description: `FlowFuse platform automation tool:
Counts a team's instances of the given type, optionally narrowed by state and application.
instanceType is required: use "hosted" for hosted instances or "remote" for remote instances (devices).
Use this for quick totals instead of listing and counting every instance yourself.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId,
instanceType: z.enum(['remote', 'hosted']).describe('Instance type to count'),
state: z.array(z.string()).optional().describe('Optional list of instance states to filter the counts by (defaults to empty)'),
applicationId: z.string().optional().describe('Application hashid to scope the counts to a single application')
},
handler: async (args, { inject }) => {
const url = appendQuery(`/api/v1/teams/${args.teamId}/instance-counts`, args, ['instanceType', 'state', 'applicationId'])
const response = await inject({ method: 'GET', url })
return response
}
},
{
name: 'platform_check_team_slug_availability',
title: 'Check Team Slug Availability',
description: `FlowFuse platform automation tool:
Checks whether a team slug is available before creating a team. This does not create or change anything.
The value "create" is reserved and is always rejected.
Use this before calling a team creation tool to make sure the chosen slug is not already taken.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
slug: z.string().regex(/^[a-z0-9-_]+$/i).describe('Team slug to check; lowercase letters, digits, hyphen and underscore')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'POST', url: '/api/v1/teams/check-slug', payload: { slug: args.slug } })
return response
}
},
{
name: 'platform_get_team_membership',
title: 'Get Team Membership',
description: `FlowFuse platform automation tool:
Gets the authenticated user's own membership (role) in a team.
Use this to check what role the current user holds in a team before attempting an action that needs a specific role.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/user` })
return response
}
},
{
name: 'platform_list_team_members',
title: 'List Team Members',
description: `FlowFuse platform automation tool:
Lists the members of a team, including their role and, when SSO is enabled, whether their membership is SSO-managed.
Use this to see who belongs to a team before inviting, removing, or changing the role of a member.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/members` })
return response
}
},
{
name: 'platform_list_team_invitations',
title: 'List Team Invitations',
description: `FlowFuse platform automation tool:
Lists the pending invitations for a team.
This requires the Owner role, so a non-Owner credential will get an access error even though this tool itself is read-only.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/invitations` })
return response
}
},
{
name: 'platform_get_team_audit_log',
title: 'Get Team Audit Log',
description: `FlowFuse platform automation tool:
Reads the audit log for a team, showing events like membership changes, billing changes,
and administrative actions taken across the team's applications, instances, and devices.
A team-scoped PAT only sees audit log entries for teams it is scoped to.
Use this when the user asks what happened on a team, or wants to investigate recent changes.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId,
...auditLogInput,
scope: z.enum(['team', 'application', 'project', 'device']).optional().describe('Entity level to include (default team)'),
includeChildren
},
handler: async (args, { inject }) => {
const url = appendQuery(`/api/v1/teams/${args.teamId}/audit-log`, args, [...auditLogKeys, 'scope', 'includeChildren'])
const response = await inject({ method: 'GET', url })
return response
}
},
{
name: 'platform_export_team_audit_log',
title: 'Export Team Audit Log',
description: `FlowFuse platform automation tool:
Exports the team audit log as a CSV file.
Use this when the user wants a downloadable or shareable copy of the team's audit history,
rather than reading entries directly with platform_get_team_audit_log.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId,
...auditLogInput,
scope: z.enum(['team', 'application', 'project', 'device']).optional().describe('Entity level to include (default team)'),
includeChildren
},
handler: async (args, { inject }) => {
const url = appendQuery(`/api/v1/teams/${args.teamId}/audit-log/export`, args, [...auditLogKeys, 'scope', 'includeChildren'])
const response = await inject({ method: 'GET', url })
return response
}
},
{
name: 'platform_list_team_npm_packages',
title: 'List Team NPM Packages',
description: `FlowFuse platform automation tool:
Lists the private npm packages owned by a team.
The npm registry is a plan-gated feature; if it is not enabled for the team's plan, or the team does not exist, the underlying API's error response is returned as-is.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/npm/packages` })
return response
}
},
{
name: 'platform_list_team_git_tokens',
title: 'List Team Git Tokens',
description: `FlowFuse platform automation tool:
Lists the git tokens configured for a team. The response never includes the raw stored personal access token, only its ID, name, and type.
Git integration is a plan-gated feature; if it is not enabled for the team's plan, or the team does not exist, the underlying API's error response is returned as-is.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/git/tokens` })
return response
}
},
{
name: 'platform_list_library_entries',
title: 'List Library Entries',
description: `FlowFuse platform automation tool:
Lists entries in the team shared library (reusable flows, functions, and other snippets shared across a team's hosted and remote instances).
Pass an empty path to list the library root, or a folder path to list its contents.
The shared library is enabled by default, but a team may still not exist or the caller may not be a member; either case returns the underlying API's error response as-is.`,
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
libraryId: z.string().describe('shared-library hashid (the team hashid)'),
path: z.string().default('').describe('Library entry path; empty string lists the library root'),
type: z.string().optional().describe('entry type filter query param (e.g. flows, functions)')
},
handler: async (args, { inject }) => {
const url = appendQuery(`/storage/library/${args.libraryId}/${args.path || ''}`, args, ['type'])
const response = await inject({ method: 'GET', url })
return response
}
}
]
6 changes: 3 additions & 3 deletions forge/ee/routes/sharedLibrary/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ module.exports = async function (app) {
// Device exists and the auth token is for this team
return
}
} else if (!request.session.ownerType) {
// This is a logged-in user. Get their teamMembership so the needsPermission
// checks in the routes will evaluate properly
} else if (!request.session.ownerType || request.session.ownerType === 'user' || request.session.ownerType === 'user:expert-mcp') {
// Cookie sessions and personal/platform-automation tokens both populate
// request.session.User the same way (see forge/routes/auth/index.js).
request.teamMembership = await request.session.User.getTeamMembership(request.team.id)
if (request.teamMembership) {
return
Expand Down
8 changes: 8 additions & 0 deletions forge/routes/auth/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ const IMPLICIT_TOKEN_SCOPES = {
// teams
'user:team:list', // list teams
'team:read', // get team details
'team:user:list', // list team members
'team:user:invite', // list team invitations
'team:create', // check team slug availability
'team:audit-log', // get team audit log
// team data
'team:packages:read', // list team npm packages
'team:git:tokens:list', // list team git tokens
'library:entry:list', // list team shared library entries
// platform
'stack:list',
'flow-blueprint:list',
Expand Down
Loading
Loading