zuiku-mcp 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -6,11 +6,12 @@
6
6
 
7
7
  ## What it exposes
8
8
 
9
- - `zuiku.open`: open Markdown or Mermaid content as a localhost editor session
9
+ - `zuiku.version`: report the running package/server version and update hints
10
+ - `zuiku.open`: open a markdown file, canonical Zuiku Markdown, or Mermaid-only content as a localhost editor session
10
11
  - `zuiku.read`: read current Markdown and hash before updating
11
- - `zuiku.apply`: write Markdown back with hash-based conflict protection
12
+ - `zuiku.apply`: write Mermaid-only or canonical Markdown back as normalized Zuiku Markdown with hash-based conflict protection
12
13
  - `zuiku.export`: export Markdown, PNG, or SVG
13
- - `zuiku.ddl_to_er`: turn a DDL string into ER diagram payloads
14
+ - `zuiku.ddl_to_er`: turn an upstream-acquired DDL string into ER diagram payloads
14
15
 
15
16
  ## Entry command
16
17
 
@@ -18,6 +19,32 @@
18
19
  npx -y zuiku-mcp
19
20
  ```
20
21
 
22
+ ## Version and update
23
+
24
+ Check the currently installed package version:
25
+
26
+ ```bash
27
+ npx -y zuiku-mcp --version
28
+ ```
29
+
30
+ Check the latest published npm version:
31
+
32
+ ```bash
33
+ npm view zuiku-mcp version
34
+ ```
35
+
36
+ Run the latest package one-shot:
37
+
38
+ ```bash
39
+ npx -y zuiku-mcp@latest
40
+ ```
41
+
42
+ Update a global install:
43
+
44
+ ```bash
45
+ npm install -g zuiku-mcp@latest
46
+ ```
47
+
21
48
  ## Typical usage
22
49
 
23
50
  - Codex CLI / Claude Code / Gemini CLI: register it as an MCP server command
@@ -35,7 +62,11 @@ npx -y zuiku-mcp
35
62
  ## Notes
36
63
 
37
64
  - Requires Node.js 20+
65
+ - `zuiku.version` can be called from MCP clients to confirm the running build and show update hints
38
66
  - For chat clients, `ZUIKU_MCP_OPEN_BROWSER=0` is usually the better default
67
+ - `zuiku.apply(openAfterApply=true)` can request opening or reusing the local editor session after save
68
+ - Discovery guides are exposed over MCP resources/prompts so AI clients can avoid external lookup
69
+ - After updating, restart the MCP client or IDE session so the new package is launched
39
70
  - During preview, CLI and manual MCP setup are the source of truth; optional IDE extension listings can be added on top later
40
71
  - Install guide: `https://zuiku.dev/install`
41
72
  - Docs: `https://zuiku.dev/docs`
package/bin/zuiku-mcp.mjs CHANGED
@@ -1,4 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const packageJson = require("../package.json");
6
+
7
+ if (process.argv.includes("--version") || process.argv.includes("-v") || process.argv[2] === "version") {
8
+ process.stdout.write(`${packageJson.version}\n`);
9
+ process.exit(0);
10
+ }
11
+
2
12
  const entryPath = new URL("../dist/zuiku-mcp-server.mjs", import.meta.url);
3
13
 
4
14
  try {
@@ -5,9 +5,19 @@ import path4 from "node:path";
5
5
 
6
6
  // ../../apps/web/src/lib/mcpContract.ts
7
7
  var MCP_TOOL_DEFINITIONS = [
8
+ {
9
+ name: "zuiku.version",
10
+ description: "Return the current zuiku-mcp package/server version and update hints. Use this to confirm which MCP build is currently running.",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {},
14
+ required: [],
15
+ additionalProperties: false
16
+ }
17
+ },
8
18
  {
9
19
  name: "zuiku.open",
10
- description: "Open a markdown file or markdown text in the local editor session",
20
+ description: "Open a markdown file, canonical Zuiku Markdown, or Mermaid-only text in a local editor session. Mermaid-only input is normalized on save, and the result includes editorUrl.",
11
21
  inputSchema: {
12
22
  type: "object",
13
23
  properties: {
@@ -22,7 +32,7 @@ var MCP_TOOL_DEFINITIONS = [
22
32
  },
23
33
  {
24
34
  name: "zuiku.read",
25
- description: "Read markdown diagram source",
35
+ description: "Read the current markdown source and hash before updating an existing Zuiku file.",
26
36
  inputSchema: {
27
37
  type: "object",
28
38
  properties: {
@@ -34,7 +44,7 @@ var MCP_TOOL_DEFINITIONS = [
34
44
  },
35
45
  {
36
46
  name: "zuiku.apply",
37
- description: "Apply markdown with hash conflict protection",
47
+ description: "Apply Mermaid-only or canonical Zuiku Markdown with hash conflict protection. The saved file is normalized to canonical Zuiku Markdown, and openAfterApply can open or reuse the local editor session.",
38
48
  inputSchema: {
39
49
  type: "object",
40
50
  properties: {
@@ -42,7 +52,8 @@ var MCP_TOOL_DEFINITIONS = [
42
52
  markdown: { type: "string" },
43
53
  expectedHash: { type: "string" },
44
54
  mode: { type: "string", enum: ["replace", "current-page"] },
45
- targetDiagramId: { type: "string" }
55
+ targetDiagramId: { type: "string" },
56
+ openAfterApply: { type: "boolean" }
46
57
  },
47
58
  required: ["path", "markdown", "expectedHash"],
48
59
  additionalProperties: false
@@ -50,7 +61,7 @@ var MCP_TOOL_DEFINITIONS = [
50
61
  },
51
62
  {
52
63
  name: "zuiku.export",
53
- description: "Export markdown / svg / png from a markdown source file",
64
+ description: "Export markdown / svg / png from a saved Zuiku markdown source file.",
54
65
  inputSchema: {
55
66
  type: "object",
56
67
  properties: {
@@ -64,7 +75,7 @@ var MCP_TOOL_DEFINITIONS = [
64
75
  },
65
76
  {
66
77
  name: "zuiku.ddl_to_er",
67
- description: "Convert SQL DDL to Mermaid erDiagram + Zuiku markdown/project payload",
78
+ description: "Convert upstream-acquired SQL DDL to Mermaid erDiagram plus canonical Zuiku markdown/project payload. Zuiku does not fetch the DDL or connect to the database.",
68
79
  inputSchema: {
69
80
  type: "object",
70
81
  properties: {
@@ -98,6 +109,18 @@ function validateMcpToolRequest(input) {
98
109
  return invalidRequest("args must be an object");
99
110
  }
100
111
  const args = input.args;
112
+ if (tool === "zuiku.version") {
113
+ if (!hasOnlyKeys(args, [])) {
114
+ return invalidRequest("zuiku.version.args contains unknown keys");
115
+ }
116
+ return {
117
+ ok: true,
118
+ value: {
119
+ tool,
120
+ args: {}
121
+ }
122
+ };
123
+ }
101
124
  if (tool === "zuiku.open") {
102
125
  if (!hasOnlyKeys(args, ["path", "markdown", "title", "openInBrowser"])) {
103
126
  return invalidRequest("zuiku.open.args contains unknown keys");
@@ -151,7 +174,7 @@ function validateMcpToolRequest(input) {
151
174
  return { ok: true, value: { tool, args: { path: path5 } } };
152
175
  }
153
176
  if (tool === "zuiku.apply") {
154
- if (!hasOnlyKeys(args, ["path", "markdown", "expectedHash", "hashAlgorithm", "mode", "targetDiagramId"])) {
177
+ if (!hasOnlyKeys(args, ["path", "markdown", "expectedHash", "hashAlgorithm", "mode", "targetDiagramId", "openAfterApply"])) {
155
178
  return invalidRequest("zuiku.apply.args contains unknown keys");
156
179
  }
157
180
  const path5 = asNonEmptyString(args.path);
@@ -160,6 +183,7 @@ function validateMcpToolRequest(input) {
160
183
  const hashAlgorithm = asString(args.hashAlgorithm);
161
184
  const mode = asString(args.mode);
162
185
  const targetDiagramId = asString(args.targetDiagramId);
186
+ const openAfterApply = asBoolean(args.openAfterApply);
163
187
  if (!path5) {
164
188
  return invalidRequest("zuiku.apply.args.path is required");
165
189
  }
@@ -193,6 +217,9 @@ function validateMcpToolRequest(input) {
193
217
  if (mode === "current-page" && !targetDiagramId) {
194
218
  return invalidRequest("zuiku.apply.args.targetDiagramId is required for current-page mode");
195
219
  }
220
+ if (args.openAfterApply !== void 0 && typeof openAfterApply !== "boolean") {
221
+ return invalidRequest("zuiku.apply.args.openAfterApply must be boolean");
222
+ }
196
223
  const normalizedHashAlgorithm = hashAlgorithm === "sha256" ? "sha256" : void 0;
197
224
  const normalizedMode = mode === "replace" || mode === "current-page" ? mode : void 0;
198
225
  return {
@@ -205,7 +232,8 @@ function validateMcpToolRequest(input) {
205
232
  expectedHash,
206
233
  ...normalizedHashAlgorithm ? { hashAlgorithm: normalizedHashAlgorithm } : {},
207
234
  ...normalizedMode ? { mode: normalizedMode } : {},
208
- ...targetDiagramId ? { targetDiagramId } : {}
235
+ ...targetDiagramId ? { targetDiagramId } : {},
236
+ ...typeof openAfterApply === "boolean" ? { openAfterApply } : {}
209
237
  }
210
238
  }
211
239
  };
@@ -326,6 +354,158 @@ function isDiagramIdLike(value) {
326
354
  return /^[A-Za-z0-9_:\-\u3040-\u30ff\u4e00-\u9faf]+$/.test(value);
327
355
  }
328
356
 
357
+ // ../../apps/web/src/lib/mcpGuidance.ts
358
+ var GUIDE_TEXT_BY_TOPIC = {
359
+ "format-guide": [
360
+ "# Zuiku format guide",
361
+ "",
362
+ "- \u5165\u529B\u306F `Zuiku Markdown` \u307E\u305F\u306F `Mermaid\u5358\u4F53` \u3092\u53D7\u3051\u3089\u308C\u308B\u3002",
363
+ "- \u4FDD\u5B58\u3068\u5171\u6709\u306E\u6B63\u898F\u5F62\u306F `H1 -> mermaid -> layout-json` \u306E Markdown 1\u30D5\u30A1\u30A4\u30EB\u3002",
364
+ "- `zuiku.open` \u306F editor session \u3092\u4F5C\u308A\u3001`editorUrl` \u3092\u8FD4\u3059\u3002",
365
+ "- `zuiku.apply` \u306F\u4FDD\u5B58\u6642\u306B\u6B63\u898F\u5F62\u3078\u5BC4\u305B\u308B\u3002",
366
+ "- \u65E2\u5B58\u66F4\u65B0\u306F `zuiku.read -> zuiku.apply(expectedHash=...)`\u3002",
367
+ "- `openAfterApply=true` \u3067 editor session \u306E\u518D\u5229\u7528\u307E\u305F\u306F\u65B0\u898F open \u3092\u8981\u6C42\u3067\u304D\u308B\u3002",
368
+ "- DDL \u53D6\u5F97\u3084 DB \u63A5\u7D9A\u306F Zuiku \u306E\u8CAC\u52D9\u3067\u306F\u306A\u3044\u3002"
369
+ ].join("\n"),
370
+ "flowchart-example": [
371
+ "# Zuiku flowchart example",
372
+ "",
373
+ "```markdown",
374
+ "# Sample Flow",
375
+ "",
376
+ "```mermaid",
377
+ "%% diagramId: flow-main",
378
+ "flowchart LR",
379
+ " A[Start] --> B{Check}",
380
+ " B -- yes --> C[Run]",
381
+ " B -- no --> D[End]",
382
+ " C --> D",
383
+ "```",
384
+ "",
385
+ "```layout-json",
386
+ "{",
387
+ ' "version": 1,',
388
+ ' "diagramId": "flow-main",',
389
+ ' "type": "flowchart",',
390
+ ' "nodes": {',
391
+ ' "A": { "x": 80, "y": 120, "w": 140, "h": 56 },',
392
+ ' "B": { "x": 280, "y": 120, "w": 140, "h": 56 },',
393
+ ' "C": { "x": 500, "y": 70, "w": 140, "h": 56 },',
394
+ ' "D": { "x": 500, "y": 170, "w": 140, "h": 56 }',
395
+ " },",
396
+ ' "edges": {',
397
+ ' "e1": { "from": "A", "to": "B", "points": [] },',
398
+ ' "e2": { "from": "B", "to": "C", "points": [] },',
399
+ ' "e3": { "from": "B", "to": "D", "points": [] },',
400
+ ' "e4": { "from": "C", "to": "D", "points": [] }',
401
+ " }",
402
+ "}",
403
+ "```",
404
+ "```"
405
+ ].join("\n"),
406
+ "er-from-ddl-guide": [
407
+ "# ER from DDL guide",
408
+ "",
409
+ "- \u307E\u305A\u4E0A\u6D41AI\u304C DB tool / \u5225 MCP / \u8A31\u53EF\u5236 install \u3067 DDL \u3092\u53D6\u5F97\u3059\u308B\u3002",
410
+ "- Zuiku \u306F DB \u3078\u63A5\u7D9A\u3057\u306A\u3044\u3002`zuiku.ddl_to_er(ddl, ...)` \u3060\u3051\u3092\u62C5\u5F53\u3059\u308B\u3002",
411
+ "- \u65B0\u898F\u306A\u3089 `zuiku.open(markdown, openInBrowser=true)`\u3002",
412
+ "- \u65E2\u5B58\u66F4\u65B0\u306A\u3089 `zuiku.read -> zuiku.apply(mode, expectedHash, openAfterApply=true)`\u3002",
413
+ "- `current-page` \u306F `targetDiagramId` \u5FC5\u9808\u3002",
414
+ "- \u4FDD\u5B58\u7D50\u679C\u306F Zuiku Markdown \u6B63\u898F\u5F62\u3078\u5BC4\u308B\u3002"
415
+ ].join("\n"),
416
+ "edit-existing-guide": [
417
+ "# Edit existing guide",
418
+ "",
419
+ "- \u65E2\u5B58\u56F3\u66F4\u65B0\u524D\u306F\u5FC5\u305A `zuiku.read(path)`\u3002",
420
+ "- AI \u306F\u65E2\u5B58 diagramId \u3068\u56F3\u7A2E\u3092\u4FDD\u3064\u3002",
421
+ "- \u5165\u529B\u306F Zuiku Markdown \u3067\u3082 Mermaid\u5358\u4F53\u3067\u3082\u3088\u3044\u3002",
422
+ "- `zuiku.apply(path, markdown, expectedHash, mode, targetDiagramId?, openAfterApply=true)` \u3092\u4F7F\u3046\u3002",
423
+ "- `replace` \u306F file \u5168\u4F53\u66F4\u65B0\u3001`current-page` \u306F diagram \u5358\u4F4D\u66F4\u65B0\u3002",
424
+ "- \u4FDD\u5B58\u6642\u306F\u6B63\u898F Zuiku Markdown \u306B\u6B63\u898F\u5316\u3055\u308C\u308B\u3002"
425
+ ].join("\n")
426
+ };
427
+ var GUIDE_RESOURCE_DEFINITIONS = [
428
+ {
429
+ topic: "format-guide",
430
+ uri: "zuiku://guide/format-guide",
431
+ name: "format-guide",
432
+ description: "Short contract for Zuiku input, save, open, and DDL responsibility boundaries."
433
+ },
434
+ {
435
+ topic: "flowchart-example",
436
+ uri: "zuiku://guide/flowchart-example",
437
+ name: "flowchart-example",
438
+ description: "Minimal flowchart example in canonical Zuiku Markdown."
439
+ },
440
+ {
441
+ topic: "er-from-ddl-guide",
442
+ uri: "zuiku://guide/er-from-ddl-guide",
443
+ name: "er-from-ddl-guide",
444
+ description: "How upstream AI should fetch DDL and hand only the DDL string to Zuiku."
445
+ },
446
+ {
447
+ topic: "edit-existing-guide",
448
+ uri: "zuiku://guide/edit-existing-guide",
449
+ name: "edit-existing-guide",
450
+ description: "Read-before-apply workflow for updating an existing Zuiku file."
451
+ }
452
+ ];
453
+ var MCP_STATIC_RESOURCES = GUIDE_RESOURCE_DEFINITIONS.map((definition) => ({
454
+ uri: definition.uri,
455
+ name: definition.name,
456
+ description: definition.description,
457
+ mimeType: "text/markdown",
458
+ text: GUIDE_TEXT_BY_TOPIC[definition.topic]
459
+ }));
460
+ var MCP_RESOURCE_TEMPLATES = [
461
+ {
462
+ uriTemplate: "zuiku://guide/{topic}",
463
+ name: "zuiku-guide",
464
+ description: "Read a short Zuiku guide for format-guide, flowchart-example, er-from-ddl-guide, or edit-existing-guide.",
465
+ mimeType: "text/markdown"
466
+ }
467
+ ];
468
+ var MCP_PROMPTS = GUIDE_RESOURCE_DEFINITIONS.map((definition) => ({
469
+ name: definition.name,
470
+ description: definition.description
471
+ }));
472
+ function resolveMcpResource(uri) {
473
+ const exact = MCP_STATIC_RESOURCES.find((resource) => resource.uri === uri);
474
+ if (exact) {
475
+ return exact;
476
+ }
477
+ const match = uri.match(/^zuiku:\/\/guide\/([^/]+)$/);
478
+ const topic = match?.[1];
479
+ if (!topic || !(topic in GUIDE_TEXT_BY_TOPIC)) {
480
+ return void 0;
481
+ }
482
+ return {
483
+ uri: `zuiku://guide/${topic}`,
484
+ name: topic,
485
+ description: `Generated guide for ${topic}.`,
486
+ mimeType: "text/markdown",
487
+ text: GUIDE_TEXT_BY_TOPIC[topic]
488
+ };
489
+ }
490
+ function resolveMcpPrompt(name) {
491
+ const resource = MCP_STATIC_RESOURCES.find((item) => item.name === name);
492
+ if (!resource) {
493
+ return void 0;
494
+ }
495
+ return {
496
+ description: resource.description,
497
+ messages: [
498
+ {
499
+ role: "user",
500
+ content: {
501
+ type: "text",
502
+ text: resource.text
503
+ }
504
+ }
505
+ ]
506
+ };
507
+ }
508
+
329
509
  // ../../apps/web/src/lib/mcpStdioRuntime.ts
330
510
  import { createHash } from "node:crypto";
331
511
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
@@ -3064,6 +3244,7 @@ var IMPORT_RECOVERY_AI_PROMPT = [
3064
3244
  "- \u5165\u529B\u306B\u65E2\u5B58\u306E Zuiku Markdown \u304C\u542B\u307E\u308C\u308B\u5834\u5408\u306F\u3001\u305D\u306E\u5185\u5BB9\u30FBdiagramId\u30FB\u65E2\u5B58\u30C7\u30B6\u30A4\u30F3\u3092\u3067\u304D\u308B\u3060\u3051\u4FDD\u3061\u306A\u304C\u3089\u4FEE\u6B63\u3059\u308B\u3002",
3065
3245
  "- \u5165\u529B\u306B\u65E2\u5B58\u306E Zuiku Markdown \u304C\u306A\u3044\u5834\u5408\u306F\u3001\u4F1A\u8A71\u5185\u5BB9\u30FB\u8AAC\u660E\u5185\u5BB9\u3092\u6574\u7406\u3057\u3066\u65B0\u898F\u306B\u56F3\u3092\u751F\u6210\u3059\u308B\u3002",
3066
3246
  "- \u69CB\u9020\u306E\u6B63\u306F `mermaid`\u3001\u898B\u305F\u76EE\u306E\u6B63\u306F `layout-json`\u3002",
3247
+ "- \u5165\u529B\u3068\u3057\u3066 `Mermaid\u5358\u4F53` \u306F\u53D7\u3051\u3089\u308C\u308B\u304C\u3001\u6700\u7D42\u51FA\u529B\u306F\u5FC5\u305A Zuiku Markdown \u6B63\u898F\u5F62\u306B\u3059\u308B\u3002",
3067
3248
  "- \u30E6\u30FC\u30B6\u30FC\u8981\u671B\u304C\u306A\u3044\u9650\u308A\u65E2\u5B58\u30C7\u30B6\u30A4\u30F3\u3092\u8E0F\u8972\u3057\u3001\u8981\u671B\u304C\u3042\u308B\u5834\u5408\u306E\u307F\u30C7\u30B6\u30A4\u30F3\u5909\u66F4\u3059\u308B\u3002",
3068
3249
  "- \u56F3\u30C7\u30FC\u30BF\u672C\u6587\u306F `H1` \u2192 `mermaid` \u2192 `layout-json` \u306E\u9806\u3002",
3069
3250
  "- `mermaid` \u5358\u4F53 / `layout-json` \u5358\u4F53 / JSON\u5358\u4F53 / HTML/CSS/JavaScript \u306E\u51FA\u529B\u306F\u7981\u6B62\u3002",
@@ -3110,6 +3291,7 @@ var IMPORT_RECOVERY_ER_AI_PROMPT = [
3110
3291
  "- \u5165\u529B\u306B\u65E2\u5B58\u306E Zuiku Markdown \u304C\u542B\u307E\u308C\u308B\u5834\u5408\u306F\u3001\u305D\u306E\u5185\u5BB9\u30FBdiagramId\u30FB\u65E2\u5B58\u30C7\u30B6\u30A4\u30F3\u3092\u3067\u304D\u308B\u3060\u3051\u4FDD\u3061\u306A\u304C\u3089\u4FEE\u6B63\u3059\u308B\u3002",
3111
3292
  "- \u5165\u529B\u306B\u65E2\u5B58\u306E Zuiku Markdown \u304C\u306A\u3044\u5834\u5408\u306F\u3001\u4F1A\u8A71\u5185\u5BB9\u30FB\u8AAC\u660E\u5185\u5BB9\u3092\u6574\u7406\u3057\u3066\u65B0\u898F\u306B\u56F3\u3092\u751F\u6210\u3059\u308B\u3002",
3112
3293
  "- \u69CB\u9020\u306E\u6B63\u306F `mermaid`\u3001\u898B\u305F\u76EE\u306E\u6B63\u306F `layout-json`\u3002",
3294
+ "- \u5165\u529B\u3068\u3057\u3066 `Mermaid\u5358\u4F53` \u306F\u53D7\u3051\u3089\u308C\u308B\u304C\u3001\u6700\u7D42\u51FA\u529B\u306F\u5FC5\u305A Zuiku Markdown \u6B63\u898F\u5F62\u306B\u3059\u308B\u3002",
3113
3295
  "- \u30E6\u30FC\u30B6\u30FC\u8981\u671B\u304C\u306A\u3044\u9650\u308A\u65E2\u5B58\u30C7\u30B6\u30A4\u30F3\u3092\u8E0F\u8972\u3057\u3001\u8981\u671B\u304C\u3042\u308B\u5834\u5408\u306E\u307F\u30C7\u30B6\u30A4\u30F3\u5909\u66F4\u3059\u308B\u3002",
3114
3296
  "- \u56F3\u30C7\u30FC\u30BF\u672C\u6587\u306F `H1` \u2192 `mermaid` \u2192 `layout-json` \u306E\u9806\u3002",
3115
3297
  "- \u65E2\u5B58\u56F3\u7A2E\u304C ER \u306E\u3068\u304D\u306F `erDiagram` \u3092\u7DAD\u6301\u3059\u308B\u3002",
@@ -3337,6 +3519,12 @@ function parseZuikuMarkdown(markdown, options = {}) {
3337
3519
  const { blocks, h1Title } = scanMarkdownFences(normalizedMarkdown);
3338
3520
  const rawMermaidBlocks = blocks.filter((block) => block.language === "mermaid");
3339
3521
  const rawLayoutBlocks = blocks.filter((block) => block.language === "layout-json");
3522
+ if (rawMermaidBlocks.length === 0 && rawLayoutBlocks.length === 0) {
3523
+ const syntheticMermaid = buildSyntheticMermaidBlock(normalizedMarkdown, h1Title);
3524
+ if (syntheticMermaid) {
3525
+ rawMermaidBlocks.push(syntheticMermaid);
3526
+ }
3527
+ }
3340
3528
  if (rawMermaidBlocks.length === 0 && rawLayoutBlocks.length === 0) {
3341
3529
  throw new ImportValidationError("not_zuiku_format");
3342
3530
  }
@@ -3344,7 +3532,11 @@ function parseZuikuMarkdown(markdown, options = {}) {
3344
3532
  throw new ImportValidationError("mermaid_block_missing");
3345
3533
  }
3346
3534
  if (rawLayoutBlocks.length === 0) {
3347
- throw new ImportValidationError("not_zuiku_format");
3535
+ pushImportWarning(warnings, {
3536
+ code: "layout_autogenerated_from_mermaid",
3537
+ severity: "info",
3538
+ message: "layout-json \u304C\u7121\u3044\u305F\u3081\u521D\u671F\u914D\u7F6E\u3092\u81EA\u52D5\u751F\u6210\u3057\u307E\u3057\u305F"
3539
+ });
3348
3540
  }
3349
3541
  const mermaidBlocks = [];
3350
3542
  const layoutBlocks = [];
@@ -3367,10 +3559,10 @@ function parseZuikuMarkdown(markdown, options = {}) {
3367
3559
  const fallbackLayouts = layoutBlocks.filter((layout) => !layout.diagramId);
3368
3560
  const layoutHasDiagramIds = layoutBlocks.some((layout) => Boolean(layout.diagramId));
3369
3561
  const mermaidHasDiagramIds = mermaidBlocks.some((block) => Boolean(block.diagramId));
3370
- if (layoutHasDiagramIds !== mermaidHasDiagramIds) {
3562
+ if (layoutBlocks.length > 0 && layoutHasDiagramIds !== mermaidHasDiagramIds) {
3371
3563
  throw new ImportValidationError("diagram_id_mismatch");
3372
3564
  }
3373
- if (layoutHasDiagramIds) {
3565
+ if (layoutBlocks.length > 0 && layoutHasDiagramIds) {
3374
3566
  if (layoutBlocks.some((layout) => !layout.diagramId) || mermaidBlocks.some((block) => !block.diagramId)) {
3375
3567
  throw new ImportValidationError("diagram_id_mismatch");
3376
3568
  }
@@ -3444,7 +3636,7 @@ function exportZuikuMarkdown(project) {
3444
3636
  lines.push("- \u3053\u308C\u306FZuiku\u306B\u3088\u3063\u3066\u751F\u6210\u3055\u308C\u305FMarkdown\u3067\u3059\u3002");
3445
3637
  lines.push("- \u69CB\u9020\u306E\u6B63\u306F `mermaid`\u3001\u898B\u305F\u76EE\u306E\u6B63\u306F `layout-json`\u3002");
3446
3638
  lines.push("- \u30E6\u30FC\u30B6\u30FC\u8981\u671B\u304C\u306A\u3044\u9650\u308A\u65E2\u5B58\u30C7\u30B6\u30A4\u30F3\u3092\u8E0F\u8972\u3057\u3001\u8981\u671B\u304C\u3042\u308B\u5834\u5408\u306E\u307F\u30C7\u30B6\u30A4\u30F3\u5909\u66F4\u3059\u308B\u3002");
3447
- lines.push("- \u56F3\u30C7\u30FC\u30BF\u672C\u6587\u306F `H1` \u2192 `mermaid` \u2192 `layout-json` \u306E\u9806\u3002");
3639
+ lines.push("- \u5165\u529B\u3068\u3057\u3066\u306F `Mermaid\u5358\u4F53` \u3082\u53D7\u3051\u3089\u308C\u308B\u304C\u3001\u4FDD\u5B58\u30FB\u5171\u6709\u306E\u6B63\u898F\u5F62\u306F `H1` \u2192 `mermaid` \u2192 `layout-json`\u3002");
3448
3640
  lines.push("- `mermaid` \u5358\u4F53 / `layout-json` \u5358\u4F53 / JSON\u5358\u4F53 / HTML/CSS/JavaScript \u306E\u51FA\u529B\u306F\u7981\u6B62\u3002");
3449
3641
  lines.push("- \u7B2C\u4E00\u9078\u629E\u306F `.md` \u30D5\u30A1\u30A4\u30EB1\u4EF6\u306E\u6DFB\u4ED8\uFF08\u5FC5\u9808\uFF09\u3002");
3450
3642
  lines.push("- \u6DFB\u4ED8\u304C\u4E0D\u53EF\u80FD\u306A\u5834\u5408\u306F\u30B3\u30FC\u30C9\u30D6\u30ED\u30C3\u30AF\u3067Markdown\u3092\u51FA\u529B\u3002\u8FD4\u7B54\u5F62\u5F0F\u306F\u6B21\u306E\u53B3\u5BC6\u5F62\u5F0F\u306E\u307F\uFF1A");
@@ -3476,6 +3668,14 @@ function exportZuikuMarkdown(project) {
3476
3668
  }
3477
3669
  return lines.join("\n").trimEnd() + "\n";
3478
3670
  }
3671
+ function normalizeToZuikuMarkdown(markdown, options = {}) {
3672
+ const parsed = parseZuikuMarkdown(markdown, options);
3673
+ const project = options.preserveSourceFileName ? parsed.project : {
3674
+ ...parsed.project,
3675
+ sourceFileName: void 0
3676
+ };
3677
+ return exportZuikuMarkdown(project);
3678
+ }
3479
3679
  function resolveProjectTitleForExport(projectName) {
3480
3680
  const trimmed = projectName.trim();
3481
3681
  if (trimmed.length === 0 || trimmed === "\u56F3\u80B2 MVP \u30B9\u30B1\u30EB\u30C8\u30F3") {
@@ -3527,6 +3727,19 @@ function scanMarkdownFences(markdown) {
3527
3727
  }
3528
3728
  return { blocks, h1Title };
3529
3729
  }
3730
+ function buildSyntheticMermaidBlock(markdown, h1Title) {
3731
+ const content = markdown.split(/\r?\n/).filter((line) => !/^\s*#{1,6}\s+/.test(line)).join("\n").trim();
3732
+ if (content.length === 0) {
3733
+ return void 0;
3734
+ }
3735
+ const candidate = {
3736
+ language: "mermaid",
3737
+ content,
3738
+ order: 0,
3739
+ heading: h1Title
3740
+ };
3741
+ return parseMermaidDiagramBlock(candidate) ? candidate : void 0;
3742
+ }
3530
3743
  function unwrapOuterMarkdownFence(markdown) {
3531
3744
  const lines = markdown.split(/\r?\n/);
3532
3745
  if (lines.length < 3) {
@@ -5498,6 +5711,56 @@ function parseBearerToken(value) {
5498
5711
  return match?.[1]?.trim();
5499
5712
  }
5500
5713
 
5714
+ // package.json
5715
+ var package_default = {
5716
+ name: "zuiku-mcp",
5717
+ version: "0.3.2",
5718
+ description: "Round-trip Diagram Editor MCP server for Zuiku",
5719
+ type: "module",
5720
+ license: "UNLICENSED",
5721
+ homepage: "https://zuiku.dev/docs",
5722
+ bugs: {
5723
+ url: "https://github.com/kamotami/zuiku/issues"
5724
+ },
5725
+ repository: {
5726
+ type: "git",
5727
+ url: "git+https://github.com/kamotami/zuiku.git",
5728
+ directory: "packages/zuiku-mcp"
5729
+ },
5730
+ keywords: [
5731
+ "zuiku",
5732
+ "mcp",
5733
+ "diagram",
5734
+ "mermaid",
5735
+ "markdown",
5736
+ "round-trip"
5737
+ ],
5738
+ engines: {
5739
+ node: ">=20.0.0"
5740
+ },
5741
+ bin: {
5742
+ "zuiku-mcp": "bin/zuiku-mcp.mjs"
5743
+ },
5744
+ scripts: {
5745
+ build: "node ./scripts/build.mjs",
5746
+ prepack: "npm run build"
5747
+ },
5748
+ files: [
5749
+ "bin",
5750
+ "dist",
5751
+ "README.md"
5752
+ ],
5753
+ publishConfig: {
5754
+ access: "public"
5755
+ }
5756
+ };
5757
+
5758
+ // ../../apps/web/src/lib/mcpServerMeta.ts
5759
+ var ZUIKU_MCP_PACKAGE_NAME = package_default.name;
5760
+ var ZUIKU_MCP_PACKAGE_VERSION = package_default.version;
5761
+ var ZUIKU_MCP_SERVER_NAME = "zuiku-mcp-min";
5762
+ var ZUIKU_MCP_PROTOCOL_VERSION = "2024-11-05";
5763
+
5501
5764
  // ../../apps/web/src/lib/mcpStdioRuntime.ts
5502
5765
  var McpToolExecutionError = class extends Error {
5503
5766
  code;
@@ -5609,6 +5872,26 @@ function warnHookFailure(tool, hookName, error) {
5609
5872
  }
5610
5873
  async function executeValidatedToolCall(request, ownerId, context) {
5611
5874
  switch (request.tool) {
5875
+ case "zuiku.version": {
5876
+ return {
5877
+ packageName: ZUIKU_MCP_PACKAGE_NAME,
5878
+ packageVersion: ZUIKU_MCP_PACKAGE_VERSION,
5879
+ serverName: ZUIKU_MCP_SERVER_NAME,
5880
+ serverVersion: ZUIKU_MCP_PACKAGE_VERSION,
5881
+ protocolVersion: ZUIKU_MCP_PROTOCOL_VERSION,
5882
+ entryCommand: "npx -y zuiku-mcp",
5883
+ versionCommands: {
5884
+ cli: "npx -y zuiku-mcp --version",
5885
+ npm: "npm view zuiku-mcp version"
5886
+ },
5887
+ updateCommands: {
5888
+ oneShotLatest: "npx -y zuiku-mcp@latest",
5889
+ globalInstallLatest: "npm install -g zuiku-mcp@latest"
5890
+ },
5891
+ restartRequiredAfterUpdate: true,
5892
+ message: "Restart your MCP client or IDE session after updating so the new package is launched."
5893
+ };
5894
+ }
5612
5895
  case "zuiku.open": {
5613
5896
  const source = request.args.path ? "path" : "markdown";
5614
5897
  if (source === "path") {
@@ -5622,13 +5905,18 @@ async function executeValidatedToolCall(request, ownerId, context) {
5622
5905
  }
5623
5906
  const markdown2 = await readFile(filePath, "utf8");
5624
5907
  if (context.localEditorHost) {
5625
- const opened = await context.localEditorHost.openSession({
5626
- path: filePath,
5627
- openInBrowser: request.args.openInBrowser ?? context.defaultOpenInBrowser
5628
- });
5908
+ const opened = await openLocalEditorSessionOrThrow(
5909
+ context.localEditorHost,
5910
+ {
5911
+ path: filePath,
5912
+ openInBrowser: request.args.openInBrowser ?? context.defaultOpenInBrowser
5913
+ },
5914
+ "failed to open Zuiku editor session"
5915
+ );
5629
5916
  return {
5630
5917
  ...opened,
5631
- modifiedAt: fileStat.mtime.toISOString()
5918
+ modifiedAt: fileStat.mtime.toISOString(),
5919
+ message: buildEditorSessionMessage(opened)
5632
5920
  };
5633
5921
  }
5634
5922
  return {
@@ -5646,23 +5934,31 @@ async function executeValidatedToolCall(request, ownerId, context) {
5646
5934
  if (sizeError) {
5647
5935
  throw new McpToolExecutionError("limit_exceeded", sizeError);
5648
5936
  }
5649
- const parsed = parseOrThrow(markdown, "<mcp-open>");
5937
+ const normalizedMarkdown = normalizeMarkdownOrThrow(markdown, "<mcp-open>");
5938
+ const parsed = parseOrThrow(normalizedMarkdown, "<mcp-open>");
5650
5939
  const shapeError = validateImportProjectShape(parsed.project);
5651
5940
  if (shapeError) {
5652
5941
  throw new McpToolExecutionError("limit_exceeded", shapeError);
5653
5942
  }
5654
5943
  if (context.localEditorHost) {
5655
- const opened = await context.localEditorHost.openSession({
5656
- markdown,
5657
- title: request.args.title,
5658
- openInBrowser: request.args.openInBrowser ?? context.defaultOpenInBrowser
5659
- });
5660
- return opened;
5944
+ const opened = await openLocalEditorSessionOrThrow(
5945
+ context.localEditorHost,
5946
+ {
5947
+ markdown: normalizedMarkdown,
5948
+ title: request.args.title,
5949
+ openInBrowser: request.args.openInBrowser ?? context.defaultOpenInBrowser
5950
+ },
5951
+ "failed to open Zuiku editor session"
5952
+ );
5953
+ return {
5954
+ ...opened,
5955
+ message: buildEditorSessionMessage(opened)
5956
+ };
5661
5957
  }
5662
5958
  return {
5663
5959
  source,
5664
- hash: hashText(markdown),
5665
- bytes: Buffer.byteLength(markdown, "utf8"),
5960
+ hash: hashText(normalizedMarkdown),
5961
+ bytes: Buffer.byteLength(normalizedMarkdown, "utf8"),
5666
5962
  openedInEditor: false,
5667
5963
  message: "local editor host is not configured"
5668
5964
  };
@@ -5774,18 +6070,47 @@ async function handleApply(request, ownerId, context) {
5774
6070
  context.locks.delete(filePath);
5775
6071
  }
5776
6072
  }
6073
+ const nextHash = hashText(nextMarkdown);
6074
+ let openedInEditor = false;
6075
+ let reusedSession = false;
6076
+ let sessionId;
6077
+ let editorUrl;
6078
+ if (request.args.openAfterApply && context.localEditorHost) {
6079
+ const opened = await openLocalEditorSessionOrThrow(
6080
+ context.localEditorHost,
6081
+ {
6082
+ path: filePath,
6083
+ openInBrowser: true
6084
+ },
6085
+ "saved markdown but failed to open the Zuiku editor session"
6086
+ );
6087
+ sessionId = opened.sessionId;
6088
+ editorUrl = opened.editorUrl;
6089
+ openedInEditor = opened.openedInEditor;
6090
+ reusedSession = opened.reusedSession;
6091
+ }
5777
6092
  return {
5778
6093
  path: filePath,
5779
6094
  previousHash,
5780
- nextHash: hashText(nextMarkdown),
6095
+ nextHash,
5781
6096
  bytes: Buffer.byteLength(nextMarkdown, "utf8"),
5782
- mode: request.args.mode ?? "replace"
6097
+ mode: request.args.mode ?? "replace",
6098
+ ...sessionId ? { sessionId } : {},
6099
+ ...editorUrl ? { editorUrl } : {},
6100
+ openedInEditor,
6101
+ reusedSession,
6102
+ message: buildApplyResultMessage({
6103
+ openAfterApply: request.args.openAfterApply ?? false,
6104
+ editorUrl,
6105
+ openedInEditor,
6106
+ reusedSession
6107
+ })
5783
6108
  };
5784
6109
  }
5785
6110
  async function buildMarkdownForApplyMode(request, filePath) {
5786
6111
  const mode = request.args.mode ?? "replace";
5787
6112
  if (mode === "replace") {
5788
- return request.args.markdown;
6113
+ return normalizeMarkdownOrThrow(request.args.markdown, "<mcp-apply>");
5789
6114
  }
5790
6115
  const targetDiagramId = request.args.targetDiagramId;
5791
6116
  if (!targetDiagramId) {
@@ -5796,7 +6121,7 @@ async function buildMarkdownForApplyMode(request, filePath) {
5796
6121
  throw new McpToolExecutionError("not_found", "target file does not exist for current-page mode");
5797
6122
  }
5798
6123
  const current = parseOrThrow(currentMarkdown, path2.basename(filePath)).project;
5799
- const incoming = parseOrThrow(request.args.markdown, "<mcp-apply>").project;
6124
+ const incoming = parseOrThrow(normalizeMarkdownOrThrow(request.args.markdown, "<mcp-apply>"), "<mcp-apply>").project;
5800
6125
  const sourceDiagram = incoming.diagrams.find((diagram) => diagram.nodes.length > 0 || diagram.edges.length > 0) ?? incoming.diagrams[0];
5801
6126
  if (!sourceDiagram) {
5802
6127
  throw new McpToolExecutionError("parse_error", "incoming markdown has no diagrams");
@@ -5899,6 +6224,17 @@ function parseOrThrow(markdown, sourceLabel) {
5899
6224
  throw new McpToolExecutionError("parse_error", message);
5900
6225
  }
5901
6226
  }
6227
+ function normalizeMarkdownOrThrow(markdown, sourceLabel) {
6228
+ try {
6229
+ return normalizeToZuikuMarkdown(markdown, {
6230
+ fileName: sourceLabel,
6231
+ now: /* @__PURE__ */ new Date()
6232
+ });
6233
+ } catch (error) {
6234
+ const message = error instanceof Error ? error.message : "parse error";
6235
+ throw new McpToolExecutionError("parse_error", message);
6236
+ }
6237
+ }
5902
6238
  function ensureMarkdownPath(filePath) {
5903
6239
  const extension = path2.extname(filePath).toLowerCase();
5904
6240
  if (extension !== ".md" && extension !== ".markdown") {
@@ -5916,6 +6252,58 @@ async function resolvePathWithinAllowedRootsOrThrow(rawPath, allowedRoots2, opti
5916
6252
  }
5917
6253
  return resolved;
5918
6254
  }
6255
+ function openLocalEditorSessionOrThrow(host, request, fallbackMessage) {
6256
+ return host.openSession(request).catch((error) => {
6257
+ throw normalizeLocalEditorHostError(error, fallbackMessage);
6258
+ });
6259
+ }
6260
+ function normalizeLocalEditorHostError(error, fallbackMessage) {
6261
+ if (error instanceof McpToolExecutionError) {
6262
+ return error;
6263
+ }
6264
+ if (isStructuredToolError(error)) {
6265
+ return new McpToolExecutionError(error.code, error.message, error.data);
6266
+ }
6267
+ if (error instanceof Error) {
6268
+ return new McpToolExecutionError("invalid_request", error.message);
6269
+ }
6270
+ return new McpToolExecutionError("invalid_request", fallbackMessage);
6271
+ }
6272
+ function isStructuredToolError(value) {
6273
+ if (typeof value !== "object" || value === null) {
6274
+ return false;
6275
+ }
6276
+ const candidate = value;
6277
+ return typeof candidate.message === "string" && isMcpErrorCode(candidate.code) && (candidate.data === void 0 || typeof candidate.data === "object" && candidate.data !== null && !Array.isArray(candidate.data));
6278
+ }
6279
+ function isMcpErrorCode(value) {
6280
+ return value === "invalid_request" || value === "parse_error" || value === "conflict" || value === "limit_exceeded" || value === "not_found";
6281
+ }
6282
+ function buildEditorSessionMessage(opened) {
6283
+ if (opened.openedInEditor && opened.reusedSession) {
6284
+ return "Reused the existing Zuiku editor session and requested it to open.";
6285
+ }
6286
+ if (opened.openedInEditor) {
6287
+ return "Requested the Zuiku editor session to open.";
6288
+ }
6289
+ if (opened.reusedSession) {
6290
+ return "Reused the existing Zuiku editor session. If your client did not open it automatically, use the editorUrl.";
6291
+ }
6292
+ return "Zuiku editor session is ready. If your client did not open it automatically, use the editorUrl.";
6293
+ }
6294
+ function buildApplyResultMessage(options) {
6295
+ if (!options.openAfterApply) {
6296
+ return "Saved canonical Zuiku Markdown.";
6297
+ }
6298
+ if (!options.editorUrl) {
6299
+ return "Saved canonical Zuiku Markdown. The editor session was not opened.";
6300
+ }
6301
+ return `Saved canonical Zuiku Markdown. ${buildEditorSessionMessage({
6302
+ editorUrl: options.editorUrl,
6303
+ openedInEditor: options.openedInEditor,
6304
+ reusedSession: options.reusedSession
6305
+ })}`;
6306
+ }
5919
6307
  function hashText(value) {
5920
6308
  return `sha256:${createHash("sha256").update(value).digest("hex")}`;
5921
6309
  }
@@ -5944,7 +6332,9 @@ function createMcpJsonRpcDispatcher(options) {
5944
6332
  result: {
5945
6333
  protocolVersion: serverInfo.protocolVersion,
5946
6334
  capabilities: {
5947
- tools: {}
6335
+ tools: {},
6336
+ resources: {},
6337
+ prompts: {}
5948
6338
  },
5949
6339
  serverInfo: {
5950
6340
  name: serverInfo.name,
@@ -5965,6 +6355,104 @@ function createMcpJsonRpcDispatcher(options) {
5965
6355
  }
5966
6356
  };
5967
6357
  }
6358
+ if (request.method === "resources/list") {
6359
+ return {
6360
+ jsonrpc: "2.0",
6361
+ id,
6362
+ result: {
6363
+ resources: MCP_STATIC_RESOURCES.map((resource) => ({
6364
+ uri: resource.uri,
6365
+ name: resource.name,
6366
+ description: resource.description,
6367
+ mimeType: resource.mimeType
6368
+ }))
6369
+ }
6370
+ };
6371
+ }
6372
+ if (request.method === "resources/templates/list") {
6373
+ return {
6374
+ jsonrpc: "2.0",
6375
+ id,
6376
+ result: {
6377
+ resourceTemplates: MCP_RESOURCE_TEMPLATES
6378
+ }
6379
+ };
6380
+ }
6381
+ if (request.method === "resources/read") {
6382
+ const uri = asNonEmptyString3(params.uri);
6383
+ if (!uri) {
6384
+ return {
6385
+ jsonrpc: "2.0",
6386
+ id,
6387
+ error: {
6388
+ code: -32602,
6389
+ message: "resources/read requires params.uri"
6390
+ }
6391
+ };
6392
+ }
6393
+ const resource = resolveMcpResource(uri);
6394
+ if (!resource) {
6395
+ return {
6396
+ jsonrpc: "2.0",
6397
+ id,
6398
+ error: {
6399
+ code: -32002,
6400
+ message: `Resource not found: ${uri}`
6401
+ }
6402
+ };
6403
+ }
6404
+ return {
6405
+ jsonrpc: "2.0",
6406
+ id,
6407
+ result: {
6408
+ contents: [
6409
+ {
6410
+ uri: resource.uri,
6411
+ mimeType: resource.mimeType,
6412
+ text: resource.text
6413
+ }
6414
+ ]
6415
+ }
6416
+ };
6417
+ }
6418
+ if (request.method === "prompts/list") {
6419
+ return {
6420
+ jsonrpc: "2.0",
6421
+ id,
6422
+ result: {
6423
+ prompts: MCP_PROMPTS
6424
+ }
6425
+ };
6426
+ }
6427
+ if (request.method === "prompts/get") {
6428
+ const promptName = asNonEmptyString3(params.name);
6429
+ if (!promptName) {
6430
+ return {
6431
+ jsonrpc: "2.0",
6432
+ id,
6433
+ error: {
6434
+ code: -32602,
6435
+ message: "prompts/get requires params.name"
6436
+ }
6437
+ };
6438
+ }
6439
+ const prompt = resolveMcpPrompt(promptName);
6440
+ if (!prompt) {
6441
+ return {
6442
+ jsonrpc: "2.0",
6443
+ id,
6444
+ error: {
6445
+ code: -32002,
6446
+ message: `Prompt not found: ${promptName}`
6447
+ }
6448
+ };
6449
+ }
6450
+ return {
6451
+ jsonrpc: "2.0",
6452
+ id,
6453
+ result: prompt
6454
+ };
6455
+ }
5968
6456
  if (request.method === "tools/call") {
5969
6457
  const toolName = asNonEmptyString3(params.name);
5970
6458
  const args = asObject(params.arguments) ?? {};
@@ -5989,7 +6477,7 @@ function createMcpJsonRpcDispatcher(options) {
5989
6477
  jsonrpc: "2.0",
5990
6478
  id,
5991
6479
  result: {
5992
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
6480
+ content: [{ type: "text", text: formatToolSuccessText(toolName, payload) }],
5993
6481
  structuredContent: payload
5994
6482
  }
5995
6483
  };
@@ -6026,7 +6514,7 @@ function createMcpJsonRpcDispatcher(options) {
6026
6514
  function toolError(code, message, data) {
6027
6515
  return {
6028
6516
  isError: true,
6029
- content: [{ type: "text", text: `${code}: ${message}` }],
6517
+ content: [{ type: "text", text: formatToolErrorText(code, message, data) }],
6030
6518
  structuredContent: {
6031
6519
  code,
6032
6520
  message,
@@ -6034,6 +6522,44 @@ function toolError(code, message, data) {
6034
6522
  }
6035
6523
  };
6036
6524
  }
6525
+ function formatToolSuccessText(toolName, payload) {
6526
+ if (toolName === "zuiku.version" && isObjectRecord(payload)) {
6527
+ const packageVersion = asString4(payload.packageVersion);
6528
+ const cliCommand = asString4(asObject(payload.versionCommands)?.cli);
6529
+ if (packageVersion && cliCommand) {
6530
+ return `zuiku-mcp ${packageVersion}
6531
+ check: ${cliCommand}`;
6532
+ }
6533
+ }
6534
+ if ((toolName === "zuiku.open" || toolName === "zuiku.apply") && isObjectRecord(payload)) {
6535
+ const message = asString4(payload.message);
6536
+ const editorUrl = asString4(payload.editorUrl);
6537
+ const lines = [
6538
+ ...message ? [message] : [],
6539
+ ...editorUrl ? ["Open this URL if needed:", editorUrl] : []
6540
+ ];
6541
+ if (lines.length > 0) {
6542
+ return lines.join("\n");
6543
+ }
6544
+ }
6545
+ return JSON.stringify(payload, null, 2);
6546
+ }
6547
+ function formatToolErrorText(code, message, data) {
6548
+ const lines = [`${code}: ${message}`];
6549
+ const suggestion = asString4(data?.suggestion);
6550
+ const editorUrl = asString4(data?.editorUrl);
6551
+ if (suggestion) {
6552
+ lines.push(`hint: ${suggestion}`);
6553
+ }
6554
+ if (editorUrl) {
6555
+ lines.push("Open this URL if needed:");
6556
+ lines.push(editorUrl);
6557
+ }
6558
+ return lines.join("\n");
6559
+ }
6560
+ function isObjectRecord(value) {
6561
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6562
+ }
6037
6563
  function asObject(value) {
6038
6564
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6039
6565
  }
@@ -6079,11 +6605,14 @@ var LocalEditorHostImpl = class {
6079
6605
  sessionIdleTimeoutMs;
6080
6606
  tempFileTtlMs;
6081
6607
  cleanupIntervalMs;
6608
+ registryFilePath;
6082
6609
  sessions = /* @__PURE__ */ new Map();
6610
+ pathSessionIds = /* @__PURE__ */ new Map();
6083
6611
  server;
6084
6612
  listeningPort;
6085
6613
  tempDir;
6086
6614
  cleanupTimer;
6615
+ attachedBaseUrl;
6087
6616
  constructor(options) {
6088
6617
  this.host = options.host?.trim() || DEFAULT_HOST;
6089
6618
  this.port = options.port ?? DEFAULT_PORT;
@@ -6103,12 +6632,13 @@ var LocalEditorHostImpl = class {
6103
6632
  options.cleanupIntervalMs,
6104
6633
  DEFAULT_CLEANUP_INTERVAL_MS
6105
6634
  );
6635
+ this.registryFilePath = path3.join(tmpdir(), `zuiku-mcp-host-${slug2(`${this.host}-${this.port}`)}.json`);
6106
6636
  }
6107
6637
  getBaseUrl() {
6108
- return `http://${this.host}:${this.listeningPort}`;
6638
+ return this.attachedBaseUrl ?? `http://${this.host}:${this.listeningPort}`;
6109
6639
  }
6110
6640
  async ensureStarted() {
6111
- if (this.server?.listening) {
6641
+ if (this.server?.listening || this.attachedBaseUrl) {
6112
6642
  return;
6113
6643
  }
6114
6644
  const server = createServer((request, response) => {
@@ -6120,21 +6650,37 @@ var LocalEditorHostImpl = class {
6120
6650
  });
6121
6651
  });
6122
6652
  });
6123
- await new Promise((resolve, reject) => {
6124
- server.once("error", reject);
6125
- server.listen(this.port, this.host, () => {
6126
- server.off("error", reject);
6127
- resolve();
6653
+ try {
6654
+ await new Promise((resolve, reject) => {
6655
+ server.once("error", reject);
6656
+ server.listen(this.port, this.host, () => {
6657
+ server.off("error", reject);
6658
+ resolve();
6659
+ });
6128
6660
  });
6129
- });
6661
+ } catch (error) {
6662
+ if (await this.tryAttachToRunningHost(error)) {
6663
+ return;
6664
+ }
6665
+ if (isAddressInUseError(error)) {
6666
+ throw buildPortInUseJsonError(this.host, this.port, this.editorUrl);
6667
+ }
6668
+ throw error;
6669
+ }
6130
6670
  const address = server.address();
6131
6671
  if (address && typeof address === "object") {
6132
6672
  this.listeningPort = address.port;
6133
6673
  }
6134
6674
  this.server = server;
6675
+ this.attachedBaseUrl = void 0;
6676
+ await this.writeRegistryFile();
6135
6677
  this.startCleanupTimer();
6136
6678
  }
6137
6679
  async stop() {
6680
+ if (this.attachedBaseUrl && !this.server) {
6681
+ this.attachedBaseUrl = void 0;
6682
+ return;
6683
+ }
6138
6684
  this.stopCleanupTimer();
6139
6685
  if (this.server) {
6140
6686
  await new Promise((resolve, reject) => {
@@ -6148,9 +6694,12 @@ var LocalEditorHostImpl = class {
6148
6694
  }).catch(() => void 0);
6149
6695
  this.server = void 0;
6150
6696
  }
6697
+ this.attachedBaseUrl = void 0;
6698
+ await this.removeRegistryFileIfOwned().catch(() => void 0);
6151
6699
  const tempDir = this.tempDir;
6152
6700
  const records = [...this.sessions.values()];
6153
6701
  this.sessions.clear();
6702
+ this.pathSessionIds.clear();
6154
6703
  for (const record of records) {
6155
6704
  if (record.isTempFile) {
6156
6705
  await rm(record.path, { force: true }).catch(() => void 0);
@@ -6163,6 +6712,9 @@ var LocalEditorHostImpl = class {
6163
6712
  }
6164
6713
  async openSession(request) {
6165
6714
  await this.ensureStarted();
6715
+ if (this.attachedBaseUrl && !this.server) {
6716
+ return this.openSessionViaAttachedHost(request);
6717
+ }
6166
6718
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
6167
6719
  const nowMs = Date.now();
6168
6720
  if (request.path && request.markdown) {
@@ -6172,6 +6724,8 @@ var LocalEditorHostImpl = class {
6172
6724
  let filePath;
6173
6725
  let markdown;
6174
6726
  let isTempFile = false;
6727
+ let reusedSession = false;
6728
+ let presentedAtMs;
6175
6729
  if (request.path) {
6176
6730
  source = "path";
6177
6731
  const resolvedPath = await this.resolveAllowedPathOrThrow(request.path, {
@@ -6183,10 +6737,47 @@ var LocalEditorHostImpl = class {
6183
6737
  throw jsonError(404, "not_found", `file not found: ${request.path}`);
6184
6738
  });
6185
6739
  markdown = await readFile2(filePath, "utf8");
6740
+ const existing = await this.getReusablePathSession(resolvedPath);
6741
+ if (existing) {
6742
+ reusedSession = true;
6743
+ const editorUrl2 = buildEditorUrl(this.editorUrl, this.getBaseUrl(), existing.sessionId, this.sessionToken);
6744
+ const shouldOpenBrowser = (request.openInBrowser ?? this.defaultOpenInBrowser) && !existing.presentedAtMs;
6745
+ const nextPresentedAtMs = shouldOpenBrowser || existing.presentedAtMs ? nowMs : void 0;
6746
+ if (shouldOpenBrowser) {
6747
+ openBrowser(editorUrl2);
6748
+ }
6749
+ const nextRecord = {
6750
+ ...existing,
6751
+ path: resolvedPath,
6752
+ markdown,
6753
+ hash: hashText2(markdown),
6754
+ bytes: Buffer.byteLength(markdown, "utf8"),
6755
+ updatedAt: nowIso,
6756
+ expiresAtMs: nowMs + this.sessionIdleTimeoutMs,
6757
+ ...nextPresentedAtMs ? { presentedAtMs: nextPresentedAtMs } : {}
6758
+ };
6759
+ this.sessions.set(existing.sessionId, nextRecord);
6760
+ this.pathSessionIds.set(path3.resolve(resolvedPath), existing.sessionId);
6761
+ return {
6762
+ sessionId: nextRecord.sessionId,
6763
+ source: nextRecord.source,
6764
+ path: nextRecord.path,
6765
+ hash: nextRecord.hash,
6766
+ bytes: nextRecord.bytes,
6767
+ createdAt: nextRecord.createdAt,
6768
+ updatedAt: nextRecord.updatedAt,
6769
+ editorUrl: editorUrl2,
6770
+ openedInEditor: Boolean(nextPresentedAtMs),
6771
+ reusedSession
6772
+ };
6773
+ }
6186
6774
  } else if (typeof request.markdown === "string") {
6187
6775
  source = "markdown";
6188
6776
  validateMarkdownForSession(request.markdown);
6189
- markdown = request.markdown;
6777
+ markdown = normalizeToZuikuMarkdown(request.markdown, {
6778
+ fileName: "mcp-session.md",
6779
+ now: new Date(nowMs)
6780
+ });
6190
6781
  filePath = await this.createTempMarkdownFile(markdown, request.title);
6191
6782
  isTempFile = true;
6192
6783
  } else {
@@ -6206,13 +6797,18 @@ var LocalEditorHostImpl = class {
6206
6797
  updatedAt: nowIso,
6207
6798
  isTempFile,
6208
6799
  expiresAtMs: nowMs + this.sessionIdleTimeoutMs,
6800
+ ...presentedAtMs ? { presentedAtMs } : {},
6209
6801
  ...isTempFile ? { tempFileExpiresAtMs: nowMs + this.tempFileTtlMs } : {}
6210
6802
  };
6211
6803
  this.sessions.set(sessionId, record);
6804
+ if (source === "path") {
6805
+ this.pathSessionIds.set(path3.resolve(filePath), sessionId);
6806
+ }
6212
6807
  const editorUrl = buildEditorUrl(this.editorUrl, this.getBaseUrl(), sessionId, this.sessionToken);
6213
6808
  const openInBrowser = request.openInBrowser ?? this.defaultOpenInBrowser;
6214
6809
  if (openInBrowser) {
6215
6810
  openBrowser(editorUrl);
6811
+ record.presentedAtMs = nowMs;
6216
6812
  }
6217
6813
  return {
6218
6814
  sessionId,
@@ -6222,9 +6818,113 @@ var LocalEditorHostImpl = class {
6222
6818
  bytes,
6223
6819
  createdAt: nowIso,
6224
6820
  updatedAt: nowIso,
6225
- editorUrl
6821
+ editorUrl,
6822
+ openedInEditor: openInBrowser,
6823
+ reusedSession
6226
6824
  };
6227
6825
  }
6826
+ async tryAttachToRunningHost(error) {
6827
+ if (!isAddressInUseError(error)) {
6828
+ return false;
6829
+ }
6830
+ const registry = await this.readRegistryFile();
6831
+ if (!registry) {
6832
+ return false;
6833
+ }
6834
+ if (registry.baseUrl !== `http://${this.host}:${this.port}`) {
6835
+ return false;
6836
+ }
6837
+ const response = await fetch(`${registry.baseUrl}/health`).catch(() => void 0);
6838
+ if (!response?.ok) {
6839
+ await rm(this.registryFilePath, { force: true }).catch(() => void 0);
6840
+ return false;
6841
+ }
6842
+ const health = await response.json().catch(() => void 0);
6843
+ if (!health || health.ok !== true || health.host !== registry.baseUrl) {
6844
+ await rm(this.registryFilePath, { force: true }).catch(() => void 0);
6845
+ return false;
6846
+ }
6847
+ this.attachedBaseUrl = registry.baseUrl;
6848
+ this.sessionToken = registry.sessionToken;
6849
+ this.listeningPort = this.port;
6850
+ return true;
6851
+ }
6852
+ async openSessionViaAttachedHost(request) {
6853
+ const response = await fetch(`${this.getBaseUrl()}/sessions?mcpToken=${encodeURIComponent(this.sessionToken)}`, {
6854
+ method: "POST",
6855
+ headers: {
6856
+ "Content-Type": "application/json"
6857
+ },
6858
+ body: JSON.stringify({
6859
+ ...request.path ? { path: request.path } : {},
6860
+ ...typeof request.markdown === "string" ? { markdown: request.markdown } : {},
6861
+ ...request.title ? { title: request.title } : {},
6862
+ ...typeof request.openInBrowser === "boolean" ? { openInBrowser: request.openInBrowser } : {}
6863
+ })
6864
+ }).catch((error) => {
6865
+ const message = error instanceof Error ? error.message : "failed to reach running editor host";
6866
+ throw jsonError(503, "invalid_request", message);
6867
+ });
6868
+ const payload = await response.json().catch(() => ({}));
6869
+ if (!response.ok) {
6870
+ throw jsonError(
6871
+ response.status,
6872
+ payload.code ?? "invalid_request",
6873
+ payload.message ?? "failed to open session through running editor host"
6874
+ );
6875
+ }
6876
+ if (typeof payload.sessionId !== "string" || typeof payload.path !== "string" || typeof payload.hash !== "string" || typeof payload.bytes !== "number" || typeof payload.createdAt !== "string" || typeof payload.updatedAt !== "string" || typeof payload.editorUrl !== "string" || typeof payload.openedInEditor !== "boolean" || typeof payload.reusedSession !== "boolean" || payload.source !== "path" && payload.source !== "markdown") {
6877
+ throw jsonError(502, "invalid_request", "running editor host returned invalid session payload");
6878
+ }
6879
+ return {
6880
+ sessionId: payload.sessionId,
6881
+ source: payload.source,
6882
+ path: payload.path,
6883
+ hash: payload.hash,
6884
+ bytes: payload.bytes,
6885
+ createdAt: payload.createdAt,
6886
+ updatedAt: payload.updatedAt,
6887
+ editorUrl: payload.editorUrl,
6888
+ openedInEditor: payload.openedInEditor,
6889
+ reusedSession: payload.reusedSession
6890
+ };
6891
+ }
6892
+ async readRegistryFile() {
6893
+ const raw = await readFile2(this.registryFilePath, "utf8").catch(() => void 0);
6894
+ if (!raw) {
6895
+ return void 0;
6896
+ }
6897
+ try {
6898
+ const parsed = JSON.parse(raw);
6899
+ if (typeof parsed.baseUrl !== "string" || typeof parsed.sessionToken !== "string" || typeof parsed.editorUrl !== "string" || typeof parsed.ownerPid !== "number") {
6900
+ return void 0;
6901
+ }
6902
+ return {
6903
+ baseUrl: parsed.baseUrl,
6904
+ sessionToken: parsed.sessionToken,
6905
+ editorUrl: parsed.editorUrl,
6906
+ ownerPid: parsed.ownerPid
6907
+ };
6908
+ } catch {
6909
+ return void 0;
6910
+ }
6911
+ }
6912
+ async writeRegistryFile() {
6913
+ const payload = {
6914
+ baseUrl: this.getBaseUrl(),
6915
+ sessionToken: this.sessionToken,
6916
+ editorUrl: this.editorUrl,
6917
+ ownerPid: process.pid
6918
+ };
6919
+ await writeFile2(this.registryFilePath, JSON.stringify(payload), "utf8");
6920
+ }
6921
+ async removeRegistryFileIfOwned() {
6922
+ const registry = await this.readRegistryFile();
6923
+ if (!registry || registry.ownerPid !== process.pid) {
6924
+ return;
6925
+ }
6926
+ await rm(this.registryFilePath, { force: true }).catch(() => void 0);
6927
+ }
6228
6928
  async handleRequest(request, response) {
6229
6929
  try {
6230
6930
  this.ensureLoopbackClient(request);
@@ -6320,9 +7020,13 @@ var LocalEditorHostImpl = class {
6320
7020
  hash,
6321
7021
  bytes,
6322
7022
  updatedAt,
6323
- expiresAtMs: nowMs + this.sessionIdleTimeoutMs
7023
+ expiresAtMs: nowMs + this.sessionIdleTimeoutMs,
7024
+ presentedAtMs: record.presentedAtMs ?? nowMs
6324
7025
  };
6325
7026
  this.sessions.set(sessionId, nextRecord);
7027
+ if (nextRecord.source === "path") {
7028
+ this.pathSessionIds.set(path3.resolve(nextRecord.path), sessionId);
7029
+ }
6326
7030
  return {
6327
7031
  sessionId: nextRecord.sessionId,
6328
7032
  source: nextRecord.source,
@@ -6430,6 +7134,12 @@ var LocalEditorHostImpl = class {
6430
7134
  return;
6431
7135
  }
6432
7136
  this.sessions.delete(sessionId);
7137
+ if (record.source === "path") {
7138
+ const resolvedPath = path3.resolve(record.path);
7139
+ if (this.pathSessionIds.get(resolvedPath) === sessionId) {
7140
+ this.pathSessionIds.delete(resolvedPath);
7141
+ }
7142
+ }
6433
7143
  if (record.isTempFile) {
6434
7144
  await rm(record.path, { force: true }).catch(() => void 0);
6435
7145
  }
@@ -6469,6 +7179,19 @@ var LocalEditorHostImpl = class {
6469
7179
  }
6470
7180
  return resolvedPath;
6471
7181
  }
7182
+ async getReusablePathSession(filePath) {
7183
+ const existingSessionId = this.pathSessionIds.get(path3.resolve(filePath));
7184
+ if (!existingSessionId) {
7185
+ return void 0;
7186
+ }
7187
+ try {
7188
+ const record = await this.getSessionOrThrow(existingSessionId);
7189
+ return record.source === "path" ? record : void 0;
7190
+ } catch {
7191
+ this.pathSessionIds.delete(path3.resolve(filePath));
7192
+ return void 0;
7193
+ }
7194
+ }
6472
7195
  async getSessionOrThrow(sessionId) {
6473
7196
  const record = this.sessions.get(sessionId);
6474
7197
  if (!record) {
@@ -6628,6 +7351,22 @@ function asString5(value) {
6628
7351
  function asBoolean2(value) {
6629
7352
  return typeof value === "boolean" ? value : void 0;
6630
7353
  }
7354
+ function isAddressInUseError(error) {
7355
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
7356
+ }
7357
+ function buildPortInUseJsonError(host, port, editorUrl) {
7358
+ return jsonError(
7359
+ 409,
7360
+ "conflict",
7361
+ `editor host port ${host}:${port} is already in use by another process`,
7362
+ {
7363
+ host,
7364
+ port,
7365
+ editorUrl,
7366
+ suggestion: "Stop the other process or set ZUIKU_MCP_EDITOR_PORT to a different free port."
7367
+ }
7368
+ );
7369
+ }
6631
7370
  function isJsonError(value) {
6632
7371
  return typeof value === "object" && value !== null && "status" in value && "code" in value;
6633
7372
  }
@@ -6709,9 +7448,9 @@ function isMainModule(currentModuleUrl) {
6709
7448
  }
6710
7449
 
6711
7450
  // ../../scripts/zuiku-mcp-server.ts
6712
- var SERVER_NAME = "zuiku-mcp-min";
6713
- var SERVER_VERSION = "0.3.0";
6714
- var PROTOCOL_VERSION = "2024-11-05";
7451
+ var SERVER_NAME = ZUIKU_MCP_SERVER_NAME;
7452
+ var SERVER_VERSION = ZUIKU_MCP_PACKAGE_VERSION;
7453
+ var PROTOCOL_VERSION = ZUIKU_MCP_PROTOCOL_VERSION;
6715
7454
  var allowedRoots = resolveAllowedRoots();
6716
7455
  var editorHostEnabled = parseBooleanEnv2(process.env.ZUIKU_MCP_ENABLE_EDITOR_HOST, true);
6717
7456
  var localEditorHost = editorHostEnabled ? createLocalEditorHost({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuiku-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Round-trip Diagram Editor MCP server for Zuiku",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",