sweetspot-remote-agent 1.8.2 → 1.8.3

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 +94 -73
  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,117 @@ 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
- }
276
+ // ── 토큰 자동 생성/저장 ──
277
+ import fs from "fs";
278
+
279
+ const tokenFile = `${os.homedir()}/.sweetspot-token`;
289
280
 
290
- async function autoRegister() {
291
- if (noRegister || isStdio) return;
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;
286
+ }
292
287
 
293
- const localIP = getLocalIP();
294
- const agentUrl = `http://${localIP}:${port}`;
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;
295
293
 
296
- console.error(`[register] 봇 서버에 등록 중... (${regName} → ${agentUrl})`);
294
+ function connect() {
295
+ if (!alive) return;
296
+ const ws = new WebSocket(wsUrl);
297
297
 
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 }),
298
+ ws.on("open", () => {
299
+ ws.send(JSON.stringify({ type: "register", name, token }));
300
+ console.error(`[remote-agent] GCP 서버에 연결 중... (${name})`);
306
301
  });
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
- }
316
- }
317
302
 
318
- async function main() {
319
- if (!isStdio) {
320
- // HTTP 모드: 원격 연결용
321
- const app = express();
322
- app.use(express.json());
323
-
324
- // 세션별 transport 관리
325
- const transports = new Map();
326
-
327
- app.all("/mcp", async (req, res) => {
328
- // 새 세션 초기화 (POST + initialize 메서드)
329
- const sessionId = req.headers["mcp-session-id"];
330
-
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
+ default: return { action, success: false, error: `알 수 없는 명령: ${action}` };
369
+ }
370
+ } catch (err) {
371
+ return { action, success: false, error: err.message };
372
+ }
373
+ }
374
+
375
+ async function main() {
376
+ if (isStdio) {
377
+ // stdio 모드: 로컬 사용 (Claude Code 등)
362
378
  const stdioServer = createServer();
363
379
  const transport = new StdioServerTransport();
364
380
  await stdioServer.connect(transport);
365
381
  console.error("[remote-agent] MCP 서버 시작 (stdio)");
382
+ } else {
383
+ // WebSocket 모드: GCP 역방향 연결 (ngrok 불필요)
384
+ const token = loadOrCreateToken();
385
+ console.error(`[remote-agent] 토큰 파일: ${tokenFile}`);
386
+ await connectWs(regName, token);
366
387
  }
367
388
  }
368
389
 
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.3",
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",