pmx-canvas 0.9.3 → 0.10.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.
Files changed (56) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +9278 -2014
  3. package/CHANGELOG.md +22 -1
  4. package/dist/canvas/index.js +1 -1
  5. package/dist/canvas/mcp-app.html +1 -1
  6. package/dist/types/mcp/canvas-access.d.ts +1 -12
  7. package/dist/types/mcp/resource-subscriptions.d.ts +7 -0
  8. package/dist/types/mcp/server.d.ts +2 -0
  9. package/dist/types/server/ax-wait.d.ts +1 -0
  10. package/dist/types/server/canvas-operations.d.ts +1 -0
  11. package/dist/types/server/index.d.ts +3 -3
  12. package/dist/types/server/operations/http.d.ts +10 -0
  13. package/dist/types/server/operations/index.d.ts +2 -2
  14. package/dist/types/server/operations/invoker.d.ts +4 -3
  15. package/dist/types/server/operations/mcp-contracts.d.ts +20 -0
  16. package/dist/types/server/operations/mcp.d.ts +2 -0
  17. package/dist/types/server/operations/ops/batch.d.ts +1 -0
  18. package/dist/types/server/operations/registry.d.ts +2 -2
  19. package/dist/types/server/operations/types.d.ts +12 -2
  20. package/dist/types/server/server.d.ts +2 -0
  21. package/dist/types/server/webpage-node.d.ts +1 -0
  22. package/docs/api-reference.md +2 -2
  23. package/docs/cli.md +1 -1
  24. package/docs/http-api.md +31 -0
  25. package/docs/mcp.md +93 -21
  26. package/docs/plans/plan-019-post-0.9-roadmap.md +123 -6
  27. package/docs/sdk.md +5 -2
  28. package/package.json +1 -1
  29. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  30. package/plugins/pmx-canvas/plugin.json +1 -1
  31. package/plugins/pmx-canvas/runtime.json +1 -1
  32. package/plugins/pmx-canvas/tools.json +9278 -2014
  33. package/skills/pmx-canvas/SKILL.md +9 -4
  34. package/skills/pmx-canvas/references/full-reference.md +35 -2
  35. package/skills/pmx-canvas/references/installing-pmx-canvas.md +7 -7
  36. package/src/client/canvas/ToolRail.tsx +2 -0
  37. package/src/mcp/canvas-access.ts +6 -47
  38. package/src/mcp/resource-subscriptions.ts +181 -0
  39. package/src/mcp/server.ts +185 -219
  40. package/src/server/ax-wait.ts +21 -6
  41. package/src/server/canvas-operations.ts +6 -2
  42. package/src/server/index.ts +12 -4
  43. package/src/server/operations/http.ts +69 -21
  44. package/src/server/operations/index.ts +8 -1
  45. package/src/server/operations/invoker.ts +53 -5
  46. package/src/server/operations/mcp-contracts.ts +339 -0
  47. package/src/server/operations/mcp.ts +153 -18
  48. package/src/server/operations/ops/ax-await.ts +18 -10
  49. package/src/server/operations/ops/ax-read.ts +2 -0
  50. package/src/server/operations/ops/ax-timeline.ts +2 -2
  51. package/src/server/operations/ops/batch.ts +27 -8
  52. package/src/server/operations/ops/nodes.ts +18 -3
  53. package/src/server/operations/registry.ts +24 -4
  54. package/src/server/operations/types.ts +16 -2
  55. package/src/server/server.ts +10 -0
  56. package/src/server/webpage-node.ts +9 -4
@@ -106,7 +106,7 @@ for the authoritative stop, viewport, persistence, reset, group-order, and captu
106
106
 
107
107
  ## Runtime prerequisites
108
108
 
109
- PMX Canvas 0.9.3 requires **Bun >=1.4.4**, including when installed through npm.
109
+ PMX Canvas 0.10.0 requires **Bun >=1.4.4**, including when installed through npm.
110
110
  Check `bun --version` and ensure Bun is on the MCP host's PATH, not just your shell's;
111
111
  use an absolute executable path if needed. Install/configure only when requested.
112
112
  For pinned installation, MCP configuration, managed services, and disposable verification,
@@ -264,10 +264,15 @@ json-render, a graph, or an HTML primitive is sufficient.
264
264
 
265
265
  ## Current MCP Composites
266
266
 
267
- The live MCP surface is **24 tools**: the 18 composites below plus 6 standalones. This table is
267
+ The live MCP surface is **25 tools**: the 18 composites below plus 7 standalones. This table is
268
268
  kept in sync with the server's composite registry; `tools/list` on a fresh `pmx-canvas --mcp` is
269
269
  always authoritative, independent of the installed release version.
270
270
 
271
+ Use `canvas_help {}` to list MCP registry operation names, then `canvas_help { op: "node.add" }`
272
+ for that operation's public tool, argument schema and guidance. Help does not start or read a
273
+ canvas. The schema includes routing selectors and required fields; handlers still enforce
274
+ conditional requirements. Use `tools/list` for non-registry tools such as `canvas_screenshot`.
275
+
271
276
  | Composite | Actions |
272
277
  |-----------|---------|
273
278
  | `canvas_import` | `list`, `read`, `submit`, `unavailable` |
@@ -303,7 +308,7 @@ Important routing:
303
308
  As of v0.3.0, the 57 legacy single-purpose tools from the v0.2 compatibility window are removed.
304
309
  The composites above plus the retained standalones are now the whole MCP surface: `canvas_batch`,
305
310
  `canvas_pin_nodes`, `canvas_screenshot`, `canvas_ax_interaction`, `canvas_ingest_activity`, and
306
- `canvas_invoke_command`. Snapshots are the `canvas_snapshot` composite (actions
311
+ `canvas_invoke_command`, plus `canvas_help`. Snapshots are the `canvas_snapshot` composite (actions
307
312
  `save | list | restore | restore-node | delete | gc | diff`); the 6 legacy snapshot standalones were removed in
308
313
  v0.4.0 after their deprecated 0.3.x window.
309
314
 
@@ -683,7 +688,7 @@ Single-card and layout reads record content delivery, not incident relations.
683
688
  Graph cards and complete brief relations carry a captured `linksRevision`: reads
684
689
  acknowledge that version, not later changes. Brief delivery is qualified by source board.
685
690
  Reads never switch boards or mark full content read; pull needed cards in full separately. MCP remains
686
- 24 tools and 15 static canvas resources, with two canvas templates (context/graph).
691
+ 25 tools and 15 static canvas resources, with two canvas templates (context/graph).
687
692
 
688
693
  Markdown saves bind `[[Board]]`, `[[Board#Card]]`, and `|alias` forms to durable
689
694
  IDs; code spans/fences are ignored. Ambiguous/missing targets never guess or
@@ -431,10 +431,15 @@ When you do set a color (group/graph), use this palette consistently to convey m
431
431
  ## MCP Tools Reference
432
432
 
433
433
  PMX Canvas exposes **18 action-discriminated composites** (the whole recommended surface) plus
434
- **6 first-class standalones** — 24 tools total. The composites fold the older
434
+ **7 first-class standalones** — 25 tools total. The composites fold the older
435
435
  single-purpose tools behind an `action` (and, for `canvas_ax_gate`, a `kind`) discriminator —
436
436
  **field names are unchanged**; only the tool name + the `action`/`kind` selector differ.
437
437
 
438
+ `canvas_help {}` lists MCP registry operation names. `canvas_help { op: "node.add" }`
439
+ returns that operation's public tool, description and JSON argument schema, including
440
+ routing selectors and required fields. It does not start or read a canvas. Handlers still
441
+ enforce conditional requirements. Non-registry tools use `tools/list` instead.
442
+
438
443
  > **Legacy single-purpose tools were removed in v0.3.0.** The old names (`canvas_add_node`,
439
444
  > `canvas_update_node`, `canvas_request_approval`, `canvas_add_work_item`, …) are no longer
440
445
  > registered — use the composites below and fresh MCP `tools/list` for the installed runtime's
@@ -531,7 +536,7 @@ AX excerpts and annotation anchors, including `canvas://ax-context` and
531
536
  or brief relation entry instead.
532
537
  Single-card and layout reads record content delivery, not incident relations.
533
538
  Reads never switch boards or record full content reads. Pull cards in full
534
- when needed. Counts stay 24 tools, 15 static canvas resources; there are now two
539
+ when needed. Counts are 25 tools, 15 static canvas resources; there are now two
535
540
  canvas templates, context and graph, besides the skill file template.
536
541
 
537
542
  Markdown save resolves `[[Board]]`, `[[Board#Card]]`, and `|alias` forms into
@@ -1124,11 +1129,39 @@ Use native `json-render` and `graph` nodes when the output should stay fully ins
1124
1129
 
1125
1130
  Spec elements support an `on` map (`on.press`, `on.change`, …) binding events to actions (`{ action, params }`) — built-in actions (`setState`, `pushState`, …) or, when named after an AX interaction type, a capability-gated AX emit. e.g. a Button with `on: { press: { action: 'ax.work.create', params: { title: '…' } } }` lets a human turn a panel control into a tracked work item; the viewer forwards it to the canvas, which validates and submits it server-side (clamped to the node's own id). See **Node AX Interactions** above.
1126
1131
 
1132
+ ## MCP results and cancellation (0.10)
1133
+
1134
+ The 25-tool catalog remains on MCP SDK v1. Each tool advertises `outputSchema`
1135
+ and effect annotations. Help returns `structuredContent.result` with operation
1136
+ metadata (or the name list); it records no content reads. Registry-dispatched tools expose
1137
+ `structuredContent.result = { operation, outcome, data }`, where `outcome` is
1138
+ `success`, `error` or `partial`; `data` matches the existing compact text JSON,
1139
+ or `{message}` for prose. Exceptions use `operation: null`; they do not prove no
1140
+ writes occurred. Interaction, activity-ingestion and screenshot tools instead
1141
+ use direct structured objects matching their schemas. Check `isError` and domain
1142
+ outcomes. A composite's annotations cover all its actions, not just reads.
1143
+
1144
+ Cancellation releases supported AX waits, aborts webpage fetches before late
1145
+ content writes, and stops subsequent batch operations. Completed writes remain;
1146
+ batch partial results retain `results` and `refs`. A created webpage can remain
1147
+ with `fetch.cancelled: true`. External app builds/runtime calls and local WebView
1148
+ captures are not interruptible mid-call. After a disconnect, inspect state before
1149
+ retrying because the response may not arrive. `_meta.progressToken` enables batch
1150
+ and supported AX wait progress notifications, including through the daemon connection. Without
1151
+ a token, calls return their normal result without progress notifications.
1152
+
1127
1153
  ## MCP Resources
1128
1154
 
1129
1155
  These resources give you read access to canvas intelligence. Read them to understand
1130
1156
  what the human has set up and what they're focusing on.
1131
1157
 
1158
+ As of 0.10, live resource updates require `resources/subscribe` on each connection.
1159
+ Subscribe to the exact URI you read, including context/graph query parameters;
1160
+ `resources/unsubscribe` stops it, and reconnecting requires a new subscription.
1161
+ Notifications invalidate cached data but do not deliver content, record a read, or
1162
+ advance a consumer cursor. Explicit `resources/read` remains available without
1163
+ subscriptions. Static schema and bundled skills do not support subscriptions.
1164
+
1132
1165
  | Resource | What it provides |
1133
1166
  |----------|-----------------|
1134
1167
  | `canvas://context{?budget,consumer,since}` | Budgeted cross-board map brief with optional durable consumer cursor |
@@ -4,7 +4,7 @@ Use this reference when the `pmx-canvas` skill is installed but the `pmx-canvas`
4
4
 
5
5
  ## Prerequisites and version
6
6
 
7
- The examples pin **PMX Canvas 0.9.3**, which requires **Bun >=1.4.4** even when
7
+ The examples pin **PMX Canvas 0.10.0**, which requires **Bun >=1.4.4** even when
8
8
  installed through npm: the CLI runs through Bun, not Node. Check `bun --version`
9
9
  first. If missing or older, follow the [Bun installation instructions](https://bun.sh/docs/installation)
10
10
  with the user's authorization. Ensure Bun is on the MCP host's PATH as well as the
@@ -22,11 +22,11 @@ in `SKILL.md`. Do not pair newer checkout guidance with an older runtime unknowi
22
22
  ## Install from npm
23
23
 
24
24
  ```bash
25
- npm install -g pmx-canvas@0.9.3
25
+ npm install -g pmx-canvas@0.10.0
26
26
  pmx-canvas --version
27
27
  ```
28
28
 
29
- Without a global install, use `bunx pmx-canvas@0.9.3` in place of `pmx-canvas` in
29
+ Without a global install, use `bunx pmx-canvas@0.10.0` in place of `pmx-canvas` in
30
30
  the commands below. Do not use unpinned `bunx pmx-canvas` for a version-specific check.
31
31
 
32
32
  ## Choose the workspace and target
@@ -96,7 +96,7 @@ discards empty env values, remove `PMX_CANVAS_URL` from its launch environment i
96
96
  "mcpServers": {
97
97
  "canvas": {
98
98
  "command": "bunx",
99
- "args": ["pmx-canvas@0.9.3", "--mcp"],
99
+ "args": ["pmx-canvas@0.10.0", "--mcp"],
100
100
  "env": {
101
101
  "PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
102
102
  "PMX_CANVAS_PORT": "14313",
@@ -140,7 +140,7 @@ Amp executes, not only on the user's laptop. Merge this into that workspace's
140
140
  "amp.mcpServers": {
141
141
  "canvas": {
142
142
  "command": "bunx",
143
- "args": ["pmx-canvas@0.9.3", "--mcp"],
143
+ "args": ["pmx-canvas@0.10.0", "--mcp"],
144
144
  "includeTools": ["canvas_*"],
145
145
  "env": {
146
146
  "PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
@@ -174,8 +174,8 @@ The agent must not run this approval itself or move the command to global settin
174
174
  `--mcp-config` to bypass workspace trust. Approval authorizes execution of this MCP
175
175
  command; it is not a new Canvas import consent or permission to mutate arbitrary boards.
176
176
  After approval, reload MCP connections (`reload_mcp` in Amp) and require `connected`.
177
- Discover `canvas` tools with `tool_search`; for the full unfiltered 0.9.3 server there
178
- are 24 tools. If still absent, inspect the server status, command/PATH and target instead
177
+ Discover `canvas` tools with `tool_search`; the full unfiltered 0.10.0 server has 25 tools,
178
+ including `canvas_help`. If still absent, inspect the server status, command/PATH and target instead
179
179
  of reinstalling blindly. See [Amp MCP documentation](https://ampcode.com/docs/customize/mcp).
180
180
 
181
181
  ### Verify native tools, then steering
@@ -97,6 +97,8 @@ function RailButton({
97
97
  }) {
98
98
  const [hint, setHint] = useState<{ left: number; top: number } | null>(null);
99
99
  const show = (e: Event) => {
100
+ // Closing the menu restores focus before it unmounts; that must not reopen the hint.
101
+ if (menuOpen) return;
100
102
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
101
103
  setHint({ left: rect.right + 10, top: rect.top + rect.height / 2 });
102
104
  };
@@ -2,7 +2,6 @@ import { realpathSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { basename, dirname, resolve } from 'node:path';
4
4
  import { createCanvas, canvasState, type CanvasLayout, type CanvasNodeState, type PmxCanvas } from '../server/index.js';
5
- import type { PmxAxSource } from '../server/ax-state.js';
6
5
  import type { ContextReadInput } from '../server/context-reads.js';
7
6
  import { HttpOperationInvoker, LocalOperationInvoker, type OperationInvoker } from '../server/operations/index.js';
8
7
 
@@ -14,13 +13,9 @@ import { HttpOperationInvoker, LocalOperationInvoker, type OperationInvoker } fr
14
13
  // not CanvasAccess. The public SDK PmxCanvas methods are unchanged.
15
14
  type AxStateResult = ReturnType<PmxCanvas['getAxState']>;
16
15
  type AxContextResult = ReturnType<PmxCanvas['getAxContext']>;
17
- type SubmitAxInteractionInput = Parameters<PmxCanvas['submitAxInteraction']>[0];
18
- type SubmitAxInteractionResult = ReturnType<PmxCanvas['submitAxInteraction']>;
19
16
  type GetPendingSteeringResult = ReturnType<PmxCanvas['getPendingSteering']>;
20
17
  type ListElicitationsResult = ReturnType<PmxCanvas['listElicitations']>;
21
18
  type ListModeRequestsResult = ReturnType<PmxCanvas['listModeRequests']>;
22
- type IngestActivityInput = Parameters<PmxCanvas['ingestActivity']>[0];
23
- type IngestActivityResult = ReturnType<PmxCanvas['ingestActivity']>;
24
19
  type GetPolicyResult = ReturnType<PmxCanvas['getPolicy']>;
25
20
  type GetAxTimelineQuery = Parameters<PmxCanvas['getAxTimeline']>[0];
26
21
  type GetAxTimelineResult = ReturnType<PmxCanvas['getAxTimeline']>;
@@ -56,14 +51,9 @@ export interface CanvasAccess {
56
51
  listWorkItems(): Promise<ListWorkItemsResult>;
57
52
  listApprovalGates(): Promise<ListApprovalGatesResult>;
58
53
  listReviewAnnotations(): Promise<ListReviewAnnotationsResult>;
59
- submitAxInteraction(
60
- input: SubmitAxInteractionInput,
61
- options?: { source?: PmxAxSource },
62
- ): Promise<SubmitAxInteractionResult>;
63
54
  getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult>;
64
55
  listElicitations(): Promise<ListElicitationsResult>;
65
56
  listModeRequests(): Promise<ListModeRequestsResult>;
66
- ingestActivity(input: IngestActivityInput, options?: { source?: PmxAxSource }): Promise<IngestActivityResult>;
67
57
  getPolicy(): Promise<GetPolicyResult>;
68
58
  getHistory(): Promise<HistoryResult>;
69
59
  getPinnedNodeIds(board?: string): Promise<string[]>;
@@ -75,7 +65,7 @@ export interface CanvasAccess {
75
65
  // canvas_screenshot (still hand-written — binary payload) is the only webview
76
66
  // tool left on CanvasAccess; it needs the status + screenshot accessors.
77
67
  getAutomationWebViewStatus(): Promise<AutomationWebViewStatus>;
78
- screenshotAutomationWebView(options?: AutomationScreenshotOptions): Promise<Uint8Array>;
68
+ screenshotAutomationWebView(options?: AutomationScreenshotOptions, signal?: AbortSignal): Promise<Uint8Array>;
79
69
  }
80
70
 
81
71
  class LocalCanvasAccess implements CanvasAccess {
@@ -123,13 +113,6 @@ class LocalCanvasAccess implements CanvasAccess {
123
113
  return this.canvas.getAxTimeline(query);
124
114
  }
125
115
 
126
- async submitAxInteraction(
127
- input: SubmitAxInteractionInput,
128
- options?: { source?: PmxAxSource },
129
- ): Promise<SubmitAxInteractionResult> {
130
- return this.canvas.submitAxInteraction(input, { source: options?.source ?? 'mcp' });
131
- }
132
-
133
116
  async getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult> {
134
117
  return this.canvas.getPendingSteering(options);
135
118
  }
@@ -142,10 +125,6 @@ class LocalCanvasAccess implements CanvasAccess {
142
125
  return this.canvas.listModeRequests();
143
126
  }
144
127
 
145
- async ingestActivity(input: IngestActivityInput, options?: { source?: PmxAxSource }): Promise<IngestActivityResult> {
146
- return this.canvas.ingestActivity(input, { source: options?.source ?? 'mcp' });
147
- }
148
-
149
128
  async getPolicy(): Promise<GetPolicyResult> {
150
129
  return this.canvas.getPolicy();
151
130
  }
@@ -299,23 +278,6 @@ class RemoteCanvasAccess implements CanvasAccess {
299
278
  return await this.requestJson<GetAxTimelineResult>('GET', `/api/canvas/ax/timeline${qs}`);
300
279
  }
301
280
 
302
- async submitAxInteraction(
303
- input: SubmitAxInteractionInput,
304
- options?: { source?: PmxAxSource },
305
- ): Promise<SubmitAxInteractionResult> {
306
- // The interaction endpoint returns its structured outcome (ok/code/error) in
307
- // the body for both accepted and rejected interactions, so read the body
308
- // regardless of HTTP status rather than throwing on a denial.
309
- const response = await fetch(`${this.remoteBaseUrl}/api/canvas/ax/interaction`, {
310
- method: 'POST',
311
- headers: { 'Content-Type': 'application/json' },
312
- body: JSON.stringify({ ...input, source: options?.source ?? 'mcp' }),
313
- });
314
- const body = await response.json().catch(() => null);
315
- if (body && typeof body === 'object') return body as SubmitAxInteractionResult;
316
- throw new Error(`Remote canvas interaction failed with HTTP ${response.status}`);
317
- }
318
-
319
281
  async getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult> {
320
282
  const params = new URLSearchParams();
321
283
  if (options?.consumer) params.set('consumer', options.consumer);
@@ -338,13 +300,6 @@ class RemoteCanvasAccess implements CanvasAccess {
338
300
  return r.modeRequests ?? [];
339
301
  }
340
302
 
341
- async ingestActivity(input: IngestActivityInput, options?: { source?: PmxAxSource }): Promise<IngestActivityResult> {
342
- return await this.requestJson<IngestActivityResult>('POST', '/api/canvas/ax/activity', {
343
- ...input,
344
- source: options?.source ?? 'mcp',
345
- });
346
- }
347
-
348
303
  async getPolicy(): Promise<GetPolicyResult> {
349
304
  const r = await this.requestJson<{ policy?: GetPolicyResult }>('GET', '/api/canvas/ax/policy');
350
305
  if (!r.policy) throw new Error('Remote canvas did not return a policy.');
@@ -404,11 +359,15 @@ class RemoteCanvasAccess implements CanvasAccess {
404
359
  return await this.requestJson<AutomationWebViewStatus>('GET', '/api/workbench/webview');
405
360
  }
406
361
 
407
- async screenshotAutomationWebView(options: AutomationScreenshotOptions = {}): Promise<Uint8Array> {
362
+ async screenshotAutomationWebView(
363
+ options: AutomationScreenshotOptions = {},
364
+ signal?: AbortSignal,
365
+ ): Promise<Uint8Array> {
408
366
  const response = await fetch(`${this.remoteBaseUrl}/api/workbench/webview/screenshot`, {
409
367
  method: 'POST',
410
368
  headers: { 'Content-Type': 'application/json' },
411
369
  body: JSON.stringify(options),
370
+ signal,
412
371
  });
413
372
  if (!response.ok) {
414
373
  const text = await response.text();
@@ -0,0 +1,181 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import {
4
+ ErrorCode,
5
+ McpError,
6
+ SubscribeRequestSchema,
7
+ UnsubscribeRequestSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import { canvasState } from '../server/canvas-state.js';
10
+ import { onWorkbenchEvent } from '../server/server.js';
11
+ import type { CanvasAccess } from './canvas-access.js';
12
+ import { ContextUriTemplate } from './context-uri-template.js';
13
+
14
+ const liveResources = new Set(
15
+ [
16
+ 'layout',
17
+ 'boards',
18
+ 'summary',
19
+ 'context',
20
+ 'graph',
21
+ 'spatial-context',
22
+ 'history',
23
+ 'code-graph',
24
+ 'pinned-context',
25
+ 'ax',
26
+ 'ax-context',
27
+ 'ax-timeline',
28
+ 'ax-work',
29
+ 'ax-pending-steering',
30
+ 'ax-delivery',
31
+ ].map((name) => `canvas://${name}`),
32
+ );
33
+ const contextTemplate = new ContextUriTemplate();
34
+ const graphTemplate = new ContextUriTemplate('graph', ['focus', 'depth']);
35
+
36
+ // A context-status-changed read receipt must never trigger another read forever.
37
+ const resourceEvents = new Set([
38
+ 'context-pins-changed',
39
+ 'boards-changed',
40
+ 'knowledge-changed',
41
+ 'ax-state-changed',
42
+ 'ax-event-created',
43
+ 'canvas-layout-update',
44
+ 'canvas-viewport-update',
45
+ 'canvas-camera-move',
46
+ 'canvas-tour-step',
47
+ 'canvas-tour-exit',
48
+ // These events synchronize node data inside server.ts.
49
+ 'workbench-open',
50
+ 'ext-app-open',
51
+ 'ext-app-update',
52
+ 'ext-app-result',
53
+ 'context-cards',
54
+ 'context-usage',
55
+ 'aux-open',
56
+ 'aux-close',
57
+ 'canvas-status',
58
+ 'execution-phase',
59
+ 'canvas-response-start',
60
+ 'canvas-response-delta',
61
+ 'canvas-response-complete',
62
+ ]);
63
+
64
+ function isLiveResource(uri: string): boolean {
65
+ return liveResources.has(uri) || contextTemplate.match(uri) !== null || graphTemplate.match(uri) !== null;
66
+ }
67
+
68
+ /** One subscription set and event source per MCP connection. Notifications are not reads. */
69
+ export function registerResourceSubscriptions(server: McpServer, getCanvas: () => Promise<CanvasAccess>) {
70
+ const subscriptions = new Set<string>();
71
+ let disposeSource: (() => void) | undefined;
72
+ let source: string | null | undefined;
73
+ let closed = false;
74
+
75
+ function notify(): void {
76
+ // Broad invalidation is intentional: context projections span pins, nodes and AX.
77
+ for (const uri of subscriptions) {
78
+ void server.server.sendResourceUpdated({ uri }).catch((error: unknown) => {
79
+ if (!closed) console.error('[mcp] resource notification failed:', error);
80
+ });
81
+ }
82
+ }
83
+
84
+ async function watchRemote(baseUrl: string, signal: AbortSignal): Promise<void> {
85
+ while (!signal.aborted) {
86
+ try {
87
+ const response = await fetch(`${baseUrl}/api/workbench/events`, { signal });
88
+ if (!response.ok || !response.body) throw new Error(`Canvas event stream: HTTP ${response.status}`);
89
+ const reader = response.body.getReader();
90
+ const decoder = new TextDecoder();
91
+ let buffer = '';
92
+ try {
93
+ // Reconnecting may have missed a change. Invalidate subscribed URIs only.
94
+ notify();
95
+ while (!signal.aborted) {
96
+ const { done, value } = await reader.read();
97
+ if (done) break;
98
+ buffer += decoder.decode(value, { stream: true });
99
+ const frames = buffer.split('\n\n');
100
+ buffer = frames.pop() ?? '';
101
+ for (const frame of frames) {
102
+ const event = frame
103
+ .split('\n')
104
+ .find((line) => line.startsWith('event:'))
105
+ ?.slice(6)
106
+ .trim();
107
+ if (event && resourceEvents.has(event)) notify();
108
+ }
109
+ }
110
+ } finally {
111
+ await reader.cancel().catch(() => {});
112
+ reader.releaseLock();
113
+ }
114
+ } catch (error) {
115
+ if (signal.aborted) return;
116
+ console.error('[mcp] canvas event stream disconnected:', error);
117
+ }
118
+ try {
119
+ await delay(1_000, undefined, { signal });
120
+ } catch {
121
+ return;
122
+ }
123
+ }
124
+ }
125
+
126
+ function attach(canvas: CanvasAccess): void {
127
+ if (closed || subscriptions.size === 0 || source === canvas.remoteBaseUrl) return;
128
+ disposeSource?.();
129
+ source = canvas.remoteBaseUrl;
130
+ if (source) {
131
+ const controller = new AbortController();
132
+ disposeSource = () => controller.abort();
133
+ void watchRemote(source, controller.signal);
134
+ } else {
135
+ const disposeState = canvasState.onChange(notify);
136
+ // Library-only writes do not change the active board's state.
137
+ const disposeEvents = onWorkbenchEvent((event) => {
138
+ if (resourceEvents.has(event)) notify();
139
+ });
140
+ disposeSource = () => {
141
+ disposeState();
142
+ disposeEvents();
143
+ };
144
+ }
145
+ }
146
+
147
+ function dispose(): void {
148
+ closed = true;
149
+ subscriptions.clear();
150
+ disposeSource?.();
151
+ disposeSource = undefined;
152
+ source = undefined;
153
+ }
154
+
155
+ server.server.registerCapabilities({ resources: { subscribe: true } });
156
+ server.server.setRequestHandler(SubscribeRequestSchema, async ({ params }, extra) => {
157
+ if (!isLiveResource(params.uri))
158
+ throw new McpError(ErrorCode.InvalidParams, `Resource does not support subscriptions: ${params.uri}`);
159
+ const canvas = await getCanvas();
160
+ extra.signal.throwIfAborted();
161
+ if (closed) throw new McpError(ErrorCode.InternalError, 'Connection closed');
162
+ subscriptions.add(params.uri);
163
+ attach(canvas);
164
+ return {};
165
+ });
166
+ server.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
167
+ subscriptions.delete(params.uri);
168
+ if (subscriptions.size === 0) {
169
+ disposeSource?.();
170
+ disposeSource = undefined;
171
+ source = undefined;
172
+ }
173
+ return {};
174
+ });
175
+ const onclose = server.server.onclose;
176
+ server.server.onclose = () => {
177
+ dispose();
178
+ onclose?.();
179
+ };
180
+ return { attach, dispose };
181
+ }