sweetspot-remote-agent 1.4.0 → 1.6.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/lib/apps.mjs +61 -13
- package/lib/input.mjs +28 -6
- package/mcp-server.js +115 -105
- package/package.json +1 -1
package/lib/apps.mjs
CHANGED
|
@@ -3,14 +3,56 @@ import os from "os";
|
|
|
3
3
|
|
|
4
4
|
const platform = os.platform();
|
|
5
5
|
|
|
6
|
+
// 한국어/약칭 → macOS 앱 이름 매핑
|
|
7
|
+
const APP_ALIASES = {
|
|
8
|
+
"메모": "Notes", "메모장": "Notes", "notes": "Notes",
|
|
9
|
+
"사파리": "Safari", "safari": "Safari",
|
|
10
|
+
"크롬": "Google Chrome", "chrome": "Google Chrome", "구글크롬": "Google Chrome",
|
|
11
|
+
"파인더": "Finder", "finder": "Finder",
|
|
12
|
+
"터미널": "Terminal", "terminal": "Terminal",
|
|
13
|
+
"설정": "System Settings", "시스템설정": "System Settings",
|
|
14
|
+
"캘린더": "Calendar", "달력": "Calendar", "calendar": "Calendar",
|
|
15
|
+
"메일": "Mail", "mail": "Mail",
|
|
16
|
+
"메시지": "Messages", "messages": "Messages",
|
|
17
|
+
"음악": "Music", "music": "Music",
|
|
18
|
+
"사진": "Photos", "photos": "Photos",
|
|
19
|
+
"미리보기": "Preview", "preview": "Preview",
|
|
20
|
+
"계산기": "Calculator", "calculator": "Calculator",
|
|
21
|
+
"카카오톡": "KakaoTalk", "카톡": "KakaoTalk",
|
|
22
|
+
"슬랙": "Slack", "slack": "Slack",
|
|
23
|
+
"엑셀": "Microsoft Excel", "excel": "Microsoft Excel",
|
|
24
|
+
"워드": "Microsoft Word", "word": "Microsoft Word",
|
|
25
|
+
"파워포인트": "Microsoft PowerPoint", "ppt": "Microsoft PowerPoint",
|
|
26
|
+
"vscode": "Visual Studio Code", "코드": "Visual Studio Code",
|
|
27
|
+
"줌": "zoom.us", "zoom": "zoom.us",
|
|
28
|
+
"노션": "Notion", "notion": "Notion",
|
|
29
|
+
"피그마": "Figma", "figma": "Figma",
|
|
30
|
+
"텔레그램": "Telegram", "telegram": "Telegram",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function resolveAppName(appName) {
|
|
34
|
+
const lower = appName.toLowerCase().trim();
|
|
35
|
+
return APP_ALIASES[lower] || APP_ALIASES[appName] || appName;
|
|
36
|
+
}
|
|
37
|
+
|
|
6
38
|
export function openApp(appName) {
|
|
39
|
+
const resolved = resolveAppName(appName);
|
|
7
40
|
if (platform === "darwin") {
|
|
8
|
-
try {
|
|
9
|
-
|
|
41
|
+
try {
|
|
42
|
+
execSync(`open -a "${resolved}"`, { timeout: 5000 });
|
|
43
|
+
} catch {
|
|
44
|
+
// Spotlight 검색으로 폴백
|
|
45
|
+
try {
|
|
46
|
+
execSync(`mdfind "kMDItemKind == 'Application'" | grep -i "${appName}" | head -1 | xargs open`, { timeout: 5000 });
|
|
47
|
+
} catch {
|
|
48
|
+
// AppleScript로 마지막 시도
|
|
49
|
+
execSync(`osascript -e 'tell application "${resolved}" to activate'`, { timeout: 5000 });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
10
52
|
} else if (platform === "win32") {
|
|
11
|
-
execSync(`powershell -Command "Start-Process '${
|
|
53
|
+
execSync(`powershell -Command "Start-Process '${resolved}'"`, { timeout: 5000 });
|
|
12
54
|
} else {
|
|
13
|
-
execSync(`${
|
|
55
|
+
execSync(`${resolved} &`, { timeout: 5000 });
|
|
14
56
|
}
|
|
15
57
|
}
|
|
16
58
|
|
|
@@ -31,15 +73,17 @@ export function listApps() {
|
|
|
31
73
|
}
|
|
32
74
|
|
|
33
75
|
export function closeApp(appName) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
else execSync(`
|
|
76
|
+
const resolved = resolveAppName(appName);
|
|
77
|
+
if (platform === "darwin") execSync(`osascript -e 'tell application "${resolved}" to quit'`, { timeout: 5000 });
|
|
78
|
+
else if (platform === "win32") execSync(`powershell -Command "Stop-Process -Name '${resolved}' -Force"`, { timeout: 5000 });
|
|
79
|
+
else execSync(`pkill -f "${resolved}"`, { timeout: 5000 });
|
|
37
80
|
}
|
|
38
81
|
|
|
39
82
|
export function focusApp(appName) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
else execSync(`
|
|
83
|
+
const resolved = resolveAppName(appName);
|
|
84
|
+
if (platform === "darwin") execSync(`osascript -e 'tell application "${resolved}" to activate'`, { timeout: 5000 });
|
|
85
|
+
else if (platform === "win32") execSync(`powershell -Command "(Get-Process '${resolved}')[0].MainWindowHandle"`, { timeout: 5000 });
|
|
86
|
+
else execSync(`xdotool search --name "${resolved}" windowactivate`, { timeout: 5000 });
|
|
43
87
|
}
|
|
44
88
|
|
|
45
89
|
export function getClipboard() {
|
|
@@ -49,7 +93,11 @@ export function getClipboard() {
|
|
|
49
93
|
}
|
|
50
94
|
|
|
51
95
|
export function setClipboard(text) {
|
|
52
|
-
if (platform === "darwin")
|
|
53
|
-
|
|
54
|
-
else
|
|
96
|
+
if (platform === "darwin") {
|
|
97
|
+
execSync("pbcopy", { input: text, timeout: 5000 });
|
|
98
|
+
} else if (platform === "win32") {
|
|
99
|
+
execSync(`powershell -Command "Set-Clipboard -Value '${text.replace(/'/g, "''")}'"`);
|
|
100
|
+
} else {
|
|
101
|
+
execSync("xclip -selection clipboard", { input: text, timeout: 5000 });
|
|
102
|
+
}
|
|
55
103
|
}
|
package/lib/input.mjs
CHANGED
|
@@ -3,6 +3,11 @@ import os from "os";
|
|
|
3
3
|
|
|
4
4
|
const platform = os.platform();
|
|
5
5
|
|
|
6
|
+
// 텍스트에 비-ASCII 문자(한글 등)가 있는지 체크
|
|
7
|
+
function hasNonAscii(text) {
|
|
8
|
+
return /[^\x00-\x7F]/.test(text);
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export function click(x, y) {
|
|
7
12
|
x = Math.round(x);
|
|
8
13
|
y = Math.round(y);
|
|
@@ -34,11 +39,23 @@ export function doubleClick(x, y) {
|
|
|
34
39
|
|
|
35
40
|
export function typeText(text) {
|
|
36
41
|
if (platform === "darwin") {
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
if (hasNonAscii(text)) {
|
|
43
|
+
// 한글 등 비-ASCII: 클립보드 경유 붙여넣기
|
|
44
|
+
execSync("pbcopy", { input: text, timeout: 5000 });
|
|
45
|
+
execSync(`osascript -e 'tell application "System Events" to keystroke "v" using command down'`, { timeout: 5000 });
|
|
46
|
+
} else {
|
|
47
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
48
|
+
execSync(`osascript -e 'tell application "System Events" to keystroke "${escaped}"'`, { timeout: 10000 });
|
|
49
|
+
}
|
|
39
50
|
} else if (platform === "win32") {
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
if (hasNonAscii(text)) {
|
|
52
|
+
// Windows: 클립보드 경유
|
|
53
|
+
execSync(`powershell -Command "Set-Clipboard -Value '${text.replace(/'/g, "''")}'"`);
|
|
54
|
+
execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('^v')"`, { timeout: 5000 });
|
|
55
|
+
} else {
|
|
56
|
+
const escaped = text.replace(/'/g, "''");
|
|
57
|
+
execSync(`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${escaped}')"`, { timeout: 10000 });
|
|
58
|
+
}
|
|
42
59
|
} else {
|
|
43
60
|
execSync(`xdotool type -- "${text.replace(/"/g, '\\"')}"`, { timeout: 10000 });
|
|
44
61
|
}
|
|
@@ -82,8 +99,13 @@ export function hotkey(keys) {
|
|
|
82
99
|
|
|
83
100
|
export function scroll(direction, amount = 3) {
|
|
84
101
|
if (platform === "darwin") {
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
// cliclick scroll이 더 안정적
|
|
103
|
+
try {
|
|
104
|
+
execSync(`cliclick "scroll:0,${direction === "up" ? amount : -amount}"`, { timeout: 5000 });
|
|
105
|
+
} catch {
|
|
106
|
+
const delta = direction === "up" ? amount : -amount;
|
|
107
|
+
execSync(`osascript -e 'tell application "System Events" to scroll ${delta}'`, { timeout: 5000 });
|
|
108
|
+
}
|
|
87
109
|
} else if (platform === "win32") {
|
|
88
110
|
const keyDir = direction === "up" ? "{UP}" : "{DOWN}";
|
|
89
111
|
for (let i = 0; i < amount; i++) {
|
package/mcp-server.js
CHANGED
|
@@ -17,109 +17,116 @@ import { click, doubleClick, typeText, pressKey, hotkey, scroll } from "./lib/in
|
|
|
17
17
|
import { openApp, openUrl, listApps, closeApp, focusApp, getClipboard, setClipboard } from "./lib/apps.mjs";
|
|
18
18
|
import { runCommand, getSystemInfo } from "./lib/shell.mjs";
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
scroll
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
20
|
+
/**
|
|
21
|
+
* 도구를 등록한 McpServer 인스턴스를 생성
|
|
22
|
+
*/
|
|
23
|
+
function createServer() {
|
|
24
|
+
const srv = new McpServer({
|
|
25
|
+
name: "sweetspot-remote-agent",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ── 스크린샷 ──
|
|
30
|
+
srv.tool("screenshot", "화면 스크린샷을 찍어 base64 PNG로 반환", {}, async () => {
|
|
31
|
+
const buf = await screenshot({ format: "png" });
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "image", data: buf.toString("base64"), mimeType: "image/png" }],
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ── 마우스 ──
|
|
38
|
+
srv.tool("click", "마우스 클릭", { x: z.number().describe("X 좌표"), y: z.number().describe("Y 좌표") }, async ({ x, y }) => {
|
|
39
|
+
click(x, y);
|
|
40
|
+
return { content: [{ type: "text", text: `클릭 (${x}, ${y}) 완료` }] };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
srv.tool("doubleclick", "마우스 더블 클릭", { x: z.number(), y: z.number() }, async ({ x, y }) => {
|
|
44
|
+
doubleClick(x, y);
|
|
45
|
+
return { content: [{ type: "text", text: `더블클릭 (${x}, ${y}) 완료` }] };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
srv.tool("scroll", "마우스 스크롤", {
|
|
49
|
+
direction: z.enum(["up", "down"]).describe("방향"),
|
|
50
|
+
amount: z.number().default(3).describe("스크롤 양"),
|
|
51
|
+
}, async ({ direction, amount }) => {
|
|
52
|
+
scroll(direction, amount);
|
|
53
|
+
return { content: [{ type: "text", text: `스크롤 ${direction} ${amount} 완료` }] };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── 키보드 ──
|
|
57
|
+
srv.tool("type", "텍스트 입력 (키보드 타이핑)", { text: z.string().describe("입력할 텍스트") }, async ({ text }) => {
|
|
58
|
+
typeText(text);
|
|
59
|
+
return { content: [{ type: "text", text: `입력 완료: "${text}"` }] };
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
srv.tool("key", "특수키 입력 (enter, tab, escape, backspace, delete, up, down, left, right, space)", {
|
|
63
|
+
key: z.string().describe("키 이름"),
|
|
64
|
+
}, async ({ key }) => {
|
|
65
|
+
pressKey(key);
|
|
66
|
+
return { content: [{ type: "text", text: `키 ${key} 완료` }] };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
srv.tool("hotkey", "단축키 실행 (예: cmd+c, ctrl+v, alt+tab)", {
|
|
70
|
+
keys: z.string().describe("단축키 조합 (예: cmd+c)"),
|
|
71
|
+
}, async ({ keys }) => {
|
|
72
|
+
hotkey(keys);
|
|
73
|
+
return { content: [{ type: "text", text: `단축키 ${keys} 완료` }] };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── 앱 제어 ──
|
|
77
|
+
srv.tool("open_app", "앱 실행", { app: z.string().describe("앱 이름 (예: Chrome, Slack, Excel)") }, async ({ app }) => {
|
|
78
|
+
openApp(app);
|
|
79
|
+
return { content: [{ type: "text", text: `${app} 실행 완료` }] };
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
srv.tool("close_app", "앱 종료", { app: z.string().describe("앱 이름") }, async ({ app }) => {
|
|
83
|
+
closeApp(app);
|
|
84
|
+
return { content: [{ type: "text", text: `${app} 종료 완료` }] };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
srv.tool("focus_app", "앱으로 포커스 전환", { app: z.string().describe("앱 이름") }, async ({ app }) => {
|
|
88
|
+
focusApp(app);
|
|
89
|
+
return { content: [{ type: "text", text: `${app} 포커스 완료` }] };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
srv.tool("list_apps", "실행 중인 앱 목록 조회", {}, async () => {
|
|
93
|
+
const apps = listApps();
|
|
94
|
+
return { content: [{ type: "text", text: apps }] };
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
srv.tool("open_url", "URL을 기본 브라우저로 열기", { url: z.string().describe("URL") }, async ({ url }) => {
|
|
98
|
+
openUrl(url);
|
|
99
|
+
return { content: [{ type: "text", text: `${url} 열기 완료` }] };
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── 클립보드 ──
|
|
103
|
+
srv.tool("get_clipboard", "클립보드 내용 읽기", {}, async () => {
|
|
104
|
+
const text = getClipboard();
|
|
105
|
+
return { content: [{ type: "text", text: text || "(빈 클립보드)" }] };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
srv.tool("set_clipboard", "클립보드에 텍스트 복사", { text: z.string() }, async ({ text }) => {
|
|
109
|
+
setClipboard(text);
|
|
110
|
+
return { content: [{ type: "text", text: "클립보드에 복사 완료" }] };
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── 셸 명령 ──
|
|
114
|
+
srv.tool("shell", "셸 명령 실행 (보안 필터링 적용)", {
|
|
115
|
+
cmd: z.string().describe("실행할 명령어"),
|
|
116
|
+
timeout: z.number().default(30000).describe("타임아웃 (ms)"),
|
|
117
|
+
}, async ({ cmd, timeout }) => {
|
|
118
|
+
const output = runCommand(cmd, timeout);
|
|
119
|
+
return { content: [{ type: "text", text: output || "(출력 없음)" }] };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── 시스템 정보 ──
|
|
123
|
+
srv.tool("sysinfo", "시스템 정보 조회 (OS, CPU, 메모리 등)", {}, async () => {
|
|
124
|
+
const info = getSystemInfo();
|
|
125
|
+
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return srv;
|
|
129
|
+
}
|
|
123
130
|
|
|
124
131
|
// ── 서버 시작 ──
|
|
125
132
|
const args = process.argv.slice(2);
|
|
@@ -203,10 +210,12 @@ async function main() {
|
|
|
203
210
|
const transport = new StreamableHTTPServerTransport({
|
|
204
211
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
205
212
|
});
|
|
213
|
+
// 세션마다 새 McpServer 인스턴스 생성 (멀티세션 지원)
|
|
214
|
+
const sessionServer = createServer();
|
|
206
215
|
transport.onclose = () => {
|
|
207
216
|
if (transport.sessionId) transports.delete(transport.sessionId);
|
|
208
217
|
};
|
|
209
|
-
await
|
|
218
|
+
await sessionServer.connect(transport);
|
|
210
219
|
await transport.handleRequest(req, res, req.body);
|
|
211
220
|
if (transport.sessionId) transports.set(transport.sessionId, transport);
|
|
212
221
|
return;
|
|
@@ -228,8 +237,9 @@ async function main() {
|
|
|
228
237
|
});
|
|
229
238
|
} else {
|
|
230
239
|
// stdio 모드: 로컬 사용
|
|
240
|
+
const stdioServer = createServer();
|
|
231
241
|
const transport = new StdioServerTransport();
|
|
232
|
-
await
|
|
242
|
+
await stdioServer.connect(transport);
|
|
233
243
|
console.error("[remote-agent] MCP 서버 시작 (stdio)");
|
|
234
244
|
}
|
|
235
245
|
}
|