zuiku-mcp 0.3.1 → 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 +29 -0
- package/bin/zuiku-mcp.mjs +10 -0
- package/dist/zuiku-mcp-server.mjs +375 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## What it exposes
|
|
8
8
|
|
|
9
|
+
- `zuiku.version`: report the running package/server version and update hints
|
|
9
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
12
|
- `zuiku.apply`: write Mermaid-only or canonical Markdown back as normalized Zuiku Markdown with hash-based conflict protection
|
|
@@ -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,9 +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
|
|
39
67
|
- `zuiku.apply(openAfterApply=true)` can request opening or reusing the local editor session after save
|
|
40
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
|
|
41
70
|
- During preview, CLI and manual MCP setup are the source of truth; optional IDE extension listings can be added on top later
|
|
42
71
|
- Install guide: `https://zuiku.dev/install`
|
|
43
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,6 +5,16 @@ 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
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.",
|
|
@@ -99,6 +109,18 @@ function validateMcpToolRequest(input) {
|
|
|
99
109
|
return invalidRequest("args must be an object");
|
|
100
110
|
}
|
|
101
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
|
+
}
|
|
102
124
|
if (tool === "zuiku.open") {
|
|
103
125
|
if (!hasOnlyKeys(args, ["path", "markdown", "title", "openInBrowser"])) {
|
|
104
126
|
return invalidRequest("zuiku.open.args contains unknown keys");
|
|
@@ -5689,6 +5711,56 @@ function parseBearerToken(value) {
|
|
|
5689
5711
|
return match?.[1]?.trim();
|
|
5690
5712
|
}
|
|
5691
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
|
+
|
|
5692
5764
|
// ../../apps/web/src/lib/mcpStdioRuntime.ts
|
|
5693
5765
|
var McpToolExecutionError = class extends Error {
|
|
5694
5766
|
code;
|
|
@@ -5800,6 +5872,26 @@ function warnHookFailure(tool, hookName, error) {
|
|
|
5800
5872
|
}
|
|
5801
5873
|
async function executeValidatedToolCall(request, ownerId, context) {
|
|
5802
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
|
+
}
|
|
5803
5895
|
case "zuiku.open": {
|
|
5804
5896
|
const source = request.args.path ? "path" : "markdown";
|
|
5805
5897
|
if (source === "path") {
|
|
@@ -5813,13 +5905,18 @@ async function executeValidatedToolCall(request, ownerId, context) {
|
|
|
5813
5905
|
}
|
|
5814
5906
|
const markdown2 = await readFile(filePath, "utf8");
|
|
5815
5907
|
if (context.localEditorHost) {
|
|
5816
|
-
const opened = await
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
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
|
+
);
|
|
5820
5916
|
return {
|
|
5821
5917
|
...opened,
|
|
5822
|
-
modifiedAt: fileStat.mtime.toISOString()
|
|
5918
|
+
modifiedAt: fileStat.mtime.toISOString(),
|
|
5919
|
+
message: buildEditorSessionMessage(opened)
|
|
5823
5920
|
};
|
|
5824
5921
|
}
|
|
5825
5922
|
return {
|
|
@@ -5844,12 +5941,19 @@ async function executeValidatedToolCall(request, ownerId, context) {
|
|
|
5844
5941
|
throw new McpToolExecutionError("limit_exceeded", shapeError);
|
|
5845
5942
|
}
|
|
5846
5943
|
if (context.localEditorHost) {
|
|
5847
|
-
const opened = await
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
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
|
+
};
|
|
5853
5957
|
}
|
|
5854
5958
|
return {
|
|
5855
5959
|
source,
|
|
@@ -5972,10 +6076,14 @@ async function handleApply(request, ownerId, context) {
|
|
|
5972
6076
|
let sessionId;
|
|
5973
6077
|
let editorUrl;
|
|
5974
6078
|
if (request.args.openAfterApply && context.localEditorHost) {
|
|
5975
|
-
const opened = await
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
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
|
+
);
|
|
5979
6087
|
sessionId = opened.sessionId;
|
|
5980
6088
|
editorUrl = opened.editorUrl;
|
|
5981
6089
|
openedInEditor = opened.openedInEditor;
|
|
@@ -5990,7 +6098,13 @@ async function handleApply(request, ownerId, context) {
|
|
|
5990
6098
|
...sessionId ? { sessionId } : {},
|
|
5991
6099
|
...editorUrl ? { editorUrl } : {},
|
|
5992
6100
|
openedInEditor,
|
|
5993
|
-
reusedSession
|
|
6101
|
+
reusedSession,
|
|
6102
|
+
message: buildApplyResultMessage({
|
|
6103
|
+
openAfterApply: request.args.openAfterApply ?? false,
|
|
6104
|
+
editorUrl,
|
|
6105
|
+
openedInEditor,
|
|
6106
|
+
reusedSession
|
|
6107
|
+
})
|
|
5994
6108
|
};
|
|
5995
6109
|
}
|
|
5996
6110
|
async function buildMarkdownForApplyMode(request, filePath) {
|
|
@@ -6138,6 +6252,58 @@ async function resolvePathWithinAllowedRootsOrThrow(rawPath, allowedRoots2, opti
|
|
|
6138
6252
|
}
|
|
6139
6253
|
return resolved;
|
|
6140
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
|
+
}
|
|
6141
6307
|
function hashText(value) {
|
|
6142
6308
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
6143
6309
|
}
|
|
@@ -6311,7 +6477,7 @@ function createMcpJsonRpcDispatcher(options) {
|
|
|
6311
6477
|
jsonrpc: "2.0",
|
|
6312
6478
|
id,
|
|
6313
6479
|
result: {
|
|
6314
|
-
content: [{ type: "text", text:
|
|
6480
|
+
content: [{ type: "text", text: formatToolSuccessText(toolName, payload) }],
|
|
6315
6481
|
structuredContent: payload
|
|
6316
6482
|
}
|
|
6317
6483
|
};
|
|
@@ -6348,7 +6514,7 @@ function createMcpJsonRpcDispatcher(options) {
|
|
|
6348
6514
|
function toolError(code, message, data) {
|
|
6349
6515
|
return {
|
|
6350
6516
|
isError: true,
|
|
6351
|
-
content: [{ type: "text", text:
|
|
6517
|
+
content: [{ type: "text", text: formatToolErrorText(code, message, data) }],
|
|
6352
6518
|
structuredContent: {
|
|
6353
6519
|
code,
|
|
6354
6520
|
message,
|
|
@@ -6356,6 +6522,44 @@ function toolError(code, message, data) {
|
|
|
6356
6522
|
}
|
|
6357
6523
|
};
|
|
6358
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
|
+
}
|
|
6359
6563
|
function asObject(value) {
|
|
6360
6564
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
6361
6565
|
}
|
|
@@ -6401,12 +6605,14 @@ var LocalEditorHostImpl = class {
|
|
|
6401
6605
|
sessionIdleTimeoutMs;
|
|
6402
6606
|
tempFileTtlMs;
|
|
6403
6607
|
cleanupIntervalMs;
|
|
6608
|
+
registryFilePath;
|
|
6404
6609
|
sessions = /* @__PURE__ */ new Map();
|
|
6405
6610
|
pathSessionIds = /* @__PURE__ */ new Map();
|
|
6406
6611
|
server;
|
|
6407
6612
|
listeningPort;
|
|
6408
6613
|
tempDir;
|
|
6409
6614
|
cleanupTimer;
|
|
6615
|
+
attachedBaseUrl;
|
|
6410
6616
|
constructor(options) {
|
|
6411
6617
|
this.host = options.host?.trim() || DEFAULT_HOST;
|
|
6412
6618
|
this.port = options.port ?? DEFAULT_PORT;
|
|
@@ -6426,12 +6632,13 @@ var LocalEditorHostImpl = class {
|
|
|
6426
6632
|
options.cleanupIntervalMs,
|
|
6427
6633
|
DEFAULT_CLEANUP_INTERVAL_MS
|
|
6428
6634
|
);
|
|
6635
|
+
this.registryFilePath = path3.join(tmpdir(), `zuiku-mcp-host-${slug2(`${this.host}-${this.port}`)}.json`);
|
|
6429
6636
|
}
|
|
6430
6637
|
getBaseUrl() {
|
|
6431
|
-
return `http://${this.host}:${this.listeningPort}`;
|
|
6638
|
+
return this.attachedBaseUrl ?? `http://${this.host}:${this.listeningPort}`;
|
|
6432
6639
|
}
|
|
6433
6640
|
async ensureStarted() {
|
|
6434
|
-
if (this.server?.listening) {
|
|
6641
|
+
if (this.server?.listening || this.attachedBaseUrl) {
|
|
6435
6642
|
return;
|
|
6436
6643
|
}
|
|
6437
6644
|
const server = createServer((request, response) => {
|
|
@@ -6443,21 +6650,37 @@ var LocalEditorHostImpl = class {
|
|
|
6443
6650
|
});
|
|
6444
6651
|
});
|
|
6445
6652
|
});
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
server.
|
|
6450
|
-
|
|
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
|
+
});
|
|
6451
6660
|
});
|
|
6452
|
-
})
|
|
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
|
+
}
|
|
6453
6670
|
const address = server.address();
|
|
6454
6671
|
if (address && typeof address === "object") {
|
|
6455
6672
|
this.listeningPort = address.port;
|
|
6456
6673
|
}
|
|
6457
6674
|
this.server = server;
|
|
6675
|
+
this.attachedBaseUrl = void 0;
|
|
6676
|
+
await this.writeRegistryFile();
|
|
6458
6677
|
this.startCleanupTimer();
|
|
6459
6678
|
}
|
|
6460
6679
|
async stop() {
|
|
6680
|
+
if (this.attachedBaseUrl && !this.server) {
|
|
6681
|
+
this.attachedBaseUrl = void 0;
|
|
6682
|
+
return;
|
|
6683
|
+
}
|
|
6461
6684
|
this.stopCleanupTimer();
|
|
6462
6685
|
if (this.server) {
|
|
6463
6686
|
await new Promise((resolve, reject) => {
|
|
@@ -6471,6 +6694,8 @@ var LocalEditorHostImpl = class {
|
|
|
6471
6694
|
}).catch(() => void 0);
|
|
6472
6695
|
this.server = void 0;
|
|
6473
6696
|
}
|
|
6697
|
+
this.attachedBaseUrl = void 0;
|
|
6698
|
+
await this.removeRegistryFileIfOwned().catch(() => void 0);
|
|
6474
6699
|
const tempDir = this.tempDir;
|
|
6475
6700
|
const records = [...this.sessions.values()];
|
|
6476
6701
|
this.sessions.clear();
|
|
@@ -6487,6 +6712,9 @@ var LocalEditorHostImpl = class {
|
|
|
6487
6712
|
}
|
|
6488
6713
|
async openSession(request) {
|
|
6489
6714
|
await this.ensureStarted();
|
|
6715
|
+
if (this.attachedBaseUrl && !this.server) {
|
|
6716
|
+
return this.openSessionViaAttachedHost(request);
|
|
6717
|
+
}
|
|
6490
6718
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
6491
6719
|
const nowMs = Date.now();
|
|
6492
6720
|
if (request.path && request.markdown) {
|
|
@@ -6595,6 +6823,108 @@ var LocalEditorHostImpl = class {
|
|
|
6595
6823
|
reusedSession
|
|
6596
6824
|
};
|
|
6597
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
|
+
}
|
|
6598
6928
|
async handleRequest(request, response) {
|
|
6599
6929
|
try {
|
|
6600
6930
|
this.ensureLoopbackClient(request);
|
|
@@ -7021,6 +7351,22 @@ function asString5(value) {
|
|
|
7021
7351
|
function asBoolean2(value) {
|
|
7022
7352
|
return typeof value === "boolean" ? value : void 0;
|
|
7023
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
|
+
}
|
|
7024
7370
|
function isJsonError(value) {
|
|
7025
7371
|
return typeof value === "object" && value !== null && "status" in value && "code" in value;
|
|
7026
7372
|
}
|
|
@@ -7102,9 +7448,9 @@ function isMainModule(currentModuleUrl) {
|
|
|
7102
7448
|
}
|
|
7103
7449
|
|
|
7104
7450
|
// ../../scripts/zuiku-mcp-server.ts
|
|
7105
|
-
var SERVER_NAME =
|
|
7106
|
-
var SERVER_VERSION =
|
|
7107
|
-
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;
|
|
7108
7454
|
var allowedRoots = resolveAllowedRoots();
|
|
7109
7455
|
var editorHostEnabled = parseBooleanEnv2(process.env.ZUIKU_MCP_ENABLE_EDITOR_HOST, true);
|
|
7110
7456
|
var localEditorHost = editorHostEnabled ? createLocalEditorHost({
|