pmx-canvas 0.6.4 → 0.7.0
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/.github/extensions/pmx-canvas/extension.mjs +67 -3
- package/CHANGELOG.md +73 -0
- package/Readme.md +35 -5
- package/dist/canvas/global.css +498 -28
- package/dist/canvas/index.js +110 -95
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +4 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +3 -3
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +8 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +65 -9
- package/dist/types/server/canvas-operations.d.ts +45 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +146 -3
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +107 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +4 -2
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +157 -0
- package/docs/http-api.md +277 -6
- package/docs/mcp.md +96 -7
- package/docs/node-types.md +106 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +62 -5
- package/skills/pmx-canvas/references/full-reference.md +44 -8
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +14 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +21 -9
- package/src/client/App.tsx +28 -4
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +49 -7
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/icons.tsx +2 -0
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +8 -0
- package/src/client/state/sse-bridge.ts +52 -5
- package/src/client/theme/global.css +498 -28
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +45 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/agent-presence.ts +15 -9
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +612 -105
- package/src/server/canvas-operations.ts +183 -31
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +22 -0
- package/src/server/canvas-state.ts +812 -48
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +670 -9
- package/src/server/document-import.ts +223 -0
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +243 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +29 -5
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +15 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +20 -4
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +99 -21
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +129 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +155 -8
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +11 -3
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/server/workboard.ts +3 -3
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
- package/src/shared/tour.ts +87 -0
package/src/client/types.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface CanvasNodeState {
|
|
|
14
14
|
| 'webpage'
|
|
15
15
|
| 'json-render'
|
|
16
16
|
| 'graph'
|
|
17
|
+
| 'board'
|
|
17
18
|
| 'prompt'
|
|
18
19
|
| 'response'
|
|
19
20
|
| 'status'
|
|
@@ -74,6 +75,7 @@ export const TYPE_LABELS: Record<CanvasNodeState['type'], string> = {
|
|
|
74
75
|
webpage: 'WEB',
|
|
75
76
|
'json-render': 'UI',
|
|
76
77
|
graph: 'GRAPH',
|
|
78
|
+
board: 'BOARD',
|
|
77
79
|
prompt: 'ASK',
|
|
78
80
|
response: 'ANS',
|
|
79
81
|
status: 'STATUS',
|
|
@@ -149,7 +149,7 @@ function bundleDir(): string {
|
|
|
149
149
|
return candidates[candidates.length - 1];
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function escapeInlineScriptSource(source: string): string {
|
|
152
|
+
export function escapeInlineScriptSource(source: string): string {
|
|
153
153
|
return source.replace(/<\/script/gi, '<\\/script');
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -542,6 +542,35 @@ export function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpe
|
|
|
542
542
|
return propsValidation.data as JsonRenderSpec;
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
/** shadcn 0.21 ignores these props unless the renderer supplies a two-way binding. */
|
|
546
|
+
export function jsonRenderFormWarnings(spec: JsonRenderSpec): string[] {
|
|
547
|
+
const bindingOnlyProps: Record<string, string> = {
|
|
548
|
+
Input: 'value',
|
|
549
|
+
Textarea: 'value',
|
|
550
|
+
Select: 'value',
|
|
551
|
+
Radio: 'value',
|
|
552
|
+
Slider: 'value',
|
|
553
|
+
ToggleGroup: 'value',
|
|
554
|
+
ButtonGroup: 'selected',
|
|
555
|
+
Tabs: 'value',
|
|
556
|
+
};
|
|
557
|
+
const warnings: string[] = [];
|
|
558
|
+
for (const [key, rawElement] of Object.entries(spec.elements)) {
|
|
559
|
+
const element = asRecord(rawElement);
|
|
560
|
+
const type = typeof element?.type === 'string' ? element.type : '';
|
|
561
|
+
const prop = bindingOnlyProps[type];
|
|
562
|
+
if (!prop) continue;
|
|
563
|
+
const value = asRecord(element?.props)?.[prop];
|
|
564
|
+
if (value === undefined || value === null) continue;
|
|
565
|
+
const expression = asRecord(value);
|
|
566
|
+
if (hasString(expression?.$bindState) || hasString(expression?.$bindItem)) continue;
|
|
567
|
+
warnings.push(
|
|
568
|
+
`elements.${key}.props.${prop}: ${type} ignores an unbound ${prop} (including literal values and $state expressions). Use { "$bindState": "/form/field" } and seed spec.state.form.field with the initial value; inside a repeat use $bindItem with seeded item data.${type === 'Tabs' ? ' For an uncontrolled initial tab, use defaultValue instead.' : ''}`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
return warnings;
|
|
572
|
+
}
|
|
573
|
+
|
|
545
574
|
export function normalizeGraphType(value: string): GraphChartType {
|
|
546
575
|
const normalized = value.toLowerCase().replace(/[-_\s]/g, '');
|
|
547
576
|
return GRAPH_TYPE_ALIASES[normalized] ?? 'LineChart';
|
|
@@ -927,6 +956,20 @@ export function createJsonRenderNodeData(
|
|
|
927
956
|
};
|
|
928
957
|
}
|
|
929
958
|
|
|
959
|
+
/** The viewer bundle's script and stylesheet, as the viewer document inlines them. */
|
|
960
|
+
export async function readJsonRenderBundle(): Promise<{ js: string; css: string }> {
|
|
961
|
+
await ensureJsonRenderBundle();
|
|
962
|
+
const dir = bundleDir();
|
|
963
|
+
const jsPath = join(dir, 'index.js');
|
|
964
|
+
const cssPath = join(dir, 'index.css');
|
|
965
|
+
return {
|
|
966
|
+
js: existsSync(jsPath)
|
|
967
|
+
? readFileSync(jsPath, 'utf-8')
|
|
968
|
+
: 'document.body.innerHTML = "<pre>json-render bundle missing</pre>";',
|
|
969
|
+
css: existsSync(cssPath) ? readFileSync(cssPath, 'utf-8') : '',
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
930
973
|
export async function buildJsonRenderViewerHtml(options: {
|
|
931
974
|
title: string;
|
|
932
975
|
spec: JsonRenderSpec;
|
|
@@ -946,14 +989,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
946
989
|
const sanitizeAxValue = (v?: string): string =>
|
|
947
990
|
typeof v === 'string' ? v.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80) : '';
|
|
948
991
|
try {
|
|
949
|
-
await
|
|
950
|
-
const dir = bundleDir();
|
|
951
|
-
const jsPath = join(dir, 'index.js');
|
|
952
|
-
const cssPath = join(dir, 'index.css');
|
|
953
|
-
const jsBundle = existsSync(jsPath)
|
|
954
|
-
? readFileSync(jsPath, 'utf-8')
|
|
955
|
-
: 'document.body.innerHTML = "<pre>json-render bundle missing</pre>";';
|
|
956
|
-
const cssBundle = existsSync(cssPath) ? readFileSync(cssPath, 'utf-8') : '';
|
|
992
|
+
const { js: jsBundle, css: cssBundle } = await readJsonRenderBundle();
|
|
957
993
|
const boot = [
|
|
958
994
|
`window.__PMX_CANVAS_JSON_RENDER_SPEC__ = ${JSON.stringify(options.spec)};`,
|
|
959
995
|
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
createCanvas,
|
|
6
|
-
canvasState,
|
|
7
|
-
type CanvasEdge,
|
|
8
|
-
type CanvasLayout,
|
|
9
|
-
type CanvasNodeState,
|
|
10
|
-
type PmxCanvas,
|
|
11
|
-
} from '../server/index.js';
|
|
4
|
+
import { createCanvas, canvasState, type CanvasLayout, type CanvasNodeState, type PmxCanvas } from '../server/index.js';
|
|
12
5
|
import type { PmxAxSource } from '../server/ax-state.js';
|
|
6
|
+
import type { ContextReadInput } from '../server/context-reads.js';
|
|
13
7
|
import { HttpOperationInvoker, LocalOperationInvoker, type OperationInvoker } from '../server/operations/index.js';
|
|
14
8
|
|
|
15
9
|
// openMcpApp / addDiagram / buildWebArtifact / refreshWebpageNode / addHtmlNode /
|
|
@@ -54,8 +48,8 @@ export interface CanvasAccess {
|
|
|
54
48
|
readonly remoteBaseUrl: string | null;
|
|
55
49
|
/** Operation-registry invoker (plan-005): local in-process or HTTP, matching the access mode. */
|
|
56
50
|
invoker(): OperationInvoker;
|
|
57
|
-
getLayout(): Promise<CanvasLayout>;
|
|
58
|
-
getNode(id: string): Promise<CanvasNodeState | undefined>;
|
|
51
|
+
getLayout(board?: string): Promise<CanvasLayout>;
|
|
52
|
+
getNode(id: string, board?: string): Promise<CanvasNodeState | undefined>;
|
|
59
53
|
getAxState(): Promise<AxStateResult>;
|
|
60
54
|
getAxContext(options?: { consumer?: string }): Promise<AxContextResult>;
|
|
61
55
|
getAxTimeline(query?: GetAxTimelineQuery): Promise<GetAxTimelineResult>;
|
|
@@ -72,7 +66,10 @@ export interface CanvasAccess {
|
|
|
72
66
|
ingestActivity(input: IngestActivityInput, options?: { source?: PmxAxSource }): Promise<IngestActivityResult>;
|
|
73
67
|
getPolicy(): Promise<GetPolicyResult>;
|
|
74
68
|
getHistory(): Promise<HistoryResult>;
|
|
75
|
-
getPinnedNodeIds(): Promise<string[]>;
|
|
69
|
+
getPinnedNodeIds(board?: string): Promise<string[]>;
|
|
70
|
+
prepareContextRead(board?: string): Promise<{ boardId: string | null; pinnedNodeIds: string[] }>;
|
|
71
|
+
/** Record a context read the agent made through this MCP server (plan-011). */
|
|
72
|
+
recordContextRead(read: ContextReadInput): Promise<void>;
|
|
76
73
|
runBatch(operations: RunBatchInput): Promise<RunBatchResult>;
|
|
77
74
|
getCodeGraph(): Promise<CodeGraphResult>;
|
|
78
75
|
// canvas_screenshot (still hand-written — binary payload) is the only webview
|
|
@@ -99,11 +96,15 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
99
96
|
return this.operationInvoker;
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
async getLayout(): Promise<CanvasLayout> {
|
|
103
|
-
return
|
|
99
|
+
async getLayout(board?: string): Promise<CanvasLayout> {
|
|
100
|
+
return board
|
|
101
|
+
? ((await this.operationInvoker.invoke('layout.get', { board, includeBlobs: true })) as CanvasLayout)
|
|
102
|
+
: this.canvas.getLayout();
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
async getNode(id: string): Promise<CanvasNodeState | undefined> {
|
|
105
|
+
async getNode(id: string, board?: string): Promise<CanvasNodeState | undefined> {
|
|
106
|
+
if (board)
|
|
107
|
+
return (await this.operationInvoker.invoke('node.get', { id, board, includeBlobs: true })) as CanvasNodeState;
|
|
107
108
|
return this.canvas.getNode(id);
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -162,10 +163,23 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
162
163
|
return this.canvas.getHistory();
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
async getPinnedNodeIds(): Promise<string[]> {
|
|
166
|
+
async getPinnedNodeIds(board?: string): Promise<string[]> {
|
|
167
|
+
if (board) {
|
|
168
|
+
const result = (await this.operationInvoker.invoke('pinned-context.get', { board })) as { nodeIds?: string[] };
|
|
169
|
+
return result.nodeIds ?? [];
|
|
170
|
+
}
|
|
166
171
|
return Array.from(canvasState.contextPinnedNodeIds);
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
async prepareContextRead(board?: string): Promise<{ boardId: string | null; pinnedNodeIds: string[] }> {
|
|
175
|
+
const boardId = board ?? canvasState.activeBoardId;
|
|
176
|
+
return { boardId, pinnedNodeIds: boardId ? await this.getPinnedNodeIds(boardId) : [] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async recordContextRead(read: ContextReadInput): Promise<void> {
|
|
180
|
+
canvasState.recordContextRead(read, read.boardId === undefined ? canvasState.activeBoardId : read.boardId);
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
async runBatch(operations: RunBatchInput): Promise<RunBatchResult> {
|
|
170
184
|
return await this.canvas.runBatch(operations);
|
|
171
185
|
}
|
|
@@ -202,7 +216,12 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
202
216
|
private async requestJson<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
203
217
|
const response = await fetch(`${this.remoteBaseUrl}${path}`, {
|
|
204
218
|
method,
|
|
205
|
-
|
|
219
|
+
// These fetches serve the agent's MCP reads, which this server records
|
|
220
|
+
// itself — the daemon must not record them a second time (plan-011).
|
|
221
|
+
headers: {
|
|
222
|
+
'x-pmx-proxied-read': '1',
|
|
223
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
224
|
+
},
|
|
206
225
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
207
226
|
});
|
|
208
227
|
const text = await response.text();
|
|
@@ -227,12 +246,16 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
227
246
|
return parsed as T;
|
|
228
247
|
}
|
|
229
248
|
|
|
230
|
-
async getLayout(): Promise<CanvasLayout> {
|
|
231
|
-
|
|
249
|
+
async getLayout(board?: string): Promise<CanvasLayout> {
|
|
250
|
+
const params = new URLSearchParams({ includeBlobs: 'true' });
|
|
251
|
+
if (board) params.set('board', board);
|
|
252
|
+
return await this.requestJson<CanvasLayout>('GET', `/api/canvas/state?${params}`);
|
|
232
253
|
}
|
|
233
254
|
|
|
234
|
-
async getNode(id: string): Promise<CanvasNodeState | undefined> {
|
|
235
|
-
const
|
|
255
|
+
async getNode(id: string, board?: string): Promise<CanvasNodeState | undefined> {
|
|
256
|
+
const params = new URLSearchParams({ includeBlobs: 'true' });
|
|
257
|
+
if (board) params.set('board', board);
|
|
258
|
+
const response = await fetch(`${this.remoteBaseUrl}/api/canvas/node/${encodeURIComponent(id)}?${params}`);
|
|
236
259
|
if (response.status === 404) return undefined;
|
|
237
260
|
const text = await response.text();
|
|
238
261
|
let parsed: unknown;
|
|
@@ -346,11 +369,25 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
346
369
|
return response.reviewAnnotations ?? [];
|
|
347
370
|
}
|
|
348
371
|
|
|
349
|
-
async getPinnedNodeIds(): Promise<string[]> {
|
|
350
|
-
const
|
|
372
|
+
async getPinnedNodeIds(board?: string): Promise<string[]> {
|
|
373
|
+
const qs = board ? `?board=${encodeURIComponent(board)}` : '';
|
|
374
|
+
const response = await this.requestJson<{ nodeIds?: string[] }>('GET', `/api/canvas/pinned-context${qs}`);
|
|
351
375
|
return Array.isArray(response.nodeIds) ? response.nodeIds : [];
|
|
352
376
|
}
|
|
353
377
|
|
|
378
|
+
async prepareContextRead(board?: string): Promise<{ boardId: string | null; pinnedNodeIds: string[] }> {
|
|
379
|
+
let boardId = board;
|
|
380
|
+
if (!boardId) {
|
|
381
|
+
const boards = await this.requestJson<{ activeBoardId?: string | null }>('GET', '/api/canvas/boards');
|
|
382
|
+
boardId = boards.activeBoardId ?? undefined;
|
|
383
|
+
}
|
|
384
|
+
return { boardId: boardId ?? null, pinnedNodeIds: boardId ? await this.getPinnedNodeIds(boardId) : [] };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async recordContextRead(read: ContextReadInput): Promise<void> {
|
|
388
|
+
await this.requestJson('POST', '/api/canvas/ax/context-reads', read);
|
|
389
|
+
}
|
|
390
|
+
|
|
354
391
|
async runBatch(operations: RunBatchInput): Promise<RunBatchResult> {
|
|
355
392
|
return await this.requestJson<RunBatchResult>('POST', '/api/canvas/batch', { operations });
|
|
356
393
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { UriTemplate } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
|
|
2
|
+
|
|
3
|
+
/** SDK v1 matches {?a,b} as two required, ordered parameters, unlike its expansion. */
|
|
4
|
+
export class ContextUriTemplate extends UriTemplate {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('canvas://context{?budget,consumer,since}');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
override match(uri: string): Record<string, string> | null {
|
|
10
|
+
if (!URL.canParse(uri)) return null;
|
|
11
|
+
const url = new URL(uri);
|
|
12
|
+
if (
|
|
13
|
+
url.protocol !== 'canvas:' ||
|
|
14
|
+
url.host !== 'context' ||
|
|
15
|
+
url.pathname ||
|
|
16
|
+
url.hash ||
|
|
17
|
+
url.username ||
|
|
18
|
+
url.password
|
|
19
|
+
)
|
|
20
|
+
return null;
|
|
21
|
+
const variables: Record<string, string> = {};
|
|
22
|
+
for (const [key, value] of url.searchParams) {
|
|
23
|
+
if (!['budget', 'consumer', 'since'].includes(key) || key in variables) return null;
|
|
24
|
+
variables[key] = value;
|
|
25
|
+
}
|
|
26
|
+
return variables;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { readFileSync } from 'node:fs';
|
|
24
24
|
import { dirname, join } from 'node:path';
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
26
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
27
27
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
28
28
|
import { z } from 'zod';
|
|
29
29
|
import { AX_SOURCES } from '../server/operations/ops/ax-shared.js';
|
|
@@ -31,6 +31,8 @@ import { canvasState, describeCanvasSchema, stopCanvasServer } from '../server/i
|
|
|
31
31
|
import { AX_INTERACTION_TYPES } from '../server/ax-interaction.js';
|
|
32
32
|
import { buildPendingAxActivity } from '../server/ax-state.js';
|
|
33
33
|
import { registerOperationTools, registerCompositeTools } from '../server/operations/index.js';
|
|
34
|
+
import { agentSourceLabel } from '../server/operations/invoker.js';
|
|
35
|
+
import { contextReadFromPayload } from '../server/context-reads.js';
|
|
34
36
|
import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './canvas-access.js';
|
|
35
37
|
import { serializeNodeForAgentContext } from '../server/agent-context.js';
|
|
36
38
|
import { buildSpatialContext, findNeighborhoods } from '../server/spatial-analysis.js';
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
} from '../server/canvas-serialization.js';
|
|
43
45
|
import { listBundledSkills, readBundledSkill } from '../server/bundled-skills.js';
|
|
44
46
|
import { registerSkillExtension } from './skills.js';
|
|
47
|
+
import { ContextUriTemplate } from './context-uri-template.js';
|
|
45
48
|
|
|
46
49
|
let canvas: CanvasAccess | null = null;
|
|
47
50
|
let resourceNotificationServer: McpServer | null = null;
|
|
@@ -102,7 +105,9 @@ function sendCanvasResourceNotifications(type: 'nodes' | 'pins' | 'ax' | 'ax-tim
|
|
|
102
105
|
server.server.sendResourceUpdated({ uri: 'canvas://ax-delivery' });
|
|
103
106
|
}
|
|
104
107
|
server.server.sendResourceUpdated({ uri: 'canvas://layout' });
|
|
108
|
+
server.server.sendResourceUpdated({ uri: 'canvas://boards' });
|
|
105
109
|
server.server.sendResourceUpdated({ uri: 'canvas://summary' });
|
|
110
|
+
server.server.sendResourceUpdated({ uri: 'canvas://context' });
|
|
106
111
|
server.server.sendResourceUpdated({ uri: 'canvas://spatial-context' });
|
|
107
112
|
server.server.sendResourceUpdated({ uri: 'canvas://history' });
|
|
108
113
|
server.server.sendResourceUpdated({ uri: 'canvas://code-graph' });
|
|
@@ -223,6 +228,36 @@ export async function startMcpServer(): Promise<void> {
|
|
|
223
228
|
resourceNotificationServer = server;
|
|
224
229
|
registerSkillExtension(server);
|
|
225
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Records what the agent actually received from a context resource or
|
|
233
|
+
* prompt (plan-011). Instrumentation must never fail the read itself.
|
|
234
|
+
*/
|
|
235
|
+
async function recordContextRead(
|
|
236
|
+
c: CanvasAccess,
|
|
237
|
+
channel: 'mcp-resource' | 'mcp-prompt',
|
|
238
|
+
resource: string,
|
|
239
|
+
text: string,
|
|
240
|
+
target: { boardId: string | null; pinnedNodeIds: string[] },
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
try {
|
|
243
|
+
const read = contextReadFromPayload(
|
|
244
|
+
{
|
|
245
|
+
channel,
|
|
246
|
+
resource,
|
|
247
|
+
source: agentSourceLabel('mcp'),
|
|
248
|
+
consumer: server.server.getClientVersion()?.name ?? null,
|
|
249
|
+
agentId: null,
|
|
250
|
+
pinnedNodeIds: target.pinnedNodeIds,
|
|
251
|
+
boardId: target.boardId,
|
|
252
|
+
},
|
|
253
|
+
text,
|
|
254
|
+
);
|
|
255
|
+
await c.recordContextRead(read);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('[pmx-canvas mcp] recording a context read failed:', error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
// ── Operation-registry tools (plan-005) ────────────────────────
|
|
227
262
|
// Standalone tools are registered from the shared operation registry for
|
|
228
263
|
// every op not folded into a composite (registerOperationTools skips
|
|
@@ -478,8 +513,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
478
513
|
},
|
|
479
514
|
async () => {
|
|
480
515
|
const c = await ensureCanvas();
|
|
481
|
-
const
|
|
482
|
-
const
|
|
516
|
+
const readTarget = await c.prepareContextRead();
|
|
517
|
+
const board = readTarget.boardId ?? undefined;
|
|
518
|
+
const pinnedIds = new Set(readTarget.pinnedNodeIds);
|
|
519
|
+
const layout = await c.getLayout(board);
|
|
483
520
|
|
|
484
521
|
const pinnedNodes = layout.nodes.filter((n) => pinnedIds.has(n.id));
|
|
485
522
|
const pinnedEdges = layout.edges.filter((e) => pinnedIds.has(e.from) && pinnedIds.has(e.to));
|
|
@@ -510,12 +547,41 @@ export async function startMcpServer(): Promise<void> {
|
|
|
510
547
|
})),
|
|
511
548
|
};
|
|
512
549
|
|
|
550
|
+
const text = JSON.stringify(context, null, 2);
|
|
551
|
+
await recordContextRead(c, 'mcp-resource', 'canvas://pinned-context', text, readTarget);
|
|
513
552
|
return {
|
|
514
553
|
contents: [
|
|
515
554
|
{
|
|
516
555
|
uri: 'canvas://pinned-context',
|
|
517
556
|
mimeType: 'application/json',
|
|
518
|
-
text
|
|
557
|
+
text,
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
};
|
|
561
|
+
},
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
server.resource(
|
|
565
|
+
'context',
|
|
566
|
+
new ResourceTemplate(new ContextUriTemplate(), { list: undefined }),
|
|
567
|
+
{
|
|
568
|
+
description:
|
|
569
|
+
'Budgeted cross-board context brief. budget is a maximum number of UTF-16 characters; consumer selects a durable board-scoped delivery cursor, while since requests an explicit revision.',
|
|
570
|
+
mimeType: 'application/json',
|
|
571
|
+
},
|
|
572
|
+
async (uri) => {
|
|
573
|
+
const c = await ensureCanvas();
|
|
574
|
+
const input: Record<string, unknown> = {};
|
|
575
|
+
if (uri.searchParams.has('consumer')) input.consumer = uri.searchParams.get('consumer');
|
|
576
|
+
if (uri.searchParams.has('budget')) input.budget = uri.searchParams.get('budget');
|
|
577
|
+
if (uri.searchParams.has('since')) input.since = uri.searchParams.get('since');
|
|
578
|
+
const document = await c.invoker().invoke('context.get', input);
|
|
579
|
+
return {
|
|
580
|
+
contents: [
|
|
581
|
+
{
|
|
582
|
+
uri: uri.href,
|
|
583
|
+
mimeType: 'application/json',
|
|
584
|
+
text: JSON.stringify(document),
|
|
519
585
|
},
|
|
520
586
|
],
|
|
521
587
|
};
|
|
@@ -555,12 +621,17 @@ export async function startMcpServer(): Promise<void> {
|
|
|
555
621
|
async () => {
|
|
556
622
|
const c = await ensureCanvas();
|
|
557
623
|
const context = await c.getAxContext({ consumer: 'mcp' });
|
|
624
|
+
const text = JSON.stringify(context, null, 2);
|
|
625
|
+
await recordContextRead(c, 'mcp-resource', 'canvas://ax-context', text, {
|
|
626
|
+
boardId: context.boardId,
|
|
627
|
+
pinnedNodeIds: context.pinned.nodeIds,
|
|
628
|
+
});
|
|
558
629
|
return {
|
|
559
630
|
contents: [
|
|
560
631
|
{
|
|
561
632
|
uri: 'canvas://ax-context',
|
|
562
633
|
mimeType: 'application/json',
|
|
563
|
-
text
|
|
634
|
+
text,
|
|
564
635
|
},
|
|
565
636
|
],
|
|
566
637
|
};
|
|
@@ -678,11 +749,16 @@ export async function startMcpServer(): Promise<void> {
|
|
|
678
749
|
async () => {
|
|
679
750
|
const c = await ensureCanvas();
|
|
680
751
|
const context = await c.getAxContext({ consumer: 'mcp' });
|
|
752
|
+
const text = `Current PMX Canvas context:\n\n${JSON.stringify(context, null, 2)}`;
|
|
753
|
+
await recordContextRead(c, 'mcp-prompt', 'pmx-current-context', text, {
|
|
754
|
+
boardId: context.boardId,
|
|
755
|
+
pinnedNodeIds: context.pinned.nodeIds,
|
|
756
|
+
});
|
|
681
757
|
return {
|
|
682
758
|
messages: [
|
|
683
759
|
{
|
|
684
760
|
role: 'user',
|
|
685
|
-
content: { type: 'text', text
|
|
761
|
+
content: { type: 'text', text },
|
|
686
762
|
},
|
|
687
763
|
],
|
|
688
764
|
};
|
|
@@ -700,19 +776,40 @@ export async function startMcpServer(): Promise<void> {
|
|
|
700
776
|
},
|
|
701
777
|
async () => {
|
|
702
778
|
const c = await ensureCanvas();
|
|
703
|
-
const
|
|
779
|
+
const readTarget = await c.prepareContextRead();
|
|
780
|
+
const layout = agentSafeFullLayoutPayload(await c.getLayout(readTarget.boardId ?? undefined));
|
|
781
|
+
const text = JSON.stringify(layout, null, 2);
|
|
782
|
+
await recordContextRead(c, 'mcp-resource', 'canvas://layout', text, readTarget);
|
|
704
783
|
return {
|
|
705
784
|
contents: [
|
|
706
785
|
{
|
|
707
786
|
uri: 'canvas://layout',
|
|
708
787
|
mimeType: 'application/json',
|
|
709
|
-
text
|
|
788
|
+
text,
|
|
710
789
|
},
|
|
711
790
|
],
|
|
712
791
|
};
|
|
713
792
|
},
|
|
714
793
|
);
|
|
715
794
|
|
|
795
|
+
server.resource(
|
|
796
|
+
'canvas-boards',
|
|
797
|
+
'canvas://boards',
|
|
798
|
+
{
|
|
799
|
+
description:
|
|
800
|
+
'The boards in this workspace with their categories, most recently opened first, and which one the ' +
|
|
801
|
+
'human has open (activeBoardId; null = Home). Your writes go to the open board; ask the human to open another.',
|
|
802
|
+
mimeType: 'application/json',
|
|
803
|
+
},
|
|
804
|
+
async () => {
|
|
805
|
+
const c = await ensureCanvas();
|
|
806
|
+
const boards = await c.invoker().invoke('board.list', {});
|
|
807
|
+
return {
|
|
808
|
+
contents: [{ uri: 'canvas://boards', mimeType: 'application/json', text: JSON.stringify(boards, null, 2) }],
|
|
809
|
+
};
|
|
810
|
+
},
|
|
811
|
+
);
|
|
812
|
+
|
|
716
813
|
server.resource(
|
|
717
814
|
'canvas-summary',
|
|
718
815
|
'canvas://summary',
|
|
@@ -724,12 +821,19 @@ export async function startMcpServer(): Promise<void> {
|
|
|
724
821
|
},
|
|
725
822
|
async () => {
|
|
726
823
|
const c = await ensureCanvas();
|
|
824
|
+
const readTarget = await c.prepareContextRead();
|
|
825
|
+
const text = JSON.stringify(
|
|
826
|
+
buildSummaryFromLayout(await c.getLayout(readTarget.boardId ?? undefined), readTarget.pinnedNodeIds),
|
|
827
|
+
null,
|
|
828
|
+
2,
|
|
829
|
+
);
|
|
830
|
+
await recordContextRead(c, 'mcp-resource', 'canvas://summary', text, readTarget);
|
|
727
831
|
return {
|
|
728
832
|
contents: [
|
|
729
833
|
{
|
|
730
834
|
uri: 'canvas://summary',
|
|
731
835
|
mimeType: 'application/json',
|
|
732
|
-
text
|
|
836
|
+
text,
|
|
733
837
|
},
|
|
734
838
|
],
|
|
735
839
|
};
|
|
@@ -750,19 +854,22 @@ export async function startMcpServer(): Promise<void> {
|
|
|
750
854
|
},
|
|
751
855
|
async () => {
|
|
752
856
|
const c = await ensureCanvas();
|
|
753
|
-
const
|
|
857
|
+
const readTarget = await c.prepareContextRead();
|
|
858
|
+
const layout = await c.getLayout(readTarget.boardId ?? undefined);
|
|
754
859
|
const spatial = buildSpatialContext(
|
|
755
860
|
layout.nodes,
|
|
756
861
|
layout.edges,
|
|
757
|
-
new Set(
|
|
862
|
+
new Set(readTarget.pinnedNodeIds),
|
|
758
863
|
layout.annotations ?? [],
|
|
759
864
|
);
|
|
865
|
+
const text = JSON.stringify(spatial, null, 2);
|
|
866
|
+
await recordContextRead(c, 'mcp-resource', 'canvas://spatial-context', text, readTarget);
|
|
760
867
|
return {
|
|
761
868
|
contents: [
|
|
762
869
|
{
|
|
763
870
|
uri: 'canvas://spatial-context',
|
|
764
871
|
mimeType: 'application/json',
|
|
765
|
-
text
|
|
872
|
+
text,
|
|
766
873
|
},
|
|
767
874
|
],
|
|
768
875
|
};
|
|
@@ -898,10 +1005,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
898
1005
|
const shutdown = (): void => {
|
|
899
1006
|
try {
|
|
900
1007
|
stopCanvasServer();
|
|
901
|
-
|
|
902
|
-
|
|
1008
|
+
process.exit(0);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
console.error('Shutdown refused: canvas changes could not be saved. Fix persistence and retry shutdown.', error);
|
|
903
1011
|
}
|
|
904
|
-
process.exit(0);
|
|
905
1012
|
};
|
|
906
1013
|
server.server.onclose = shutdown;
|
|
907
1014
|
process.stdin.on('end', shutdown);
|