Skip to content

Commit 84f5685

Browse files
authored
Extend MCP functionality
- Restructure MCP resources and added PNG resource - Restructured MCP tools - Use ID aliases - Extend agent options - Add workflow-specific
1 parent 05c8fc5 commit 84f5685

51 files changed

Lines changed: 3956 additions & 1015 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/workflow-server/src/common/graph-extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class TaskNodeBuilder<T extends TaskNode = TaskNode> extends GNodeBuilder
8686
protected createCompartmentHeader(): GLabel {
8787
return new GLabelBuilder(GLabel)
8888
.type(ModelTypes.LABEL_HEADING)
89-
.id(this.proxy.id + '_classname')
89+
.id(this.proxy.id + '_label')
9090
.text(this.proxy.name)
9191
.build();
9292
}
@@ -151,7 +151,7 @@ export class CategoryNodeBuilder<T extends Category = Category> extends Activity
151151
protected createCompartmentHeader(): GLabel {
152152
return new GLabelBuilder(GLabel)
153153
.type(ModelTypes.LABEL_HEADING)
154-
.id(this.proxy.id + '_classname')
154+
.id(this.proxy.id + '_label')
155155
.text(this.proxy.name)
156156
.build();
157157
}

examples/workflow-server/src/common/handler/create-decision-node-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ export class CreateDecisionNodeHandler extends CreateActivityNodeHandler {
2525
label = 'Decision Node';
2626

2727
protected override builder(point?: Point): ActivityNodeBuilder {
28-
return super.builder(point).addCssClass('decision').resizeLocations(GResizeLocation.CROSS);
28+
return super.builder(point).addCssClass('decision').resizeLocations(GResizeLocation.CROSS).size(32, 32);
2929
}
3030
}

examples/workflow-server/src/common/handler/create-merge-node-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ export class CreateMergeNodeHandler extends CreateActivityNodeHandler {
2525
label = 'Merge Node';
2626

2727
protected override builder(point: Point | undefined): ActivityNodeBuilder {
28-
return super.builder(point).addCssClass('merge').resizeLocations(GResizeLocation.CROSS);
28+
return super.builder(point).addCssClass('merge').resizeLocations(GResizeLocation.CROSS).size(32, 32);
2929
}
3030
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/********************************************************************************
2+
* Copyright (c) 2026 EclipseSource and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { GLabel, GModelElement } from '@eclipse-glsp/server';
18+
import { CreateNodesMcpToolHandler } from '@eclipse-glsp/server-mcp';
19+
import { injectable } from 'inversify';
20+
import { ModelTypes } from '../util/model-types';
21+
22+
@injectable()
23+
export class WorkflowCreateNodesMcpToolHandler extends CreateNodesMcpToolHandler {
24+
override getCorrespondingLabelId(element: GModelElement): string | undefined {
25+
// Category labels are nested in a header component
26+
if (element.type === ModelTypes.CATEGORY) {
27+
return element.children.find(child => child.type === ModelTypes.COMP_HEADER)?.children.find(child => child instanceof GLabel)
28+
?.id;
29+
}
30+
31+
// Assume that generally, labelled nodes have those labels as direct children
32+
return element.children.find(child => child instanceof GLabel)?.id;
33+
}
34+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/********************************************************************************
2+
* Copyright (c) 2026 EclipseSource and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { DefaultTypes } from '@eclipse-glsp/server';
18+
import {
19+
createResourceToolResult,
20+
ElementTypesMcpResourceHandler,
21+
GLSPMcpServer,
22+
objectArrayToMarkdownTable,
23+
ResourceHandlerResult
24+
} from '@eclipse-glsp/server-mcp';
25+
import { injectable } from 'inversify';
26+
import { ModelTypes } from '../util/model-types';
27+
import * as z from 'zod/v4';
28+
29+
interface ElementType {
30+
id: string;
31+
label: string;
32+
description: string;
33+
hasLabel: boolean;
34+
}
35+
36+
const WORKFLOW_NODE_ELEMENT_TYPES: ElementType[] = [
37+
{
38+
id: ModelTypes.AUTOMATED_TASK,
39+
label: 'Automated Task',
40+
description: 'Task without human input',
41+
hasLabel: true
42+
},
43+
{
44+
id: ModelTypes.MANUAL_TASK,
45+
label: 'Manual Task',
46+
description: 'Task done by a human',
47+
hasLabel: true
48+
},
49+
{
50+
id: ModelTypes.JOIN_NODE,
51+
label: 'Join Node',
52+
description: 'Gateway that merges parallel flows',
53+
hasLabel: false
54+
},
55+
{
56+
id: ModelTypes.FORK_NODE,
57+
label: 'Fork Node',
58+
description: 'Gateway that splits into parallel flows',
59+
hasLabel: false
60+
},
61+
{
62+
id: ModelTypes.MERGE_NODE,
63+
label: 'Merge Node',
64+
description: 'Gateway that merges alternative flows',
65+
hasLabel: false
66+
},
67+
{
68+
id: ModelTypes.DECISION_NODE,
69+
label: 'Decision Node',
70+
description: 'Gateway that splits into alternative flows',
71+
hasLabel: false
72+
},
73+
{
74+
id: ModelTypes.CATEGORY,
75+
label: 'Category',
76+
description: 'Container node that groups other elements',
77+
hasLabel: true
78+
}
79+
];
80+
const WORKFLOW_EDGE_ELEMENT_TYPES: ElementType[] = [
81+
{
82+
id: DefaultTypes.EDGE,
83+
label: 'Edge',
84+
description: 'Standard control flow edge',
85+
hasLabel: false
86+
},
87+
{
88+
id: ModelTypes.WEIGHTED_EDGE,
89+
label: 'Weighted Edge',
90+
description: 'Edge that indicates a weighted probability. Typically used with a Decision Node.',
91+
hasLabel: false
92+
}
93+
];
94+
95+
const WORKFLOW_ELEMENT_TYPES_STRING = [
96+
'# Creatable element types for diagram type "workflow-diagram"',
97+
'## Node Types',
98+
objectArrayToMarkdownTable(WORKFLOW_NODE_ELEMENT_TYPES),
99+
'## Edge Types',
100+
objectArrayToMarkdownTable(WORKFLOW_EDGE_ELEMENT_TYPES)
101+
].join('\n');
102+
103+
/**
104+
* The default {@link ElementTypesMcpResourceHandler} extracts a list of operations generically from
105+
* the `OperationHandlerRegistry`, because it can't know the details of a specific GLSP implementation.
106+
* This is naturally quite limited in expression and relies on semantically meaningful model types to be
107+
* able to inform an MCP client reliably.
108+
*
109+
* However, when overriding this for a specific implementation, we don't have those limitations. Rather,
110+
* since the available element types do not change dynamically, we can simply provide a statically generated
111+
* string.
112+
*/
113+
@injectable()
114+
export class WorkflowElementTypesMcpResourceHandler extends ElementTypesMcpResourceHandler {
115+
override registerToolAlternative(server: GLSPMcpServer): void {
116+
server.registerTool(
117+
'element-types',
118+
{
119+
title: 'Creatable Element Types',
120+
description:
121+
'List all element types (nodes and edges) that can be created for a specific diagram type. ' +
122+
'Use this to discover valid elementTypeId values for creation tools.',
123+
inputSchema: {
124+
diagramType: z.string().describe('Diagram type whose elements should be discovered')
125+
}
126+
},
127+
async params => createResourceToolResult(await this.handle(params))
128+
);
129+
}
130+
131+
override async handle({ diagramType }: { diagramType?: string }): Promise<ResourceHandlerResult> {
132+
this.logger.info(`'element-types' invoked for diagram type '${diagramType}'`);
133+
134+
// In this specifc GLSP implementation, only 'workflow-diagram' is valid
135+
if (diagramType !== 'workflow-diagram') {
136+
return {
137+
content: {
138+
uri: `glsp://types/${diagramType}/elements`,
139+
mimeType: 'text/plain',
140+
text: 'Invalid diagram type.'
141+
},
142+
isError: true
143+
};
144+
}
145+
146+
return {
147+
content: {
148+
uri: `glsp://types/${diagramType}/elements`,
149+
mimeType: 'text/markdown',
150+
text: WORKFLOW_ELEMENT_TYPES_STRING
151+
},
152+
isError: false
153+
};
154+
}
155+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/********************************************************************************
2+
* Copyright (c) 2026 EclipseSource and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { GModelElement } from '@eclipse-glsp/graph';
18+
import { DefaultTypes } from '@eclipse-glsp/server';
19+
import { DefaultMcpModelSerializer } from '@eclipse-glsp/server-mcp';
20+
import { injectable } from 'inversify';
21+
import { ModelTypes } from '../util/model-types';
22+
23+
/**
24+
* As compared to the {@link DefaultMcpModelSerializer}, this is a specific implementation and we
25+
* know not only the structure of our graph but also each relevant attribute. This enables us to
26+
* order them semantically so the produced serialization makes more sense if read with semantics
27+
* mind. As LLMs (i.e., the MCP clients) work semantically, this is superior to a random ordering.
28+
* Furthermore, including only the relevant information without redundancies decreases context size.
29+
*/
30+
@injectable()
31+
export class WorkflowMcpModelSerializer extends DefaultMcpModelSerializer {
32+
override prepareElement(element: GModelElement): Record<string, Record<string, any>[]> {
33+
const elements = this.flattenStructure(element);
34+
35+
// Define the order of keys
36+
const result: Record<string, Record<string, any>[]> = {
37+
[DefaultTypes.GRAPH]: [],
38+
[ModelTypes.CATEGORY]: [],
39+
[ModelTypes.AUTOMATED_TASK]: [],
40+
[ModelTypes.MANUAL_TASK]: [],
41+
[ModelTypes.FORK_NODE]: [],
42+
[ModelTypes.JOIN_NODE]: [],
43+
[ModelTypes.DECISION_NODE]: [],
44+
[ModelTypes.MERGE_NODE]: [],
45+
[DefaultTypes.EDGE]: [],
46+
[ModelTypes.WEIGHTED_EDGE]: []
47+
};
48+
elements.forEach(element => {
49+
this.combinePositionAndSize(element);
50+
51+
const adjustedElement = this.adjustElement(element);
52+
if (!adjustedElement) {
53+
return;
54+
}
55+
56+
result[element.type].push(adjustedElement);
57+
});
58+
59+
return result;
60+
}
61+
62+
private adjustElement(element: Record<string, any>): Record<string, any> | undefined {
63+
switch (element.type) {
64+
case ModelTypes.AUTOMATED_TASK:
65+
case ModelTypes.MANUAL_TASK: {
66+
const label = element.children.find((child: { type: string }) => child.type === ModelTypes.LABEL_HEADING);
67+
68+
// For tasks, the only content with impact on element size is the label
69+
// Therefore, all other factors get integrated into the label size for the AI to do proper resizing operations
70+
const labelSize = {
71+
// 10px padding right, 31px padding left (incl. icon)
72+
width: Math.trunc(label.size.width + 10 + 31),
73+
// 7px padding top and bottom each
74+
height: Math.trunc(label.size.height + 14)
75+
};
76+
77+
return {
78+
id: element.id,
79+
position: element.position,
80+
size: element.size,
81+
bounds: element.bounds,
82+
label: label.text,
83+
labelSize: labelSize,
84+
parentId: element.parent.type === ModelTypes.STRUCTURE ? element.parent.parent.id : element.parentId
85+
};
86+
}
87+
case ModelTypes.CATEGORY: {
88+
const label = element.children
89+
.find((child: { type: string }) => child.type === ModelTypes.COMP_HEADER)
90+
?.children.find((child: { type: string }) => child.type === ModelTypes.LABEL_HEADING);
91+
92+
const labelSize = {
93+
width: Math.trunc(label.size.width + 20),
94+
height: Math.trunc(label.size.height + 20)
95+
};
96+
97+
const usableSpaceSize = {
98+
width: Math.trunc(Math.max(0, element.size.width - 10)),
99+
height: Math.trunc(Math.max(0, element.size.height - labelSize.height - 10))
100+
};
101+
102+
return {
103+
id: element.id,
104+
isContainer: true,
105+
position: element.position,
106+
size: element.size,
107+
bounds: element.bounds,
108+
label: label.text,
109+
labelSize: labelSize,
110+
usableSpaceSize: usableSpaceSize,
111+
parentId: element.parentId
112+
};
113+
}
114+
case ModelTypes.JOIN_NODE:
115+
case ModelTypes.MERGE_NODE:
116+
case ModelTypes.DECISION_NODE:
117+
case ModelTypes.FORK_NODE: {
118+
return {
119+
id: element.id,
120+
position: element.position,
121+
size: element.size,
122+
bounds: element.bounds,
123+
parentId: element.parentId
124+
};
125+
}
126+
case DefaultTypes.EDGE: {
127+
return {
128+
id: element.id,
129+
sourceId: element.sourceId,
130+
targetId: element.targetId,
131+
parentId: element.parentId
132+
};
133+
}
134+
case ModelTypes.WEIGHTED_EDGE: {
135+
return {
136+
id: element.id,
137+
sourceId: element.sourceId,
138+
targetId: element.targetId,
139+
probability: element.probability,
140+
parentId: element.parentId
141+
};
142+
}
143+
case DefaultTypes.GRAPH: {
144+
return {
145+
id: element.id,
146+
isContainer: true
147+
};
148+
}
149+
default:
150+
return undefined;
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)