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
300 changes: 198 additions & 102 deletions examples/workflow-standalone/app/example1.wf

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/client/src/default-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { toolFocusLossModule } from './features/tools/tool-focus-loss-module';
import { markerNavigatorModule, validationModule } from './features/validation/validation-modules';
import { viewportModule } from './features/viewport/viewport-modules';
import { zorderModule } from './features/zorder/zorder-module';
import { mcpModule } from './mcp/mcp-module';

export const DEFAULT_MODULES = [
defaultModule,
Expand Down Expand Up @@ -104,7 +105,8 @@ export const DEFAULT_MODULES = [
statusModule,
resizeModule,
searchPaletteModule,
searchPaletteDefaultSuggestionsModule
searchPaletteDefaultSuggestionsModule,
mcpModule
] as const;

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ export * from './features/viewport/viewport-tool';
export * from './features/viewport/zoom-viewport-action';
export * from './features/zorder/bring-to-front-command';
export * from './features/zorder/zorder-module';
export * from './mcp/mcp-export-png';
export * from './mcp/mcp-get-selection';
export * from './mcp/mcp-module';
export * from './model';
export * from './re-exports';
export * from './standalone-modules';
Expand Down
182 changes: 182 additions & 0 deletions packages/client/src/mcp/mcp-export-png.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/********************************************************************************
* 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 {
Action,
CommandExecutionContext,
CommandResult,
ExportPngMcpAction,
ExportPngMcpActionResult,
GModelElement,
GModelRoot,
HiddenCommand,
isExportable,
isHoverable,
isSelectable,
isViewport,
IVNodePostprocessor,
TYPES
} from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { VNode } from 'snabbdom';
import { GLSPSvgExporter } from '../features/export/glsp-svg-exporter';

/**
* This class extends {@link GLSPSvgExporter} in order to make use of the SVG creation logic.
* It then uses the SVG string to generate an equivalent PNG instead.
*
* This class should not be used for standard SVG generation, but only for generating PNG for MCP purposes.
*/
@injectable()
export class GLSPMcpPngExporter extends GLSPSvgExporter {
async exportPng(root: GModelRoot, request?: ExportPngMcpAction): Promise<void> {
if (!request) {
return;
}

return new Promise((resolve, reject) => {
if (typeof document === 'undefined') {
reject(new Error('Failed to find document.'));
return;
}

let svgElement = this.findSvgElement();
if (!svgElement) {
reject(new Error('Failed to find SVG element.'));
return;
}

svgElement = this.prepareSvgElement(svgElement, root);
const serializedSvg = this.createSvg(svgElement, root, request?.options ?? {}, request);
const svgExport = this.getSvgExport(serializedSvg, svgElement, root);

const svgBlob = new Blob([svgExport], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);

const img = new Image();

img.onload = async () => {
try {
const bitmap = await createImageBitmap(img);

const aspect = bitmap.width / bitmap.height;
const width = request?.options?.width ?? 1024;
const height = width / aspect;
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext('2d');

if (!ctx) {
reject(new Error('Failed to get offscreen context.'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make sure to call URL.revokeObjectURL(url) here as well.

return;
}

ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(bitmap, 0, 0, width, height);

const outBlob = await offscreen.convertToBlob({ type: 'image/png' });
const reader = new FileReader();
reader.onloadend = () => {
URL.revokeObjectURL(url);
const result = reader.result as string;
this.actionDispatcher.dispatch(
ExportPngMcpActionResult.create(
result.replace('data:image/png;base64,', ''),
request.mcpRequestId,
request.options
)
);
resolve();
};
reader.readAsDataURL(outBlob);
} catch (err) {
reject(err);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make sure to call URL.revokeObjectURL(url) here as well.

}
};

img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load SVG into image element.'));
};

img.src = url;
});
}
}

/**
* See sprotty's `ExportSvgCommand`
*/
export class ExportPngMcpCommand extends HiddenCommand {
static readonly KIND = ExportPngMcpAction.KIND;

constructor(@inject(TYPES.Action) protected action: ExportPngMcpAction) {
super();
}

execute(context: CommandExecutionContext): CommandResult {
if (isExportable(context.root)) {
const root = context.modelFactory.createRoot(context.root);
if (isExportable(root)) {
if (isViewport(root)) {
root.zoom = 1;
root.scroll = { x: 0, y: 0 };
}
root.index.all().forEach(element => {
if (isSelectable(element) && element.selected) {
element.selected = false;
}
if (isHoverable(element) && element.hoverFeedback) {
element.hoverFeedback = false;
}
});
return {
model: root,
modelChanged: true,
cause: this.action
};
}
}
return {
model: context.root,
modelChanged: false
};
}
}

/**
* See sprotty's `ExportSvgPostprocessor`
*/
@injectable()
export class ExportPngMcpPostprocessor implements IVNodePostprocessor {
protected root: GModelRoot;

@inject(GLSPMcpPngExporter)
protected pngExporter: GLSPMcpPngExporter;

decorate(vnode: VNode, element: GModelElement): VNode {
if (element instanceof GModelRoot) {
this.root = element;
}
return vnode;
}

postUpdate(cause?: Action): void {
if (this.root && cause !== undefined && cause.kind === ExportPngMcpAction.KIND) {
this.pngExporter.exportPng(this.root, cause as ExportPngMcpAction);
}
}
}
58 changes: 58 additions & 0 deletions packages/client/src/mcp/mcp-get-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/********************************************************************************
* 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 {
Command,
CommandExecutionContext,
CommandReturn,
GetSelectionMcpAction,
GetSelectionMcpResultAction,
IActionDispatcher,
isSelectable,
toArray,
TYPES
} from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';

@injectable()
export class GetSelectionMcpCommand extends Command {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might need to be a SystemCommand so that we do not pollute the undo/redo stack.

static readonly KIND = GetSelectionMcpAction.KIND;

@inject(TYPES.IActionDispatcher)
protected actionDispatcher: IActionDispatcher;

constructor(@inject(TYPES.Action) protected readonly action: GetSelectionMcpAction) {
super();
}

execute(context: CommandExecutionContext): CommandReturn {
const selection = context.root.index
.all()
.filter(e => isSelectable(e) && e.selected)
.map(e => e.id);
const result = GetSelectionMcpResultAction.create(toArray(selection), this.action.mcpRequestId);
this.actionDispatcher.dispatch(result);
return { model: context.root, modelChanged: false };
}

undo(context: CommandExecutionContext): CommandReturn {
return { model: context.root, modelChanged: false };
}

redo(context: CommandExecutionContext): CommandReturn {
return { model: context.root, modelChanged: false };
}
}
31 changes: 31 additions & 0 deletions packages/client/src/mcp/mcp-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/********************************************************************************
* 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 { configureCommand, FeatureModule, TYPES } from '@eclipse-glsp/sprotty';
import { ExportPngMcpCommand, ExportPngMcpPostprocessor, GLSPMcpPngExporter } from './mcp-export-png';
import { GetSelectionMcpCommand } from './mcp-get-selection';

export const mcpModule = new FeatureModule(
(bind, _unbind, isBound) => {
configureCommand({ bind, isBound }, GetSelectionMcpCommand);

bind(ExportPngMcpPostprocessor).toSelf().inSingletonScope();
bind(TYPES.HiddenVNodePostprocessor).toService(ExportPngMcpPostprocessor);
configureCommand({ bind, isBound }, ExportPngMcpCommand);
bind(GLSPMcpPngExporter).toSelf().inSingletonScope();
},
{ featureId: Symbol('mcp') }
);
Loading