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.
- package/.github/workflows/build.yml +85 -0
- package/.kiro/steering/dev-workflow.md +166 -0
- package/AGENTS.md +247 -0
- package/CLAUDE.md +162 -0
- package/DOCKER.md +85 -0
- package/Dockerfile +46 -0
- package/README.md +720 -0
- package/TODO-anti-detection.md +326 -0
- package/bin/cicy +176 -0
- package/bin/preinstall.sh +32 -0
- package/copy-to-desktop.sh +26 -0
- package/docs/AUTOMATION-API.md +342 -0
- package/docs/REQUEST_MONITORING.md +435 -0
- package/docs/REST-API-FEATURE.md +155 -0
- package/docs/REST-API.md +319 -0
- package/docs/feature-distributed-multi-agent.md +555 -0
- package/docs/yaml.md +255 -0
- package/electron-mcp-fixed.command +134 -0
- package/electron-mcp-simple.command +135 -0
- package/electron-mcp.command +92 -0
- package/generate-openapi.js +158 -0
- package/jest.config.js +10 -0
- package/jest.setup.global.js +13 -0
- package/jest.teardown.global.js +7 -0
- package/package.json +75 -0
- package/service.sh +164 -0
- package/src/config.js +8 -0
- package/src/extension/inject.js +135 -0
- package/src/main-old.js +837 -0
- package/src/main.js +403 -0
- package/src/preload-rpc.js +4 -0
- package/src/server/args-parser.js +37 -0
- package/src/server/electron-setup.js +33 -0
- package/src/server/express-app.js +166 -0
- package/src/server/logging.js +58 -0
- package/src/server/mcp-server.js +53 -0
- package/src/server/tool-registry.js +77 -0
- package/src/server/ui-routes.js +81 -0
- package/src/swagger-ui.html +41 -0
- package/src/tools/account-tools.js +194 -0
- package/src/tools/automation-tools.js +297 -0
- package/src/tools/cdp-tools.js +444 -0
- package/src/tools/clipboard-tools.js +180 -0
- package/src/tools/download-tools.js +57 -0
- package/src/tools/exec-js.js +297 -0
- package/src/tools/exec-tools.js +139 -0
- package/src/tools/file-tools.js +212 -0
- package/src/tools/hook-chatgpt.js +489 -0
- package/src/tools/hook-gemini.js +454 -0
- package/src/tools/index.js +19 -0
- package/src/tools/ipc-bridge.js +31 -0
- package/src/tools/ping.js +60 -0
- package/src/tools/r-reset.js +28 -0
- package/src/tools/screenshot-tools.js +28 -0
- package/src/tools/system-tools.js +531 -0
- package/src/tools/window-tools.js +882 -0
- package/src/ui.html +914 -0
- package/src/utils/auth.js +81 -0
- package/src/utils/cdp-utils.js +8 -0
- package/src/utils/download-manager.js +41 -0
- package/src/utils/process-utils.js +185 -0
- package/src/utils/snapshot-utils.js +56 -0
- package/src/utils/window-monitor.js +605 -0
- package/src/utils/window-state.js +137 -0
- package/src/utils/window-utils.js +336 -0
- package/update-desktop.sh +33 -0
package/src/main-old.js
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
const { app: electronApp, BrowserWindow } = require("electron");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const log = require("electron-log");
|
|
5
|
+
const express = require("express");
|
|
6
|
+
const cors = require("cors");
|
|
7
|
+
const http = require("http");
|
|
8
|
+
const swaggerUi = require("swagger-ui-express");
|
|
9
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
10
|
+
const { SSEServerTransport } = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
11
|
+
const { z } = require("zod");
|
|
12
|
+
const { config } = require("./config");
|
|
13
|
+
const { createWindow } = require("./utils/window-utils");
|
|
14
|
+
const { AuthManager } = require("./utils/auth");
|
|
15
|
+
|
|
16
|
+
if (process.platform === "linux") {
|
|
17
|
+
// electronApp.disableHardwareAcceleration(); // 这一行比命令行开关更彻底
|
|
18
|
+
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
|
|
19
|
+
electronApp.commandLine.appendSwitch("log-level", "3");
|
|
20
|
+
// electronApp.commandLine.appendSwitch("no-sandbox");
|
|
21
|
+
|
|
22
|
+
electronApp.commandLine.appendSwitch("disable-notifications");
|
|
23
|
+
electronApp.commandLine.appendSwitch("disable-geolocation");
|
|
24
|
+
|
|
25
|
+
electronApp.commandLine.appendSwitch("disable-dev-shm-usage");
|
|
26
|
+
electronApp.commandLine.appendSwitch("disable-setuid-sandbox");
|
|
27
|
+
|
|
28
|
+
// --- 新增下面这几行以修复 :2 白屏问题 ---
|
|
29
|
+
electronApp.commandLine.appendSwitch("disable-gpu");
|
|
30
|
+
electronApp.commandLine.appendSwitch("disable-software-rasterizer");
|
|
31
|
+
electronApp.commandLine.appendSwitch("disable-gpu-compositing");
|
|
32
|
+
electronApp.commandLine.appendSwitch("disable-gpu-rasterization");
|
|
33
|
+
// 强制使用 SwiftShader 或逻辑渲染器
|
|
34
|
+
electronApp.commandLine.appendSwitch("use-gl", "swiftshader");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 捕获未处理的异常,防止弹窗
|
|
38
|
+
process.on("uncaughtException", (error) => {
|
|
39
|
+
log.error("[Uncaught Exception]", error);
|
|
40
|
+
console.error("[Uncaught Exception]", error);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
44
|
+
log.error("[Unhandled Rejection]", reason);
|
|
45
|
+
console.error("[Unhandled Rejection]", reason);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const transports = new Map();
|
|
49
|
+
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
let PORT = args.find((arg) => arg.startsWith("--port="))?.split("=")[1];
|
|
52
|
+
if (!PORT) {
|
|
53
|
+
const portIndex = args.indexOf("--port");
|
|
54
|
+
if (portIndex !== -1 && args[portIndex + 1]) {
|
|
55
|
+
PORT = args[portIndex + 1];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!PORT) {
|
|
59
|
+
PORT = process.env.PORT;
|
|
60
|
+
}
|
|
61
|
+
PORT = parseInt(PORT) || 8101;
|
|
62
|
+
config.port = PORT;
|
|
63
|
+
|
|
64
|
+
let START_URL = args.find((arg) => arg.startsWith("--url="))?.split("=")[1];
|
|
65
|
+
if (!START_URL) {
|
|
66
|
+
const urlIndex = args.indexOf("--url");
|
|
67
|
+
if (urlIndex !== -1 && args[urlIndex + 1]) {
|
|
68
|
+
START_URL = args[urlIndex + 1];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for --one-window argument
|
|
73
|
+
if (args.includes("--one-window")) {
|
|
74
|
+
config.oneWindow = true;
|
|
75
|
+
log.info("[MCP] Single window mode enabled via --one-window");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const logsDir = path.join(electronApp.getPath("home"), "logs");
|
|
79
|
+
if (!fs.existsSync(logsDir)) {
|
|
80
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
config.logsDir = logsDir;
|
|
83
|
+
config.logFilePath = path.join(logsDir, `electron-mcp-${config.port}.log`);
|
|
84
|
+
|
|
85
|
+
log.transports.file.resolvePathFn = () => config.logFilePath;
|
|
86
|
+
|
|
87
|
+
// 配置日志格式
|
|
88
|
+
log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}";
|
|
89
|
+
log.transports.console.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}";
|
|
90
|
+
|
|
91
|
+
// 包装 log 方法以添加调用位置信息
|
|
92
|
+
const originalInfo = log.info.bind(log);
|
|
93
|
+
const originalError = log.error.bind(log);
|
|
94
|
+
const originalWarn = log.warn.bind(log);
|
|
95
|
+
const originalDebug = log.debug.bind(log);
|
|
96
|
+
|
|
97
|
+
const getCallerInfo = () => {
|
|
98
|
+
const stack = new Error().stack;
|
|
99
|
+
const stackLines = stack.split("\n");
|
|
100
|
+
for (let i = 3; i < stackLines.length; i++) {
|
|
101
|
+
const line = stackLines[i];
|
|
102
|
+
if (line.includes("/src/") || line.includes("/tools/")) {
|
|
103
|
+
// 匹配格式: "at functionName (path:line:col)" 或 "at path:line:col"
|
|
104
|
+
const matchWithFunc = line.match(/at\s+(\S+)\s+\((.+):(\d+):\d+\)/);
|
|
105
|
+
const matchNoFunc = line.match(/at\s+(.+):(\d+):\d+/);
|
|
106
|
+
|
|
107
|
+
if (matchWithFunc) {
|
|
108
|
+
const funcName = matchWithFunc[1];
|
|
109
|
+
const fullPath = matchWithFunc[2];
|
|
110
|
+
const fileName = fullPath.split("/").pop();
|
|
111
|
+
const lineNumber = matchWithFunc[3];
|
|
112
|
+
return `(${fileName}:${funcName}:${lineNumber})`;
|
|
113
|
+
} else if (matchNoFunc) {
|
|
114
|
+
const fullPath = matchNoFunc[1];
|
|
115
|
+
const fileName = fullPath.split("/").pop();
|
|
116
|
+
const lineNumber = matchNoFunc[2];
|
|
117
|
+
return `(${fileName}:${lineNumber})`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return "";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
log.info = (...args) => originalInfo(...args, getCallerInfo());
|
|
125
|
+
log.error = (...args) => originalError(...args, getCallerInfo());
|
|
126
|
+
log.warn = (...args) => originalWarn(...args, getCallerInfo());
|
|
127
|
+
log.debug = (...args) => originalDebug(...args, getCallerInfo());
|
|
128
|
+
|
|
129
|
+
log.info(`[MCP] Server starting at ${new Date().toISOString()}`);
|
|
130
|
+
|
|
131
|
+
// 初始化认证管理器
|
|
132
|
+
const authManager = new AuthManager();
|
|
133
|
+
|
|
134
|
+
// 将 Zod schema 转换为 JSON Schema
|
|
135
|
+
function zodToJsonSchema(zodSchema) {
|
|
136
|
+
if (!zodSchema || !zodSchema._def) {
|
|
137
|
+
return { type: "object" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const def = zodSchema._def;
|
|
141
|
+
|
|
142
|
+
if (def.typeName === "ZodObject") {
|
|
143
|
+
const properties = {};
|
|
144
|
+
const required = [];
|
|
145
|
+
|
|
146
|
+
const shape = def.shape();
|
|
147
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
148
|
+
properties[key] = zodToJsonSchema(value);
|
|
149
|
+
// 检查是否是 optional 或 default
|
|
150
|
+
const isOptional =
|
|
151
|
+
value._def.typeName === "ZodOptional" || value._def.typeName === "ZodDefault";
|
|
152
|
+
if (!isOptional) {
|
|
153
|
+
required.push(key);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties,
|
|
160
|
+
...(required.length > 0 && { required }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (def.typeName === "ZodString") {
|
|
165
|
+
return { type: "string" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (def.typeName === "ZodNumber") {
|
|
169
|
+
return { type: "number" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (def.typeName === "ZodBoolean") {
|
|
173
|
+
return { type: "boolean" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (def.typeName === "ZodArray") {
|
|
177
|
+
return {
|
|
178
|
+
type: "array",
|
|
179
|
+
items: zodToJsonSchema(def.type),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (def.typeName === "ZodOptional") {
|
|
184
|
+
return zodToJsonSchema(def.innerType);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (def.typeName === "ZodDefault") {
|
|
188
|
+
const schema = zodToJsonSchema(def.innerType);
|
|
189
|
+
schema.default = def.defaultValue();
|
|
190
|
+
return schema;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (def.typeName === "ZodEnum") {
|
|
194
|
+
return {
|
|
195
|
+
type: "string",
|
|
196
|
+
enum: def.values,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { type: "object" };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const app = express();
|
|
204
|
+
const server = http.createServer(app);
|
|
205
|
+
|
|
206
|
+
app.use(
|
|
207
|
+
cors({
|
|
208
|
+
origin: "*",
|
|
209
|
+
methods: ["GET", "POST", "OPTIONS"],
|
|
210
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
app.use(express.json({ limit: "50mb" }));
|
|
214
|
+
|
|
215
|
+
// Ping endpoint - 无需认证
|
|
216
|
+
app.get("/ping", (req, res) => {
|
|
217
|
+
res.json({ ping: "pong", ts: Date.now() });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// OpenAPI spec - 无需认证,动态生成,默认 YAML
|
|
221
|
+
app.get("/openapi.json", (req, res) => {
|
|
222
|
+
const acceptHeader = req.get("Accept") || "application/yaml";
|
|
223
|
+
const useJson =
|
|
224
|
+
acceptHeader.includes("application/json") && !acceptHeader.includes("application/yaml");
|
|
225
|
+
|
|
226
|
+
const tools = Array.from(toolHandlers.keys()).map((name) => ({
|
|
227
|
+
name: name,
|
|
228
|
+
description: toolDescriptions.get(name) || "",
|
|
229
|
+
schema: zodToJsonSchema(toolSchemas.get(name)),
|
|
230
|
+
tag: toolTags.get(name) || "Tools",
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
const openapi = {
|
|
234
|
+
openapi: "3.0.0",
|
|
235
|
+
info: {
|
|
236
|
+
title: "Electron MCP REST API",
|
|
237
|
+
version: "1.0.0",
|
|
238
|
+
description: `REST API for Electron MCP tools - ${tools.length} tools available`,
|
|
239
|
+
},
|
|
240
|
+
servers: [
|
|
241
|
+
{
|
|
242
|
+
url: "https://g-electron.cicy.de5.net",
|
|
243
|
+
description: "Remote server",
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
components: {
|
|
247
|
+
securitySchemes: {
|
|
248
|
+
bearerAuth: {
|
|
249
|
+
type: "http",
|
|
250
|
+
scheme: "bearer",
|
|
251
|
+
bearerFormat: "token",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
security: [{ bearerAuth: [] }],
|
|
256
|
+
paths: {
|
|
257
|
+
"/rpc/tools": {
|
|
258
|
+
get: {
|
|
259
|
+
summary: "List all available tools",
|
|
260
|
+
tags: ["Tools"],
|
|
261
|
+
security: [{ bearerAuth: [] }],
|
|
262
|
+
responses: {
|
|
263
|
+
200: {
|
|
264
|
+
description: "List of tools",
|
|
265
|
+
content: {
|
|
266
|
+
"application/json": {
|
|
267
|
+
schema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
tools: {
|
|
271
|
+
type: "array",
|
|
272
|
+
items: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
name: { type: "string" },
|
|
276
|
+
description: { type: "string" },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// 为每个工具生成端点
|
|
292
|
+
tools.forEach((tool) => {
|
|
293
|
+
openapi.paths[`/rpc/${tool.name}`] = {
|
|
294
|
+
post: {
|
|
295
|
+
description: tool.description,
|
|
296
|
+
tags: [tool.tag],
|
|
297
|
+
security: [{ bearerAuth: [] }],
|
|
298
|
+
requestBody: {
|
|
299
|
+
required: true,
|
|
300
|
+
content: {
|
|
301
|
+
"application/json": {
|
|
302
|
+
schema: tool.schema,
|
|
303
|
+
},
|
|
304
|
+
"application/yaml": {
|
|
305
|
+
schema: tool.schema,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
responses: {
|
|
310
|
+
200: {
|
|
311
|
+
description: "Tool execution result",
|
|
312
|
+
content: {
|
|
313
|
+
"application/json": {
|
|
314
|
+
schema: {
|
|
315
|
+
type: "object",
|
|
316
|
+
properties: {
|
|
317
|
+
content: {
|
|
318
|
+
type: "array",
|
|
319
|
+
items: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
type: { type: "string" },
|
|
323
|
+
text: { type: "string" },
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
"application/yaml": {
|
|
331
|
+
schema: {
|
|
332
|
+
type: "string",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
404: { description: "Tool not found" },
|
|
338
|
+
500: { description: "Execution error" },
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (useJson) {
|
|
345
|
+
res.json(openapi);
|
|
346
|
+
} else {
|
|
347
|
+
const yaml = require("js-yaml");
|
|
348
|
+
res.type("application/yaml").send(yaml.dump(openapi));
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// 认证中间件
|
|
353
|
+
function authMiddleware(req, res, next) {
|
|
354
|
+
if (!authManager.validateAuth(req)) {
|
|
355
|
+
log.warn("[MCP] Unauthorized access attempt:", req.url);
|
|
356
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
next();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const mcpServer = new McpServer({
|
|
363
|
+
name: "electron-mcp",
|
|
364
|
+
version: "1.0.0",
|
|
365
|
+
description: "Electron MCP Server with browser automation tools",
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// 保存工具处理函数
|
|
369
|
+
const toolHandlers = new Map();
|
|
370
|
+
const toolDescriptions = new Map();
|
|
371
|
+
const toolSchemas = new Map();
|
|
372
|
+
const toolTags = new Map();
|
|
373
|
+
|
|
374
|
+
function registerTool(title, description, schema, handler, options = {}) {
|
|
375
|
+
const { tag = "Tools" } = options;
|
|
376
|
+
|
|
377
|
+
// 确保 schema 是有效的 Zod 对象
|
|
378
|
+
if (!schema || typeof schema !== "object" || !schema._def) {
|
|
379
|
+
log.warn(`Tool "${title}" has invalid schema, using empty object`);
|
|
380
|
+
log.warn(` Schema type: ${typeof schema}, has _def: ${schema && schema._def ? "yes" : "no"}`);
|
|
381
|
+
schema = z.object({});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 保存处理函数、描述、schema 和 tag
|
|
385
|
+
toolHandlers.set(title, handler);
|
|
386
|
+
toolDescriptions.set(title, description);
|
|
387
|
+
toolSchemas.set(title, schema);
|
|
388
|
+
toolTags.set(title, tag);
|
|
389
|
+
|
|
390
|
+
mcpServer.registerTool(
|
|
391
|
+
title,
|
|
392
|
+
{
|
|
393
|
+
title,
|
|
394
|
+
description,
|
|
395
|
+
inputSchema: schema,
|
|
396
|
+
},
|
|
397
|
+
async (s) => {
|
|
398
|
+
try {
|
|
399
|
+
return handler(s);
|
|
400
|
+
} catch (e) {
|
|
401
|
+
log.error("Error", title, e);
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: "text", text: `${title} invoke error:${e},tool desc: ${description}` }],
|
|
404
|
+
isError: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
//log.debug(`Registered tool: ${title}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
require("./tools/ping")(registerTool);
|
|
413
|
+
require("./tools/window-tools")(registerTool);
|
|
414
|
+
require("./tools/cdp-tools")(registerTool);
|
|
415
|
+
require("./tools/exec-js")(registerTool);
|
|
416
|
+
|
|
417
|
+
function createTransport(res) {
|
|
418
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
419
|
+
transports[transport.sessionId] = transport;
|
|
420
|
+
|
|
421
|
+
const originalSend = transport.send.bind(transport);
|
|
422
|
+
transport.send = async (message) => {
|
|
423
|
+
return originalSend(message);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return transport;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
app.get("/mcp", authMiddleware, async (req, res) => {
|
|
430
|
+
try {
|
|
431
|
+
const transport = createTransport(res);
|
|
432
|
+
const mcpServer = createMcpServer(); // 为每个连接创建新的 server 实例
|
|
433
|
+
|
|
434
|
+
res.on("close", () => {
|
|
435
|
+
delete transports[transport.sessionId];
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// 包装 transport 的 send 方法来捕获错误
|
|
439
|
+
const originalSend = transport.send.bind(transport);
|
|
440
|
+
transport.send = async (message) => {
|
|
441
|
+
try {
|
|
442
|
+
return await originalSend(message);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
log.error("[MCP] Error sending message:", error);
|
|
445
|
+
log.error("[MCP] Message:", JSON.stringify(message, null, 2));
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
await mcpServer.connect(transport);
|
|
451
|
+
log.info("[MCP] SSE connection established:", transport.sessionId, req.url);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
log.error("[MCP] SSE error:", error);
|
|
454
|
+
log.error("[MCP] SSE error stack:", error.stack);
|
|
455
|
+
res.status(500).end();
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
app.post("/messages", authMiddleware, async (req, res) => {
|
|
460
|
+
const sessionId = req.query.sessionId;
|
|
461
|
+
const transport = transports[sessionId];
|
|
462
|
+
|
|
463
|
+
if (!transport) {
|
|
464
|
+
res.status(400).send("No transport found for sessionId");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
log.debug(`[MCP] Handling message: ${req.body.method}`);
|
|
470
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
log.error("[MCP] Error handling message:", error);
|
|
473
|
+
log.error("[MCP] Error stack:", error.stack);
|
|
474
|
+
res.status(500).json({ error: error.message });
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// RPC endpoint - 直接调用工具,不需要 SSE
|
|
479
|
+
app.post("/rpc", authMiddleware, express.json(), async (req, res) => {
|
|
480
|
+
try {
|
|
481
|
+
const { method, params } = req.body;
|
|
482
|
+
const acceptHeader = req.get("Accept") || "application/json";
|
|
483
|
+
const useYaml = acceptHeader.includes("application/yaml");
|
|
484
|
+
|
|
485
|
+
if (method === "tools/call") {
|
|
486
|
+
const { name, arguments: args } = params;
|
|
487
|
+
|
|
488
|
+
// 从 Map 中获取处理函数
|
|
489
|
+
const handler = toolHandlers.get(name);
|
|
490
|
+
if (!handler) {
|
|
491
|
+
const errorResponse = {
|
|
492
|
+
jsonrpc: "2.0",
|
|
493
|
+
id: req.body.id || 1,
|
|
494
|
+
error: { message: `Tool not found: ${name}` },
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
if (useYaml) {
|
|
498
|
+
const yaml = require("js-yaml");
|
|
499
|
+
res.type("application/yaml").send(yaml.dump(errorResponse));
|
|
500
|
+
} else {
|
|
501
|
+
res.status(404).json(errorResponse);
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 直接调用处理函数
|
|
507
|
+
const result = await handler(args);
|
|
508
|
+
|
|
509
|
+
const response = {
|
|
510
|
+
jsonrpc: "2.0",
|
|
511
|
+
id: req.body.id || 1,
|
|
512
|
+
result: result,
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
if (useYaml) {
|
|
516
|
+
const yaml = require("js-yaml");
|
|
517
|
+
res.type("application/yaml").send(yaml.dump(response));
|
|
518
|
+
} else {
|
|
519
|
+
res.json(response);
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
res.status(400).json({ error: "Unsupported method" });
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
log.error("[RPC] Error:", error);
|
|
526
|
+
const errorResponse = {
|
|
527
|
+
jsonrpc: "2.0",
|
|
528
|
+
id: req.body.id || 1,
|
|
529
|
+
error: { message: error.message },
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const acceptHeader = req.get("Accept") || "application/json";
|
|
533
|
+
if (acceptHeader.includes("application/yaml")) {
|
|
534
|
+
const yaml = require("js-yaml");
|
|
535
|
+
res.type("application/yaml").send(yaml.dump(errorResponse));
|
|
536
|
+
} else {
|
|
537
|
+
res.status(500).json(errorResponse);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// RPC tools list - 返回所有可用工具
|
|
543
|
+
app.get("/rpc/tools", authMiddleware, (req, res) => {
|
|
544
|
+
const acceptHeader = req.get("Accept") || "application/yaml";
|
|
545
|
+
const useJson =
|
|
546
|
+
acceptHeader.includes("application/json") && !acceptHeader.includes("application/yaml");
|
|
547
|
+
|
|
548
|
+
const tools = Array.from(toolHandlers.keys()).map((name) => ({
|
|
549
|
+
name: name,
|
|
550
|
+
description: toolDescriptions.get(name) || "",
|
|
551
|
+
}));
|
|
552
|
+
|
|
553
|
+
const result = { tools };
|
|
554
|
+
|
|
555
|
+
if (useJson) {
|
|
556
|
+
res.json(result);
|
|
557
|
+
} else {
|
|
558
|
+
const yaml = require("js-yaml");
|
|
559
|
+
res.type("application/yaml").send(yaml.dump(result));
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// RPC tools schemas - 返回所有工具的 schema(用于生成 OpenAPI)
|
|
564
|
+
app.get("/rpc/schemas", authMiddleware, (req, res) => {
|
|
565
|
+
const schemas = {};
|
|
566
|
+
|
|
567
|
+
// 从保存的 toolSchemas 转换
|
|
568
|
+
for (const [name, zodSchema] of toolSchemas) {
|
|
569
|
+
try {
|
|
570
|
+
schemas[name] = zodToJsonSchema(zodSchema);
|
|
571
|
+
} catch (e) {
|
|
572
|
+
log.error(`Error converting schema for ${name}:`, e);
|
|
573
|
+
schemas[name] = { type: "object" };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
res.json({ schemas });
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// REST API - 为每个工具创建独立端点
|
|
581
|
+
app.post("/rpc/:toolName", authMiddleware, express.json(), async (req, res) => {
|
|
582
|
+
try {
|
|
583
|
+
const { toolName } = req.params;
|
|
584
|
+
const args = req.body;
|
|
585
|
+
const acceptHeader = req.get("Accept") || "application/yaml";
|
|
586
|
+
const useJson =
|
|
587
|
+
acceptHeader.includes("application/json") && !acceptHeader.includes("application/yaml");
|
|
588
|
+
|
|
589
|
+
const handler = toolHandlers.get(toolName);
|
|
590
|
+
if (!handler) {
|
|
591
|
+
const errorResponse = {
|
|
592
|
+
error: `Tool not found: ${toolName}`,
|
|
593
|
+
available: Array.from(toolHandlers.keys()),
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
if (useJson) {
|
|
597
|
+
return res.status(404).json(errorResponse);
|
|
598
|
+
} else {
|
|
599
|
+
const yaml = require("js-yaml");
|
|
600
|
+
return res.status(404).type("application/yaml").send(yaml.dump(errorResponse));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const result = await handler(args);
|
|
605
|
+
|
|
606
|
+
if (useJson) {
|
|
607
|
+
res.json(result);
|
|
608
|
+
} else {
|
|
609
|
+
const yaml = require("js-yaml");
|
|
610
|
+
res.type("application/yaml").send(yaml.dump(result));
|
|
611
|
+
}
|
|
612
|
+
} catch (error) {
|
|
613
|
+
log.error(`[REST] Error calling ${req.params.toolName}:`, error);
|
|
614
|
+
const acceptHeader = req.get("Accept") || "application/yaml";
|
|
615
|
+
const useJson =
|
|
616
|
+
acceptHeader.includes("application/json") && !acceptHeader.includes("application/yaml");
|
|
617
|
+
|
|
618
|
+
if (useJson) {
|
|
619
|
+
res.status(500).json({ error: error.message });
|
|
620
|
+
} else {
|
|
621
|
+
const yaml = require("js-yaml");
|
|
622
|
+
res
|
|
623
|
+
.status(500)
|
|
624
|
+
.type("application/yaml")
|
|
625
|
+
.send(yaml.dump({ error: error.message }));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Python execution endpoint
|
|
631
|
+
app.post("/rpc/python", authMiddleware, express.json(), async (req, res) => {
|
|
632
|
+
try {
|
|
633
|
+
const { code } = req.body;
|
|
634
|
+
if (!code) {
|
|
635
|
+
return res.status(400).json({ error: "code is required" });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const { exec } = require("child_process");
|
|
639
|
+
const pythonPath = path.join(__dirname, "..", "python-exec.py");
|
|
640
|
+
|
|
641
|
+
exec(`python3 ${pythonPath} '${code.replace(/'/g, "'\\''")}'`, (error, stdout, stderr) => {
|
|
642
|
+
if (error) {
|
|
643
|
+
return res.json({
|
|
644
|
+
content: [
|
|
645
|
+
{
|
|
646
|
+
type: "text",
|
|
647
|
+
text: JSON.stringify({ success: false, error: stderr || error.message }),
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
const result = JSON.parse(stdout);
|
|
655
|
+
res.json({
|
|
656
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
657
|
+
});
|
|
658
|
+
} catch (e) {
|
|
659
|
+
res.json({
|
|
660
|
+
content: [{ type: "text", text: stdout }],
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
} catch (error) {
|
|
665
|
+
log.error("[Python] Error:", error);
|
|
666
|
+
res.status(500).json({ error: error.message });
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Enable remote debugging
|
|
671
|
+
const REMOTE_DEBUGGING_PORT =
|
|
672
|
+
args.find((arg) => arg.startsWith("--remote-debugging-port="))?.split("=")[1] ||
|
|
673
|
+
(args.indexOf("--remote-debugging-port") !== -1
|
|
674
|
+
? args[args.indexOf("--remote-debugging-port") + 1]
|
|
675
|
+
: null) ||
|
|
676
|
+
process.env.REMOTE_DEBUGGING_PORT ||
|
|
677
|
+
"9222";
|
|
678
|
+
|
|
679
|
+
electronApp.commandLine.appendSwitch("remote-debugging-port", REMOTE_DEBUGGING_PORT);
|
|
680
|
+
log.info(`[MCP] Remote debugging enabled on port ${REMOTE_DEBUGGING_PORT}`);
|
|
681
|
+
|
|
682
|
+
electronApp.whenReady().then(() => {
|
|
683
|
+
log.info(`[MCP] Log file: ${config.logFilePath}`);
|
|
684
|
+
log.info(`[MCP] Server listening on http://localhost:${PORT}`);
|
|
685
|
+
log.info(`[MCP] SSE endpoint: http://localhost:${PORT}/mcp`);
|
|
686
|
+
log.info(`[MCP] REST API docs: http://localhost:${PORT}/docs`);
|
|
687
|
+
log.info(`[MCP] Remote debugger: http://localhost:${REMOTE_DEBUGGING_PORT}`);
|
|
688
|
+
|
|
689
|
+
// 动态生成 Swagger spec
|
|
690
|
+
const tools = Array.from(toolHandlers.keys()).map((name) => ({
|
|
691
|
+
name: name,
|
|
692
|
+
description: toolDescriptions.get(name) || "",
|
|
693
|
+
}));
|
|
694
|
+
|
|
695
|
+
// 为每个工具生成 path
|
|
696
|
+
const paths = {
|
|
697
|
+
"/rpc/tools": {
|
|
698
|
+
get: {
|
|
699
|
+
summary: "List all available tools",
|
|
700
|
+
tags: ["Tools"],
|
|
701
|
+
security: [{ bearerAuth: [] }],
|
|
702
|
+
responses: {
|
|
703
|
+
200: {
|
|
704
|
+
description: "List of tools",
|
|
705
|
+
content: {
|
|
706
|
+
"application/json": {
|
|
707
|
+
schema: {
|
|
708
|
+
type: "object",
|
|
709
|
+
properties: {
|
|
710
|
+
tools: {
|
|
711
|
+
type: "array",
|
|
712
|
+
items: {
|
|
713
|
+
type: "object",
|
|
714
|
+
properties: {
|
|
715
|
+
name: { type: "string" },
|
|
716
|
+
description: { type: "string" },
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// 为每个工具生成端点
|
|
731
|
+
tools.forEach((tool) => {
|
|
732
|
+
paths[`/rpc/${tool.name}`] = {
|
|
733
|
+
post: {
|
|
734
|
+
summary: tool.description || `Call ${tool.name}`,
|
|
735
|
+
description: tool.description,
|
|
736
|
+
tags: ["Tools"],
|
|
737
|
+
security: [{ bearerAuth: [] }],
|
|
738
|
+
requestBody: {
|
|
739
|
+
required: true,
|
|
740
|
+
content: {
|
|
741
|
+
"application/json": {
|
|
742
|
+
schema: { type: "object" },
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
responses: {
|
|
747
|
+
200: {
|
|
748
|
+
description: "Tool execution result",
|
|
749
|
+
content: {
|
|
750
|
+
"application/json": {
|
|
751
|
+
schema: {
|
|
752
|
+
type: "object",
|
|
753
|
+
properties: {
|
|
754
|
+
content: {
|
|
755
|
+
type: "array",
|
|
756
|
+
items: {
|
|
757
|
+
type: "object",
|
|
758
|
+
properties: {
|
|
759
|
+
type: { type: "string" },
|
|
760
|
+
text: { type: "string" },
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
404: { description: "Tool not found" },
|
|
770
|
+
500: { description: "Execution error" },
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const swaggerSpec = {
|
|
777
|
+
openapi: "3.0.0",
|
|
778
|
+
info: {
|
|
779
|
+
title: "Electron MCP REST API",
|
|
780
|
+
version: "1.0.0",
|
|
781
|
+
description: `REST API for Electron MCP tools - ${tools.length} tools available`,
|
|
782
|
+
},
|
|
783
|
+
servers: [
|
|
784
|
+
{
|
|
785
|
+
url: "https://g-electron.cicy.de5.net",
|
|
786
|
+
description: "Remote server",
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
components: {
|
|
790
|
+
securitySchemes: {
|
|
791
|
+
bearerAuth: {
|
|
792
|
+
type: "http",
|
|
793
|
+
scheme: "bearer",
|
|
794
|
+
bearerFormat: "token",
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
security: [{ bearerAuth: [] }],
|
|
799
|
+
paths: paths,
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
// Swagger UI - 使用自定义 HTML
|
|
803
|
+
app.get("/docs", (req, res) => {
|
|
804
|
+
res.sendFile(__dirname + "/swagger-ui.html");
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (START_URL) {
|
|
808
|
+
log.info(`[MCP] Opening window with URL: ${START_URL}`);
|
|
809
|
+
createWindow({ url: START_URL });
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
server.listen(PORT).on("error", (err) => {
|
|
813
|
+
if (err.code === "EADDRINUSE") {
|
|
814
|
+
log.error(`[MCP] Port ${PORT} is already in use`);
|
|
815
|
+
} else {
|
|
816
|
+
log.error("[MCP] Server error:", err);
|
|
817
|
+
}
|
|
818
|
+
electronApp.quit();
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
electronApp.on("window-all-closed", () => {
|
|
823
|
+
if (!START_URL) {
|
|
824
|
+
electronApp.quit();
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
function cleanup() {
|
|
829
|
+
log.info("[MCP] Shutting down...");
|
|
830
|
+
server.close(() => {
|
|
831
|
+
log.info("[MCP] HTTP server closed");
|
|
832
|
+
electronApp.quit();
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
process.on("SIGINT", cleanup);
|
|
837
|
+
process.on("SIGTERM", cleanup);
|