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.
- package/mcp-server.js +94 -73
- 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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
291
|
-
if (
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
294
|
+
function connect() {
|
|
295
|
+
if (!alive) return;
|
|
296
|
+
const ws = new WebSocket(wsUrl);
|
|
297
297
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
+
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.
|
|
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",
|