junis 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +987 -0
- package/dist/server/mcp.js +793 -0
- package/dist/server/stdio.js +549 -0
- package/package.json +36 -0
|
@@ -0,0 +1,987 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/cli/config.ts
|
|
30
|
+
var import_fs = __toESM(require("fs"));
|
|
31
|
+
var import_path = __toESM(require("path"));
|
|
32
|
+
var import_os = __toESM(require("os"));
|
|
33
|
+
var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".junis");
|
|
34
|
+
var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
|
|
35
|
+
function loadConfig() {
|
|
36
|
+
try {
|
|
37
|
+
if (!import_fs.default.existsSync(CONFIG_FILE)) return null;
|
|
38
|
+
const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function saveConfig(config) {
|
|
45
|
+
import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
46
|
+
import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
47
|
+
}
|
|
48
|
+
function clearConfig() {
|
|
49
|
+
if (import_fs.default.existsSync(CONFIG_FILE)) {
|
|
50
|
+
import_fs.default.unlinkSync(CONFIG_FILE);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/cli/auth.ts
|
|
55
|
+
var import_open = __toESM(require("open"));
|
|
56
|
+
var JUNIS_API = process.env.JUNIS_API_URL ?? "https://api.junis.ai";
|
|
57
|
+
var JUNIS_WEB = process.env.JUNIS_WEB_URL ?? "https://junis.ai";
|
|
58
|
+
async function authenticate() {
|
|
59
|
+
const startRes = await fetch(`${JUNIS_API}/api/auth/device/start`, {
|
|
60
|
+
method: "POST"
|
|
61
|
+
});
|
|
62
|
+
if (!startRes.ok) {
|
|
63
|
+
throw new Error(`Auth \uC2DC\uC791 \uC2E4\uD328: ${startRes.status}`);
|
|
64
|
+
}
|
|
65
|
+
const startData = await startRes.json();
|
|
66
|
+
const verificationUri = startData.verification_uri.replace(
|
|
67
|
+
/^https?:\/\/[^/]+/,
|
|
68
|
+
JUNIS_WEB
|
|
69
|
+
);
|
|
70
|
+
console.log("\n\u{1F310} \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778 \uD6C4 \uC2B9\uC778 \uBC84\uD2BC\uC744 \uD074\uB9AD\uD558\uC138\uC694.");
|
|
71
|
+
console.log(` URL: ${verificationUri}
|
|
72
|
+
`);
|
|
73
|
+
await (0, import_open.default)(verificationUri);
|
|
74
|
+
const deadline = Date.now() + startData.expires_in * 1e3;
|
|
75
|
+
const intervalMs = startData.interval * 1e3;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
await sleep(intervalMs);
|
|
78
|
+
const pollRes = await fetch(
|
|
79
|
+
`${JUNIS_API}/api/auth/device/poll?code=${startData.device_code}`
|
|
80
|
+
);
|
|
81
|
+
if (pollRes.status === 202) {
|
|
82
|
+
process.stdout.write(".");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (pollRes.status === 200) {
|
|
86
|
+
const result = await pollRes.json();
|
|
87
|
+
console.log("\n\u2705 \uC778\uC99D \uC644\uB8CC!\n");
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
if (pollRes.status === 410) {
|
|
91
|
+
throw new Error("\uC778\uC99D \uCF54\uB4DC\uAC00 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`\uC608\uC0C1\uCE58 \uBABB\uD55C \uC751\uB2F5: ${pollRes.status}`);
|
|
94
|
+
}
|
|
95
|
+
throw new Error("\uC778\uC99D \uC2DC\uAC04 \uCD08\uACFC (5\uBD84). \uB2E4\uC2DC \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
|
|
96
|
+
}
|
|
97
|
+
function sleep(ms) {
|
|
98
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/relay/client.ts
|
|
102
|
+
var import_ws = __toESM(require("ws"));
|
|
103
|
+
var JUNIS_WS = process.env.JUNIS_WS_URL ?? "wss://api.junis.ai";
|
|
104
|
+
var RelayClient = class {
|
|
105
|
+
constructor(config, onMCPRequest) {
|
|
106
|
+
this.config = config;
|
|
107
|
+
this.onMCPRequest = onMCPRequest;
|
|
108
|
+
}
|
|
109
|
+
ws = null;
|
|
110
|
+
reconnectDelay = 1e3;
|
|
111
|
+
heartbeatTimer = null;
|
|
112
|
+
destroyed = false;
|
|
113
|
+
async connect() {
|
|
114
|
+
if (this.destroyed) return;
|
|
115
|
+
const url = `${JUNIS_WS}/api/ws/devices/${this.config.device_key}?token=${this.config.token}`;
|
|
116
|
+
console.log(`\u{1F517} \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0 \uC911...`);
|
|
117
|
+
this.ws = new import_ws.default(url);
|
|
118
|
+
this.ws.on("open", () => {
|
|
119
|
+
console.log("\u2705 \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0\uB428");
|
|
120
|
+
this.reconnectDelay = 1e3;
|
|
121
|
+
this.startHeartbeat();
|
|
122
|
+
});
|
|
123
|
+
this.ws.on("message", async (raw) => {
|
|
124
|
+
try {
|
|
125
|
+
const msg = JSON.parse(raw.toString());
|
|
126
|
+
if (msg.type === "pong") return;
|
|
127
|
+
if (msg.type === "mcp_request") {
|
|
128
|
+
try {
|
|
129
|
+
const result = await this.onMCPRequest(msg.id, msg.payload);
|
|
130
|
+
this.send({ type: "mcp_response", id: msg.id, payload: result });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.send({
|
|
133
|
+
type: "mcp_response",
|
|
134
|
+
id: msg.id,
|
|
135
|
+
payload: { error: String(err) }
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
this.ws.on("close", () => {
|
|
143
|
+
this.stopHeartbeat();
|
|
144
|
+
if (!this.destroyed) {
|
|
145
|
+
console.log(`\u26A0\uFE0F \uC5F0\uACB0 \uB04A\uAE40. ${this.reconnectDelay / 1e3}\uCD08 \uD6C4 \uC7AC\uC811\uC18D...`);
|
|
146
|
+
setTimeout(() => this.connect(), this.reconnectDelay);
|
|
147
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 3e4);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
this.ws.on("error", (err) => {
|
|
151
|
+
console.error(`\uB9B4\uB808\uC774 \uC624\uB958: ${err.message}`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
send(data) {
|
|
155
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
156
|
+
this.ws.send(JSON.stringify(data));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
startHeartbeat() {
|
|
160
|
+
this.heartbeatTimer = setInterval(() => {
|
|
161
|
+
this.send({ type: "heartbeat" });
|
|
162
|
+
}, 3e4);
|
|
163
|
+
}
|
|
164
|
+
stopHeartbeat() {
|
|
165
|
+
if (this.heartbeatTimer) {
|
|
166
|
+
clearInterval(this.heartbeatTimer);
|
|
167
|
+
this.heartbeatTimer = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
destroy() {
|
|
171
|
+
this.destroyed = true;
|
|
172
|
+
this.stopHeartbeat();
|
|
173
|
+
this.ws?.close();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// src/server/mcp.ts
|
|
178
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
179
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
180
|
+
var import_http = require("http");
|
|
181
|
+
|
|
182
|
+
// src/tools/filesystem.ts
|
|
183
|
+
var import_child_process = require("child_process");
|
|
184
|
+
var import_util = require("util");
|
|
185
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
186
|
+
var import_path2 = __toESM(require("path"));
|
|
187
|
+
var import_glob = require("glob");
|
|
188
|
+
var import_zod = require("zod");
|
|
189
|
+
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
190
|
+
var FilesystemTools = class {
|
|
191
|
+
register(server) {
|
|
192
|
+
server.tool(
|
|
193
|
+
"execute_command",
|
|
194
|
+
"\uD130\uBBF8\uB110 \uBA85\uB839 \uC2E4\uD589",
|
|
195
|
+
{
|
|
196
|
+
command: import_zod.z.string().describe("\uC2E4\uD589\uD560 \uC258 \uBA85\uB839"),
|
|
197
|
+
timeout_ms: import_zod.z.number().optional().default(3e4).describe("\uD0C0\uC784\uC544\uC6C3 (ms)"),
|
|
198
|
+
background: import_zod.z.boolean().optional().default(false).describe("\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589")
|
|
199
|
+
},
|
|
200
|
+
async ({ command, timeout_ms, background }) => {
|
|
201
|
+
if (background) {
|
|
202
|
+
(0, import_child_process.exec)(command);
|
|
203
|
+
return { content: [{ type: "text", text: "\uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589 \uC2DC\uC791\uB428" }] };
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
207
|
+
timeout: timeout_ms
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: stdout || stderr || "(\uCD9C\uB825 \uC5C6\uC74C)" }]
|
|
211
|
+
};
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const error = err;
|
|
214
|
+
return {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: `\uC624\uB958 (exit ${error.code ?? "?"}): ${error.message}
|
|
219
|
+
${error.stderr ?? ""}`
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
isError: true
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
server.tool(
|
|
228
|
+
"read_file",
|
|
229
|
+
"\uD30C\uC77C \uC77D\uAE30",
|
|
230
|
+
{
|
|
231
|
+
path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
|
|
232
|
+
encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
|
|
233
|
+
},
|
|
234
|
+
async ({ path: filePath, encoding }) => {
|
|
235
|
+
const content = await import_promises.default.readFile(filePath, encoding);
|
|
236
|
+
return { content: [{ type: "text", text: content }] };
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
server.tool(
|
|
240
|
+
"write_file",
|
|
241
|
+
"\uD30C\uC77C \uC4F0\uAE30/\uC0DD\uC131",
|
|
242
|
+
{
|
|
243
|
+
path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
|
|
244
|
+
content: import_zod.z.string().describe("\uD30C\uC77C \uB0B4\uC6A9")
|
|
245
|
+
},
|
|
246
|
+
async ({ path: filePath, content }) => {
|
|
247
|
+
await import_promises.default.mkdir(import_path2.default.dirname(filePath), { recursive: true });
|
|
248
|
+
await import_promises.default.writeFile(filePath, content, "utf-8");
|
|
249
|
+
return { content: [{ type: "text", text: "\uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC" }] };
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
server.tool(
|
|
253
|
+
"list_directory",
|
|
254
|
+
"\uB514\uB809\uD1A0\uB9AC \uBAA9\uB85D \uC870\uD68C",
|
|
255
|
+
{
|
|
256
|
+
path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
|
|
257
|
+
},
|
|
258
|
+
async ({ path: dirPath }) => {
|
|
259
|
+
const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
|
|
260
|
+
const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
|
|
261
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
server.tool(
|
|
265
|
+
"search_code",
|
|
266
|
+
"\uCF54\uB4DC/\uD14D\uC2A4\uD2B8 \uAC80\uC0C9",
|
|
267
|
+
{
|
|
268
|
+
pattern: import_zod.z.string().describe("\uAC80\uC0C9 \uD328\uD134 (\uC815\uADDC\uC2DD \uC9C0\uC6D0)"),
|
|
269
|
+
directory: import_zod.z.string().optional().default(".").describe("\uAC80\uC0C9 \uB514\uB809\uD1A0\uB9AC"),
|
|
270
|
+
file_pattern: import_zod.z.string().optional().default("**/*").describe("\uD30C\uC77C \uD328\uD134")
|
|
271
|
+
},
|
|
272
|
+
async ({ pattern, directory, file_pattern }) => {
|
|
273
|
+
try {
|
|
274
|
+
const { stdout } = await execAsync(
|
|
275
|
+
`rg --no-heading -n "${pattern}" ${directory}`,
|
|
276
|
+
{ timeout: 1e4 }
|
|
277
|
+
);
|
|
278
|
+
return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
|
|
279
|
+
} catch {
|
|
280
|
+
const files = await (0, import_glob.glob)(file_pattern, { cwd: directory });
|
|
281
|
+
const results = [];
|
|
282
|
+
for (const file of files.slice(0, 100)) {
|
|
283
|
+
try {
|
|
284
|
+
const content = await import_promises.default.readFile(
|
|
285
|
+
import_path2.default.join(directory, file),
|
|
286
|
+
"utf-8"
|
|
287
|
+
);
|
|
288
|
+
const lines = content.split("\n");
|
|
289
|
+
const re = new RegExp(pattern, "gi");
|
|
290
|
+
lines.forEach((line, i) => {
|
|
291
|
+
if (re.test(line)) results.push(`${file}:${i + 1}: ${line}`);
|
|
292
|
+
});
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
content: [
|
|
298
|
+
{ type: "text", text: results.join("\n") || "\uACB0\uACFC \uC5C6\uC74C" }
|
|
299
|
+
]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
server.tool(
|
|
305
|
+
"list_processes",
|
|
306
|
+
"\uC2E4\uD589 \uC911\uC778 \uD504\uB85C\uC138\uC2A4 \uBAA9\uB85D",
|
|
307
|
+
{},
|
|
308
|
+
async () => {
|
|
309
|
+
const cmd = process.platform === "win32" ? "tasklist" : process.platform === "darwin" ? "ps aux | sort -rk 3 | head -30" : "ps aux --sort=-%cpu | head -30";
|
|
310
|
+
const { stdout } = await execAsync(cmd);
|
|
311
|
+
return { content: [{ type: "text", text: stdout }] };
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
server.tool(
|
|
315
|
+
"kill_process",
|
|
316
|
+
"\uD504\uB85C\uC138\uC2A4 \uC885\uB8CC (SIGTERM \uD6C4 3\uCD08 \uB300\uAE30, \uC0B4\uC544\uC788\uC73C\uBA74 SIGKILL \uC790\uB3D9 \uC801\uC6A9)",
|
|
317
|
+
{
|
|
318
|
+
pid: import_zod.z.number().describe("\uC885\uB8CC\uD560 \uD504\uB85C\uC138\uC2A4 PID"),
|
|
319
|
+
signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("\uCD08\uAE30 \uC2DC\uADF8\uB110 (\uAE30\uBCF8\uAC12: SIGTERM). SIGKILL \uC9C0\uC815 \uC2DC \uC989\uC2DC \uAC15\uC81C \uC885\uB8CC)")
|
|
320
|
+
},
|
|
321
|
+
async ({ pid, signal }) => {
|
|
322
|
+
const isWindows = process.platform === "win32";
|
|
323
|
+
if (isWindows) {
|
|
324
|
+
await execAsync(`taskkill /PID ${pid} /F`);
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: `PID ${pid} \uC885\uB8CC \uC644\uB8CC (taskkill /F)` }]
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (signal === "SIGKILL") {
|
|
330
|
+
await execAsync(`kill -9 ${pid}`);
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: "text", text: `PID ${pid} \uAC15\uC81C \uC885\uB8CC \uC644\uB8CC (SIGKILL)` }]
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
await execAsync(`kill -15 ${pid}`);
|
|
337
|
+
} catch {
|
|
338
|
+
return {
|
|
339
|
+
content: [
|
|
340
|
+
{ type: "text", text: `PID ${pid} \uC885\uB8CC \uC2E4\uD328: \uD504\uB85C\uC138\uC2A4\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uAC70\uB098 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.` }
|
|
341
|
+
],
|
|
342
|
+
isError: true
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
346
|
+
const isAlive = await execAsync(`kill -0 ${pid}`).then(() => true).catch(() => false);
|
|
347
|
+
if (!isAlive) {
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: "text", text: `PID ${pid} \uC885\uB8CC \uC644\uB8CC (SIGTERM)` }]
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
await execAsync(`kill -9 ${pid}`);
|
|
353
|
+
return {
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: `PID ${pid} \uAC15\uC81C \uC885\uB8CC \uC644\uB8CC (SIGTERM \uBB34\uC751\uB2F5 \u2192 SIGKILL \uC790\uB3D9 \uC801\uC6A9)`
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/tools/browser.ts
|
|
367
|
+
var import_playwright = require("playwright");
|
|
368
|
+
var import_zod2 = require("zod");
|
|
369
|
+
var BrowserTools = class {
|
|
370
|
+
browser = null;
|
|
371
|
+
page = null;
|
|
372
|
+
async init() {
|
|
373
|
+
try {
|
|
374
|
+
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
375
|
+
this.page = await this.browser.newPage();
|
|
376
|
+
} catch {
|
|
377
|
+
console.warn(
|
|
378
|
+
"\u26A0\uFE0F Playwright \uBBF8\uC124\uCE58. \uBE0C\uB77C\uC6B0\uC800 \uB3C4\uAD6C \uBE44\uD65C\uC131\uD654.\n \uD65C\uC131\uD654: npx playwright install chromium"
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async cleanup() {
|
|
383
|
+
await this.browser?.close();
|
|
384
|
+
}
|
|
385
|
+
register(server) {
|
|
386
|
+
const requirePage = () => {
|
|
387
|
+
if (!this.page) throw new Error("\uBE0C\uB77C\uC6B0\uC800 \uBBF8\uCD08\uAE30\uD654. playwright \uC124\uCE58 \uD655\uC778.");
|
|
388
|
+
return this.page;
|
|
389
|
+
};
|
|
390
|
+
server.tool(
|
|
391
|
+
"browser_navigate",
|
|
392
|
+
"URL\uB85C \uC774\uB3D9",
|
|
393
|
+
{ url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
|
|
394
|
+
async ({ url }) => {
|
|
395
|
+
const page = requirePage();
|
|
396
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
server.tool(
|
|
403
|
+
"browser_click",
|
|
404
|
+
"\uC694\uC18C \uD074\uB9AD",
|
|
405
|
+
{ selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
|
|
406
|
+
async ({ selector }) => {
|
|
407
|
+
await requirePage().click(selector);
|
|
408
|
+
return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
server.tool(
|
|
412
|
+
"browser_type",
|
|
413
|
+
"\uD14D\uC2A4\uD2B8 \uC785\uB825",
|
|
414
|
+
{
|
|
415
|
+
selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790"),
|
|
416
|
+
text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
|
|
417
|
+
clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
|
|
418
|
+
},
|
|
419
|
+
async ({ selector, text, clear }) => {
|
|
420
|
+
const page = requirePage();
|
|
421
|
+
if (clear) await page.fill(selector, text);
|
|
422
|
+
else await page.type(selector, text);
|
|
423
|
+
return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
server.tool(
|
|
427
|
+
"browser_screenshot",
|
|
428
|
+
"\uD604\uC7AC \uD398\uC774\uC9C0 \uC2A4\uD06C\uB9B0\uC0F7",
|
|
429
|
+
{
|
|
430
|
+
path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
|
|
431
|
+
full_page: import_zod2.z.boolean().optional().default(false)
|
|
432
|
+
},
|
|
433
|
+
async ({ path: path3, full_page }) => {
|
|
434
|
+
const page = requirePage();
|
|
435
|
+
const screenshot = await page.screenshot({
|
|
436
|
+
path: path3 ?? void 0,
|
|
437
|
+
fullPage: full_page
|
|
438
|
+
});
|
|
439
|
+
if (path3) {
|
|
440
|
+
return { content: [{ type: "text", text: `\uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
content: [
|
|
444
|
+
{
|
|
445
|
+
type: "image",
|
|
446
|
+
data: screenshot.toString("base64"),
|
|
447
|
+
mimeType: "image/png"
|
|
448
|
+
}
|
|
449
|
+
]
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
server.tool(
|
|
454
|
+
"browser_snapshot",
|
|
455
|
+
"\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
|
|
456
|
+
{},
|
|
457
|
+
async () => {
|
|
458
|
+
const page = requirePage();
|
|
459
|
+
const snapshot = await page.locator("body").ariaSnapshot();
|
|
460
|
+
return {
|
|
461
|
+
content: [
|
|
462
|
+
{ type: "text", text: snapshot }
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
server.tool(
|
|
468
|
+
"browser_evaluate",
|
|
469
|
+
"JavaScript \uC2E4\uD589",
|
|
470
|
+
{ code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
|
|
471
|
+
async ({ code }) => {
|
|
472
|
+
const result = await requirePage().evaluate(code);
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
476
|
+
]
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
server.tool(
|
|
481
|
+
"browser_pdf",
|
|
482
|
+
"\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
|
|
483
|
+
{ path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
|
|
484
|
+
async ({ path: path3 }) => {
|
|
485
|
+
await requirePage().pdf({ path: path3 });
|
|
486
|
+
return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/tools/notebook.ts
|
|
493
|
+
var import_zod3 = require("zod");
|
|
494
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
495
|
+
var import_child_process2 = require("child_process");
|
|
496
|
+
var import_util2 = require("util");
|
|
497
|
+
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
498
|
+
async function readNotebook(filePath) {
|
|
499
|
+
const raw = await import_promises2.default.readFile(filePath, "utf-8");
|
|
500
|
+
return JSON.parse(raw);
|
|
501
|
+
}
|
|
502
|
+
async function writeNotebook(filePath, nb) {
|
|
503
|
+
await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
|
|
504
|
+
}
|
|
505
|
+
var NotebookTools = class {
|
|
506
|
+
register(server) {
|
|
507
|
+
server.tool(
|
|
508
|
+
"notebook_read",
|
|
509
|
+
".ipynb \uB178\uD2B8\uBD81 \uC77D\uAE30",
|
|
510
|
+
{ path: import_zod3.z.string().describe("\uB178\uD2B8\uBD81 \uD30C\uC77C \uACBD\uB85C") },
|
|
511
|
+
async ({ path: filePath }) => {
|
|
512
|
+
const nb = await readNotebook(filePath);
|
|
513
|
+
const cells = nb.cells.map((cell, i) => ({
|
|
514
|
+
index: i,
|
|
515
|
+
type: cell.cell_type,
|
|
516
|
+
source: cell.source.join(""),
|
|
517
|
+
outputs: cell.outputs?.length ?? 0
|
|
518
|
+
}));
|
|
519
|
+
return {
|
|
520
|
+
content: [{ type: "text", text: JSON.stringify(cells, null, 2) }]
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
server.tool(
|
|
525
|
+
"notebook_edit_cell",
|
|
526
|
+
"\uB178\uD2B8\uBD81 \uD2B9\uC815 \uC140 \uC218\uC815",
|
|
527
|
+
{
|
|
528
|
+
path: import_zod3.z.string(),
|
|
529
|
+
cell_index: import_zod3.z.number().describe("0\uBD80\uD130 \uC2DC\uC791\uD558\uB294 \uC140 \uC778\uB371\uC2A4"),
|
|
530
|
+
source: import_zod3.z.string().describe("\uC0C8 \uC18C\uC2A4 \uCF54\uB4DC")
|
|
531
|
+
},
|
|
532
|
+
async ({ path: filePath, cell_index, source }) => {
|
|
533
|
+
const nb = await readNotebook(filePath);
|
|
534
|
+
if (cell_index < 0 || cell_index >= nb.cells.length) {
|
|
535
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC140 \uC778\uB371\uC2A4: ${cell_index}`);
|
|
536
|
+
}
|
|
537
|
+
nb.cells[cell_index].source = source.split("\n").map(
|
|
538
|
+
(l, i, arr) => i < arr.length - 1 ? l + "\n" : l
|
|
539
|
+
);
|
|
540
|
+
await writeNotebook(filePath, nb);
|
|
541
|
+
return { content: [{ type: "text", text: "\uC140 \uC218\uC815 \uC644\uB8CC" }] };
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
server.tool(
|
|
545
|
+
"notebook_execute",
|
|
546
|
+
"\uB178\uD2B8\uBD81 \uC2E4\uD589 (nbconvert --execute)",
|
|
547
|
+
{
|
|
548
|
+
path: import_zod3.z.string().describe("\uB178\uD2B8\uBD81 \uD30C\uC77C \uACBD\uB85C"),
|
|
549
|
+
timeout: import_zod3.z.number().optional().default(300).describe("\uC140\uB2F9 \uD0C0\uC784\uC544\uC6C3 (\uCD08)")
|
|
550
|
+
},
|
|
551
|
+
async ({ path: filePath, timeout }) => {
|
|
552
|
+
const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
|
|
553
|
+
const candidates = [
|
|
554
|
+
"jupyter",
|
|
555
|
+
`${process.env.HOME}/Library/Python/3.9/bin/jupyter`,
|
|
556
|
+
`${process.env.HOME}/Library/Python/3.10/bin/jupyter`,
|
|
557
|
+
`${process.env.HOME}/Library/Python/3.11/bin/jupyter`,
|
|
558
|
+
`${process.env.HOME}/Library/Python/3.12/bin/jupyter`,
|
|
559
|
+
"/usr/local/bin/jupyter",
|
|
560
|
+
"/opt/homebrew/bin/jupyter"
|
|
561
|
+
];
|
|
562
|
+
for (const jupyter of candidates) {
|
|
563
|
+
try {
|
|
564
|
+
const { stdout, stderr } = await execAsync2(`${jupyter} ${nbconvertArgs}`);
|
|
565
|
+
return { content: [{ type: "text", text: stdout || stderr || "\uC2E4\uD589 \uC644\uB8CC" }] };
|
|
566
|
+
} catch (err) {
|
|
567
|
+
const error = err;
|
|
568
|
+
if (error.code !== "127" && !error.message?.includes("not found") && !error.message?.includes("No such file")) {
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
throw new Error("jupyter\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC124\uCE58 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694: pip install jupyter");
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// src/tools/device.ts
|
|
580
|
+
var import_child_process3 = require("child_process");
|
|
581
|
+
var import_util3 = require("util");
|
|
582
|
+
var import_zod4 = require("zod");
|
|
583
|
+
var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
|
|
584
|
+
function platform() {
|
|
585
|
+
if (process.platform === "darwin") return "mac";
|
|
586
|
+
if (process.platform === "win32") return "win";
|
|
587
|
+
return "linux";
|
|
588
|
+
}
|
|
589
|
+
var DeviceTools = class {
|
|
590
|
+
register(server) {
|
|
591
|
+
server.tool(
|
|
592
|
+
"screen_capture",
|
|
593
|
+
"\uD654\uBA74 \uC2A4\uD06C\uB9B0\uC0F7 (OS \uB124\uC774\uD2F0\uBE0C)",
|
|
594
|
+
{
|
|
595
|
+
output_path: import_zod4.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 temp \uC800\uC7A5 \uD6C4 base64 \uBC18\uD658)")
|
|
596
|
+
},
|
|
597
|
+
async ({ output_path }) => {
|
|
598
|
+
const p = platform();
|
|
599
|
+
const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
|
|
600
|
+
const cmd = {
|
|
601
|
+
mac: `screencapture -x "${tmpPath}"`,
|
|
602
|
+
win: `nircmd.exe savescreenshot "${tmpPath}"`,
|
|
603
|
+
linux: `scrot "${tmpPath}"`
|
|
604
|
+
}[p];
|
|
605
|
+
await execAsync3(cmd);
|
|
606
|
+
const { readFileSync } = await import("fs");
|
|
607
|
+
const data = readFileSync(tmpPath).toString("base64");
|
|
608
|
+
return {
|
|
609
|
+
content: [{ type: "image", data, mimeType: "image/png" }]
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
server.tool(
|
|
614
|
+
"camera_capture",
|
|
615
|
+
"\uCE74\uBA54\uB77C \uC0AC\uC9C4 \uCD2C\uC601",
|
|
616
|
+
{
|
|
617
|
+
output_path: import_zod4.z.string().optional()
|
|
618
|
+
},
|
|
619
|
+
async ({ output_path }) => {
|
|
620
|
+
const p = platform();
|
|
621
|
+
const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
|
|
622
|
+
const cmd = {
|
|
623
|
+
mac: `imagesnap "${tmpPath}"`,
|
|
624
|
+
win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
|
|
625
|
+
linux: `fswebcam -r 1280x720 "${tmpPath}"`
|
|
626
|
+
}[p];
|
|
627
|
+
await execAsync3(cmd);
|
|
628
|
+
const { readFileSync } = await import("fs");
|
|
629
|
+
const data = readFileSync(tmpPath).toString("base64");
|
|
630
|
+
return {
|
|
631
|
+
content: [{ type: "image", data, mimeType: "image/jpeg" }]
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
server.tool(
|
|
636
|
+
"notification_send",
|
|
637
|
+
"OS \uC54C\uB9BC \uC804\uC1A1",
|
|
638
|
+
{
|
|
639
|
+
title: import_zod4.z.string().describe("\uC54C\uB9BC \uC81C\uBAA9"),
|
|
640
|
+
message: import_zod4.z.string().describe("\uC54C\uB9BC \uB0B4\uC6A9")
|
|
641
|
+
},
|
|
642
|
+
async ({ title, message }) => {
|
|
643
|
+
const p = platform();
|
|
644
|
+
const cmd = {
|
|
645
|
+
mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
|
|
646
|
+
win: `powershell -Command "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime]::CreateToastNotifier('Junis').Show([Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime]::new())"`,
|
|
647
|
+
linux: `notify-send "${title}" "${message}"`
|
|
648
|
+
}[p];
|
|
649
|
+
await execAsync3(cmd);
|
|
650
|
+
return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
server.tool(
|
|
654
|
+
"clipboard_read",
|
|
655
|
+
"\uD074\uB9BD\uBCF4\uB4DC \uC77D\uAE30",
|
|
656
|
+
{},
|
|
657
|
+
async () => {
|
|
658
|
+
const p = platform();
|
|
659
|
+
const cmd = { mac: "pbpaste", win: "powershell Get-Clipboard", linux: "xclip -o" }[p];
|
|
660
|
+
const { stdout } = await execAsync3(cmd);
|
|
661
|
+
return { content: [{ type: "text", text: stdout }] };
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
server.tool(
|
|
665
|
+
"clipboard_write",
|
|
666
|
+
"\uD074\uB9BD\uBCF4\uB4DC \uC4F0\uAE30",
|
|
667
|
+
{ text: import_zod4.z.string() },
|
|
668
|
+
async ({ text }) => {
|
|
669
|
+
const p = platform();
|
|
670
|
+
const cmd = {
|
|
671
|
+
mac: `echo "${text}" | pbcopy`,
|
|
672
|
+
win: `powershell Set-Clipboard "${text}"`,
|
|
673
|
+
linux: `echo "${text}" | xclip -selection clipboard`
|
|
674
|
+
}[p];
|
|
675
|
+
await execAsync3(cmd);
|
|
676
|
+
return { content: [{ type: "text", text: "\uD074\uB9BD\uBCF4\uB4DC \uC800\uC7A5 \uC644\uB8CC" }] };
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/server/mcp.ts
|
|
683
|
+
var mcpPort = 3e3;
|
|
684
|
+
var globalBrowserTools = null;
|
|
685
|
+
function createMcpServer() {
|
|
686
|
+
const server = new import_mcp.McpServer({
|
|
687
|
+
name: "junis",
|
|
688
|
+
version: "0.1.0"
|
|
689
|
+
});
|
|
690
|
+
const fsTools = new FilesystemTools();
|
|
691
|
+
fsTools.register(server);
|
|
692
|
+
if (globalBrowserTools) {
|
|
693
|
+
globalBrowserTools.register(server);
|
|
694
|
+
}
|
|
695
|
+
const notebookTools = new NotebookTools();
|
|
696
|
+
notebookTools.register(server);
|
|
697
|
+
const deviceTools = new DeviceTools();
|
|
698
|
+
deviceTools.register(server);
|
|
699
|
+
return server;
|
|
700
|
+
}
|
|
701
|
+
function readBody(req) {
|
|
702
|
+
return new Promise((resolve, reject) => {
|
|
703
|
+
const chunks = [];
|
|
704
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
705
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
706
|
+
req.on("error", reject);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
function tryListen(httpServer, port, maxRetries) {
|
|
710
|
+
return new Promise((resolve, reject) => {
|
|
711
|
+
let currentPort = port;
|
|
712
|
+
let attempts = 0;
|
|
713
|
+
const attempt = () => {
|
|
714
|
+
httpServer.listen(currentPort, () => {
|
|
715
|
+
resolve(currentPort);
|
|
716
|
+
});
|
|
717
|
+
httpServer.once("error", (err) => {
|
|
718
|
+
if (err.code === "EADDRINUSE" && attempts < maxRetries) {
|
|
719
|
+
attempts++;
|
|
720
|
+
currentPort++;
|
|
721
|
+
httpServer.removeAllListeners("error");
|
|
722
|
+
attempt();
|
|
723
|
+
} else {
|
|
724
|
+
reject(err);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
attempt();
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function handleCors(res) {
|
|
732
|
+
res.writeHead(204, {
|
|
733
|
+
"Access-Control-Allow-Origin": "*",
|
|
734
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
735
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version, mcp-protocol-version, MCP-Session-Id"
|
|
736
|
+
});
|
|
737
|
+
res.end();
|
|
738
|
+
}
|
|
739
|
+
function handleOAuthDiscovery(req, res, port) {
|
|
740
|
+
const url = req.url ?? "";
|
|
741
|
+
const baseUrl = `http://localhost:${port}`;
|
|
742
|
+
if (url === "/.well-known/oauth-protected-resource" || url === "/.well-known/oauth-protected-resource/mcp") {
|
|
743
|
+
const metadata = {
|
|
744
|
+
resource: `${baseUrl}/mcp`,
|
|
745
|
+
// authorization_servers 필드 자체를 생략 → 클라이언트가 AS를 찾지 않음
|
|
746
|
+
bearer_methods_supported: []
|
|
747
|
+
};
|
|
748
|
+
res.writeHead(200, {
|
|
749
|
+
"Content-Type": "application/json",
|
|
750
|
+
"Access-Control-Allow-Origin": "*"
|
|
751
|
+
});
|
|
752
|
+
res.end(JSON.stringify(metadata));
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
if (url === "/.well-known/oauth-authorization-server") {
|
|
756
|
+
const metadata = {
|
|
757
|
+
issuer: baseUrl,
|
|
758
|
+
authorization_endpoint: `${baseUrl}/oauth/authorize`,
|
|
759
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
760
|
+
registration_endpoint: `${baseUrl}/oauth/register`,
|
|
761
|
+
response_types_supported: ["code"],
|
|
762
|
+
grant_types_supported: ["authorization_code"],
|
|
763
|
+
code_challenge_methods_supported: ["S256"],
|
|
764
|
+
token_endpoint_auth_methods_supported: ["none"]
|
|
765
|
+
};
|
|
766
|
+
res.writeHead(200, {
|
|
767
|
+
"Content-Type": "application/json",
|
|
768
|
+
"Access-Control-Allow-Origin": "*"
|
|
769
|
+
});
|
|
770
|
+
res.end(JSON.stringify(metadata));
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
if (url === "/oauth/register" && req.method === "POST") {
|
|
774
|
+
const clientInfo = {
|
|
775
|
+
client_id: `local-${Date.now()}`,
|
|
776
|
+
client_id_issued_at: Math.floor(Date.now() / 1e3),
|
|
777
|
+
redirect_uris: [],
|
|
778
|
+
grant_types: ["authorization_code"],
|
|
779
|
+
response_types: ["code"],
|
|
780
|
+
token_endpoint_auth_method: "none"
|
|
781
|
+
};
|
|
782
|
+
res.writeHead(201, {
|
|
783
|
+
"Content-Type": "application/json",
|
|
784
|
+
"Access-Control-Allow-Origin": "*"
|
|
785
|
+
});
|
|
786
|
+
res.end(JSON.stringify(clientInfo));
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
if (url === "/oauth/token" && req.method === "POST") {
|
|
790
|
+
const tokenResponse = {
|
|
791
|
+
access_token: `local-token-${Date.now()}`,
|
|
792
|
+
token_type: "Bearer",
|
|
793
|
+
expires_in: 86400
|
|
794
|
+
};
|
|
795
|
+
res.writeHead(200, {
|
|
796
|
+
"Content-Type": "application/json",
|
|
797
|
+
"Access-Control-Allow-Origin": "*"
|
|
798
|
+
});
|
|
799
|
+
res.end(JSON.stringify(tokenResponse));
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
async function startMCPServer(port) {
|
|
805
|
+
globalBrowserTools = new BrowserTools();
|
|
806
|
+
await globalBrowserTools.init();
|
|
807
|
+
let resolvedPort = port;
|
|
808
|
+
const httpServer = (0, import_http.createServer)(
|
|
809
|
+
async (req, res) => {
|
|
810
|
+
try {
|
|
811
|
+
if (req.method === "OPTIONS") {
|
|
812
|
+
handleCors(res);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (handleOAuthDiscovery(req, res, resolvedPort)) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const url = req.url ?? "";
|
|
819
|
+
if (url === "/mcp") {
|
|
820
|
+
if (req.method === "POST") {
|
|
821
|
+
const mcpServer = createMcpServer();
|
|
822
|
+
const transport = new import_streamableHttp.StreamableHTTPServerTransport({
|
|
823
|
+
sessionIdGenerator: void 0
|
|
824
|
+
// stateless
|
|
825
|
+
});
|
|
826
|
+
try {
|
|
827
|
+
const rawBody = await readBody(req);
|
|
828
|
+
const parsedBody = rawBody ? JSON.parse(rawBody) : void 0;
|
|
829
|
+
await mcpServer.connect(transport);
|
|
830
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
831
|
+
res.on("close", () => {
|
|
832
|
+
transport.close().catch(() => {
|
|
833
|
+
});
|
|
834
|
+
mcpServer.close().catch(() => {
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
} catch (err) {
|
|
838
|
+
console.error("MCP request error:", err);
|
|
839
|
+
if (!res.headersSent) {
|
|
840
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
841
|
+
res.end(
|
|
842
|
+
JSON.stringify({
|
|
843
|
+
jsonrpc: "2.0",
|
|
844
|
+
error: { code: -32603, message: "Internal server error" },
|
|
845
|
+
id: null
|
|
846
|
+
})
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} else if (req.method === "GET") {
|
|
851
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
852
|
+
res.end(
|
|
853
|
+
JSON.stringify({
|
|
854
|
+
jsonrpc: "2.0",
|
|
855
|
+
error: { code: -32e3, message: "Method not allowed." },
|
|
856
|
+
id: null
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
} else if (req.method === "DELETE") {
|
|
860
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
861
|
+
res.end(
|
|
862
|
+
JSON.stringify({
|
|
863
|
+
jsonrpc: "2.0",
|
|
864
|
+
error: { code: -32e3, message: "Method not allowed." },
|
|
865
|
+
id: null
|
|
866
|
+
})
|
|
867
|
+
);
|
|
868
|
+
} else {
|
|
869
|
+
res.writeHead(405);
|
|
870
|
+
res.end("Method Not Allowed");
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
res.writeHead(404);
|
|
874
|
+
res.end("Not found");
|
|
875
|
+
}
|
|
876
|
+
} catch (err) {
|
|
877
|
+
console.error("HTTP server error:", err);
|
|
878
|
+
if (!res.headersSent) {
|
|
879
|
+
res.writeHead(500);
|
|
880
|
+
res.end("Internal server error");
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
const actualPort = await tryListen(httpServer, port, 10);
|
|
886
|
+
resolvedPort = actualPort;
|
|
887
|
+
mcpPort = actualPort;
|
|
888
|
+
console.log(`MCP \uC11C\uBC84 \uC2DC\uC791: http://localhost:${actualPort}/mcp`);
|
|
889
|
+
const cleanup = async () => {
|
|
890
|
+
if (globalBrowserTools) {
|
|
891
|
+
await globalBrowserTools.cleanup();
|
|
892
|
+
}
|
|
893
|
+
httpServer.close();
|
|
894
|
+
};
|
|
895
|
+
process.on("SIGINT", async () => {
|
|
896
|
+
await cleanup();
|
|
897
|
+
process.exit(0);
|
|
898
|
+
});
|
|
899
|
+
process.on("SIGTERM", async () => {
|
|
900
|
+
await cleanup();
|
|
901
|
+
process.exit(0);
|
|
902
|
+
});
|
|
903
|
+
return actualPort;
|
|
904
|
+
}
|
|
905
|
+
async function handleMCPRequest(id, payload) {
|
|
906
|
+
const url = `http://localhost:${mcpPort}/mcp`;
|
|
907
|
+
const res = await fetch(url, {
|
|
908
|
+
method: "POST",
|
|
909
|
+
headers: {
|
|
910
|
+
"Content-Type": "application/json",
|
|
911
|
+
Accept: "application/json, text/event-stream"
|
|
912
|
+
},
|
|
913
|
+
body: JSON.stringify(payload)
|
|
914
|
+
});
|
|
915
|
+
if (!res.ok) {
|
|
916
|
+
throw new Error(`MCP request failed: ${res.status} ${res.statusText}`);
|
|
917
|
+
}
|
|
918
|
+
const text = await res.text();
|
|
919
|
+
const lines = text.split("\n");
|
|
920
|
+
for (const line of lines) {
|
|
921
|
+
if (line.startsWith("data: ")) {
|
|
922
|
+
try {
|
|
923
|
+
return JSON.parse(line.slice(6));
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/cli/index.ts
|
|
932
|
+
import_commander.program.name("junis").description("AI\uAC00 \uB0B4 \uB514\uBC14\uC774\uC2A4\uB97C \uC644\uC804 \uC81C\uC5B4\uD558\uB294 MCP \uC11C\uBC84").version("0.1.0");
|
|
933
|
+
import_commander.program.command("start", { isDefault: true }).description("Junis \uC5D0\uC774\uC804\uD2B8\uC640 \uC5F0\uACB0 \uC2DC\uC791").option("--local", "\uB85C\uCEEC MCP \uC11C\uBC84\uB9CC \uC2E4\uD589 (\uD074\uB77C\uC6B0\uB4DC \uC5F0\uACB0 \uC5C6\uC74C)").option("--port <number>", "\uD3EC\uD2B8 \uBC88\uD638", "3000").option("--reset", "\uAE30\uC874 \uC778\uC99D \uCD08\uAE30\uD654 \uD6C4 \uC7AC\uB85C\uADF8\uC778").action(async (options) => {
|
|
934
|
+
const port = parseInt(options.port, 10);
|
|
935
|
+
if (options.local) {
|
|
936
|
+
console.log("\u{1F3E0} \uB85C\uCEEC \uBAA8\uB4DC: \uD074\uB77C\uC6B0\uB4DC \uC5F0\uACB0 \uC5C6\uC774 MCP \uC11C\uBC84\uB9CC \uC2E4\uD589\uD569\uB2C8\uB2E4.");
|
|
937
|
+
const actualPort2 = await startMCPServer(port);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
let config = options.reset ? null : loadConfig();
|
|
941
|
+
if (!config) {
|
|
942
|
+
console.log("\u{1F511} Junis \uACC4\uC815 \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
943
|
+
const authResult = await authenticate();
|
|
944
|
+
config = {
|
|
945
|
+
device_key: authResult.device_key,
|
|
946
|
+
token: authResult.token,
|
|
947
|
+
device_name: `${process.env["USER"] ?? "user"}'s ${getDeviceName()}`,
|
|
948
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
949
|
+
};
|
|
950
|
+
saveConfig(config);
|
|
951
|
+
console.log(`\u{1F4BE} \uC778\uC99D \uC815\uBCF4 \uC800\uC7A5: ~/.junis/config.json`);
|
|
952
|
+
} else {
|
|
953
|
+
console.log(`\u{1F511} \uAE30\uC874 \uC778\uC99D \uC0AC\uC6A9: ${config.device_name}`);
|
|
954
|
+
}
|
|
955
|
+
const actualPort = await startMCPServer(port);
|
|
956
|
+
const relay = new RelayClient(config, handleMCPRequest);
|
|
957
|
+
await relay.connect();
|
|
958
|
+
console.log(`
|
|
959
|
+
\u2728 Junis \uC2DC\uC791 \uC644\uB8CC!
|
|
960
|
+
\uB85C\uCEEC MCP: http://localhost:${actualPort}/mcp
|
|
961
|
+
\uB514\uBC14\uC774\uC2A4: ${config.device_name}
|
|
962
|
+
\uC0C1\uD0DC: \uD074\uB77C\uC6B0\uB4DC \uB9B4\uB808\uC774 \uC5F0\uACB0\uB428
|
|
963
|
+
`);
|
|
964
|
+
});
|
|
965
|
+
import_commander.program.command("logout").description("\uC778\uC99D \uC815\uBCF4 \uC0AD\uC81C").action(() => {
|
|
966
|
+
clearConfig();
|
|
967
|
+
console.log("\u2705 \uC778\uC99D \uC815\uBCF4 \uC0AD\uC81C \uC644\uB8CC");
|
|
968
|
+
});
|
|
969
|
+
import_commander.program.command("status").description("\uD604\uC7AC \uC0C1\uD0DC \uD655\uC778").action(() => {
|
|
970
|
+
const config = loadConfig();
|
|
971
|
+
if (!config) {
|
|
972
|
+
console.log("\u274C \uC778\uC99D \uC5C6\uC74C (npx junis \uC2E4\uD589 \uD544\uC694)");
|
|
973
|
+
} else {
|
|
974
|
+
console.log(
|
|
975
|
+
`\u2705 \uC778\uC99D\uB428
|
|
976
|
+
\uB514\uBC14\uC774\uC2A4: ${config.device_name}
|
|
977
|
+
\uB4F1\uB85D\uC77C: ${config.created_at}`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
function getDeviceName() {
|
|
982
|
+
const platform2 = process.platform;
|
|
983
|
+
if (platform2 === "darwin") return "Mac";
|
|
984
|
+
if (platform2 === "win32") return "Windows PC";
|
|
985
|
+
return "Linux PC";
|
|
986
|
+
}
|
|
987
|
+
import_commander.program.parse();
|