pmx-canvas 0.1.36 → 0.2.1
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 +447 -0
- package/Readme.md +2 -2
- package/dist/json-render/index.js +89 -334
- package/dist/types/mcp/canvas-access.d.ts +5 -171
- package/dist/types/server/ax-state-manager.d.ts +267 -0
- package/dist/types/server/ax-state.d.ts +3 -1
- package/dist/types/server/canvas-db.d.ts +13 -0
- package/dist/types/server/canvas-operations.d.ts +1 -12
- package/dist/types/server/canvas-state.d.ts +8 -23
- package/dist/types/server/index.d.ts +6 -24
- package/dist/types/server/operations/composites.d.ts +121 -0
- package/dist/types/server/operations/http.d.ts +7 -0
- package/dist/types/server/operations/index.d.ts +8 -0
- package/dist/types/server/operations/invoker.d.ts +13 -0
- package/dist/types/server/operations/mcp.d.ts +15 -0
- package/dist/types/server/operations/ops/annotation.d.ts +2 -0
- package/dist/types/server/operations/ops/app.d.ts +33 -0
- package/dist/types/server/operations/ops/ax-await.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +31 -0
- package/dist/types/server/operations/ops/ax-state.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-timeline.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-work.d.ts +2 -0
- package/dist/types/server/operations/ops/batch.d.ts +19 -0
- package/dist/types/server/operations/ops/edges.d.ts +2 -0
- package/dist/types/server/operations/ops/groups.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +31 -0
- package/dist/types/server/operations/ops/nodes.d.ts +62 -0
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/ops/snapshots.d.ts +2 -0
- package/dist/types/server/operations/ops/validate.d.ts +2 -0
- package/dist/types/server/operations/ops/viewport.d.ts +2 -0
- package/dist/types/server/operations/ops/webview.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +15 -0
- package/dist/types/server/operations/types.d.ts +116 -0
- package/dist/types/server/operations/webview-runner.d.ts +69 -0
- package/docs/RELEASE.md +5 -0
- package/docs/adr-001-bun-only-runtime.md +46 -0
- package/docs/api-stability.md +57 -0
- package/docs/ax-host-adapter-contract.md +19 -1
- package/docs/ax-state-contract.md +72 -0
- package/docs/http-api.md +4 -0
- package/docs/mcp.md +61 -12
- package/docs/plans/plan-005-operation-registry.md +84 -0
- package/docs/plans/plan-006-mcp-tool-consolidation.md +109 -0
- package/docs/plans/plan-007-ax-domain.md +99 -0
- package/docs/plans/plan-008-registry-finish.md +91 -0
- package/docs/tech-debt-assessment-2026-06.md +90 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +221 -193
- package/skills/pmx-canvas/evals/evals.json +3 -3
- package/skills/pmx-canvas/references/ax-html-control-surface.md +93 -0
- package/skills/pmx-canvas/references/codex-app-adapter.md +13 -14
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +26 -11
- package/src/cli/agent.ts +52 -31
- package/src/mcp/canvas-access.ts +30 -830
- package/src/mcp/server.ts +162 -2014
- package/src/server/ax-context.ts +8 -1
- package/src/server/ax-state-manager.ts +826 -0
- package/src/server/ax-state.ts +10 -2
- package/src/server/canvas-db.ts +35 -0
- package/src/server/canvas-operations.ts +2 -328
- package/src/server/canvas-schema.ts +2 -2
- package/src/server/canvas-state.ts +103 -465
- package/src/server/index.ts +54 -190
- package/src/server/operations/composites.ts +355 -0
- package/src/server/operations/http.ts +103 -0
- package/src/server/operations/index.ts +65 -0
- package/src/server/operations/invoker.ts +87 -0
- package/src/server/operations/mcp.ts +221 -0
- package/src/server/operations/ops/annotation.ts +60 -0
- package/src/server/operations/ops/app.ts +447 -0
- package/src/server/operations/ops/ax-await.ts +216 -0
- package/src/server/operations/ops/ax-shared.ts +38 -0
- package/src/server/operations/ops/ax-state.ts +249 -0
- package/src/server/operations/ops/ax-timeline.ts +381 -0
- package/src/server/operations/ops/ax-work.ts +635 -0
- package/src/server/operations/ops/batch.ts +365 -0
- package/src/server/operations/ops/edges.ts +166 -0
- package/src/server/operations/ops/groups.ts +176 -0
- package/src/server/operations/ops/json-render.ts +691 -0
- package/src/server/operations/ops/nodes.ts +1047 -0
- package/src/server/operations/ops/query.ts +281 -0
- package/src/server/operations/ops/snapshots.ts +366 -0
- package/src/server/operations/ops/validate.ts +37 -0
- package/src/server/operations/ops/viewport.ts +219 -0
- package/src/server/operations/ops/webview.ts +339 -0
- package/src/server/operations/registry.ts +79 -0
- package/src/server/operations/types.ts +150 -0
- package/src/server/operations/webview-runner.ts +77 -0
- package/src/server/server.ts +158 -2255
- package/src/server/web-artifacts.ts +6 -2
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool registration for registered operations.
|
|
3
|
+
*
|
|
4
|
+
* Iterates the registry, advertises `{ ...op.inputShape, ...extraShape }` as
|
|
5
|
+
* the tool schema, invokes through the host's invoker (local or HTTP), and
|
|
6
|
+
* formats the wire-shaped result with the op's `formatResult`. Errors
|
|
7
|
+
* (including OperationError) become `isError` tool results with the bare
|
|
8
|
+
* message text, matching the legacy hand-written tools.
|
|
9
|
+
*/
|
|
10
|
+
import { z, type ZodRawShape, type ZodTypeAny } from 'zod';
|
|
11
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
import { getOperation, listOperations } from './registry.js';
|
|
13
|
+
import type { OperationInvoker } from './invoker.js';
|
|
14
|
+
import { OperationError, type OperationMcpToolHost } from './types.js';
|
|
15
|
+
import { buildCompositeDeprecationNotes, compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
|
|
16
|
+
|
|
17
|
+
export interface OperationToolHost extends OperationMcpToolHost {
|
|
18
|
+
invoker(): OperationInvoker;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerOperationTools(
|
|
22
|
+
server: McpServer,
|
|
23
|
+
getHost: () => Promise<OperationToolHost>,
|
|
24
|
+
): void {
|
|
25
|
+
// Legacy tools folded by a composite get a "Deprecated: use canvas_x …" prefix
|
|
26
|
+
// (plan-006 step 2) so agents migrate during the v0.2 overlap window.
|
|
27
|
+
const deprecations = buildCompositeDeprecationNotes();
|
|
28
|
+
for (const op of listOperations()) {
|
|
29
|
+
const tool = op.mcp;
|
|
30
|
+
if (!tool) continue;
|
|
31
|
+
const note = deprecations.get(op.name);
|
|
32
|
+
server.tool(
|
|
33
|
+
tool.toolName,
|
|
34
|
+
note ? note + tool.description : tool.description,
|
|
35
|
+
{ ...op.inputShape, ...(tool.extraShape ?? {}) },
|
|
36
|
+
async (input: Record<string, unknown>) => {
|
|
37
|
+
try {
|
|
38
|
+
const host = await getHost();
|
|
39
|
+
const opInput = tool.buildInput ? tool.buildInput(input) : input;
|
|
40
|
+
const result = await host.invoker().invoke(op.name, opInput);
|
|
41
|
+
if (tool.formatResult) {
|
|
42
|
+
return await tool.formatResult(result, input, host);
|
|
43
|
+
}
|
|
44
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
|
|
48
|
+
isError: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Member op names a composite folds: the flat `actions` map values for a
|
|
58
|
+
* single-discriminator composite, or the explicit `memberOps` for a
|
|
59
|
+
* two-discriminator one (canvas_ax_gate).
|
|
60
|
+
*/
|
|
61
|
+
function compositeMemberOps(def: CompositeToolDefinition): string[] {
|
|
62
|
+
return def.memberOps ?? Object.values(def.actions);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Derive a composite tool's advertised schema from its member operations: the
|
|
67
|
+
* union of every member op's `inputShape` + `extraShape`, each made optional
|
|
68
|
+
* (not every action uses every field), plus the `action` discriminator (and, for
|
|
69
|
+
* two-discriminator composites, the extra `kind` discriminator). Deriving it
|
|
70
|
+
* (instead of hand-listing fields) keeps the composite schema in lockstep with
|
|
71
|
+
* the operations it folds — no drift.
|
|
72
|
+
*
|
|
73
|
+
* Collision handling (`fieldRemap`): a member-op field whose name equals a
|
|
74
|
+
* discriminator (e.g. `ax.approval.request`'s own `action`) is advertised under
|
|
75
|
+
* its remapped public name (`approvalAction`) so the discriminator stays clean;
|
|
76
|
+
* dispatch maps it back before invoking the op.
|
|
77
|
+
*
|
|
78
|
+
* Shared-field widening: a field contributed by ONE member op is advertised with
|
|
79
|
+
* that op's strict MCP schema (best agent ergonomics). A field contributed by
|
|
80
|
+
* MULTIPLE member ops is advertised with the loose `inputShape` schema instead —
|
|
81
|
+
* those ops may disagree on the strict type (e.g. `canvas_ax_timeline`'s `kind`
|
|
82
|
+
* is the event-kind enum for `ax.event.record` but the evidence-kind enum for
|
|
83
|
+
* `ax.evidence.add`), and a first-write-wins strict enum would wrongly reject a
|
|
84
|
+
* value valid for another action. The loose shape never over-rejects; each op
|
|
85
|
+
* still validates its own input against that same loose shape + its handler.
|
|
86
|
+
*/
|
|
87
|
+
function buildCompositeShape(def: CompositeToolDefinition): ZodRawShape {
|
|
88
|
+
// The action discriminator: explicit `actionEnum` (two-discriminator path) or
|
|
89
|
+
// the flat `actions` map keys (single-discriminator path).
|
|
90
|
+
const actionKeys = (def.actionEnum ?? Object.keys(def.actions)) as [string, ...string[]];
|
|
91
|
+
const shape: Record<string, ZodTypeAny> = {
|
|
92
|
+
action: z.enum(actionKeys).describe(`Operation to perform: ${def.actionSummary}.`),
|
|
93
|
+
...(def.extraDiscriminatorShape ?? {}),
|
|
94
|
+
};
|
|
95
|
+
// Reserve discriminator names so a colliding member field never overwrites them.
|
|
96
|
+
const discriminators = new Set(Object.keys(shape));
|
|
97
|
+
// op-field-name → public-field-name (inverse of fieldRemap), so a colliding
|
|
98
|
+
// op field is advertised under its namespaced public name.
|
|
99
|
+
const opToPublic = new Map<string, string>();
|
|
100
|
+
for (const [publicName, opField] of Object.entries(def.fieldRemap ?? {})) {
|
|
101
|
+
opToPublic.set(opField, publicName);
|
|
102
|
+
}
|
|
103
|
+
// First pass: count how many member ops contribute each public field, and
|
|
104
|
+
// capture both the strict (extraShape-preferred) and loose (inputShape) schema.
|
|
105
|
+
const fields = new Map<string, { count: number; strict: ZodTypeAny; loose: ZodTypeAny }>();
|
|
106
|
+
for (const opName of new Set(compositeMemberOps(def))) {
|
|
107
|
+
const op = getOperation(opName);
|
|
108
|
+
const looseShape = op.inputShape;
|
|
109
|
+
const strictShape = { ...op.inputShape, ...(op.mcp?.extraShape ?? {}) };
|
|
110
|
+
for (const [key, schema] of Object.entries(strictShape)) {
|
|
111
|
+
const publicKey = opToPublic.get(key) ?? key;
|
|
112
|
+
if (discriminators.has(publicKey)) continue; // discriminator wins; drop the field.
|
|
113
|
+
const entry = fields.get(publicKey);
|
|
114
|
+
if (entry) {
|
|
115
|
+
entry.count += 1;
|
|
116
|
+
} else {
|
|
117
|
+
fields.set(publicKey, {
|
|
118
|
+
count: 1,
|
|
119
|
+
strict: schema as ZodTypeAny,
|
|
120
|
+
loose: (looseShape[key] ?? schema) as ZodTypeAny,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const [publicKey, entry] of fields) {
|
|
126
|
+
// Multi-op fields widen to the loose shape (ops may disagree on the strict
|
|
127
|
+
// type); single-op fields keep the strict schema.
|
|
128
|
+
const schema = entry.count > 1 ? entry.loose : entry.strict;
|
|
129
|
+
shape[publicKey] = schema.optional();
|
|
130
|
+
}
|
|
131
|
+
return shape;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve the registry op a composite invocation dispatches to. Two-discriminator
|
|
136
|
+
* composites (canvas_ax_gate) use `resolveOp(kind, action)`; single-discriminator
|
|
137
|
+
* composites use the flat `actions` map. An unknown action / invalid combo is a
|
|
138
|
+
* loud OperationError (never a silent no-op).
|
|
139
|
+
*/
|
|
140
|
+
function resolveCompositeOp(def: CompositeToolDefinition, input: Record<string, unknown>): string {
|
|
141
|
+
const action = typeof input.action === 'string' ? input.action : '';
|
|
142
|
+
if (def.resolveOp) {
|
|
143
|
+
const kind = typeof input.kind === 'string' ? input.kind : '';
|
|
144
|
+
const opName = def.resolveOp({ kind, action });
|
|
145
|
+
if (!opName) {
|
|
146
|
+
throw new OperationError(
|
|
147
|
+
`Invalid kind/action combination (kind "${kind}", action "${action}") for ${def.toolName}.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return opName;
|
|
151
|
+
}
|
|
152
|
+
const opName = def.actions[action];
|
|
153
|
+
if (!opName) {
|
|
154
|
+
throw new OperationError(
|
|
155
|
+
`Unknown action "${action}" for ${def.toolName}. Valid actions: ${Object.keys(def.actions).join(', ')}.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return opName;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Strip the composite discriminators (`action` plus any `extraDiscriminatorShape`
|
|
163
|
+
* keys such as `kind`) and undo the field remap (public field name → op field
|
|
164
|
+
* name, e.g. `approvalAction` → `action`). The result is the raw MCP arg object
|
|
165
|
+
* the standalone tool would receive.
|
|
166
|
+
*/
|
|
167
|
+
function stripCompositeDiscriminators(
|
|
168
|
+
def: CompositeToolDefinition,
|
|
169
|
+
input: Record<string, unknown>,
|
|
170
|
+
): Record<string, unknown> {
|
|
171
|
+
const discriminators = new Set<string>(['action', ...Object.keys(def.extraDiscriminatorShape ?? {})]);
|
|
172
|
+
const out: Record<string, unknown> = {};
|
|
173
|
+
for (const [key, value] of Object.entries(input)) {
|
|
174
|
+
if (discriminators.has(key)) continue;
|
|
175
|
+
const opField = def.fieldRemap?.[key] ?? key;
|
|
176
|
+
out[opField] = value;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Register composite (action-discriminated) MCP tools (plan-006). Each action
|
|
183
|
+
* dispatches to a registered operation, reusing that op's `mcp.buildInput` and
|
|
184
|
+
* `mcp.formatResult` so the composite action is byte-identical to the standalone
|
|
185
|
+
* tool it folds. Defaults to `compositeToolDefinitions`.
|
|
186
|
+
*/
|
|
187
|
+
export function registerCompositeTools(
|
|
188
|
+
server: McpServer,
|
|
189
|
+
getHost: () => Promise<OperationToolHost>,
|
|
190
|
+
definitions: CompositeToolDefinition[] = compositeToolDefinitions,
|
|
191
|
+
): void {
|
|
192
|
+
for (const def of definitions) {
|
|
193
|
+
server.tool(
|
|
194
|
+
def.toolName,
|
|
195
|
+
def.description,
|
|
196
|
+
buildCompositeShape(def),
|
|
197
|
+
async (input: Record<string, unknown>) => {
|
|
198
|
+
try {
|
|
199
|
+
const host = await getHost();
|
|
200
|
+
const opName = resolveCompositeOp(def, input);
|
|
201
|
+
const op = getOperation(opName);
|
|
202
|
+
// Strip the composite discriminators (action + any extra, e.g. `kind`)
|
|
203
|
+
// and undo any field remap; the rest is the op's raw MCP args — the same
|
|
204
|
+
// value the standalone tool would receive.
|
|
205
|
+
const rest = stripCompositeDiscriminators(def, input);
|
|
206
|
+
const opInput = op.mcp?.buildInput ? op.mcp.buildInput(rest) : rest;
|
|
207
|
+
const result = await host.invoker().invoke(opName, opInput);
|
|
208
|
+
if (op.mcp?.formatResult) {
|
|
209
|
+
return await op.mcp.formatResult(result, rest, host);
|
|
210
|
+
}
|
|
211
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
|
|
215
|
+
isError: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotation mutation op (plan-008 Wave 1): annotation.remove.
|
|
3
|
+
*
|
|
4
|
+
* DELETE-by-id of a human-drawn canvas annotation. mutates: true → the registry
|
|
5
|
+
* auto-emits one canvas-layout-update after success, matching the legacy
|
|
6
|
+
* handleCanvasRemoveAnnotation handler (which emitted the layout update only on a
|
|
7
|
+
* successful removal). A non-existent id is a 404 OperationError carrying the
|
|
8
|
+
* exact legacy message; an omitted id is a 400 so composite callers get a loud
|
|
9
|
+
* input error instead of a misleading "not found". HTTP renders errors as
|
|
10
|
+
* { ok:false, error } and MCP renders them as isError tool results.
|
|
11
|
+
*
|
|
12
|
+
* This module must never import server.ts or index.ts.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { canvasState } from '../../canvas-state.js';
|
|
16
|
+
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
17
|
+
|
|
18
|
+
const annotationRemoveShape = {
|
|
19
|
+
id: z.string().optional().catch(undefined).describe('Annotation ID to remove'),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const annotationRemoveSchema = z.looseObject(annotationRemoveShape);
|
|
23
|
+
|
|
24
|
+
const annotationRemoveOperation = defineOperation<z.infer<typeof annotationRemoveSchema>, Record<string, unknown>>({
|
|
25
|
+
name: 'annotation.remove',
|
|
26
|
+
mutates: true,
|
|
27
|
+
input: annotationRemoveSchema,
|
|
28
|
+
inputShape: annotationRemoveShape,
|
|
29
|
+
http: {
|
|
30
|
+
method: 'DELETE',
|
|
31
|
+
path: '/api/canvas/annotation/:id',
|
|
32
|
+
},
|
|
33
|
+
mcp: {
|
|
34
|
+
toolName: 'canvas_remove_annotation',
|
|
35
|
+
description: 'Remove a human-drawn canvas annotation by ID.',
|
|
36
|
+
extraShape: {
|
|
37
|
+
id: z.string().describe('Annotation ID to remove'),
|
|
38
|
+
},
|
|
39
|
+
formatResult: (result) => ({
|
|
40
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
handler: ({ id }) => {
|
|
44
|
+
const annotationId = id ?? '';
|
|
45
|
+
// A missing id is a 400, not a misleading 404 — matches edge.remove. The
|
|
46
|
+
// canvas_view composite widens `id` to optional (node.focus also contributes
|
|
47
|
+
// it), so the schema does not reject an absent id at the MCP boundary; guard
|
|
48
|
+
// here so `{ action: "remove-annotation" }` with no id fails loudly.
|
|
49
|
+
if (!annotationId) {
|
|
50
|
+
throw new OperationError('Missing id.', 400);
|
|
51
|
+
}
|
|
52
|
+
const removed = canvasState.removeAnnotation(annotationId);
|
|
53
|
+
if (!removed) {
|
|
54
|
+
throw new OperationError(`Annotation "${annotationId}" not found.`, 404);
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, removed: annotationId };
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const annotationOperations: Operation[] = [annotationRemoveOperation];
|