-
Notifications
You must be signed in to change notification settings - Fork 10
MCP: Instance Execution Monitoring #737
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
3b72eb9
Added tools that give users access to running instances
jjoderis b0fd9b6
Merge branch 'main' of github.com:PROCEED-Labs/proceed into mcp-proce…
jjoderis 7d6039f
Ran prettier
jjoderis a737e18
Fixed: Build error due to missing import
jjoderis 45e80b2
Fixed: typescript error
jjoderis 3cc5861
Merge branch 'main' of github.com:PROCEED-Labs/proceed into mcp-proce…
jjoderis f0f6062
Fixed a typo
jjoderis 62c3766
Adding initiator information when an instance is started through mcp
jjoderis 75d2b89
Small code improvement
jjoderis 3dff5c2
Merge branch 'main' into mcp-process-execution-monitoring
jjoderis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { type InferSchema } from 'xmcp'; | ||
| import { isAccessible, toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; | ||
| import { isUserErrorResponse } from '@/lib/user-error'; | ||
| import { getCorrectTargetEngines } from '@/lib/engines/server-actions'; | ||
| import { getDeployments } from '@/lib/engines/deployment'; | ||
|
|
||
| // Define the schema for tool parameters | ||
| export const schema = toAuthorizationSchema({}); | ||
|
|
||
| // Define tool metadata | ||
| export const metadata = { | ||
| name: 'get-executions', | ||
| description: 'Get all process executions that are accessible to the current user.', | ||
| annotations: { | ||
| title: 'Get executions', | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| }, | ||
| }; | ||
|
|
||
| // Tool implementation | ||
| export default async function getExecutions({ userCode }: InferSchema<typeof schema>) { | ||
| 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<string>(); | ||
|
|
||
| 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'; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| 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<typeof schema>) { | ||
| 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: l.changedBy && 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) => { | ||
|
canptura marked this conversation as resolved.
Outdated
|
||
| 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'; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.