sweetspot-remote-agent 1.8.2 → 1.8.4
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/mcp-server.js +121 -72
- package/package.json +2 -2
package/mcp-server.js
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11
|
-
import express from "express";
|
|
12
10
|
import crypto from "crypto";
|
|
13
11
|
import { z } from "zod";
|
|
14
12
|
|
|
@@ -253,8 +251,6 @@ function createServer() {
|
|
|
253
251
|
// ── 서버 시작 ──
|
|
254
252
|
const args = process.argv.slice(2);
|
|
255
253
|
const isStdio = args.includes("--stdio");
|
|
256
|
-
const portIdx = args.indexOf("--port");
|
|
257
|
-
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 8080;
|
|
258
254
|
|
|
259
255
|
import os from "os";
|
|
260
256
|
|
|
@@ -277,92 +273,145 @@ if (!isStdio && !noRegister && !regName) {
|
|
|
277
273
|
process.exit(1);
|
|
278
274
|
}
|
|
279
275
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
for (const iface of Object.values(nets)) {
|
|
283
|
-
for (const net of iface) {
|
|
284
|
-
if (net.family === "IPv4" && !net.internal) return net.address;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return "127.0.0.1";
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function autoRegister() {
|
|
291
|
-
if (noRegister || isStdio) return;
|
|
292
|
-
|
|
293
|
-
const localIP = getLocalIP();
|
|
294
|
-
const agentUrl = `http://${localIP}:${port}`;
|
|
276
|
+
// ── 토큰 자동 생성/저장 ──
|
|
277
|
+
import fs from "fs";
|
|
295
278
|
|
|
296
|
-
|
|
279
|
+
const tokenFile = `${os.homedir()}/.sweetspot-token`;
|
|
297
280
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"ngrok-skip-browser-warning": "true",
|
|
304
|
-
},
|
|
305
|
-
body: JSON.stringify({ name: regName, agentUrl }),
|
|
306
|
-
});
|
|
307
|
-
const data = await res.json();
|
|
308
|
-
if (data.ok) {
|
|
309
|
-
console.error(`[register] ✅ 등록 완료! Slack 유저: ${data.name}`);
|
|
310
|
-
} else {
|
|
311
|
-
console.error(`[register] ❌ 등록 실패: ${data.error}`);
|
|
312
|
-
}
|
|
313
|
-
} catch (e) {
|
|
314
|
-
console.error(`[register] ❌ 봇 서버 연결 실패: ${e.message}`);
|
|
315
|
-
}
|
|
281
|
+
function loadOrCreateToken() {
|
|
282
|
+
if (fs.existsSync(tokenFile)) return fs.readFileSync(tokenFile, "utf-8").trim();
|
|
283
|
+
const token = crypto.randomUUID();
|
|
284
|
+
fs.writeFileSync(tokenFile, token, "utf-8");
|
|
285
|
+
return token;
|
|
316
286
|
}
|
|
317
287
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
288
|
+
// ── WebSocket 클라이언트 (GCP 역방향 연결) ──
|
|
289
|
+
async function connectWs(name, token) {
|
|
290
|
+
const { default: WebSocket } = await import("ws");
|
|
291
|
+
const wsUrl = BOT_SERVER.replace(/^http/, "ws") + "/agent";
|
|
292
|
+
let alive = true;
|
|
323
293
|
|
|
324
|
-
|
|
325
|
-
|
|
294
|
+
function connect() {
|
|
295
|
+
if (!alive) return;
|
|
296
|
+
const ws = new WebSocket(wsUrl);
|
|
326
297
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
298
|
+
ws.on("open", () => {
|
|
299
|
+
ws.send(JSON.stringify({ type: "register", name, token }));
|
|
300
|
+
console.error(`[remote-agent] GCP 서버에 연결 중... (${name})`);
|
|
301
|
+
});
|
|
330
302
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
transport.onclose = () => {
|
|
338
|
-
if (transport.sessionId) transports.delete(transport.sessionId);
|
|
339
|
-
};
|
|
340
|
-
await sessionServer.connect(transport);
|
|
341
|
-
await transport.handleRequest(req, res, req.body);
|
|
342
|
-
if (transport.sessionId) transports.set(transport.sessionId, transport);
|
|
303
|
+
ws.on("message", async (data) => {
|
|
304
|
+
let msg;
|
|
305
|
+
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
306
|
+
|
|
307
|
+
if (msg.type === "registered") {
|
|
308
|
+
console.error(`[remote-agent] ✅ 등록 완료! Slack 유저: ${msg.name}`);
|
|
343
309
|
return;
|
|
344
310
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (sessionId && transports.has(sessionId)) {
|
|
348
|
-
await transports.get(sessionId).handleRequest(req, res, req.body);
|
|
311
|
+
if (msg.type === "error") {
|
|
312
|
+
console.error(`[remote-agent] ❌ ${msg.message}`);
|
|
349
313
|
return;
|
|
350
314
|
}
|
|
351
|
-
|
|
352
|
-
|
|
315
|
+
if (msg.type === "ping") {
|
|
316
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (msg.type === "command") {
|
|
320
|
+
const result = await executeAction(msg);
|
|
321
|
+
ws.send(JSON.stringify({ type: "result", id: msg.id, ...result }));
|
|
322
|
+
}
|
|
353
323
|
});
|
|
354
324
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
325
|
+
ws.on("close", () => {
|
|
326
|
+
if (alive) {
|
|
327
|
+
console.error("[remote-agent] 연결 끊김, 5초 후 재연결...");
|
|
328
|
+
setTimeout(connect, 5000);
|
|
329
|
+
}
|
|
359
330
|
});
|
|
360
|
-
|
|
361
|
-
|
|
331
|
+
|
|
332
|
+
ws.on("error", (e) => console.error("[remote-agent] 연결 오류:", e.message));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
connect();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── 명령 실행 ──
|
|
339
|
+
async function executeAction(msg) {
|
|
340
|
+
const { action } = msg;
|
|
341
|
+
try {
|
|
342
|
+
const { click, doubleClick, typeText, pressKey, hotkey, scroll } = await import("./lib/input.mjs");
|
|
343
|
+
const { openApp, openUrl, listApps, closeApp, focusApp, getClipboard, setClipboard } = await import("./lib/apps.mjs");
|
|
344
|
+
const { runCommand, getSystemInfo } = await import("./lib/shell.mjs");
|
|
345
|
+
const screenshotMod = await import("screenshot-desktop");
|
|
346
|
+
const screenshot = screenshotMod.default;
|
|
347
|
+
|
|
348
|
+
switch (action) {
|
|
349
|
+
case "screenshot": {
|
|
350
|
+
const buf = await screenshot({ format: "png" });
|
|
351
|
+
return { action, success: true, image: buf.toString("base64") };
|
|
352
|
+
}
|
|
353
|
+
case "click": { click(msg.x, msg.y); return { action, success: true }; }
|
|
354
|
+
case "doubleclick": { doubleClick(msg.x, msg.y); return { action, success: true }; }
|
|
355
|
+
case "type": { typeText(msg.text); return { action, success: true }; }
|
|
356
|
+
case "key": { pressKey(msg.key); return { action, success: true }; }
|
|
357
|
+
case "hotkey": { hotkey(msg.keys); return { action, success: true }; }
|
|
358
|
+
case "scroll": { scroll(msg.direction || "down", msg.amount || 3); return { action, success: true }; }
|
|
359
|
+
case "open_app": { openApp(msg.app); return { action, success: true }; }
|
|
360
|
+
case "open_url": { openUrl(msg.url); return { action, success: true }; }
|
|
361
|
+
case "list_apps": { return { action, success: true, apps: listApps() }; }
|
|
362
|
+
case "close_app": { closeApp(msg.app); return { action, success: true }; }
|
|
363
|
+
case "focus_app": { focusApp(msg.app); return { action, success: true }; }
|
|
364
|
+
case "get_clipboard": { return { action, success: true, text: getClipboard() }; }
|
|
365
|
+
case "set_clipboard": { setClipboard(msg.text); return { action, success: true }; }
|
|
366
|
+
case "shell": { return { action, success: true, output: runCommand(msg.cmd, msg.timeout || 30000) }; }
|
|
367
|
+
case "sysinfo": { return { action, success: true, info: getSystemInfo() }; }
|
|
368
|
+
case "applescript": {
|
|
369
|
+
const { execSync } = await import("child_process");
|
|
370
|
+
const output = execSync(`osascript -e '${msg.script.replace(/'/g, "'\\''")}'`, {
|
|
371
|
+
timeout: 15000, encoding: "utf-8", maxBuffer: 1024 * 1024,
|
|
372
|
+
});
|
|
373
|
+
return { action, success: true, output: output || "(실행 완료)" };
|
|
374
|
+
}
|
|
375
|
+
case "list_files": {
|
|
376
|
+
const { execSync } = await import("child_process");
|
|
377
|
+
const resolved = (msg.path || "~").replace(/^~/, process.env.HOME || "~");
|
|
378
|
+
const output = execSync(`ls -la "${resolved}" 2>/dev/null || echo "경로 없음"`, { timeout: 5000, encoding: "utf-8" });
|
|
379
|
+
return { action, success: true, output };
|
|
380
|
+
}
|
|
381
|
+
case "read_file": {
|
|
382
|
+
const { readFileSync } = await import("fs");
|
|
383
|
+
const resolved = (msg.path || "").replace(/^~/, process.env.HOME || "~");
|
|
384
|
+
const content = readFileSync(resolved, "utf-8");
|
|
385
|
+
return { action, success: true, content };
|
|
386
|
+
}
|
|
387
|
+
case "search_files": {
|
|
388
|
+
const { execSync } = await import("child_process");
|
|
389
|
+
const resolved = (msg.path || "~").replace(/^~/, process.env.HOME || "~");
|
|
390
|
+
const cmd = msg.content
|
|
391
|
+
? `grep -rl "${msg.query}" "${resolved}" 2>/dev/null | head -20`
|
|
392
|
+
: `find "${resolved}" -maxdepth 4 -iname "*${msg.query}*" -not -path '*/.*' 2>/dev/null | head -30`;
|
|
393
|
+
const output = execSync(cmd, { timeout: 15000, encoding: "utf-8" });
|
|
394
|
+
return { action, success: true, output: output || "검색 결과 없음" };
|
|
395
|
+
}
|
|
396
|
+
default: return { action, success: false, error: `알 수 없는 명령: ${action}` };
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
return { action, success: false, error: err.message };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function main() {
|
|
404
|
+
if (isStdio) {
|
|
405
|
+
// stdio 모드: 로컬 사용 (Claude Code 등)
|
|
362
406
|
const stdioServer = createServer();
|
|
363
407
|
const transport = new StdioServerTransport();
|
|
364
408
|
await stdioServer.connect(transport);
|
|
365
409
|
console.error("[remote-agent] MCP 서버 시작 (stdio)");
|
|
410
|
+
} else {
|
|
411
|
+
// WebSocket 모드: GCP 역방향 연결 (ngrok 불필요)
|
|
412
|
+
const token = loadOrCreateToken();
|
|
413
|
+
console.error(`[remote-agent] 토큰 파일: ${tokenFile}`);
|
|
414
|
+
await connectWs(regName, token);
|
|
366
415
|
}
|
|
367
416
|
}
|
|
368
417
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweetspot-remote-agent",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "Sweetspot 원격 제어 MCP 서버 — 스크린샷, 마우스/키보드, 앱 제어, 파일 탐색, 셸 실행",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "mcp-server.js",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
20
|
-
"express": "^5.2.1",
|
|
21
20
|
"screenshot-desktop": "^1.15.0",
|
|
21
|
+
"ws": "^8.18.0",
|
|
22
22
|
"zod": "^4.3.6"
|
|
23
23
|
},
|
|
24
24
|
"license": "ISC",
|