cicy-desktop 1.0.8

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.
Files changed (66) hide show
  1. package/.github/workflows/build.yml +85 -0
  2. package/.kiro/steering/dev-workflow.md +166 -0
  3. package/AGENTS.md +247 -0
  4. package/CLAUDE.md +162 -0
  5. package/DOCKER.md +85 -0
  6. package/Dockerfile +46 -0
  7. package/README.md +720 -0
  8. package/TODO-anti-detection.md +326 -0
  9. package/bin/cicy +176 -0
  10. package/bin/preinstall.sh +32 -0
  11. package/copy-to-desktop.sh +26 -0
  12. package/docs/AUTOMATION-API.md +342 -0
  13. package/docs/REQUEST_MONITORING.md +435 -0
  14. package/docs/REST-API-FEATURE.md +155 -0
  15. package/docs/REST-API.md +319 -0
  16. package/docs/feature-distributed-multi-agent.md +555 -0
  17. package/docs/yaml.md +255 -0
  18. package/electron-mcp-fixed.command +134 -0
  19. package/electron-mcp-simple.command +135 -0
  20. package/electron-mcp.command +92 -0
  21. package/generate-openapi.js +158 -0
  22. package/jest.config.js +10 -0
  23. package/jest.setup.global.js +13 -0
  24. package/jest.teardown.global.js +7 -0
  25. package/package.json +75 -0
  26. package/service.sh +164 -0
  27. package/src/config.js +8 -0
  28. package/src/extension/inject.js +135 -0
  29. package/src/main-old.js +837 -0
  30. package/src/main.js +403 -0
  31. package/src/preload-rpc.js +4 -0
  32. package/src/server/args-parser.js +37 -0
  33. package/src/server/electron-setup.js +33 -0
  34. package/src/server/express-app.js +166 -0
  35. package/src/server/logging.js +58 -0
  36. package/src/server/mcp-server.js +53 -0
  37. package/src/server/tool-registry.js +77 -0
  38. package/src/server/ui-routes.js +81 -0
  39. package/src/swagger-ui.html +41 -0
  40. package/src/tools/account-tools.js +194 -0
  41. package/src/tools/automation-tools.js +297 -0
  42. package/src/tools/cdp-tools.js +444 -0
  43. package/src/tools/clipboard-tools.js +180 -0
  44. package/src/tools/download-tools.js +57 -0
  45. package/src/tools/exec-js.js +297 -0
  46. package/src/tools/exec-tools.js +139 -0
  47. package/src/tools/file-tools.js +212 -0
  48. package/src/tools/hook-chatgpt.js +489 -0
  49. package/src/tools/hook-gemini.js +454 -0
  50. package/src/tools/index.js +19 -0
  51. package/src/tools/ipc-bridge.js +31 -0
  52. package/src/tools/ping.js +60 -0
  53. package/src/tools/r-reset.js +28 -0
  54. package/src/tools/screenshot-tools.js +28 -0
  55. package/src/tools/system-tools.js +531 -0
  56. package/src/tools/window-tools.js +882 -0
  57. package/src/ui.html +914 -0
  58. package/src/utils/auth.js +81 -0
  59. package/src/utils/cdp-utils.js +8 -0
  60. package/src/utils/download-manager.js +41 -0
  61. package/src/utils/process-utils.js +185 -0
  62. package/src/utils/snapshot-utils.js +56 -0
  63. package/src/utils/window-monitor.js +605 -0
  64. package/src/utils/window-state.js +137 -0
  65. package/src/utils/window-utils.js +336 -0
  66. package/update-desktop.sh +33 -0
package/src/main.js ADDED
@@ -0,0 +1,403 @@
1
+ const { app: electronApp } = require("electron");
2
+ const { default: contextMenu } = require("electron-context-menu");
3
+
4
+ // 🎯 添加右键上下文菜单
5
+ contextMenu({
6
+ showLookUpSelection: true,
7
+ showSearchWithGoogle: true,
8
+ showCopyImage: true,
9
+ showCopyImageAddress: true,
10
+ showSaveImageAs: true,
11
+ showCopyVideoAddress: true,
12
+ showSaveVideoAs: true,
13
+ showCopyLink: true,
14
+ showSaveLinkAs: true,
15
+ showInspectElement: true,
16
+ showServices: true,
17
+ labels: {
18
+ cut: '剪切',
19
+ copy: '复制',
20
+ paste: '粘贴',
21
+ selectAll: '全选',
22
+ reload: '重新加载',
23
+ forceReload: '强制重新加载',
24
+ toggleDevTools: '切换开发者工具',
25
+ inspectElement: '检查元素',
26
+ services: '服务',
27
+ lookUpSelection: '查找选中内容',
28
+ searchWithGoogle: '用 Google 搜索',
29
+ copyImage: '复制图片',
30
+ copyImageAddress: '复制图片地址',
31
+ saveImage: '保存图片',
32
+ copyVideoAddress: '复制视频地址',
33
+ saveVideo: '保存视频',
34
+ copyLink: '复制链接',
35
+ saveLinkAs: '链接另存为...'
36
+ }
37
+ });
38
+
39
+
40
+ // Setup Electron flags IMMEDIATELY after require
41
+ electronApp.commandLine.appendSwitch("ignore-certificate-errors");
42
+ if (process.platform === "linux") {
43
+ process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
44
+ // electronApp.commandLine.appendSwitch("disable-setuid-sandbox");
45
+ electronApp.commandLine.appendSwitch("log-level", "3");
46
+ electronApp.commandLine.appendSwitch("disable-notifications");
47
+ electronApp.commandLine.appendSwitch("ignore-certificate-errors");
48
+ if (process.env.ELECTRON_DISABLE_HTTP_CACHE === "1") {
49
+ electronApp.commandLine.appendSwitch("disable-http-cache");
50
+ electronApp.commandLine.appendSwitch("disable-application-cache");
51
+ }
52
+ electronApp.commandLine.appendSwitch("disable-geolocation");
53
+ electronApp.commandLine.appendSwitch("disable-dev-shm-usage");
54
+ electronApp.commandLine.appendSwitch("use-gl", "angle");
55
+ electronApp.commandLine.appendSwitch("use-angle", "swiftshader");
56
+ }
57
+
58
+ const http = require("http");
59
+ const log = require("electron-log");
60
+ const { z } = require("zod");
61
+ const { config } = require("./config");
62
+ const { createWindow } = require("./utils/window-utils");
63
+ const { AuthManager } = require("./utils/auth");
64
+ const { setupElectronFlags, setupErrorHandlers } = require("./server/electron-setup");
65
+ const { parseArgs } = require("./server/args-parser");
66
+ const { setupLogging, wrapLogger } = require("./server/logging");
67
+ const { createExpressApp } = require("./server/express-app");
68
+ const { createMcpServer, setupMcpRoutes } = require("./server/mcp-server");
69
+ const { registerTool } = require("./server/tool-registry");
70
+
71
+ // Setup
72
+ // setupElectronFlags(); // Already done above
73
+ setupErrorHandlers();
74
+
75
+ // Parse arguments
76
+ const { PORT, START_URL, PROXY, oneWindow } = parseArgs();
77
+ config.port = PORT;
78
+ if (PROXY) {
79
+ config.proxy = PROXY;
80
+ log.info(`[MCP] Global proxy enabled: ${PROXY}`);
81
+ }
82
+ if (oneWindow) {
83
+ config.oneWindow = true;
84
+ log.info("[MCP] Single window mode enabled");
85
+ }
86
+
87
+ // Setup logging
88
+ setupLogging(config);
89
+ wrapLogger();
90
+
91
+ log.info("[MCP] Server starting at", new Date().toISOString());
92
+
93
+ // Initialize auth
94
+ const authManager = new AuthManager();
95
+ global.authManager = authManager; // Make it globally accessible
96
+ const authMiddleware = (req, res, next) => {
97
+ if (!authManager.validateAuth(req)) {
98
+ res.setHeader("WWW-Authenticate", 'Basic realm="Electron MCP"');
99
+ return res.status(401).json({ error: "Unauthorized" });
100
+ }
101
+ next();
102
+ };
103
+
104
+ // Create servers
105
+ const mcpServer = createMcpServer();
106
+ const tools = {};
107
+ const app = createExpressApp(authMiddleware, tools);
108
+
109
+ // Register tools
110
+ const toolModules = require("./tools");
111
+
112
+ toolModules.forEach((module) => {
113
+ module((title, description, schema, handler, options) => {
114
+ registerTool(mcpServer, tools, title, description, schema, handler, options);
115
+ });
116
+ });
117
+
118
+ // Setup MCP routes
119
+ setupMcpRoutes(app, mcpServer, authMiddleware);
120
+
121
+ // RPC endpoint with hot reload
122
+ app.post("/rpc/tools/call", authMiddleware, async (req, res) => {
123
+ let body = req.body;
124
+
125
+ // Parse YAML if Content-Type is application/yaml
126
+ if (req.get("Content-Type")?.includes("application/yaml")) {
127
+ try {
128
+ const yaml = require("js-yaml");
129
+ const rawBody = await new Promise((resolve) => {
130
+ let data = "";
131
+ req.on("data", (chunk) => (data += chunk));
132
+ req.on("end", () => resolve(data));
133
+ });
134
+ body = yaml.load(rawBody);
135
+ } catch (error) {
136
+ return res.status(400).json({ error: `Invalid YAML: ${error.message}` });
137
+ }
138
+ }
139
+
140
+ const { name, arguments: args } = body;
141
+ try {
142
+ // Re-load tool modules
143
+ const toolModules = require("./tools");
144
+
145
+ let handler = null;
146
+ let schema = null;
147
+
148
+ toolModules.forEach((module) => {
149
+ module((title, description, toolSchema, toolHandler, options) => {
150
+ if (title === name) {
151
+ handler = toolHandler;
152
+ schema = toolSchema;
153
+ }
154
+ });
155
+ });
156
+
157
+ if (!handler) {
158
+ throw new Error(`Tool '${name}' not found`);
159
+ }
160
+
161
+ const validatedArgs = schema.parse(args || {});
162
+ const result = await handler(validatedArgs);
163
+
164
+ // Support YAML response
165
+ const accept = req.headers.accept || "application/json";
166
+ if (accept.includes("application/yaml") || accept.includes("text/yaml")) {
167
+ const yaml = require("js-yaml");
168
+ res.type("yaml").send(yaml.dump({ result }));
169
+ } else {
170
+ res.json({ result });
171
+ }
172
+ } catch (error) {
173
+ // Handle Zod validation errors
174
+ if (error.name === "ZodError") {
175
+ const errorMsg = error.errors.map((e) => e.message).join(", ");
176
+ return res.json({
177
+ result: {
178
+ content: [{ type: "text", text: errorMsg }],
179
+ isError: true,
180
+ },
181
+ });
182
+ }
183
+ res.status(500).json({ error: error.message });
184
+ }
185
+ });
186
+
187
+ app.get("/rpc/tools", authMiddleware, (req, res) => {
188
+ const accept = req.headers.accept || "application/json";
189
+ const allTools = Object.values(tools).flat();
190
+
191
+ if (accept.includes("application/yaml") || accept.includes("text/yaml")) {
192
+ const yaml = require("js-yaml");
193
+ res.type("yaml").send(yaml.dump({ tools: allTools }));
194
+ } else {
195
+ res.json({ tools: allTools });
196
+ }
197
+ });
198
+
199
+ // Static file server for uploads/downloads
200
+ const fs = require("fs");
201
+ const path = require("path");
202
+ const serveIndex = require("serve-index");
203
+ const FILES_DIR = path.join(require("os").homedir(), "electron-mcp-files");
204
+ if (!fs.existsSync(FILES_DIR)) {
205
+ fs.mkdirSync(FILES_DIR, { recursive: true });
206
+ }
207
+
208
+ // Serve files with directory listing (auth required)
209
+ app.use("/files", authMiddleware, require("express").static(FILES_DIR));
210
+ app.use("/files", authMiddleware, serveIndex(FILES_DIR, { icons: true, view: "details" }));
211
+
212
+ // Dynamic tool endpoints: /rpc/{tool_name}
213
+ Object.values(tools)
214
+ .flat()
215
+ .forEach((tool) => {
216
+ app.post(`/rpc/${tool.name}`, authMiddleware, async (req, res) => {
217
+ let body = req.body;
218
+
219
+ // Parse YAML if Content-Type is application/yaml
220
+ if (req.get("Content-Type")?.includes("application/yaml")) {
221
+ try {
222
+ const yaml = require("js-yaml");
223
+ const rawBody = await new Promise((resolve) => {
224
+ let data = "";
225
+ req.on("data", (chunk) => (data += chunk));
226
+ req.on("end", () => resolve(data));
227
+ });
228
+ body = yaml.load(rawBody) || {};
229
+ } catch (error) {
230
+ return res.status(400).json({ error: `Invalid YAML: ${error.message}` });
231
+ }
232
+ }
233
+
234
+ try {
235
+ // Re-load tool modules
236
+ const toolModules = require("./tools");
237
+
238
+ let handler = null;
239
+ let schema = null;
240
+
241
+ toolModules.forEach((module) => {
242
+ module((name, desc, inputSchema, fn) => {
243
+ if (name === tool.name) {
244
+ handler = fn;
245
+ schema = inputSchema;
246
+ }
247
+ });
248
+ });
249
+
250
+ if (!handler) {
251
+ throw new Error(`Tool '${tool.name}' not found`);
252
+ }
253
+
254
+ const validatedArgs = schema.parse(body || {});
255
+ const result = await handler(validatedArgs);
256
+
257
+ // Support YAML response
258
+ const accept = req.headers.accept || "application/json";
259
+ if (accept.includes("application/yaml") || accept.includes("text/yaml")) {
260
+ const yaml = require("js-yaml");
261
+ res.type("yaml").send(yaml.dump({ result }));
262
+ } else {
263
+ res.json({ result });
264
+ }
265
+ } catch (error) {
266
+ if (error.name === "ZodError") {
267
+ const errorMsg = error.errors.map((e) => e.message).join(", ");
268
+ return res.json({
269
+ result: {
270
+ content: [{ type: "text", text: errorMsg }],
271
+ isError: true,
272
+ },
273
+ });
274
+ }
275
+ res.status(500).json({ error: error.message });
276
+ }
277
+ });
278
+ });
279
+
280
+
281
+
282
+ // File upload to path: curl --data-binary @local.js http://localhost:8101/rpc/upload/C:/Users/Administrator/data/file.js
283
+ app.post("/rpc/upload/*", authMiddleware, require("express").raw({ type: "*/*", limit: "10mb" }), (req, res) => {
284
+ try {
285
+ const filePath = req.params[0];
286
+ if (!filePath) return res.status(400).json({ error: "Missing path" });
287
+ const dir = require("path").dirname(filePath);
288
+ if (!require("fs").existsSync(dir)) require("fs").mkdirSync(dir, { recursive: true });
289
+ require("fs").writeFileSync(filePath, req.body);
290
+ const size = require("fs").statSync(filePath).size;
291
+ res.json({ success: true, path: filePath, size });
292
+ } catch (error) {
293
+ res.status(500).json({ error: error.message });
294
+ }
295
+ });
296
+
297
+ // File upload + execute: curl -X POST --data-binary @local.js http://localhost:8101/rpc/exec/node
298
+ // Supported types: shell, python, node, js (js = browser exec_js)
299
+ app.post("/rpc/exec/:type", authMiddleware, require("express").text({ type: "*/*", limit: "10mb" }), async (req, res) => {
300
+ const type = req.params.type;
301
+ const body = typeof req.body === "string" ? req.body : req.body.toString("utf-8");
302
+ if (!body) return res.status(400).json({ error: "Empty body" });
303
+
304
+ const TMP = require("path").join(require("os").homedir(), "tmp");
305
+ if (!require("fs").existsSync(TMP)) require("fs").mkdirSync(TMP, { recursive: true });
306
+
307
+ try {
308
+ // Re-load tools for hot reload
309
+ const toolModules = require("./tools");
310
+ const toolName = type === "js" ? "exec_js_file" : `exec_${type}_file`;
311
+ let handler = null, schema = null;
312
+ toolModules.forEach((module) => {
313
+ module((name, desc, toolSchema, fn) => {
314
+ if (name === toolName) { handler = fn; schema = toolSchema; }
315
+ });
316
+ });
317
+ if (!handler) return res.status(404).json({ error: `Unknown type: ${type}` });
318
+
319
+ const result = await handler({ content: body, win_id: parseInt(req.query.win_id) || 1 });
320
+ res.json({ result });
321
+ } catch (error) {
322
+ res.status(500).json({ error: error.message });
323
+ }
324
+ });
325
+
326
+ // Start server
327
+ const server = http.createServer(app);
328
+
329
+ // 必须在 whenReady 之前设置调试端口
330
+ electronApp.commandLine.appendSwitch("remote-debugging-port", "9221");
331
+ log.info("[MCP] Remote debugging enabled on port 9221");
332
+
333
+ // IPC Bridge: expose all RPC tools to renderer via ipcMain.handle
334
+ const { ipcMain } = require("electron");
335
+ ipcMain.handle("rpc", async (event, toolName, args) => {
336
+ console.log("[IPC Bridge] called:", toolName, JSON.stringify(args));
337
+ try {
338
+ const toolModules = require("./tools");
339
+ let handler = null, schema = null;
340
+ toolModules.forEach((module) => {
341
+ module((name, desc, toolSchema, fn) => {
342
+ if (name === toolName) { handler = fn; schema = toolSchema; }
343
+ });
344
+ });
345
+ if (!handler) throw new Error("Tool '" + toolName + "' not found");
346
+ const validatedArgs = schema.parse(args || {});
347
+ const result = await handler(validatedArgs);
348
+ console.log("[IPC Bridge] success:", toolName);
349
+ return result;
350
+ } catch(e) { console.error("[IPC Bridge] error:", toolName, e.message); throw e; }
351
+ });
352
+ console.log("[IPC Bridge] All RPC tools available via ipcRenderer.invoke('rpc', toolName, args)");
353
+
354
+ electronApp.whenReady().then(() => {
355
+
356
+ // 为 webview partition 设置代理
357
+ if (config.proxy) {
358
+ const { session } = require("electron");
359
+ const mainSession = session.fromPartition("persist:main");
360
+ mainSession.setProxy({
361
+ proxyRules: config.proxy
362
+ }).then(() => {
363
+ log.info(`[Proxy] persist:main partition 已设置代理: ${config.proxy}`);
364
+ }).catch(err => {
365
+ log.error("[Proxy] persist:main partition 设置代理失败:", err);
366
+ });
367
+ }
368
+ server.listen(PORT, () => {
369
+ log.info(`[MCP] Log file: ${config.logFilePath}`);
370
+ log.info(`[MCP] Server listening on http://localhost:${PORT}`);
371
+ log.info(`[MCP] SSE endpoint: http://localhost:${PORT}/mcp`);
372
+ log.info(`[MCP] REST API docs: http://localhost:${PORT}/docs`);
373
+ log.info(`[MCP] Remote debugger: http://localhost:9221`);
374
+
375
+ createWindow({ url: START_URL ||"https://ide.cicy.de5.net"}, 0);
376
+
377
+ });
378
+ });
379
+
380
+ electronApp.on("window-all-closed", () => {
381
+ // Keep app running
382
+ });
383
+
384
+ function cleanup() {
385
+ log.info("[MCP] Server shutting down");
386
+ server.close();
387
+ electronApp.quit();
388
+ }
389
+
390
+ process.on("SIGTERM", cleanup);
391
+
392
+ // 为所有 session(包括 webview partition)设置代理
393
+ electronApp.on('session-created', (session) => {
394
+ if (config.proxy) {
395
+ session.setProxy({
396
+ proxyRules: config.proxy
397
+ }).then(() => {
398
+ log.info(`[Proxy] Session ${session.partition || 'default'} 已设置代理: ${config.proxy}`);
399
+ }).catch(err => {
400
+ log.error(`[Proxy] Session ${session.partition || 'default'} 设置代理失败:`, err);
401
+ });
402
+ }
403
+ });
@@ -0,0 +1,4 @@
1
+ const { contextBridge, ipcRenderer } = require("electron");
2
+ contextBridge.exposeInMainWorld("electronRPC", {
3
+ invoke: (toolName, args) => ipcRenderer.invoke("rpc", toolName, args),
4
+ });
@@ -0,0 +1,37 @@
1
+ function parseArgs() {
2
+ const args = process.argv.slice(2);
3
+
4
+ let PORT = args.find((arg) => arg.startsWith("--port="))?.split("=")[1];
5
+ if (!PORT) {
6
+ const portIndex = args.indexOf("--port");
7
+ if (portIndex !== -1 && args[portIndex + 1]) {
8
+ PORT = args[portIndex + 1];
9
+ }
10
+ }
11
+ if (!PORT) {
12
+ PORT = process.env.PORT;
13
+ }
14
+ PORT = parseInt(PORT) || 8101;
15
+
16
+ let START_URL = args.find((arg) => arg.startsWith("--url="))?.split("=")[1];
17
+ if (!START_URL) {
18
+ const urlIndex = args.indexOf("--url");
19
+ if (urlIndex !== -1 && args[urlIndex + 1]) {
20
+ START_URL = args[urlIndex + 1];
21
+ }
22
+ }
23
+
24
+ let PROXY = args.find((arg) => arg.startsWith("--proxy="))?.split("=")[1];
25
+ if (!PROXY) {
26
+ const proxyIndex = args.indexOf("--proxy");
27
+ if (proxyIndex !== -1 && args[proxyIndex + 1]) {
28
+ PROXY = args[proxyIndex + 1];
29
+ }
30
+ }
31
+
32
+ const oneWindow = args.includes("--one-window");
33
+
34
+ return { PORT, START_URL, PROXY, oneWindow };
35
+ }
36
+
37
+ module.exports = { parseArgs };
@@ -0,0 +1,33 @@
1
+ const { app: electronApp } = require("electron");
2
+ const log = require("electron-log");
3
+
4
+ function setupElectronFlags() {
5
+ // Must be called before app.whenReady()
6
+ electronApp.commandLine.appendSwitch("no-sandbox");
7
+ electronApp.commandLine.appendSwitch("disable-setuid-sandbox");
8
+
9
+ if (process.platform === "linux") {
10
+ process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
11
+ electronApp.commandLine.appendSwitch("log-level", "3");
12
+ electronApp.commandLine.appendSwitch("disable-notifications");
13
+ electronApp.commandLine.appendSwitch("disable-geolocation");
14
+ electronApp.commandLine.appendSwitch("disable-dev-shm-usage");
15
+ electronApp.commandLine.appendSwitch("disable-gpu");
16
+ electronApp.commandLine.appendSwitch("disable-software-rasterizer");
17
+ electronApp.commandLine.appendSwitch("disable-gpu-compositing");
18
+ electronApp.commandLine.appendSwitch("disable-gpu-rasterization");
19
+ electronApp.commandLine.appendSwitch("use-gl", "swiftshader");
20
+ }
21
+ }
22
+
23
+ function setupErrorHandlers() {
24
+ process.on("uncaughtException", (error) => {
25
+ log.error("[Uncaught Exception]", error);
26
+ });
27
+
28
+ process.on("unhandledRejection", (reason, promise) => {
29
+ log.error("[Unhandled Rejection]", reason);
30
+ });
31
+ }
32
+
33
+ module.exports = { setupElectronFlags, setupErrorHandlers };
@@ -0,0 +1,166 @@
1
+ const express = require("express");
2
+ const cors = require("cors");
3
+ const path = require("path");
4
+ const uiRoutes = require("./ui-routes");
5
+
6
+ function createExpressApp(authMiddleware, tools = {}) {
7
+ const app = express();
8
+
9
+ app.use(
10
+ cors({
11
+ origin: "*",
12
+ methods: ["GET", "POST", "OPTIONS"],
13
+ allowedHeaders: ["Content-Type", "Authorization"],
14
+ })
15
+ );
16
+
17
+ app.use(express.json());
18
+ app.use(express.text());
19
+
20
+ // Health check - no auth
21
+ app.get("/ping", (req, res) => {
22
+ res.json({ ping: "pong", ts: Date.now() });
23
+ });
24
+
25
+ // Swagger UI
26
+ app.get("/docs", (req, res) => {
27
+ const htmlPath = path.join(__dirname, "../swagger-ui.html");
28
+ res.sendFile(htmlPath);
29
+ });
30
+
31
+ // OpenAPI spec - dynamic generation, no auth
32
+ app.get("/openapi.json", (req, res) => {
33
+ const acceptHeader = req.get("Accept") || "application/json";
34
+ const useYaml = acceptHeader.includes("application/yaml");
35
+
36
+ const allTools = Object.entries(tools).flatMap(([tag, toolList]) =>
37
+ toolList.map((tool) => ({ ...tool, tag }))
38
+ );
39
+
40
+ const openapi = {
41
+ openapi: "3.0.0",
42
+ info: {
43
+ title: "Electron MCP REST API",
44
+ version: "1.0.0",
45
+ description: `REST API for Electron MCP tools - ${allTools.length} tools available`,
46
+ },
47
+ servers: [
48
+ {
49
+ url: "https://g-electron.cicy.de5.net",
50
+ description: "Remote server",
51
+ },
52
+ ],
53
+ components: {
54
+ securitySchemes: {
55
+ bearerAuth: {
56
+ type: "http",
57
+ scheme: "bearer",
58
+ bearerFormat: "token",
59
+ },
60
+ },
61
+ },
62
+ security: [{ bearerAuth: [] }],
63
+ paths: {
64
+ "/rpc/tools": {
65
+ get: {
66
+ summary: "List all available tools",
67
+ tags: ["Tools"],
68
+ security: [{ bearerAuth: [] }],
69
+ responses: {
70
+ 200: {
71
+ description: "List of tools",
72
+ content: {
73
+ "application/json": {
74
+ schema: {
75
+ type: "object",
76
+ properties: {
77
+ tools: {
78
+ type: "array",
79
+ items: {
80
+ type: "object",
81
+ properties: {
82
+ name: { type: "string" },
83
+ description: { type: "string" },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ };
97
+
98
+ // Generate endpoint for each tool
99
+ allTools.forEach((tool) => {
100
+ openapi.paths[`/rpc/${tool.name}`] = {
101
+ post: {
102
+ summary: tool.name,
103
+ description: tool.description,
104
+ tags: [tool.tag],
105
+ security: [{ bearerAuth: [] }],
106
+ requestBody: {
107
+ required: true,
108
+ content: {
109
+ "application/yaml": {
110
+ schema: tool.inputSchema,
111
+ },
112
+ "application/json": {
113
+ schema: tool.inputSchema,
114
+ },
115
+ },
116
+ },
117
+ responses: {
118
+ 200: {
119
+ description: "Tool execution result",
120
+ content: {
121
+ "application/json": {
122
+ schema: {
123
+ type: "object",
124
+ properties: {
125
+ content: {
126
+ type: "array",
127
+ items: {
128
+ type: "object",
129
+ properties: {
130
+ type: { type: "string" },
131
+ text: { type: "string" },
132
+ },
133
+ },
134
+ },
135
+ },
136
+ },
137
+ },
138
+ },
139
+ },
140
+ 404: { description: "Tool not found" },
141
+ 500: { description: "Execution error" },
142
+ },
143
+ },
144
+ };
145
+ });
146
+
147
+ if (useYaml) {
148
+ const yaml = require("js-yaml");
149
+ res.type("application/yaml").send(yaml.dump(openapi));
150
+ } else {
151
+ res.json(openapi);
152
+ }
153
+ });
154
+
155
+ // Serve UI page (no auth — page handles token itself)
156
+ app.get("/ui", (req, res) => {
157
+ res.sendFile(path.join(__dirname, "../ui.html"));
158
+ });
159
+
160
+ // Mount UI API routes
161
+ app.use("/ui", uiRoutes);
162
+
163
+ return app;
164
+ }
165
+
166
+ module.exports = { createExpressApp };