Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d0e330a
Restructured MCP resources and added PNG resource
Sakrafux Mar 6, 2026
c6fed18
Restructured MCP tools
Sakrafux Mar 7, 2026
e41d24f
Reorganized tool and resource registration
Sakrafux Mar 7, 2026
c6ec0f8
Added info and modifying tools
Sakrafux Mar 8, 2026
5737dad
Switched IDs from UUIDs to semantic IDs
Sakrafux Mar 8, 2026
6b8b49f
Moved new ID generation from generic framework code to actual impleme…
Sakrafux Mar 9, 2026
14dd726
Added workflow specific implementations
Sakrafux Mar 9, 2026
87f4ce0
Further refined model creation and modification, specifically for wor…
Sakrafux Mar 9, 2026
e67d7da
Make node and edge creation multi-input handlers
Sakrafux Mar 10, 2026
3051037
Aligned logic across handlers and added more documentation
Sakrafux Mar 10, 2026
25ebb63
Added undo/redo tools
Sakrafux Mar 10, 2026
832028e
Added modify-edges tool
Sakrafux Mar 10, 2026
6472a41
Added request-layout tool
Sakrafux Mar 10, 2026
80a42a8
Added get-selection tool and refined diagram-elements
Sakrafux Mar 11, 2026
eeeabbe
Restructured MCP png components
Sakrafux Mar 11, 2026
6407fca
Added viewport changes via change-view
Sakrafux Mar 11, 2026
7aa6f98
Added export
Sakrafux Mar 11, 2026
bbaae34
reverted custom ID generation
Sakrafux Mar 13, 2026
bb0c276
Added JSON/Markdown switch
Sakrafux Mar 13, 2026
b2bf157
Added ID alias feature with flag
Sakrafux Mar 13, 2026
bbe418c
Cleanup and agent guidance
Sakrafux Mar 24, 2026
3cfa276
Removed JSON flag
Sakrafux Mar 24, 2026
b6303f9
Cleanup of feature flags
Sakrafux Mar 24, 2026
6026bbc
Added README
Sakrafux Mar 24, 2026
8d5c119
Cleanup server across connections
Sakrafux Mar 25, 2026
58d34dd
Fixed buggy or unstable behavior
Sakrafux Mar 25, 2026
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
4 changes: 2 additions & 2 deletions examples/workflow-server/src/common/graph-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class TaskNodeBuilder<T extends TaskNode = TaskNode> extends GNodeBuilder
protected createCompartmentHeader(): GLabel {
return new GLabelBuilder(GLabel)
.type(ModelTypes.LABEL_HEADING)
.id(this.proxy.id + '_classname')
.id(this.proxy.id + '_label')
.text(this.proxy.name)
.build();
}
Expand Down Expand Up @@ -151,7 +151,7 @@ export class CategoryNodeBuilder<T extends Category = Category> extends Activity
protected createCompartmentHeader(): GLabel {
return new GLabelBuilder(GLabel)
.type(ModelTypes.LABEL_HEADING)
.id(this.proxy.id + '_classname')
.id(this.proxy.id + '_label')
.text(this.proxy.name)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ export class CreateDecisionNodeHandler extends CreateActivityNodeHandler {
label = 'Decision Node';

protected override builder(point?: Point): ActivityNodeBuilder {
return super.builder(point).addCssClass('decision').resizeLocations(GResizeLocation.CROSS);
return super.builder(point).addCssClass('decision').resizeLocations(GResizeLocation.CROSS).size(32, 32);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ export class CreateMergeNodeHandler extends CreateActivityNodeHandler {
label = 'Merge Node';

protected override builder(point: Point | undefined): ActivityNodeBuilder {
return super.builder(point).addCssClass('merge').resizeLocations(GResizeLocation.CROSS);
return super.builder(point).addCssClass('merge').resizeLocations(GResizeLocation.CROSS).size(32, 32);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/********************************************************************************
* Copyright (c) 2026 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { GLabel, GModelElement } from '@eclipse-glsp/server';
import { CreateNodesMcpToolHandler } from '@eclipse-glsp/server-mcp';
import { injectable } from 'inversify';
import { ModelTypes } from '../util/model-types';

@injectable()
export class WorkflowCreateNodesMcpToolHandler extends CreateNodesMcpToolHandler {
override getCorrespondingLabelId(element: GModelElement): string | undefined {
// Category labels are nested in a header component
if (element.type === ModelTypes.CATEGORY) {
return element.children.find(child => child.type === ModelTypes.COMP_HEADER)?.children.find(child => child instanceof GLabel)
?.id;
}

// Assume that generally, labelled nodes have those labels as direct children
return element.children.find(child => child instanceof GLabel)?.id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/********************************************************************************
* Copyright (c) 2026 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { DefaultTypes } from '@eclipse-glsp/server';
import {
createResourceToolResult,
ElementTypesMcpResourceHandler,
GLSPMcpServer,
objectArrayToMarkdownTable,
ResourceHandlerResult
} from '@eclipse-glsp/server-mcp';
import { injectable } from 'inversify';
import { ModelTypes } from '../util/model-types';
import * as z from 'zod/v4';

interface ElementType {
id: string;
label: string;
description: string;
hasLabel: boolean;
}

const WORKFLOW_NODE_ELEMENT_TYPES: ElementType[] = [
{
id: ModelTypes.AUTOMATED_TASK,
label: 'Automated Task',
description: 'Task without human input',
hasLabel: true
},
{
id: ModelTypes.MANUAL_TASK,
label: 'Manual Task',
description: 'Task done by a human',
hasLabel: true
},
{
id: ModelTypes.JOIN_NODE,
label: 'Join Node',
description: 'Gateway that merges parallel flows',
hasLabel: false
},
{
id: ModelTypes.FORK_NODE,
label: 'Fork Node',
description: 'Gateway that splits into parallel flows',
hasLabel: false
},
{
id: ModelTypes.MERGE_NODE,
label: 'Merge Node',
description: 'Gateway that merges alternative flows',
hasLabel: false
},
{
id: ModelTypes.DECISION_NODE,
label: 'Decision Node',
description: 'Gateway that splits into alternative flows',
hasLabel: false
},
{
id: ModelTypes.CATEGORY,
label: 'Category',
description: 'Container node that groups other elements',
hasLabel: true
}
];
const WORKFLOW_EDGE_ELEMENT_TYPES: ElementType[] = [
{
id: DefaultTypes.EDGE,
label: 'Edge',
description: 'Standard control flow edge',
hasLabel: false
},
{
id: ModelTypes.WEIGHTED_EDGE,
label: 'Weighted Edge',
description: 'Edge that indicates a weighted probability. Typically used with a Decision Node.',
hasLabel: false
}
];

const WORKFLOW_ELEMENT_TYPES_STRING = [
'# Creatable element types for diagram type "workflow-diagram"',
'## Node Types',
objectArrayToMarkdownTable(WORKFLOW_NODE_ELEMENT_TYPES),
'## Edge Types',
objectArrayToMarkdownTable(WORKFLOW_EDGE_ELEMENT_TYPES)
].join('\n');

/**
* The default {@link ElementTypesMcpResourceHandler} extracts a list of operations generically from
* the `OperationHandlerRegistry`, because it can't know the details of a specific GLSP implementation.
* This is naturally quite limited in expression and relies on semantically meaningful model types to be
* able to inform an MCP client reliably.
*
* However, when overriding this for a specific implementation, we don't have those limitations. Rather,
* since the available element types do not change dynamically, we can simply provide a statically generated
* string.
*/
@injectable()
export class WorkflowElementTypesMcpResourceHandler extends ElementTypesMcpResourceHandler {
override registerTool(server: GLSPMcpServer): void {
server.registerTool(
'element-types',
{
title: 'Creatable Element Types',
description:
'List all element types (nodes and edges) that can be created for a specific diagram type. ' +
'Use this to discover valid elementTypeId values for creation tools.',
inputSchema: {
diagramType: z.string().describe('Diagram type whose elements should be discovered')
}
},
async params => createResourceToolResult(await this.handle(params))
);
}

override async handle({ diagramType }: { diagramType?: string }): Promise<ResourceHandlerResult> {
this.logger.info(`'element-types' invoked for diagram type '${diagramType}'`);

// In this specifc GLSP implementation, only 'workflow-diagram' is valid
if (diagramType !== 'workflow-diagram') {
return {
content: {
uri: `glsp://types/${diagramType}/elements`,
mimeType: 'text/plain',
text: 'Invalid diagram type.'
},
isError: true
};
}

return {
content: {
uri: `glsp://types/${diagramType}/elements`,
mimeType: 'text/markdown',
text: WORKFLOW_ELEMENT_TYPES_STRING
},
isError: false
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/********************************************************************************
* Copyright (c) 2026 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { GModelElement } from '@eclipse-glsp/graph';
import { DefaultTypes } from '@eclipse-glsp/server';
import { DefaultMcpModelSerializer } from '@eclipse-glsp/server-mcp';
import { injectable } from 'inversify';
import { ModelTypes } from '../util/model-types';

/**
* As compared to the {@link DefaultMcpModelSerializer}, this is a specific implementation and we
* know not only the structure of our graph but also each relevant attribute. This enables us to
* order them semantically so the produced serialization makes more sense if read with semantics
* mind. As LLMs (i.e., the MCP clients) work semantically, this is superior to a random ordering.
* Furthermore, including only the relevant information without redundancies decreases context size.
*/
@injectable()
export class WorkflowMcpModelSerializer extends DefaultMcpModelSerializer {
override prepareElement(element: GModelElement): Record<string, Record<string, any>[]> {
const elements = this.flattenStructure(element);

// Define the order of keys
const result: Record<string, Record<string, any>[]> = {
[DefaultTypes.GRAPH]: [],
[ModelTypes.CATEGORY]: [],
[ModelTypes.AUTOMATED_TASK]: [],
[ModelTypes.MANUAL_TASK]: [],
[ModelTypes.FORK_NODE]: [],
[ModelTypes.JOIN_NODE]: [],
[ModelTypes.DECISION_NODE]: [],
[ModelTypes.MERGE_NODE]: [],
[DefaultTypes.EDGE]: [],
[ModelTypes.WEIGHTED_EDGE]: []
};
elements.forEach(element => {
this.combinePositionAndSize(element);

const adjustedElement = this.adjustElement(element);
if (!adjustedElement) {
return;
}

result[element.type].push(adjustedElement);
});

return result;
}

private adjustElement(element: Record<string, any>): Record<string, any> | undefined {
switch (element.type) {
case ModelTypes.AUTOMATED_TASK:
case ModelTypes.MANUAL_TASK: {
const label = element.children.find((child: { type: string }) => child.type === ModelTypes.LABEL_HEADING);

// For tasks, the only content with impact on element size is the label
// Therefore, all other factors get integrated into the label size for the AI to do proper resizing operations
const labelSize = {
// 10px padding right, 31px padding left (incl. icon)
width: Math.trunc(label.size.width + 10 + 31),
// 7px padding top and bottom each
height: Math.trunc(label.size.height + 14)
};

return {
id: element.id,
position: element.position,
size: element.size,
bounds: element.bounds,
label: label.text,
labelSize: labelSize,
parentId: element.parent.type === ModelTypes.STRUCTURE ? element.parent.parent.id : element.parentId
};
}
case ModelTypes.CATEGORY: {
const label = element.children
.find((child: { type: string }) => child.type === ModelTypes.COMP_HEADER)
?.children.find((child: { type: string }) => child.type === ModelTypes.LABEL_HEADING);

const labelSize = {
width: Math.trunc(label.size.width + 20),
height: Math.trunc(label.size.height + 20)
};

const usableSpaceSize = {
width: Math.trunc(Math.max(0, element.size.width - 10)),
height: Math.trunc(Math.max(0, element.size.height - labelSize.height - 10))
};

return {
id: element.id,
isContainer: true,
position: element.position,
size: element.size,
bounds: element.bounds,
label: label.text,
labelSize: labelSize,
usableSpaceSize: usableSpaceSize,
parentId: element.parentId
};
}
case ModelTypes.JOIN_NODE:
case ModelTypes.MERGE_NODE:
case ModelTypes.DECISION_NODE:
case ModelTypes.FORK_NODE: {
return {
id: element.id,
position: element.position,
size: element.size,
bounds: element.bounds,
parentId: element.parentId
};
}
case DefaultTypes.EDGE: {
return {
id: element.id,
sourceId: element.sourceId,
targetId: element.targetId,
parentId: element.parentId
};
}
case ModelTypes.WEIGHTED_EDGE: {
return {
id: element.id,
sourceId: element.sourceId,
targetId: element.targetId,
probability: element.probability,
parentId: element.parentId
};
}
case DefaultTypes.GRAPH: {
return {
id: element.id,
isContainer: true
};
}
default:
return undefined;
}
}
}
Loading