pmx-canvas 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/Readme.md +19 -6
- package/dist/canvas/global.css +35 -2
- package/dist/canvas/index.js +70 -69
- package/dist/json-render/index.js +109 -109
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/icons.d.ts +2 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/json-render/charts/components.d.ts +5 -1
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/mcp/canvas-access.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +4 -0
- package/dist/types/server/canvas-schema.d.ts +19 -3
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +6 -2
- package/dist/types/server/html-primitives.d.ts +34 -0
- package/dist/types/server/index.d.ts +19 -0
- package/docs/cli.md +4 -1
- package/docs/http-api.md +10 -0
- package/docs/mcp.md +6 -4
- package/docs/node-types.md +30 -2
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +11 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +8 -0
- package/src/cli/agent.ts +150 -5
- package/src/client/App.tsx +20 -1
- package/src/client/canvas/AnnotationLayer.tsx +33 -12
- package/src/client/canvas/CanvasViewport.tsx +88 -7
- package/src/client/canvas/CommandPalette.tsx +1 -1
- package/src/client/canvas/ContextMenu.tsx +2 -2
- package/src/client/canvas/ExpandedNodeOverlay.tsx +7 -1
- package/src/client/icons.tsx +13 -0
- package/src/client/nodes/McpAppNode.tsx +12 -4
- package/src/client/state/canvas-store.ts +15 -5
- package/src/client/state/sse-bridge.ts +4 -3
- package/src/client/theme/global.css +35 -2
- package/src/client/types.ts +2 -1
- package/src/json-render/charts/components.tsx +41 -7
- package/src/json-render/charts/extra-components.tsx +13 -12
- package/src/json-render/renderer/index.tsx +1 -0
- package/src/json-render/server.ts +3 -1
- package/src/mcp/canvas-access.ts +23 -0
- package/src/mcp/server.ts +83 -27
- package/src/server/agent-context.ts +17 -0
- package/src/server/canvas-operations.ts +91 -38
- package/src/server/canvas-schema.ts +83 -3
- package/src/server/canvas-serialization.ts +9 -2
- package/src/server/canvas-state.ts +9 -4
- package/src/server/demo-state.json +1143 -0
- package/src/server/demo.ts +25 -777
- package/src/server/html-primitives.ts +990 -0
- package/src/server/index.ts +43 -2
- package/src/server/server.ts +138 -14
- package/src/server/spatial-analysis.ts +3 -3
|
@@ -173,10 +173,18 @@ export function addNode(node: CanvasNodeState): void {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
export function updateNode(id: string, patch: Partial<CanvasNodeState>): void {
|
|
176
|
+
updateNodeWithOptions(id, patch);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updateNodeWithOptions(
|
|
180
|
+
id: string,
|
|
181
|
+
patch: Partial<CanvasNodeState>,
|
|
182
|
+
options: { skipGroupChildTranslation?: boolean } = {},
|
|
183
|
+
): void {
|
|
176
184
|
const existing = nodes.value.get(id);
|
|
177
185
|
if (!existing) return;
|
|
178
186
|
const next = new Map(nodes.value);
|
|
179
|
-
if (existing.type === 'group' && patch.position) {
|
|
187
|
+
if (existing.type === 'group' && patch.position && options.skipGroupChildTranslation !== true) {
|
|
180
188
|
const deltaX = patch.position.x - existing.position.x;
|
|
181
189
|
const deltaY = patch.position.y - existing.position.y;
|
|
182
190
|
if (deltaX !== 0 || deltaY !== 0) {
|
|
@@ -272,9 +280,11 @@ export function removeAnnotation(id: string): void {
|
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
export async function createAnnotationFromClient(input: {
|
|
283
|
+
type?: CanvasAnnotation['type'];
|
|
275
284
|
points: CanvasAnnotation['points'];
|
|
276
285
|
color: string;
|
|
277
286
|
width: number;
|
|
287
|
+
text?: string;
|
|
278
288
|
label?: string;
|
|
279
289
|
}): Promise<{ ok: boolean }> {
|
|
280
290
|
try {
|
|
@@ -748,10 +758,10 @@ export function autoArrange(): void {
|
|
|
748
758
|
updateNode(id, { position });
|
|
749
759
|
}
|
|
750
760
|
for (const [groupId, bounds] of result.groupBounds.entries()) {
|
|
751
|
-
|
|
761
|
+
updateNodeWithOptions(groupId, {
|
|
752
762
|
position: { x: bounds.x, y: bounds.y },
|
|
753
763
|
size: { width: bounds.width, height: bounds.height },
|
|
754
|
-
});
|
|
764
|
+
}, { skipGroupChildTranslation: true });
|
|
755
765
|
}
|
|
756
766
|
});
|
|
757
767
|
persistLayout();
|
|
@@ -766,10 +776,10 @@ export function forceDirectedArrange(): void {
|
|
|
766
776
|
updateNode(id, { position });
|
|
767
777
|
}
|
|
768
778
|
for (const [groupId, bounds] of result.groupBounds.entries()) {
|
|
769
|
-
|
|
779
|
+
updateNodeWithOptions(groupId, {
|
|
770
780
|
position: { x: bounds.x, y: bounds.y },
|
|
771
781
|
size: { width: bounds.width, height: bounds.height },
|
|
772
|
-
});
|
|
782
|
+
}, { skipGroupChildTranslation: true });
|
|
773
783
|
}
|
|
774
784
|
});
|
|
775
785
|
persistLayout();
|
|
@@ -388,21 +388,22 @@ function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
|
|
|
388
388
|
|
|
389
389
|
function parseCanvasAnnotation(raw: Record<string, unknown>): CanvasAnnotation | null {
|
|
390
390
|
if (typeof raw.id !== 'string' || !raw.id) return null;
|
|
391
|
-
if (raw.type !== 'freehand') return null;
|
|
391
|
+
if (raw.type !== 'freehand' && raw.type !== 'text') return null;
|
|
392
392
|
if (!Array.isArray(raw.points)) return null;
|
|
393
393
|
const points = raw.points
|
|
394
394
|
.map((point) => parseCanvasPosition(point))
|
|
395
395
|
.filter((point): point is { x: number; y: number } => point !== null);
|
|
396
396
|
const bounds = parseCanvasRect(raw.bounds);
|
|
397
|
-
if (points.length < 2 || !bounds) return null;
|
|
397
|
+
if (points.length < (raw.type === 'text' ? 1 : 2) || !bounds) return null;
|
|
398
398
|
|
|
399
399
|
return {
|
|
400
400
|
id: raw.id,
|
|
401
|
-
type:
|
|
401
|
+
type: raw.type,
|
|
402
402
|
points,
|
|
403
403
|
bounds,
|
|
404
404
|
color: typeof raw.color === 'string' ? raw.color : '#f97316',
|
|
405
405
|
width: typeof raw.width === 'number' ? raw.width : 4,
|
|
406
|
+
...(typeof raw.text === 'string' ? { text: raw.text } : {}),
|
|
406
407
|
...(typeof raw.label === 'string' ? { label: raw.label } : {}),
|
|
407
408
|
createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : '',
|
|
408
409
|
};
|
|
@@ -392,6 +392,19 @@ body,
|
|
|
392
392
|
margin: 0.4em 0;
|
|
393
393
|
color: var(--c-muted);
|
|
394
394
|
}
|
|
395
|
+
|
|
396
|
+
.canvas-node .node-body ul,
|
|
397
|
+
.canvas-node .node-body ol {
|
|
398
|
+
margin: 0.4em 0;
|
|
399
|
+
padding-left: 0.25em;
|
|
400
|
+
list-style-position: inside;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.canvas-node .node-body li {
|
|
404
|
+
margin: 0.2em 0;
|
|
405
|
+
padding-left: 0.15em;
|
|
406
|
+
}
|
|
407
|
+
|
|
395
408
|
.canvas-node .node-body a {
|
|
396
409
|
color: var(--c-accent);
|
|
397
410
|
text-decoration: none;
|
|
@@ -1481,14 +1494,14 @@ body,
|
|
|
1481
1494
|
height: 1px;
|
|
1482
1495
|
overflow: visible;
|
|
1483
1496
|
pointer-events: none;
|
|
1484
|
-
z-index:
|
|
1497
|
+
z-index: 9000;
|
|
1485
1498
|
}
|
|
1486
1499
|
|
|
1487
1500
|
.annotation-capture-layer {
|
|
1488
1501
|
position: absolute;
|
|
1489
1502
|
inset: 0;
|
|
1490
1503
|
z-index: 9996;
|
|
1491
|
-
pointer-events:
|
|
1504
|
+
pointer-events: auto;
|
|
1492
1505
|
background: color-mix(in srgb, var(--c-accent) 5%, transparent);
|
|
1493
1506
|
}
|
|
1494
1507
|
|
|
@@ -1496,6 +1509,26 @@ body,
|
|
|
1496
1509
|
background: color-mix(in srgb, var(--c-danger) 6%, transparent);
|
|
1497
1510
|
}
|
|
1498
1511
|
|
|
1512
|
+
.annotation-capture-layer.text {
|
|
1513
|
+
background: color-mix(in srgb, var(--c-annotation) 4%, transparent);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
.annotation-text-input {
|
|
1517
|
+
position: absolute;
|
|
1518
|
+
z-index: 9997;
|
|
1519
|
+
min-width: 120px;
|
|
1520
|
+
max-width: min(560px, calc(100% - 24px));
|
|
1521
|
+
padding: 2px 4px;
|
|
1522
|
+
border: 1px solid var(--c-annotation);
|
|
1523
|
+
border-radius: 4px;
|
|
1524
|
+
background: color-mix(in srgb, var(--c-bg) 72%, transparent);
|
|
1525
|
+
color: var(--c-annotation);
|
|
1526
|
+
font-family: var(--font);
|
|
1527
|
+
font-weight: 700;
|
|
1528
|
+
line-height: 1.15;
|
|
1529
|
+
outline: none;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1499
1532
|
/* ── Drop Zone (file drag-and-drop) ─────────────────────────── */
|
|
1500
1533
|
.drop-zone-overlay {
|
|
1501
1534
|
position: absolute;
|
package/src/client/types.ts
CHANGED
|
@@ -48,11 +48,12 @@ export interface CanvasAnnotationPoint {
|
|
|
48
48
|
|
|
49
49
|
export interface CanvasAnnotation {
|
|
50
50
|
id: string;
|
|
51
|
-
type: 'freehand';
|
|
51
|
+
type: 'freehand' | 'text';
|
|
52
52
|
points: CanvasAnnotationPoint[];
|
|
53
53
|
bounds: { x: number; y: number; width: number; height: number };
|
|
54
54
|
color: string;
|
|
55
55
|
width: number;
|
|
56
|
+
text?: string;
|
|
56
57
|
label?: string;
|
|
57
58
|
createdAt: string;
|
|
58
59
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* a responsive chart inside a styled container.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
12
12
|
import type { BaseComponentProps } from '@json-render/react';
|
|
13
13
|
import {
|
|
14
14
|
BarChart as RechartsBarChart,
|
|
@@ -106,6 +106,40 @@ export const polarChartMargin = { top: 18, right: 40, bottom: 30, left: 40 };
|
|
|
106
106
|
export const axisTickMargin = 8;
|
|
107
107
|
export const legendMargin = { top: 10 };
|
|
108
108
|
|
|
109
|
+
export function useChartFrameHeight(explicitHeight: number | null | undefined, fallbackHeight = 300) {
|
|
110
|
+
const frameRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
const [autoHeight, setAutoHeight] = useState(fallbackHeight);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const frame = frameRef.current;
|
|
115
|
+
if (!frame) return;
|
|
116
|
+
|
|
117
|
+
const updateHeight = () => {
|
|
118
|
+
const rect = frame.getBoundingClientRect();
|
|
119
|
+
const doc = document.documentElement;
|
|
120
|
+
const currentHeight = frame.getBoundingClientRect().height;
|
|
121
|
+
const overflow = Math.max(0, doc.scrollHeight - doc.clientHeight);
|
|
122
|
+
const available = overflow > 0 ? currentHeight - overflow : window.innerHeight - rect.top - 24;
|
|
123
|
+
setAutoHeight(Math.max(220, Math.round(available)));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
updateHeight();
|
|
127
|
+
const observer = new ResizeObserver(updateHeight);
|
|
128
|
+
observer.observe(document.documentElement);
|
|
129
|
+
observer.observe(frame);
|
|
130
|
+
window.addEventListener('resize', updateHeight);
|
|
131
|
+
return () => {
|
|
132
|
+
observer.disconnect();
|
|
133
|
+
window.removeEventListener('resize', updateHeight);
|
|
134
|
+
};
|
|
135
|
+
}, [explicitHeight]);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
frameRef,
|
|
139
|
+
height: typeof explicitHeight === 'number' ? Math.min(explicitHeight, autoHeight) : autoHeight,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
109
143
|
/** Shared wrapper for cartesian charts (Line + Bar). */
|
|
110
144
|
export function CartesianChart({
|
|
111
145
|
props,
|
|
@@ -117,12 +151,12 @@ export function CartesianChart({
|
|
|
117
151
|
className?: string;
|
|
118
152
|
}) {
|
|
119
153
|
const chartData = processChartData(props.data ?? [], props.xKey, props.yKey, props.aggregate);
|
|
120
|
-
const
|
|
154
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
121
155
|
|
|
122
156
|
return (
|
|
123
|
-
<div className={`pmx-chart${className ? ` ${className}` : ''}`}>
|
|
157
|
+
<div ref={frameRef} className={`pmx-chart${className ? ` ${className}` : ''}`}>
|
|
124
158
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
125
|
-
<ResponsiveContainer width="100%" height={
|
|
159
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
126
160
|
{children(chartData)}
|
|
127
161
|
</ResponsiveContainer>
|
|
128
162
|
</div>
|
|
@@ -172,12 +206,12 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
|
172
206
|
|
|
173
207
|
function ChartPieChart({ props }: BaseComponentProps<PieChartProps>) {
|
|
174
208
|
const data = props.data ?? [];
|
|
175
|
-
const
|
|
209
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
176
210
|
|
|
177
211
|
return (
|
|
178
|
-
<div className="pmx-chart pmx-chart--pie">
|
|
212
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--pie">
|
|
179
213
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
180
|
-
<ResponsiveContainer width="100%" height={
|
|
214
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
181
215
|
<RechartsPieChart margin={polarChartMargin}>
|
|
182
216
|
<Tooltip contentStyle={tooltipStyle} />
|
|
183
217
|
{props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
legendMargin,
|
|
41
41
|
polarChartMargin,
|
|
42
42
|
tooltipStyle,
|
|
43
|
+
useChartFrameHeight,
|
|
43
44
|
type CartesianChartProps,
|
|
44
45
|
} from './components';
|
|
45
46
|
|
|
@@ -89,12 +90,12 @@ interface ScatterChartProps {
|
|
|
89
90
|
function ChartScatterChart({ props }: BaseComponentProps<ScatterChartProps>) {
|
|
90
91
|
const fill = props.color ?? CHART_COLORS[0];
|
|
91
92
|
const data = props.data ?? [];
|
|
92
|
-
const
|
|
93
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
93
94
|
|
|
94
95
|
return (
|
|
95
|
-
<div className="pmx-chart pmx-chart--scatter">
|
|
96
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--scatter">
|
|
96
97
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
97
|
-
<ResponsiveContainer width="100%" height={
|
|
98
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
98
99
|
<RechartsScatterChart margin={chartMargin}>
|
|
99
100
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
100
101
|
<XAxis type="number" dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} name={props.xKey} />
|
|
@@ -120,12 +121,12 @@ interface RadarChartProps {
|
|
|
120
121
|
function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
|
|
121
122
|
const data = props.data ?? [];
|
|
122
123
|
const metrics = (props.metrics ?? []).filter((m) => typeof m === 'string' && m.length > 0);
|
|
123
|
-
const
|
|
124
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 320);
|
|
124
125
|
|
|
125
126
|
return (
|
|
126
|
-
<div className="pmx-chart pmx-chart--radar">
|
|
127
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--radar">
|
|
127
128
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
128
|
-
<ResponsiveContainer width="100%" height={
|
|
129
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
129
130
|
<RechartsRadarChart data={data} outerRadius="66%" margin={polarChartMargin}>
|
|
130
131
|
<PolarGrid stroke="var(--border, #e5e5e5)" />
|
|
131
132
|
<PolarAngleAxis dataKey={props.axisKey} tick={axisStyle} />
|
|
@@ -166,12 +167,12 @@ function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps
|
|
|
166
167
|
const chartData = props.aggregate
|
|
167
168
|
? mergeAggregated(props.data ?? [], props.xKey, series, props.aggregate)
|
|
168
169
|
: props.data ?? [];
|
|
169
|
-
const
|
|
170
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
170
171
|
|
|
171
172
|
return (
|
|
172
|
-
<div className="pmx-chart pmx-chart--stacked-bar">
|
|
173
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--stacked-bar">
|
|
173
174
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
174
|
-
<ResponsiveContainer width="100%" height={
|
|
175
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
175
176
|
<RechartsBarChart data={chartData} margin={chartMargin}>
|
|
176
177
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
177
178
|
<XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
|
|
@@ -238,12 +239,12 @@ function ChartComposedChart({ props }: BaseComponentProps<ComposedChartProps>) {
|
|
|
238
239
|
const data = props.data ?? [];
|
|
239
240
|
const barFill = props.barColor ?? CHART_COLORS[0];
|
|
240
241
|
const lineStroke = props.lineColor ?? CHART_COLORS[3];
|
|
241
|
-
const
|
|
242
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
242
243
|
|
|
243
244
|
return (
|
|
244
|
-
<div className="pmx-chart pmx-chart--composed">
|
|
245
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--composed">
|
|
245
246
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
246
|
-
<ResponsiveContainer width="100%" height={
|
|
247
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
247
248
|
<RechartsComposedChart data={data} margin={chartMargin}>
|
|
248
249
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
249
250
|
<XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
|
|
@@ -511,7 +511,7 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
|
511
511
|
|
|
512
512
|
const chartProps: Record<string, unknown> = {
|
|
513
513
|
data: input.data,
|
|
514
|
-
height: input.height
|
|
514
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
515
515
|
};
|
|
516
516
|
|
|
517
517
|
switch (chartType) {
|
|
@@ -648,6 +648,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
648
648
|
title: string;
|
|
649
649
|
spec: JsonRenderSpec;
|
|
650
650
|
theme?: 'dark' | 'light' | 'high-contrast';
|
|
651
|
+
display?: 'expanded';
|
|
651
652
|
}): Promise<string> {
|
|
652
653
|
try {
|
|
653
654
|
await ensureJsonRenderBundle();
|
|
@@ -661,6 +662,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
661
662
|
const boot = [
|
|
662
663
|
`window.__PMX_CANVAS_JSON_RENDER_SPEC__ = ${JSON.stringify(options.spec)};`,
|
|
663
664
|
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
|
665
|
+
...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
|
|
664
666
|
jsBundle,
|
|
665
667
|
].join('\n');
|
|
666
668
|
return buildAppHtml({
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -19,6 +19,8 @@ type AddDiagramInput = Parameters<PmxCanvas['addDiagram']>[0];
|
|
|
19
19
|
type AddJsonRenderNodeInput = Parameters<PmxCanvas['addJsonRenderNode']>[0];
|
|
20
20
|
type AddJsonRenderNodeResult = ReturnType<PmxCanvas['addJsonRenderNode']>;
|
|
21
21
|
type AddHtmlNodeInput = Parameters<PmxCanvas['addHtmlNode']>[0];
|
|
22
|
+
type AddHtmlPrimitiveInput = Parameters<PmxCanvas['addHtmlPrimitive']>[0];
|
|
23
|
+
type AddHtmlPrimitiveResult = ReturnType<PmxCanvas['addHtmlPrimitive']>;
|
|
22
24
|
type AddGraphNodeInput = Parameters<PmxCanvas['addGraphNode']>[0];
|
|
23
25
|
type AddGraphNodeResult = ReturnType<PmxCanvas['addGraphNode']>;
|
|
24
26
|
type UpdateNodePatch = Parameters<PmxCanvas['updateNode']>[1];
|
|
@@ -102,6 +104,7 @@ export interface CanvasAccess {
|
|
|
102
104
|
addDiagram(input: AddDiagramInput): Promise<OpenMcpAppResult>;
|
|
103
105
|
addJsonRenderNode(input: AddJsonRenderNodeInput): Promise<AddJsonRenderNodeResult>;
|
|
104
106
|
addHtmlNode(input: AddHtmlNodeInput): Promise<string>;
|
|
107
|
+
addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult>;
|
|
105
108
|
addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult>;
|
|
106
109
|
buildWebArtifact(input: WebArtifactInput): Promise<WebArtifactResult>;
|
|
107
110
|
updateNode(id: string, patch: UpdateNodePatch): Promise<void>;
|
|
@@ -188,6 +191,10 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
188
191
|
return this.canvas.addHtmlNode(input);
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
|
|
195
|
+
return this.canvas.addHtmlPrimitive(input);
|
|
196
|
+
}
|
|
197
|
+
|
|
191
198
|
async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
|
|
192
199
|
return this.canvas.addGraphNode(input);
|
|
193
200
|
}
|
|
@@ -438,6 +445,22 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
438
445
|
return await this.requestNodeId('POST', '/api/canvas/node', { type: 'html', ...input });
|
|
439
446
|
}
|
|
440
447
|
|
|
448
|
+
async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
|
|
449
|
+
const response = await this.requestJson<{
|
|
450
|
+
id?: string;
|
|
451
|
+
node?: { id?: string };
|
|
452
|
+
primitive?: { kind?: string; title?: string; htmlBytes?: number };
|
|
453
|
+
}>('POST', '/api/canvas/node', { type: 'html', ...input, primitive: input.kind });
|
|
454
|
+
const id = typeof response.id === 'string' ? response.id : response.node?.id;
|
|
455
|
+
if (!id) throw new Error('html primitive response did not include a node id.');
|
|
456
|
+
return {
|
|
457
|
+
id,
|
|
458
|
+
kind: input.kind,
|
|
459
|
+
title: response.primitive?.title ?? input.title ?? input.kind,
|
|
460
|
+
htmlBytes: response.primitive?.htmlBytes ?? 0,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
441
464
|
async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
|
|
442
465
|
const response = await this.requestJson<GraphNodeResponse>('POST', '/api/canvas/graph', {
|
|
443
466
|
...input,
|
package/src/mcp/server.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
25
25
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
26
26
|
import { z } from 'zod';
|
|
27
27
|
import { canvasState, describeCanvasSchema, validateStructuredCanvasPayload } from '../server/index.js';
|
|
28
|
+
import { isHtmlPrimitiveKind } from '../server/html-primitives.js';
|
|
29
|
+
import type { HtmlPrimitiveKind } from '../server/html-primitives.js';
|
|
28
30
|
import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './canvas-access.js';
|
|
29
31
|
import { serializeNodeForAgentContext } from '../server/agent-context.js';
|
|
30
32
|
import { wrapCanvasAutomationScript } from '../server/server.js';
|
|
@@ -56,6 +58,8 @@ const jsonRenderSpecSchema = z.union([
|
|
|
56
58
|
}).passthrough(),
|
|
57
59
|
]);
|
|
58
60
|
|
|
61
|
+
const htmlPrimitiveKindSchema = z.string().refine(isHtmlPrimitiveKind, 'Unknown HTML primitive kind');
|
|
62
|
+
|
|
59
63
|
function structuredSchemaDescription(): string {
|
|
60
64
|
const routing = describeCanvasSchema().mcp.nodeTypeRouting;
|
|
61
65
|
return Object.entries(routing)
|
|
@@ -428,6 +432,46 @@ export async function startMcpServer(): Promise<void> {
|
|
|
428
432
|
},
|
|
429
433
|
);
|
|
430
434
|
|
|
435
|
+
server.tool(
|
|
436
|
+
'canvas_add_html_primitive',
|
|
437
|
+
'Create a reusable HTML communication primitive as a normal sandboxed html node. Use this instead of long markdown for side-by-side choices, implementation plans, PR review sheets, module maps, design sheets, component galleries, flowcharts, slide decks, explainers, status reports, and throwaway editors with export/copy paths.',
|
|
438
|
+
{
|
|
439
|
+
kind: htmlPrimitiveKindSchema.describe('Primitive kind. Call canvas_describe_schema and read htmlPrimitives for data shapes and examples.'),
|
|
440
|
+
title: z.string().optional().describe('Node title shown in the canvas titlebar.'),
|
|
441
|
+
data: z.record(z.string(), z.unknown()).optional().describe('Primitive-specific data payload. See canvas_describe_schema.htmlPrimitives for each shape.'),
|
|
442
|
+
x: z.number().optional().describe('X position (auto-placed if omitted).'),
|
|
443
|
+
y: z.number().optional().describe('Y position (auto-placed if omitted).'),
|
|
444
|
+
width: z.number().optional().describe('Width in pixels (defaults per primitive).'),
|
|
445
|
+
height: z.number().optional().describe('Height in pixels (defaults per primitive).'),
|
|
446
|
+
strictSize: z.boolean().optional().describe('Keep explicit width/height fixed; iframe scrolls overflow internally.'),
|
|
447
|
+
full: z.boolean().optional().describe('Return the full created node payload. Default false returns compact metadata.'),
|
|
448
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
449
|
+
},
|
|
450
|
+
async (input) => {
|
|
451
|
+
const c = await ensureCanvas();
|
|
452
|
+
const kind = input.kind as HtmlPrimitiveKind;
|
|
453
|
+
const result = await c.addHtmlPrimitive({
|
|
454
|
+
kind,
|
|
455
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
456
|
+
...(input.data ? { data: input.data } : {}),
|
|
457
|
+
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
458
|
+
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
459
|
+
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
460
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
461
|
+
...(input.strictSize === true ? { strictSize: true } : {}),
|
|
462
|
+
});
|
|
463
|
+
return {
|
|
464
|
+
content: [{
|
|
465
|
+
type: 'text',
|
|
466
|
+
text: JSON.stringify({
|
|
467
|
+
...(await createdNodePayload(c, result.id, input)),
|
|
468
|
+
primitive: { kind: result.kind, title: result.title, htmlBytes: result.htmlBytes },
|
|
469
|
+
}, null, 2),
|
|
470
|
+
}],
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
|
|
431
475
|
server.tool(
|
|
432
476
|
'canvas_open_mcp_app',
|
|
433
477
|
'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node. This is a full external-MCP transport call, not the CLI kind shortcut; use canvas_add_diagram for the built-in Excalidraw preset.',
|
|
@@ -543,10 +587,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
543
587
|
|
|
544
588
|
server.tool(
|
|
545
589
|
'canvas_validate_spec',
|
|
546
|
-
'Validate a json-render spec or
|
|
590
|
+
'Validate a json-render spec, graph payload, or HTML primitive payload without creating a node. Returns normalized metadata the server would accept.',
|
|
547
591
|
{
|
|
548
|
-
type: z.enum(['json-render', 'graph']).describe('Structured payload type to validate'),
|
|
592
|
+
type: z.enum(['json-render', 'graph', 'html-primitive']).describe('Structured payload type to validate'),
|
|
549
593
|
spec: jsonRenderSpecSchema.optional().describe('json-render spec to validate when type="json-render"'),
|
|
594
|
+
kind: htmlPrimitiveKindSchema.optional().describe('HTML primitive kind when type="html-primitive"'),
|
|
595
|
+
primitive: htmlPrimitiveKindSchema.optional().describe('Alias for kind when type="html-primitive"'),
|
|
596
|
+
primitiveData: z.record(z.string(), z.unknown()).optional().describe('HTML primitive data payload when type="html-primitive"'),
|
|
550
597
|
title: z.string().optional().describe('Optional graph title'),
|
|
551
598
|
graphType: z.string().optional().describe('Graph type when type="graph"'),
|
|
552
599
|
data: z.array(z.record(z.string(), z.unknown())).optional().describe('Graph dataset when type="graph"'),
|
|
@@ -573,29 +620,38 @@ export async function startMcpServer(): Promise<void> {
|
|
|
573
620
|
type: 'json-render',
|
|
574
621
|
spec: input.spec,
|
|
575
622
|
})
|
|
576
|
-
:
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
623
|
+
: input.type === 'html-primitive'
|
|
624
|
+
? validateStructuredCanvasPayload({
|
|
625
|
+
type: 'html-primitive',
|
|
626
|
+
primitive: {
|
|
627
|
+
kind: (input.kind ?? input.primitive ?? '') as HtmlPrimitiveKind | '',
|
|
628
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
629
|
+
...(input.primitiveData ? { data: input.primitiveData } : {}),
|
|
630
|
+
},
|
|
631
|
+
})
|
|
632
|
+
: validateStructuredCanvasPayload({
|
|
633
|
+
type: 'graph',
|
|
634
|
+
graph: {
|
|
635
|
+
title: input.title,
|
|
636
|
+
graphType: input.graphType ?? 'line',
|
|
637
|
+
data: input.data ?? [],
|
|
638
|
+
...(typeof input.xKey === 'string' ? { xKey: input.xKey } : {}),
|
|
639
|
+
...(typeof input.yKey === 'string' ? { yKey: input.yKey } : {}),
|
|
640
|
+
...(typeof input.zKey === 'string' ? { zKey: input.zKey } : {}),
|
|
641
|
+
...(typeof input.nameKey === 'string' ? { nameKey: input.nameKey } : {}),
|
|
642
|
+
...(typeof input.valueKey === 'string' ? { valueKey: input.valueKey } : {}),
|
|
643
|
+
...(typeof input.axisKey === 'string' ? { axisKey: input.axisKey } : {}),
|
|
644
|
+
...(Array.isArray(input.metrics) ? { metrics: input.metrics } : {}),
|
|
645
|
+
...(Array.isArray(input.series) ? { series: input.series } : {}),
|
|
646
|
+
...(typeof input.barKey === 'string' ? { barKey: input.barKey } : {}),
|
|
647
|
+
...(typeof input.lineKey === 'string' ? { lineKey: input.lineKey } : {}),
|
|
648
|
+
...(typeof input.aggregate === 'string' ? { aggregate: input.aggregate } : {}),
|
|
649
|
+
...(typeof input.color === 'string' ? { color: input.color } : {}),
|
|
650
|
+
...(typeof input.barColor === 'string' ? { barColor: input.barColor } : {}),
|
|
651
|
+
...(typeof input.lineColor === 'string' ? { lineColor: input.lineColor } : {}),
|
|
652
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
653
|
+
},
|
|
654
|
+
});
|
|
599
655
|
|
|
600
656
|
return {
|
|
601
657
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -736,7 +792,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
736
792
|
content: [{
|
|
737
793
|
type: 'text',
|
|
738
794
|
text: JSON.stringify({
|
|
739
|
-
...await createdNodePayload(c, result.id),
|
|
795
|
+
...(await createdNodePayload(c, result.id)),
|
|
740
796
|
url: result.url,
|
|
741
797
|
spec: result.spec,
|
|
742
798
|
}, null, 2),
|
|
@@ -816,7 +872,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
816
872
|
content: [{
|
|
817
873
|
type: 'text',
|
|
818
874
|
text: JSON.stringify({
|
|
819
|
-
...await createdNodePayload(c, result.id),
|
|
875
|
+
...(await createdNodePayload(c, result.id)),
|
|
820
876
|
url: result.url,
|
|
821
877
|
spec: result.spec,
|
|
822
878
|
}, null, 2),
|
|
@@ -155,6 +155,17 @@ function summarizeWebArtifactData(data: Record<string, unknown>, maxLength: numb
|
|
|
155
155
|
return parts.length > 0 ? truncateContextText(parts.join('\n'), maxLength) : 'Web artifact node';
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
function summarizeHtmlPrimitiveData(data: Record<string, unknown>, maxLength: number): string {
|
|
159
|
+
const parts: string[] = [];
|
|
160
|
+
const primitive = typeof data.htmlPrimitive === 'string' ? data.htmlPrimitive : '';
|
|
161
|
+
const description = typeof data.description === 'string' ? data.description : '';
|
|
162
|
+
const primitiveData = data.primitiveData;
|
|
163
|
+
if (primitive) parts.push(`HTML primitive: ${primitive}`);
|
|
164
|
+
if (description) parts.push(description);
|
|
165
|
+
if (primitiveData !== undefined) parts.push(`Data: ${stringifyContextValue(primitiveData, maxLength)}`);
|
|
166
|
+
return truncateContextText(parts.join('\n'), maxLength);
|
|
167
|
+
}
|
|
168
|
+
|
|
158
169
|
function metadataForNode(node: CanvasNodeState): Record<string, unknown> | undefined {
|
|
159
170
|
switch (node.type) {
|
|
160
171
|
case 'webpage': {
|
|
@@ -233,6 +244,12 @@ export function summarizeNodeForAgentContext(
|
|
|
233
244
|
if (graphCfg) return truncateContextText(`Graph: ${JSON.stringify(graphCfg)}`, defaultTextLength);
|
|
234
245
|
return stringifyContextValue(node.data.spec ?? {}, defaultTextLength);
|
|
235
246
|
}
|
|
247
|
+
case 'html': {
|
|
248
|
+
if (typeof node.data.htmlPrimitive === 'string') {
|
|
249
|
+
return summarizeHtmlPrimitiveData(node.data, defaultTextLength);
|
|
250
|
+
}
|
|
251
|
+
return stringifyContextValue({ title: node.data.title, description: node.data.description }, defaultTextLength);
|
|
252
|
+
}
|
|
236
253
|
case 'prompt':
|
|
237
254
|
case 'response': {
|
|
238
255
|
const text = (node.data.text as string) || (node.data.content as string) || '';
|