Skip to content

Commit 9d267f6

Browse files
author
andypalmi
committed
feat(mcp): add team data and package read tools
Add read-only MCP tools for team databases and tables, npm packages, git tokens and shared library entries, with unit and equivalence tests. Also let the shared library route accept the expert-mcp platform token. Allow-list their scopes for the expert-mcp platform token.
1 parent 913be23 commit 9d267f6

4 files changed

Lines changed: 630 additions & 4 deletions

File tree

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

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
const { z } = require('zod')
2+
3+
const { teamId, paginationParams, paginationParamsKeys, appendQuery } = require('../schemas')
4+
5+
// Strips credentials (including the password) before returning results to the caller.
6+
function redactDatabaseCredentials (database) {
7+
if (!database) {
8+
return database
9+
}
10+
const { credentials, ...rest } = database
11+
return rest
12+
}
13+
14+
module.exports = [
15+
{
16+
name: 'platform_list_team_databases',
17+
title: 'List Team Databases',
18+
description: `FlowFuse platform automation tool:
19+
Lists the FlowFuse Tables databases for a team.
20+
FlowFuse Tables is a plan-gated feature; if it is not enabled for the team's plan, the underlying API's error response is returned as-is.
21+
The underlying API response includes a credentials object with connection details, including a password. This tool strips that object before returning results, so no credentials are ever exposed.`,
22+
annotations: { readOnlyHint: true, destructiveHint: false },
23+
inputSchema: {
24+
teamId
25+
},
26+
handler: async (args, { inject }) => {
27+
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/databases` })
28+
if (response.statusCode >= 400) {
29+
return response
30+
}
31+
const databases = response.json().map(redactDatabaseCredentials)
32+
return {
33+
statusCode: response.statusCode,
34+
json: () => databases
35+
}
36+
}
37+
},
38+
{
39+
name: 'platform_get_team_database',
40+
title: 'Get Team Database',
41+
description: `FlowFuse platform automation tool:
42+
Gets a single FlowFuse Tables database for a team.
43+
FlowFuse Tables is a plan-gated feature; if it is not enabled for the team's plan, the underlying API's error response is returned as-is.
44+
The underlying API response includes a credentials object with connection details, including a password. This tool strips that object before returning the result, so no credentials are ever exposed.`,
45+
annotations: { readOnlyHint: true, destructiveHint: false },
46+
inputSchema: {
47+
teamId,
48+
databaseId: z.string().describe('database hashid')
49+
},
50+
handler: async (args, { inject }) => {
51+
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/databases/${args.databaseId}` })
52+
if (response.statusCode >= 400) {
53+
return response
54+
}
55+
const database = redactDatabaseCredentials(response.json())
56+
return {
57+
statusCode: response.statusCode,
58+
json: () => database
59+
}
60+
}
61+
},
62+
{
63+
name: 'platform_list_database_tables',
64+
title: 'List Database Tables',
65+
description: `FlowFuse platform automation tool:
66+
Lists the tables defined in a FlowFuse Tables database, with pagination.
67+
FlowFuse Tables is a plan-gated feature; if it is not enabled for the team's plan, the underlying API's error response is returned as-is.
68+
Use platform_get_database_table to get the full schema of a single table, or platform_query_database_table_data to read row data.`,
69+
annotations: { readOnlyHint: true, destructiveHint: false },
70+
inputSchema: {
71+
teamId,
72+
databaseId: z.string().describe('database hashid'),
73+
...paginationParams
74+
},
75+
handler: async (args, { inject }) => {
76+
const url = appendQuery(`/api/v1/teams/${args.teamId}/databases/${args.databaseId}/tables`, args, paginationParamsKeys)
77+
const response = await inject({ method: 'GET', url })
78+
return response
79+
}
80+
},
81+
{
82+
name: 'platform_get_database_table',
83+
title: 'Get Database Table',
84+
description: `FlowFuse platform automation tool:
85+
Gets the schema definition of a single table in a FlowFuse Tables database (column names, types, and constraints). Does not return row data or credentials.
86+
FlowFuse Tables is a plan-gated feature; if it is not enabled for the team's plan, the underlying API's error response is returned as-is.
87+
Use platform_query_database_table_data to read row data instead.`,
88+
annotations: { readOnlyHint: true, destructiveHint: false },
89+
inputSchema: {
90+
teamId,
91+
databaseId: z.string().describe('database hashid'),
92+
tableName: z.string().describe('Name of the database table')
93+
},
94+
handler: async (args, { inject }) => {
95+
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/databases/${args.databaseId}/tables/${args.tableName}` })
96+
return response
97+
}
98+
},
99+
{
100+
name: 'platform_query_database_table_data',
101+
title: 'Query Database Table Data',
102+
description: `FlowFuse platform automation tool:
103+
Reads the row data of a table in a FlowFuse Tables database, with pagination. There are no column-filter parameters; this returns rows as stored.
104+
FlowFuse Tables is a plan-gated feature; if it is not enabled for the team's plan, the underlying API's error response is returned as-is.
105+
Use platform_get_database_table first if you need to know the column names and types.`,
106+
annotations: { readOnlyHint: true, destructiveHint: false },
107+
inputSchema: {
108+
teamId,
109+
databaseId: z.string().describe('database hashid'),
110+
tableName: z.string().describe('Name of the database table'),
111+
...paginationParams
112+
},
113+
handler: async (args, { inject }) => {
114+
const url = appendQuery(`/api/v1/teams/${args.teamId}/databases/${args.databaseId}/tables/${args.tableName}/data`, args, paginationParamsKeys)
115+
const response = await inject({ method: 'GET', url })
116+
return response
117+
}
118+
},
119+
{
120+
name: 'platform_list_team_npm_packages',
121+
title: 'List Team NPM Packages',
122+
description: `FlowFuse platform automation tool:
123+
Lists the private npm packages owned by a team.
124+
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.`,
125+
annotations: { readOnlyHint: true, destructiveHint: false },
126+
inputSchema: {
127+
teamId
128+
},
129+
handler: async (args, { inject }) => {
130+
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/npm/packages` })
131+
return response
132+
}
133+
},
134+
{
135+
name: 'platform_list_team_git_tokens',
136+
title: 'List Team Git Tokens',
137+
description: `FlowFuse platform automation tool:
138+
Lists the git tokens configured for a team. The response never includes the raw stored personal access token, only its ID, name, and type.
139+
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.`,
140+
annotations: { readOnlyHint: true, destructiveHint: false },
141+
inputSchema: {
142+
teamId
143+
},
144+
handler: async (args, { inject }) => {
145+
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/git/tokens` })
146+
return response
147+
}
148+
},
149+
{
150+
name: 'platform_list_library_entries',
151+
title: 'List Library Entries',
152+
description: `FlowFuse platform automation tool:
153+
Lists entries in the team shared library (reusable flows, functions, and other snippets shared across a team's hosted and remote instances).
154+
Pass an empty path to list the library root, or a folder path to list its contents.
155+
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.`,
156+
annotations: { readOnlyHint: true, destructiveHint: false },
157+
inputSchema: {
158+
libraryId: z.string().describe('shared-library hashid (the team hashid)'),
159+
path: z.string().default('').describe('Library entry path; empty string lists the library root'),
160+
type: z.string().optional().describe('entry type filter query param (e.g. flows, functions)')
161+
},
162+
handler: async (args, { inject }) => {
163+
const url = appendQuery(`/storage/library/${args.libraryId}/${args.path || ''}`, args, ['type'])
164+
const response = await inject({ method: 'GET', url })
165+
return response
166+
}
167+
}
168+
]

forge/ee/routes/sharedLibrary/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ module.exports = async function (app) {
2525
// Device exists and the auth token is for this team
2626
return
2727
}
28-
} else if (!request.session.ownerType) {
29-
// This is a logged-in user. Get their teamMembership so the needsPermission
30-
// checks in the routes will evaluate properly
28+
} else if (!request.session.ownerType || request.session.ownerType === 'user' || request.session.ownerType === 'user:expert-mcp') {
29+
// Cookie sessions and personal/platform-automation tokens both populate
30+
// request.session.User the same way (see forge/routes/auth/index.js).
3131
request.teamMembership = await request.session.User.getTeamMembership(request.team.id)
3232
if (request.teamMembership) {
3333
return

forge/routes/auth/permissions.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ const IMPLICIT_TOKEN_SCOPES = {
5252
'stack:list',
5353
'flow-blueprint:list',
5454
'project:status',
55-
'template:list'
55+
'template:list',
56+
// team data and packages
57+
'team:database:list', // list/get team databases and tables
58+
'team:packages:read', // list team npm packages
59+
'team:git:tokens:list', // list team git tokens
60+
'library:entry:list' // list team shared library entries
5661
]
5762
}
5863

0 commit comments

Comments
 (0)