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 +34 -3
- package/bin/zuiku-mcp.mjs +10 -0
- package/dist/zuiku-mcp-server.mjs +786 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
## What it exposes
|
|
8
8
|
|
|
9
|
-
- `zuiku.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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("- \
|
|
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
|
|
5626
|
-
|
|
5627
|
-
|
|
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
|
|
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
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
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(
|
|
5665
|
-
bytes: Buffer.byteLength(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
server.
|
|
6127
|
-
|
|
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 =
|
|
6713
|
-
var SERVER_VERSION =
|
|
6714
|
-
var PROTOCOL_VERSION =
|
|
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({
|