sweetspot-remote-agent 1.0.0

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/bin/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // MCP 서버 실행 (stdio 모드)
3
+ import("../mcp-server.js");
package/lib/apps.mjs ADDED
@@ -0,0 +1,55 @@
1
+ import { execSync } from "child_process";
2
+ import os from "os";
3
+
4
+ const platform = os.platform();
5
+
6
+ export function openApp(appName) {
7
+ if (platform === "darwin") {
8
+ try { execSync(`open -a "${appName}"`, { timeout: 5000 }); }
9
+ catch { execSync(`mdfind "kMDItemKind == 'Application'" | grep -i "${appName}" | head -1 | xargs open`, { timeout: 5000 }); }
10
+ } else if (platform === "win32") {
11
+ execSync(`powershell -Command "Start-Process '${appName}'"`, { timeout: 5000 });
12
+ } else {
13
+ execSync(`${appName} &`, { timeout: 5000 });
14
+ }
15
+ }
16
+
17
+ export function openUrl(url) {
18
+ if (platform === "darwin") execSync(`open "${url}"`, { timeout: 5000 });
19
+ else if (platform === "win32") execSync(`powershell -Command "Start-Process '${url}'"`, { timeout: 5000 });
20
+ else execSync(`xdg-open "${url}"`, { timeout: 5000 });
21
+ }
22
+
23
+ export function listApps() {
24
+ if (platform === "darwin") {
25
+ return execSync(`osascript -e 'tell application "System Events" to get name of every process whose background only is false'`, { timeout: 5000 }).toString().trim();
26
+ } else if (platform === "win32") {
27
+ return execSync(`powershell -Command "Get-Process | Where-Object {$_.MainWindowTitle} | Select-Object -Property Name,MainWindowTitle | Format-Table -AutoSize"`, { timeout: 5000 }).toString().trim();
28
+ } else {
29
+ return execSync(`wmctrl -l 2>/dev/null || xdotool search --name '' getwindowname 2>/dev/null | head -20`, { timeout: 5000 }).toString().trim();
30
+ }
31
+ }
32
+
33
+ export function closeApp(appName) {
34
+ if (platform === "darwin") execSync(`osascript -e 'tell application "${appName}" to quit'`, { timeout: 5000 });
35
+ else if (platform === "win32") execSync(`powershell -Command "Stop-Process -Name '${appName}' -Force"`, { timeout: 5000 });
36
+ else execSync(`pkill -f "${appName}"`, { timeout: 5000 });
37
+ }
38
+
39
+ export function focusApp(appName) {
40
+ if (platform === "darwin") execSync(`osascript -e 'tell application "${appName}" to activate'`, { timeout: 5000 });
41
+ else if (platform === "win32") execSync(`powershell -Command "(Get-Process '${appName}')[0].MainWindowHandle"`, { timeout: 5000 });
42
+ else execSync(`xdotool search --name "${appName}" windowactivate`, { timeout: 5000 });
43
+ }
44
+
45
+ export function getClipboard() {
46
+ if (platform === "darwin") return execSync("pbpaste", { timeout: 5000 }).toString();
47
+ else if (platform === "win32") return execSync("powershell -Command Get-Clipboard", { timeout: 5000 }).toString();
48
+ else return execSync("xclip -selection clipboard -o 2>/dev/null || xsel --clipboard --output", { timeout: 5000 }).toString();
49
+ }
50
+
51
+ export function setClipboard(text) {
52
+ if (platform === "darwin") execSync(`echo "${text.replace(/"/g, '\\"')}" | pbcopy`, { timeout: 5000 });
53
+ else if (platform === "win32") execSync(`powershell -Command "Set-Clipboard -Value '${text.replace(/'/g, "''")}'"`);
54
+ else execSync(`echo "${text.replace(/"/g, '\\"')}" | xclip -selection clipboard`, { timeout: 5000 });
55
+ }
package/lib/input.mjs ADDED
@@ -0,0 +1,96 @@
1
+ import { execSync } from "child_process";
2
+ import os from "os";
3
+
4
+ const platform = os.platform();
5
+
6
+ export function click(x, y) {
7
+ x = Math.round(x);
8
+ y = Math.round(y);
9
+ if (platform === "darwin") {
10
+ try {
11
+ execSync(`cliclick c:${x},${y}`, { timeout: 5000 });
12
+ } catch {
13
+ execSync(`osascript -e 'tell application "System Events" to click at {${x}, ${y}}'`, { timeout: 5000 });
14
+ }
15
+ } else if (platform === "win32") {
16
+ const ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x},${y}); Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern void mouse_event(int f,int x,int y,int d,int e);' -Name U -Namespace W; [W.U]::mouse_event(2,0,0,0,0); [W.U]::mouse_event(4,0,0,0,0)`;
17
+ execSync(`powershell -Command "${ps}"`, { timeout: 5000 });
18
+ } else {
19
+ execSync(`xdotool mousemove ${x} ${y} click 1`, { timeout: 5000 });
20
+ }
21
+ }
22
+
23
+ export function doubleClick(x, y) {
24
+ x = Math.round(x);
25
+ y = Math.round(y);
26
+ if (platform === "darwin") {
27
+ try { execSync(`cliclick dc:${x},${y}`, { timeout: 5000 }); } catch { click(x, y); click(x, y); }
28
+ } else if (platform === "win32") {
29
+ click(x, y); click(x, y);
30
+ } else {
31
+ execSync(`xdotool mousemove ${x} ${y} click --repeat 2 1`, { timeout: 5000 });
32
+ }
33
+ }
34
+
35
+ export function typeText(text) {
36
+ if (platform === "darwin") {
37
+ const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
38
+ execSync(`osascript -e 'tell application "System Events" to keystroke "${escaped}"'`, { timeout: 10000 });
39
+ } else if (platform === "win32") {
40
+ const escaped = text.replace(/'/g, "''");
41
+ execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${escaped}')"`, { timeout: 10000 });
42
+ } else {
43
+ execSync(`xdotool type -- "${text.replace(/"/g, '\\"')}"`, { timeout: 10000 });
44
+ }
45
+ }
46
+
47
+ export function pressKey(key) {
48
+ const macKeys = { enter: 36, tab: 48, escape: 53, backspace: 51, delete: 117, up: 126, down: 125, left: 123, right: 124, space: 49 };
49
+ const winKeys = { enter: "{ENTER}", tab: "{TAB}", escape: "{ESC}", backspace: "{BACKSPACE}", delete: "{DELETE}", up: "{UP}", down: "{DOWN}", left: "{LEFT}", right: "{RIGHT}", space: " " };
50
+ const linuxKeys = { enter: "Return", tab: "Tab", escape: "Escape", backspace: "BackSpace", delete: "Delete", up: "Up", down: "Down", left: "Left", right: "Right", space: "space" };
51
+
52
+ const k = key.toLowerCase();
53
+ if (platform === "darwin") {
54
+ const code = macKeys[k];
55
+ if (code !== undefined) execSync(`osascript -e 'tell application "System Events" to key code ${code}'`, { timeout: 5000 });
56
+ } else if (platform === "win32") {
57
+ const mapped = winKeys[k] || `{${key.toUpperCase()}}`;
58
+ execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${mapped}')"`, { timeout: 5000 });
59
+ } else {
60
+ execSync(`xdotool key ${linuxKeys[k] || key}`, { timeout: 5000 });
61
+ }
62
+ }
63
+
64
+ export function hotkey(keys) {
65
+ const parts = keys.toLowerCase().split("+").map(k => k.trim());
66
+ if (platform === "darwin") {
67
+ const modMap = { cmd: "command", ctrl: "control", alt: "option", shift: "shift" };
68
+ const mods = parts.slice(0, -1).map(m => modMap[m] || m);
69
+ const key = parts[parts.length - 1];
70
+ const modStr = mods.map(m => `${m} down`).join(", ");
71
+ execSync(`osascript -e 'tell application "System Events" to keystroke "${key}" using {${modStr}}'`, { timeout: 5000 });
72
+ } else if (platform === "win32") {
73
+ const modMap = { ctrl: "^", alt: "%", shift: "+" };
74
+ let combo = "";
75
+ for (const p of parts.slice(0, -1)) combo += modMap[p] || "";
76
+ combo += parts[parts.length - 1];
77
+ execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${combo}')"`, { timeout: 5000 });
78
+ } else {
79
+ execSync(`xdotool key ${parts.join("+")}`, { timeout: 5000 });
80
+ }
81
+ }
82
+
83
+ export function scroll(direction, amount = 3) {
84
+ if (platform === "darwin") {
85
+ const delta = direction === "up" ? amount : -amount;
86
+ execSync(`osascript -e 'tell application "System Events" to scroll ${delta}'`, { timeout: 5000 });
87
+ } else if (platform === "win32") {
88
+ const keyDir = direction === "up" ? "{UP}" : "{DOWN}";
89
+ for (let i = 0; i < amount; i++) {
90
+ execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${keyDir}')"`, { timeout: 5000 });
91
+ }
92
+ } else {
93
+ const btn = direction === "up" ? 4 : 5;
94
+ execSync(`xdotool click --repeat ${amount} ${btn}`, { timeout: 5000 });
95
+ }
96
+ }
package/lib/shell.mjs ADDED
@@ -0,0 +1,39 @@
1
+ import { execSync } from "child_process";
2
+ import os from "os";
3
+
4
+ const ALLOWED_COMMANDS = [
5
+ "ls", "dir", "pwd", "cd", "cat", "head", "tail", "find", "grep",
6
+ "echo", "date", "whoami", "hostname", "uname",
7
+ "mkdir", "cp", "mv", "touch",
8
+ "df", "du", "free", "top", "ps",
9
+ "ping", "curl", "wget",
10
+ "python", "python3", "node", "npm", "npx",
11
+ "git", "code", "open", "start", "xdg-open",
12
+ ];
13
+
14
+ const BLOCKED_PATTERNS = [
15
+ /rm\s+(-rf?|--force)/i, /rmdir/i, /format\s+/i, /dd\s+if=/i,
16
+ /mkfs/i, />\s*\/dev\//i, /shutdown/i, /reboot/i, /halt/i,
17
+ /kill\s+-9\s+1\b/i, /:(){ :|:& };:/,
18
+ ];
19
+
20
+ export function runCommand(cmd, timeoutMs = 30000) {
21
+ for (const pattern of BLOCKED_PATTERNS) {
22
+ if (pattern.test(cmd)) throw new Error(`보안 정책에 의해 차단된 명령: ${cmd}`);
23
+ }
24
+ const firstCmd = cmd.split(/[\s|;&]/)[0].replace(/^.*\//, "");
25
+ if (!ALLOWED_COMMANDS.includes(firstCmd)) {
26
+ throw new Error(`허용되지 않은 명령어: ${firstCmd}`);
27
+ }
28
+ const result = execSync(cmd, { timeout: timeoutMs, maxBuffer: 1024 * 1024, cwd: os.homedir(), encoding: "utf-8" });
29
+ return result.length > 5000 ? result.slice(0, 5000) + `\n\n... (${result.length - 5000}자 생략)` : result;
30
+ }
31
+
32
+ export function getSystemInfo() {
33
+ return {
34
+ platform: os.platform(), arch: os.arch(), hostname: os.hostname(),
35
+ username: os.userInfo().username, homedir: os.homedir(),
36
+ cpus: os.cpus().length, memory: `${Math.round(os.totalmem() / 1024 / 1024 / 1024)}GB`,
37
+ uptime: `${Math.round(os.uptime() / 3600)}시간`,
38
+ };
39
+ }
package/mcp-server.js ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sweetspot Remote Agent — MCP 서버
4
+ *
5
+ * 로컬 사용: .mcp.json에 추가 (stdio)
6
+ * 원격 사용: node mcp-server.js --sse --port 8080
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z } from "zod";
11
+
12
+ import screenshot from "screenshot-desktop";
13
+ import { click, doubleClick, typeText, pressKey, hotkey, scroll } from "./lib/input.mjs";
14
+ import { openApp, openUrl, listApps, closeApp, focusApp, getClipboard, setClipboard } from "./lib/apps.mjs";
15
+ import { runCommand, getSystemInfo } from "./lib/shell.mjs";
16
+
17
+ const server = new McpServer({
18
+ name: "sweetspot-remote-agent",
19
+ version: "1.0.0",
20
+ });
21
+
22
+ // ── 스크린샷 ──
23
+ server.tool("screenshot", "화면 스크린샷을 찍어 base64 PNG로 반환", {}, async () => {
24
+ const buf = await screenshot({ format: "png" });
25
+ return {
26
+ content: [{ type: "image", data: buf.toString("base64"), mimeType: "image/png" }],
27
+ };
28
+ });
29
+
30
+ // ── 마우스 ──
31
+ server.tool("click", "마우스 클릭", { x: z.number().describe("X 좌표"), y: z.number().describe("Y 좌표") }, async ({ x, y }) => {
32
+ click(x, y);
33
+ return { content: [{ type: "text", text: `클릭 (${x}, ${y}) 완료` }] };
34
+ });
35
+
36
+ server.tool("doubleclick", "마우스 더블 클릭", { x: z.number(), y: z.number() }, async ({ x, y }) => {
37
+ doubleClick(x, y);
38
+ return { content: [{ type: "text", text: `더블클릭 (${x}, ${y}) 완료` }] };
39
+ });
40
+
41
+ server.tool("scroll", "마우스 스크롤", {
42
+ direction: z.enum(["up", "down"]).describe("방향"),
43
+ amount: z.number().default(3).describe("스크롤 양"),
44
+ }, async ({ direction, amount }) => {
45
+ scroll(direction, amount);
46
+ return { content: [{ type: "text", text: `스크롤 ${direction} ${amount} 완료` }] };
47
+ });
48
+
49
+ // ── 키보드 ──
50
+ server.tool("type", "텍스트 입력 (키보드 타이핑)", { text: z.string().describe("입력할 텍스트") }, async ({ text }) => {
51
+ typeText(text);
52
+ return { content: [{ type: "text", text: `입력 완료: "${text}"` }] };
53
+ });
54
+
55
+ server.tool("key", "특수키 입력 (enter, tab, escape, backspace, delete, up, down, left, right, space)", {
56
+ key: z.string().describe("키 이름"),
57
+ }, async ({ key }) => {
58
+ pressKey(key);
59
+ return { content: [{ type: "text", text: `키 ${key} 완료` }] };
60
+ });
61
+
62
+ server.tool("hotkey", "단축키 실행 (예: cmd+c, ctrl+v, alt+tab)", {
63
+ keys: z.string().describe("단축키 조합 (예: cmd+c)"),
64
+ }, async ({ keys }) => {
65
+ hotkey(keys);
66
+ return { content: [{ type: "text", text: `단축키 ${keys} 완료` }] };
67
+ });
68
+
69
+ // ── 앱 제어 ──
70
+ server.tool("open_app", "앱 실행", { app: z.string().describe("앱 이름 (예: Chrome, Slack, Excel)") }, async ({ app }) => {
71
+ openApp(app);
72
+ return { content: [{ type: "text", text: `${app} 실행 완료` }] };
73
+ });
74
+
75
+ server.tool("close_app", "앱 종료", { app: z.string().describe("앱 이름") }, async ({ app }) => {
76
+ closeApp(app);
77
+ return { content: [{ type: "text", text: `${app} 종료 완료` }] };
78
+ });
79
+
80
+ server.tool("focus_app", "앱으로 포커스 전환", { app: z.string().describe("앱 이름") }, async ({ app }) => {
81
+ focusApp(app);
82
+ return { content: [{ type: "text", text: `${app} 포커스 완료` }] };
83
+ });
84
+
85
+ server.tool("list_apps", "실행 중인 앱 목록 조회", {}, async () => {
86
+ const apps = listApps();
87
+ return { content: [{ type: "text", text: apps }] };
88
+ });
89
+
90
+ server.tool("open_url", "URL을 기본 브라우저로 열기", { url: z.string().describe("URL") }, async ({ url }) => {
91
+ openUrl(url);
92
+ return { content: [{ type: "text", text: `${url} 열기 완료` }] };
93
+ });
94
+
95
+ // ── 클립보드 ──
96
+ server.tool("get_clipboard", "클립보드 내용 읽기", {}, async () => {
97
+ const text = getClipboard();
98
+ return { content: [{ type: "text", text: text || "(빈 클립보드)" }] };
99
+ });
100
+
101
+ server.tool("set_clipboard", "클립보드에 텍스트 복사", { text: z.string() }, async ({ text }) => {
102
+ setClipboard(text);
103
+ return { content: [{ type: "text", text: "클립보드에 복사 완료" }] };
104
+ });
105
+
106
+ // ── 셸 명령 ──
107
+ server.tool("shell", "셸 명령 실행 (보안 필터링 적용)", {
108
+ cmd: z.string().describe("실행할 명령어"),
109
+ timeout: z.number().default(30000).describe("타임아웃 (ms)"),
110
+ }, async ({ cmd, timeout }) => {
111
+ const output = runCommand(cmd, timeout);
112
+ return { content: [{ type: "text", text: output || "(출력 없음)" }] };
113
+ });
114
+
115
+ // ── 시스템 정보 ──
116
+ server.tool("sysinfo", "시스템 정보 조회 (OS, CPU, 메모리 등)", {}, async () => {
117
+ const info = getSystemInfo();
118
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
119
+ });
120
+
121
+ // ── 서버 시작 ──
122
+ async function main() {
123
+ const transport = new StdioServerTransport();
124
+ await server.connect(transport);
125
+ console.error("[remote-agent] MCP 서버 시작 (stdio)");
126
+ }
127
+
128
+ main().catch((err) => {
129
+ console.error("MCP 서버 실행 실패:", err);
130
+ process.exit(1);
131
+ });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "sweetspot-remote-agent",
3
+ "version": "1.0.0",
4
+ "description": "Sweetspot 원격 제어 MCP 서버 — 스크린샷, 마우스/키보드, 앱 제어, 셸 실행",
5
+ "type": "module",
6
+ "main": "mcp-server.js",
7
+ "bin": {
8
+ "sweetspot-agent": "bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node mcp-server.js"
12
+ },
13
+ "files": [
14
+ "mcp-server.js",
15
+ "bin/",
16
+ "lib/*.mjs"
17
+ ],
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.27.1",
20
+ "screenshot-desktop": "^1.15.0",
21
+ "zod": "^4.3.6"
22
+ },
23
+ "license": "ISC",
24
+ "keywords": ["mcp", "remote-control", "screenshot", "automation"]
25
+ }