@flowfuse/flowfuse
Version:
An open source low-code development platform
110 lines (99 loc) • 5.57 kB
JavaScript
/**
* Filter MCP server features based on user access permissions for the owner application.
* If a user does not have access to a specific feature (e.g. a tool with destructive hint), it is removed from the server's feature list.
* If a server has no accessible features after filtering, it is removed from the list.
* @param {ForgeApplication} app
* @param {Array<{server: object, application: import('sequelize').Model}>} serverList
* @param {import('sequelize').Model} team
* @param {import('sequelize').Model} teamMembership
* @returns {MCPServerItem[]}
*/
module.exports.filterAccessibleMCPServerFeatures = function (app, serverList, team, teamMembership) {
const servers = []
for (const serverDetails of serverList) {
const { server, application } = serverDetails
const permissionContext = { application }
// sanity checks
if (!application) {
continue // not expected, but just in case
}
if (team.id !== application.TeamId || team.hashid !== server.team) {
continue
}
// first pass - is the user allowed access to this MCP server at all?
if (!app.hasPermission(teamMembership, 'expert:insights:mcp:allow', { application })) {
continue // user doesn't have access to this instance
}
// NOTE: prompts are not yet implemented in the expert backend
// const defaultPromptPermission = app.hasPermission(teamMembership, 'expert:insights:mcp:prompt:allow', permissionContext)
const defaultResourcePermission = app.hasPermission(teamMembership, 'expert:insights:mcp:resource:allow', permissionContext)
const defaultResourceTemplatePermission = app.hasPermission(teamMembership, 'expert:insights:mcp:resourcetemplate:allow', permissionContext)
const defaultToolPermission = app.hasPermission(teamMembership, 'expert:insights:mcp:tool:allow', permissionContext)
const allowToolWrite = app.hasPermission(teamMembership, 'expert:insights:mcp:tool:write', permissionContext)
const allowToolDestructive = app.hasPermission(teamMembership, 'expert:insights:mcp:tool:destructive', permissionContext)
const allowToolOpenWorld = app.hasPermission(teamMembership, 'expert:insights:mcp:tool:open-world', permissionContext)
const allowToolNonIdempotent = app.hasPermission(teamMembership, 'expert:insights:mcp:tool:non-idempotent', permissionContext)
const result = { ...server }
if (result.resources && Array.isArray(result.resources)) {
result.resources = result.resources.filter(_resource => {
return defaultResourcePermission
})
}
if (result.resourceTemplates && Array.isArray(result.resourceTemplates)) {
result.resourceTemplates = result.resourceTemplates.filter(_resourceTemplate => {
return defaultResourceTemplatePermission
})
}
if (result.tools && Array.isArray(result.tools)) {
result.tools = result.tools.filter(tool => {
if (defaultToolPermission !== true) {
return false
}
// at this point, we have established the user has general tool access - now filter based on annotations/hints
const isReadonly = tool.annotations?.readOnlyHint === true
const isDestructive = tool.annotations?.destructiveHint !== false // air on side of caution - if not specified, assume destructive
const isOpenWorld = tool.annotations?.openWorldHint === true
const isIdempotent = tool.annotations?.idempotentHint === true
const writeAccessRequired = isDestructive === true || isReadonly === false
// Sanity check combinations
if (isReadonly && isDestructive) {
// this is not a valid combination - destructive tools cannot be read-only
return false
}
// test access based on hints - worst to best
if (writeAccessRequired) {
if (isDestructive === true) {
if (!allowToolDestructive) {
return false
}
}
if (isReadonly === false) {
if (!allowToolWrite) {
return false
}
}
if (isIdempotent === false) {
if (!allowToolNonIdempotent) {
return false
}
}
}
if (isOpenWorld === true) {
if (!allowToolOpenWorld) {
return false
}
}
return true
})
}
servers.push(result)
}
// finally, before sending the response, filter out any servers that have no accessible features
return servers.filter(server => {
const hasPrompts = Array.isArray(server.prompts) && server.prompts.length > 0
const hasResources = Array.isArray(server.resources) && server.resources.length > 0
const hasResourceTemplates = Array.isArray(server.resourceTemplates) && server.resourceTemplates.length > 0
const hasTools = Array.isArray(server.tools) && server.tools.length > 0
return hasPrompts || hasResources || hasResourceTemplates || hasTools
})
}