From 3b72eb9856c5f04bb29671ba9760521a1f341d05 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 25 Mar 2026 15:31:11 +0100 Subject: [PATCH 1/7] Added tools that give users access to running instances --- src/management-system-v2/lib/data/roles.ts | 6 +- .../lib/engines/deployment.ts | 25 ++- .../lib/engines/server-actions.ts | 8 +- .../lib/helpers/javascriptHelpers.ts | 20 ++ .../tools/getAccessibleTools.ts | 6 +- .../tools/getAllExecutions.ts | 59 ++++++ .../tools/getExecutionInfo.ts | 193 ++++++++++++++++++ 7 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 src/management-system-v2/tools/getAllExecutions.ts create mode 100644 src/management-system-v2/tools/getExecutionInfo.ts diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts index d8c4bf5b5..036e05ab9 100644 --- a/src/management-system-v2/lib/data/roles.ts +++ b/src/management-system-v2/lib/data/roles.ts @@ -10,7 +10,7 @@ import { getRoles as _getRoles, getUserRoles as _getUserRoles, } from '@/lib/data/db/iam/roles'; -import { UnauthorizedError } from '../ability/abilityHelper'; +import Ability, { UnauthorizedError } from '../ability/abilityHelper'; export async function deleteRoles(envitonmentId: string, roleIds: string[]) { const { ability } = await getCurrentEnvironment(envitonmentId); @@ -63,9 +63,9 @@ export async function updateRole( } } -export async function getRoles(environmentId: string) { +export async function getRoles(environmentId: string, ability?: Ability) { try { - const { ability } = await getCurrentEnvironment(environmentId); + if (!ability) ({ ability } = await getCurrentEnvironment(environmentId)); return await _getRoles(environmentId, ability); } catch (_) { diff --git a/src/management-system-v2/lib/engines/deployment.ts b/src/management-system-v2/lib/engines/deployment.ts index ef341b51f..013ec2f32 100644 --- a/src/management-system-v2/lib/engines/deployment.ts +++ b/src/management-system-v2/lib/engines/deployment.ts @@ -273,8 +273,23 @@ export type InstanceInfo = { milestones: { [name: string]: number }; priority?: number; costsRealSetByOwner?: string; + performers?: { + user: string[]; + roles: string[]; + }; + actualOwner?: string[]; }[]; - variables: {}; + variables: Record< + string, + { + value: any; + log: { + changedTime: number; + changedBy: string; + oldValue?: any; + }[]; + } + >; log: { flowElementId: string; tokenId: string; @@ -294,10 +309,18 @@ export type InstanceInfo = { executionWasInterrupted?: true; priority?: number; costsRealSetByOwner?: string; + performers?: { + user: string[]; + roles: string[]; + }; + actualOwner?: string[]; }[]; adaptationLog: any[]; processVersion: string; userTasks: any[]; + managementSystemLocation?: string; + processInitiator?: string; + spaceIdOfProcessInitiator?: string; }; export type DeployedProcessInfo = { definitionId: string; diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 68aa51741..09473796b 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -2,7 +2,6 @@ import { UserFacingError, getErrorMessage, userError } from '../user-error'; import { - DeployedProcessInfo, deployProcess as _deployProcess, getDeployments as fetchDeployments, getDeployment as fetchDeployment, @@ -11,7 +10,7 @@ import { } from './deployment'; import { Engine, SpaceEngine } from './machines'; import { savedEnginesToEngines } from './saved-engines-helpers'; -import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; +import { getCurrentEnvironment } from '@/components/auth'; import { enableUseDB } from 'FeatureFlags'; import { getDbEngines, getDbEngineByAddress } from '@/lib/data/db/engines'; import { asyncFilter, asyncMap, asyncForEach } from '../helpers/javascriptHelpers'; @@ -46,7 +45,6 @@ import { import { getFileFromMachine, submitFileToMachine, updateVariablesOnMachine } from './instances'; import { getProcessIds, getVariablesFromElementById } from '@proceed/bpmn-helper'; import { Variable } from '@proceed/bpmn-helper/src/getters'; -import { getUsersInSpace } from '../data/db/iam/memberships'; import Ability from '../ability/abilityHelper'; import { getUserById } from '../data/db/iam/users'; @@ -614,8 +612,8 @@ export async function getAvailableSpaceEngines(spaceId: string) { } } -export async function getDeployment(spaceId: string, definitionId: string) { - const engines = await getCorrectTargetEngines(spaceId); +export async function getDeployment(spaceId: string, definitionId: string, ability?: Ability) { + const engines = await getCorrectTargetEngines(spaceId, undefined, undefined, ability); const deployments = await fetchDeployments(engines); diff --git a/src/management-system-v2/lib/helpers/javascriptHelpers.ts b/src/management-system-v2/lib/helpers/javascriptHelpers.ts index b95d12f8f..a912253a4 100644 --- a/src/management-system-v2/lib/helpers/javascriptHelpers.ts +++ b/src/management-system-v2/lib/helpers/javascriptHelpers.ts @@ -1,3 +1,5 @@ +import { Prettify } from '../typescript-utils'; + export async function asyncMap( array: Array, cb: (entry: Type, index: number) => Promise, @@ -26,6 +28,24 @@ export async function asyncFilter(array: Array, cb: (entry: Type) => ).filter((entry) => entry) as Array; } +export function pick( + obj: T, + keys: PickKeys, +): Prettify> { + return Object.fromEntries( + Object.entries(obj).filter(([key]) => keys.includes(key as keyof T)), + ) as Prettify>; +} + +export function omit( + obj: T, + keys: OmitKeys, +): Prettify> { + return Object.fromEntries( + Object.entries(obj).filter(([key]) => !keys.includes(key as keyof T)), + ) as Prettify>; +} + export interface DiffResult { path: string; valueA: any; diff --git a/src/management-system-v2/tools/getAccessibleTools.ts b/src/management-system-v2/tools/getAccessibleTools.ts index cd9a73d94..8e71affa4 100644 --- a/src/management-system-v2/tools/getAccessibleTools.ts +++ b/src/management-system-v2/tools/getAccessibleTools.ts @@ -60,11 +60,13 @@ export default async function getAvailableTools({ userCode }: InferSchema) { + try { + const verification = await verifyCode(userCode); + if (isUserErrorResponse(verification)) return `Error: ${verification.error.message}`; + + const { userId, environmentId, ability } = verification; + + let accessible = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.executions'], + [ + ['view', 'Execution'], + ['view', 'Machine'], + ], + ); + + if (!accessible) + return 'Error: The user cannot access execution information in this space. This might be due to a space wide setting or due to the user not having the permission to view execution information.'; + + const engines = await getCorrectTargetEngines(environmentId, undefined, undefined, ability); + + const deployments = await getDeployments(engines, 'instances'); + + const instanceIds = new Set(); + + deployments.forEach((d) => d.instances.forEach((i) => instanceIds.add(i.processInstanceId))); + + return { + content: [{ type: 'text', text: JSON.stringify([...instanceIds]) }], + }; + } catch (err) { + if (err instanceof Error) return err.message; + else return 'Error: Something went wrong'; + } +} diff --git a/src/management-system-v2/tools/getExecutionInfo.ts b/src/management-system-v2/tools/getExecutionInfo.ts new file mode 100644 index 000000000..815a7fe7a --- /dev/null +++ b/src/management-system-v2/tools/getExecutionInfo.ts @@ -0,0 +1,193 @@ +import { z } from 'zod'; +import { type InferSchema } from 'xmcp'; +import { isAccessible, toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; +import { isUserErrorResponse } from '@/lib/user-error'; +import { getDeployment } from '@/lib/engines/server-actions'; +import { omit, pick } from '@/lib/helpers/javascriptHelpers'; +import { getRoles } from '@/lib/data/roles'; +import { truthyFilter } from '@/lib/typescript-utils'; +import { getFullMembersWithRoles } from '@/lib/data/db/iam/memberships'; +import { getElementById, toBpmnObject } from '@proceed/bpmn-helper'; +import { InstanceInfo } from '@/lib/engines/deployment'; + +// Define the schema for tool parameters +export const schema = toAuthorizationSchema({ + instanceId: z.string().describe('The id of the process execution to inspect.'), +}); + +// Define tool metadata +export const metadata = { + name: 'get-execution-info', + description: + "Returns information about the current state of a process' execution in the form of a json file.", + annotations: { + title: 'Get execution info', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + }, +}; + +// Tool implementation +export default async function getExecutionInfo({ + userCode, + instanceId, +}: InferSchema) { + try { + const verification = await verifyCode(userCode); + if (isUserErrorResponse(verification)) return `Error: ${verification.error.message}`; + + const { userId, environmentId, ability } = verification; + + let accessible = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.executions'], + [ + ['view', 'Execution'], + ['view', 'Machine'], + ], + ); + + if (!accessible) + return 'Error: The user cannot access execution information in this space. This might be due to a space wide setting or due to the user not having the permission to view execution information.'; + + const [definitionId] = instanceId.split('-_'); + + const deployment = await getDeployment(environmentId, definitionId, ability); + + if (!deployment) return 'Could not find an execution with the given id.'; + + const instance = deployment.instances.find((i) => i.processInstanceId === instanceId); + + if (!instance) return 'Could not find an execution with the given id.'; + + const usersWithRoles = await getFullMembersWithRoles(environmentId, ability); + + let roles = await getRoles(environmentId, ability); + if (isUserErrorResponse(roles)) roles = []; + + const users = Object.fromEntries( + usersWithRoles.map((user) => [ + user.id, + { + ...pick(user, ['id', 'username', 'firstName', 'lastName', 'email']), + roles: user.roles.map((r) => pick(r, ['id', 'name', 'description'])), + }, + ]), + ); + const roleMap = Object.fromEntries( + roles.map((role) => [role.id, pick(role, ['id', 'name', 'description'])]), + ); + + const idToUser = (id: string) => { + if (id in users) return { type: 'user', ...users[id] }; + }; + + const idToRole = (id: string) => { + if (id in roleMap) return { type: 'role', ...roleMap[id] }; + }; + + const version = deployment.versions.find((v) => v.versionId === instance.processVersion); + + const bpmnObj = version ? await toBpmnObject(version.bpmn) : undefined; + const idToName = (id: string) => { + if (bpmnObj) { + return (getElementById(bpmnObj, id) as any)?.name; + } + }; + + const transformPerformerInfo = < + T extends { + actualOwner?: string[]; + performers?: InstanceInfo['tokens'][number]['performers']; + }, + >( + input: T, + ) => { + return { + ...omit(input, ['actualOwner', 'performers']), + actualPerformers: input.actualOwner + ? input.actualOwner.map(idToUser).filter(truthyFilter) + : undefined, + potentialPerformers: input.performers + ? { + user: input.performers.user.map(idToUser).filter(truthyFilter), + roles: input.performers.roles.map(idToRole).filter(truthyFilter), + } + : undefined, + }; + }; + + // extend the instance information object with data that might be useful to the user and the LLM + // the most significant changes are mapping from user/role ids to actual user/role information + // insertions of process element names alongside process element ids + const mappedInstance = { + // this information is not needed by/already known to the LLM + ...omit(instance, ['managementSystemLocation', 'spaceIdOfProcessInitiator']), + processInitiator: instance.processInitiator ? idToUser(instance.processInitiator) : undefined, + tokens: instance.tokens.map((t) => ({ + ...transformPerformerInfo(t), + // extend with user readable information + currentFlowElementName: idToName(t.currentFlowElementId), + })), + variables: Object.fromEntries( + Object.entries(instance.variables).map(([key, info]) => [ + key, + { + ...info, + // add the name so the llm can show it instead of the id + log: info.log.map((l) => ({ ...l, changedByElementName: idToName(l.changedBy) })), + }, + ]), + ), + log: instance.log.map((l) => ({ + ...transformPerformerInfo(l), + // add the name so the llm can show it instead of the id + flowElementName: idToName(l.flowElementId), + actualPerformers: l.actualOwner ? l.actualOwner.map(idToUser) : undefined, + potentialPerformers: l.performers + ? { + user: l.performers.user.map(idToUser), + roles: l.performers.roles.map(idToRole), + } + : undefined, + })), + // remove execution information needed by the engine + processVersion: version ? omit(version, ['bpmn', 'needs']) : instance.processVersion, + userTasks: instance.userTasks?.map((uT) => ({ + // remove execution information needed by the engine + ...omit(transformPerformerInfo(uT), [ + 'processInstance', + 'definitionVersion', + '$type', + 'implementation', + 'attrs', + 'resources', + ]), + // unwrap the name of the file in which the html for the user task is saved + fileName: uT.attrs?.['proceed:fileName'], + // map engine internal information about potential owners to user readable information + potentialPerformers: uT.resources + ?.map((r: any) => { + try { + const { user, roles } = JSON.parse(r.resourceAssignmentExpression.expression.body); + return [...user.map(idToUser), ...roles.map(idToRole)]; + } catch (err) { } + + return undefined; + }) + .filter(truthyFilter) + .flat(), + })), + }; + + return { + content: [{ type: 'text', text: JSON.stringify(mappedInstance) }], + }; + } catch (err) { + if (err instanceof Error) return err.message; + else return 'Error: Something went wrong'; + } +} From 7d6039f7471955cb0bcd48e24b668a4b0c560b06 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 6 May 2026 15:55:11 +0200 Subject: [PATCH 2/7] Ran prettier --- src/management-system-v2/tools/getExecutionInfo.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/management-system-v2/tools/getExecutionInfo.ts b/src/management-system-v2/tools/getExecutionInfo.ts index 815a7fe7a..ba5c40eeb 100644 --- a/src/management-system-v2/tools/getExecutionInfo.ts +++ b/src/management-system-v2/tools/getExecutionInfo.ts @@ -113,9 +113,9 @@ export default async function getExecutionInfo({ : undefined, potentialPerformers: input.performers ? { - user: input.performers.user.map(idToUser).filter(truthyFilter), - roles: input.performers.roles.map(idToRole).filter(truthyFilter), - } + user: input.performers.user.map(idToUser).filter(truthyFilter), + roles: input.performers.roles.map(idToRole).filter(truthyFilter), + } : undefined, }; }; @@ -149,9 +149,9 @@ export default async function getExecutionInfo({ actualPerformers: l.actualOwner ? l.actualOwner.map(idToUser) : undefined, potentialPerformers: l.performers ? { - user: l.performers.user.map(idToUser), - roles: l.performers.roles.map(idToRole), - } + user: l.performers.user.map(idToUser), + roles: l.performers.roles.map(idToRole), + } : undefined, })), // remove execution information needed by the engine @@ -174,7 +174,7 @@ export default async function getExecutionInfo({ try { const { user, roles } = JSON.parse(r.resourceAssignmentExpression.expression.body); return [...user.map(idToUser), ...roles.map(idToRole)]; - } catch (err) { } + } catch (err) {} return undefined; }) From a737e18162d08aa9091bec1b5d3894006759f0e9 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 6 May 2026 16:05:56 +0200 Subject: [PATCH 3/7] Fixed: Build error due to missing import --- src/management-system-v2/lib/engines/server-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 61abc6afc..acb4033f2 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -13,7 +13,7 @@ import { } from './deployment'; import { Engine, SpaceEngine } from './machines'; import { savedEnginesToEngines } from './saved-engines-helpers'; -import { getCurrentEnvironment } from '@/components/auth'; +import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; import { enableUseDB } from 'FeatureFlags'; import { getDbEngines, getDbEngineByAddress } from '@/lib/data/db/engines'; import { asyncFilter, asyncMap, asyncForEach } from '../helpers/javascriptHelpers'; From 45e80b26ca91d594cee9b724ef8dcfd456e231a9 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 6 May 2026 16:21:43 +0200 Subject: [PATCH 4/7] Fixed: typescript error --- src/management-system-v2/tools/getExecutionInfo.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/tools/getExecutionInfo.ts b/src/management-system-v2/tools/getExecutionInfo.ts index ba5c40eeb..f98ce3012 100644 --- a/src/management-system-v2/tools/getExecutionInfo.ts +++ b/src/management-system-v2/tools/getExecutionInfo.ts @@ -138,7 +138,10 @@ export default async function getExecutionInfo({ { ...info, // add the name so the llm can show it instead of the id - log: info.log.map((l) => ({ ...l, changedByElementName: idToName(l.changedBy) })), + log: info.log.map((l) => ({ + ...l, + changedByElementName: l.changedBy && idToName(l.changedBy), + })), }, ]), ), From f0f60627f7138688abe611da62724ec304d13d1a Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Mon, 18 May 2026 18:13:03 +0200 Subject: [PATCH 5/7] Fixed a typo --- src/management-system-v2/lib/data/roles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts index 88decb969..99462501d 100644 --- a/src/management-system-v2/lib/data/roles.ts +++ b/src/management-system-v2/lib/data/roles.ts @@ -16,8 +16,8 @@ import { Role } from './role-schema'; import db from '@/lib/data/db'; import { addRoleMappings } from './db/iam/role-mappings'; -export async function deleteRoles(envitonmentId: string, roleIds: string[]) { - const { ability } = await getCurrentEnvironment(envitonmentId); +export async function deleteRoles(environmentId: string, roleIds: string[]) { + const { ability } = await getCurrentEnvironment(environmentId); try { for (const roleId of roleIds) { From 62c376657e53421355067bdac89fab3223082398 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Mon, 18 May 2026 18:31:22 +0200 Subject: [PATCH 6/7] Adding initiator information when an instance is started through mcp --- src/management-system-v2/tools/startProcess.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/management-system-v2/tools/startProcess.ts b/src/management-system-v2/tools/startProcess.ts index 98453dacd..a6394968d 100644 --- a/src/management-system-v2/tools/startProcess.ts +++ b/src/management-system-v2/tools/startProcess.ts @@ -94,6 +94,10 @@ export default async function startProcess({ engine, startParameters && Object.fromEntries(Object.entries(startParameters).map(([key, value]) => [key, { value }])), + { + processInitiator: userId, + spaceIdOfProcessInitiator: environmentId, + }, ); if (isUserErrorResponse(instanceId)) { From 75d2b89e8dd374343d64afaf62a2b91efc345448 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Mon, 18 May 2026 18:37:04 +0200 Subject: [PATCH 7/7] Small code improvement --- src/management-system-v2/tools/getExecutionInfo.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/management-system-v2/tools/getExecutionInfo.ts b/src/management-system-v2/tools/getExecutionInfo.ts index f98ce3012..c74c18ee4 100644 --- a/src/management-system-v2/tools/getExecutionInfo.ts +++ b/src/management-system-v2/tools/getExecutionInfo.ts @@ -120,6 +120,8 @@ export default async function getExecutionInfo({ }; }; + console.log(JSON.stringify(instance, null, 2)); + // extend the instance information object with data that might be useful to the user and the LLM // the most significant changes are mapping from user/role ids to actual user/role information // insertions of process element names alongside process element ids @@ -173,7 +175,7 @@ export default async function getExecutionInfo({ fileName: uT.attrs?.['proceed:fileName'], // map engine internal information about potential owners to user readable information potentialPerformers: uT.resources - ?.map((r: any) => { + ?.flatMap((r: any) => { try { const { user, roles } = JSON.parse(r.resourceAssignmentExpression.expression.body); return [...user.map(idToUser), ...roles.map(idToRole)]; @@ -181,8 +183,7 @@ export default async function getExecutionInfo({ return undefined; }) - .filter(truthyFilter) - .flat(), + .filter(truthyFilter), })), };