drawio-mcp-server 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -425
- package/build/assets/downloader.js +96 -0
- package/build/assets/index.js +2 -0
- package/build/assets/manager.js +31 -0
- package/build/config.js +45 -0
- package/build/config.test.js +54 -0
- package/build/index.js +138 -16
- package/build/plugin/mcp-plugin.js +1825 -0
- package/package.json +8 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createWriteStream, existsSync, mkdirSync, createReadStream, rmSync, } from "node:fs";
|
|
2
|
+
import { pipeline } from "node:stream/promises";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Extract } from "unzipper";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const DRAWIO_GITHUB_API = "https://api.github.com/repos/jgraph/drawio/releases/latest";
|
|
8
|
+
export async function getLatestWarUrl() {
|
|
9
|
+
const response = await fetch(DRAWIO_GITHUB_API);
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(`Failed to get draw.io release info: ${response.status}`);
|
|
12
|
+
}
|
|
13
|
+
const data = await response.json();
|
|
14
|
+
const warAsset = data.assets?.find((asset) => asset.name === "draw.war");
|
|
15
|
+
if (!warAsset) {
|
|
16
|
+
throw new Error("Could not find draw.war in latest release");
|
|
17
|
+
}
|
|
18
|
+
return warAsset.browser_download_url;
|
|
19
|
+
}
|
|
20
|
+
export async function downloadFile(url, destPath) {
|
|
21
|
+
const response = await fetch(url);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
if (!response.body) {
|
|
26
|
+
throw new Error("No response body");
|
|
27
|
+
}
|
|
28
|
+
const destDir = dirname(destPath);
|
|
29
|
+
if (!existsSync(destDir)) {
|
|
30
|
+
mkdirSync(destDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
await pipeline(response.body, createWriteStream(destPath));
|
|
33
|
+
}
|
|
34
|
+
export async function extractWar(warPath, extractDir) {
|
|
35
|
+
if (!existsSync(extractDir)) {
|
|
36
|
+
mkdirSync(extractDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const extract = Extract({ path: extractDir });
|
|
40
|
+
const stream = createReadStream(warPath);
|
|
41
|
+
stream.pipe(extract);
|
|
42
|
+
extract.on("close", resolve);
|
|
43
|
+
extract.on("error", reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function cleanupExtractedFiles(extractDir) {
|
|
47
|
+
const webappDir = join(extractDir, "webapp");
|
|
48
|
+
const pathsToRemove = [
|
|
49
|
+
join(webappDir, "WEB-INF"),
|
|
50
|
+
join(webappDir, "META-INF"),
|
|
51
|
+
];
|
|
52
|
+
for (const path of pathsToRemove) {
|
|
53
|
+
if (existsSync(path)) {
|
|
54
|
+
try {
|
|
55
|
+
rmSync(path, { recursive: true, force: true });
|
|
56
|
+
console.log(`Removed: ${path}`);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.warn(`Failed to remove ${path}:`, err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function downloadAndExtractAssets(targetDir, onProgress) {
|
|
65
|
+
const progress = onProgress || console.log;
|
|
66
|
+
progress("Fetching draw.io release info...");
|
|
67
|
+
const warUrl = await getLatestWarUrl();
|
|
68
|
+
const warPath = join(targetDir, "draw.war");
|
|
69
|
+
progress(`Downloading draw.war from ${warUrl}...`);
|
|
70
|
+
await downloadFile(warUrl, warPath);
|
|
71
|
+
progress("Download complete.");
|
|
72
|
+
const webappDir = join(targetDir, "webapp");
|
|
73
|
+
progress("Extracting archive...");
|
|
74
|
+
await extractWar(warPath, webappDir);
|
|
75
|
+
progress("Extraction complete.");
|
|
76
|
+
progress("Cleaning up unnecessary files...");
|
|
77
|
+
cleanupExtractedFiles(targetDir);
|
|
78
|
+
// Remove the WAR file
|
|
79
|
+
try {
|
|
80
|
+
rmSync(warPath, { force: true });
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.warn("Failed to remove WAR file:", err);
|
|
84
|
+
}
|
|
85
|
+
progress("Assets ready!");
|
|
86
|
+
}
|
|
87
|
+
export async function ensureAssets(config, onProgress) {
|
|
88
|
+
const { getCacheDir, getAssetRoot, assetsExist } = await import("./manager.js");
|
|
89
|
+
const cacheDir = getCacheDir(config.assetPath);
|
|
90
|
+
const assetRoot = getAssetRoot(config);
|
|
91
|
+
if (!assetsExist(config)) {
|
|
92
|
+
console.log(`Assets not found in ${assetRoot}. Downloading...`);
|
|
93
|
+
await downloadAndExtractAssets(cacheDir, onProgress);
|
|
94
|
+
}
|
|
95
|
+
return { assetRoot, isLocal: true };
|
|
96
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import cachedir from "cachedir";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const DEFAULT_CACHE_NAME = "drawio-mcp-server";
|
|
7
|
+
export function getCacheDir(customPath) {
|
|
8
|
+
if (customPath) {
|
|
9
|
+
return customPath;
|
|
10
|
+
}
|
|
11
|
+
const dir = cachedir(DEFAULT_CACHE_NAME);
|
|
12
|
+
if (!dir) {
|
|
13
|
+
throw new Error("Could not determine cache directory. Use --asset-path to specify one.");
|
|
14
|
+
}
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
export function getAssetRoot(config) {
|
|
18
|
+
const cacheDir = getCacheDir(config.assetPath);
|
|
19
|
+
return join(cacheDir, "webapp");
|
|
20
|
+
}
|
|
21
|
+
export function assetsExist(config) {
|
|
22
|
+
const assetRoot = getAssetRoot(config);
|
|
23
|
+
const indexPath = join(assetRoot, "index.html");
|
|
24
|
+
return existsSync(indexPath);
|
|
25
|
+
}
|
|
26
|
+
export function getLocalPluginPath() {
|
|
27
|
+
return join(__dirname, "..", "..", "build", "plugin", "mcp-plugin.js");
|
|
28
|
+
}
|
|
29
|
+
export function isUsingLocalAssets(config) {
|
|
30
|
+
return assetsExist(config);
|
|
31
|
+
}
|
package/build/config.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application configuration interface
|
|
3
|
+
*/
|
|
1
4
|
/**
|
|
2
5
|
* Default configuration values
|
|
3
6
|
*/
|
|
@@ -5,6 +8,7 @@ const DEFAULT_CONFIG = {
|
|
|
5
8
|
extensionPort: 3333,
|
|
6
9
|
httpPort: 3000,
|
|
7
10
|
transports: ["stdio"],
|
|
11
|
+
editorEnabled: false,
|
|
8
12
|
};
|
|
9
13
|
/**
|
|
10
14
|
* Valid port range
|
|
@@ -97,6 +101,8 @@ export const parseConfig = (args) => {
|
|
|
97
101
|
let httpPortValue;
|
|
98
102
|
let parsedHttpPort;
|
|
99
103
|
let transportValues;
|
|
104
|
+
let editorEnabled = false;
|
|
105
|
+
let assetPath;
|
|
100
106
|
for (let i = 0; i < args.length; i += 1) {
|
|
101
107
|
const arg = args[i];
|
|
102
108
|
if (arg === "--extension-port" || arg === "-p") {
|
|
@@ -123,6 +129,31 @@ export const parseConfig = (args) => {
|
|
|
123
129
|
transportValues = [nextValue];
|
|
124
130
|
i += 1;
|
|
125
131
|
}
|
|
132
|
+
else if (arg === "--editor" || arg === "-e") {
|
|
133
|
+
const nextValue = args[i + 1];
|
|
134
|
+
if (nextValue !== undefined &&
|
|
135
|
+
(nextValue === "false" || nextValue === "true")) {
|
|
136
|
+
editorEnabled = nextValue === "true";
|
|
137
|
+
i += 1;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
editorEnabled = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (arg === "--editor=true") {
|
|
144
|
+
editorEnabled = true;
|
|
145
|
+
}
|
|
146
|
+
else if (arg === "--editor=false") {
|
|
147
|
+
editorEnabled = false;
|
|
148
|
+
}
|
|
149
|
+
else if (arg === "--asset-path") {
|
|
150
|
+
const nextValue = args[i + 1];
|
|
151
|
+
if (nextValue === undefined) {
|
|
152
|
+
return new Error("--asset-path flag requires a path");
|
|
153
|
+
}
|
|
154
|
+
assetPath = nextValue;
|
|
155
|
+
i += 1;
|
|
156
|
+
}
|
|
126
157
|
}
|
|
127
158
|
if (httpPortValue !== undefined) {
|
|
128
159
|
const httpPort = parseHttpPortValue(httpPortValue);
|
|
@@ -145,6 +176,8 @@ export const parseConfig = (args) => {
|
|
|
145
176
|
extensionPort,
|
|
146
177
|
httpPort: parsedHttpPort !== undefined ? parsedHttpPort : DEFAULT_CONFIG.httpPort,
|
|
147
178
|
transports,
|
|
179
|
+
editorEnabled,
|
|
180
|
+
assetPath,
|
|
148
181
|
};
|
|
149
182
|
}
|
|
150
183
|
if (httpPortValue !== undefined) {
|
|
@@ -156,6 +189,8 @@ export const parseConfig = (args) => {
|
|
|
156
189
|
...DEFAULT_CONFIG,
|
|
157
190
|
httpPort: parsedHttpPort,
|
|
158
191
|
transports,
|
|
192
|
+
editorEnabled,
|
|
193
|
+
assetPath,
|
|
159
194
|
};
|
|
160
195
|
}
|
|
161
196
|
const transports = parseTransports(transportValues);
|
|
@@ -166,6 +201,8 @@ export const parseConfig = (args) => {
|
|
|
166
201
|
return {
|
|
167
202
|
...DEFAULT_CONFIG,
|
|
168
203
|
transports,
|
|
204
|
+
editorEnabled,
|
|
205
|
+
assetPath,
|
|
169
206
|
};
|
|
170
207
|
};
|
|
171
208
|
/**
|
|
@@ -177,3 +214,11 @@ export const buildConfig = () => {
|
|
|
177
214
|
const args = process.argv.slice(2);
|
|
178
215
|
return parseConfig(args);
|
|
179
216
|
};
|
|
217
|
+
export function getHttpFeatureConfig(config) {
|
|
218
|
+
return {
|
|
219
|
+
enableMcp: config.transports.includes("http"),
|
|
220
|
+
enableEditor: config.editorEnabled,
|
|
221
|
+
enableHealth: true,
|
|
222
|
+
enableConfig: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
package/build/config.test.js
CHANGED
|
@@ -114,6 +114,7 @@ describe("parseConfig", () => {
|
|
|
114
114
|
extensionPort: 3333,
|
|
115
115
|
httpPort: 3000,
|
|
116
116
|
transports: ["stdio"],
|
|
117
|
+
editorEnabled: false,
|
|
117
118
|
});
|
|
118
119
|
});
|
|
119
120
|
test("--extension-port flag sets custom port", () => {
|
|
@@ -121,6 +122,7 @@ describe("parseConfig", () => {
|
|
|
121
122
|
extensionPort: 8080,
|
|
122
123
|
httpPort: 3000,
|
|
123
124
|
transports: ["stdio"],
|
|
125
|
+
editorEnabled: false,
|
|
124
126
|
});
|
|
125
127
|
});
|
|
126
128
|
test("-p flag sets custom port", () => {
|
|
@@ -128,6 +130,7 @@ describe("parseConfig", () => {
|
|
|
128
130
|
extensionPort: 8080,
|
|
129
131
|
httpPort: 3000,
|
|
130
132
|
transports: ["stdio"],
|
|
133
|
+
editorEnabled: false,
|
|
131
134
|
});
|
|
132
135
|
});
|
|
133
136
|
test("--http-port flag sets custom port", () => {
|
|
@@ -135,6 +138,7 @@ describe("parseConfig", () => {
|
|
|
135
138
|
extensionPort: 3333,
|
|
136
139
|
httpPort: 4242,
|
|
137
140
|
transports: ["stdio"],
|
|
141
|
+
editorEnabled: false,
|
|
138
142
|
});
|
|
139
143
|
});
|
|
140
144
|
test("both ports can be configured", () => {
|
|
@@ -142,6 +146,7 @@ describe("parseConfig", () => {
|
|
|
142
146
|
extensionPort: 8080,
|
|
143
147
|
httpPort: 4242,
|
|
144
148
|
transports: ["stdio"],
|
|
149
|
+
editorEnabled: false,
|
|
145
150
|
});
|
|
146
151
|
});
|
|
147
152
|
test("help flag is ignored in config parsing", () => {
|
|
@@ -149,6 +154,7 @@ describe("parseConfig", () => {
|
|
|
149
154
|
extensionPort: 3333,
|
|
150
155
|
httpPort: 3000,
|
|
151
156
|
transports: ["stdio"],
|
|
157
|
+
editorEnabled: false,
|
|
152
158
|
});
|
|
153
159
|
});
|
|
154
160
|
test("invalid port returns Error", () => {
|
|
@@ -176,6 +182,7 @@ describe("parseConfig", () => {
|
|
|
176
182
|
extensionPort: 9090,
|
|
177
183
|
httpPort: 3000,
|
|
178
184
|
transports: ["stdio"],
|
|
185
|
+
editorEnabled: false,
|
|
179
186
|
});
|
|
180
187
|
});
|
|
181
188
|
test("short and long form both work, last wins", () => {
|
|
@@ -183,6 +190,7 @@ describe("parseConfig", () => {
|
|
|
183
190
|
extensionPort: 9090,
|
|
184
191
|
httpPort: 3000,
|
|
185
192
|
transports: ["stdio"],
|
|
193
|
+
editorEnabled: false,
|
|
186
194
|
});
|
|
187
195
|
});
|
|
188
196
|
test("last http-port flag wins", () => {
|
|
@@ -190,6 +198,7 @@ describe("parseConfig", () => {
|
|
|
190
198
|
extensionPort: 3333,
|
|
191
199
|
httpPort: 5000,
|
|
192
200
|
transports: ["stdio"],
|
|
201
|
+
editorEnabled: false,
|
|
193
202
|
});
|
|
194
203
|
});
|
|
195
204
|
test("sets single transport", () => {
|
|
@@ -197,6 +206,7 @@ describe("parseConfig", () => {
|
|
|
197
206
|
extensionPort: 3333,
|
|
198
207
|
httpPort: 3000,
|
|
199
208
|
transports: ["stdio"],
|
|
209
|
+
editorEnabled: false,
|
|
200
210
|
});
|
|
201
211
|
});
|
|
202
212
|
test("sets multiple transports", () => {
|
|
@@ -204,12 +214,53 @@ describe("parseConfig", () => {
|
|
|
204
214
|
extensionPort: 3333,
|
|
205
215
|
httpPort: 3000,
|
|
206
216
|
transports: ["stdio", "http"],
|
|
217
|
+
editorEnabled: false,
|
|
207
218
|
});
|
|
208
219
|
});
|
|
209
220
|
test("rejects unknown transport", () => {
|
|
210
221
|
const result = parseConfig(["--transport", "foo"]);
|
|
211
222
|
expect(result).toBeInstanceOf(Error);
|
|
212
223
|
});
|
|
224
|
+
test("--editor flag enables editor", () => {
|
|
225
|
+
expect(parseConfig(["--editor"])).toEqual({
|
|
226
|
+
extensionPort: 3333,
|
|
227
|
+
httpPort: 3000,
|
|
228
|
+
transports: ["stdio"],
|
|
229
|
+
editorEnabled: true,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
test("-e flag enables editor", () => {
|
|
233
|
+
expect(parseConfig(["-e"])).toEqual({
|
|
234
|
+
extensionPort: 3333,
|
|
235
|
+
httpPort: 3000,
|
|
236
|
+
transports: ["stdio"],
|
|
237
|
+
editorEnabled: true,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
test("--editor false disables editor explicitly", () => {
|
|
241
|
+
expect(parseConfig(["--editor", "false"])).toEqual({
|
|
242
|
+
extensionPort: 3333,
|
|
243
|
+
httpPort: 3000,
|
|
244
|
+
transports: ["stdio"],
|
|
245
|
+
editorEnabled: false,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
test("--editor=true enables editor", () => {
|
|
249
|
+
expect(parseConfig(["--editor=true"])).toEqual({
|
|
250
|
+
extensionPort: 3333,
|
|
251
|
+
httpPort: 3000,
|
|
252
|
+
transports: ["stdio"],
|
|
253
|
+
editorEnabled: true,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
test("--editor=false disables editor explicitly", () => {
|
|
257
|
+
expect(parseConfig(["--editor=false"])).toEqual({
|
|
258
|
+
extensionPort: 3333,
|
|
259
|
+
httpPort: 3000,
|
|
260
|
+
transports: ["stdio"],
|
|
261
|
+
editorEnabled: false,
|
|
262
|
+
});
|
|
263
|
+
});
|
|
213
264
|
});
|
|
214
265
|
describe("buildConfig", () => {
|
|
215
266
|
const originalArgv = process.argv;
|
|
@@ -223,6 +274,7 @@ describe("buildConfig", () => {
|
|
|
223
274
|
extensionPort: 3333,
|
|
224
275
|
httpPort: 3000,
|
|
225
276
|
transports: ["stdio"],
|
|
277
|
+
editorEnabled: false,
|
|
226
278
|
});
|
|
227
279
|
});
|
|
228
280
|
test("parses custom port from argv", () => {
|
|
@@ -232,6 +284,7 @@ describe("buildConfig", () => {
|
|
|
232
284
|
extensionPort: 8080,
|
|
233
285
|
httpPort: 3000,
|
|
234
286
|
transports: ["stdio"],
|
|
287
|
+
editorEnabled: false,
|
|
235
288
|
});
|
|
236
289
|
});
|
|
237
290
|
test("parses custom http port from argv", () => {
|
|
@@ -241,6 +294,7 @@ describe("buildConfig", () => {
|
|
|
241
294
|
extensionPort: 3333,
|
|
242
295
|
httpPort: 4242,
|
|
243
296
|
transports: ["stdio"],
|
|
297
|
+
editorEnabled: false,
|
|
244
298
|
});
|
|
245
299
|
});
|
|
246
300
|
test("returns Error for invalid config", () => {
|
package/build/index.js
CHANGED
|
@@ -8,15 +8,18 @@ import { Hono } from "hono";
|
|
|
8
8
|
import { cors } from "hono/cors";
|
|
9
9
|
import EventEmitter from "node:events";
|
|
10
10
|
import { createServer } from "node:net";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
|
|
11
13
|
import { WebSocket, WebSocketServer } from "ws";
|
|
12
|
-
import { buildConfig, shouldShowHelp } from "./config.js";
|
|
14
|
+
import { buildConfig, shouldShowHelp, getHttpFeatureConfig, } from "./config.js";
|
|
13
15
|
import { bus_reply_stream, bus_request_stream, } from "./types.js";
|
|
14
16
|
import { create_bus } from "./emitter_bus.js";
|
|
15
17
|
import { default_tool } from "./tool.js";
|
|
16
18
|
import { nanoid_id_generator } from "./nanoid_id_generator.js";
|
|
17
19
|
import { create_logger as create_console_logger } from "./mcp_console_logger.js";
|
|
18
20
|
import { create_logger as create_server_logger, validLogLevels, } from "./mcp_server_logger.js";
|
|
19
|
-
|
|
21
|
+
import { getLocalPluginPath, isUsingLocalAssets, getAssetRoot, ensureAssets, } from "./assets/index.js";
|
|
22
|
+
const VERSION = "1.8.0";
|
|
20
23
|
/**
|
|
21
24
|
* Display help message and exit
|
|
22
25
|
*/
|
|
@@ -28,12 +31,19 @@ Usage: drawio-mcp-server [options]
|
|
|
28
31
|
|
|
29
32
|
Options:
|
|
30
33
|
--extension-port, -p <number> WebSocket server port for browser extension (default: 3333)
|
|
34
|
+
--editor, -e Enable draw.io editor endpoint
|
|
35
|
+
--http-port HTTP server port for Streamable HTTP transport (default: 3000)
|
|
36
|
+
--transport Transport type: stdio, http (default: stdio)
|
|
37
|
+
--asset-path <path> Custom path for downloaded assets
|
|
31
38
|
--help, -h Show this help message
|
|
32
39
|
|
|
33
40
|
Examples:
|
|
34
41
|
drawio-mcp-server # Use default extension port 3333
|
|
35
42
|
drawio-mcp-server --extension-port 8080 # Use custom extension port 8080
|
|
36
43
|
drawio-mcp-server -p 8080 # Short form
|
|
44
|
+
drawio-mcp-server --editor # Enable draw.io editor endpoint
|
|
45
|
+
drawio-mcp-server -e --http # Enable editor and HTTP transport
|
|
46
|
+
drawio-mcp-server --editor --asset-path /data/assets # Use custom asset path
|
|
37
47
|
`);
|
|
38
48
|
process.exit(0);
|
|
39
49
|
}
|
|
@@ -375,12 +385,7 @@ async function start_stdio_transport() {
|
|
|
375
385
|
await server.connect(transport);
|
|
376
386
|
log.debug(`Draw.io MCP Server STDIO transport active`);
|
|
377
387
|
}
|
|
378
|
-
|
|
379
|
-
// Create a stateless transport (no options = no session management)
|
|
380
|
-
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
381
|
-
// Create the Hono app
|
|
382
|
-
const app = new Hono();
|
|
383
|
-
// Enable CORS for all origins
|
|
388
|
+
function setupCors(app) {
|
|
384
389
|
app.use("*", cors({
|
|
385
390
|
origin: "*",
|
|
386
391
|
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
@@ -392,16 +397,125 @@ async function start_streamable_http_transport(http_port) {
|
|
|
392
397
|
],
|
|
393
398
|
exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
|
|
394
399
|
}));
|
|
400
|
+
}
|
|
401
|
+
function registerHealthRoute(app) {
|
|
395
402
|
app.get("/health", (c) => c.json({ status: server.isConnected() ? "ok" : "mcp not ready" }));
|
|
403
|
+
}
|
|
404
|
+
function registerConfigRoute(app, config) {
|
|
405
|
+
app.get("/api/config", (c) => c.json({
|
|
406
|
+
websocketPort: config.extensionPort,
|
|
407
|
+
serverUrl: `http://localhost:${config.httpPort}`,
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
function registerEditorRoutes(app, config) {
|
|
411
|
+
const assetConfig = {
|
|
412
|
+
assetPath: config.assetPath,
|
|
413
|
+
};
|
|
414
|
+
const isLocal = isUsingLocalAssets(assetConfig);
|
|
415
|
+
const assetRoot = isLocal ? getAssetRoot(assetConfig) : null;
|
|
416
|
+
const localPluginPath = getLocalPluginPath();
|
|
417
|
+
app.get("/js/mcp-plugin.js", (c) => {
|
|
418
|
+
if (existsSync(localPluginPath)) {
|
|
419
|
+
const content = readFileSync(localPluginPath);
|
|
420
|
+
c.header("Content-Type", "application/javascript");
|
|
421
|
+
return c.body(content);
|
|
422
|
+
}
|
|
423
|
+
return c.text("Plugin not found", 404);
|
|
424
|
+
});
|
|
425
|
+
app.use("/*", async (c, next) => {
|
|
426
|
+
let path = c.req.path;
|
|
427
|
+
if (path === "" || path === "/") {
|
|
428
|
+
path = "index.html";
|
|
429
|
+
}
|
|
430
|
+
if (!isLocal || !assetRoot) {
|
|
431
|
+
await next();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
let filePath = join(assetRoot, path);
|
|
435
|
+
if (existsSync(filePath)) {
|
|
436
|
+
const fileStats = statSync(filePath);
|
|
437
|
+
if (fileStats.isDirectory()) {
|
|
438
|
+
const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
439
|
+
const parentDir = join(assetRoot, normalizedPath);
|
|
440
|
+
if (existsSync(parentDir) && statSync(parentDir).isDirectory()) {
|
|
441
|
+
const entries = readdirSync(parentDir);
|
|
442
|
+
const firstFile = entries.find((e) => !statSync(join(parentDir, e)).isDirectory());
|
|
443
|
+
if (firstFile) {
|
|
444
|
+
filePath = join(parentDir, firstFile);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
await next();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
await next();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
let content = readFileSync(filePath);
|
|
457
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
458
|
+
if (ext === "html") {
|
|
459
|
+
const contentStr = content.toString("utf-8");
|
|
460
|
+
if (contentStr.includes("</body>") &&
|
|
461
|
+
!contentStr.includes("mcp-plugin.js")) {
|
|
462
|
+
const pluginScript = `<script src="/js/mcp-plugin.js"></script>`;
|
|
463
|
+
content = Buffer.from(contentStr.replace("</body>", `${pluginScript}</body>`));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const contentTypes = {
|
|
467
|
+
html: "text/html",
|
|
468
|
+
css: "text/css",
|
|
469
|
+
js: "application/javascript",
|
|
470
|
+
json: "application/json",
|
|
471
|
+
svg: "image/svg+xml",
|
|
472
|
+
png: "image/png",
|
|
473
|
+
ico: "image/x-icon",
|
|
474
|
+
map: "application/json",
|
|
475
|
+
};
|
|
476
|
+
c.header("Content-Type", contentTypes[ext || ""] || "text/plain");
|
|
477
|
+
return c.body(content);
|
|
478
|
+
}
|
|
479
|
+
await next();
|
|
480
|
+
});
|
|
481
|
+
app.get("/", (c) => {
|
|
482
|
+
return c.redirect("/index.html?offline=1&local=1");
|
|
483
|
+
});
|
|
484
|
+
log.debug(`Draw.io editor enabled at: http://localhost:${config.httpPort}/`);
|
|
485
|
+
}
|
|
486
|
+
function registerMcpRoute(app) {
|
|
487
|
+
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
396
488
|
app.all("/mcp", (c) => transport.handleRequest(c.req.raw));
|
|
397
|
-
|
|
489
|
+
return transport;
|
|
490
|
+
}
|
|
491
|
+
function createHttpApp(config, features) {
|
|
492
|
+
const app = new Hono();
|
|
493
|
+
setupCors(app);
|
|
494
|
+
if (features.enableHealth)
|
|
495
|
+
registerHealthRoute(app);
|
|
496
|
+
if (features.enableConfig)
|
|
497
|
+
registerConfigRoute(app, config);
|
|
498
|
+
const mcpTransport = features.enableMcp ? registerMcpRoute(app) : undefined;
|
|
499
|
+
if (features.enableEditor)
|
|
500
|
+
registerEditorRoutes(app, config);
|
|
501
|
+
return { app, mcpTransport };
|
|
502
|
+
}
|
|
503
|
+
async function startHttpServer(httpPort, config, features) {
|
|
504
|
+
const { app, mcpTransport } = createHttpApp(config, features);
|
|
505
|
+
if (mcpTransport) {
|
|
506
|
+
await server.connect(mcpTransport);
|
|
507
|
+
}
|
|
398
508
|
serve({
|
|
399
509
|
fetch: app.fetch,
|
|
400
|
-
port:
|
|
510
|
+
port: httpPort,
|
|
401
511
|
});
|
|
402
|
-
log.debug(`Draw.io MCP Server
|
|
403
|
-
|
|
404
|
-
|
|
512
|
+
log.debug(`Draw.io MCP Server HTTP active on port ${httpPort}`);
|
|
513
|
+
if (features.enableMcp) {
|
|
514
|
+
log.debug(`MCP endpoint: http://localhost:${httpPort}/mcp`);
|
|
515
|
+
}
|
|
516
|
+
if (features.enableEditor) {
|
|
517
|
+
log.debug(`Editor: http://localhost:${httpPort}/`);
|
|
518
|
+
}
|
|
405
519
|
}
|
|
406
520
|
async function main() {
|
|
407
521
|
// Check if help was requested (before parsing config)
|
|
@@ -417,13 +531,21 @@ async function main() {
|
|
|
417
531
|
process.exit(1);
|
|
418
532
|
}
|
|
419
533
|
const config = configResult;
|
|
534
|
+
const features = getHttpFeatureConfig(config);
|
|
535
|
+
// Initialize assets if needed
|
|
536
|
+
if (features.enableEditor) {
|
|
537
|
+
console.log("Initializing draw.io assets...");
|
|
538
|
+
const assetConfig = {
|
|
539
|
+
assetPath: config.assetPath,
|
|
540
|
+
};
|
|
541
|
+
await ensureAssets(assetConfig, (msg) => console.log(msg));
|
|
542
|
+
console.log("Assets ready!");
|
|
543
|
+
}
|
|
420
544
|
await start_websocket_server(config.extensionPort);
|
|
421
545
|
if (config.transports.indexOf("stdio") > -1) {
|
|
422
546
|
await start_stdio_transport();
|
|
423
547
|
}
|
|
424
|
-
|
|
425
|
-
start_streamable_http_transport(config.httpPort);
|
|
426
|
-
}
|
|
548
|
+
startHttpServer(config.httpPort, config, features);
|
|
427
549
|
log.debug(`Draw.io MCP Server running on ${config.transports}`);
|
|
428
550
|
}
|
|
429
551
|
main().catch((error) => {
|