@usex/mikrotik-mcp 3.21.0 → 3.23.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 +96 -54
- package/dist/index.d.ts +38 -12
- package/dist/index.js +96 -54
- package/dist/ui/dashboard.html +1 -0
- package/dist/ui/firewall-audit.html +1 -0
- package/dist/ui/firewall.html +1 -0
- package/dist/ui/interfaces.html +1 -0
- package/dist/ui/records.html +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -239,13 +239,38 @@ function getConfig() {
|
|
|
239
239
|
function listDevices() {
|
|
240
240
|
return { names: Object.keys(active.devices), default: active.defaultDevice };
|
|
241
241
|
}
|
|
242
|
+
function deviceKeyForLabel(name) {
|
|
243
|
+
const target = name.trim().toLowerCase();
|
|
244
|
+
for (const [key, dc] of Object.entries(active.devices)) {
|
|
245
|
+
if (dc.description && dc.description.trim().toLowerCase() === target)
|
|
246
|
+
return key;
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
function deviceLabels() {
|
|
251
|
+
const seen = new Set;
|
|
252
|
+
const out = [];
|
|
253
|
+
for (const [key, dc] of Object.entries(active.devices)) {
|
|
254
|
+
const label = dc.description?.trim();
|
|
255
|
+
if (label && label !== key && !(label in active.devices) && !seen.has(label)) {
|
|
256
|
+
seen.add(label);
|
|
257
|
+
out.push(label);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
242
262
|
function resolveDeviceName(name) {
|
|
243
|
-
if (name
|
|
244
|
-
|
|
263
|
+
if (name) {
|
|
264
|
+
if (name in active.devices)
|
|
265
|
+
return name;
|
|
266
|
+
const byLabel = deviceKeyForLabel(name);
|
|
267
|
+
if (byLabel)
|
|
268
|
+
return byLabel;
|
|
269
|
+
}
|
|
245
270
|
return active.defaultDevice in active.devices ? active.defaultDevice : Object.keys(active.devices)[0] ?? active.defaultDevice;
|
|
246
271
|
}
|
|
247
272
|
function getDevice(name) {
|
|
248
|
-
if (name && !(name in active.devices)) {
|
|
273
|
+
if (name && !(name in active.devices) && !deviceKeyForLabel(name)) {
|
|
249
274
|
throw new Error(`Unknown device '${name}'. Configured devices: ${Object.keys(active.devices).join(", ")}`);
|
|
250
275
|
}
|
|
251
276
|
const key = resolveDeviceName(name);
|
|
@@ -894,31 +919,39 @@ class SafeModeManager {
|
|
|
894
919
|
}
|
|
895
920
|
commit() {
|
|
896
921
|
return this.lock(async () => {
|
|
897
|
-
if (!this.active || !this.channel)
|
|
898
|
-
return "Safe mode is not active. Nothing to commit.";
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const probe = await this.readSettledPrompt();
|
|
902
|
-
const before = classifyPrompt(probe);
|
|
922
|
+
if (!this.active || !this.channel) {
|
|
923
|
+
return { ok: true, message: "Safe mode is not active. Nothing to commit." };
|
|
924
|
+
}
|
|
925
|
+
const before = await this.probeMode();
|
|
903
926
|
if (before === "released") {
|
|
904
927
|
this.cleanup();
|
|
905
|
-
return
|
|
928
|
+
return {
|
|
929
|
+
ok: true,
|
|
930
|
+
message: "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED."
|
|
931
|
+
};
|
|
906
932
|
}
|
|
907
933
|
if (before === "unknown") {
|
|
908
|
-
return
|
|
934
|
+
return {
|
|
935
|
+
ok: false,
|
|
936
|
+
message: "Could not read a prompt to determine Safe Mode state. The session is left open so " + "nothing is reverted \u2014 call get_safe_mode_status, retry commit_safe_mode, or rollback_safe_mode."
|
|
937
|
+
};
|
|
909
938
|
}
|
|
910
939
|
this.channel.write(CTRL_X);
|
|
911
|
-
this.
|
|
912
|
-
|
|
913
|
-
const after = await this.readSettledPrompt();
|
|
914
|
-
switch (classifyPrompt(after)) {
|
|
940
|
+
const after = await this.probeMode();
|
|
941
|
+
switch (after) {
|
|
915
942
|
case "released":
|
|
916
943
|
this.cleanup();
|
|
917
|
-
return "Changes committed successfully. Safe mode DISABLED.";
|
|
944
|
+
return { ok: true, message: "Changes committed successfully. Safe mode DISABLED." };
|
|
918
945
|
case "safe":
|
|
919
|
-
return
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
message: "Commit NOT completed \u2014 the device is still in Safe Mode (the Ctrl+X commit did not " + "take). Your changes remain held in memory and are NOT yet saved; call commit_safe_mode " + "again to retry, or rollback_safe_mode to discard them. If retries keep failing, this " + "RouterOS build may not accept an interactive Safe Mode commit over SSH \u2014 apply the " + "change without Safe Mode instead."
|
|
949
|
+
};
|
|
920
950
|
default:
|
|
921
|
-
return
|
|
951
|
+
return {
|
|
952
|
+
ok: false,
|
|
953
|
+
message: "Commit status unclear \u2014 no prompt seen after the commit. The session is left open so " + "nothing is reverted; verify with get_safe_mode_status or retry commit_safe_mode."
|
|
954
|
+
};
|
|
922
955
|
}
|
|
923
956
|
});
|
|
924
957
|
}
|
|
@@ -955,32 +988,17 @@ class SafeModeManager {
|
|
|
955
988
|
channel.on("data", onData);
|
|
956
989
|
});
|
|
957
990
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
clearTimeout(hard);
|
|
968
|
-
if (quiet)
|
|
969
|
-
clearTimeout(quiet);
|
|
970
|
-
channel.removeListener("data", onData);
|
|
971
|
-
resolve2(stripAnsi(buf));
|
|
972
|
-
}
|
|
973
|
-
function onData(chunk) {
|
|
974
|
-
buf += decodeOutput(chunk);
|
|
975
|
-
if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
|
|
976
|
-
if (quiet)
|
|
977
|
-
clearTimeout(quiet);
|
|
978
|
-
quiet = setTimeout(done, quietMs);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
hard = setTimeout(done, maxMs);
|
|
982
|
-
channel.on("data", onData);
|
|
991
|
+
async probeMode() {
|
|
992
|
+
if (!this.channel)
|
|
993
|
+
return "unknown";
|
|
994
|
+
const token = "__MCP_SAFEMODE_PROBE__";
|
|
995
|
+
this.channel.write(`:put "${token}"
|
|
996
|
+
`);
|
|
997
|
+
const out = await this.readUntilPrompt(8000, (cleaned) => {
|
|
998
|
+
const i = cleaned.lastIndexOf(token);
|
|
999
|
+
return i >= 0 && PROMPT_RE.test(cleaned.slice(i));
|
|
983
1000
|
});
|
|
1001
|
+
return classifyPrompt(out);
|
|
984
1002
|
}
|
|
985
1003
|
extractOutput(raw, command) {
|
|
986
1004
|
const text = raw.replace(/\r\n/g, `
|
|
@@ -1654,6 +1672,7 @@ function recordToolCall(raw) {
|
|
|
1654
1672
|
|
|
1655
1673
|
// src/core/registry.ts
|
|
1656
1674
|
var AUTO_RECORDS_VERB = /^(list|get|show|print)_/;
|
|
1675
|
+
var UI_OUTPUT_SCHEMA = z2.object({}).passthrough();
|
|
1657
1676
|
function effectiveUi(def) {
|
|
1658
1677
|
if (def.ui)
|
|
1659
1678
|
return { ui: def.ui, auto: false };
|
|
@@ -1694,12 +1713,13 @@ function defineTool(def) {
|
|
|
1694
1713
|
inputSchema: def.inputSchema,
|
|
1695
1714
|
ui: def.ui,
|
|
1696
1715
|
register(server, opts = {}) {
|
|
1697
|
-
const { sendLog, deviceNames } = opts;
|
|
1716
|
+
const { sendLog, deviceNames, deviceAliases } = opts;
|
|
1698
1717
|
const multiDevice = !!deviceNames && deviceNames.length > 1;
|
|
1699
1718
|
const { ui, auto } = effectiveUi(def);
|
|
1719
|
+
const selectorNames = multiDevice && deviceNames ? [...new Set([...deviceNames, ...deviceAliases ?? []])] : [];
|
|
1700
1720
|
const inputSchema = multiDevice ? {
|
|
1701
1721
|
...def.inputSchema,
|
|
1702
|
-
device: z2.enum(
|
|
1722
|
+
device: z2.enum(selectorNames).optional().describe(`Which configured MikroTik device to run this on. One of: ${selectorNames.join(", ")} (a config key or its label). Omit to use the default device.`)
|
|
1703
1723
|
} : def.inputSchema;
|
|
1704
1724
|
const risk = riskOf(def.annotations);
|
|
1705
1725
|
const callback = async (args) => {
|
|
@@ -1722,7 +1742,10 @@ function defineTool(def) {
|
|
|
1722
1742
|
return { content: [{ type: "text", text: out.text }], isError: true };
|
|
1723
1743
|
}
|
|
1724
1744
|
if (auto && !out.structuredContent) {
|
|
1725
|
-
|
|
1745
|
+
const view = buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
|
|
1746
|
+
if (view.rows.length > 0) {
|
|
1747
|
+
out.structuredContent = view;
|
|
1748
|
+
}
|
|
1726
1749
|
}
|
|
1727
1750
|
const result = {
|
|
1728
1751
|
content: [{ type: "text", text: out.text }]
|
|
@@ -1765,7 +1788,8 @@ function defineTool(def) {
|
|
|
1765
1788
|
description: def.description,
|
|
1766
1789
|
inputSchema,
|
|
1767
1790
|
annotations: { ...def.annotations, title: def.title },
|
|
1768
|
-
...ui ? { _meta: toolUiMeta(ui) } : {}
|
|
1791
|
+
...ui ? { _meta: toolUiMeta(ui) } : {},
|
|
1792
|
+
...ui && !auto ? { outputSchema: UI_OUTPUT_SCHEMA } : {}
|
|
1769
1793
|
}, callback);
|
|
1770
1794
|
}
|
|
1771
1795
|
};
|
|
@@ -2632,11 +2656,19 @@ async function restoreLocalBackup(device, name, confirm) {
|
|
|
2632
2656
|
return fail("device stopped responding after applying \u2014 rolled back to avoid a lock-out", applied);
|
|
2633
2657
|
}
|
|
2634
2658
|
const committed = await safe.commit();
|
|
2659
|
+
if (!committed.ok) {
|
|
2660
|
+
return {
|
|
2661
|
+
ok: false,
|
|
2662
|
+
applied,
|
|
2663
|
+
committed: false,
|
|
2664
|
+
message: `applied ${applied} command(s) but COMMIT FAILED \u2014 not saved: ${committed.message}`
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2635
2667
|
return {
|
|
2636
2668
|
ok: true,
|
|
2637
2669
|
applied,
|
|
2638
2670
|
committed: true,
|
|
2639
|
-
message: `committed ${applied} command(s). ${committed}`
|
|
2671
|
+
message: `committed ${applied} command(s). ${committed.message}`
|
|
2640
2672
|
};
|
|
2641
2673
|
} catch (e) {
|
|
2642
2674
|
await safe.rollback();
|
|
@@ -3870,9 +3902,14 @@ DRY-RUN: all changes rolled back. Re-run with confirm=true to commit.`;
|
|
|
3870
3902
|
ABORTED: the device stopped responding after applying \u2014 changes rolled back to avoid a lock-out.`;
|
|
3871
3903
|
}
|
|
3872
3904
|
const committed = await safe.commit();
|
|
3905
|
+
if (!committed.ok) {
|
|
3906
|
+
return `${header}
|
|
3907
|
+
|
|
3908
|
+
COMMIT FAILED \u2014 changes are NOT saved (still pending in Safe Mode). ` + `${committed.message}`;
|
|
3909
|
+
}
|
|
3873
3910
|
return `${header}
|
|
3874
3911
|
|
|
3875
|
-
COMMITTED: changes are now permanent. ${committed}`;
|
|
3912
|
+
COMMITTED: changes are now permanent. ${committed.message}`;
|
|
3876
3913
|
} catch (e) {
|
|
3877
3914
|
await safe.rollback();
|
|
3878
3915
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -5935,8 +5972,12 @@ ${useSafe ? "Safe Mode rolled back \u2014 no changes kept." : "Partial rules rem
|
|
|
5935
5972
|
}
|
|
5936
5973
|
done.push(cmd);
|
|
5937
5974
|
}
|
|
5938
|
-
if (useSafe)
|
|
5939
|
-
await mgr.commit();
|
|
5975
|
+
if (useSafe) {
|
|
5976
|
+
const c = await mgr.commit();
|
|
5977
|
+
if (!c.ok) {
|
|
5978
|
+
return `Applied ${done.length} rule(s) but Safe Mode COMMIT FAILED \u2014 changes are NOT saved ` + `and will revert: ${c.message}`;
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5940
5981
|
return `Security Shield applied \u2014 ${done.length} rule(s) across ${groups.length} protection(s)${useSafe ? " (committed via Safe Mode)" : ""}. Audit with audit_firewall_hardening; undo with remove_firewall_hardening.`;
|
|
5941
5982
|
}
|
|
5942
5983
|
}),
|
|
@@ -13512,7 +13553,7 @@ ${redactSecrets(result)}`;
|
|
|
13512
13553
|
|
|
13513
13554
|
// src/tools/poe.ts
|
|
13514
13555
|
import { z as z69 } from "zod";
|
|
13515
|
-
var NO_POE = "This device
|
|
13556
|
+
var NO_POE = "This device has no PoE-OUT hardware, so the `/interface ethernet poe` menu " + "(used to SUPPLY power to other devices) is not available \u2014 this is normal, not an error. " + "Note: PoE-OUT (this router powering other devices) is different from PoE-IN (this router being " + "powered over Ethernet). If your device is PoE-powered, read its PoE-IN voltage/state with " + "get_system_health (`/system health`), not the PoE tools.";
|
|
13516
13557
|
var poeTools = [
|
|
13517
13558
|
defineTool({
|
|
13518
13559
|
name: "get_poe_monitor",
|
|
@@ -17583,7 +17624,7 @@ var safeModeTools = [
|
|
|
17583
17624
|
async handler(_a, ctx) {
|
|
17584
17625
|
const device = resolveDeviceName(ctx.device);
|
|
17585
17626
|
ctx.info(`[${device}] Committing safe mode changes`);
|
|
17586
|
-
return getSafeModeManager(device).commit();
|
|
17627
|
+
return (await getSafeModeManager(device).commit()).message;
|
|
17587
17628
|
}
|
|
17588
17629
|
}),
|
|
17589
17630
|
defineTool({
|
|
@@ -23951,7 +23992,7 @@ function registerPrompts(server) {
|
|
|
23951
23992
|
// package.json
|
|
23952
23993
|
var package_default = {
|
|
23953
23994
|
name: "@usex/mikrotik-mcp",
|
|
23954
|
-
version: "3.
|
|
23995
|
+
version: "3.23.0",
|
|
23955
23996
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
23956
23997
|
keywords: [
|
|
23957
23998
|
"ai",
|
|
@@ -24155,6 +24196,7 @@ function createServer(opts = {}) {
|
|
|
24155
24196
|
const toolCount = registerTools(server, toolModules, {
|
|
24156
24197
|
sendLog,
|
|
24157
24198
|
deviceNames: names,
|
|
24199
|
+
deviceAliases: deviceLabels(),
|
|
24158
24200
|
readOnly
|
|
24159
24201
|
});
|
|
24160
24202
|
const promptCount = registerPrompts(server);
|
package/dist/index.d.ts
CHANGED
|
@@ -49,9 +49,16 @@ interface UiLink {
|
|
|
49
49
|
/** Options threaded into every tool registration. */
|
|
50
50
|
interface RegisterOptions {
|
|
51
51
|
sendLog?: SendLog;
|
|
52
|
-
/** Configured device
|
|
52
|
+
/** Configured device KEYS; when more than one, a `device` selector is injected. */
|
|
53
53
|
deviceNames?: string[];
|
|
54
54
|
/**
|
|
55
|
+
* Extra accepted values for the `device` selector — a device's free-text
|
|
56
|
+
* label (`description`, e.g. "Ali Home") so the AI can target it by the
|
|
57
|
+
* friendly name as well as its config key. Resolved back to the key by
|
|
58
|
+
* {@link resolveDeviceName}. Does NOT affect the single-vs-multi decision.
|
|
59
|
+
*/
|
|
60
|
+
deviceAliases?: string[];
|
|
61
|
+
/**
|
|
55
62
|
* Read-only mode: register only tools annotated `readOnlyHint`. Used to
|
|
56
63
|
* withhold every write/destructive tool from a publicly-exposed surface (e.g.
|
|
57
64
|
* a ChatGPT Apps connector) until authentication is in place.
|
|
@@ -110,11 +117,15 @@ declare function listDevices(): {
|
|
|
110
117
|
names: string[];
|
|
111
118
|
default: string;
|
|
112
119
|
};
|
|
113
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a (possibly undefined) device name to a concrete, existing config key.
|
|
122
|
+
* Accepts a config key OR a device's free-text label; falls back to the default.
|
|
123
|
+
*/
|
|
114
124
|
declare function resolveDeviceName(name?: string): string;
|
|
115
125
|
/**
|
|
116
|
-
* Return the connection config for a device by
|
|
117
|
-
* `name` is undefined). Throws if an explicit
|
|
126
|
+
* Return the connection config for a device by key or label (or the default when
|
|
127
|
+
* `name` is undefined). Throws if an explicit name matches neither a key nor a
|
|
128
|
+
* label.
|
|
118
129
|
*/
|
|
119
130
|
declare function getDevice(name?: string): DeviceConfig;
|
|
120
131
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -163,6 +174,16 @@ declare class MikroTikSSHClient {
|
|
|
163
174
|
/** Close the SSH connection. Safe to call multiple times. */
|
|
164
175
|
disconnect(): void;
|
|
165
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Outcome of {@link SafeModeManager.commit}. `ok` is true ONLY when Safe Mode is
|
|
179
|
+
* confirmed exited (changes persisted) — callers must check it before telling
|
|
180
|
+
* the user anything was committed, so a failed commit can never masquerade as a
|
|
181
|
+
* success that then silently reverts.
|
|
182
|
+
*/
|
|
183
|
+
interface CommitResult {
|
|
184
|
+
ok: boolean;
|
|
185
|
+
message: string;
|
|
186
|
+
}
|
|
166
187
|
declare class SafeModeManager {
|
|
167
188
|
private readonly deviceName;
|
|
168
189
|
private ssh;
|
|
@@ -179,8 +200,12 @@ declare class SafeModeManager {
|
|
|
179
200
|
enable(): Promise<string>;
|
|
180
201
|
/** Execute a command through the safe-mode persistent shell session. */
|
|
181
202
|
execute(command: string): Promise<string>;
|
|
182
|
-
/**
|
|
183
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Send Ctrl+X again to exit Safe Mode and persist all changes. Returns a
|
|
205
|
+
* structured result so callers NEVER report "committed" on a failed commit —
|
|
206
|
+
* `ok` is true only when Safe Mode is confirmed exited (changes saved).
|
|
207
|
+
*/
|
|
208
|
+
commit(): Promise<CommitResult>;
|
|
184
209
|
/** Close the session to trigger MikroTik's automatic safe-mode revert. */
|
|
185
210
|
rollback(): Promise<string>;
|
|
186
211
|
status(): string;
|
|
@@ -192,13 +217,14 @@ declare class SafeModeManager {
|
|
|
192
217
|
*/
|
|
193
218
|
private readUntilPrompt;
|
|
194
219
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* a
|
|
220
|
+
* Determine the shell's CURRENT mode definitively by round-tripping a sentinel
|
|
221
|
+
* command (`:put "<token>"`) and classifying the prompt that follows ITS
|
|
222
|
+
* output. A real command — not a bare Enter — forces RouterOS to fully process
|
|
223
|
+
* any pending Ctrl+X and render a prompt that reflects the true mode, so we
|
|
224
|
+
* never settle on a transient `<SAFE>`→normal redraw and report a commit that
|
|
225
|
+
* did not actually take. Returns `safe`, `released`, or `unknown` (no prompt).
|
|
200
226
|
*/
|
|
201
|
-
private
|
|
227
|
+
private probeMode;
|
|
202
228
|
private extractOutput;
|
|
203
229
|
private cleanup;
|
|
204
230
|
}
|
package/dist/index.js
CHANGED
|
@@ -231,13 +231,38 @@ function getConfig() {
|
|
|
231
231
|
function listDevices() {
|
|
232
232
|
return { names: Object.keys(active.devices), default: active.defaultDevice };
|
|
233
233
|
}
|
|
234
|
+
function deviceKeyForLabel(name) {
|
|
235
|
+
const target = name.trim().toLowerCase();
|
|
236
|
+
for (const [key, dc] of Object.entries(active.devices)) {
|
|
237
|
+
if (dc.description && dc.description.trim().toLowerCase() === target)
|
|
238
|
+
return key;
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
function deviceLabels() {
|
|
243
|
+
const seen = new Set;
|
|
244
|
+
const out = [];
|
|
245
|
+
for (const [key, dc] of Object.entries(active.devices)) {
|
|
246
|
+
const label = dc.description?.trim();
|
|
247
|
+
if (label && label !== key && !(label in active.devices) && !seen.has(label)) {
|
|
248
|
+
seen.add(label);
|
|
249
|
+
out.push(label);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
234
254
|
function resolveDeviceName(name) {
|
|
235
|
-
if (name
|
|
236
|
-
|
|
255
|
+
if (name) {
|
|
256
|
+
if (name in active.devices)
|
|
257
|
+
return name;
|
|
258
|
+
const byLabel = deviceKeyForLabel(name);
|
|
259
|
+
if (byLabel)
|
|
260
|
+
return byLabel;
|
|
261
|
+
}
|
|
237
262
|
return active.defaultDevice in active.devices ? active.defaultDevice : Object.keys(active.devices)[0] ?? active.defaultDevice;
|
|
238
263
|
}
|
|
239
264
|
function getDevice(name) {
|
|
240
|
-
if (name && !(name in active.devices)) {
|
|
265
|
+
if (name && !(name in active.devices) && !deviceKeyForLabel(name)) {
|
|
241
266
|
throw new Error(`Unknown device '${name}'. Configured devices: ${Object.keys(active.devices).join(", ")}`);
|
|
242
267
|
}
|
|
243
268
|
const key = resolveDeviceName(name);
|
|
@@ -880,31 +905,39 @@ class SafeModeManager {
|
|
|
880
905
|
}
|
|
881
906
|
commit() {
|
|
882
907
|
return this.lock(async () => {
|
|
883
|
-
if (!this.active || !this.channel)
|
|
884
|
-
return "Safe mode is not active. Nothing to commit.";
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const probe = await this.readSettledPrompt();
|
|
888
|
-
const before = classifyPrompt(probe);
|
|
908
|
+
if (!this.active || !this.channel) {
|
|
909
|
+
return { ok: true, message: "Safe mode is not active. Nothing to commit." };
|
|
910
|
+
}
|
|
911
|
+
const before = await this.probeMode();
|
|
889
912
|
if (before === "released") {
|
|
890
913
|
this.cleanup();
|
|
891
|
-
return
|
|
914
|
+
return {
|
|
915
|
+
ok: true,
|
|
916
|
+
message: "Safe mode already exited \u2014 your changes are committed. Safe mode DISABLED."
|
|
917
|
+
};
|
|
892
918
|
}
|
|
893
919
|
if (before === "unknown") {
|
|
894
|
-
return
|
|
920
|
+
return {
|
|
921
|
+
ok: false,
|
|
922
|
+
message: "Could not read a prompt to determine Safe Mode state. The session is left open so " + "nothing is reverted \u2014 call get_safe_mode_status, retry commit_safe_mode, or rollback_safe_mode."
|
|
923
|
+
};
|
|
895
924
|
}
|
|
896
925
|
this.channel.write(CTRL_X);
|
|
897
|
-
this.
|
|
898
|
-
|
|
899
|
-
const after = await this.readSettledPrompt();
|
|
900
|
-
switch (classifyPrompt(after)) {
|
|
926
|
+
const after = await this.probeMode();
|
|
927
|
+
switch (after) {
|
|
901
928
|
case "released":
|
|
902
929
|
this.cleanup();
|
|
903
|
-
return "Changes committed successfully. Safe mode DISABLED.";
|
|
930
|
+
return { ok: true, message: "Changes committed successfully. Safe mode DISABLED." };
|
|
904
931
|
case "safe":
|
|
905
|
-
return
|
|
932
|
+
return {
|
|
933
|
+
ok: false,
|
|
934
|
+
message: "Commit NOT completed \u2014 the device is still in Safe Mode (the Ctrl+X commit did not " + "take). Your changes remain held in memory and are NOT yet saved; call commit_safe_mode " + "again to retry, or rollback_safe_mode to discard them. If retries keep failing, this " + "RouterOS build may not accept an interactive Safe Mode commit over SSH \u2014 apply the " + "change without Safe Mode instead."
|
|
935
|
+
};
|
|
906
936
|
default:
|
|
907
|
-
return
|
|
937
|
+
return {
|
|
938
|
+
ok: false,
|
|
939
|
+
message: "Commit status unclear \u2014 no prompt seen after the commit. The session is left open so " + "nothing is reverted; verify with get_safe_mode_status or retry commit_safe_mode."
|
|
940
|
+
};
|
|
908
941
|
}
|
|
909
942
|
});
|
|
910
943
|
}
|
|
@@ -941,32 +974,17 @@ class SafeModeManager {
|
|
|
941
974
|
channel.on("data", onData);
|
|
942
975
|
});
|
|
943
976
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
clearTimeout(hard);
|
|
954
|
-
if (quiet)
|
|
955
|
-
clearTimeout(quiet);
|
|
956
|
-
channel.removeListener("data", onData);
|
|
957
|
-
resolve2(stripAnsi(buf));
|
|
958
|
-
}
|
|
959
|
-
function onData(chunk) {
|
|
960
|
-
buf += decodeOutput(chunk);
|
|
961
|
-
if (PROMPT_RE.test(lastNonEmptyLine(stripAnsi(buf)))) {
|
|
962
|
-
if (quiet)
|
|
963
|
-
clearTimeout(quiet);
|
|
964
|
-
quiet = setTimeout(done, quietMs);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
hard = setTimeout(done, maxMs);
|
|
968
|
-
channel.on("data", onData);
|
|
977
|
+
async probeMode() {
|
|
978
|
+
if (!this.channel)
|
|
979
|
+
return "unknown";
|
|
980
|
+
const token = "__MCP_SAFEMODE_PROBE__";
|
|
981
|
+
this.channel.write(`:put "${token}"
|
|
982
|
+
`);
|
|
983
|
+
const out = await this.readUntilPrompt(8000, (cleaned) => {
|
|
984
|
+
const i = cleaned.lastIndexOf(token);
|
|
985
|
+
return i >= 0 && PROMPT_RE.test(cleaned.slice(i));
|
|
969
986
|
});
|
|
987
|
+
return classifyPrompt(out);
|
|
970
988
|
}
|
|
971
989
|
extractOutput(raw, command) {
|
|
972
990
|
const text = raw.replace(/\r\n/g, `
|
|
@@ -1560,6 +1578,7 @@ function recordToolCall(raw) {
|
|
|
1560
1578
|
|
|
1561
1579
|
// src/core/registry.ts
|
|
1562
1580
|
var AUTO_RECORDS_VERB = /^(list|get|show|print)_/;
|
|
1581
|
+
var UI_OUTPUT_SCHEMA = z2.object({}).passthrough();
|
|
1563
1582
|
function effectiveUi(def) {
|
|
1564
1583
|
if (def.ui)
|
|
1565
1584
|
return { ui: def.ui, auto: false };
|
|
@@ -1600,12 +1619,13 @@ function defineTool(def) {
|
|
|
1600
1619
|
inputSchema: def.inputSchema,
|
|
1601
1620
|
ui: def.ui,
|
|
1602
1621
|
register(server, opts = {}) {
|
|
1603
|
-
const { sendLog, deviceNames } = opts;
|
|
1622
|
+
const { sendLog, deviceNames, deviceAliases } = opts;
|
|
1604
1623
|
const multiDevice = !!deviceNames && deviceNames.length > 1;
|
|
1605
1624
|
const { ui, auto } = effectiveUi(def);
|
|
1625
|
+
const selectorNames = multiDevice && deviceNames ? [...new Set([...deviceNames, ...deviceAliases ?? []])] : [];
|
|
1606
1626
|
const inputSchema = multiDevice ? {
|
|
1607
1627
|
...def.inputSchema,
|
|
1608
|
-
device: z2.enum(
|
|
1628
|
+
device: z2.enum(selectorNames).optional().describe(`Which configured MikroTik device to run this on. One of: ${selectorNames.join(", ")} (a config key or its label). Omit to use the default device.`)
|
|
1609
1629
|
} : def.inputSchema;
|
|
1610
1630
|
const risk = riskOf(def.annotations);
|
|
1611
1631
|
const callback = async (args) => {
|
|
@@ -1628,7 +1648,10 @@ function defineTool(def) {
|
|
|
1628
1648
|
return { content: [{ type: "text", text: out.text }], isError: true };
|
|
1629
1649
|
}
|
|
1630
1650
|
if (auto && !out.structuredContent) {
|
|
1631
|
-
|
|
1651
|
+
const view = buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
|
|
1652
|
+
if (view.rows.length > 0) {
|
|
1653
|
+
out.structuredContent = view;
|
|
1654
|
+
}
|
|
1632
1655
|
}
|
|
1633
1656
|
const result = {
|
|
1634
1657
|
content: [{ type: "text", text: out.text }]
|
|
@@ -1671,7 +1694,8 @@ function defineTool(def) {
|
|
|
1671
1694
|
description: def.description,
|
|
1672
1695
|
inputSchema,
|
|
1673
1696
|
annotations: { ...def.annotations, title: def.title },
|
|
1674
|
-
...ui ? { _meta: toolUiMeta(ui) } : {}
|
|
1697
|
+
...ui ? { _meta: toolUiMeta(ui) } : {},
|
|
1698
|
+
...ui && !auto ? { outputSchema: UI_OUTPUT_SCHEMA } : {}
|
|
1675
1699
|
}, callback);
|
|
1676
1700
|
}
|
|
1677
1701
|
};
|
|
@@ -2644,11 +2668,19 @@ async function restoreLocalBackup(device, name, confirm) {
|
|
|
2644
2668
|
return fail("device stopped responding after applying \u2014 rolled back to avoid a lock-out", applied);
|
|
2645
2669
|
}
|
|
2646
2670
|
const committed = await safe.commit();
|
|
2671
|
+
if (!committed.ok) {
|
|
2672
|
+
return {
|
|
2673
|
+
ok: false,
|
|
2674
|
+
applied,
|
|
2675
|
+
committed: false,
|
|
2676
|
+
message: `applied ${applied} command(s) but COMMIT FAILED \u2014 not saved: ${committed.message}`
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2647
2679
|
return {
|
|
2648
2680
|
ok: true,
|
|
2649
2681
|
applied,
|
|
2650
2682
|
committed: true,
|
|
2651
|
-
message: `committed ${applied} command(s). ${committed}`
|
|
2683
|
+
message: `committed ${applied} command(s). ${committed.message}`
|
|
2652
2684
|
};
|
|
2653
2685
|
} catch (e) {
|
|
2654
2686
|
await safe.rollback();
|
|
@@ -3882,9 +3914,14 @@ DRY-RUN: all changes rolled back. Re-run with confirm=true to commit.`;
|
|
|
3882
3914
|
ABORTED: the device stopped responding after applying \u2014 changes rolled back to avoid a lock-out.`;
|
|
3883
3915
|
}
|
|
3884
3916
|
const committed = await safe.commit();
|
|
3917
|
+
if (!committed.ok) {
|
|
3918
|
+
return `${header}
|
|
3919
|
+
|
|
3920
|
+
COMMIT FAILED \u2014 changes are NOT saved (still pending in Safe Mode). ` + `${committed.message}`;
|
|
3921
|
+
}
|
|
3885
3922
|
return `${header}
|
|
3886
3923
|
|
|
3887
|
-
COMMITTED: changes are now permanent. ${committed}`;
|
|
3924
|
+
COMMITTED: changes are now permanent. ${committed.message}`;
|
|
3888
3925
|
} catch (e) {
|
|
3889
3926
|
await safe.rollback();
|
|
3890
3927
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -5947,8 +5984,12 @@ ${useSafe ? "Safe Mode rolled back \u2014 no changes kept." : "Partial rules rem
|
|
|
5947
5984
|
}
|
|
5948
5985
|
done.push(cmd);
|
|
5949
5986
|
}
|
|
5950
|
-
if (useSafe)
|
|
5951
|
-
await mgr.commit();
|
|
5987
|
+
if (useSafe) {
|
|
5988
|
+
const c = await mgr.commit();
|
|
5989
|
+
if (!c.ok) {
|
|
5990
|
+
return `Applied ${done.length} rule(s) but Safe Mode COMMIT FAILED \u2014 changes are NOT saved ` + `and will revert: ${c.message}`;
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5952
5993
|
return `Security Shield applied \u2014 ${done.length} rule(s) across ${groups.length} protection(s)${useSafe ? " (committed via Safe Mode)" : ""}. Audit with audit_firewall_hardening; undo with remove_firewall_hardening.`;
|
|
5953
5994
|
}
|
|
5954
5995
|
}),
|
|
@@ -13524,7 +13565,7 @@ ${redactSecrets(result)}`;
|
|
|
13524
13565
|
|
|
13525
13566
|
// src/tools/poe.ts
|
|
13526
13567
|
import { z as z70 } from "zod";
|
|
13527
|
-
var NO_POE = "This device
|
|
13568
|
+
var NO_POE = "This device has no PoE-OUT hardware, so the `/interface ethernet poe` menu " + "(used to SUPPLY power to other devices) is not available \u2014 this is normal, not an error. " + "Note: PoE-OUT (this router powering other devices) is different from PoE-IN (this router being " + "powered over Ethernet). If your device is PoE-powered, read its PoE-IN voltage/state with " + "get_system_health (`/system health`), not the PoE tools.";
|
|
13528
13569
|
var poeTools = [
|
|
13529
13570
|
defineTool({
|
|
13530
13571
|
name: "get_poe_monitor",
|
|
@@ -17595,7 +17636,7 @@ var safeModeTools = [
|
|
|
17595
17636
|
async handler(_a, ctx) {
|
|
17596
17637
|
const device = resolveDeviceName(ctx.device);
|
|
17597
17638
|
ctx.info(`[${device}] Committing safe mode changes`);
|
|
17598
|
-
return getSafeModeManager(device).commit();
|
|
17639
|
+
return (await getSafeModeManager(device).commit()).message;
|
|
17599
17640
|
}
|
|
17600
17641
|
}),
|
|
17601
17642
|
defineTool({
|
|
@@ -22351,7 +22392,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
|
22351
22392
|
// package.json
|
|
22352
22393
|
var package_default = {
|
|
22353
22394
|
name: "@usex/mikrotik-mcp",
|
|
22354
|
-
version: "3.
|
|
22395
|
+
version: "3.23.0",
|
|
22355
22396
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
22356
22397
|
keywords: [
|
|
22357
22398
|
"ai",
|
|
@@ -22555,6 +22596,7 @@ function createServer(opts = {}) {
|
|
|
22555
22596
|
const toolCount = registerTools(server, toolModules, {
|
|
22556
22597
|
sendLog,
|
|
22557
22598
|
deviceNames: names,
|
|
22599
|
+
deviceAliases: deviceLabels(),
|
|
22558
22600
|
readOnly
|
|
22559
22601
|
});
|
|
22560
22602
|
const promptCount = registerPrompts(server);
|
package/dist/ui/dashboard.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
6
7
|
<title>MikroTik Device Dashboard</title>
|
|
7
8
|
<script type="module">
|
|
8
9
|
import{a as e,i as t,n,r,t as i}from"./app-DU6NBjf_.js";var a=`show_system_dashboard`,o=document.getElementById(`app`);function s(e,t={},...n){let r=document.createElement(e);for(let[e,n]of Object.entries(t))e===`class`?r.className=n:r.setAttribute(e,n);for(let e of n.flat())e===!1||e==null||r.append(e instanceof Node?e:document.createTextNode(String(e)));return r}function c(e){if(e==null)return`—`;let t=[`B`,`KiB`,`MiB`,`GiB`,`TiB`],n=0,r=e;for(;r>=1024&&n<t.length-1;)r/=1024,n++;return`${r.toFixed(+(r<10&&n>0))} ${t[n]}`}function l(e){return e==null?``:e>=90?`is-bad`:e>=70?`is-warn`:`is-good`}var u=`http://www.w3.org/2000/svg`;function d(e,t){let n=document.createElementNS(u,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,String(r));return n}function f(e,t,n){let r=2*Math.PI*26,i=t==null?0:Math.max(0,Math.min(100,t))/100,a=d(`svg`,{width:64,height:64,viewBox:`0 0 64 64`}),o=d(`circle`,{cx:32,cy:32,r:26,fill:`none`,"stroke-width":7});o.setAttribute(`class`,`gauge__track`);let c=d(`circle`,{cx:32,cy:32,r:26,fill:`none`,"stroke-width":7,"stroke-dasharray":r,"stroke-dashoffset":r*(1-i)});c.setAttribute(`class`,`gauge__bar ${l(t)}`);let u=d(`text`,{x:32,y:37,"text-anchor":`middle`});return u.setAttribute(`class`,`gauge__pct`),u.textContent=t==null?`—`:`${Math.round(t)}%`,a.replaceChildren(o,c,u),s(`div`,{class:`card gauge`},a,s(`div`,{class:`gauge__meta`},s(`p`,{class:`card__label`},e),s(`small`,{},n)))}function p(e,t){return s(`div`,{class:`card`},s(`p`,{class:`card__label`},e),s(`div`,{class:`card__value`},t))}function m(e,t){let n=Object.entries(t);if(n.length===0)return null;let r=s(`div`,{class:`kv__body`},...n.flatMap(([e,t])=>[s(`div`,{class:`kv__k`},e),s(`div`,{class:`kv__v`},t||`—`)]));return s(`details`,{class:`kv`},s(`summary`,{},`${e} (${n.length})`),r)}var h=null,g=!1;function _(){if(!h){o.replaceChildren(s(`div`,{class:`skeleton`},`Waiting for device data…`));return}let e=h,t=e.derived,n=e.resource.version??`?`,r=e.routerboard.model??e.resource[`board-name`]??`?`,i=[f(`CPU load`,t.cpuLoadPct,`${e.resource[`cpu-count`]??`?`} cores`),f(`Memory`,t.memUsedPct,`${c(t.memUsedBytes)} / ${c(t.memTotalBytes)}`),t.hddUsedPct!=null&&f(`Disk`,t.hddUsedPct,`${c(t.hddUsedBytes)} / ${c(t.hddTotalBytes)}`)].filter(Boolean),a=[p(`Uptime`,e.resource.uptime??`—`),t.temperatureC!=null&&p(`Temperature`,`${t.temperatureC} °C`),t.voltageV!=null&&p(`Voltage`,`${t.voltageV} V`),p(`Architecture`,e.resource[`architecture-name`]??`—`)].filter(Boolean),l=s(`button`,{class:`btn`},g?`Refreshing…`:`↻ Refresh`);g&&l.setAttribute(`disabled`,`true`),l.addEventListener(`click`,b);let u=s(`span`,{class:`pill`},`device `,s(`b`,{},e.device)),d=s(`header`,{class:`hd`},s(`span`,{class:`hd__dot`}),s(`div`,{},s(`h1`,{class:`hd__title`},e.identity),s(`p`,{class:`hd__sub`},`${r} · RouterOS ${n}`)),s(`span`,{class:`hd__spacer`}),u),_=s(`footer`,{class:`foot`},l,s(`span`,{},`updated ${new Date(e.generatedAt).toLocaleTimeString()}`)),v=[d,s(`section`,{class:`grid`},...i),s(`section`,{class:`grid`},...a),m(`System resource`,e.resource),m(`RouterBOARD`,e.routerboard),_].filter(e=>e!=null);o.replaceChildren(...v)}var v=new t({name:`mikrotik-dashboard`,version:`1.0.0`});function y(e){e&&typeof e==`object`&&`device`in e&&(h=e,_())}async function b(){if(!g){g=!0,_();try{y((await v.callServerTool({name:a,arguments:{}})).structuredContent)}catch(e){console.error(`[dashboard] refresh failed`,e)}finally{g=!1,_()}}}v.ontoolresult=e=>y(e.structuredContent),v.ontoolinput=()=>{h||_()},v.onhostcontextchanged=t=>{if(t.theme&&e(t.theme),t.styles?.variables&&r(t.styles.variables),t.styles?.css?.fonts&&n(t.styles.css.fonts),t.safeAreaInsets){let{top:e,right:n,bottom:r,left:i}=t.safeAreaInsets;document.body.style.padding=`${e+16}px ${n+16}px ${r+16}px ${i+16}px`}},v.onteardown=async()=>({}),e(i()),_(),v.connect().catch(e=>console.error(`[dashboard] connect failed`,e));
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
6
7
|
<title>MikroTik Firewall Audit</title>
|
|
7
8
|
<script type="module">
|
|
8
9
|
import{i as e}from"./app-DU6NBjf_.js";import{i as t,o as n,t as r}from"./kit-DAVkZl3Q.js";var i=`firewall_audit`,a=document.getElementById(`app`),o=null,s=!1,c=null,l=new e({name:`mikrotik-firewall-audit`,version:`1.0.0`});function u(e){return e===`clean`||e===`good`?`is-good`:e===`fair`?`is-warn`:`is-bad`}function d(e){return e===`high`?`is-bad`:e===`medium`?`is-warn`:`is-low`}function f(e,t){let n=2*Math.PI*34,r=Math.max(0,Math.min(100,e))/100,i=document.createElementNS(`http://www.w3.org/2000/svg`,`svg`);i.setAttribute(`viewBox`,`0 0 88 88`),i.setAttribute(`class`,`gauge ${u(t)}`);let a=(e,t)=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,`circle`);n.setAttribute(`class`,e),n.setAttribute(`cx`,`44`),n.setAttribute(`cy`,`44`),n.setAttribute(`r`,`34`);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n};i.append(a(`gauge__bg`,{})),i.append(a(`gauge__fg`,{"stroke-dasharray":`${n*r} ${n}`,transform:`rotate(-90 44 44)`}));let o=document.createElementNS(`http://www.w3.org/2000/svg`,`text`);return o.setAttribute(`x`,`44`),o.setAttribute(`y`,`48`),o.setAttribute(`text-anchor`,`middle`),o.setAttribute(`class`,`gauge__num`),o.textContent=String(e),i.append(o),i}async function p(e,t){if(!(!e.action||s)){c=t,h();try{await l.callServerTool({name:e.action.tool,arguments:e.action.args}),await _()}catch(e){console.error(`[firewall-audit] action failed`,e)}finally{c=null,h()}}}function m(e,n){let i=`${e.kind}-${e.table}-${e.ruleIndex??e.chain}-${n}`,a=t(`div`,{class:`f-head`},t(`span`,{class:`chip ${d(e.severity)}`},e.severity),t(`span`,{class:`f-title`},e.title),t(`span`,{class:`f-where`},`${e.table}/${e.chain}`)),o=t(`div`,{class:`f-body`},t(`p`,{class:`f-detail`},e.detail),t(`p`,{class:`f-fix`},t(`b`,{},`Fix: `),e.suggestion)),l=t(`div`,{class:`f-card ${d(e.severity)}`},a,o);if(e.action){let n=c===i?`Working…`:e.action.label;l.append(t(`div`,{class:`f-actions`},r(n,()=>void p(e,i),{class:`btn-danger`,disabled:s||c===i,title:`Calls ${e.action.tool}`})))}return l}function h(){if(!o){a.replaceChildren(t(`div`,{class:`skeleton`},`Running firewall audit…`));return}let e=o,n=t(`header`,{class:`hd`},f(e.riskScore,e.grade),t(`div`,{class:`hd__meta`},t(`h1`,{class:`hd__title`},`Firewall audit`),t(`p`,{class:`hd__sub`},`device `,t(`b`,{},e.device),` · ${e.ruleCount} rules · grade `,t(`b`,{class:u(e.grade)},e.grade)),t(`div`,{class:`counts`},t(`span`,{class:`chip is-bad`},`${e.counts.high} high`),t(`span`,{class:`chip is-warn`},`${e.counts.medium} medium`),t(`span`,{class:`chip is-low`},`${e.counts.low} low`))),t(`span`,{class:`hd__spacer`}),r(s?`Auditing…`:`↻ Re-audit`,_,{disabled:s})),i;i=e.findings.length===0?t(`div`,{class:`empty`},`No issues found — the ruleset looks clean. ✓`):t(`div`,{class:`f-list`},...e.findings.map(m));let c=t(`footer`,{class:`foot`},t(`span`,{class:`grow`}),t(`span`,{},`updated ${new Date(e.generatedAt).toLocaleTimeString()}`));a.replaceChildren(n,i,c)}function g(e){e&&typeof e==`object`&&e.__mikrotikView===`firewall-audit`&&(o=e,h())}async function _(){if(!s){s=!0,h();try{g((await l.callServerTool({name:i,arguments:{}})).structuredContent)}catch(e){console.error(`[firewall-audit] refresh failed`,e)}finally{s=!1,h()}}}l.ontoolresult=e=>g(e.structuredContent),l.ontoolinput=()=>{o||h()},n(l),l.onteardown=async()=>({}),h(),l.connect().catch(e=>console.error(`[firewall-audit] connect failed`,e));
|
package/dist/ui/firewall.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
6
7
|
<title>MikroTik Firewall Rules</title>
|
|
7
8
|
<script type="module">
|
|
8
9
|
import{i as e}from"./app-DU6NBjf_.js";import{a as t,i as n,o as r,r as i,t as a}from"./kit-DAVkZl3Q.js";var o=`show_firewall_filter`,s=document.getElementById(`app`),c=null,l=``,u=!1,d=[`protocol`,`src-address`,`dst-address`,`src-port`,`dst-port`,`in-interface`,`out-interface`,`in-interface-list`,`out-interface-list`,`connection-state`,`src-address-list`,`dst-address-list`];function f(e){return e===`accept`?`act is-accept`:e===`drop`||e===`reject`?`act is-drop`:e===`jump`||e===`return`?`act is-jump`:`act`}function p(e){let t=Number(e);if(!e||Number.isNaN(t))return e??`—`;if(t<1e3)return String(t);let n=[`k`,`M`,`G`,`T`],r=-1,i=t;for(;i>=1e3&&r<n.length-1;)i/=1e3,r++;return`${i.toFixed(1)}${n[r]}`}function m(e){let t=[];for(let r of d)e[r]&&t.push(n(`span`,{},`${r}=`,n(`b`,{},e[r])));let r=n(`td`,{class:`matchers`});return t.forEach((e,t)=>{t&&r.append(document.createTextNode(` `)),r.append(e)}),t.length||r.append(document.createTextNode(e.comment??`—`)),r}function h(e){let t=l.trim().toLowerCase();return t?e.rows.filter(e=>Object.values(e).some(e=>e.toLowerCase().includes(t))):e.rows}function g(e){let t=(e.flags??``).includes(`X`),r=e.action??`—`,i=n(`tr`,t?{class:`is-disabled`}:{});return i.append(n(`td`,{class:`col-num`},e[`#`]??`—`),n(`td`,{},e.chain??`—`),n(`td`,{},n(`span`,{class:f(r)},r)),m(e),n(`td`,{class:`num`},p(e.packets)),n(`td`,{class:`num`},p(e.bytes))),i}function _(){if(!c){s.replaceChildren(n(`div`,{class:`skeleton`},`Waiting for firewall rules…`));return}let e=c,r=h(e),o=e.rows.filter(e=>(e.flags??``).includes(`X`)).length,f=n(`input`,{class:`search`,type:`search`,placeholder:`Search ${e.rows.length} rule(s)…`,value:l});f.addEventListener(`input`,()=>{l=f.value,_()});let p=n(`header`,{class:`hd`},n(`span`,{class:`hd__dot`}),n(`div`,{},n(`h1`,{class:`hd__title`},`Firewall — Filter`),n(`p`,{class:`hd__sub`},`${e.rows.length} rules · ${o} disabled`)),n(`span`,{class:`hd__spacer`}),n(`span`,{class:`pill`},`device `,n(`b`,{},e.device))),m=n(`div`,{class:`toolbar`},n(`div`,{class:`grow`},f),a(u?`Refreshing…`:`↻ Refresh`,b,{disabled:u}),a(`CSV`,()=>{let n=[`#`,`chain`,`action`,...d,`packets`,`bytes`];i(`firewall-${e.chain}.csv`,t(n,h(e)),`text/csv`)},{title:`Export visible rules as CSV`})),v;v=e.rows.length?r.length?n(`div`,{class:`tablewrap`},n(`table`,{class:`tbl`},n(`thead`,{},n(`tr`,{},n(`th`,{class:`col-num`},`#`),n(`th`,{},`chain`),n(`th`,{},`action`),n(`th`,{},`matchers`),n(`th`,{class:`num`},`packets`),n(`th`,{class:`num`},`bytes`))),n(`tbody`,{},...r.map(g)))):n(`div`,{class:`empty`},`No rules match the search.`):n(`div`,{class:`empty`},`No filter rules configured.`);let y=n(`footer`,{class:`foot`},n(`span`,{class:`grow`}),n(`span`,{},`updated ${new Date(e.generatedAt).toLocaleTimeString()}`));s.replaceChildren(p,m,v,y)}var v=new e({name:`mikrotik-firewall`,version:`1.0.0`});function y(e){e&&typeof e==`object`&&e.__mikrotikView===`firewall`&&(c=e,_())}async function b(){if(!u){u=!0,_();try{y((await v.callServerTool({name:o,arguments:{}})).structuredContent)}catch(e){console.error(`[firewall] refresh failed`,e)}finally{u=!1,_()}}}v.ontoolresult=e=>y(e.structuredContent),v.ontoolinput=()=>{c||_()},r(v),v.onteardown=async()=>({}),_(),v.connect().catch(e=>console.error(`[firewall] connect failed`,e));
|
package/dist/ui/interfaces.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
6
7
|
<title>MikroTik Interfaces</title>
|
|
7
8
|
<script type="module">
|
|
8
9
|
import{i as e}from"./app-DU6NBjf_.js";import{i as t,o as n,t as r}from"./kit-DAVkZl3Q.js";var i=`show_interfaces`,a=document.getElementById(`app`),o=null,s=``,c=!1;function l(e){return(e.flags??``).includes(`R`)||e.running===`true`}function u(e){return(e.flags??``).includes(`X`)||e.disabled===`true`}function d(e,n){return!!n&&t(`div`,{class:`contents`},t(`span`,{},e),t(`b`,{},n))}function f(e){let n=u(e),r=n?`dot is-down`:l(e)?`dot is-up`:`dot`,i=t(`div`,{class:`if-card__meta`},d(`MTU`,e.mtu??e[`actual-mtu`]),d(`MAC`,e[`mac-address`]),d(`comment`,e.comment));return t(`div`,{class:`card if-card${n?` is-disabled`:``}`},t(`div`,{class:`if-card__top`},t(`span`,{class:r}),t(`span`,{class:`if-card__name`},e.name??`?`),t(`span`,{class:`badge`},e.type??`—`)),i)}function p(e){let t=s.trim().toLowerCase();return t?e.rows.filter(e=>Object.values(e).some(e=>e.toLowerCase().includes(t))):e.rows}function m(){if(!o){a.replaceChildren(t(`div`,{class:`skeleton`},`Waiting for interfaces…`));return}let e=o,n=p(e),i=e.rows.filter(l).length,d=e.rows.filter(u).length,h=t(`input`,{class:`search`,type:`search`,placeholder:`Search ${e.rows.length} interface(s)…`,value:s});h.addEventListener(`input`,()=>{s=h.value,m()});let g=t(`header`,{class:`hd`},t(`span`,{class:`hd__dot`}),t(`div`,{},t(`h1`,{class:`hd__title`},`Interfaces`),t(`p`,{class:`hd__sub`},`${i} running · ${d} disabled · ${e.rows.length} total`)),t(`span`,{class:`hd__spacer`}),t(`span`,{class:`pill`},`device `,t(`b`,{},e.device))),v=t(`div`,{class:`toolbar`},t(`div`,{class:`grow`},h),r(c?`Refreshing…`:`↻ Refresh`,_,{disabled:c})),y=n.length?t(`section`,{class:`if-grid`},...n.map(f)):t(`div`,{class:`empty`},`No interfaces match the search.`),b=t(`footer`,{class:`foot`},t(`span`,{class:`grow`}),t(`span`,{},`updated ${new Date(e.generatedAt).toLocaleTimeString()}`));a.replaceChildren(g,v,y,b)}var h=new e({name:`mikrotik-interfaces`,version:`1.0.0`});function g(e){e&&typeof e==`object`&&e.__mikrotikView===`interfaces`&&(o=e,m())}async function _(){if(!c){c=!0,m();try{g((await h.callServerTool({name:i,arguments:{}})).structuredContent)}catch(e){console.error(`[interfaces] refresh failed`,e)}finally{c=!1,m()}}}h.ontoolresult=e=>g(e.structuredContent),h.ontoolinput=()=>{o||m()},n(h),h.onteardown=async()=>({}),m(),h.connect().catch(e=>console.error(`[interfaces] connect failed`,e));
|
package/dist/ui/records.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
6
7
|
<title>MikroTik Records Viewer</title>
|
|
7
8
|
<script type="module">
|
|
8
9
|
import{i as e}from"./app-DU6NBjf_.js";import{a as t,i as n,n as r,o as i,r as a,t as o}from"./kit-DAVkZl3Q.js";var s=document.getElementById(`app`),c=null,l=``,u=null,d=1,f=new Set,p=null,m=!1,h=0,g=null,_={};function v(e){let t=[`#`,`flags`].filter(t=>e.columns.includes(t));return[...t,...e.columns.filter(e=>!t.includes(e))]}function y(e){let t=l.trim().toLowerCase(),n=e.rows;if(t&&(n=n.filter(e=>Object.values(e).some(e=>e.toLowerCase().includes(t)))),f.size&&(n=n.filter(e=>{let t=e.flags??``;return[...f].every(e=>t.includes(e))})),u){let e=u;n=[...n].sort((t,n)=>{let r=t[e]??``,i=n[e]??``,a=Number(r),o=Number(i);return(r!==``&&i!==``&&!Number.isNaN(a)&&!Number.isNaN(o)?a-o:r.localeCompare(i))*d})}return n}function b(e){let t=new Set;for(let n of e.rows)for(let e of n.flags??``)t.add(e);return[...t].sort()}function x(e){return Object.entries(e).filter(([e])=>e!==`#`&&e!==`flags`).map(([e,t])=>/\s/.test(t)?`${e}="${t}"`:`${e}=${t}`).join(` `)}function S(e){let t=b(e);return t.length?n(`div`,{class:`toolbar`},...t.map(t=>{let n=e.flags[t]??t,r=f.has(t);return o(`${t} · ${n}`,()=>{r?f.delete(t):f.add(t),j()},{class:r?`is-active`:``,title:`Filter rows flagged "${n}"`})})):!1}function C(e){let t=n(`th`,e===`#`?{class:`col-num`}:{},e);return u===e&&t.append(n(`span`,{class:`arrow`},d===1?`▲`:`▼`)),t.addEventListener(`click`,()=>{u===e?d=d===1?-1:1:(u=e,d=1),j()}),t}function w(e,t){let r=v(e),i=n(`thead`,{},n(`tr`,{},...r.map(C))),a=n(`tbody`);return t.forEach(t=>{let i=n(`tr`,(t.flags??``).includes(`X`)?{class:`is-disabled`}:{});for(let e of r)i.append(n(`td`,e===`#`?{class:`col-num`}:{},t[e]??`—`));i.addEventListener(`click`,()=>{p=e.rows.indexOf(t),j()}),a.append(i)}),n(`div`,{class:`tablewrap`},n(`table`,{class:`tbl`},i,a))}function T(e){let t=n(`div`,{class:`kv__body`});for(let[r,i]of Object.entries(e))t.append(n(`div`,{class:`kv__k`},r),n(`div`,{class:`kv__v`},i||`—`));return t}function E(e){if(p==null||!e.rows[p])return!1;let t=e.rows[p],i=t.name??t[`#`]??`record`;return n(`div`,{class:`drawer`},n(`div`,{class:`drawer__hd`},n(`span`,{},`Row `),n(`b`,{},String(i)),n(`span`,{class:`hd__spacer`}),o(`Copy as CLI`,()=>void r(x(t)),{title:`Copy key=value pairs`}),o(`Close`,()=>{p=null,j()})),T(t))}function D(e,i){let s=n(`input`,{class:`search`,type:`search`,placeholder:`Search ${e.count} row${e.count===1?``:`s`}…`,value:l});s.addEventListener(`input`,()=>{l=s.value,p=null,k()});let c=n(`select`,{class:`btn`,title:`Auto-refresh interval`});for(let[e,t]of[[`Auto: off`,`0`],[`Auto: 5s`,`5000`],[`Auto: 15s`,`15000`],[`Auto: 60s`,`60000`]]){let r=n(`option`,{value:t},e);Number(t)===h&&(r.selected=!0),c.append(r)}c.addEventListener(`change`,()=>F(Number(c.value)));let u=()=>y(e),d=v(e);return n(`div`,{class:`toolbar`},n(`div`,{class:`grow`},s),o(m?`Refreshing…`:`↻ Refresh`,P,{disabled:m}),c,o(`CSV`,()=>a(`${e.tool}.csv`,t(d,u()),`text/csv`),{title:`Export visible rows as CSV`}),o(`JSON`,()=>a(`${e.tool}.json`,JSON.stringify(u(),null,2),`application/json`),{title:`Export visible rows as JSON`}),o(`Copy`,()=>void r(JSON.stringify(u(),null,2)),{title:`Copy visible rows as JSON`}),n(`span`,{class:`pill count-pill`},`showing `,n(`b`,{},`${i}/${e.count}`)))}function O(e){return n(`header`,{class:`hd`},n(`span`,{class:`hd__dot`}),n(`div`,{},n(`h1`,{class:`hd__title`},e.title),n(`p`,{class:`hd__sub`},e.tool)),n(`span`,{class:`hd__spacer`}))}function k(){if(!c)return;let e=document.getElementById(`results`);if(!e){j();return}e.replaceChildren(A(c));let t=document.querySelector(`.count-pill b`);t&&(t.textContent=`${y(c).length}/${c.count}`)}function A(e){let t=y(e);return e.rows.length===0?e.raw?n(`pre`,{class:`raw`},e.raw):n(`div`,{class:`empty`},`No records returned.`):e.kind===`record`&&e.rows.length===1?T(e.rows[0]):t.length===0?n(`div`,{class:`empty`},`No rows match the current filter.`):w(e,t)}function j(){if(!c){s.replaceChildren(n(`div`,{class:`skeleton`},`Waiting for data…`));return}let e=c,t=y(e).length,r=e.kind===`list`&&e.rows.length>0,i=[O(e),r&&D(e,t),r&&S(e),n(`div`,{id:`results`},A(e)),E(e),n(`footer`,{class:`foot`},n(`span`,{},`format: ${e.format}`),n(`span`,{class:`grow`}),n(`span`,{},`updated ${new Date(e.generatedAt).toLocaleTimeString()}`))];s.replaceChildren(...i.filter(Boolean))}var M=new e({name:`mikrotik-records`,version:`1.0.0`});function N(e){e&&typeof e==`object`&&e.__mikrotikView===`records`&&(c=e,p=null,j())}async function P(){if(!(m||!c)){m=!0,j();try{N((await M.callServerTool({name:c.tool,arguments:_})).structuredContent)}catch(e){console.error(`[records] refresh failed`,e)}finally{m=!1,j()}}}function F(e){h=e,g&&=(clearInterval(g),null),e>0&&(g=setInterval(()=>void P(),e)),j()}M.ontoolresult=e=>N(e.structuredContent),M.ontoolinput=e=>{e&&typeof e==`object`&&`arguments`in e&&(_=e.arguments??{}),c||j()},i(M),M.onteardown=async()=>(g&&clearInterval(g),{}),j(),M.connect().catch(e=>console.error(`[records] connect failed`,e));
|
package/package.json
CHANGED