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
9 changes: 8 additions & 1 deletion forge/ee/routes/mcp/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ module.exports = async function (app) {
await server.connect(transport)

// Hand off response handling to the MCP transport.
// reply.hijack() tells Fastify we're managing the response directly.
// reply.hijack() tells Fastify we're managing the response directly,
// which means Fastify plugins (including CORS) won't set headers.
// Set CORS headers manually on the raw response before hijacking.
const origin = request.headers.origin
if (origin) {
reply.raw.setHeader('Access-Control-Allow-Origin', origin)
reply.raw.setHeader('Access-Control-Allow-Credentials', 'true')
}
reply.hijack()

// The MCP SDK's transport uses @hono/node-server internally, which sets
Expand Down
46 changes: 46 additions & 0 deletions forge/ee/routes/mcp/tools/applications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { z } = require('zod')

module.exports = [
{
name: 'platform.list-applications',
description: 'List all applications in a team.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId: z.string().describe('The ID or hashid of the team')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/applications` })
return response
}
},
{
name: 'platform.get-application',
description: 'Get details of a specific application, including its instances and devices.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
applicationId: z.string().describe('The ID or hashid of the application')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/applications/${args.applicationId}` })
return response
}
},
{
name: 'platform.create-application',
description: 'Create a new application in a team.',
annotations: { readOnlyHint: false, destructiveHint: false },
inputSchema: {
name: z.string().describe('Name for the new application'),
teamId: z.string().describe('The ID or hashid of the team to create the application in'),
description: z.string().optional().describe('Optional description for the application')
},
handler: async (args, { inject }) => {
const payload = { name: args.name, teamId: args.teamId }
if (args.description) {
payload.description = args.description
}
const response = await inject({ method: 'POST', url: '/api/v1/applications', payload })
return response
}
}
]
32 changes: 32 additions & 0 deletions forge/ee/routes/mcp/tools/catalog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module.exports = [
{
name: 'platform.list-instance-types',
description: 'List all available instance types. Use this to find valid projectType values when creating an instance.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: '/api/v1/project-types' })
return response
}
},
{
name: 'platform.list-stacks',
description: 'List all available stacks (Node-RED versions). Use this to find valid stack values when creating an instance.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: '/api/v1/stacks' })
return response
}
},
{
name: 'platform.list-blueprints',
description: 'List all available flow blueprints. Blueprints provide starter flows that can be used when creating a new instance.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: '/api/v1/flow-blueprints' })
return response
}
}
]
61 changes: 61 additions & 0 deletions forge/ee/routes/mcp/tools/devices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { z } = require('zod')

module.exports = [
{
name: 'platform.list-devices',
description: 'List all devices in a team.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
teamId: z.string().describe('The ID or hashid of the team')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/teams/${args.teamId}/devices` })
return response
}
},
{
name: 'platform.get-device',
description: 'Get details of a specific device, including its status, assigned application, and target snapshot.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
deviceId: z.string().describe('The ID or hashid of the device')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/devices/${args.deviceId}` })
return response
}
},
{
name: 'platform.list-device-snapshots',
description: 'List all snapshots for an application-owned device.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
deviceId: z.string().describe('The ID or hashid of the device')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/devices/${args.deviceId}/snapshots` })
return response
}
},
{
name: 'platform.create-device-snapshot',
description: 'Create a snapshot from an application-owned device, capturing its current state.',
annotations: { readOnlyHint: false, destructiveHint: false },
inputSchema: {
deviceId: z.string().describe('The ID or hashid of the device'),
name: z.string().optional().describe('Name for the snapshot'),
description: z.string().optional().describe('Description of the snapshot')
},
handler: async (args, { inject }) => {
const payload = {}
if (args.name) {
payload.name = args.name
}
if (args.description) {
payload.description = args.description
}
const response = await inject({ method: 'POST', url: `/api/v1/devices/${args.deviceId}/snapshots`, payload })
return response
}
}
]
104 changes: 104 additions & 0 deletions forge/ee/routes/mcp/tools/instances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const { z } = require('zod')

module.exports = [
{
name: 'platform.list-instances',
description: 'List all instances in an application.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
applicationId: z.string().describe('The ID or hashid of the application')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/applications/${args.applicationId}/instances` })
return response
}
},
{
name: 'platform.get-instance',
description: 'Get details of a specific instance, including its current state, URL, and settings.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
instanceId: z.string().describe('The ID or hashid of the instance')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}` })
return response
}
},
{
name: 'platform.get-instance-status',
description: 'Get the live runtime status of an instance (running, stopped, suspended, starting, etc).',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
instanceId: z.string().describe('The ID or hashid of the instance')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/status` })
return response
}
},
{
name: 'platform.get-instance-logs',
description: 'Get runtime logs for an instance. Useful for debugging after restarts or failures.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
instanceId: z.string().describe('The ID or hashid of the instance'),
limit: z.number().optional().describe('Number of log entries to return (default 30)'),
cursor: z.string().optional().describe('Cursor for pagination')
},
handler: async (args, { inject }) => {
let url = `/api/v1/projects/${args.instanceId}/logs`
const params = []
if (args.limit) {
params.push(`limit=${args.limit}`)
}
if (args.cursor) {
params.push(`cursor=${args.cursor}`)
}
if (params.length > 0) {
url += '?' + params.join('&')
}
const response = await inject({ method: 'GET', url })
return response
}
},
{
name: 'platform.check-name-availability',
description: 'Check if an instance name is available.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
name: z.string().describe('The instance name to check')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'POST', url: '/api/v1/projects/check-name', payload: { name: args.name } })
return response
}
},
{
name: 'platform.create-instance',
description: 'Create a new Node-RED instance in an application. The instance starts automatically after creation. Use platform.list-instance-types, platform.list-stacks, and platform.list-blueprints to discover valid values for the required parameters.',
annotations: { readOnlyHint: false, destructiveHint: false },
inputSchema: {
name: z.string().describe('Name for the new instance'),
applicationId: z.string().describe('The ID or hashid of the application'),
projectType: z.string().describe('The ID of the instance type (use platform.list-instance-types to find valid values)'),
stack: z.string().describe('The ID of the stack (use platform.list-stacks to find valid values)'),
template: z.string().describe('The ID of the template'),
flowBlueprintId: z.string().optional().describe('Optional blueprint ID to initialize the instance with starter flows')
},
handler: async (args, { inject }) => {
const payload = {
name: args.name,
applicationId: args.applicationId,
projectType: args.projectType,
stack: args.stack,
template: args.template
}
if (args.flowBlueprintId) {
payload.flowBlueprintId = args.flowBlueprintId
}
const response = await inject({ method: 'POST', url: '/api/v1/projects', payload })
return response
}
}
]
42 changes: 42 additions & 0 deletions forge/ee/routes/mcp/tools/navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { z } = require('zod')

module.exports = [
{
name: 'platform.open-editor',
description: 'Get the URL to open the Node-RED editor for an instance. Returns a URL the user can open in their browser.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
instanceId: z.string().describe('The ID or hashid of the instance')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}` })
if (response.statusCode >= 400) {
return response
}
const instance = response.json()
return {
statusCode: 200,
json: () => ({ url: `${instance.url}/editor`, name: instance.name })
}
}
},
{
name: 'platform.open-instance',
description: 'Get the URL to open the instance dashboard in the FlowFuse platform. Returns a URL the user can open in their browser.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
instanceId: z.string().describe('The ID or hashid of the instance')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}` })
if (response.statusCode >= 400) {
return response
}
const instance = response.json()
return {
statusCode: 200,
json: () => ({ url: instance.url, name: instance.name })
}
}
}
]
37 changes: 37 additions & 0 deletions forge/ee/routes/mcp/tools/snapshots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { z } = require('zod')

module.exports = [
{
name: 'platform.list-snapshots',
description: 'List all snapshots for an instance.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
instanceId: z.string().describe('The ID or hashid of the instance')
},
handler: async (args, { inject }) => {
const response = await inject({ method: 'GET', url: `/api/v1/projects/${args.instanceId}/snapshots` })
return response
}
},
{
name: 'platform.create-snapshot',
description: 'Create a snapshot of an instance, capturing its current flows, settings, and credentials.',
annotations: { readOnlyHint: false, destructiveHint: false },
inputSchema: {
instanceId: z.string().describe('The ID or hashid of the instance'),
name: z.string().optional().describe('Name for the snapshot'),
description: z.string().optional().describe('Description of the snapshot')
},
handler: async (args, { inject }) => {
const payload = {}
if (args.name) {
payload.name = args.name
}
if (args.description) {
payload.description = args.description
}
const response = await inject({ method: 'POST', url: `/api/v1/projects/${args.instanceId}/snapshots`, payload })
return response
}
}
]
4 changes: 2 additions & 2 deletions forge/ee/routes/mcp/tools/teams.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { z } = require('zod')

module.exports = [
{
name: 'list-teams',
name: 'platform.list-teams',
description: 'List all teams the authenticated user belongs to. Returns team names, slugs, IDs, and membership roles.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {},
Expand All @@ -12,7 +12,7 @@ module.exports = [
}
},
{
name: 'get-team',
name: 'platform.get-team',
description: 'Get details of a specific team by its ID, including team type, member count, and instance counts.',
annotations: { readOnlyHint: true, destructiveHint: false },
inputSchema: {
Expand Down
Loading
Loading