junis 0.2.5 → 0.3.1
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/README.md +17 -4
- package/dist/cli/index.js +1244 -316
- package/dist/server/mcp.js +919 -204
- package/dist/server/stdio.js +661 -204
- package/package.json +5 -2
package/dist/server/stdio.js
CHANGED
|
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
9
13
|
var __copyProps = (to, from, except, desc) => {
|
|
10
14
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
15
|
for (let key of __getOwnPropNames(from))
|
|
@@ -22,8 +26,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
26
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
27
|
mod
|
|
24
28
|
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
30
|
|
|
26
31
|
// src/server/stdio.ts
|
|
32
|
+
var stdio_exports = {};
|
|
33
|
+
__export(stdio_exports, {
|
|
34
|
+
startStdioServer: () => startStdioServer
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(stdio_exports);
|
|
27
37
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
28
38
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
29
39
|
|
|
@@ -34,29 +44,81 @@ var import_promises = __toESM(require("fs/promises"));
|
|
|
34
44
|
var import_path = __toESM(require("path"));
|
|
35
45
|
var import_glob = require("glob");
|
|
36
46
|
var import_zod = require("zod");
|
|
47
|
+
|
|
48
|
+
// src/server/permissions.ts
|
|
49
|
+
var toolPermissions = {
|
|
50
|
+
// 읽기 전용 — 자동 허용
|
|
51
|
+
browser_snapshot: "auto",
|
|
52
|
+
browser_screenshot: "auto",
|
|
53
|
+
desktop_see: "auto",
|
|
54
|
+
desktop_list_apps: "auto",
|
|
55
|
+
desktop_list_windows: "auto",
|
|
56
|
+
cron_list: "auto",
|
|
57
|
+
read_file: "auto",
|
|
58
|
+
list_directory: "auto",
|
|
59
|
+
list_processes: "auto",
|
|
60
|
+
search_code: "auto",
|
|
61
|
+
// 상호작용 — 확인 권장 (현재: auto와 동일하게 실행, 향후 UI 연동)
|
|
62
|
+
browser_click: "confirm",
|
|
63
|
+
browser_type: "confirm",
|
|
64
|
+
browser_navigate: "confirm",
|
|
65
|
+
browser_fill: "confirm",
|
|
66
|
+
browser_select: "confirm",
|
|
67
|
+
browser_press: "confirm",
|
|
68
|
+
browser_hover: "confirm",
|
|
69
|
+
browser_drag: "confirm",
|
|
70
|
+
browser_upload: "confirm",
|
|
71
|
+
browser_cookies: "confirm",
|
|
72
|
+
browser_storage: "confirm",
|
|
73
|
+
browser_dialog: "confirm",
|
|
74
|
+
desktop_click: "confirm",
|
|
75
|
+
desktop_type: "confirm",
|
|
76
|
+
desktop_hotkey: "confirm",
|
|
77
|
+
desktop_scroll: "confirm",
|
|
78
|
+
desktop_menu: "confirm",
|
|
79
|
+
desktop_screenshot: "confirm",
|
|
80
|
+
cron_create: "confirm",
|
|
81
|
+
cron_delete: "confirm",
|
|
82
|
+
edit_block: "confirm",
|
|
83
|
+
kill_process: "confirm",
|
|
84
|
+
// 시스템 변경 — 기본 차단 (PDF 7.3절)
|
|
85
|
+
execute_command: "deny",
|
|
86
|
+
write_file: "deny"
|
|
87
|
+
};
|
|
88
|
+
function checkPermission(toolName) {
|
|
89
|
+
const level = toolPermissions[toolName];
|
|
90
|
+
if (level === "deny") {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Tool '${toolName}' is blocked by permission policy (deny). To allow, update toolPermissions in src/server/permissions.ts.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/tools/filesystem.ts
|
|
37
98
|
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
38
99
|
var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
|
|
39
100
|
var FilesystemTools = class {
|
|
40
101
|
register(server) {
|
|
41
102
|
server.tool(
|
|
42
103
|
"execute_command",
|
|
43
|
-
"
|
|
104
|
+
"Execute terminal command",
|
|
44
105
|
{
|
|
45
|
-
command: import_zod.z.string().describe("
|
|
46
|
-
timeout_ms: import_zod.z.number().optional().default(3e4).describe("
|
|
47
|
-
background: import_zod.z.boolean().optional().default(false).describe("
|
|
106
|
+
command: import_zod.z.string().describe("Shell command to execute"),
|
|
107
|
+
timeout_ms: import_zod.z.number().optional().default(3e4).describe("Timeout (ms)"),
|
|
108
|
+
background: import_zod.z.boolean().optional().default(false).describe("Run in background")
|
|
48
109
|
},
|
|
49
110
|
async ({ command, timeout_ms, background }) => {
|
|
111
|
+
checkPermission("execute_command");
|
|
50
112
|
if (background) {
|
|
51
113
|
(0, import_child_process.exec)(command);
|
|
52
|
-
return { content: [{ type: "text", text: "
|
|
114
|
+
return { content: [{ type: "text", text: "Background execution started" }] };
|
|
53
115
|
}
|
|
54
116
|
try {
|
|
55
117
|
const { stdout, stderr } = await execAsync(command, {
|
|
56
118
|
timeout: timeout_ms
|
|
57
119
|
});
|
|
58
120
|
return {
|
|
59
|
-
content: [{ type: "text", text: stdout || stderr || "(
|
|
121
|
+
content: [{ type: "text", text: stdout || stderr || "(no output)" }]
|
|
60
122
|
};
|
|
61
123
|
} catch (err) {
|
|
62
124
|
const error = err;
|
|
@@ -64,7 +126,7 @@ var FilesystemTools = class {
|
|
|
64
126
|
content: [
|
|
65
127
|
{
|
|
66
128
|
type: "text",
|
|
67
|
-
text:
|
|
129
|
+
text: `Error (exit ${error.code ?? "?"}): ${error.message}
|
|
68
130
|
${error.stderr ?? ""}`
|
|
69
131
|
}
|
|
70
132
|
],
|
|
@@ -75,10 +137,10 @@ ${error.stderr ?? ""}`
|
|
|
75
137
|
);
|
|
76
138
|
server.tool(
|
|
77
139
|
"read_file",
|
|
78
|
-
"
|
|
140
|
+
"Read file",
|
|
79
141
|
{
|
|
80
|
-
path: import_zod.z.string().describe("
|
|
81
|
-
encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("
|
|
142
|
+
path: import_zod.z.string().describe("File path"),
|
|
143
|
+
encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("Encoding")
|
|
82
144
|
},
|
|
83
145
|
async ({ path: filePath, encoding }) => {
|
|
84
146
|
try {
|
|
@@ -87,30 +149,31 @@ ${error.stderr ?? ""}`
|
|
|
87
149
|
} catch (err) {
|
|
88
150
|
const e = err;
|
|
89
151
|
if (e.code === "ENOENT") {
|
|
90
|
-
return { content: [{ type: "text", text: `\u274C
|
|
152
|
+
return { content: [{ type: "text", text: `\u274C File not found: ${filePath}` }], isError: true };
|
|
91
153
|
}
|
|
92
|
-
return { content: [{ type: "text", text: `\u274C
|
|
154
|
+
return { content: [{ type: "text", text: `\u274C Failed to read file: ${e.message}` }], isError: true };
|
|
93
155
|
}
|
|
94
156
|
}
|
|
95
157
|
);
|
|
96
158
|
server.tool(
|
|
97
159
|
"write_file",
|
|
98
|
-
"
|
|
160
|
+
"Write/create file",
|
|
99
161
|
{
|
|
100
|
-
path: import_zod.z.string().describe("
|
|
101
|
-
content: import_zod.z.string().describe("
|
|
162
|
+
path: import_zod.z.string().describe("File path"),
|
|
163
|
+
content: import_zod.z.string().describe("File content")
|
|
102
164
|
},
|
|
103
165
|
async ({ path: filePath, content }) => {
|
|
166
|
+
checkPermission("write_file");
|
|
104
167
|
await import_promises.default.mkdir(import_path.default.dirname(filePath), { recursive: true });
|
|
105
168
|
await import_promises.default.writeFile(filePath, content, "utf-8");
|
|
106
|
-
return { content: [{ type: "text", text: "
|
|
169
|
+
return { content: [{ type: "text", text: "File saved" }] };
|
|
107
170
|
}
|
|
108
171
|
);
|
|
109
172
|
server.tool(
|
|
110
173
|
"list_directory",
|
|
111
|
-
"
|
|
174
|
+
"List directory contents",
|
|
112
175
|
{
|
|
113
|
-
path: import_zod.z.string().describe("
|
|
176
|
+
path: import_zod.z.string().describe("Directory path")
|
|
114
177
|
},
|
|
115
178
|
async ({ path: dirPath }) => {
|
|
116
179
|
try {
|
|
@@ -120,19 +183,19 @@ ${error.stderr ?? ""}`
|
|
|
120
183
|
} catch (err) {
|
|
121
184
|
const e = err;
|
|
122
185
|
if (e.code === "ENOENT") {
|
|
123
|
-
return { content: [{ type: "text", text: `\u274C
|
|
186
|
+
return { content: [{ type: "text", text: `\u274C Directory not found: ${dirPath}` }], isError: true };
|
|
124
187
|
}
|
|
125
|
-
return { content: [{ type: "text", text: `\u274C
|
|
188
|
+
return { content: [{ type: "text", text: `\u274C Failed to read directory: ${e.message}` }], isError: true };
|
|
126
189
|
}
|
|
127
190
|
}
|
|
128
191
|
);
|
|
129
192
|
server.tool(
|
|
130
193
|
"search_code",
|
|
131
|
-
"
|
|
194
|
+
"Search code/text",
|
|
132
195
|
{
|
|
133
|
-
pattern: import_zod.z.string().describe("
|
|
134
|
-
directory: import_zod.z.string().optional().default(".").describe("
|
|
135
|
-
file_pattern: import_zod.z.string().optional().default("**/*").describe("
|
|
196
|
+
pattern: import_zod.z.string().describe("Search pattern (regex supported)"),
|
|
197
|
+
directory: import_zod.z.string().optional().default(".").describe("Search directory"),
|
|
198
|
+
file_pattern: import_zod.z.string().optional().default("**/*").describe("File pattern")
|
|
136
199
|
},
|
|
137
200
|
async ({ pattern, directory, file_pattern }) => {
|
|
138
201
|
try {
|
|
@@ -141,7 +204,7 @@ ${error.stderr ?? ""}`
|
|
|
141
204
|
["--no-heading", "-n", pattern, directory],
|
|
142
205
|
{ timeout: 1e4 }
|
|
143
206
|
);
|
|
144
|
-
return { content: [{ type: "text", text: stdout || "
|
|
207
|
+
return { content: [{ type: "text", text: stdout || "No results" }] };
|
|
145
208
|
} catch {
|
|
146
209
|
const safeDirectory = import_path.default.resolve(directory);
|
|
147
210
|
const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
|
|
@@ -162,7 +225,7 @@ ${error.stderr ?? ""}`
|
|
|
162
225
|
}
|
|
163
226
|
return {
|
|
164
227
|
content: [
|
|
165
|
-
{ type: "text", text: results.join("\n") || "
|
|
228
|
+
{ type: "text", text: results.join("\n") || "No results" }
|
|
166
229
|
]
|
|
167
230
|
};
|
|
168
231
|
}
|
|
@@ -170,7 +233,7 @@ ${error.stderr ?? ""}`
|
|
|
170
233
|
);
|
|
171
234
|
server.tool(
|
|
172
235
|
"list_processes",
|
|
173
|
-
"
|
|
236
|
+
"List running processes",
|
|
174
237
|
{},
|
|
175
238
|
async () => {
|
|
176
239
|
const cmd = process.platform === "win32" ? "tasklist" : process.platform === "darwin" ? "ps aux | sort -rk 3 | head -30" : "ps aux --sort=-%cpu | head -30";
|
|
@@ -180,23 +243,23 @@ ${error.stderr ?? ""}`
|
|
|
180
243
|
);
|
|
181
244
|
server.tool(
|
|
182
245
|
"kill_process",
|
|
183
|
-
"
|
|
246
|
+
"Kill process (SIGTERM then 3s wait, auto SIGKILL if still alive)",
|
|
184
247
|
{
|
|
185
|
-
pid: import_zod.z.number().describe("
|
|
186
|
-
signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("
|
|
248
|
+
pid: import_zod.z.number().describe("PID of the process to kill"),
|
|
249
|
+
signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("Initial signal (default: SIGTERM). SIGKILL for immediate force kill)")
|
|
187
250
|
},
|
|
188
251
|
async ({ pid, signal }) => {
|
|
189
252
|
const isWindows = process.platform === "win32";
|
|
190
253
|
if (isWindows) {
|
|
191
254
|
await execAsync(`taskkill /PID ${pid} /F`);
|
|
192
255
|
return {
|
|
193
|
-
content: [{ type: "text", text: `PID ${pid}
|
|
256
|
+
content: [{ type: "text", text: `PID ${pid} killed (taskkill /F)` }]
|
|
194
257
|
};
|
|
195
258
|
}
|
|
196
259
|
if (signal === "SIGKILL") {
|
|
197
260
|
await execAsync(`kill -9 ${pid}`);
|
|
198
261
|
return {
|
|
199
|
-
content: [{ type: "text", text: `PID ${pid}
|
|
262
|
+
content: [{ type: "text", text: `PID ${pid} force killed (SIGKILL)` }]
|
|
200
263
|
};
|
|
201
264
|
}
|
|
202
265
|
try {
|
|
@@ -204,7 +267,7 @@ ${error.stderr ?? ""}`
|
|
|
204
267
|
} catch {
|
|
205
268
|
return {
|
|
206
269
|
content: [
|
|
207
|
-
{ type: "text", text: `PID ${pid}
|
|
270
|
+
{ type: "text", text: `PID ${pid} kill failed: process does not exist or permission denied.` }
|
|
208
271
|
],
|
|
209
272
|
isError: true
|
|
210
273
|
};
|
|
@@ -213,7 +276,7 @@ ${error.stderr ?? ""}`
|
|
|
213
276
|
const isAlive = await execAsync(`kill -0 ${pid}`).then(() => true).catch(() => false);
|
|
214
277
|
if (!isAlive) {
|
|
215
278
|
return {
|
|
216
|
-
content: [{ type: "text", text: `PID ${pid}
|
|
279
|
+
content: [{ type: "text", text: `PID ${pid} killed (SIGTERM)` }]
|
|
217
280
|
};
|
|
218
281
|
}
|
|
219
282
|
await execAsync(`kill -9 ${pid}`);
|
|
@@ -221,7 +284,7 @@ ${error.stderr ?? ""}`
|
|
|
221
284
|
content: [
|
|
222
285
|
{
|
|
223
286
|
type: "text",
|
|
224
|
-
text: `PID ${pid}
|
|
287
|
+
text: `PID ${pid} force killed (SIGTERM unresponsive, auto SIGKILL applied)`
|
|
225
288
|
}
|
|
226
289
|
]
|
|
227
290
|
};
|
|
@@ -229,17 +292,17 @@ ${error.stderr ?? ""}`
|
|
|
229
292
|
);
|
|
230
293
|
server.tool(
|
|
231
294
|
"edit_block",
|
|
232
|
-
"
|
|
295
|
+
"Replace a specific text block in a file with new text (diff-based partial edit)",
|
|
233
296
|
{
|
|
234
|
-
path: import_zod.z.string().describe("
|
|
235
|
-
old_string: import_zod.z.string().describe("
|
|
236
|
-
new_string: import_zod.z.string().describe("
|
|
237
|
-
replace_all: import_zod.z.boolean().optional().default(false).describe("true
|
|
297
|
+
path: import_zod.z.string().describe("File path"),
|
|
298
|
+
old_string: import_zod.z.string().describe("Existing text to replace (must match exactly)"),
|
|
299
|
+
new_string: import_zod.z.string().describe("New text"),
|
|
300
|
+
replace_all: import_zod.z.boolean().optional().default(false).describe("If true, replace all matches; if false, replace only the first")
|
|
238
301
|
},
|
|
239
302
|
async ({ path: filePath, old_string, new_string, replace_all }) => {
|
|
240
303
|
const content = await import_promises.default.readFile(filePath, "utf-8");
|
|
241
304
|
if (!content.includes(old_string)) {
|
|
242
|
-
throw new Error(`old_string
|
|
305
|
+
throw new Error(`old_string not found in file: ${filePath}`);
|
|
243
306
|
}
|
|
244
307
|
let count = 0;
|
|
245
308
|
let pos = 0;
|
|
@@ -249,7 +312,7 @@ ${error.stderr ?? ""}`
|
|
|
249
312
|
}
|
|
250
313
|
if (!replace_all && count > 1) {
|
|
251
314
|
throw new Error(
|
|
252
|
-
|
|
315
|
+
`Found ${count} matches. Set replace_all to true or include more context to narrow it down.`
|
|
253
316
|
);
|
|
254
317
|
}
|
|
255
318
|
let result;
|
|
@@ -263,21 +326,197 @@ ${error.stderr ?? ""}`
|
|
|
263
326
|
}
|
|
264
327
|
await import_promises.default.writeFile(filePath, result, "utf-8");
|
|
265
328
|
return {
|
|
266
|
-
content: [{ type: "text", text:
|
|
329
|
+
content: [{ type: "text", text: `Replaced (${replaced} occurrence(s) changed)` }]
|
|
267
330
|
};
|
|
268
331
|
}
|
|
269
332
|
);
|
|
333
|
+
server.tool(
|
|
334
|
+
"cron_create",
|
|
335
|
+
"Create a recurring cron job. schedule uses cron syntax (e.g. '0 9 * * 1-5' = weekdays 9am).",
|
|
336
|
+
{
|
|
337
|
+
schedule: import_zod.z.string().describe("Cron schedule expression (e.g. '*/5 * * * *' for every 5 min, '0 9 * * 1-5' for weekdays 9am)"),
|
|
338
|
+
command: import_zod.z.string().describe("Shell command to execute"),
|
|
339
|
+
label: import_zod.z.string().optional().describe("Optional label/comment for identification")
|
|
340
|
+
},
|
|
341
|
+
async ({ schedule, command, label }) => {
|
|
342
|
+
try {
|
|
343
|
+
let existing = "";
|
|
344
|
+
try {
|
|
345
|
+
const { stdout } = await execAsync("crontab -l");
|
|
346
|
+
existing = stdout;
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
if (existing.includes(command)) {
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: `\u26A0\uFE0F A cron job with this command already exists.` }],
|
|
352
|
+
isError: true
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const comment = label ? `# junis:${label}
|
|
356
|
+
` : "# junis-cron\n";
|
|
357
|
+
const newEntry = `${comment}${schedule} ${command}
|
|
358
|
+
`;
|
|
359
|
+
const updated = existing.trimEnd() + "\n" + newEntry;
|
|
360
|
+
const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
361
|
+
await import_promises.default.writeFile(tmpFile, updated, "utf-8");
|
|
362
|
+
await execAsync(`crontab ${tmpFile}`);
|
|
363
|
+
await import_promises.default.unlink(tmpFile).catch(() => {
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
content: [{ type: "text", text: `\u2705 Cron job created:
|
|
367
|
+
schedule: ${schedule}
|
|
368
|
+
command: ${command}${label ? `
|
|
369
|
+
label: ${label}` : ""}` }]
|
|
370
|
+
};
|
|
371
|
+
} catch (err) {
|
|
372
|
+
return {
|
|
373
|
+
content: [{ type: "text", text: `\u274C Failed to create cron job: ${err.message}` }],
|
|
374
|
+
isError: true
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
server.tool(
|
|
380
|
+
"cron_list",
|
|
381
|
+
"List all cron jobs in the current user's crontab",
|
|
382
|
+
{},
|
|
383
|
+
async () => {
|
|
384
|
+
try {
|
|
385
|
+
const { stdout } = await execAsync("crontab -l");
|
|
386
|
+
const lines = stdout.trim().split("\n").filter((l) => l.trim());
|
|
387
|
+
if (lines.length === 0) {
|
|
388
|
+
return { content: [{ type: "text", text: "No cron jobs found." }] };
|
|
389
|
+
}
|
|
390
|
+
const entries = [];
|
|
391
|
+
let pendingLabel;
|
|
392
|
+
let id = 1;
|
|
393
|
+
for (const line of lines) {
|
|
394
|
+
if (line.startsWith("#")) {
|
|
395
|
+
const match = line.match(/^# junis:(.+)$/);
|
|
396
|
+
pendingLabel = match ? match[1].trim() : void 0;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const parts = line.split(/\s+/);
|
|
400
|
+
if (parts.length >= 6) {
|
|
401
|
+
const schedule = parts.slice(0, 5).join(" ");
|
|
402
|
+
const command = parts.slice(5).join(" ");
|
|
403
|
+
entries.push({ id: id++, label: pendingLabel, schedule, command });
|
|
404
|
+
}
|
|
405
|
+
pendingLabel = void 0;
|
|
406
|
+
}
|
|
407
|
+
if (entries.length === 0) {
|
|
408
|
+
return { content: [{ type: "text", text: stdout }] };
|
|
409
|
+
}
|
|
410
|
+
const output = entries.map(
|
|
411
|
+
(e) => `[${e.id}] ${e.label ? `(${e.label}) ` : ""}${e.schedule} \u2192 ${e.command}`
|
|
412
|
+
).join("\n");
|
|
413
|
+
return { content: [{ type: "text", text: output }] };
|
|
414
|
+
} catch (err) {
|
|
415
|
+
const e = err;
|
|
416
|
+
if (e.code === 1) {
|
|
417
|
+
return { content: [{ type: "text", text: "No cron jobs found (crontab is empty)." }] };
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
content: [{ type: "text", text: `\u274C Failed to list cron jobs: ${e.message}` }],
|
|
421
|
+
isError: true
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
server.tool(
|
|
427
|
+
"cron_delete",
|
|
428
|
+
"Delete a cron job by its ID (from cron_list) or by matching command string",
|
|
429
|
+
{
|
|
430
|
+
id: import_zod.z.number().optional().describe("Cron job ID from cron_list output"),
|
|
431
|
+
command: import_zod.z.string().optional().describe("Delete job matching this command string")
|
|
432
|
+
},
|
|
433
|
+
async ({ id, command }) => {
|
|
434
|
+
if (!id && !command) {
|
|
435
|
+
return {
|
|
436
|
+
content: [{ type: "text", text: "\u274C Provide either id or command to identify the cron job." }],
|
|
437
|
+
isError: true
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
let existing = "";
|
|
442
|
+
try {
|
|
443
|
+
const { stdout } = await execAsync("crontab -l");
|
|
444
|
+
existing = stdout;
|
|
445
|
+
} catch {
|
|
446
|
+
return { content: [{ type: "text", text: "No cron jobs to delete." }] };
|
|
447
|
+
}
|
|
448
|
+
const lines = existing.split("\n");
|
|
449
|
+
if (command) {
|
|
450
|
+
const filtered2 = [];
|
|
451
|
+
for (let i = 0; i < lines.length; i++) {
|
|
452
|
+
if (lines[i].includes(command)) {
|
|
453
|
+
if (filtered2.length > 0 && filtered2[filtered2.length - 1].trim().startsWith("#")) {
|
|
454
|
+
filtered2.pop();
|
|
455
|
+
}
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
filtered2.push(lines[i]);
|
|
459
|
+
}
|
|
460
|
+
if (filtered2.length === lines.length) {
|
|
461
|
+
return {
|
|
462
|
+
content: [{ type: "text", text: `\u274C No cron job found matching: ${command}` }],
|
|
463
|
+
isError: true
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const updated2 = filtered2.join("\n");
|
|
467
|
+
const tmpFile2 = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
468
|
+
await import_promises.default.writeFile(tmpFile2, updated2, "utf-8");
|
|
469
|
+
await execAsync(`crontab ${tmpFile2}`);
|
|
470
|
+
await import_promises.default.unlink(tmpFile2).catch(() => {
|
|
471
|
+
});
|
|
472
|
+
return { content: [{ type: "text", text: `\u2705 Deleted cron job matching: ${command}` }] };
|
|
473
|
+
}
|
|
474
|
+
const entries = [];
|
|
475
|
+
let idx = 1;
|
|
476
|
+
for (let i = 0; i < lines.length; i++) {
|
|
477
|
+
const line = lines[i].trim();
|
|
478
|
+
if (line.startsWith("#")) continue;
|
|
479
|
+
const parts = line.split(/\s+/);
|
|
480
|
+
if (parts.length >= 6) {
|
|
481
|
+
const prevIsComment = i > 0 && lines[i - 1].trim().startsWith("#");
|
|
482
|
+
entries.push({ lineStart: prevIsComment ? i - 1 : i, lineEnd: i, idx: idx++ });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const target = entries.find((e) => e.idx === id);
|
|
486
|
+
if (!target) {
|
|
487
|
+
return {
|
|
488
|
+
content: [{ type: "text", text: `\u274C No cron job found with id=${id}. Use cron_list to see current IDs.` }],
|
|
489
|
+
isError: true
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const filtered = lines.filter((_, i) => i < target.lineStart || i > target.lineEnd);
|
|
493
|
+
const updated = filtered.join("\n");
|
|
494
|
+
const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
495
|
+
await import_promises.default.writeFile(tmpFile, updated, "utf-8");
|
|
496
|
+
await execAsync(`crontab ${tmpFile}`);
|
|
497
|
+
await import_promises.default.unlink(tmpFile).catch(() => {
|
|
498
|
+
});
|
|
499
|
+
return { content: [{ type: "text", text: `\u2705 Deleted cron job #${id}` }] };
|
|
500
|
+
} catch (err) {
|
|
501
|
+
return {
|
|
502
|
+
content: [{ type: "text", text: `\u274C Failed to delete cron job: ${err.message}` }],
|
|
503
|
+
isError: true
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
);
|
|
270
508
|
}
|
|
271
509
|
};
|
|
272
510
|
|
|
273
511
|
// src/tools/browser.ts
|
|
274
|
-
var
|
|
512
|
+
var import_browserclaw = require("browserclaw");
|
|
513
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
275
514
|
var import_zod2 = require("zod");
|
|
276
515
|
var BrowserTools = class {
|
|
277
516
|
browser = null;
|
|
278
517
|
page = null;
|
|
279
|
-
// 동시 요청 시 race condition 방지용 직렬화 락
|
|
280
518
|
lock = Promise.resolve();
|
|
519
|
+
armedDialog = null;
|
|
281
520
|
withLock(fn) {
|
|
282
521
|
let release;
|
|
283
522
|
const next = new Promise((r) => {
|
|
@@ -287,128 +526,373 @@ var BrowserTools = class {
|
|
|
287
526
|
this.lock = this.lock.then(() => next);
|
|
288
527
|
return current.then(() => fn()).finally(() => release());
|
|
289
528
|
}
|
|
529
|
+
/** mcp.ts에서 호출하는 init — BrowserClaw는 browser_start 도구로 명시적 시작하므로 noop */
|
|
290
530
|
async init() {
|
|
291
|
-
try {
|
|
292
|
-
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
293
|
-
this.page = await this.browser.newPage();
|
|
294
|
-
} catch {
|
|
295
|
-
console.warn(
|
|
296
|
-
"\u26A0\uFE0F Playwright \uBBF8\uC124\uCE58. \uBE0C\uB77C\uC6B0\uC800 \uB3C4\uAD6C \uBE44\uD65C\uC131\uD654.\n \uD65C\uC131\uD654: npx playwright install chromium"
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
531
|
}
|
|
300
532
|
async cleanup() {
|
|
301
|
-
await this.browser?.
|
|
533
|
+
await this.browser?.stop();
|
|
534
|
+
this.browser = null;
|
|
535
|
+
this.page = null;
|
|
302
536
|
}
|
|
303
537
|
register(server) {
|
|
304
538
|
const requirePage = () => {
|
|
305
|
-
if (!this.page) throw new Error("
|
|
539
|
+
if (!this.page) throw new Error("Browser not started. Call browser_start first.");
|
|
306
540
|
return this.page;
|
|
307
541
|
};
|
|
542
|
+
server.tool(
|
|
543
|
+
"browser_start",
|
|
544
|
+
"Start browser (BrowserClaw). mode='managed'(default) launches new Chromium; mode='remote-cdp' connects to existing Chrome via CDP URL.",
|
|
545
|
+
{
|
|
546
|
+
mode: import_zod2.z.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome"),
|
|
547
|
+
headless: import_zod2.z.boolean().optional().default(false).describe("Run headless (managed mode only)"),
|
|
548
|
+
cdpUrl: import_zod2.z.string().optional().describe("CDP URL for remote-cdp mode (e.g. http://localhost:9222)"),
|
|
549
|
+
profile: import_zod2.z.string().optional().describe("Profile name (managed mode only)"),
|
|
550
|
+
allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow localhost/internal URLs")
|
|
551
|
+
},
|
|
552
|
+
({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
|
|
553
|
+
if (this.browser) {
|
|
554
|
+
return { content: [{ type: "text", text: "Browser is already running. Call browser_stop first." }] };
|
|
555
|
+
}
|
|
556
|
+
if (mode === "remote-cdp") {
|
|
557
|
+
if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
|
|
558
|
+
this.browser = await import_browserclaw.BrowserClaw.connect(cdpUrl, { allowInternal });
|
|
559
|
+
} else {
|
|
560
|
+
this.browser = await import_browserclaw.BrowserClaw.launch({
|
|
561
|
+
headless,
|
|
562
|
+
profileName: profile,
|
|
563
|
+
allowInternal
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return { content: [{ type: "text", text: `Browser started (mode: ${mode})` }] };
|
|
567
|
+
})
|
|
568
|
+
);
|
|
569
|
+
server.tool(
|
|
570
|
+
"browser_stop",
|
|
571
|
+
"Stop browser and release resources",
|
|
572
|
+
{},
|
|
573
|
+
() => this.withLock(async () => {
|
|
574
|
+
await this.cleanup();
|
|
575
|
+
return { content: [{ type: "text", text: "Browser stopped" }] };
|
|
576
|
+
})
|
|
577
|
+
);
|
|
308
578
|
server.tool(
|
|
309
579
|
"browser_navigate",
|
|
310
|
-
"URL
|
|
311
|
-
{
|
|
580
|
+
"Navigate to URL. Opens new tab if browser started but no page yet.",
|
|
581
|
+
{
|
|
582
|
+
url: import_zod2.z.string().describe("URL to navigate to")
|
|
583
|
+
},
|
|
312
584
|
({ url }) => this.withLock(async () => {
|
|
313
|
-
|
|
314
|
-
|
|
585
|
+
if (!this.browser) throw new Error("Browser not started. Call browser_start first.");
|
|
586
|
+
if (!this.page) {
|
|
587
|
+
this.page = await this.browser.open(url);
|
|
588
|
+
} else {
|
|
589
|
+
await this.page.goto(url);
|
|
590
|
+
}
|
|
591
|
+
const currentUrl = await this.page.url();
|
|
592
|
+
return { content: [{ type: "text", text: `Navigated to: ${currentUrl}` }] };
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
server.tool(
|
|
596
|
+
"browser_snapshot",
|
|
597
|
+
"Get Accessibility Tree snapshot with ref numbers. Use refs to interact with elements (e.g. browser_click with ref='e1').",
|
|
598
|
+
{
|
|
599
|
+
interactive: import_zod2.z.boolean().optional().default(true).describe("Only include interactive elements"),
|
|
600
|
+
compact: import_zod2.z.boolean().optional().default(true).describe("Remove empty containers")
|
|
601
|
+
},
|
|
602
|
+
({ interactive, compact }) => this.withLock(async () => {
|
|
603
|
+
const result = await requirePage().snapshot({ interactive, compact });
|
|
604
|
+
const { snapshot, refs, stats } = result;
|
|
605
|
+
const refList = Object.entries(refs).map(([r, info]) => ` ${r}: ${info.role} "${info.name ?? ""}"`).join("\n");
|
|
606
|
+
const total = stats?.refs ?? Object.keys(refs).length;
|
|
315
607
|
return {
|
|
316
|
-
content: [{
|
|
608
|
+
content: [{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: `${snapshot}
|
|
611
|
+
|
|
612
|
+
--- refs (${total} total) ---
|
|
613
|
+
${refList}`
|
|
614
|
+
}]
|
|
317
615
|
};
|
|
318
616
|
})
|
|
319
617
|
);
|
|
320
618
|
server.tool(
|
|
321
619
|
"browser_click",
|
|
322
|
-
"
|
|
323
|
-
{
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
620
|
+
"Click element by ref number from browser_snapshot",
|
|
621
|
+
{
|
|
622
|
+
ref: import_zod2.z.string().describe("Ref number from snapshot (e.g. 'e1')"),
|
|
623
|
+
doubleClick: import_zod2.z.boolean().optional().default(false),
|
|
624
|
+
button: import_zod2.z.enum(["left", "right", "middle"]).optional().default("left")
|
|
625
|
+
},
|
|
626
|
+
({ ref, doubleClick, button }) => this.withLock(async () => {
|
|
627
|
+
await requirePage().click(ref, { doubleClick, button });
|
|
628
|
+
return { content: [{ type: "text", text: `Clicked ref=${ref}` }] };
|
|
327
629
|
})
|
|
328
630
|
);
|
|
329
631
|
server.tool(
|
|
330
632
|
"browser_type",
|
|
331
|
-
"
|
|
633
|
+
"Type text into element by ref number",
|
|
332
634
|
{
|
|
333
|
-
|
|
334
|
-
text: import_zod2.z.string().describe("
|
|
335
|
-
|
|
635
|
+
ref: import_zod2.z.string().describe("Ref number from snapshot"),
|
|
636
|
+
text: import_zod2.z.string().describe("Text to type"),
|
|
637
|
+
submit: import_zod2.z.boolean().optional().default(false).describe("Press Enter after typing"),
|
|
638
|
+
slowly: import_zod2.z.boolean().optional().default(false).describe("Type slowly (75ms per char)")
|
|
336
639
|
},
|
|
337
|
-
({
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
640
|
+
({ ref, text, submit, slowly }) => this.withLock(async () => {
|
|
641
|
+
await requirePage().type(ref, text, { submit, slowly });
|
|
642
|
+
return { content: [{ type: "text", text: `Typed into ref=${ref}` }] };
|
|
643
|
+
})
|
|
644
|
+
);
|
|
645
|
+
server.tool(
|
|
646
|
+
"browser_fill",
|
|
647
|
+
"Fill multiple form fields at once",
|
|
648
|
+
{
|
|
649
|
+
fields: import_zod2.z.array(import_zod2.z.object({
|
|
650
|
+
ref: import_zod2.z.string(),
|
|
651
|
+
type: import_zod2.z.enum(["text", "checkbox", "radio"]),
|
|
652
|
+
value: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.boolean()])
|
|
653
|
+
})).describe("Array of {ref, type, value}")
|
|
654
|
+
},
|
|
655
|
+
({ fields }) => this.withLock(async () => {
|
|
656
|
+
await requirePage().fill(fields);
|
|
657
|
+
return { content: [{ type: "text", text: `Filled ${fields.length} field(s)` }] };
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
server.tool(
|
|
661
|
+
"browser_select",
|
|
662
|
+
"Select dropdown option(s) by ref",
|
|
663
|
+
{
|
|
664
|
+
ref: import_zod2.z.string().describe("Ref number from snapshot"),
|
|
665
|
+
values: import_zod2.z.array(import_zod2.z.string()).describe("Option value(s) to select")
|
|
666
|
+
},
|
|
667
|
+
({ ref, values }) => this.withLock(async () => {
|
|
668
|
+
await requirePage().select(ref, ...values);
|
|
669
|
+
return { content: [{ type: "text", text: `Selected option(s) in ref=${ref}` }] };
|
|
670
|
+
})
|
|
671
|
+
);
|
|
672
|
+
server.tool(
|
|
673
|
+
"browser_press",
|
|
674
|
+
"Press keyboard key or combination (e.g. 'Enter', 'Control+a', 'Escape')",
|
|
675
|
+
{
|
|
676
|
+
key: import_zod2.z.string().describe("Key combination (e.g. 'Enter', 'Control+a', 'Escape', 'Tab')")
|
|
677
|
+
},
|
|
678
|
+
({ key }) => this.withLock(async () => {
|
|
679
|
+
await requirePage().press(key);
|
|
680
|
+
return { content: [{ type: "text", text: `Pressed: ${key}` }] };
|
|
681
|
+
})
|
|
682
|
+
);
|
|
683
|
+
server.tool(
|
|
684
|
+
"browser_hover",
|
|
685
|
+
"Hover mouse over element by ref",
|
|
686
|
+
{
|
|
687
|
+
ref: import_zod2.z.string().describe("Ref number from snapshot")
|
|
688
|
+
},
|
|
689
|
+
({ ref }) => this.withLock(async () => {
|
|
690
|
+
await requirePage().hover(ref);
|
|
691
|
+
return { content: [{ type: "text", text: `Hovered over ref=${ref}` }] };
|
|
692
|
+
})
|
|
693
|
+
);
|
|
694
|
+
server.tool(
|
|
695
|
+
"browser_drag",
|
|
696
|
+
"Drag element from startRef to endRef",
|
|
697
|
+
{
|
|
698
|
+
startRef: import_zod2.z.string().describe("Source element ref"),
|
|
699
|
+
endRef: import_zod2.z.string().describe("Target element ref")
|
|
700
|
+
},
|
|
701
|
+
({ startRef, endRef }) => this.withLock(async () => {
|
|
702
|
+
await requirePage().drag(startRef, endRef);
|
|
703
|
+
return { content: [{ type: "text", text: `Dragged ref=${startRef} \u2192 ref=${endRef}` }] };
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
server.tool(
|
|
707
|
+
"browser_upload",
|
|
708
|
+
"Upload file(s) to file input element by ref",
|
|
709
|
+
{
|
|
710
|
+
ref: import_zod2.z.string().describe("Ref number of file input element"),
|
|
711
|
+
paths: import_zod2.z.array(import_zod2.z.string()).describe("Absolute file path(s) to upload")
|
|
712
|
+
},
|
|
713
|
+
({ ref, paths }) => this.withLock(async () => {
|
|
714
|
+
await requirePage().uploadFile(ref, paths);
|
|
715
|
+
return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ref=${ref}` }] };
|
|
342
716
|
})
|
|
343
717
|
);
|
|
344
718
|
server.tool(
|
|
345
719
|
"browser_screenshot",
|
|
346
|
-
"
|
|
720
|
+
"Take screenshot of current page",
|
|
347
721
|
{
|
|
348
|
-
path: import_zod2.z.string().optional().describe("
|
|
349
|
-
|
|
722
|
+
path: import_zod2.z.string().optional().describe("Save path (if omitted, returns base64)"),
|
|
723
|
+
fullPage: import_zod2.z.boolean().optional().default(false),
|
|
724
|
+
ref: import_zod2.z.string().optional().describe("Capture specific element by ref")
|
|
350
725
|
},
|
|
351
|
-
({ path: path2,
|
|
352
|
-
const
|
|
353
|
-
const screenshot = await page.screenshot({
|
|
354
|
-
path: path2 ?? void 0,
|
|
355
|
-
fullPage: full_page
|
|
356
|
-
});
|
|
726
|
+
({ path: path2, fullPage, ref }) => this.withLock(async () => {
|
|
727
|
+
const buffer = await requirePage().screenshot({ fullPage, ref });
|
|
357
728
|
if (path2) {
|
|
358
|
-
|
|
729
|
+
await import_promises2.default.writeFile(path2, buffer);
|
|
730
|
+
return { content: [{ type: "text", text: `Screenshot saved: ${path2}` }] };
|
|
359
731
|
}
|
|
360
732
|
return {
|
|
361
|
-
content: [
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
]
|
|
733
|
+
content: [{
|
|
734
|
+
type: "image",
|
|
735
|
+
data: buffer.toString("base64"),
|
|
736
|
+
mimeType: "image/png"
|
|
737
|
+
}]
|
|
368
738
|
};
|
|
369
739
|
})
|
|
370
740
|
);
|
|
371
741
|
server.tool(
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
]
|
|
382
|
-
};
|
|
742
|
+
"browser_pdf",
|
|
743
|
+
"Save current page as PDF",
|
|
744
|
+
{
|
|
745
|
+
path: import_zod2.z.string().describe("Save path (.pdf)")
|
|
746
|
+
},
|
|
747
|
+
({ path: path2 }) => this.withLock(async () => {
|
|
748
|
+
const buffer = await requirePage().pdf();
|
|
749
|
+
await import_promises2.default.writeFile(path2, buffer);
|
|
750
|
+
return { content: [{ type: "text", text: `PDF saved: ${path2}` }] };
|
|
383
751
|
})
|
|
384
752
|
);
|
|
385
753
|
server.tool(
|
|
386
754
|
"browser_evaluate",
|
|
387
|
-
"JavaScript
|
|
388
|
-
{
|
|
755
|
+
"Execute JavaScript in page context",
|
|
756
|
+
{
|
|
757
|
+
code: import_zod2.z.string().describe("JavaScript code to execute (wrap in function if needed)")
|
|
758
|
+
},
|
|
389
759
|
({ code }) => this.withLock(async () => {
|
|
390
760
|
try {
|
|
391
761
|
const result = await requirePage().evaluate(code);
|
|
392
762
|
return {
|
|
393
|
-
content: [
|
|
394
|
-
|
|
395
|
-
|
|
763
|
+
content: [{
|
|
764
|
+
type: "text",
|
|
765
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
766
|
+
}]
|
|
396
767
|
};
|
|
397
768
|
} catch (err) {
|
|
398
769
|
return {
|
|
399
|
-
content: [{ type: "text", text: `\u274C
|
|
770
|
+
content: [{ type: "text", text: `\u274C JS error: ${err.message}` }],
|
|
400
771
|
isError: true
|
|
401
772
|
};
|
|
402
773
|
}
|
|
403
774
|
})
|
|
404
775
|
);
|
|
405
776
|
server.tool(
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
777
|
+
"browser_wait",
|
|
778
|
+
"Wait for a condition: text appearance/disappearance, URL pattern, or fixed time",
|
|
779
|
+
{
|
|
780
|
+
text: import_zod2.z.string().optional().describe("Wait until this text appears"),
|
|
781
|
+
textGone: import_zod2.z.string().optional().describe("Wait until this text disappears"),
|
|
782
|
+
url: import_zod2.z.string().optional().describe("Wait until URL matches (glob pattern, e.g. '**/dashboard')"),
|
|
783
|
+
loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for load state"),
|
|
784
|
+
timeMs: import_zod2.z.number().optional().describe("Wait fixed milliseconds")
|
|
785
|
+
},
|
|
786
|
+
({ text, textGone, url, loadState, timeMs }) => this.withLock(async () => {
|
|
787
|
+
const condition = {};
|
|
788
|
+
if (text) condition.text = text;
|
|
789
|
+
if (textGone) condition.textGone = textGone;
|
|
790
|
+
if (url) condition.url = url;
|
|
791
|
+
if (loadState) condition.loadState = loadState;
|
|
792
|
+
if (timeMs) condition.timeMs = timeMs;
|
|
793
|
+
await requirePage().waitFor(condition);
|
|
794
|
+
return { content: [{ type: "text", text: "Wait condition met" }] };
|
|
795
|
+
})
|
|
796
|
+
);
|
|
797
|
+
server.tool(
|
|
798
|
+
"browser_cookies",
|
|
799
|
+
"Get, set, or clear cookies",
|
|
800
|
+
{
|
|
801
|
+
action: import_zod2.z.enum(["get", "set", "clear"]).describe("Action to perform"),
|
|
802
|
+
cookie: import_zod2.z.object({
|
|
803
|
+
name: import_zod2.z.string(),
|
|
804
|
+
value: import_zod2.z.string(),
|
|
805
|
+
domain: import_zod2.z.string().optional(),
|
|
806
|
+
path: import_zod2.z.string().optional(),
|
|
807
|
+
httpOnly: import_zod2.z.boolean().optional(),
|
|
808
|
+
secure: import_zod2.z.boolean().optional()
|
|
809
|
+
}).optional().describe("Cookie data (required for set action)")
|
|
810
|
+
},
|
|
811
|
+
({ action, cookie }) => this.withLock(async () => {
|
|
812
|
+
const page = requirePage();
|
|
813
|
+
if (action === "get") {
|
|
814
|
+
const cookies = await page.cookies();
|
|
815
|
+
return { content: [{ type: "text", text: JSON.stringify(cookies, null, 2) }] };
|
|
816
|
+
} else if (action === "set") {
|
|
817
|
+
if (!cookie) throw new Error("cookie is required for set action");
|
|
818
|
+
await page.setCookie({ path: "/", ...cookie });
|
|
819
|
+
return { content: [{ type: "text", text: `Cookie set: ${cookie.name}` }] };
|
|
820
|
+
} else {
|
|
821
|
+
await page.clearCookies();
|
|
822
|
+
return { content: [{ type: "text", text: "All cookies cleared" }] };
|
|
823
|
+
}
|
|
824
|
+
})
|
|
825
|
+
);
|
|
826
|
+
server.tool(
|
|
827
|
+
"browser_storage",
|
|
828
|
+
"Read/write/clear localStorage or sessionStorage",
|
|
829
|
+
{
|
|
830
|
+
action: import_zod2.z.enum(["get", "set", "clear"]).describe("Action to perform"),
|
|
831
|
+
kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("Storage type"),
|
|
832
|
+
key: import_zod2.z.string().optional().describe("Storage key (get/set)"),
|
|
833
|
+
value: import_zod2.z.string().optional().describe("Value to set (set action)")
|
|
834
|
+
},
|
|
835
|
+
({ action, kind, key, value }) => this.withLock(async () => {
|
|
836
|
+
const page = requirePage();
|
|
837
|
+
if (action === "get") {
|
|
838
|
+
const result = await page.storageGet(kind, key);
|
|
839
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
840
|
+
} else if (action === "set") {
|
|
841
|
+
if (!key || value === void 0) throw new Error("key and value are required for set action");
|
|
842
|
+
await page.storageSet(kind, key, value);
|
|
843
|
+
return { content: [{ type: "text", text: `Storage set: ${key}` }] };
|
|
844
|
+
} else {
|
|
845
|
+
await page.storageClear(kind);
|
|
846
|
+
return { content: [{ type: "text", text: `${kind}Storage cleared` }] };
|
|
847
|
+
}
|
|
848
|
+
})
|
|
849
|
+
);
|
|
850
|
+
server.tool(
|
|
851
|
+
"browser_dialog",
|
|
852
|
+
[
|
|
853
|
+
"Handle JavaScript dialogs (alert/confirm/prompt).",
|
|
854
|
+
"Two-step usage:",
|
|
855
|
+
" 1. action='arm' \u2014 register a one-shot handler (returns immediately, does NOT block).",
|
|
856
|
+
" 2. Trigger the dialog (e.g. browser_click on the button that calls confirm()).",
|
|
857
|
+
" 3. action='wait' \u2014 await the handler to confirm the dialog was handled.",
|
|
858
|
+
"The 'accept' and 'promptText' params are only used with action='arm'."
|
|
859
|
+
].join(" "),
|
|
860
|
+
{
|
|
861
|
+
action: import_zod2.z.enum(["arm", "wait"]).describe(
|
|
862
|
+
"'arm' = register handler and return immediately; 'wait' = await the previously armed handler"
|
|
863
|
+
),
|
|
864
|
+
accept: import_zod2.z.boolean().optional().default(true).describe(
|
|
865
|
+
"Accept (true) or dismiss (false) the dialog. Only used with action='arm'."
|
|
866
|
+
),
|
|
867
|
+
promptText: import_zod2.z.string().optional().describe(
|
|
868
|
+
"Text to enter if the dialog is a prompt. Only used with action='arm'."
|
|
869
|
+
),
|
|
870
|
+
timeoutMs: import_zod2.z.number().optional().describe(
|
|
871
|
+
"Timeout in ms for 'wait' action. Default: 30000."
|
|
872
|
+
)
|
|
873
|
+
},
|
|
874
|
+
({ action, accept, promptText, timeoutMs }) => this.withLock(async () => {
|
|
875
|
+
if (action === "arm") {
|
|
876
|
+
this.armedDialog = requirePage().armDialog({
|
|
877
|
+
accept: accept ?? true,
|
|
878
|
+
promptText,
|
|
879
|
+
timeoutMs
|
|
880
|
+
});
|
|
881
|
+
this.armedDialog.catch(() => {
|
|
882
|
+
});
|
|
883
|
+
return { content: [{ type: "text", text: "Dialog handler armed. Trigger the dialog now, then call browser_dialog with action='wait'." }] };
|
|
884
|
+
} else {
|
|
885
|
+
if (!this.armedDialog) {
|
|
886
|
+
return {
|
|
887
|
+
content: [{ type: "text", text: "No dialog handler is armed. Call browser_dialog with action='arm' first." }],
|
|
888
|
+
isError: true
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
const pending = this.armedDialog;
|
|
892
|
+
this.armedDialog = null;
|
|
893
|
+
await pending;
|
|
894
|
+
return { content: [{ type: "text", text: "Dialog handled successfully." }] };
|
|
895
|
+
}
|
|
412
896
|
})
|
|
413
897
|
);
|
|
414
898
|
}
|
|
@@ -416,33 +900,33 @@ var BrowserTools = class {
|
|
|
416
900
|
|
|
417
901
|
// src/tools/notebook.ts
|
|
418
902
|
var import_zod3 = require("zod");
|
|
419
|
-
var
|
|
903
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
420
904
|
var import_child_process2 = require("child_process");
|
|
421
905
|
var import_util2 = require("util");
|
|
422
906
|
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
423
907
|
async function readNotebook(filePath) {
|
|
424
|
-
const raw = await
|
|
908
|
+
const raw = await import_promises3.default.readFile(filePath, "utf-8");
|
|
425
909
|
try {
|
|
426
910
|
return JSON.parse(raw);
|
|
427
911
|
} catch {
|
|
428
|
-
throw new Error(
|
|
912
|
+
throw new Error(`Invalid Jupyter notebook file: ${filePath}`);
|
|
429
913
|
}
|
|
430
914
|
}
|
|
431
915
|
async function writeNotebook(filePath, nb) {
|
|
432
|
-
await
|
|
916
|
+
await import_promises3.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
|
|
433
917
|
}
|
|
434
918
|
var NotebookTools = class {
|
|
435
919
|
register(server) {
|
|
436
920
|
server.tool(
|
|
437
921
|
"notebook_read",
|
|
438
|
-
".ipynb
|
|
439
|
-
{ path: import_zod3.z.string().describe("
|
|
922
|
+
"Read .ipynb notebook",
|
|
923
|
+
{ path: import_zod3.z.string().describe("Notebook file path") },
|
|
440
924
|
async ({ path: filePath }) => {
|
|
441
925
|
const nb = await readNotebook(filePath);
|
|
442
926
|
const cells = nb.cells.map((cell, i) => ({
|
|
443
927
|
index: i,
|
|
444
928
|
type: cell.cell_type,
|
|
445
|
-
source: cell.source.join(""),
|
|
929
|
+
source: Array.isArray(cell.source) ? cell.source.join("") : cell.source,
|
|
446
930
|
outputs: cell.outputs?.length ?? 0
|
|
447
931
|
}));
|
|
448
932
|
return {
|
|
@@ -452,30 +936,30 @@ var NotebookTools = class {
|
|
|
452
936
|
);
|
|
453
937
|
server.tool(
|
|
454
938
|
"notebook_edit_cell",
|
|
455
|
-
"
|
|
939
|
+
"Edit a specific notebook cell",
|
|
456
940
|
{
|
|
457
941
|
path: import_zod3.z.string(),
|
|
458
|
-
cell_index: import_zod3.z.number().describe("
|
|
459
|
-
source: import_zod3.z.string().describe("
|
|
942
|
+
cell_index: import_zod3.z.number().describe("Cell index (0-based)"),
|
|
943
|
+
source: import_zod3.z.string().describe("New source code")
|
|
460
944
|
},
|
|
461
945
|
async ({ path: filePath, cell_index, source }) => {
|
|
462
946
|
const nb = await readNotebook(filePath);
|
|
463
947
|
if (cell_index < 0 || cell_index >= nb.cells.length) {
|
|
464
|
-
throw new Error(
|
|
948
|
+
throw new Error(`Invalid cell index: ${cell_index}`);
|
|
465
949
|
}
|
|
466
950
|
nb.cells[cell_index].source = source.split("\n").map(
|
|
467
951
|
(l, i, arr) => i < arr.length - 1 ? l + "\n" : l
|
|
468
952
|
);
|
|
469
953
|
await writeNotebook(filePath, nb);
|
|
470
|
-
return { content: [{ type: "text", text: "
|
|
954
|
+
return { content: [{ type: "text", text: "Cell updated" }] };
|
|
471
955
|
}
|
|
472
956
|
);
|
|
473
957
|
server.tool(
|
|
474
958
|
"notebook_execute",
|
|
475
|
-
"
|
|
959
|
+
"Execute notebook (nbconvert --execute)",
|
|
476
960
|
{
|
|
477
|
-
path: import_zod3.z.string().describe("
|
|
478
|
-
timeout: import_zod3.z.number().optional().default(300).describe("
|
|
961
|
+
path: import_zod3.z.string().describe("Notebook file path"),
|
|
962
|
+
timeout: import_zod3.z.number().optional().default(300).describe("Timeout per cell (seconds)")
|
|
479
963
|
},
|
|
480
964
|
async ({ path: filePath, timeout }) => {
|
|
481
965
|
const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
|
|
@@ -491,7 +975,7 @@ var NotebookTools = class {
|
|
|
491
975
|
for (const jupyter of candidates) {
|
|
492
976
|
try {
|
|
493
977
|
const { stdout, stderr } = await execAsync2(`${jupyter} ${nbconvertArgs}`);
|
|
494
|
-
return { content: [{ type: "text", text: stdout || stderr || "
|
|
978
|
+
return { content: [{ type: "text", text: stdout || stderr || "Execution complete" }] };
|
|
495
979
|
} catch (err) {
|
|
496
980
|
const error = err;
|
|
497
981
|
if (error.code !== "127" && !error.message?.includes("not found") && !error.message?.includes("No such file")) {
|
|
@@ -499,17 +983,17 @@ var NotebookTools = class {
|
|
|
499
983
|
}
|
|
500
984
|
}
|
|
501
985
|
}
|
|
502
|
-
throw new Error("jupyter
|
|
986
|
+
throw new Error("jupyter not found. Install it and try again: pip install jupyter");
|
|
503
987
|
}
|
|
504
988
|
);
|
|
505
989
|
server.tool(
|
|
506
990
|
"notebook_add_cell",
|
|
507
|
-
"
|
|
991
|
+
"Add a new cell to notebook",
|
|
508
992
|
{
|
|
509
|
-
path: import_zod3.z.string().describe(".ipynb
|
|
510
|
-
cell_type: import_zod3.z.enum(["code", "markdown"]).describe("
|
|
511
|
-
source: import_zod3.z.string().describe("
|
|
512
|
-
position: import_zod3.z.number().optional().describe("
|
|
993
|
+
path: import_zod3.z.string().describe(".ipynb file path"),
|
|
994
|
+
cell_type: import_zod3.z.enum(["code", "markdown"]).describe("Cell type"),
|
|
995
|
+
source: import_zod3.z.string().describe("Cell source content"),
|
|
996
|
+
position: import_zod3.z.number().optional().describe("Insert position (0-based). Appends to end if omitted")
|
|
513
997
|
},
|
|
514
998
|
async ({ path: filePath, cell_type: cellType, source, position }) => {
|
|
515
999
|
const nb = await readNotebook(filePath);
|
|
@@ -528,31 +1012,31 @@ var NotebookTools = class {
|
|
|
528
1012
|
} else if (position > nb.cells.length) {
|
|
529
1013
|
nb.cells.push(newCell);
|
|
530
1014
|
actualIndex = nb.cells.length - 1;
|
|
531
|
-
warning = ` (
|
|
1015
|
+
warning = ` (warning: position ${position} exceeded range, appended at end (index: ${actualIndex}))`;
|
|
532
1016
|
} else {
|
|
533
1017
|
const clamped = Math.max(0, position);
|
|
534
1018
|
nb.cells.splice(clamped, 0, newCell);
|
|
535
1019
|
actualIndex = clamped;
|
|
536
1020
|
}
|
|
537
1021
|
await writeNotebook(filePath, nb);
|
|
538
|
-
return { content: [{ type: "text", text:
|
|
1022
|
+
return { content: [{ type: "text", text: `Cell added (index: ${actualIndex})${warning}` }] };
|
|
539
1023
|
}
|
|
540
1024
|
);
|
|
541
1025
|
server.tool(
|
|
542
1026
|
"notebook_delete_cell",
|
|
543
|
-
"
|
|
1027
|
+
"Delete a specific notebook cell",
|
|
544
1028
|
{
|
|
545
|
-
path: import_zod3.z.string().describe(".ipynb
|
|
546
|
-
cell_index: import_zod3.z.number().describe("
|
|
1029
|
+
path: import_zod3.z.string().describe(".ipynb file path"),
|
|
1030
|
+
cell_index: import_zod3.z.number().describe("Cell index to delete (0-based)")
|
|
547
1031
|
},
|
|
548
1032
|
async ({ path: filePath, cell_index }) => {
|
|
549
1033
|
const nb = await readNotebook(filePath);
|
|
550
1034
|
if (cell_index < 0 || cell_index >= nb.cells.length) {
|
|
551
|
-
throw new Error(
|
|
1035
|
+
throw new Error(`Invalid cell index: ${cell_index}`);
|
|
552
1036
|
}
|
|
553
1037
|
nb.cells.splice(cell_index, 1);
|
|
554
1038
|
await writeNotebook(filePath, nb);
|
|
555
|
-
return { content: [{ type: "text", text:
|
|
1039
|
+
return { content: [{ type: "text", text: `Cell deleted (index: ${cell_index})` }] };
|
|
556
1040
|
}
|
|
557
1041
|
);
|
|
558
1042
|
}
|
|
@@ -572,42 +1056,9 @@ function platform() {
|
|
|
572
1056
|
}
|
|
573
1057
|
var DeviceTools = class {
|
|
574
1058
|
register(server) {
|
|
575
|
-
server.tool(
|
|
576
|
-
"screen_capture",
|
|
577
|
-
"\uD654\uBA74 \uC2A4\uD06C\uB9B0\uC0F7 (OS \uB124\uC774\uD2F0\uBE0C)",
|
|
578
|
-
{
|
|
579
|
-
output_path: import_zod4.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 temp \uC800\uC7A5 \uD6C4 base64 \uBC18\uD658)")
|
|
580
|
-
},
|
|
581
|
-
async ({ output_path }) => {
|
|
582
|
-
const p = platform();
|
|
583
|
-
const isTmp = !output_path;
|
|
584
|
-
const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
|
|
585
|
-
const cmd = {
|
|
586
|
-
mac: `screencapture -x "${tmpPath}"`,
|
|
587
|
-
win: `nircmd.exe savescreenshot "${tmpPath}"`,
|
|
588
|
-
linux: `scrot "${tmpPath}"`
|
|
589
|
-
}[p];
|
|
590
|
-
try {
|
|
591
|
-
await execAsync3(cmd);
|
|
592
|
-
} catch (err) {
|
|
593
|
-
throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
|
|
594
|
-
}
|
|
595
|
-
const { readFileSync, unlinkSync } = await import("fs");
|
|
596
|
-
const data = readFileSync(tmpPath).toString("base64");
|
|
597
|
-
if (isTmp) {
|
|
598
|
-
try {
|
|
599
|
-
unlinkSync(tmpPath);
|
|
600
|
-
} catch {
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return {
|
|
604
|
-
content: [{ type: "image", data, mimeType: "image/png" }]
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
);
|
|
608
1059
|
server.tool(
|
|
609
1060
|
"camera_capture",
|
|
610
|
-
"
|
|
1061
|
+
"Camera photo capture",
|
|
611
1062
|
{
|
|
612
1063
|
output_path: import_zod4.z.string().optional()
|
|
613
1064
|
},
|
|
@@ -625,10 +1076,10 @@ var DeviceTools = class {
|
|
|
625
1076
|
} catch (err) {
|
|
626
1077
|
const e = err;
|
|
627
1078
|
return {
|
|
628
|
-
content: [{ type: "text", text: `\u274C
|
|
629
|
-
|
|
1079
|
+
content: [{ type: "text", text: `\u274C Camera not found or inaccessible.
|
|
1080
|
+
Cause: ${e.message}
|
|
630
1081
|
|
|
631
|
-
|
|
1082
|
+
Please check if a camera is connected.` }],
|
|
632
1083
|
isError: true
|
|
633
1084
|
};
|
|
634
1085
|
}
|
|
@@ -645,10 +1096,10 @@ var DeviceTools = class {
|
|
|
645
1096
|
);
|
|
646
1097
|
server.tool(
|
|
647
1098
|
"notification_send",
|
|
648
|
-
"OS
|
|
1099
|
+
"Send OS notification",
|
|
649
1100
|
{
|
|
650
|
-
title: import_zod4.z.string().describe("
|
|
651
|
-
message: import_zod4.z.string().describe("
|
|
1101
|
+
title: import_zod4.z.string().describe("Notification title"),
|
|
1102
|
+
message: import_zod4.z.string().describe("Notification body")
|
|
652
1103
|
},
|
|
653
1104
|
async ({ title, message }) => {
|
|
654
1105
|
try {
|
|
@@ -661,10 +1112,10 @@ var DeviceTools = class {
|
|
|
661
1112
|
}
|
|
662
1113
|
);
|
|
663
1114
|
});
|
|
664
|
-
return { content: [{ type: "text", text: "
|
|
1115
|
+
return { content: [{ type: "text", text: "Notification sent" }] };
|
|
665
1116
|
} catch (err) {
|
|
666
1117
|
return {
|
|
667
|
-
content: [{ type: "text", text:
|
|
1118
|
+
content: [{ type: "text", text: `Notification failed: ${err.message}` }],
|
|
668
1119
|
isError: true
|
|
669
1120
|
};
|
|
670
1121
|
}
|
|
@@ -672,7 +1123,7 @@ var DeviceTools = class {
|
|
|
672
1123
|
);
|
|
673
1124
|
server.tool(
|
|
674
1125
|
"clipboard_read",
|
|
675
|
-
"
|
|
1126
|
+
"Read clipboard",
|
|
676
1127
|
{},
|
|
677
1128
|
async () => {
|
|
678
1129
|
const p = platform();
|
|
@@ -683,7 +1134,7 @@ var DeviceTools = class {
|
|
|
683
1134
|
);
|
|
684
1135
|
server.tool(
|
|
685
1136
|
"clipboard_write",
|
|
686
|
-
"
|
|
1137
|
+
"Write to clipboard",
|
|
687
1138
|
{ text: import_zod4.z.string() },
|
|
688
1139
|
async ({ text }) => {
|
|
689
1140
|
const p = platform();
|
|
@@ -693,21 +1144,21 @@ var DeviceTools = class {
|
|
|
693
1144
|
linux: `echo "${text}" | xclip -selection clipboard`
|
|
694
1145
|
}[p];
|
|
695
1146
|
await execAsync3(cmd);
|
|
696
|
-
return { content: [{ type: "text", text: "
|
|
1147
|
+
return { content: [{ type: "text", text: "Saved to clipboard" }] };
|
|
697
1148
|
}
|
|
698
1149
|
);
|
|
699
1150
|
server.tool(
|
|
700
1151
|
"screen_record",
|
|
701
|
-
"
|
|
1152
|
+
"Start/stop screen recording (macOS: screencapture -v, others: ffmpeg)",
|
|
702
1153
|
{
|
|
703
|
-
action: import_zod4.z.enum(["start", "stop"]).describe("start:
|
|
704
|
-
output_path: import_zod4.z.string().optional().describe("
|
|
1154
|
+
action: import_zod4.z.enum(["start", "stop"]).describe("start: begin recording, stop: end recording"),
|
|
1155
|
+
output_path: import_zod4.z.string().optional().describe("Output path (used on start, default: /tmp/junis_record_<timestamp>.mp4)")
|
|
705
1156
|
},
|
|
706
1157
|
async ({ action, output_path }) => {
|
|
707
1158
|
const p = platform();
|
|
708
1159
|
if (action === "start") {
|
|
709
1160
|
if (screenRecordPid) {
|
|
710
|
-
return { content: [{ type: "text", text: "
|
|
1161
|
+
return { content: [{ type: "text", text: "Already recording." }] };
|
|
711
1162
|
}
|
|
712
1163
|
const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
|
|
713
1164
|
const { spawn } = await import("child_process");
|
|
@@ -715,10 +1166,10 @@ var DeviceTools = class {
|
|
|
715
1166
|
const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
|
|
716
1167
|
child.unref();
|
|
717
1168
|
screenRecordPid = child.pid ?? null;
|
|
718
|
-
return { content: [{ type: "text", text:
|
|
1169
|
+
return { content: [{ type: "text", text: `Recording started. Output path: ${tmpPath} (PID: ${screenRecordPid})` }] };
|
|
719
1170
|
} else {
|
|
720
1171
|
if (!screenRecordPid) {
|
|
721
|
-
return { content: [{ type: "text", text: "
|
|
1172
|
+
return { content: [{ type: "text", text: "Not currently recording." }] };
|
|
722
1173
|
}
|
|
723
1174
|
try {
|
|
724
1175
|
process.kill(screenRecordPid, "SIGINT");
|
|
@@ -726,13 +1177,13 @@ var DeviceTools = class {
|
|
|
726
1177
|
} catch {
|
|
727
1178
|
}
|
|
728
1179
|
screenRecordPid = null;
|
|
729
|
-
return { content: [{ type: "text", text: "
|
|
1180
|
+
return { content: [{ type: "text", text: "Recording stopped." }] };
|
|
730
1181
|
}
|
|
731
1182
|
}
|
|
732
1183
|
);
|
|
733
1184
|
server.tool(
|
|
734
1185
|
"location_get",
|
|
735
|
-
"
|
|
1186
|
+
"Get current location (macOS: CoreLocation CLI, others: IP-based fallback)",
|
|
736
1187
|
{},
|
|
737
1188
|
async () => {
|
|
738
1189
|
const p = platform();
|
|
@@ -740,28 +1191,28 @@ var DeviceTools = class {
|
|
|
740
1191
|
try {
|
|
741
1192
|
const { stdout } = await execAsync3("CoreLocationCLI -once -format '%latitude,%longitude'", { timeout: 1e4 });
|
|
742
1193
|
const [lat, lon] = stdout.trim().split(",");
|
|
743
|
-
return { content: [{ type: "text", text:
|
|
1194
|
+
return { content: [{ type: "text", text: `Latitude: ${lat}, Longitude: ${lon}` }] };
|
|
744
1195
|
} catch {
|
|
745
1196
|
}
|
|
746
1197
|
}
|
|
747
1198
|
const res = await fetch("http://ip-api.com/json/");
|
|
748
1199
|
const data = await res.json();
|
|
749
1200
|
if (data.status !== "success") {
|
|
750
|
-
throw new Error(`IP
|
|
1201
|
+
throw new Error(`IP location lookup failed: ${data.message ?? data.status}`);
|
|
751
1202
|
}
|
|
752
1203
|
return {
|
|
753
1204
|
content: [{
|
|
754
1205
|
type: "text",
|
|
755
|
-
text:
|
|
1206
|
+
text: `Latitude: ${data.lat}, Longitude: ${data.lon}, City: ${data.city}, Country: ${data.country} (estimated via IP)`
|
|
756
1207
|
}]
|
|
757
1208
|
};
|
|
758
1209
|
}
|
|
759
1210
|
);
|
|
760
1211
|
server.tool(
|
|
761
1212
|
"audio_play",
|
|
762
|
-
"
|
|
1213
|
+
"Play audio file (macOS: afplay, others: ffplay)",
|
|
763
1214
|
{
|
|
764
|
-
file_path: import_zod4.z.string().describe("
|
|
1215
|
+
file_path: import_zod4.z.string().describe("Path to the audio file to play")
|
|
765
1216
|
},
|
|
766
1217
|
async ({ file_path }) => {
|
|
767
1218
|
const p = platform();
|
|
@@ -771,14 +1222,14 @@ var DeviceTools = class {
|
|
|
771
1222
|
linux: `ffplay -nodisp -autoexit "${file_path}"`
|
|
772
1223
|
}[p];
|
|
773
1224
|
await execAsync3(cmd);
|
|
774
|
-
return { content: [{ type: "text", text:
|
|
1225
|
+
return { content: [{ type: "text", text: `Playback complete: ${file_path}` }] };
|
|
775
1226
|
}
|
|
776
1227
|
);
|
|
777
1228
|
}
|
|
778
1229
|
};
|
|
779
1230
|
|
|
780
1231
|
// src/server/stdio.ts
|
|
781
|
-
async function
|
|
1232
|
+
async function startStdioServer() {
|
|
782
1233
|
const server = new import_mcp.McpServer({ name: "junis", version: "0.1.0" });
|
|
783
1234
|
const fsTools = new FilesystemTools();
|
|
784
1235
|
fsTools.register(server);
|
|
@@ -796,4 +1247,10 @@ async function main() {
|
|
|
796
1247
|
process.exit(0);
|
|
797
1248
|
});
|
|
798
1249
|
}
|
|
799
|
-
|
|
1250
|
+
if (require.main === module) {
|
|
1251
|
+
startStdioServer().catch(console.error);
|
|
1252
|
+
}
|
|
1253
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1254
|
+
0 && (module.exports = {
|
|
1255
|
+
startStdioServer
|
|
1256
|
+
});
|