pmx-canvas 0.7.2 → 0.8.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/THIRD_PARTY_LICENSES.txt +331 -0
- package/.github/extensions/pmx-canvas/extension.mjs +25 -172
- package/.github/extensions/pmx-canvas/mcp-bootstrap.ts +198 -0
- package/.github/extensions/pmx-canvas/runtime.d.mts +41 -0
- package/.github/extensions/pmx-canvas/runtime.json +3 -0
- package/.github/extensions/pmx-canvas/runtime.mjs +194 -0
- package/.github/extensions/pmx-canvas/start-mcp.mjs +51 -0
- package/.github/extensions/pmx-canvas/tools.json +2560 -0
- package/CHANGELOG.md +60 -0
- package/Readme.md +44 -16
- package/dist/canvas/fonts/OFL.txt +93 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/dist/canvas/global.css +844 -710
- package/dist/canvas/index.js +92 -90
- package/dist/canvas/mcp-app.html +7878 -0
- package/dist/canvas/mermaid-entry.js +1 -1
- package/dist/canvas/surface-theme.css +151 -46
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +55 -55
- package/dist/types/cli/commands/record.d.ts +1 -1
- package/dist/types/client/canvas/ChoiceMenu.d.ts +13 -0
- package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
- package/dist/types/client/icons.d.ts +1 -2
- package/dist/types/client/mcp-app/index.d.ts +1 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
- package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
- package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
- package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -0
- package/dist/types/client/state/attention-store.d.ts +0 -5
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/context-status-store.d.ts +53 -0
- package/dist/types/client/state/intent-bridge.d.ts +4 -1
- package/dist/types/client/state/near-pin-store.d.ts +16 -0
- package/dist/types/client/state/presence-store.d.ts +5 -0
- package/dist/types/client/state/session-store.d.ts +33 -3
- package/dist/types/client/state/workbench-transport.d.ts +16 -0
- package/dist/types/client/theme/tokens.d.ts +1 -3
- package/dist/types/client/types.d.ts +9 -0
- package/dist/types/json-render/renderer/index.d.ts +2 -1
- package/dist/types/json-render/server.d.ts +3 -1
- package/dist/types/mcp/app-server.d.ts +3 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -1
- package/dist/types/mcp/canvas-app.d.ts +4 -0
- package/dist/types/mcp/workbench-app.d.ts +4 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/ax-state.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +8 -0
- package/dist/types/server/canvas-geometry.d.ts +25 -0
- package/dist/types/server/canvas-operations.d.ts +1 -11
- package/dist/types/server/canvas-state.d.ts +25 -3
- package/dist/types/server/context-brief.d.ts +8 -1
- package/dist/types/server/context-reads.d.ts +30 -7
- package/dist/types/server/index.d.ts +6 -1
- package/dist/types/server/mcp-app-runtime.d.ts +1 -0
- package/dist/types/server/operations/http.d.ts +2 -0
- package/dist/types/server/operations/invoker.d.ts +4 -2
- package/dist/types/server/operations/ops/nodes.d.ts +0 -6
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/spatial-analysis.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +22 -0
- package/dist/types/shared/agent-presence.d.ts +3 -10
- package/dist/types/shared/ax-surface-protocol.d.ts +2 -0
- package/dist/types/shared/canvas-app.d.ts +54 -0
- package/dist/types/shared/themes.d.ts +1 -1
- package/docs/RELEASE.md +7 -3
- package/docs/agent-plugin.md +123 -0
- package/docs/ax-host-adapter-contract.md +13 -2
- package/docs/ax-state-contract.md +40 -0
- package/docs/cli.md +7 -2
- package/docs/design/AgentContext.dc.html +186 -0
- package/docs/design/Before.dc.html +208 -0
- package/docs/design/BoardPins.dc.html +151 -0
- package/docs/design/Chrome.dc.html +208 -0
- package/docs/design/Context.dc.html +239 -0
- package/docs/design/Flows.dc.html +181 -0
- package/docs/design/Home.dc.html +209 -0
- package/docs/design/HomeGraph.dc.html +211 -0
- package/docs/design/HomeMap.dc.html +117 -0
- package/docs/design/HomeTeam.dc.html +176 -0
- package/docs/design/Light.dc.html +28 -0
- package/docs/design/Main.dc.html +280 -0
- package/docs/design/NearPin.dc.html +260 -0
- package/docs/design/Pane600.dc.html +120 -0
- package/docs/design/Relations.dc.html +297 -0
- package/docs/design/Themes.dc.html +121 -0
- package/docs/design/TokenSystem.dc.html +136 -0
- package/docs/design/Tokens.dc.html +163 -0
- package/docs/design/Types1.dc.html +177 -0
- package/docs/design/Types2.dc.html +226 -0
- package/docs/design/Types3.dc.html +195 -0
- package/docs/design/Uses.dc.html +152 -0
- package/docs/design/Vision.dc.html +116 -0
- package/docs/design/Zoom.dc.html +82 -0
- package/docs/design/canvas.json +274 -0
- package/docs/design.md +214 -0
- package/docs/environment.md +1 -1
- package/docs/http-api.md +80 -15
- package/docs/mcp.md +198 -3
- package/docs/node-types.md +34 -10
- package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
- package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
- package/docs/product-vision-2026-09.md +47 -7
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +1 -0
- package/package.json +10 -4
- package/plugins/pmx-canvas/LICENSE +21 -0
- package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/plugins/pmx-canvas/assets/preview.png +0 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/plugins/pmx-canvas/mcp.json +12 -0
- package/plugins/pmx-canvas/plugin.json +24 -0
- package/plugins/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/start-mcp.mjs +51 -0
- package/plugins/pmx-canvas/tools.json +2560 -0
- package/skills/pmx-canvas/SKILL.md +31 -16
- package/skills/pmx-canvas/evals/evals.json +69 -0
- package/skills/pmx-canvas/references/full-reference.md +14 -10
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +76 -4
- package/skills/pmx-canvas-testing/SKILL.md +54 -4
- package/src/cli/commands/copilot.ts +5 -0
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +8 -3
- package/src/client/App.tsx +6 -1
- package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
- package/src/client/canvas/AnnotationLayer.tsx +1 -1
- package/src/client/canvas/CanvasNode.tsx +208 -220
- package/src/client/canvas/CanvasViewport.tsx +6 -2
- package/src/client/canvas/ChoiceMenu.tsx +122 -0
- package/src/client/canvas/CommandBar.tsx +25 -23
- package/src/client/canvas/CommandPalette.tsx +3 -2
- package/src/client/canvas/ContextMenu.tsx +11 -0
- package/src/client/canvas/EdgeLayer.tsx +1 -1
- package/src/client/canvas/ExportDialog.tsx +33 -6
- package/src/client/canvas/FeedbackDialog.tsx +10 -8
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/Minimap.tsx +12 -7
- package/src/client/canvas/NodeContextMark.tsx +156 -0
- package/src/client/canvas/SessionPanel.tsx +4 -7
- package/src/client/canvas/SessionReceipt.tsx +114 -17
- package/src/client/canvas/ShortcutOverlay.tsx +6 -1
- package/src/client/canvas/SnapshotPanel.tsx +11 -2
- package/src/client/canvas/ToolRail.tsx +58 -108
- package/src/client/canvas/TopBar.tsx +16 -11
- package/src/client/icons.tsx +2 -12
- package/src/client/mcp-app/index.tsx +137 -0
- package/src/client/mcp-app/style.css +10 -0
- package/src/client/nodes/AttachmentNode.tsx +8 -1
- package/src/client/nodes/AxStepControls.tsx +1 -1
- package/src/client/nodes/ContextNode.tsx +9 -9
- package/src/client/nodes/ExtAppFrame.tsx +13 -8
- package/src/client/nodes/FileNode.tsx +8 -5
- package/src/client/nodes/ImageNode.tsx +4 -2
- package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
- package/src/client/nodes/LedgerNode.tsx +2 -2
- package/src/client/nodes/MarkdownNode.tsx +44 -9
- package/src/client/nodes/McpAppNode.tsx +22 -4
- package/src/client/nodes/PromptNode.tsx +5 -17
- package/src/client/nodes/ResponseNode.tsx +3 -15
- package/src/client/nodes/StatusNode.tsx +2 -2
- package/src/client/nodes/StatusSummary.tsx +3 -1
- package/src/client/nodes/TraceNode.tsx +6 -4
- package/src/client/nodes/WebpageNode.tsx +1 -1
- package/src/client/nodes/ax-node-actions.ts +2 -2
- package/src/client/nodes/iframe-document-url.ts +3 -2
- package/src/client/nodes/markdown-sanitize.ts +10 -0
- package/src/client/nodes/surface-url.ts +10 -0
- package/src/client/nodes/use-ax-surface-bridge.ts +4 -2
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/nodes/use-workbench-asset.ts +26 -0
- package/src/client/nodes/viewer-runtime-state.ts +78 -0
- package/src/client/state/attention-bridge.ts +0 -8
- package/src/client/state/attention-store.ts +0 -17
- package/src/client/state/canvas-store.ts +7 -2
- package/src/client/state/context-status-store.ts +66 -0
- package/src/client/state/human-store.ts +2 -0
- package/src/client/state/iframe-mode.ts +5 -0
- package/src/client/state/intent-bridge.ts +24 -6
- package/src/client/state/near-pin-store.ts +53 -0
- package/src/client/state/presence-store.ts +5 -10
- package/src/client/state/session-store.ts +106 -9
- package/src/client/state/sse-bridge.ts +26 -7
- package/src/client/state/theme-override.ts +1 -1
- package/src/client/state/workbench-transport.ts +36 -0
- package/src/client/theme/global.css +844 -710
- package/src/client/theme/surface-theme.css +151 -46
- package/src/client/theme/tokens.ts +7 -11
- package/src/client/types.ts +10 -0
- package/src/json-render/renderer/index.tsx +58 -38
- package/src/json-render/server.ts +79 -15
- package/src/mcp/app-server.ts +64 -0
- package/src/mcp/canvas-access.ts +14 -11
- package/src/mcp/canvas-app.ts +318 -0
- package/src/mcp/server.ts +1 -1
- package/src/mcp/workbench-app.ts +211 -0
- package/src/server/agent-presence.ts +45 -6
- package/src/server/ax-context.ts +1 -1
- package/src/server/ax-state.ts +1 -1
- package/src/server/board-export.ts +5 -2
- package/src/server/canvas-db.ts +46 -12
- package/src/server/canvas-geometry.ts +64 -0
- package/src/server/canvas-operations.ts +6 -24
- package/src/server/canvas-state.ts +105 -15
- package/src/server/canvas-validation.ts +19 -4
- package/src/server/context-brief.ts +39 -2
- package/src/server/context-reads.ts +86 -15
- package/src/server/demo-state.json +31 -31
- package/src/server/html-surface.ts +4 -5
- package/src/server/index.ts +19 -3
- package/src/server/mcp-app-runtime.ts +11 -0
- package/src/server/operations/composites.ts +4 -3
- package/src/server/operations/http.ts +4 -4
- package/src/server/operations/invoker.ts +13 -4
- package/src/server/operations/ops/ax-read.ts +1 -1
- package/src/server/operations/ops/ax-timeline.ts +37 -0
- package/src/server/operations/ops/boards.ts +1 -1
- package/src/server/operations/ops/canvas-wire.ts +3 -6
- package/src/server/operations/ops/intent.ts +31 -2
- package/src/server/operations/ops/json-render.ts +1 -1
- package/src/server/operations/ops/nodes.ts +8 -76
- package/src/server/operations/ops/query.ts +8 -2
- package/src/server/operations/ops/snapshots.ts +4 -1
- package/src/server/operations/ops/viewport.ts +29 -5
- package/src/server/operations/registry.ts +38 -4
- package/src/server/scope-fence.ts +55 -10
- package/src/server/server.ts +86 -6
- package/src/server/spatial-analysis.ts +2 -2
- package/src/server/workboard.ts +6 -1
- package/src/{server → shared}/agent-context.ts +2 -2
- package/src/shared/agent-presence.ts +3 -15
- package/src/shared/ax-surface-protocol.ts +3 -0
- package/src/shared/canvas-app.ts +62 -0
- package/src/shared/themes.ts +5 -5
- package/src/client/canvas/FocusFieldLayer.tsx +0 -116
- package/src/client/canvas/kind-colors.ts +0 -23
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
CallToolResultSchema,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
ListResourcesRequestSchema,
|
|
10
|
+
ListResourceTemplatesRequestSchema,
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
ListToolsResultSchema,
|
|
13
|
+
McpError,
|
|
14
|
+
ReadResourceRequestSchema,
|
|
15
|
+
ResourceUpdatedNotificationSchema,
|
|
16
|
+
ResourceListChangedNotificationSchema,
|
|
17
|
+
ToolListChangedNotificationSchema,
|
|
18
|
+
CreateMessageRequestSchema,
|
|
19
|
+
ElicitRequestSchema,
|
|
20
|
+
ListRootsRequestSchema,
|
|
21
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
22
|
+
import { isAbsolute, dirname, relative, sep } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { canonicalWorkspace, ensurePmxServer, probePmxServer, runtimeCommand, runtimeVersion } from './runtime.mjs';
|
|
26
|
+
|
|
27
|
+
const connector = 'canvas_connect_workspace';
|
|
28
|
+
const packageRoot = canonicalWorkspace(dirname(fileURLToPath(import.meta.url)));
|
|
29
|
+
const catalog = ListToolsResultSchema.parse(JSON.parse(readFileSync(new URL('./tools.json', import.meta.url), 'utf8')));
|
|
30
|
+
const server = new Server(
|
|
31
|
+
{ name: 'pmx-canvas-plugin', version: runtimeVersion },
|
|
32
|
+
{
|
|
33
|
+
capabilities: { tools: { listChanged: true }, resources: { listChanged: true } },
|
|
34
|
+
instructions: `Before using any PMX board tool, call ${connector} with the absolute active project directory. Never use the plugin installation directory as the workspace. Then read skill://pmx-canvas/SKILL.md for workbench guidance. The connection returns a browser workbench URL; the native Copilot canvas is client-specific.`,
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
let upstream: Client | undefined;
|
|
38
|
+
let upstreamTransport: StdioClientTransport | undefined;
|
|
39
|
+
let binding: Promise<{ workspaceRoot: string; serverUrl: string }> | undefined;
|
|
40
|
+
let boundWorkspace: string | undefined;
|
|
41
|
+
let boundServerUrl: string | undefined;
|
|
42
|
+
|
|
43
|
+
async function connected(): Promise<Client> {
|
|
44
|
+
if (!upstream || !boundServerUrl || !boundWorkspace) {
|
|
45
|
+
throw new McpError(ErrorCode.InvalidRequest, `Call ${connector} with your absolute project directory first.`);
|
|
46
|
+
}
|
|
47
|
+
const health = await probePmxServer(boundServerUrl, boundWorkspace);
|
|
48
|
+
if (!health.ok) {
|
|
49
|
+
throw new McpError(ErrorCode.InvalidRequest, `The bound PMX server is unavailable or belongs to another workspace: ${health.error}`);
|
|
50
|
+
}
|
|
51
|
+
return upstream;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function bindWorkspace(root: string) {
|
|
55
|
+
if (!isAbsolute(root)) throw new McpError(ErrorCode.InvalidParams, 'workspaceRoot must be an absolute project directory.');
|
|
56
|
+
root = canonicalWorkspace(root);
|
|
57
|
+
const withinPackage = relative(packageRoot, root);
|
|
58
|
+
if (!withinPackage || (withinPackage !== '..' && !withinPackage.startsWith(`..${sep}`) && !isAbsolute(withinPackage))) {
|
|
59
|
+
throw new McpError(ErrorCode.InvalidParams, 'The plugin installation directory is not a project workspace.');
|
|
60
|
+
}
|
|
61
|
+
if (boundWorkspace && boundWorkspace !== root) {
|
|
62
|
+
throw new McpError(ErrorCode.InvalidParams, 'This MCP session is already bound to another workspace. Start a new session.');
|
|
63
|
+
}
|
|
64
|
+
if (binding) {
|
|
65
|
+
const target = await binding;
|
|
66
|
+
await connected();
|
|
67
|
+
return target;
|
|
68
|
+
}
|
|
69
|
+
boundWorkspace = root;
|
|
70
|
+
binding = (async () => {
|
|
71
|
+
const target = await ensurePmxServer(root);
|
|
72
|
+
if (!target.ok || !target.baseUrl || !target.health) throw new Error(target.error ?? 'PMX Canvas is unavailable.');
|
|
73
|
+
if (closing) throw new Error('MCP session closed while connecting the workspace.');
|
|
74
|
+
const runtime = runtimeCommand();
|
|
75
|
+
const transport = new StdioClientTransport({
|
|
76
|
+
command: runtime.command,
|
|
77
|
+
args: [...runtime.args, '--mcp'],
|
|
78
|
+
cwd: root,
|
|
79
|
+
env: {
|
|
80
|
+
...Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined)),
|
|
81
|
+
PMX_CANVAS_URL: target.baseUrl,
|
|
82
|
+
PMX_CANVAS_WORKSPACE_ROOT: root,
|
|
83
|
+
PMX_CANVAS_DISABLE_BROWSER_OPEN: '1',
|
|
84
|
+
},
|
|
85
|
+
stderr: 'inherit',
|
|
86
|
+
});
|
|
87
|
+
const capabilities = server.getClientCapabilities() ?? {};
|
|
88
|
+
const client = new Client(
|
|
89
|
+
{ name: 'pmx-canvas-plugin', version: runtimeVersion },
|
|
90
|
+
{ capabilities },
|
|
91
|
+
);
|
|
92
|
+
if (capabilities.roots) client.setRequestHandler(ListRootsRequestSchema, () => server.listRoots());
|
|
93
|
+
if (capabilities.sampling) {
|
|
94
|
+
client.setRequestHandler(CreateMessageRequestSchema, (request) => server.createMessage(request.params));
|
|
95
|
+
}
|
|
96
|
+
if (capabilities.elicitation) {
|
|
97
|
+
client.setRequestHandler(ElicitRequestSchema, (request) => server.elicitInput(request.params));
|
|
98
|
+
}
|
|
99
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, () => server.sendToolListChanged());
|
|
100
|
+
client.setNotificationHandler(ResourceListChangedNotificationSchema, () => server.sendResourceListChanged());
|
|
101
|
+
client.setNotificationHandler(ResourceUpdatedNotificationSchema, (notification) => server.notification(notification));
|
|
102
|
+
try {
|
|
103
|
+
await client.connect(transport);
|
|
104
|
+
if (closing) throw new Error('MCP session closed while connecting the workspace.');
|
|
105
|
+
upstream = client;
|
|
106
|
+
upstreamTransport = transport;
|
|
107
|
+
boundServerUrl = target.baseUrl;
|
|
108
|
+
await server.sendToolListChanged();
|
|
109
|
+
await server.sendResourceListChanged();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
upstream = undefined;
|
|
112
|
+
upstreamTransport = undefined;
|
|
113
|
+
boundServerUrl = undefined;
|
|
114
|
+
await client.close();
|
|
115
|
+
await transport.close();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
return { workspaceRoot: root, serverUrl: target.baseUrl };
|
|
119
|
+
})();
|
|
120
|
+
try {
|
|
121
|
+
return await binding;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
binding = undefined;
|
|
124
|
+
boundWorkspace = undefined;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Some hosts cache the initial tool set even after tools/list_changed; board tools stay locked until binding.
|
|
130
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => ({
|
|
131
|
+
tools: [
|
|
132
|
+
{
|
|
133
|
+
name: connector,
|
|
134
|
+
description: 'Connect PMX Canvas to the user-selected project BEFORE using board tools. Pass the absolute active project directory, never the plugin installation folder. Returns the browser workbench URL and enables the PMX tools for this MCP session.',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: { workspaceRoot: { type: 'string', description: 'Absolute directory of the intended project.' } },
|
|
138
|
+
required: ['workspaceRoot'],
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
...(upstream ? (await (await connected()).listTools(request.params)).tools : catalog.tools),
|
|
143
|
+
],
|
|
144
|
+
}));
|
|
145
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
146
|
+
if (request.params.name !== connector) {
|
|
147
|
+
return await (await connected()).request(request, CallToolResultSchema, { signal: extra.signal });
|
|
148
|
+
}
|
|
149
|
+
const root = request.params.arguments?.workspaceRoot;
|
|
150
|
+
if (typeof root !== 'string') throw new McpError(ErrorCode.InvalidParams, 'workspaceRoot is required.');
|
|
151
|
+
try {
|
|
152
|
+
const target = await bindWorkspace(root);
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: 'text', text: JSON.stringify({ ...target, workbenchUrl: `${target.serverUrl}/workbench` }) }],
|
|
155
|
+
structuredContent: { ...target, workbenchUrl: `${target.serverUrl}/workbench` },
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
+
process.stderr.write(`[pmx-canvas] Workspace connection failed: ${message}\n`);
|
|
160
|
+
return { isError: true, content: [{ type: 'text', text: message }] };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (request) =>
|
|
164
|
+
upstream ? await (await connected()).listResources(request.params) : { resources: [] });
|
|
165
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) =>
|
|
166
|
+
upstream ? await (await connected()).listResourceTemplates(request.params) : { resourceTemplates: [] });
|
|
167
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => await (await connected()).readResource(request.params));
|
|
168
|
+
|
|
169
|
+
server.oninitialized = async () => {
|
|
170
|
+
try {
|
|
171
|
+
const configured = process.env.PMX_CANVAS_WORKSPACE_ROOT;
|
|
172
|
+
if (configured) {
|
|
173
|
+
await bindWorkspace(configured);
|
|
174
|
+
} else if (server.getClientCapabilities()?.roots) {
|
|
175
|
+
const { roots } = await server.listRoots();
|
|
176
|
+
if (roots.length === 1 && roots[0].uri.startsWith('file:')) await bindWorkspace(fileURLToPath(roots[0].uri));
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
process.stderr.write(`[pmx-canvas] Automatic workspace connection failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
let closing = false;
|
|
183
|
+
async function shutdown() {
|
|
184
|
+
if (closing) return;
|
|
185
|
+
closing = true;
|
|
186
|
+
try {
|
|
187
|
+
await upstream?.close();
|
|
188
|
+
await upstreamTransport?.close();
|
|
189
|
+
await server.close();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
process.stderr.write(`[pmx-canvas] MCP shutdown failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
server.onclose = () => void shutdown();
|
|
196
|
+
process.once('SIGTERM', () => void shutdown());
|
|
197
|
+
process.once('SIGINT', () => void shutdown());
|
|
198
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface RuntimeInput {
|
|
2
|
+
serverUrl?: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
autoStart?: boolean;
|
|
5
|
+
allowWorkspaceMismatch?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RuntimeCommand {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ServerProbe {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
baseUrl: string | null;
|
|
16
|
+
health: { ok: boolean; workspace: string } | null;
|
|
17
|
+
workspaceOk: boolean;
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const runtimeVersion: string;
|
|
22
|
+
export function canonicalWorkspace(path: string): string;
|
|
23
|
+
export function runtimeCommand(): RuntimeCommand;
|
|
24
|
+
export function preferredPort(input?: RuntimeInput): number;
|
|
25
|
+
export function explicitServerUrl(input?: RuntimeInput): string | null;
|
|
26
|
+
export function probePmxServer(
|
|
27
|
+
baseUrl: string,
|
|
28
|
+
workspaceRoot: string,
|
|
29
|
+
input?: RuntimeInput,
|
|
30
|
+
fetchImpl?: typeof fetch,
|
|
31
|
+
): Promise<ServerProbe>;
|
|
32
|
+
export function findPmxServer(
|
|
33
|
+
workspaceRoot: string,
|
|
34
|
+
input?: RuntimeInput,
|
|
35
|
+
fetchImpl?: typeof fetch,
|
|
36
|
+
): Promise<ServerProbe>;
|
|
37
|
+
export function ensurePmxServer(
|
|
38
|
+
workspaceRoot: string,
|
|
39
|
+
input?: RuntimeInput,
|
|
40
|
+
command?: RuntimeCommand,
|
|
41
|
+
): Promise<ServerProbe>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { closeSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const runtimeVersion = JSON.parse(readFileSync(new URL("./runtime.json", import.meta.url), "utf8")).version;
|
|
7
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(runtimeVersion)) {
|
|
8
|
+
throw new Error("PMX Canvas runtime.json must contain an exact package version.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function canonicalWorkspace(path) {
|
|
12
|
+
// .native expands Windows 8.3 short names (RUNNER~1), so every caller compares one spelling.
|
|
13
|
+
const directory = realpathSync.native(resolve(path));
|
|
14
|
+
if (!statSync(directory).isDirectory()) throw new Error("PMX Canvas workspace must be a directory.");
|
|
15
|
+
return directory;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function runtimeCommand() {
|
|
19
|
+
return { command: "bun", args: ["x", "--bun", `pmx-canvas@${runtimeVersion}`] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function preferredPort(input = {}) {
|
|
23
|
+
const port = Number(input.port ?? process.env.PMX_CANVAS_PORT ?? process.env.PMX_WEB_CANVAS_PORT ?? 4313);
|
|
24
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PMX Canvas port must be between 1 and 65535.");
|
|
25
|
+
return port;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function explicitServerUrl(input = {}) {
|
|
29
|
+
const value = input.serverUrl ?? process.env.PMX_CANVAS_URL;
|
|
30
|
+
if (!value) return null;
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (!["http:", "https:"].includes(url.protocol)) throw new Error("PMX Canvas server URL must use HTTP or HTTPS.");
|
|
33
|
+
url.search = "";
|
|
34
|
+
url.hash = "";
|
|
35
|
+
return url.toString().replace(/\/$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const NO_SERVER = "No matching PMX Canvas server is running.";
|
|
39
|
+
|
|
40
|
+
export async function probePmxServer(baseUrl, workspaceRoot, input = {}, fetchImpl = fetch) {
|
|
41
|
+
let response;
|
|
42
|
+
try {
|
|
43
|
+
response = await fetchImpl(`${baseUrl}/health`, { signal: AbortSignal.timeout(500) });
|
|
44
|
+
} catch {
|
|
45
|
+
// Nothing answered (refused, timed out). The raw fetch error ("fetch failed") is not
|
|
46
|
+
// actionable; a reachable server's own failure below is reported verbatim.
|
|
47
|
+
return { ok: false, baseUrl, health: null, workspaceOk: false, error: NO_SERVER };
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
if (!response.ok) throw new Error(`Health check returned HTTP ${response.status}.`);
|
|
51
|
+
const health = await response.json();
|
|
52
|
+
const workspaceOk = typeof health.workspace === "string" &&
|
|
53
|
+
(input.allowWorkspaceMismatch === true || canonicalWorkspace(health.workspace) === canonicalWorkspace(workspaceRoot));
|
|
54
|
+
return {
|
|
55
|
+
ok: health.ok === true && workspaceOk,
|
|
56
|
+
baseUrl,
|
|
57
|
+
health,
|
|
58
|
+
workspaceOk,
|
|
59
|
+
error: workspaceOk ? null : `PMX server belongs to ${health.workspace ?? "an unknown workspace"}.`,
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { ok: false, baseUrl, health: null, workspaceOk: false, error: error.message };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function daemonPorts(workspaceRoot) {
|
|
67
|
+
try {
|
|
68
|
+
return readdirSync(resolve(workspaceRoot, ".pmx-canvas"))
|
|
69
|
+
.map((name) => /^daemon-(\d+)\.pid$/.exec(name))
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((match) => Number(match[1]))
|
|
72
|
+
.filter((port) => port > 0 && port <= 65535)
|
|
73
|
+
.sort((a, b) => a - b);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code === "ENOENT") return [];
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function findPmxServer(workspaceRoot, input = {}, fetchImpl = fetch) {
|
|
81
|
+
const explicit = explicitServerUrl(input);
|
|
82
|
+
if (explicit) return await probePmxServer(explicit, workspaceRoot, input, fetchImpl);
|
|
83
|
+
const port = preferredPort(input);
|
|
84
|
+
// Daemon pid filenames locate arbitrary ports; health, not the pid file, proves ownership.
|
|
85
|
+
const ports = [...new Set([...daemonPorts(workspaceRoot), port, ...Array.from({ length: 9 }, (_, i) => 4313 + i)])];
|
|
86
|
+
const results = await Promise.all(ports.map((candidate) =>
|
|
87
|
+
probePmxServer(`http://127.0.0.1:${candidate}`, workspaceRoot, input, fetchImpl)));
|
|
88
|
+
return results.find((result) => result.ok) ??
|
|
89
|
+
{ ok: false, baseUrl: null, health: null, workspaceOk: false, error: NO_SERVER };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function portAvailable(port) {
|
|
93
|
+
return await new Promise((resolveAvailable) => {
|
|
94
|
+
const server = createServer();
|
|
95
|
+
server.once("error", () => resolveAvailable(false));
|
|
96
|
+
server.listen(port, "127.0.0.1", () => server.close(() => resolveAvailable(true)));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function ensurePmxServer(workspaceRoot, input = {}, command = runtimeCommand()) {
|
|
101
|
+
workspaceRoot = canonicalWorkspace(workspaceRoot);
|
|
102
|
+
const existing = await findPmxServer(workspaceRoot, input);
|
|
103
|
+
if (existing.ok || input.autoStart === false) return existing;
|
|
104
|
+
if (explicitServerUrl(input)) throw new Error(existing.error);
|
|
105
|
+
|
|
106
|
+
const directory = resolve(workspaceRoot, ".pmx-canvas");
|
|
107
|
+
const lock = resolve(directory, "plugin-start.lock");
|
|
108
|
+
mkdirSync(directory, { recursive: true });
|
|
109
|
+
const deadline = Date.now() + 75_000;
|
|
110
|
+
while (Date.now() < deadline) {
|
|
111
|
+
let fd;
|
|
112
|
+
try {
|
|
113
|
+
fd = openSync(lock, "wx");
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.code !== "EEXIST") throw error;
|
|
116
|
+
try {
|
|
117
|
+
const pid = Number(readFileSync(lock, "utf8"));
|
|
118
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
} catch (signalError) {
|
|
122
|
+
if (signalError.code !== "ESRCH") throw signalError;
|
|
123
|
+
rmSync(lock);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
} else if (Date.now() - statSync(lock).mtimeMs > 5_000) {
|
|
127
|
+
rmSync(lock);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
} catch (lockError) {
|
|
131
|
+
if (lockError.code === "ENOENT") continue;
|
|
132
|
+
throw lockError;
|
|
133
|
+
}
|
|
134
|
+
const server = await findPmxServer(workspaceRoot, input);
|
|
135
|
+
if (server.ok) return server;
|
|
136
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
try {
|
|
141
|
+
writeFileSync(fd, String(process.pid));
|
|
142
|
+
} finally {
|
|
143
|
+
closeSync(fd);
|
|
144
|
+
}
|
|
145
|
+
const server = await findPmxServer(workspaceRoot, input);
|
|
146
|
+
if (server.ok) return server;
|
|
147
|
+
return await startPmxServer(workspaceRoot, input, command);
|
|
148
|
+
} finally {
|
|
149
|
+
rmSync(lock, { force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw new Error("Timed out waiting for another PMX Canvas plugin startup.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function startPmxServer(workspaceRoot, input, command) {
|
|
156
|
+
const firstPort = preferredPort(input);
|
|
157
|
+
let port;
|
|
158
|
+
for (let candidate = firstPort; candidate < Math.min(firstPort + 20, 65536); candidate++) {
|
|
159
|
+
// Another starter may have claimed this port since discovery.
|
|
160
|
+
const probe = await probePmxServer(`http://127.0.0.1:${candidate}`, workspaceRoot);
|
|
161
|
+
if (probe.ok) return probe;
|
|
162
|
+
if (await portAvailable(candidate)) {
|
|
163
|
+
port = candidate;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!port) throw new Error(`No free PMX Canvas port near ${firstPort}.`);
|
|
168
|
+
|
|
169
|
+
// serve --daemon owns the exclusive spawn lock and outlives either client.
|
|
170
|
+
let startupError;
|
|
171
|
+
try {
|
|
172
|
+
await new Promise((resolveStart, rejectStart) => execFile(command.command, [
|
|
173
|
+
...command.args, "serve", "--daemon", "--no-open", `--port=${port}`,
|
|
174
|
+
], {
|
|
175
|
+
cwd: workspaceRoot,
|
|
176
|
+
env: { ...process.env, PMX_CANVAS_WORKSPACE_ROOT: workspaceRoot, PMX_CANVAS_DISABLE_BROWSER_OPEN: "1" },
|
|
177
|
+
timeout: 60_000,
|
|
178
|
+
maxBuffer: 1024 * 1024,
|
|
179
|
+
}, (error, stdout, stderr) => {
|
|
180
|
+
if (error) rejectStart(new Error(`PMX Canvas startup failed: ${stderr || stdout || error.message}`));
|
|
181
|
+
else resolveStart();
|
|
182
|
+
}));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
startupError = error;
|
|
185
|
+
}
|
|
186
|
+
// A concurrent starter can win the lock. Only verified health makes that a success.
|
|
187
|
+
for (let attempt = 0; attempt < (startupError ? 40 : 1); attempt++) {
|
|
188
|
+
const result = await findPmxServer(workspaceRoot, input);
|
|
189
|
+
if (result.ok) return result;
|
|
190
|
+
if (startupError?.message.includes("ENOENT")) break;
|
|
191
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
192
|
+
}
|
|
193
|
+
throw startupError ?? new Error("PMX Canvas started but its workspace health check failed.");
|
|
194
|
+
}
|