@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/index.js
CHANGED
|
@@ -23,6 +23,10 @@ var DeviceConfigSchema = z.object({
|
|
|
23
23
|
privateKey: z.string().optional(),
|
|
24
24
|
keyPassphrase: z.string().optional(),
|
|
25
25
|
timeoutMs: z.coerce.number().int().positive().default(1e4),
|
|
26
|
+
mac: z.string().optional(),
|
|
27
|
+
sourceMac: z.string().optional(),
|
|
28
|
+
macHost: z.string().optional(),
|
|
29
|
+
macPort: z.coerce.number().int().positive().optional(),
|
|
26
30
|
description: z.string().optional()
|
|
27
31
|
});
|
|
28
32
|
var S3ConfigSchema = z.object({
|
|
@@ -110,7 +114,11 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
110
114
|
keyFilename: pick("key-filename", "MIKROTIK_KEY_FILENAME"),
|
|
111
115
|
privateKey: pick("private-key", "MIKROTIK_PRIVATE_KEY"),
|
|
112
116
|
keyPassphrase: pick("key-passphrase", "MIKROTIK_KEY_PASSPHRASE"),
|
|
113
|
-
timeoutMs: pick("timeout-ms", "MIKROTIK_TIMEOUT_MS")
|
|
117
|
+
timeoutMs: pick("timeout-ms", "MIKROTIK_TIMEOUT_MS"),
|
|
118
|
+
mac: pick("mac", "MIKROTIK_MAC"),
|
|
119
|
+
sourceMac: pick("source-mac", "MIKROTIK_SOURCE_MAC"),
|
|
120
|
+
macHost: pick("mac-host", "MIKROTIK_MAC_HOST"),
|
|
121
|
+
macPort: pick("mac-port", "MIKROTIK_MAC_PORT")
|
|
114
122
|
};
|
|
115
123
|
const hasSingle = Object.values(single).some((v) => v !== undefined);
|
|
116
124
|
const devices = {};
|
|
@@ -211,10 +219,6 @@ function getDevice(name) {
|
|
|
211
219
|
return dc;
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
// src/ssh/client.ts
|
|
215
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
216
|
-
import { Client } from "ssh2";
|
|
217
|
-
|
|
218
222
|
// src/logger.ts
|
|
219
223
|
import { stderr } from "process";
|
|
220
224
|
var LEVELS = {
|
|
@@ -247,7 +251,343 @@ var logger = {
|
|
|
247
251
|
error: (m) => emit("error", m)
|
|
248
252
|
};
|
|
249
253
|
|
|
254
|
+
// src/mac-telnet/console.ts
|
|
255
|
+
import { MacTelnetSession } from "@tikoci/centrs/protocols";
|
|
256
|
+
var ESC = "\x1B";
|
|
257
|
+
var enc = new TextEncoder;
|
|
258
|
+
var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
259
|
+
var ANSI_ESC2 = new RegExp(`${ESC}[@-_]`, "g");
|
|
260
|
+
var ROUTEROS_PROMPT_RE = /\[[^\]@\r\n]+@[^\]\r\n]*\][^\r\n]*>\s*$/;
|
|
261
|
+
var TICK_INTERVAL_MS = 15;
|
|
262
|
+
var LICENSE_RE = /do you want to see the software license/i;
|
|
263
|
+
function emulateScreen(text) {
|
|
264
|
+
const clean = text.replace(ANSI_CSI, "").replace(ANSI_ESC2, "");
|
|
265
|
+
const lines = [[]];
|
|
266
|
+
let row = 0;
|
|
267
|
+
let col = 0;
|
|
268
|
+
for (const ch of clean) {
|
|
269
|
+
const code = ch.charCodeAt(0);
|
|
270
|
+
if (ch === `
|
|
271
|
+
`) {
|
|
272
|
+
row += 1;
|
|
273
|
+
if (!lines[row])
|
|
274
|
+
lines[row] = [];
|
|
275
|
+
col = 0;
|
|
276
|
+
} else if (ch === "\r") {
|
|
277
|
+
col = 0;
|
|
278
|
+
} else if (code === 8) {
|
|
279
|
+
col = Math.max(0, col - 1);
|
|
280
|
+
} else if (code >= 32 && code !== 127) {
|
|
281
|
+
const line = lines[row];
|
|
282
|
+
line[col] = ch;
|
|
283
|
+
col += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return lines.map((line) => Array.from(line, (c) => c ?? " ").join("").replace(/\s+$/, ""));
|
|
287
|
+
}
|
|
288
|
+
function extractCommandOutput(raw, command) {
|
|
289
|
+
const lines = emulateScreen(raw);
|
|
290
|
+
let start = 1;
|
|
291
|
+
if (command !== undefined && lines.length > 0) {
|
|
292
|
+
const first = lines[0] ?? "";
|
|
293
|
+
const promptMatch = first.match(/^\[[^\]\r\n]*\][^\r\n]*?>\s?/);
|
|
294
|
+
const echoedOnFirst = promptMatch ? first.slice(promptMatch[0].length) : first;
|
|
295
|
+
let consumed = echoedOnFirst.length;
|
|
296
|
+
while (consumed < command.length && start < lines.length) {
|
|
297
|
+
consumed += (lines[start] ?? "").length;
|
|
298
|
+
start += 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const body = lines.slice(start);
|
|
302
|
+
while (body.length > 0) {
|
|
303
|
+
const last = body[body.length - 1];
|
|
304
|
+
if (last.length === 0 || ROUTEROS_PROMPT_RE.test(last)) {
|
|
305
|
+
body.pop();
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
return body.join(`
|
|
311
|
+
`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
class MacTelnetConsole {
|
|
315
|
+
options;
|
|
316
|
+
session;
|
|
317
|
+
buffer = "";
|
|
318
|
+
ready = false;
|
|
319
|
+
closed = false;
|
|
320
|
+
closeError;
|
|
321
|
+
waiter;
|
|
322
|
+
tickTimer;
|
|
323
|
+
readyWaiters = [];
|
|
324
|
+
decoder = new TextDecoder;
|
|
325
|
+
probeTail = "";
|
|
326
|
+
constructor(options) {
|
|
327
|
+
this.options = {
|
|
328
|
+
rows: 9999,
|
|
329
|
+
cols: 512,
|
|
330
|
+
primeTimeoutMs: 30000,
|
|
331
|
+
commandTimeoutMs: 15000,
|
|
332
|
+
settleMs: 150,
|
|
333
|
+
acceptLicense: true,
|
|
334
|
+
...options
|
|
335
|
+
};
|
|
336
|
+
this.session = new MacTelnetSession({
|
|
337
|
+
sink: options.sink,
|
|
338
|
+
sourceMac: options.sourceMac,
|
|
339
|
+
destinationMac: options.destinationMac,
|
|
340
|
+
username: options.username,
|
|
341
|
+
password: options.password,
|
|
342
|
+
sessionKey: options.sessionKey,
|
|
343
|
+
terminalType: "vt102",
|
|
344
|
+
terminalWidth: this.options.cols,
|
|
345
|
+
terminalHeight: this.options.rows,
|
|
346
|
+
onReady: () => this.onReady(),
|
|
347
|
+
onData: (bytes) => this.onData(bytes),
|
|
348
|
+
onClose: (error) => this.onClose(error)
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
handlePacket(bytes) {
|
|
352
|
+
try {
|
|
353
|
+
this.session.handlePacket(bytes);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
this.onClose(error instanceof Error ? error : new Error("Failed to process a MAC-Telnet datagram."));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async open() {
|
|
359
|
+
this.tickTimer = setInterval(() => {
|
|
360
|
+
try {
|
|
361
|
+
this.session.tick(Date.now());
|
|
362
|
+
} catch {}
|
|
363
|
+
}, TICK_INTERVAL_MS);
|
|
364
|
+
this.tickTimer.unref?.();
|
|
365
|
+
this.session.start();
|
|
366
|
+
await this.waitReady(this.options.primeTimeoutMs);
|
|
367
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer) || LICENSE_RE.test(buffer), this.options.primeTimeoutMs, "waiting for the RouterOS console prompt");
|
|
368
|
+
if (this.options.acceptLicense && LICENSE_RE.test(this.buffer)) {
|
|
369
|
+
this.buffer = "";
|
|
370
|
+
this.session.sendInput(enc.encode("n\r"));
|
|
371
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.primeTimeoutMs, "waiting for the prompt after the license screen");
|
|
372
|
+
}
|
|
373
|
+
this.buffer = "";
|
|
374
|
+
this.session.sendInput(enc.encode("\r"));
|
|
375
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.commandTimeoutMs, "waiting for a clean prompt");
|
|
376
|
+
this.buffer = "";
|
|
377
|
+
}
|
|
378
|
+
async run(cli) {
|
|
379
|
+
this.assertOpen();
|
|
380
|
+
this.buffer = "";
|
|
381
|
+
this.session.sendInput(enc.encode(`${cli}\r`));
|
|
382
|
+
await this.waitFor((buffer) => this.endsWithPrompt(buffer), this.options.commandTimeoutMs, `running over mac-telnet: ${cli}`);
|
|
383
|
+
const raw = this.buffer;
|
|
384
|
+
return { output: extractCommandOutput(raw, cli), raw };
|
|
385
|
+
}
|
|
386
|
+
close() {
|
|
387
|
+
if (this.tickTimer) {
|
|
388
|
+
clearInterval(this.tickTimer);
|
|
389
|
+
this.tickTimer = undefined;
|
|
390
|
+
}
|
|
391
|
+
if (!this.closed)
|
|
392
|
+
this.session.end();
|
|
393
|
+
}
|
|
394
|
+
get isReady() {
|
|
395
|
+
return this.ready && !this.closed;
|
|
396
|
+
}
|
|
397
|
+
onReady() {
|
|
398
|
+
this.ready = true;
|
|
399
|
+
for (const w of this.readyWaiters.splice(0))
|
|
400
|
+
w.resolve();
|
|
401
|
+
}
|
|
402
|
+
onData(bytes) {
|
|
403
|
+
const chunk = this.decoder.decode(bytes, { stream: true });
|
|
404
|
+
const combined = `${this.probeTail}${chunk}`;
|
|
405
|
+
this.answerSizeProbe(combined);
|
|
406
|
+
this.probeTail = combined.slice(-3);
|
|
407
|
+
this.buffer += chunk;
|
|
408
|
+
this.checkWaiter();
|
|
409
|
+
}
|
|
410
|
+
onClose(error) {
|
|
411
|
+
this.closed = true;
|
|
412
|
+
this.closeError = error;
|
|
413
|
+
if (this.tickTimer) {
|
|
414
|
+
clearInterval(this.tickTimer);
|
|
415
|
+
this.tickTimer = undefined;
|
|
416
|
+
}
|
|
417
|
+
const failure = error ?? new Error("The MAC-Telnet console session closed.");
|
|
418
|
+
for (const w of this.readyWaiters.splice(0))
|
|
419
|
+
w.reject(failure);
|
|
420
|
+
if (this.waiter) {
|
|
421
|
+
const waiter = this.waiter;
|
|
422
|
+
this.waiter = undefined;
|
|
423
|
+
clearTimeout(waiter.timeout);
|
|
424
|
+
if (waiter.settle)
|
|
425
|
+
clearTimeout(waiter.settle);
|
|
426
|
+
waiter.reject(failure);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
answerSizeProbe(chunk) {
|
|
430
|
+
if (chunk.includes(`${ESC}[6n`)) {
|
|
431
|
+
this.session.sendInput(enc.encode(`${ESC}[${this.options.rows};${this.options.cols}R`));
|
|
432
|
+
}
|
|
433
|
+
if (chunk.includes(`${ESC}Z`) || chunk.includes(`${ESC}[c`)) {
|
|
434
|
+
this.session.sendInput(enc.encode(`${ESC}[?6c`));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
endsWithPrompt(buffer) {
|
|
438
|
+
const lines = emulateScreen(buffer).filter((line) => line.length > 0);
|
|
439
|
+
return ROUTEROS_PROMPT_RE.test(lines[lines.length - 1] ?? "");
|
|
440
|
+
}
|
|
441
|
+
waitReady(timeoutMs) {
|
|
442
|
+
if (this.ready)
|
|
443
|
+
return Promise.resolve();
|
|
444
|
+
if (this.closed) {
|
|
445
|
+
return Promise.reject(this.closeError ?? new Error("The MAC-Telnet session closed before login completed."));
|
|
446
|
+
}
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
let timer;
|
|
449
|
+
const entry = {
|
|
450
|
+
resolve: () => {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
resolve();
|
|
453
|
+
},
|
|
454
|
+
reject: (error) => {
|
|
455
|
+
clearTimeout(timer);
|
|
456
|
+
reject(error);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
timer = setTimeout(() => {
|
|
460
|
+
this.readyWaiters = this.readyWaiters.filter((w) => w !== entry);
|
|
461
|
+
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."));
|
|
462
|
+
}, timeoutMs);
|
|
463
|
+
this.readyWaiters.push(entry);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
waitFor(predicate, timeoutMs, label) {
|
|
467
|
+
if (this.closed) {
|
|
468
|
+
return Promise.reject(this.closeError ?? new Error(`MAC-Telnet session closed while ${label}.`));
|
|
469
|
+
}
|
|
470
|
+
return new Promise((resolve, reject) => {
|
|
471
|
+
const timeout = setTimeout(() => {
|
|
472
|
+
this.waiter = undefined;
|
|
473
|
+
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."));
|
|
474
|
+
}, timeoutMs);
|
|
475
|
+
this.waiter = { predicate, resolve, reject, timeout };
|
|
476
|
+
this.checkWaiter();
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
checkWaiter() {
|
|
480
|
+
const waiter = this.waiter;
|
|
481
|
+
if (!waiter)
|
|
482
|
+
return;
|
|
483
|
+
if (!waiter.predicate(this.buffer)) {
|
|
484
|
+
if (waiter.settle) {
|
|
485
|
+
clearTimeout(waiter.settle);
|
|
486
|
+
waiter.settle = undefined;
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (waiter.settle)
|
|
491
|
+
clearTimeout(waiter.settle);
|
|
492
|
+
waiter.settle = setTimeout(() => {
|
|
493
|
+
if (this.waiter !== waiter)
|
|
494
|
+
return;
|
|
495
|
+
this.waiter = undefined;
|
|
496
|
+
clearTimeout(waiter.timeout);
|
|
497
|
+
waiter.resolve();
|
|
498
|
+
}, this.options.settleMs);
|
|
499
|
+
}
|
|
500
|
+
assertOpen() {
|
|
501
|
+
if (!this.ready || this.closed) {
|
|
502
|
+
throw new Error("The MAC-Telnet console is not open. Call open() and await it before running commands.");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/mac-telnet/client.ts
|
|
508
|
+
import {
|
|
509
|
+
DEFAULT_MAC_TELNET_BROADCAST,
|
|
510
|
+
isBroadcastHost,
|
|
511
|
+
MAC_TELNET_PORT,
|
|
512
|
+
createUdpMacTelnetTransport,
|
|
513
|
+
parseMac,
|
|
514
|
+
resolveMacTelnetRoute
|
|
515
|
+
} from "@tikoci/centrs/protocols";
|
|
516
|
+
|
|
517
|
+
class MikroTikMacTelnetClient {
|
|
518
|
+
transport = null;
|
|
519
|
+
console = null;
|
|
520
|
+
opts;
|
|
521
|
+
lastError;
|
|
522
|
+
constructor(opts) {
|
|
523
|
+
this.opts = {
|
|
524
|
+
port: MAC_TELNET_PORT,
|
|
525
|
+
timeoutMs: 1e4,
|
|
526
|
+
...opts
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
async connect() {
|
|
530
|
+
this.lastError = undefined;
|
|
531
|
+
try {
|
|
532
|
+
const destinationMac = parseMac(this.opts.mac);
|
|
533
|
+
const explicitSourceMac = this.opts.sourceMac ? parseMac(this.opts.sourceMac) : undefined;
|
|
534
|
+
const host = this.opts.host ?? DEFAULT_MAC_TELNET_BROADCAST;
|
|
535
|
+
const route = await resolveMacTelnetRoute({
|
|
536
|
+
destinationMac,
|
|
537
|
+
host,
|
|
538
|
+
port: this.opts.port,
|
|
539
|
+
timeoutMs: this.opts.timeoutMs,
|
|
540
|
+
explicitSourceMac
|
|
541
|
+
});
|
|
542
|
+
const transport = createUdpMacTelnetTransport({
|
|
543
|
+
host: route.host,
|
|
544
|
+
port: this.opts.port,
|
|
545
|
+
broadcast: isBroadcastHost(route.host)
|
|
546
|
+
});
|
|
547
|
+
this.transport = transport;
|
|
548
|
+
await transport.ready();
|
|
549
|
+
const console = new MacTelnetConsole({
|
|
550
|
+
sink: transport,
|
|
551
|
+
sourceMac: route.sourceMac,
|
|
552
|
+
destinationMac,
|
|
553
|
+
username: this.opts.username,
|
|
554
|
+
password: this.opts.password ?? "",
|
|
555
|
+
primeTimeoutMs: Math.max(this.opts.timeoutMs, 30000),
|
|
556
|
+
commandTimeoutMs: Math.max(this.opts.timeoutMs, 15000)
|
|
557
|
+
});
|
|
558
|
+
this.console = console;
|
|
559
|
+
transport.onMessage((bytes) => console.handlePacket(bytes));
|
|
560
|
+
await console.open();
|
|
561
|
+
return true;
|
|
562
|
+
} catch (e) {
|
|
563
|
+
this.lastError = e instanceof Error ? e.message : String(e);
|
|
564
|
+
logger.error(`Failed to connect to MikroTik over MAC-Telnet: ${this.lastError}`);
|
|
565
|
+
this.disconnect();
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async run(command) {
|
|
570
|
+
if (!this.console || !this.console.isReady) {
|
|
571
|
+
throw new Error("Not connected to MikroTik device (MAC-Telnet)");
|
|
572
|
+
}
|
|
573
|
+
const { output } = await this.console.run(command);
|
|
574
|
+
return output;
|
|
575
|
+
}
|
|
576
|
+
disconnect() {
|
|
577
|
+
try {
|
|
578
|
+
this.console?.close();
|
|
579
|
+
} catch {}
|
|
580
|
+
try {
|
|
581
|
+
this.transport?.close();
|
|
582
|
+
} catch {}
|
|
583
|
+
this.console = null;
|
|
584
|
+
this.transport = null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
250
588
|
// src/ssh/client.ts
|
|
589
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
590
|
+
import { Client } from "ssh2";
|
|
251
591
|
function decodeOutput(data) {
|
|
252
592
|
if (!data || data.length === 0)
|
|
253
593
|
return "";
|
|
@@ -347,6 +687,42 @@ class MikroTikSSHClient {
|
|
|
347
687
|
}
|
|
348
688
|
}
|
|
349
689
|
|
|
690
|
+
// src/core/transport.ts
|
|
691
|
+
function isMacTelnetDevice(dc) {
|
|
692
|
+
return Boolean(dc.mac);
|
|
693
|
+
}
|
|
694
|
+
function createDeviceClient(dc) {
|
|
695
|
+
if (isMacTelnetDevice(dc)) {
|
|
696
|
+
return new MikroTikMacTelnetClient({
|
|
697
|
+
mac: dc.mac,
|
|
698
|
+
username: dc.username,
|
|
699
|
+
password: dc.password,
|
|
700
|
+
sourceMac: dc.sourceMac,
|
|
701
|
+
host: dc.macHost,
|
|
702
|
+
port: dc.macPort,
|
|
703
|
+
timeoutMs: dc.timeoutMs
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return new MikroTikSSHClient({
|
|
707
|
+
host: dc.host,
|
|
708
|
+
username: dc.username,
|
|
709
|
+
password: dc.password,
|
|
710
|
+
keyFilename: dc.keyFilename,
|
|
711
|
+
privateKey: dc.privateKey,
|
|
712
|
+
keyPassphrase: dc.keyPassphrase,
|
|
713
|
+
port: dc.port,
|
|
714
|
+
timeoutMs: dc.timeoutMs
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function connectErrorMessage(name, dc, lastError) {
|
|
718
|
+
const reason = lastError ? ` \u2014 ${lastError}` : "";
|
|
719
|
+
if (dc.mac) {
|
|
720
|
+
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.";
|
|
721
|
+
}
|
|
722
|
+
const authMode = dc.keyFilename || dc.privateKey ? "SSH key" : dc.password ? "password" : "no credentials";
|
|
723
|
+
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.";
|
|
724
|
+
}
|
|
725
|
+
|
|
350
726
|
// src/ssh/safe-mode.ts
|
|
351
727
|
var PROMPT_RE = /\[.+?@.+?\] (?:<SAFE> )?> ?$/m;
|
|
352
728
|
var ANSI_RE = /\x1B(?:\[[0-9;]*[mA-HJ-MSTfhilnprsu]|[()][0-9A-Za-z]|\[?\?\d+[hl])/g;
|
|
@@ -381,6 +757,9 @@ class SafeModeManager {
|
|
|
381
757
|
if (this.active)
|
|
382
758
|
return "Safe mode is already active.";
|
|
383
759
|
const dc = getDevice(this.deviceName);
|
|
760
|
+
if (dc.mac) {
|
|
761
|
+
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.";
|
|
762
|
+
}
|
|
384
763
|
const ssh = new MikroTikSSHClient({
|
|
385
764
|
host: dc.host,
|
|
386
765
|
username: dc.username,
|
|
@@ -515,25 +894,14 @@ function getSafeModeManager(deviceName) {
|
|
|
515
894
|
async function runOnce(command, deviceName) {
|
|
516
895
|
const name = resolveDeviceName(deviceName);
|
|
517
896
|
const dc = getDevice(deviceName);
|
|
518
|
-
const
|
|
519
|
-
host: dc.host,
|
|
520
|
-
username: dc.username,
|
|
521
|
-
password: dc.password,
|
|
522
|
-
keyFilename: dc.keyFilename,
|
|
523
|
-
privateKey: dc.privateKey,
|
|
524
|
-
keyPassphrase: dc.keyPassphrase,
|
|
525
|
-
port: dc.port,
|
|
526
|
-
timeoutMs: dc.timeoutMs
|
|
527
|
-
});
|
|
897
|
+
const client = createDeviceClient(dc);
|
|
528
898
|
try {
|
|
529
|
-
if (!await
|
|
530
|
-
|
|
531
|
-
const reason = ssh.lastError ? ` \u2014 ${ssh.lastError}` : "";
|
|
532
|
-
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.");
|
|
899
|
+
if (!await client.connect()) {
|
|
900
|
+
throw new Error(connectErrorMessage(name, dc, client.lastError));
|
|
533
901
|
}
|
|
534
|
-
return await
|
|
902
|
+
return await client.run(command);
|
|
535
903
|
} finally {
|
|
536
|
-
|
|
904
|
+
client.disconnect();
|
|
537
905
|
}
|
|
538
906
|
}
|
|
539
907
|
async function executeMikrotikCommand(command, ctx) {
|
|
@@ -17343,7 +17711,7 @@ var allToolModules = moduleCatalog.map((m) => m.tools);
|
|
|
17343
17711
|
// package.json
|
|
17344
17712
|
var package_default = {
|
|
17345
17713
|
name: "@usex/mikrotik-mcp",
|
|
17346
|
-
version: "2.
|
|
17714
|
+
version: "2.3.0",
|
|
17347
17715
|
description: "Bun-native MCP server for MikroTik RouterOS \u2014 200+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
17348
17716
|
keywords: [
|
|
17349
17717
|
"ai",
|
|
@@ -17417,6 +17785,7 @@ var package_default = {
|
|
|
17417
17785
|
dependencies: {
|
|
17418
17786
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
17419
17787
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
17788
|
+
"@tikoci/centrs": "^0.1.0",
|
|
17420
17789
|
ssh2: "^1.17.0",
|
|
17421
17790
|
zod: "^4.4.3"
|
|
17422
17791
|
},
|