@usex/mikrotik-mcp 3.22.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 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 && name in active.devices)
244
- return name;
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);
@@ -1647,6 +1672,7 @@ function recordToolCall(raw) {
1647
1672
 
1648
1673
  // src/core/registry.ts
1649
1674
  var AUTO_RECORDS_VERB = /^(list|get|show|print)_/;
1675
+ var UI_OUTPUT_SCHEMA = z2.object({}).passthrough();
1650
1676
  function effectiveUi(def) {
1651
1677
  if (def.ui)
1652
1678
  return { ui: def.ui, auto: false };
@@ -1687,12 +1713,13 @@ function defineTool(def) {
1687
1713
  inputSchema: def.inputSchema,
1688
1714
  ui: def.ui,
1689
1715
  register(server, opts = {}) {
1690
- const { sendLog, deviceNames } = opts;
1716
+ const { sendLog, deviceNames, deviceAliases } = opts;
1691
1717
  const multiDevice = !!deviceNames && deviceNames.length > 1;
1692
1718
  const { ui, auto } = effectiveUi(def);
1719
+ const selectorNames = multiDevice && deviceNames ? [...new Set([...deviceNames, ...deviceAliases ?? []])] : [];
1693
1720
  const inputSchema = multiDevice ? {
1694
1721
  ...def.inputSchema,
1695
- device: z2.enum(deviceNames).optional().describe(`Which configured MikroTik device to run this on. One of: ${deviceNames.join(", ")}. Omit to use the default device.`)
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.`)
1696
1723
  } : def.inputSchema;
1697
1724
  const risk = riskOf(def.annotations);
1698
1725
  const callback = async (args) => {
@@ -1715,7 +1742,10 @@ function defineTool(def) {
1715
1742
  return { content: [{ type: "text", text: out.text }], isError: true };
1716
1743
  }
1717
1744
  if (auto && !out.structuredContent) {
1718
- out.structuredContent = buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
1745
+ const view = buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
1746
+ if (view.rows.length > 0) {
1747
+ out.structuredContent = view;
1748
+ }
1719
1749
  }
1720
1750
  const result = {
1721
1751
  content: [{ type: "text", text: out.text }]
@@ -1758,7 +1788,8 @@ function defineTool(def) {
1758
1788
  description: def.description,
1759
1789
  inputSchema,
1760
1790
  annotations: { ...def.annotations, title: def.title },
1761
- ...ui ? { _meta: toolUiMeta(ui) } : {}
1791
+ ...ui ? { _meta: toolUiMeta(ui) } : {},
1792
+ ...ui && !auto ? { outputSchema: UI_OUTPUT_SCHEMA } : {}
1762
1793
  }, callback);
1763
1794
  }
1764
1795
  };
@@ -13522,7 +13553,7 @@ ${redactSecrets(result)}`;
13522
13553
 
13523
13554
  // src/tools/poe.ts
13524
13555
  import { z as z69 } from "zod";
13525
- var NO_POE = "This device does not have PoE-out hardware \u2014 the /interface ethernet poe menu is not available.";
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.";
13526
13557
  var poeTools = [
13527
13558
  defineTool({
13528
13559
  name: "get_poe_monitor",
@@ -23961,7 +23992,7 @@ function registerPrompts(server) {
23961
23992
  // package.json
23962
23993
  var package_default = {
23963
23994
  name: "@usex/mikrotik-mcp",
23964
- version: "3.22.0",
23995
+ version: "3.23.0",
23965
23996
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
23966
23997
  keywords: [
23967
23998
  "ai",
@@ -24165,6 +24196,7 @@ function createServer(opts = {}) {
24165
24196
  const toolCount = registerTools(server, toolModules, {
24166
24197
  sendLog,
24167
24198
  deviceNames: names,
24199
+ deviceAliases: deviceLabels(),
24168
24200
  readOnly
24169
24201
  });
24170
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 names; when more than one, a `device` selector is injected. */
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
- /** Resolve a (possibly undefined) device name to a concrete, existing name. */
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 name (or the default when
117
- * `name` is undefined). Throws if an explicit, unknown name is given.
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";
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 && name in active.devices)
236
- return name;
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);
@@ -1553,6 +1578,7 @@ function recordToolCall(raw) {
1553
1578
 
1554
1579
  // src/core/registry.ts
1555
1580
  var AUTO_RECORDS_VERB = /^(list|get|show|print)_/;
1581
+ var UI_OUTPUT_SCHEMA = z2.object({}).passthrough();
1556
1582
  function effectiveUi(def) {
1557
1583
  if (def.ui)
1558
1584
  return { ui: def.ui, auto: false };
@@ -1593,12 +1619,13 @@ function defineTool(def) {
1593
1619
  inputSchema: def.inputSchema,
1594
1620
  ui: def.ui,
1595
1621
  register(server, opts = {}) {
1596
- const { sendLog, deviceNames } = opts;
1622
+ const { sendLog, deviceNames, deviceAliases } = opts;
1597
1623
  const multiDevice = !!deviceNames && deviceNames.length > 1;
1598
1624
  const { ui, auto } = effectiveUi(def);
1625
+ const selectorNames = multiDevice && deviceNames ? [...new Set([...deviceNames, ...deviceAliases ?? []])] : [];
1599
1626
  const inputSchema = multiDevice ? {
1600
1627
  ...def.inputSchema,
1601
- device: z2.enum(deviceNames).optional().describe(`Which configured MikroTik device to run this on. One of: ${deviceNames.join(", ")}. Omit to use the default device.`)
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.`)
1602
1629
  } : def.inputSchema;
1603
1630
  const risk = riskOf(def.annotations);
1604
1631
  const callback = async (args) => {
@@ -1621,7 +1648,10 @@ function defineTool(def) {
1621
1648
  return { content: [{ type: "text", text: out.text }], isError: true };
1622
1649
  }
1623
1650
  if (auto && !out.structuredContent) {
1624
- out.structuredContent = buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
1651
+ const view = buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
1652
+ if (view.rows.length > 0) {
1653
+ out.structuredContent = view;
1654
+ }
1625
1655
  }
1626
1656
  const result = {
1627
1657
  content: [{ type: "text", text: out.text }]
@@ -1664,7 +1694,8 @@ function defineTool(def) {
1664
1694
  description: def.description,
1665
1695
  inputSchema,
1666
1696
  annotations: { ...def.annotations, title: def.title },
1667
- ...ui ? { _meta: toolUiMeta(ui) } : {}
1697
+ ...ui ? { _meta: toolUiMeta(ui) } : {},
1698
+ ...ui && !auto ? { outputSchema: UI_OUTPUT_SCHEMA } : {}
1668
1699
  }, callback);
1669
1700
  }
1670
1701
  };
@@ -13534,7 +13565,7 @@ ${redactSecrets(result)}`;
13534
13565
 
13535
13566
  // src/tools/poe.ts
13536
13567
  import { z as z70 } from "zod";
13537
- var NO_POE = "This device does not have PoE-out hardware \u2014 the /interface ethernet poe menu is not available.";
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.";
13538
13569
  var poeTools = [
13539
13570
  defineTool({
13540
13571
  name: "get_poe_monitor",
@@ -22361,7 +22392,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
22361
22392
  // package.json
22362
22393
  var package_default = {
22363
22394
  name: "@usex/mikrotik-mcp",
22364
- version: "3.22.0",
22395
+ version: "3.23.0",
22365
22396
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
22366
22397
  keywords: [
22367
22398
  "ai",
@@ -22565,6 +22596,7 @@ function createServer(opts = {}) {
22565
22596
  const toolCount = registerTools(server, toolModules, {
22566
22597
  sendLog,
22567
22598
  deviceNames: names,
22599
+ deviceAliases: deviceLabels(),
22568
22600
  readOnly
22569
22601
  });
22570
22602
  const promptCount = registerPrompts(server);
@@ -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));
@@ -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));
@@ -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));
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usex/mikrotik-mcp",
3
- "version": "3.22.0",
3
+ "version": "3.23.0",
4
4
  "description": "MCP server for MikroTik RouterOS — 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
5
5
  "keywords": [
6
6
  "ai",