junis 0.3.4 → 0.3.6
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/dist/cli/index.js +395 -330
- package/dist/server/mcp.js +182 -220
- package/dist/server/stdio.js +150 -183
- package/package.json +3 -2
package/dist/server/stdio.js
CHANGED
|
@@ -1,49 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
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
|
-
};
|
|
13
|
-
var __copyProps = (to, from, except, desc) => {
|
|
14
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
-
for (let key of __getOwnPropNames(from))
|
|
16
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
-
}
|
|
19
|
-
return to;
|
|
20
|
-
};
|
|
21
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
-
mod
|
|
28
|
-
));
|
|
29
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
2
|
|
|
31
3
|
// src/server/stdio.ts
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
startStdioServer: () => startStdioServer
|
|
35
|
-
});
|
|
36
|
-
module.exports = __toCommonJS(stdio_exports);
|
|
37
|
-
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
38
|
-
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
39
6
|
|
|
40
7
|
// src/tools/filesystem.ts
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
8
|
+
import { exec, execFile } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
10
|
+
import fs from "fs/promises";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { glob } from "glob";
|
|
13
|
+
import { z } from "zod";
|
|
47
14
|
|
|
48
15
|
// src/server/permissions.ts
|
|
49
16
|
var toolPermissions = {
|
|
@@ -81,9 +48,9 @@ var toolPermissions = {
|
|
|
81
48
|
cron_delete: "confirm",
|
|
82
49
|
edit_block: "confirm",
|
|
83
50
|
kill_process: "confirm",
|
|
84
|
-
// 시스템 변경 —
|
|
85
|
-
execute_command: "
|
|
86
|
-
write_file: "
|
|
51
|
+
// 시스템 변경 — 기본 차단 (PDF 7.3절)
|
|
52
|
+
execute_command: "deny",
|
|
53
|
+
write_file: "deny"
|
|
87
54
|
};
|
|
88
55
|
function checkPermission(toolName) {
|
|
89
56
|
const level = toolPermissions[toolName];
|
|
@@ -95,8 +62,8 @@ function checkPermission(toolName) {
|
|
|
95
62
|
}
|
|
96
63
|
|
|
97
64
|
// src/tools/filesystem.ts
|
|
98
|
-
var execAsync =
|
|
99
|
-
var execFileAsync =
|
|
65
|
+
var execAsync = promisify(exec);
|
|
66
|
+
var execFileAsync = promisify(execFile);
|
|
100
67
|
var FilesystemTools = class {
|
|
101
68
|
register(server) {
|
|
102
69
|
server.tool(
|
|
@@ -118,14 +85,14 @@ var FilesystemTools = class {
|
|
|
118
85
|
"- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
|
|
119
86
|
].join("\n"),
|
|
120
87
|
{
|
|
121
|
-
command:
|
|
122
|
-
timeout_ms:
|
|
123
|
-
background:
|
|
88
|
+
command: z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
|
|
89
|
+
timeout_ms: z.number().optional().default(3e4).describe("Maximum execution time in milliseconds (default: 30000). Increase for long-running builds or downloads."),
|
|
90
|
+
background: z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
|
|
124
91
|
},
|
|
125
92
|
async ({ command, timeout_ms, background }) => {
|
|
126
93
|
checkPermission("execute_command");
|
|
127
94
|
if (background) {
|
|
128
|
-
|
|
95
|
+
exec(command);
|
|
129
96
|
return { content: [{ type: "text", text: "Background execution started" }] };
|
|
130
97
|
}
|
|
131
98
|
try {
|
|
@@ -159,12 +126,12 @@ ${error.stderr ?? ""}`
|
|
|
159
126
|
"For searching within files, prefer search_code instead. For listing directory contents, use list_directory."
|
|
160
127
|
].join("\n"),
|
|
161
128
|
{
|
|
162
|
-
path:
|
|
163
|
-
encoding:
|
|
129
|
+
path: z.string().describe("Absolute or relative file path to read"),
|
|
130
|
+
encoding: z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("'utf-8' for text files (default), 'base64' for binary files (images, PDFs, archives)")
|
|
164
131
|
},
|
|
165
132
|
async ({ path: filePath, encoding }) => {
|
|
166
133
|
try {
|
|
167
|
-
const content = await
|
|
134
|
+
const content = await fs.readFile(filePath, encoding);
|
|
168
135
|
return { content: [{ type: "text", text: content }] };
|
|
169
136
|
} catch (err) {
|
|
170
137
|
const e = err;
|
|
@@ -184,13 +151,13 @@ ${error.stderr ?? ""}`
|
|
|
184
151
|
"Prefer edit_block over write_file for existing files \u2014 it's safer and preserves unmodified content."
|
|
185
152
|
].join("\n"),
|
|
186
153
|
{
|
|
187
|
-
path:
|
|
188
|
-
content:
|
|
154
|
+
path: z.string().describe("File path to create or overwrite. Parent directories are auto-created."),
|
|
155
|
+
content: z.string().describe("Complete file content. This replaces the entire file.")
|
|
189
156
|
},
|
|
190
157
|
async ({ path: filePath, content }) => {
|
|
191
158
|
checkPermission("write_file");
|
|
192
|
-
await
|
|
193
|
-
await
|
|
159
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
160
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
194
161
|
return { content: [{ type: "text", text: "File saved" }] };
|
|
195
162
|
}
|
|
196
163
|
);
|
|
@@ -201,11 +168,11 @@ ${error.stderr ?? ""}`
|
|
|
201
168
|
"Use this to explore project structure before reading or modifying files."
|
|
202
169
|
].join("\n"),
|
|
203
170
|
{
|
|
204
|
-
path:
|
|
171
|
+
path: z.string().describe("Directory path to list")
|
|
205
172
|
},
|
|
206
173
|
async ({ path: dirPath }) => {
|
|
207
174
|
try {
|
|
208
|
-
const entries = await
|
|
175
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
209
176
|
const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
|
|
210
177
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
211
178
|
} catch (err) {
|
|
@@ -226,9 +193,9 @@ ${error.stderr ?? ""}`
|
|
|
226
193
|
"Returns matching lines with file paths and line numbers for precise navigation."
|
|
227
194
|
].join("\n"),
|
|
228
195
|
{
|
|
229
|
-
pattern:
|
|
230
|
-
directory:
|
|
231
|
-
file_pattern:
|
|
196
|
+
pattern: z.string().describe("Search pattern with full regex support (e.g. 'function\\s+\\w+', 'import.*from', 'TODO')"),
|
|
197
|
+
directory: z.string().optional().default(".").describe("Root directory to search from (default: current working directory)"),
|
|
198
|
+
file_pattern: z.string().optional().default("**/*").describe("Glob pattern to filter files (e.g. '**/*.ts', '*.py', 'src/**/*.js')")
|
|
232
199
|
},
|
|
233
200
|
async ({ pattern, directory, file_pattern }) => {
|
|
234
201
|
try {
|
|
@@ -239,13 +206,13 @@ ${error.stderr ?? ""}`
|
|
|
239
206
|
);
|
|
240
207
|
return { content: [{ type: "text", text: stdout || "No results" }] };
|
|
241
208
|
} catch {
|
|
242
|
-
const safeDirectory =
|
|
243
|
-
const files = await
|
|
209
|
+
const safeDirectory = path.resolve(directory);
|
|
210
|
+
const files = await glob(file_pattern, { cwd: safeDirectory });
|
|
244
211
|
const results = [];
|
|
245
212
|
for (const file of files.slice(0, 100)) {
|
|
246
213
|
try {
|
|
247
|
-
const content = await
|
|
248
|
-
|
|
214
|
+
const content = await fs.readFile(
|
|
215
|
+
path.join(safeDirectory, file),
|
|
249
216
|
"utf-8"
|
|
250
217
|
);
|
|
251
218
|
const lines = content.split("\n");
|
|
@@ -282,8 +249,8 @@ ${error.stderr ?? ""}`
|
|
|
282
249
|
"SAFETY: Only kill processes the user explicitly identifies. Never kill system-critical processes (init, systemd, loginwindow, WindowServer) without explicit instruction."
|
|
283
250
|
].join("\n"),
|
|
284
251
|
{
|
|
285
|
-
pid:
|
|
286
|
-
signal:
|
|
252
|
+
pid: z.number().describe("PID of the process to terminate (use list_processes to find PIDs)"),
|
|
253
|
+
signal: z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("SIGTERM (default): graceful shutdown with 3s auto-SIGKILL fallback. SIGKILL: immediate force kill.")
|
|
287
254
|
},
|
|
288
255
|
async ({ pid, signal }) => {
|
|
289
256
|
const isWindows = process.platform === "win32";
|
|
@@ -339,13 +306,13 @@ ${error.stderr ?? ""}`
|
|
|
339
306
|
"Prefer this over write_file for modifying existing files \u2014 it only changes what you specify and preserves the rest."
|
|
340
307
|
].join("\n"),
|
|
341
308
|
{
|
|
342
|
-
path:
|
|
343
|
-
old_string:
|
|
344
|
-
new_string:
|
|
345
|
-
replace_all:
|
|
309
|
+
path: z.string().describe("Path to the file to edit. The file must already exist."),
|
|
310
|
+
old_string: z.string().describe("The exact text to find and replace. Must match character-for-character including whitespace and newlines. Include enough context for uniqueness."),
|
|
311
|
+
new_string: z.string().describe("The replacement text. Use empty string to delete the matched text."),
|
|
312
|
+
replace_all: z.boolean().optional().default(false).describe("If true, replace ALL matches. If false (default), require exactly one match (errors on ambiguous multiple matches).")
|
|
346
313
|
},
|
|
347
314
|
async ({ path: filePath, old_string, new_string, replace_all }) => {
|
|
348
|
-
const content = await
|
|
315
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
349
316
|
if (!content.includes(old_string)) {
|
|
350
317
|
throw new Error(`old_string not found in file: ${filePath}`);
|
|
351
318
|
}
|
|
@@ -369,7 +336,7 @@ ${error.stderr ?? ""}`
|
|
|
369
336
|
result = content.replace(old_string, new_string);
|
|
370
337
|
replaced = 1;
|
|
371
338
|
}
|
|
372
|
-
await
|
|
339
|
+
await fs.writeFile(filePath, result, "utf-8");
|
|
373
340
|
return {
|
|
374
341
|
content: [{ type: "text", text: `Replaced (${replaced} occurrence(s) changed)` }]
|
|
375
342
|
};
|
|
@@ -384,9 +351,9 @@ ${error.stderr ?? ""}`
|
|
|
384
351
|
"Duplicate commands are automatically detected and rejected. Use cron_list to see existing jobs."
|
|
385
352
|
].join("\n"),
|
|
386
353
|
{
|
|
387
|
-
schedule:
|
|
388
|
-
command:
|
|
389
|
-
label:
|
|
354
|
+
schedule: z.string().describe("Cron schedule expression (5 fields: minute hour day month weekday). Examples: '*/5 * * * *' (every 5 min), '0 9 * * 1-5' (weekdays 9am)"),
|
|
355
|
+
command: z.string().describe("Shell command to execute on schedule"),
|
|
356
|
+
label: z.string().optional().describe("Human-readable label for identification (e.g. 'daily-backup', 'log-cleanup')")
|
|
390
357
|
},
|
|
391
358
|
async ({ schedule, command, label }) => {
|
|
392
359
|
try {
|
|
@@ -408,9 +375,9 @@ ${error.stderr ?? ""}`
|
|
|
408
375
|
`;
|
|
409
376
|
const updated = existing.trimEnd() + "\n" + newEntry;
|
|
410
377
|
const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
411
|
-
await
|
|
378
|
+
await fs.writeFile(tmpFile, updated, "utf-8");
|
|
412
379
|
await execAsync(`crontab ${tmpFile}`);
|
|
413
|
-
await
|
|
380
|
+
await fs.unlink(tmpFile).catch(() => {
|
|
414
381
|
});
|
|
415
382
|
return {
|
|
416
383
|
content: [{ type: "text", text: `\u2705 Cron job created:
|
|
@@ -477,8 +444,8 @@ ${error.stderr ?? ""}`
|
|
|
477
444
|
"cron_delete",
|
|
478
445
|
"Delete a scheduled cron job by its ID (from cron_list output) or by matching command string. Associated comment labels are automatically cleaned up.",
|
|
479
446
|
{
|
|
480
|
-
id:
|
|
481
|
-
command:
|
|
447
|
+
id: z.number().optional().describe("Cron job ID from cron_list output (e.g. 1, 2, 3)"),
|
|
448
|
+
command: z.string().optional().describe("Delete all jobs matching this command string")
|
|
482
449
|
},
|
|
483
450
|
async ({ id, command }) => {
|
|
484
451
|
if (!id && !command) {
|
|
@@ -515,9 +482,9 @@ ${error.stderr ?? ""}`
|
|
|
515
482
|
}
|
|
516
483
|
const updated2 = filtered2.join("\n");
|
|
517
484
|
const tmpFile2 = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
518
|
-
await
|
|
485
|
+
await fs.writeFile(tmpFile2, updated2, "utf-8");
|
|
519
486
|
await execAsync(`crontab ${tmpFile2}`);
|
|
520
|
-
await
|
|
487
|
+
await fs.unlink(tmpFile2).catch(() => {
|
|
521
488
|
});
|
|
522
489
|
return { content: [{ type: "text", text: `\u2705 Deleted cron job matching: ${command}` }] };
|
|
523
490
|
}
|
|
@@ -542,9 +509,9 @@ ${error.stderr ?? ""}`
|
|
|
542
509
|
const filtered = lines.filter((_, i) => i < target.lineStart || i > target.lineEnd);
|
|
543
510
|
const updated = filtered.join("\n");
|
|
544
511
|
const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
545
|
-
await
|
|
512
|
+
await fs.writeFile(tmpFile, updated, "utf-8");
|
|
546
513
|
await execAsync(`crontab ${tmpFile}`);
|
|
547
|
-
await
|
|
514
|
+
await fs.unlink(tmpFile).catch(() => {
|
|
548
515
|
});
|
|
549
516
|
return { content: [{ type: "text", text: `\u2705 Deleted cron job #${id}` }] };
|
|
550
517
|
} catch (err) {
|
|
@@ -559,9 +526,9 @@ ${error.stderr ?? ""}`
|
|
|
559
526
|
};
|
|
560
527
|
|
|
561
528
|
// src/tools/browser.ts
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
529
|
+
import { BrowserClaw } from "browserclaw";
|
|
530
|
+
import fs2 from "fs/promises";
|
|
531
|
+
import { z as z2 } from "zod";
|
|
565
532
|
var BrowserTools = class {
|
|
566
533
|
browser = null;
|
|
567
534
|
page = null;
|
|
@@ -602,11 +569,11 @@ var BrowserTools = class {
|
|
|
602
569
|
"Always call browser_stop when done to release system resources."
|
|
603
570
|
].join("\n"),
|
|
604
571
|
{
|
|
605
|
-
mode:
|
|
606
|
-
headless:
|
|
607
|
-
cdpUrl:
|
|
608
|
-
profile:
|
|
609
|
-
allowInternal:
|
|
572
|
+
mode: z2.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome via CDP"),
|
|
573
|
+
headless: z2.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
|
|
574
|
+
cdpUrl: z2.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
|
|
575
|
+
profile: z2.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
|
|
576
|
+
allowInternal: z2.boolean().optional().default(false).describe("Allow navigation to localhost and internal network URLs")
|
|
610
577
|
},
|
|
611
578
|
({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
|
|
612
579
|
if (this.browser) {
|
|
@@ -614,9 +581,9 @@ var BrowserTools = class {
|
|
|
614
581
|
}
|
|
615
582
|
if (mode === "remote-cdp") {
|
|
616
583
|
if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
|
|
617
|
-
this.browser = await
|
|
584
|
+
this.browser = await BrowserClaw.connect(cdpUrl, { allowInternal });
|
|
618
585
|
} else {
|
|
619
|
-
this.browser = await
|
|
586
|
+
this.browser = await BrowserClaw.launch({
|
|
620
587
|
headless,
|
|
621
588
|
profileName: profile,
|
|
622
589
|
allowInternal
|
|
@@ -638,7 +605,7 @@ var BrowserTools = class {
|
|
|
638
605
|
"browser_navigate",
|
|
639
606
|
"Navigate the browser to a URL. Automatically opens a new tab if the browser is started but no page exists yet. Waits for the page to load before returning.",
|
|
640
607
|
{
|
|
641
|
-
url:
|
|
608
|
+
url: z2.string().describe("Full URL to navigate to (include https://)")
|
|
642
609
|
},
|
|
643
610
|
({ url }) => this.withLock(async () => {
|
|
644
611
|
if (!this.browser) throw new Error("Browser not started. Call browser_start first.");
|
|
@@ -662,8 +629,8 @@ var BrowserTools = class {
|
|
|
662
629
|
"Prefer this over browser_screenshot for understanding page structure \u2014 it's faster, structured, and machine-readable."
|
|
663
630
|
].join("\n"),
|
|
664
631
|
{
|
|
665
|
-
interactive:
|
|
666
|
-
compact:
|
|
632
|
+
interactive: z2.boolean().optional().default(true).describe("true (default): only show clickable/typeable elements. false: show all elements including static text."),
|
|
633
|
+
compact: z2.boolean().optional().default(true).describe("true (default): hide empty containers for cleaner output")
|
|
667
634
|
},
|
|
668
635
|
({ interactive, compact }) => this.withLock(async () => {
|
|
669
636
|
const result = await requirePage().snapshot({ interactive, compact });
|
|
@@ -685,9 +652,9 @@ ${refList}`
|
|
|
685
652
|
"browser_click",
|
|
686
653
|
"Click an element by its ref number from browser_snapshot. Always call browser_snapshot first to get current refs \u2014 they change after page updates.",
|
|
687
654
|
{
|
|
688
|
-
ref:
|
|
689
|
-
doubleClick:
|
|
690
|
-
button:
|
|
655
|
+
ref: z2.string().describe("Element ref from browser_snapshot (e.g. 'e1', 'e15'). Call browser_snapshot first to get current refs."),
|
|
656
|
+
doubleClick: z2.boolean().optional().default(false).describe("Double-click instead of single click"),
|
|
657
|
+
button: z2.enum(["left", "right", "middle"]).optional().default("left").describe("Mouse button to use")
|
|
691
658
|
},
|
|
692
659
|
({ ref, doubleClick, button }) => this.withLock(async () => {
|
|
693
660
|
await requirePage().click(ref, { doubleClick, button });
|
|
@@ -698,10 +665,10 @@ ${refList}`
|
|
|
698
665
|
"browser_type",
|
|
699
666
|
"Type text into an input element by ref number. Use 'submit=true' to press Enter after typing (e.g. for search forms). Use 'slowly=true' for sites requiring keystroke-by-keystroke input.",
|
|
700
667
|
{
|
|
701
|
-
ref:
|
|
702
|
-
text:
|
|
703
|
-
submit:
|
|
704
|
-
slowly:
|
|
668
|
+
ref: z2.string().describe("Element ref from browser_snapshot (e.g. 'e3')"),
|
|
669
|
+
text: z2.string().describe("Text to type into the element"),
|
|
670
|
+
submit: z2.boolean().optional().default(false).describe("Press Enter after typing (useful for search boxes and forms)"),
|
|
671
|
+
slowly: z2.boolean().optional().default(false).describe("Type slowly (75ms per char) for sites that process each keystroke")
|
|
705
672
|
},
|
|
706
673
|
({ ref, text, submit, slowly }) => this.withLock(async () => {
|
|
707
674
|
await requirePage().type(ref, text, { submit, slowly });
|
|
@@ -712,10 +679,10 @@ ${refList}`
|
|
|
712
679
|
"browser_fill",
|
|
713
680
|
"Fill multiple form fields at once \u2014 more efficient than calling browser_type repeatedly. Each field needs a ref from browser_snapshot.",
|
|
714
681
|
{
|
|
715
|
-
fields:
|
|
716
|
-
ref:
|
|
717
|
-
type:
|
|
718
|
-
value:
|
|
682
|
+
fields: z2.array(z2.object({
|
|
683
|
+
ref: z2.string(),
|
|
684
|
+
type: z2.enum(["text", "checkbox", "radio"]),
|
|
685
|
+
value: z2.union([z2.string(), z2.boolean()])
|
|
719
686
|
})).describe("Array of {ref, type, value}. type='text': value is string. type='checkbox'/'radio': value is boolean.")
|
|
720
687
|
},
|
|
721
688
|
({ fields }) => this.withLock(async () => {
|
|
@@ -727,8 +694,8 @@ ${refList}`
|
|
|
727
694
|
"browser_select",
|
|
728
695
|
"Select one or more options from a dropdown/select element. Values should match the option value attributes, not display text.",
|
|
729
696
|
{
|
|
730
|
-
ref:
|
|
731
|
-
values:
|
|
697
|
+
ref: z2.string().describe("Ref of the <select> element from browser_snapshot"),
|
|
698
|
+
values: z2.array(z2.string()).describe("Option value(s) to select")
|
|
732
699
|
},
|
|
733
700
|
({ ref, values }) => this.withLock(async () => {
|
|
734
701
|
await requirePage().select(ref, ...values);
|
|
@@ -739,7 +706,7 @@ ${refList}`
|
|
|
739
706
|
"browser_press",
|
|
740
707
|
"Press a keyboard key or key combination. Use for shortcuts (e.g. 'Control+a', 'Escape'), form submission ('Enter'), or navigation ('Tab'). Does not require a specific element ref.",
|
|
741
708
|
{
|
|
742
|
-
key:
|
|
709
|
+
key: z2.string().describe("Key or combination: 'Enter', 'Escape', 'Tab', 'Control+a', 'Meta+c', 'ArrowDown', 'Backspace'")
|
|
743
710
|
},
|
|
744
711
|
({ key }) => this.withLock(async () => {
|
|
745
712
|
await requirePage().press(key);
|
|
@@ -750,7 +717,7 @@ ${refList}`
|
|
|
750
717
|
"browser_hover",
|
|
751
718
|
"Move the mouse cursor over an element by ref. Use to trigger hover menus, tooltips, or dropdown previews before clicking.",
|
|
752
719
|
{
|
|
753
|
-
ref:
|
|
720
|
+
ref: z2.string().describe("Element ref from browser_snapshot")
|
|
754
721
|
},
|
|
755
722
|
({ ref }) => this.withLock(async () => {
|
|
756
723
|
await requirePage().hover(ref);
|
|
@@ -761,8 +728,8 @@ ${refList}`
|
|
|
761
728
|
"browser_drag",
|
|
762
729
|
"Drag an element from startRef to endRef. Both refs must come from a recent browser_snapshot. Use for drag-and-drop interfaces, sliders, or reorderable lists.",
|
|
763
730
|
{
|
|
764
|
-
startRef:
|
|
765
|
-
endRef:
|
|
731
|
+
startRef: z2.string().describe("Source element ref to drag from"),
|
|
732
|
+
endRef: z2.string().describe("Target element ref to drag to")
|
|
766
733
|
},
|
|
767
734
|
({ startRef, endRef }) => this.withLock(async () => {
|
|
768
735
|
await requirePage().drag(startRef, endRef);
|
|
@@ -773,8 +740,8 @@ ${refList}`
|
|
|
773
740
|
"browser_upload",
|
|
774
741
|
"Upload local files to a file input element (<input type='file'>). The ref must point to a file input from browser_snapshot.",
|
|
775
742
|
{
|
|
776
|
-
ref:
|
|
777
|
-
paths:
|
|
743
|
+
ref: z2.string().describe("Ref of the file input element from browser_snapshot"),
|
|
744
|
+
paths: z2.array(z2.string()).describe("Absolute file path(s) on the local device to upload")
|
|
778
745
|
},
|
|
779
746
|
({ ref, paths }) => this.withLock(async () => {
|
|
780
747
|
await requirePage().uploadFile(ref, paths);
|
|
@@ -790,14 +757,14 @@ ${refList}`
|
|
|
790
757
|
"Use browser_screenshot only when visual layout matters (charts, images, styling, visual verification)."
|
|
791
758
|
].join("\n"),
|
|
792
759
|
{
|
|
793
|
-
path:
|
|
794
|
-
fullPage:
|
|
795
|
-
ref:
|
|
760
|
+
path: z2.string().optional().describe("Save path for the screenshot. If omitted, returns base64 image data directly."),
|
|
761
|
+
fullPage: z2.boolean().optional().default(false).describe("Capture the full scrollable page, not just the visible viewport"),
|
|
762
|
+
ref: z2.string().optional().describe("Capture only a specific element by its ref from browser_snapshot")
|
|
796
763
|
},
|
|
797
764
|
({ path: path2, fullPage, ref }) => this.withLock(async () => {
|
|
798
765
|
const buffer = await requirePage().screenshot({ fullPage, ref });
|
|
799
766
|
if (path2) {
|
|
800
|
-
await
|
|
767
|
+
await fs2.writeFile(path2, buffer);
|
|
801
768
|
return { content: [{ type: "text", text: `Screenshot saved: ${path2}` }] };
|
|
802
769
|
}
|
|
803
770
|
return {
|
|
@@ -813,11 +780,11 @@ ${refList}`
|
|
|
813
780
|
"browser_pdf",
|
|
814
781
|
"Save the current page as a PDF file. Renders the full page including below-the-fold content. Useful for archiving, sharing, or offline reading.",
|
|
815
782
|
{
|
|
816
|
-
path:
|
|
783
|
+
path: z2.string().describe("Output file path (.pdf)")
|
|
817
784
|
},
|
|
818
785
|
({ path: path2 }) => this.withLock(async () => {
|
|
819
786
|
const buffer = await requirePage().pdf();
|
|
820
|
-
await
|
|
787
|
+
await fs2.writeFile(path2, buffer);
|
|
821
788
|
return { content: [{ type: "text", text: `PDF saved: ${path2}` }] };
|
|
822
789
|
})
|
|
823
790
|
);
|
|
@@ -830,7 +797,7 @@ ${refList}`
|
|
|
830
797
|
"Wrap complex logic in an IIFE: (function(){ ... })()"
|
|
831
798
|
].join("\n"),
|
|
832
799
|
{
|
|
833
|
-
code:
|
|
800
|
+
code: z2.string().describe("JavaScript code to execute in the page context. Return values are automatically serialized.")
|
|
834
801
|
},
|
|
835
802
|
({ code }) => this.withLock(async () => {
|
|
836
803
|
try {
|
|
@@ -857,11 +824,11 @@ ${refList}`
|
|
|
857
824
|
"OPTIONS (use one): 'text' (wait for text to appear), 'textGone' (wait for text to disappear), 'url' (URL matches glob), 'loadState' (page load state), 'timeMs' (fixed delay as last resort)."
|
|
858
825
|
].join("\n"),
|
|
859
826
|
{
|
|
860
|
-
text:
|
|
861
|
-
textGone:
|
|
862
|
-
url:
|
|
863
|
-
loadState:
|
|
864
|
-
timeMs:
|
|
827
|
+
text: z2.string().optional().describe("Wait until this text appears on the page"),
|
|
828
|
+
textGone: z2.string().optional().describe("Wait until this text disappears from the page"),
|
|
829
|
+
url: z2.string().optional().describe("Wait until URL matches this glob pattern (e.g. '**/dashboard', '**/success')"),
|
|
830
|
+
loadState: z2.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for page load state: 'load' (full), 'domcontentloaded' (DOM ready), 'networkidle' (no pending requests)"),
|
|
831
|
+
timeMs: z2.number().optional().describe("Fixed wait in milliseconds \u2014 use as last resort when other conditions don't apply")
|
|
865
832
|
},
|
|
866
833
|
({ text, textGone, url, loadState, timeMs }) => this.withLock(async () => {
|
|
867
834
|
const condition = {};
|
|
@@ -878,14 +845,14 @@ ${refList}`
|
|
|
878
845
|
"browser_cookies",
|
|
879
846
|
"Manage browser cookies: get all cookies, set a specific cookie, or clear all cookies. Useful for authentication state, session management, or testing.",
|
|
880
847
|
{
|
|
881
|
-
action:
|
|
882
|
-
cookie:
|
|
883
|
-
name:
|
|
884
|
-
value:
|
|
885
|
-
domain:
|
|
886
|
-
path:
|
|
887
|
-
httpOnly:
|
|
888
|
-
secure:
|
|
848
|
+
action: z2.enum(["get", "set", "clear"]).describe("'get': retrieve all cookies, 'set': add/update a cookie, 'clear': remove all cookies"),
|
|
849
|
+
cookie: z2.object({
|
|
850
|
+
name: z2.string(),
|
|
851
|
+
value: z2.string(),
|
|
852
|
+
domain: z2.string().optional(),
|
|
853
|
+
path: z2.string().optional(),
|
|
854
|
+
httpOnly: z2.boolean().optional(),
|
|
855
|
+
secure: z2.boolean().optional()
|
|
889
856
|
}).optional().describe("Cookie data (required for 'set' action)")
|
|
890
857
|
},
|
|
891
858
|
({ action, cookie }) => this.withLock(async () => {
|
|
@@ -907,10 +874,10 @@ ${refList}`
|
|
|
907
874
|
"browser_storage",
|
|
908
875
|
"Read, write, or clear browser localStorage/sessionStorage. Useful for managing client-side state, authentication tokens, or application preferences.",
|
|
909
876
|
{
|
|
910
|
-
action:
|
|
911
|
-
kind:
|
|
912
|
-
key:
|
|
913
|
-
value:
|
|
877
|
+
action: z2.enum(["get", "set", "clear"]).describe("'get': read value(s), 'set': write a key-value pair, 'clear': remove all entries"),
|
|
878
|
+
kind: z2.enum(["local", "session"]).optional().default("local").describe("'local' (persistent) or 'session' (cleared on tab close)"),
|
|
879
|
+
key: z2.string().optional().describe("Storage key to get or set. Omit key with 'get' to retrieve all entries."),
|
|
880
|
+
value: z2.string().optional().describe("Value to store (required for 'set' action)")
|
|
914
881
|
},
|
|
915
882
|
({ action, kind, key, value }) => this.withLock(async () => {
|
|
916
883
|
const page = requirePage();
|
|
@@ -938,16 +905,16 @@ ${refList}`
|
|
|
938
905
|
"The 'accept' and 'promptText' params are only used with action='arm'."
|
|
939
906
|
].join("\n"),
|
|
940
907
|
{
|
|
941
|
-
action:
|
|
908
|
+
action: z2.enum(["arm", "wait"]).describe(
|
|
942
909
|
"'arm' = register handler and return immediately; 'wait' = await the previously armed handler"
|
|
943
910
|
),
|
|
944
|
-
accept:
|
|
911
|
+
accept: z2.boolean().optional().default(true).describe(
|
|
945
912
|
"Accept (true) or dismiss (false) the dialog. Only used with action='arm'."
|
|
946
913
|
),
|
|
947
|
-
promptText:
|
|
914
|
+
promptText: z2.string().optional().describe(
|
|
948
915
|
"Text to enter if the dialog is a prompt. Only used with action='arm'."
|
|
949
916
|
),
|
|
950
|
-
timeoutMs:
|
|
917
|
+
timeoutMs: z2.number().optional().describe(
|
|
951
918
|
"Timeout in ms for 'wait' action (default: 30000). Increase for slow-loading dialogs."
|
|
952
919
|
)
|
|
953
920
|
},
|
|
@@ -979,13 +946,13 @@ ${refList}`
|
|
|
979
946
|
};
|
|
980
947
|
|
|
981
948
|
// src/tools/notebook.ts
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
var execAsync2 = (
|
|
949
|
+
import { z as z3 } from "zod";
|
|
950
|
+
import fs3 from "fs/promises";
|
|
951
|
+
import { exec as exec2 } from "child_process";
|
|
952
|
+
import { promisify as promisify2 } from "util";
|
|
953
|
+
var execAsync2 = promisify2(exec2);
|
|
987
954
|
async function readNotebook(filePath) {
|
|
988
|
-
const raw = await
|
|
955
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
989
956
|
try {
|
|
990
957
|
return JSON.parse(raw);
|
|
991
958
|
} catch {
|
|
@@ -993,14 +960,14 @@ async function readNotebook(filePath) {
|
|
|
993
960
|
}
|
|
994
961
|
}
|
|
995
962
|
async function writeNotebook(filePath, nb) {
|
|
996
|
-
await
|
|
963
|
+
await fs3.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
|
|
997
964
|
}
|
|
998
965
|
var NotebookTools = class {
|
|
999
966
|
register(server) {
|
|
1000
967
|
server.tool(
|
|
1001
968
|
"notebook_read",
|
|
1002
969
|
"Read a Jupyter notebook (.ipynb) and return all cells with their types (code/markdown), source content, and output counts. Use this to understand notebook structure before making edits.",
|
|
1003
|
-
{ path:
|
|
970
|
+
{ path: z3.string().describe("Path to the .ipynb notebook file") },
|
|
1004
971
|
async ({ path: filePath }) => {
|
|
1005
972
|
const nb = await readNotebook(filePath);
|
|
1006
973
|
const cells = nb.cells.map((cell, i) => ({
|
|
@@ -1018,9 +985,9 @@ var NotebookTools = class {
|
|
|
1018
985
|
"notebook_edit_cell",
|
|
1019
986
|
"Replace the source code of a specific cell in a Jupyter notebook. Use notebook_read first to identify the correct cell index (0-based). Existing outputs for the cell are preserved \u2014 use notebook_execute to re-run.",
|
|
1020
987
|
{
|
|
1021
|
-
path:
|
|
1022
|
-
cell_index:
|
|
1023
|
-
source:
|
|
988
|
+
path: z3.string().describe("Path to the .ipynb notebook file"),
|
|
989
|
+
cell_index: z3.number().describe("Cell index to edit (0-based). Use notebook_read to find the right index."),
|
|
990
|
+
source: z3.string().describe("New source code/content for the cell (replaces entire cell content)")
|
|
1024
991
|
},
|
|
1025
992
|
async ({ path: filePath, cell_index, source }) => {
|
|
1026
993
|
const nb = await readNotebook(filePath);
|
|
@@ -1043,8 +1010,8 @@ var NotebookTools = class {
|
|
|
1043
1010
|
"If execution fails on a cell, the error is captured in the cell output and subsequent cells may not execute."
|
|
1044
1011
|
].join("\n"),
|
|
1045
1012
|
{
|
|
1046
|
-
path:
|
|
1047
|
-
timeout:
|
|
1013
|
+
path: z3.string().describe("Path to the .ipynb notebook file to execute"),
|
|
1014
|
+
timeout: z3.number().optional().default(300).describe("Maximum execution time per cell in seconds (default: 300). Increase for cells with heavy computation.")
|
|
1048
1015
|
},
|
|
1049
1016
|
async ({ path: filePath, timeout }) => {
|
|
1050
1017
|
const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
|
|
@@ -1075,10 +1042,10 @@ var NotebookTools = class {
|
|
|
1075
1042
|
"notebook_add_cell",
|
|
1076
1043
|
"Insert a new cell into a Jupyter notebook. If position is omitted, the cell is appended at the end. Use cell_type='code' for executable Python cells, 'markdown' for documentation/text cells.",
|
|
1077
1044
|
{
|
|
1078
|
-
path:
|
|
1079
|
-
cell_type:
|
|
1080
|
-
source:
|
|
1081
|
-
position:
|
|
1045
|
+
path: z3.string().describe("Path to the .ipynb notebook file"),
|
|
1046
|
+
cell_type: z3.enum(["code", "markdown"]).describe("'code' for executable cells, 'markdown' for text/documentation cells"),
|
|
1047
|
+
source: z3.string().describe("Cell source content (Python code or Markdown text)"),
|
|
1048
|
+
position: z3.number().optional().describe("Insert position (0-based index). Omit to append at the end. If position exceeds cell count, appends at end with a warning.")
|
|
1082
1049
|
},
|
|
1083
1050
|
async ({ path: filePath, cell_type: cellType, source, position }) => {
|
|
1084
1051
|
const nb = await readNotebook(filePath);
|
|
@@ -1111,8 +1078,8 @@ var NotebookTools = class {
|
|
|
1111
1078
|
"notebook_delete_cell",
|
|
1112
1079
|
"Delete a cell from a Jupyter notebook by its 0-based index. Use notebook_read first to verify the cell content before deletion. This action cannot be undone.",
|
|
1113
1080
|
{
|
|
1114
|
-
path:
|
|
1115
|
-
cell_index:
|
|
1081
|
+
path: z3.string().describe("Path to the .ipynb notebook file"),
|
|
1082
|
+
cell_index: z3.number().describe("Cell index to delete (0-based). Use notebook_read first to verify content.")
|
|
1116
1083
|
},
|
|
1117
1084
|
async ({ path: filePath, cell_index }) => {
|
|
1118
1085
|
const nb = await readNotebook(filePath);
|
|
@@ -1128,11 +1095,11 @@ var NotebookTools = class {
|
|
|
1128
1095
|
};
|
|
1129
1096
|
|
|
1130
1097
|
// src/tools/device.ts
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
var execAsync3 = (
|
|
1098
|
+
import { exec as exec3 } from "child_process";
|
|
1099
|
+
import { promisify as promisify3 } from "util";
|
|
1100
|
+
import { z as z4 } from "zod";
|
|
1101
|
+
import notifier from "node-notifier";
|
|
1102
|
+
var execAsync3 = promisify3(exec3);
|
|
1136
1103
|
var screenRecordPid = null;
|
|
1137
1104
|
function platform() {
|
|
1138
1105
|
if (process.platform === "darwin") return "mac";
|
|
@@ -1150,7 +1117,7 @@ var DeviceTools = class {
|
|
|
1150
1117
|
"Requires a connected camera with OS permissions granted. If output_path is provided, the file is also saved to disk."
|
|
1151
1118
|
].join("\n"),
|
|
1152
1119
|
{
|
|
1153
|
-
output_path:
|
|
1120
|
+
output_path: z4.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
|
|
1154
1121
|
},
|
|
1155
1122
|
async ({ output_path }) => {
|
|
1156
1123
|
const p = platform();
|
|
@@ -1188,13 +1155,13 @@ Please check if a camera is connected.` }],
|
|
|
1188
1155
|
"notification_send",
|
|
1189
1156
|
"Send a native OS notification (banner/toast) to the user's desktop. Use for task completion alerts, reminders, or important status updates. The notification appears even when the terminal is not focused.",
|
|
1190
1157
|
{
|
|
1191
|
-
title:
|
|
1192
|
-
message:
|
|
1158
|
+
title: z4.string().describe("Notification title (displayed prominently)"),
|
|
1159
|
+
message: z4.string().describe("Notification body text")
|
|
1193
1160
|
},
|
|
1194
1161
|
async ({ title, message }) => {
|
|
1195
1162
|
try {
|
|
1196
1163
|
await new Promise((resolve, reject) => {
|
|
1197
|
-
|
|
1164
|
+
notifier.notify(
|
|
1198
1165
|
{ title, message },
|
|
1199
1166
|
(err) => {
|
|
1200
1167
|
if (err) reject(err);
|
|
@@ -1226,7 +1193,7 @@ Please check if a camera is connected.` }],
|
|
|
1226
1193
|
"clipboard_write",
|
|
1227
1194
|
"Write text to the system clipboard, replacing its current contents. Use to prepare content for the user to paste elsewhere.",
|
|
1228
1195
|
{
|
|
1229
|
-
text:
|
|
1196
|
+
text: z4.string().describe("Text to copy to the clipboard")
|
|
1230
1197
|
},
|
|
1231
1198
|
async ({ text }) => {
|
|
1232
1199
|
const p = platform();
|
|
@@ -1248,8 +1215,8 @@ Please check if a camera is connected.` }],
|
|
|
1248
1215
|
"Platform-specific: macOS (screencapture -v), Windows/Linux (ffmpeg)."
|
|
1249
1216
|
].join("\n"),
|
|
1250
1217
|
{
|
|
1251
|
-
action:
|
|
1252
|
-
output_path:
|
|
1218
|
+
action: z4.enum(["start", "stop"]).describe("'start': begin recording, 'stop': end recording and save the file"),
|
|
1219
|
+
output_path: z4.string().optional().describe("Output file path (used with 'start'). Default: /tmp/junis_record_<timestamp>.mp4")
|
|
1253
1220
|
},
|
|
1254
1221
|
async ({ action, output_path }) => {
|
|
1255
1222
|
const p = platform();
|
|
@@ -1314,7 +1281,7 @@ Please check if a camera is connected.` }],
|
|
|
1314
1281
|
"audio_play",
|
|
1315
1282
|
"Play an audio file through the device's speakers. Supports MP3, WAV, AAC, and other common formats. Playback is synchronous \u2014 the tool returns after playback completes. Platform-specific: macOS (afplay), Windows/Linux (ffplay).",
|
|
1316
1283
|
{
|
|
1317
|
-
file_path:
|
|
1284
|
+
file_path: z4.string().describe("Absolute path to the audio file to play")
|
|
1318
1285
|
},
|
|
1319
1286
|
async ({ file_path }) => {
|
|
1320
1287
|
const p = platform();
|
|
@@ -1331,8 +1298,9 @@ Please check if a camera is connected.` }],
|
|
|
1331
1298
|
};
|
|
1332
1299
|
|
|
1333
1300
|
// src/server/stdio.ts
|
|
1301
|
+
import { fileURLToPath } from "url";
|
|
1334
1302
|
async function startStdioServer() {
|
|
1335
|
-
const server = new
|
|
1303
|
+
const server = new McpServer({ name: "junis", version: "0.1.0" });
|
|
1336
1304
|
const fsTools = new FilesystemTools();
|
|
1337
1305
|
fsTools.register(server);
|
|
1338
1306
|
const browserTools = new BrowserTools();
|
|
@@ -1342,17 +1310,16 @@ async function startStdioServer() {
|
|
|
1342
1310
|
notebookTools.register(server);
|
|
1343
1311
|
const deviceTools = new DeviceTools();
|
|
1344
1312
|
deviceTools.register(server);
|
|
1345
|
-
const transport = new
|
|
1313
|
+
const transport = new StdioServerTransport();
|
|
1346
1314
|
await server.connect(transport);
|
|
1347
1315
|
process.on("SIGINT", async () => {
|
|
1348
1316
|
await browserTools.cleanup();
|
|
1349
1317
|
process.exit(0);
|
|
1350
1318
|
});
|
|
1351
1319
|
}
|
|
1352
|
-
if (
|
|
1320
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
1353
1321
|
startStdioServer().catch(console.error);
|
|
1354
1322
|
}
|
|
1355
|
-
|
|
1356
|
-
0 && (module.exports = {
|
|
1323
|
+
export {
|
|
1357
1324
|
startStdioServer
|
|
1358
|
-
}
|
|
1325
|
+
};
|