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
3 changes: 3 additions & 0 deletions forge/ee/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ module.exports = fp(async function (app, opts) {
// Set the expert assistant Feature Flag
app.config.features.register('expertAssistant', isAiEnabled && (app.config?.expert?.enabled ?? false), true)

// Set the expert platform automation Feature Flag (MCP platform tools server)
app.config.features.register('expertPlatformAutomation', isAiEnabled && (app.config?.expert?.enabled ?? false), true)

// temporary until FF Expert Insights can be enabled on Self Hosted EE instance
const isInsightsEnabled = isAiEnabled && app.config?.expert?.enabled && app.config?.expert?.insights?.enabled
app.config.features.register('expertInsights', isInsightsEnabled ?? false, false)
Expand Down
2 changes: 1 addition & 1 deletion forge/ee/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ module.exports = async function (app) {
if (app.config.tables?.enabled) {
await app.register(require('./tables'), { prefix: '/api/v1/teams/:teamId/databases', logLevel: app.config.logging.http })
}
await app.register(require('./mcp'), { prefix: '/api/v1/teams/:teamId/mcp', logLevel: app.config.logging.http })
await app.register(require('./mcp'), { logLevel: app.config.logging.http })
await app.register(require('./autoUpdateStacks'), { prefix: '/api/v1/projects/:projectId/autoUpdateStack', logLevel: app.config.logging.http })
await app.register(require('./expert'), { prefix: '/api/v1/expert', logLevel: app.config.logging.http })

Expand Down
206 changes: 10 additions & 196 deletions forge/ee/routes/mcp/index.js
Original file line number Diff line number Diff line change
@@ -1,198 +1,12 @@
/**
* MCP routes
*
* - registrations: NR instance/device MCP server registration and discovery
* - server: Platform MCP server endpoint for external AI agents
*
* @param {import('../../../forge').ForgeApplication} app
*/
module.exports = async function (app) {
app.addHook('preHandler', async (request, reply) => {
if (request.params.teamId !== undefined || request.params.teamSlug !== undefined) {
if (!request.team) {
// For a :teamId route, we can now lookup the full team object
request.team = await app.db.models.Team.byId(request.params.teamId)
if (!request.team) {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
}
}
if (request.session.User) {
request.sessionUser = true
request.instanceTokenReq = false
if (!request.teamMembership) {
request.teamMembership = await request.session.User.getTeamMembership(request.team.id)
}
} else if (request.session.ownerType === 'project' || request.session.ownerType === 'device') {
// this is a request from a project or device
request.sessionUserReq = false
request.instanceTokenReq = true
} else {
reply.code(403).send({ code: 'unauthorized', error: 'Unauthorized' })
throw new Error('Unauthorized')
}
})

/**
* Get the MCP servers for a team
* @name /api/v1/teams/:teamId/mcp
* @static
* @memberof forge.routes.api.team.mcp
*/
app.get('/', {
preHandler: app.needsPermission('team:mcp:list'),
schema: {
summary: '',
tags: ['MCP'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
count: { type: 'number' },
servers: { $ref: 'MCPRegistrationSummaryList' }
}
},
'4xx': {
$ref: 'APIError'
},
500: {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
try {
const mcpServers = await app.db.models.MCPRegistration.byTeam(request.params.teamId)
const mcpServersView = app.db.views.MCPRegistrations.MCPRegistrationSummaryList(mcpServers)
reply.send({ count: mcpServers.length, servers: mcpServersView })
} catch (err) {
reply.status(500).send({ code: 'unexpected_error', error: 'Failed to find mcp entries for team' })
}
})

app.post('/:type/:typeId/:nodeId', {
preHandler: async (request, reply) => {
if (request.session.ownerType === 'project' || request.session.ownerType === 'device') {
// all good
} else {
reply.code(403).send({ code: 'unauthorized', error: 'Unauthorized' })
}
},
schema: {
summary: '',
tags: ['MCP'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' },
type: { type: 'string' },
typeId: { type: 'string' },
nodeId: { type: 'string' }
}
},
body: {
type: 'object',
properties: {
name: { type: 'string' },
endpointRoute: { type: 'string' },
protocol: { type: 'string' },
title: { type: 'string' },
version: { type: 'string' },
description: { type: 'string' }
}
},
response: {
200: {
type: 'object'
},
500: {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
try {
let typeId = request.params.typeId
if (request.params.type === 'device') {
const device = await app.db.models.Device.byId(request.params.typeId)
if (!device) {
throw new Error(`Device '${request.params.typeId}' not found`)
}
typeId = device.id
} else if (request.params.type === 'instance') {
const project = await app.db.models.Project.byId(request.params.typeId)
if (!project) {
throw new Error(`Instance '${request.params.typeId}' not found`)
}
} else {
throw new Error(`Unknown MCP target type '${request.params.type}'`)
}

await app.db.models.MCPRegistration.upsert({
targetType: request.params.type,
targetId: typeId,
nodeId: request.params.nodeId,
title: request.body.title,
version: request.body.version,
description: request.body.description,
name: request.body.name,
endpointRoute: request.body.endpointRoute,
protocol: request.body.protocol,
TeamId: request.team.id
}, {
fields: ['name', 'endpointRoute', 'title', 'version', 'description'],
conflictFields: ['TeamId', 'targetType', 'nodeId', 'targetId']
})
} catch (err) {
app.log.error(`register MCP Server ${err.toString()}`)
reply.status(500).send({ code: 'unexpected_error', error: 'Failed to create mcp entry' })
return
}
reply.send({})
})

app.delete('/:type/:typeId/:nodeId', {
preHandler: async (request, reply) => {
if (request.session.ownerType === 'project' || request.session.ownerType === 'device') {
// all good
} else {
reply.code(403).send({ code: 'unauthorized', error: 'Unauthorized' })
}
},
schema: {
summary: '',
tags: ['MCP'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' },
type: { type: 'string' },
typeId: { type: 'string' },
nodeId: { type: 'string' }
}
},
response: {
200: {
type: 'object'
},
'4xx': {
$ref: 'APIError'
},
500: {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
try {
const mcpServer = await app.db.models.MCPRegistration.byTypeAndIDs(request.params.type, request.params.typeId, request.params.nodeId)
if (mcpServer) {
await mcpServer.destroy()
reply.send({})
} else {
reply.status(404).send({ code: 'not_found', error: 'MCP server not found' })
}
} catch (err) {
app.log.error(`delete MCP Server ${err.toString()}`)
reply.status(500).send({ code: 'unexpected_error', error: 'Failed to delete mcp entry' })
}
})
await app.register(require('./registrations'), { prefix: '/api/v1/teams/:teamId/mcp', logLevel: app.config.logging.http })
await app.register(require('./server'), { prefix: '/api/v1/mcp', logLevel: app.config.logging.http })
}
Loading
Loading