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.
Files changed (2) hide show
  1. package/mcp-server.js +121 -72
  2. 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
- function getLocalIP() {
281
- const nets = os.networkInterfaces();
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
- console.error(`[register] 서버에 등록 중... (${regName} → ${agentUrl})`);
279
+ const tokenFile = `${os.homedir()}/.sweetspot-token`;
297
280
 
298
- try {
299
- const res = await fetch(`${BOT_SERVER}/register`, {
300
- method: "POST",
301
- headers: {
302
- "Content-Type": "application/json",
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
- async function main() {
319
- if (!isStdio) {
320
- // HTTP 모드: 원격 연결용
321
- const app = express();
322
- app.use(express.json());
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
- // 세션별 transport 관리
325
- const transports = new Map();
294
+ function connect() {
295
+ if (!alive) return;
296
+ const ws = new WebSocket(wsUrl);
326
297
 
327
- app.all("/mcp", async (req, res) => {
328
- // 세션 초기화 (POST + initialize 메서드)
329
- const sessionId = req.headers["mcp-session-id"];
298
+ ws.on("open", () => {
299
+ ws.send(JSON.stringify({ type: "register", name, token }));
300
+ console.error(`[remote-agent] GCP 서버에 연결 중... (${name})`);
301
+ });
330
302
 
331
- if (!sessionId && req.method === "POST") {
332
- const transport = new StreamableHTTPServerTransport({
333
- sessionIdGenerator: () => crypto.randomUUID(),
334
- });
335
- // 세션마다 McpServer 인스턴스 생성 (멀티세션 지원)
336
- const sessionServer = createServer();
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
- res.status(400).json({ error: "Invalid or missing session" });
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
- app.listen(port, "0.0.0.0", () => {
356
- console.error(`[remote-agent] MCP 서버 시작 (HTTP 0.0.0.0:${port})`);
357
- // 서버 시작자동 등록
358
- autoRegister();
325
+ ws.on("close", () => {
326
+ if (alive) {
327
+ console.error("[remote-agent] 연결 끊김, 5초 재연결...");
328
+ setTimeout(connect, 5000);
329
+ }
359
330
  });
360
- } else {
361
- // stdio 모드: 로컬 사용
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.2",
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",