@usex/mikrotik-mcp 2.1.0 → 2.3.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/dist/cli.js +424 -52
- package/dist/index.js +391 -22
- package/dist/ui/observability.html +2 -2
- package/package.json +2 -1
- package/schemas/config.schema.json +15 -1
- package/schemas/tool-catalog.json +553 -2110
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,10 @@ var DeviceConfigSchema = z.object({
|
|
|
29
29
|
privateKey: z.string().optional(),
|
|
30
30
|
keyPassphrase: z.string().optional(),
|
|
31
31
|
timeoutMs: z.coerce.number().int().positive().default(1e4),
|
|
32
|
+
mac: z.string().optional(),
|
|
33
|
+
sourceMac: z.string().optional(),
|
|
34
|
+
macHost: z.string().optional(),
|
|
35
|
+
macPort: z.coerce.number().int().positive().optional(),
|
|
32
36
|
description: z.string().optional()
|
|
33
37
|
});
|
|
34
38
|
var S3ConfigSchema = z.object({
|
|
@@ -116,7 +120,11 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
116
120
|
keyFilename: pick("key-filename", "MIKROTIK_KEY_FILENAME"),
|
|
117
121
|
privateKey: pick("private-key", "MIKROTIK_PRIVATE_KEY"),
|
|
118
122
|
keyPassphrase: pick("key-passphrase", "MIKROTIK_KEY_PASSPHRASE"),
|
|
119
|
-
timeoutMs: pick("timeout-ms", "MIKROTIK_TIMEOUT_MS")
|
|
123
|
+
timeoutMs: pick("timeout-ms", "MIKROTIK_TIMEOUT_MS"),
|
|
124
|
+
mac: pick("mac", "MIKROTIK_MAC"),
|
|
125
|
+
sourceMac: pick("source-mac", "MIKROTIK_SOURCE_MAC"),
|
|
126
|
+
macHost: pick("mac-host", "MIKROTIK_MAC_HOST"),
|
|
127
|
+
macPort: pick("mac-port", "MIKROTIK_MAC_PORT")
|
|
120
128
|
};
|
|
121
129
|
const hasSingle = Object.values(single).some((v) => v !== undefined);
|
|
122
130
|
const devices = {};
|
|
@@ -250,6 +258,340 @@ var logger = {
|
|
|
250
258
|
error: (m) => emit("error", m)
|
|
251
259
|
};
|
|
252
260
|
|
|
261
|
+
// src/mac-telnet/console.ts
|
|
262
|
+
import { MacTelnetSession } from "@tikoci/centrs/protocols";
|
|
263
|
+
var ESC = "\x1B";
|
|
264
|
+
var enc = new TextEncoder;
|
|
265
|
+
var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
266
|
+
var ANSI_ESC2 = new RegExp(`${ESC}[@-_]`, "g");
|
|
267
|
+
var ROUTEROS_PROMPT_RE = /\[[^\]@\r\n]+@[^\]\r\n]*\][^\r\n]*>\s*$/;
|
|
268
|
+
var TICK_INTERVAL_MS = 15;
|
|
269
|
+
var LICENSE_RE = /do you want to see the software license/i;
|
|
270
|
+
function emulateScreen(text) {
|
|
271
|
+
const clean = text.replace(ANSI_CSI, "").replace(ANSI_ESC2, "");
|
|
272
|
+
const lines = [[]];
|
|
273
|
+
let row = 0;
|
|
274
|
+
let col = 0;
|
|
275
|
+
for (const ch of clean) {
|
|
276
|
+
const code = ch.charCodeAt(0);
|
|
277
|
+
if (ch === `
|
|
278
|
+
`) {
|
|
279
|
+
row += 1;
|
|
280
|
+
if (!lines[row])
|
|
281
|
+
lines[row] = [];
|
|
282
|
+
col = 0;
|
|
283
|
+
} else if (ch === "\r") {
|
|
284
|
+
col = 0;
|
|
285
|
+
} else if (code === 8) {
|
|
286
|
+
col = Math.max(0, col - 1);
|
|
287
|
+
} else if (code >= 32 && code !== 127) {
|
|
288
|
+
const line = lines[row];
|
|
289
|
+
line[col] = ch;
|
|
290
|
+
col += 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return lines.map((line) => Array.from(line, (c) => c ?? " ").join("").replace(/\s+$/, ""));
|
|
294
|
+
}
|
|
295
|
+
function extractCommandOutput(raw, command) {
|
|
296
|
+
const lines = emulateScreen(raw);
|
|
297
|
+
let start = 1;
|
|
298
|
+
if (command !== undefined && lines.length > 0) {
|
|
299
|
+
const first = lines[0] ?? "";
|
|
300
|
+
const promptMatch = first.match(/^\[[^\]\r\n]*\][^\r\n]*?>\s?/);
|
|
301
|
+
const echoedOnFirst = promptMatch ? first.slice(promptMatch[0].length) : first;
|
|
302
|
+
let consumed = echoedOnFirst.length;
|
|
303
|
+
while (consumed < command.length && start < lines.length) {
|
|
304
|
+
consumed += (lines[start] ?? "").length;
|
|
305
|
+
start += 1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const body = lines.slice(start);
|
|
309
|
+
while (body.length > 0) {
|
|
310
|
+
const last = body[body.length - 1];
|
|
311
|
+
if (last.length === 0 || ROUTEROS_PROMPT_RE.test(last)) {
|
|
312
|
+
body.pop();
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
return body.join(`
|
|
318
|
+
`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
class MacTelnetConsole {
|
|
322
|
+
options;
|
|
323
|
+
session;
|
|
324
|
+
buffer = "";
|
|
325
|
+
ready = false;
|
|
326
|
+
closed = false;
|
|
327
|
+
closeError;
|
|
328
|
+
waiter;
|
|
329
|
+
tickTimer;
|
|
330
|
+
readyWaiters = [];
|
|
331
|
+
decoder = new TextDecoder;
|
|
332
|
+
probeTail = "";
|
|
333
|
+
constructor(options) {
|
|
334
|
+
this.options = {
|
|
335
|
+
rows: 9999,
|
|
336
|
+
cols: 512,
|
|
337
|
+
primeTimeoutMs: 30000,
|
|
338
|
+
commandTimeoutMs: 15000,
|
|
339
|
+
settleMs: 150,
|
|
340
|
+
acceptLicense: true,
|
|
341
|
+
...options
|
|
342
|
+
};
|
|
343
|
+
this.session = new MacTelnetSession({
|
|
344
|
+
sink: options.sink,
|
|
345
|
+
sourceMac: options.sourceMac,
|
|
346
|
+
destinationMac: options.destinationMac,
|
|
347
|
+
username: options.username,
|
|
348
|
+
password: options.password,
|
|
349
|
+
sessionKey: options.sessionKey,
|
|
350
|
+
terminalType: "vt102",
|
|
351
|
+
terminalWidth: this.options.cols,
|
|
352
|
+
terminalHeight: this.options.rows,
|
|
353
|
+
onReady: () => this.onReady(),
|
|
354
|
+
onData: (bytes) => this.onData(bytes),
|
|
355
|
+
onClose: (error) => this.onClose(error)
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
handlePacket(bytes) {
|
|
359
|
+
try {
|
|
360
|
+
this.session.handlePacket(bytes);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
this.onClose(error instanceof Error ? error : new Error("Failed to process a MAC-Telnet datagram."));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async open() {
|
|
366
|
+
this.tickTimer = setInterval(() => {
|
|
367
|
+
try {
|
|
368
|
+
this.session.tick(Date.now());
|
|
369
|
+
} catch {}
|
|
370
|
+
}, TICK_INTERVAL_MS);
|
|
371
|
+
this.tickTimer.unref?.();
|
|
372
|
+
this.session.start();
|
|
373
|
+
await this.waitReady(this.options.primeTimeoutMs);
|
|
374
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer) || LICENSE_RE.test(buffer), this.options.primeTimeoutMs, "waiting for the RouterOS console prompt");
|
|
375
|
+
if (this.options.acceptLicense && LICENSE_RE.test(this.buffer)) {
|
|
376
|
+
this.buffer = "";
|
|
377
|
+
this.session.sendInput(enc.encode("n\r"));
|
|
378
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.primeTimeoutMs, "waiting for the prompt after the license screen");
|
|
379
|
+
}
|
|
380
|
+
this.buffer = "";
|
|
381
|
+
this.session.sendInput(enc.encode("\r"));
|
|
382
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.commandTimeoutMs, "waiting for a clean prompt");
|
|
383
|
+
this.buffer = "";
|
|
384
|
+
}
|
|
385
|
+
async run(cli) {
|
|
386
|
+
this.assertOpen();
|
|
387
|
+
this.buffer = "";
|
|
388
|
+
this.session.sendInput(enc.encode(`${cli}\r`));
|
|
389
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.commandTimeoutMs, `running over mac-telnet: ${cli}`);
|
|
390
|
+
const raw = this.buffer;
|
|
391
|
+
return { output: extractCommandOutput(raw, cli), raw };
|
|
392
|
+
}
|
|
393
|
+
close() {
|
|
394
|
+
if (this.tickTimer) {
|
|
395
|
+
clearInterval(this.tickTimer);
|
|
396
|
+
this.tickTimer = undefined;
|
|
397
|
+
}
|
|
398
|
+
if (!this.closed)
|
|
399
|
+
this.session.end();
|
|
400
|
+
}
|
|
401
|
+
get isReady() {
|
|
402
|
+
return this.ready && !this.closed;
|
|
403
|
+
}
|
|
404
|
+
onReady() {
|
|
405
|
+
this.ready = true;
|
|
406
|
+
for (const w of this.readyWaiters.splice(0))
|
|
407
|
+
w.resolve();
|
|
408
|
+
}
|
|
409
|
+
onData(bytes) {
|
|
410
|
+
const chunk = this.decoder.decode(bytes, { stream: true });
|
|
411
|
+
const combined = `${this.probeTail}${chunk}`;
|
|
412
|
+
this.answerSizeProbe(combined);
|
|
413
|
+
this.probeTail = combined.slice(-3);
|
|
414
|
+
this.buffer += chunk;
|
|
415
|
+
this.checkWaiter();
|
|
416
|
+
}
|
|
417
|
+
onClose(error) {
|
|
418
|
+
this.closed = true;
|
|
419
|
+
this.closeError = error;
|
|
420
|
+
if (this.tickTimer) {
|
|
421
|
+
clearInterval(this.tickTimer);
|
|
422
|
+
this.tickTimer = undefined;
|
|
423
|
+
}
|
|
424
|
+
const failure = error ?? new Error("The MAC-Telnet console session closed.");
|
|
425
|
+
for (const w of this.readyWaiters.splice(0))
|
|
426
|
+
w.reject(failure);
|
|
427
|
+
if (this.waiter) {
|
|
428
|
+
const waiter = this.waiter;
|
|
429
|
+
this.waiter = undefined;
|
|
430
|
+
clearTimeout(waiter.timeout);
|
|
431
|
+
if (waiter.settle)
|
|
432
|
+
clearTimeout(waiter.settle);
|
|
433
|
+
waiter.reject(failure);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
answerSizeProbe(chunk) {
|
|
437
|
+
if (chunk.includes(`${ESC}[6n`)) {
|
|
438
|
+
this.session.sendInput(enc.encode(`${ESC}[${this.options.rows};${this.options.cols}R`));
|
|
439
|
+
}
|
|
440
|
+
if (chunk.includes(`${ESC}Z`) || chunk.includes(`${ESC}[c`)) {
|
|
441
|
+
this.session.sendInput(enc.encode(`${ESC}[?6c`));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
endsWithPrompt(buffer) {
|
|
445
|
+
const lines = emulateScreen(buffer).filter((line) => line.length > 0);
|
|
446
|
+
return ROUTEROS_PROMPT_RE.test(lines[lines.length - 1] ?? "");
|
|
447
|
+
}
|
|
448
|
+
waitReady(timeoutMs) {
|
|
449
|
+
if (this.ready)
|
|
450
|
+
return Promise.resolve();
|
|
451
|
+
if (this.closed) {
|
|
452
|
+
return Promise.reject(this.closeError ?? new Error("The MAC-Telnet session closed before login completed."));
|
|
453
|
+
}
|
|
454
|
+
return new Promise((resolve, reject) => {
|
|
455
|
+
let timer;
|
|
456
|
+
const entry = {
|
|
457
|
+
resolve: () => {
|
|
458
|
+
clearTimeout(timer);
|
|
459
|
+
resolve();
|
|
460
|
+
},
|
|
461
|
+
reject: (error) => {
|
|
462
|
+
clearTimeout(timer);
|
|
463
|
+
reject(error);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
timer = setTimeout(() => {
|
|
467
|
+
this.readyWaiters = this.readyWaiters.filter((w) => w !== entry);
|
|
468
|
+
reject(new Error("MAC-Telnet login did not complete \u2014 no console response from the device. " + "Confirm the device is reachable over mac-telnet (mac-server interface list) and the credentials are correct."));
|
|
469
|
+
}, timeoutMs);
|
|
470
|
+
this.readyWaiters.push(entry);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
waitFor(predicate, timeoutMs, label) {
|
|
474
|
+
if (this.closed) {
|
|
475
|
+
return Promise.reject(this.closeError ?? new Error(`MAC-Telnet session closed while ${label}.`));
|
|
476
|
+
}
|
|
477
|
+
return new Promise((resolve, reject) => {
|
|
478
|
+
const timeout = setTimeout(() => {
|
|
479
|
+
this.waiter = undefined;
|
|
480
|
+
reject(new Error(`Timed out ${label}. The RouterOS console did not return a prompt in time; ` + "raise the timeout or confirm the device is responsive over mac-telnet."));
|
|
481
|
+
}, timeoutMs);
|
|
482
|
+
this.waiter = { predicate, resolve, reject, timeout };
|
|
483
|
+
this.checkWaiter();
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
checkWaiter() {
|
|
487
|
+
const waiter = this.waiter;
|
|
488
|
+
if (!waiter)
|
|
489
|
+
return;
|
|
490
|
+
if (!waiter.predicate(this.buffer)) {
|
|
491
|
+
if (waiter.settle) {
|
|
492
|
+
clearTimeout(waiter.settle);
|
|
493
|
+
waiter.settle = undefined;
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (waiter.settle)
|
|
498
|
+
clearTimeout(waiter.settle);
|
|
499
|
+
waiter.settle = setTimeout(() => {
|
|
500
|
+
if (this.waiter !== waiter)
|
|
501
|
+
return;
|
|
502
|
+
this.waiter = undefined;
|
|
503
|
+
clearTimeout(waiter.timeout);
|
|
504
|
+
waiter.resolve();
|
|
505
|
+
}, this.options.settleMs);
|
|
506
|
+
}
|
|
507
|
+
assertOpen() {
|
|
508
|
+
if (!this.ready || this.closed) {
|
|
509
|
+
throw new Error("The MAC-Telnet console is not open. Call open() and await it before running commands.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/mac-telnet/client.ts
|
|
515
|
+
import {
|
|
516
|
+
DEFAULT_MAC_TELNET_BROADCAST,
|
|
517
|
+
isBroadcastHost,
|
|
518
|
+
MAC_TELNET_PORT,
|
|
519
|
+
createUdpMacTelnetTransport,
|
|
520
|
+
parseMac,
|
|
521
|
+
resolveMacTelnetRoute
|
|
522
|
+
} from "@tikoci/centrs/protocols";
|
|
523
|
+
|
|
524
|
+
class MikroTikMacTelnetClient {
|
|
525
|
+
transport = null;
|
|
526
|
+
console = null;
|
|
527
|
+
opts;
|
|
528
|
+
lastError;
|
|
529
|
+
constructor(opts) {
|
|
530
|
+
this.opts = {
|
|
531
|
+
port: MAC_TELNET_PORT,
|
|
532
|
+
timeoutMs: 1e4,
|
|
533
|
+
...opts
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
async connect() {
|
|
537
|
+
this.lastError = undefined;
|
|
538
|
+
try {
|
|
539
|
+
const destinationMac = parseMac(this.opts.mac);
|
|
540
|
+
const explicitSourceMac = this.opts.sourceMac ? parseMac(this.opts.sourceMac) : undefined;
|
|
541
|
+
const host = this.opts.host ?? DEFAULT_MAC_TELNET_BROADCAST;
|
|
542
|
+
const route = await resolveMacTelnetRoute({
|
|
543
|
+
destinationMac,
|
|
544
|
+
host,
|
|
545
|
+
port: this.opts.port,
|
|
546
|
+
timeoutMs: this.opts.timeoutMs,
|
|
547
|
+
explicitSourceMac
|
|
548
|
+
});
|
|
549
|
+
const transport = createUdpMacTelnetTransport({
|
|
550
|
+
host: route.host,
|
|
551
|
+
port: this.opts.port,
|
|
552
|
+
broadcast: isBroadcastHost(route.host)
|
|
553
|
+
});
|
|
554
|
+
this.transport = transport;
|
|
555
|
+
await transport.ready();
|
|
556
|
+
const console = new MacTelnetConsole({
|
|
557
|
+
sink: transport,
|
|
558
|
+
sourceMac: route.sourceMac,
|
|
559
|
+
destinationMac,
|
|
560
|
+
username: this.opts.username,
|
|
561
|
+
password: this.opts.password ?? "",
|
|
562
|
+
primeTimeoutMs: Math.max(this.opts.timeoutMs, 30000),
|
|
563
|
+
commandTimeoutMs: Math.max(this.opts.timeoutMs, 15000)
|
|
564
|
+
});
|
|
565
|
+
this.console = console;
|
|
566
|
+
transport.onMessage((bytes) => console.handlePacket(bytes));
|
|
567
|
+
await console.open();
|
|
568
|
+
return true;
|
|
569
|
+
} catch (e) {
|
|
570
|
+
this.lastError = e instanceof Error ? e.message : String(e);
|
|
571
|
+
logger.error(`Failed to connect to MikroTik over MAC-Telnet: ${this.lastError}`);
|
|
572
|
+
this.disconnect();
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async run(command) {
|
|
577
|
+
if (!this.console || !this.console.isReady) {
|
|
578
|
+
throw new Error("Not connected to MikroTik device (MAC-Telnet)");
|
|
579
|
+
}
|
|
580
|
+
const { output } = await this.console.run(command);
|
|
581
|
+
return output;
|
|
582
|
+
}
|
|
583
|
+
disconnect() {
|
|
584
|
+
try {
|
|
585
|
+
this.console?.close();
|
|
586
|
+
} catch {}
|
|
587
|
+
try {
|
|
588
|
+
this.transport?.close();
|
|
589
|
+
} catch {}
|
|
590
|
+
this.console = null;
|
|
591
|
+
this.transport = null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
253
595
|
// src/ssh/client.ts
|
|
254
596
|
import { readFileSync as readFileSync2 } from "fs";
|
|
255
597
|
import { Client } from "ssh2";
|
|
@@ -352,6 +694,45 @@ class MikroTikSSHClient {
|
|
|
352
694
|
}
|
|
353
695
|
}
|
|
354
696
|
|
|
697
|
+
// src/core/transport.ts
|
|
698
|
+
function isMacTelnetDevice(dc) {
|
|
699
|
+
return Boolean(dc.mac);
|
|
700
|
+
}
|
|
701
|
+
function createDeviceClient(dc) {
|
|
702
|
+
if (isMacTelnetDevice(dc)) {
|
|
703
|
+
return new MikroTikMacTelnetClient({
|
|
704
|
+
mac: dc.mac,
|
|
705
|
+
username: dc.username,
|
|
706
|
+
password: dc.password,
|
|
707
|
+
sourceMac: dc.sourceMac,
|
|
708
|
+
host: dc.macHost,
|
|
709
|
+
port: dc.macPort,
|
|
710
|
+
timeoutMs: dc.timeoutMs
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return new MikroTikSSHClient({
|
|
714
|
+
host: dc.host,
|
|
715
|
+
username: dc.username,
|
|
716
|
+
password: dc.password,
|
|
717
|
+
keyFilename: dc.keyFilename,
|
|
718
|
+
privateKey: dc.privateKey,
|
|
719
|
+
keyPassphrase: dc.keyPassphrase,
|
|
720
|
+
port: dc.port,
|
|
721
|
+
timeoutMs: dc.timeoutMs
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
function describeTransport(dc) {
|
|
725
|
+
return dc.mac ? `MAC ${dc.mac} (mac-telnet)` : `${dc.host}:${dc.port}`;
|
|
726
|
+
}
|
|
727
|
+
function connectErrorMessage(name, dc, lastError) {
|
|
728
|
+
const reason = lastError ? ` \u2014 ${lastError}` : "";
|
|
729
|
+
if (dc.mac) {
|
|
730
|
+
return `Failed to connect to MikroTik device '${name}' over MAC-Telnet at ${dc.mac}${reason}. ` + "Check you are on the same Layer-2 segment, MAC-Telnet is enabled (/tool mac-server) on a reachable interface, and the credentials are correct.";
|
|
731
|
+
}
|
|
732
|
+
const authMode = dc.keyFilename || dc.privateKey ? "SSH key" : dc.password ? "password" : "no credentials";
|
|
733
|
+
return `Failed to connect to MikroTik device '${name}' at ${dc.host}:${dc.port} (auth: ${authMode})${reason}. ` + "Check the host/port are reachable, the SSH service is enabled (/ip service), and the credentials are correct.";
|
|
734
|
+
}
|
|
735
|
+
|
|
355
736
|
// src/tools/address-list.ts
|
|
356
737
|
import { z as z3 } from "zod";
|
|
357
738
|
|
|
@@ -389,6 +770,9 @@ class SafeModeManager {
|
|
|
389
770
|
if (this.active)
|
|
390
771
|
return "Safe mode is already active.";
|
|
391
772
|
const dc = getDevice(this.deviceName);
|
|
773
|
+
if (dc.mac) {
|
|
774
|
+
return "Error: Safe Mode is not supported for a MAC-Telnet device " + `('${this.deviceName}' is reached by MAC ${dc.mac}). ` + "Connect over SSH (configure host/credentials) to use Safe Mode.";
|
|
775
|
+
}
|
|
392
776
|
const ssh = new MikroTikSSHClient({
|
|
393
777
|
host: dc.host,
|
|
394
778
|
username: dc.username,
|
|
@@ -523,25 +907,14 @@ function getSafeModeManager(deviceName) {
|
|
|
523
907
|
async function runOnce(command, deviceName) {
|
|
524
908
|
const name = resolveDeviceName(deviceName);
|
|
525
909
|
const dc = getDevice(deviceName);
|
|
526
|
-
const
|
|
527
|
-
host: dc.host,
|
|
528
|
-
username: dc.username,
|
|
529
|
-
password: dc.password,
|
|
530
|
-
keyFilename: dc.keyFilename,
|
|
531
|
-
privateKey: dc.privateKey,
|
|
532
|
-
keyPassphrase: dc.keyPassphrase,
|
|
533
|
-
port: dc.port,
|
|
534
|
-
timeoutMs: dc.timeoutMs
|
|
535
|
-
});
|
|
910
|
+
const client = createDeviceClient(dc);
|
|
536
911
|
try {
|
|
537
|
-
if (!await
|
|
538
|
-
|
|
539
|
-
const reason = ssh.lastError ? ` \u2014 ${ssh.lastError}` : "";
|
|
540
|
-
throw new Error(`Failed to connect to MikroTik device '${name}' at ${dc.host}:${dc.port} (auth: ${authMode})${reason}. ` + "Check the host/port are reachable, the SSH service is enabled (/ip service), and the credentials are correct.");
|
|
912
|
+
if (!await client.connect()) {
|
|
913
|
+
throw new Error(connectErrorMessage(name, dc, client.lastError));
|
|
541
914
|
}
|
|
542
|
-
return await
|
|
915
|
+
return await client.run(command);
|
|
543
916
|
} finally {
|
|
544
|
-
|
|
917
|
+
client.disconnect();
|
|
545
918
|
}
|
|
546
919
|
}
|
|
547
920
|
async function executeMikrotikCommand(command, ctx) {
|
|
@@ -17276,30 +17649,34 @@ function getDeviceStatus(name) {
|
|
|
17276
17649
|
return statuses.get(name) ?? UNKNOWN;
|
|
17277
17650
|
}
|
|
17278
17651
|
async function probeDevice(name, dc) {
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17652
|
+
if (dc.mac) {
|
|
17653
|
+
const status2 = {
|
|
17654
|
+
reachable: null,
|
|
17655
|
+
checkedAt: Date.now(),
|
|
17656
|
+
latencyMs: null,
|
|
17657
|
+
error: "MAC-Telnet device \u2014 reachability is verified on tool use, not background-probed."
|
|
17658
|
+
};
|
|
17659
|
+
statuses.set(name, status2);
|
|
17660
|
+
return status2;
|
|
17661
|
+
}
|
|
17662
|
+
const client = createDeviceClient({
|
|
17663
|
+
...dc,
|
|
17287
17664
|
timeoutMs: Math.min(dc.timeoutMs ?? 1e4, 8000)
|
|
17288
17665
|
});
|
|
17289
17666
|
const t0 = Date.now();
|
|
17290
17667
|
let status;
|
|
17291
17668
|
try {
|
|
17292
|
-
const ok = await
|
|
17669
|
+
const ok = await client.connect();
|
|
17293
17670
|
if (!ok) {
|
|
17294
17671
|
status = {
|
|
17295
17672
|
reachable: false,
|
|
17296
17673
|
checkedAt: Date.now(),
|
|
17297
17674
|
latencyMs: null,
|
|
17298
|
-
error:
|
|
17675
|
+
error: client.lastError ?? "connection failed"
|
|
17299
17676
|
};
|
|
17300
17677
|
} else {
|
|
17301
|
-
const identity = parseKeyValues(await
|
|
17302
|
-
const version = parseKeyValues(await
|
|
17678
|
+
const identity = parseKeyValues(await client.run("/system identity print")).name;
|
|
17679
|
+
const version = parseKeyValues(await client.run("/system resource print")).version;
|
|
17303
17680
|
status = {
|
|
17304
17681
|
reachable: true,
|
|
17305
17682
|
checkedAt: Date.now(),
|
|
@@ -17316,7 +17693,7 @@ async function probeDevice(name, dc) {
|
|
|
17316
17693
|
error: e instanceof Error ? e.message : String(e)
|
|
17317
17694
|
};
|
|
17318
17695
|
} finally {
|
|
17319
|
-
|
|
17696
|
+
client.disconnect();
|
|
17320
17697
|
}
|
|
17321
17698
|
statuses.set(name, status);
|
|
17322
17699
|
return status;
|
|
@@ -17662,8 +18039,11 @@ function devicesPayload(store2) {
|
|
|
17662
18039
|
name,
|
|
17663
18040
|
host: dc.host,
|
|
17664
18041
|
port: dc.port,
|
|
18042
|
+
mac: dc.mac,
|
|
18043
|
+
transport: dc.mac ? "mac-telnet" : "ssh",
|
|
18044
|
+
address: dc.mac ? dc.mac : `${dc.host}:${dc.port}`,
|
|
17665
18045
|
username: dc.username,
|
|
17666
|
-
authMode: dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
|
|
18046
|
+
authMode: dc.mac ? "mac-telnet" : dc.keyFilename || dc.privateKey ? "key" : dc.password ? "password" : "none",
|
|
17667
18047
|
isDefault: name === cfg.defaultDevice,
|
|
17668
18048
|
description: dc.description,
|
|
17669
18049
|
status: getDeviceStatus(name),
|
|
@@ -17687,10 +18067,10 @@ function sseResponse(transportLabel) {
|
|
|
17687
18067
|
let ping;
|
|
17688
18068
|
const stream = new ReadableStream({
|
|
17689
18069
|
start(controller) {
|
|
17690
|
-
const
|
|
18070
|
+
const enc2 = new TextEncoder;
|
|
17691
18071
|
const send = (text) => {
|
|
17692
18072
|
try {
|
|
17693
|
-
controller.enqueue(
|
|
18073
|
+
controller.enqueue(enc2.encode(text));
|
|
17694
18074
|
} catch {}
|
|
17695
18075
|
};
|
|
17696
18076
|
send(`event: hello
|
|
@@ -17978,7 +18358,7 @@ function registerPrompts(server) {
|
|
|
17978
18358
|
// package.json
|
|
17979
18359
|
var package_default = {
|
|
17980
18360
|
name: "@usex/mikrotik-mcp",
|
|
17981
|
-
version: "2.
|
|
18361
|
+
version: "2.3.0",
|
|
17982
18362
|
description: "Bun-native MCP server for MikroTik RouterOS \u2014 200+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
17983
18363
|
keywords: [
|
|
17984
18364
|
"ai",
|
|
@@ -18052,6 +18432,7 @@ var package_default = {
|
|
|
18052
18432
|
dependencies: {
|
|
18053
18433
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
18054
18434
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
18435
|
+
"@tikoci/centrs": "^0.1.0",
|
|
18055
18436
|
ssh2: "^1.17.0",
|
|
18056
18437
|
zod: "^4.4.3"
|
|
18057
18438
|
},
|
|
@@ -18310,9 +18691,9 @@ function listDevicesCli() {
|
|
|
18310
18691
|
const cfg = loadConfig();
|
|
18311
18692
|
for (const [name, d] of Object.entries(cfg.devices)) {
|
|
18312
18693
|
const tag = name === cfg.defaultDevice ? " (default)" : "";
|
|
18313
|
-
const auth = d.keyFilename || d.privateKey ? "key" : d.password ? "password" : "none";
|
|
18694
|
+
const auth = d.mac ? "mac-telnet" : d.keyFilename || d.privateKey ? "key" : d.password ? "password" : "none";
|
|
18314
18695
|
const desc = d.description ? ` \u2014 ${d.description}` : "";
|
|
18315
|
-
process.stdout.write(`${name}${tag} ${d.username}@${d
|
|
18696
|
+
process.stdout.write(`${name}${tag} ${d.username}@${describeTransport(d)} [auth: ${auth}]${desc}
|
|
18316
18697
|
`);
|
|
18317
18698
|
}
|
|
18318
18699
|
process.stdout.write(`
|
|
@@ -18320,31 +18701,22 @@ ${Object.keys(cfg.devices).length} device(s)
|
|
|
18320
18701
|
`);
|
|
18321
18702
|
}
|
|
18322
18703
|
async function probeDevice2(name, d) {
|
|
18323
|
-
const authMode = d.keyFilename || d.privateKey ? "SSH key" : "password";
|
|
18324
|
-
logger.info(`[${name}] Connecting to ${d.username}@${d
|
|
18325
|
-
const
|
|
18326
|
-
|
|
18327
|
-
|
|
18328
|
-
password: d.password,
|
|
18329
|
-
keyFilename: d.keyFilename,
|
|
18330
|
-
privateKey: d.privateKey,
|
|
18331
|
-
keyPassphrase: d.keyPassphrase,
|
|
18332
|
-
port: d.port,
|
|
18333
|
-
timeoutMs: d.timeoutMs
|
|
18334
|
-
});
|
|
18335
|
-
if (!await ssh.connect()) {
|
|
18336
|
-
logger.error(`[${name}] Connection FAILED. Check host/credentials/reachability.`);
|
|
18704
|
+
const authMode = d.mac ? "mac-telnet" : d.keyFilename || d.privateKey ? "SSH key" : "password";
|
|
18705
|
+
logger.info(`[${name}] Connecting to ${d.username}@${describeTransport(d)} (auth: ${authMode}) \u2026`);
|
|
18706
|
+
const client = createDeviceClient(d);
|
|
18707
|
+
if (!await client.connect()) {
|
|
18708
|
+
logger.error(`[${name}] Connection FAILED. ${client.lastError ?? "Check credentials/reachability."}`);
|
|
18337
18709
|
return false;
|
|
18338
18710
|
}
|
|
18339
18711
|
try {
|
|
18340
|
-
const identity = (await
|
|
18712
|
+
const identity = (await client.run("/system identity print")).trim();
|
|
18341
18713
|
process.stdout.write(`
|
|
18342
18714
|
[${name}] Connection OK.
|
|
18343
18715
|
${identity}
|
|
18344
18716
|
`);
|
|
18345
18717
|
return true;
|
|
18346
18718
|
} finally {
|
|
18347
|
-
|
|
18719
|
+
client.disconnect();
|
|
18348
18720
|
}
|
|
18349
18721
|
}
|
|
18350
18722
|
async function authCheck() {
|