mikromcp 1.6.0 → 1.7.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.
Files changed (3) hide show
  1. package/README.md +60 -353
  2. package/dist/main.js +497 -72
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -450,9 +450,21 @@ function paginate(items, offset2, limit2) {
450
450
  hasMore: offset2 + limit2 < total
451
451
  };
452
452
  }
453
- function listSummary(label, routerId2, shown, total, offset2) {
454
- const range = total === 0 ? "none" : `${offset2 + 1}-${offset2 + shown} of ${total}`;
455
- return `${label} on ${routerId2}: ${range}. Full records in structuredContent.`;
453
+ function compactFields(record, fields) {
454
+ const parts = [];
455
+ for (const field of fields) {
456
+ const value = record[field];
457
+ if (value === void 0 || value === null || value === "") continue;
458
+ const str = String(value);
459
+ parts.push(str.includes(" ") ? `${field}="${str}"` : `${field}=${str}`);
460
+ }
461
+ return parts.join(" ");
462
+ }
463
+ function listContent(label, routerId2, page, total, offset2, renderItem) {
464
+ const range = total === 0 ? "none" : `${offset2 + 1}-${offset2 + page.length} of ${total}`;
465
+ const header = `${label} on ${routerId2}: ${range}.`;
466
+ if (page.length === 0) return header;
467
+ return [header, ...page.map((item) => ` ${renderItem(item)}`)].join("\n");
456
468
  }
457
469
 
458
470
  // src/domain/tools/interface-tools.ts
@@ -536,7 +548,14 @@ var listInterfacesTool = {
536
548
  return enriched;
537
549
  });
538
550
  return {
539
- content: listSummary("Interfaces", context.routerId, results2.length, total, parsed.offset),
551
+ content: listContent(
552
+ "Interfaces",
553
+ context.routerId,
554
+ results2,
555
+ total,
556
+ parsed.offset,
557
+ (i) => compactFields(i, ["name", "type", "running", "disabled", "mtu", "comment"])
558
+ ),
540
559
  structuredContent: {
541
560
  routerId: context.routerId,
542
561
  interfaces: results2,
@@ -875,7 +894,14 @@ var listDhcpLeasesTool = {
875
894
  }
876
895
  const { items: paginated, total, hasMore } = paginate(leases, parsed.offset, parsed.limit);
877
896
  return {
878
- content: listSummary("DHCP leases", context.routerId, paginated.length, total, parsed.offset),
897
+ content: listContent(
898
+ "DHCP leases",
899
+ context.routerId,
900
+ paginated,
901
+ total,
902
+ parsed.offset,
903
+ (l) => compactFields(l, ["address", "mac-address", "host-name", "server", "status"])
904
+ ),
879
905
  structuredContent: {
880
906
  routerId: context.routerId,
881
907
  leases: paginated,
@@ -1044,7 +1070,14 @@ var listRoutesTool = {
1044
1070
  }
1045
1071
  const { items: paginated, total, hasMore } = paginate(routes, parsed.offset, parsed.limit);
1046
1072
  return {
1047
- content: listSummary("Routes", context.routerId, paginated.length, total, parsed.offset),
1073
+ content: listContent(
1074
+ "Routes",
1075
+ context.routerId,
1076
+ paginated,
1077
+ total,
1078
+ parsed.offset,
1079
+ (r) => compactFields(r, ["dst-address", "gateway", "distance", "routing-table", "active", "disabled"])
1080
+ ),
1048
1081
  structuredContent: {
1049
1082
  routerId: context.routerId,
1050
1083
  routes: paginated,
@@ -1301,12 +1334,24 @@ var listFirewallRulesTool = {
1301
1334
  }
1302
1335
  const { items: paginated, total, hasMore } = paginate(rules, parsed.offset, parsed.limit);
1303
1336
  return {
1304
- content: listSummary(
1337
+ content: listContent(
1305
1338
  `Firewall ${parsed.table} rules`,
1306
1339
  context.routerId,
1307
- paginated.length,
1340
+ paginated,
1308
1341
  total,
1309
- parsed.offset
1342
+ parsed.offset,
1343
+ (r) => compactFields(r, [
1344
+ "chain",
1345
+ "action",
1346
+ "protocol",
1347
+ "src-address",
1348
+ "dst-address",
1349
+ "dst-port",
1350
+ "in-interface",
1351
+ "out-interface",
1352
+ "disabled",
1353
+ "comment"
1354
+ ])
1310
1355
  ),
1311
1356
  structuredContent: {
1312
1357
  routerId: context.routerId,
@@ -1751,9 +1796,36 @@ var getLogInputSchema = z9.object({
1751
1796
  'Filter entries whose topics field contains any of these strings (e.g. ["firewall", "dhcp"])'
1752
1797
  ),
1753
1798
  prefix: z9.string().optional().describe("Substring to match against the log message"),
1754
- sinceMinutes: z9.number().int().min(1).max(1440).optional().describe("Only return entries from the last N minutes (1\u20131440)")
1799
+ sinceMinutes: z9.number().int().min(1).max(1440).optional().describe(
1800
+ "Only return entries from the last N minutes (1\u20131440), measured against the router's clock"
1801
+ )
1755
1802
  }).strict();
1803
+ var MONTHS = {
1804
+ jan: 0,
1805
+ feb: 1,
1806
+ mar: 2,
1807
+ apr: 3,
1808
+ may: 4,
1809
+ jun: 5,
1810
+ jul: 6,
1811
+ aug: 7,
1812
+ sep: 8,
1813
+ oct: 9,
1814
+ nov: 10,
1815
+ dec: 11
1816
+ };
1756
1817
  function parseRouterOsTimestamp(ts, now) {
1818
+ const isoDateTime = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(ts);
1819
+ if (isoDateTime) {
1820
+ return new Date(
1821
+ parseInt(isoDateTime[1], 10),
1822
+ parseInt(isoDateTime[2], 10) - 1,
1823
+ parseInt(isoDateTime[3], 10),
1824
+ parseInt(isoDateTime[4], 10),
1825
+ parseInt(isoDateTime[5], 10),
1826
+ parseInt(isoDateTime[6], 10)
1827
+ );
1828
+ }
1757
1829
  const timeOnly = /^(\d{2}):(\d{2}):(\d{2})$/.exec(ts);
1758
1830
  if (timeOnly) {
1759
1831
  const d = new Date(now);
@@ -1761,20 +1833,6 @@ function parseRouterOsTimestamp(ts, now) {
1761
1833
  if (d > now) d.setDate(d.getDate() - 1);
1762
1834
  return d;
1763
1835
  }
1764
- const MONTHS = {
1765
- jan: 0,
1766
- feb: 1,
1767
- mar: 2,
1768
- apr: 3,
1769
- may: 4,
1770
- jun: 5,
1771
- jul: 6,
1772
- aug: 7,
1773
- sep: 8,
1774
- oct: 9,
1775
- nov: 10,
1776
- dec: 11
1777
- };
1778
1836
  const monthDay = /^([a-z]{3})\/(\d{2}) (\d{2}):(\d{2}):(\d{2})$/i.exec(ts);
1779
1837
  if (monthDay) {
1780
1838
  const month = MONTHS[monthDay[1].toLowerCase()];
@@ -1792,10 +1850,29 @@ function parseRouterOsTimestamp(ts, now) {
1792
1850
  }
1793
1851
  return null;
1794
1852
  }
1853
+ function parseRouterClock(dateStr, timeStr) {
1854
+ if (!dateStr || !timeStr) return null;
1855
+ const time = /^(\d{2}):(\d{2}):(\d{2})$/.exec(timeStr);
1856
+ if (!time) return null;
1857
+ const hh = parseInt(time[1], 10);
1858
+ const mm = parseInt(time[2], 10);
1859
+ const ss = parseInt(time[3], 10);
1860
+ const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
1861
+ if (iso) {
1862
+ return new Date(parseInt(iso[1], 10), parseInt(iso[2], 10) - 1, parseInt(iso[3], 10), hh, mm, ss);
1863
+ }
1864
+ const named = /^([a-z]{3})\/(\d{2})\/(\d{4})$/i.exec(dateStr);
1865
+ if (named) {
1866
+ const month = MONTHS[named[1].toLowerCase()];
1867
+ if (month === void 0) return null;
1868
+ return new Date(parseInt(named[3], 10), month, parseInt(named[2], 10), hh, mm, ss);
1869
+ }
1870
+ return null;
1871
+ }
1795
1872
  var getLogTool = {
1796
1873
  name: "get_log",
1797
1874
  title: "Get Log",
1798
- description: "Read and filter the system log from a MikroTik router. Supports filtering by topic, message prefix, and a time window (last N minutes). Entries with unparseable timestamps are included conservatively.",
1875
+ description: "Read and filter the system log from a MikroTik router. Supports filtering by topic, message prefix, and a time window (last N minutes) measured against the router's own clock. Entries with unparseable timestamps are included conservatively.",
1799
1876
  inputSchema: getLogInputSchema,
1800
1877
  annotations: {
1801
1878
  readOnlyHint: true,
@@ -1811,7 +1888,18 @@ var getLogTool = {
1811
1888
  );
1812
1889
  try {
1813
1890
  const allEntries = await context.routerClient.get("log");
1814
- const now = /* @__PURE__ */ new Date();
1891
+ let now = /* @__PURE__ */ new Date();
1892
+ if (parsed.sinceMinutes !== void 0) {
1893
+ try {
1894
+ const clockResult = await context.routerClient.get("system/clock");
1895
+ const clock = Array.isArray(clockResult) ? clockResult[0] : clockResult;
1896
+ const routerNow = parseRouterClock(clock?.date, clock?.time);
1897
+ if (routerNow !== null) now = routerNow;
1898
+ else log7.warn({ routerId: context.routerId }, "Unparseable router clock; using host clock");
1899
+ } catch {
1900
+ log7.warn({ routerId: context.routerId }, "Router clock unavailable; using host clock");
1901
+ }
1902
+ }
1815
1903
  const cutoff = parsed.sinceMinutes !== void 0 ? new Date(now.getTime() - parsed.sinceMinutes * 6e4) : null;
1816
1904
  let filtered = allEntries.map((e) => e);
1817
1905
  if (parsed.topics && parsed.topics.length > 0) {
@@ -2168,7 +2256,14 @@ var listBridgesTool = {
2168
2256
  });
2169
2257
  const { items: paginated, total, hasMore } = paginate(enriched, parsed.offset, parsed.limit);
2170
2258
  return {
2171
- content: listSummary("Bridges", context.routerId, paginated.length, total, parsed.offset),
2259
+ content: listContent(
2260
+ "Bridges",
2261
+ context.routerId,
2262
+ paginated,
2263
+ total,
2264
+ parsed.offset,
2265
+ (b) => compactFields(b, ["name", "protocol-mode", "vlan-filtering", "mtu", "disabled", "comment"])
2266
+ ),
2172
2267
  structuredContent: {
2173
2268
  routerId: context.routerId,
2174
2269
  bridges: paginated,
@@ -2420,7 +2515,14 @@ var listWifiTool = {
2420
2515
  });
2421
2516
  const { items: paginated, total, hasMore } = paginate(interfaces, parsed.offset, parsed.limit);
2422
2517
  return {
2423
- content: listSummary("WiFi interfaces", context.routerId, paginated.length, total, parsed.offset),
2518
+ content: listContent(
2519
+ "WiFi interfaces",
2520
+ context.routerId,
2521
+ paginated,
2522
+ total,
2523
+ parsed.offset,
2524
+ (i) => compactFields(i, ["name", "master-interface", "mac-address", "disabled", "comment"])
2525
+ ),
2424
2526
  structuredContent: {
2425
2527
  routerId: context.routerId,
2426
2528
  interfaces: paginated,
@@ -2461,7 +2563,14 @@ var listWifiClientsTool = {
2461
2563
  const allClients = await context.routerClient.get(path, { filter });
2462
2564
  const { items: clients, total, hasMore } = paginate(allClients, parsed.offset, parsed.limit);
2463
2565
  return {
2464
- content: listSummary("WiFi clients", context.routerId, clients.length, total, parsed.offset),
2566
+ content: listContent(
2567
+ "WiFi clients",
2568
+ context.routerId,
2569
+ clients,
2570
+ total,
2571
+ parsed.offset,
2572
+ (c) => compactFields(c, ["interface", "mac-address", "signal", "tx-rate", "rx-rate", "uptime"])
2573
+ ),
2465
2574
  structuredContent: {
2466
2575
  routerId: context.routerId,
2467
2576
  clients,
@@ -2593,12 +2702,13 @@ var listWgTool = {
2593
2702
  });
2594
2703
  const { items: paginated, total, hasMore } = paginate(interfaces, parsed.offset, parsed.limit);
2595
2704
  return {
2596
- content: listSummary(
2705
+ content: listContent(
2597
2706
  "WireGuard interfaces",
2598
2707
  context.routerId,
2599
- paginated.length,
2708
+ paginated,
2600
2709
  total,
2601
- parsed.offset
2710
+ parsed.offset,
2711
+ (i) => compactFields(i, ["name", "listen-port", "mtu", "running", "disabled", "comment"])
2602
2712
  ),
2603
2713
  structuredContent: {
2604
2714
  routerId: context.routerId,
@@ -2644,7 +2754,14 @@ var listPeersTool = {
2644
2754
  });
2645
2755
  const { items: peers, total, hasMore } = paginate(allPeers, parsed.offset, parsed.limit);
2646
2756
  return {
2647
- content: listSummary("WireGuard peers", context.routerId, peers.length, total, parsed.offset),
2757
+ content: listContent(
2758
+ "WireGuard peers",
2759
+ context.routerId,
2760
+ peers,
2761
+ total,
2762
+ parsed.offset,
2763
+ (p) => compactFields(p, ["interface", "public-key", "endpoint-address", "allowed-address", "disabled"])
2764
+ ),
2648
2765
  structuredContent: {
2649
2766
  routerId: context.routerId,
2650
2767
  peers,
@@ -2939,7 +3056,14 @@ var listDnsTool = {
2939
3056
  }
2940
3057
  const { items: paginated, total, hasMore } = paginate(entries, parsed.offset, parsed.limit);
2941
3058
  return {
2942
- content: listSummary("DNS entries", context.routerId, paginated.length, total, parsed.offset),
3059
+ content: listContent(
3060
+ "DNS entries",
3061
+ context.routerId,
3062
+ paginated,
3063
+ total,
3064
+ parsed.offset,
3065
+ (e) => compactFields(e, ["name", "type", "address", "cname", "ttl", "disabled"])
3066
+ ),
2943
3067
  structuredContent: {
2944
3068
  routerId: context.routerId,
2945
3069
  entries: paginated,
@@ -3260,7 +3384,23 @@ var listMangleRulesTool = {
3260
3384
  });
3261
3385
  }
3262
3386
  return {
3263
- content: `Mangle rules on ${context.routerId}: ${rules.length} rule(s).`,
3387
+ content: listContent(
3388
+ "Mangle rules",
3389
+ context.routerId,
3390
+ rules,
3391
+ rules.length,
3392
+ 0,
3393
+ (r) => compactFields(r, [
3394
+ "chain",
3395
+ "action",
3396
+ "new-packet-mark",
3397
+ "new-connection-mark",
3398
+ "new-routing-mark",
3399
+ "passthrough",
3400
+ "disabled",
3401
+ "comment"
3402
+ ])
3403
+ ),
3264
3404
  structuredContent: { routerId: context.routerId, rules, total: rules.length }
3265
3405
  };
3266
3406
  } catch (err) {
@@ -3510,7 +3650,14 @@ var listAddressListTool = {
3510
3650
  offset: void 0
3511
3651
  });
3512
3652
  return {
3513
- content: `Address list entries on ${context.routerId}: ${entries.length} entry(ies).`,
3653
+ content: listContent(
3654
+ "Address list entries",
3655
+ context.routerId,
3656
+ entries,
3657
+ entries.length,
3658
+ 0,
3659
+ (e) => compactFields(e, ["list", "address", "timeout", "dynamic", "disabled", "comment"])
3660
+ ),
3514
3661
  structuredContent: { routerId: context.routerId, entries, total: entries.length }
3515
3662
  };
3516
3663
  } catch (err) {
@@ -3664,7 +3811,14 @@ var listRoutingRulesTool = {
3664
3811
  });
3665
3812
  }
3666
3813
  return {
3667
- content: `Routing rules on ${context.routerId}: ${rules.length} rule(s).`,
3814
+ content: listContent(
3815
+ "Routing rules",
3816
+ context.routerId,
3817
+ rules,
3818
+ rules.length,
3819
+ 0,
3820
+ (r) => compactFields(r, ["src-address", "dst-address", "action", "table", "interface", "disabled"])
3821
+ ),
3668
3822
  structuredContent: { routerId: context.routerId, rules, total: rules.length }
3669
3823
  };
3670
3824
  } catch (err) {
@@ -3865,7 +4019,14 @@ var listRoutingTablesTool = {
3865
4019
  offset: void 0
3866
4020
  });
3867
4021
  return {
3868
- content: `Routing tables on ${context.routerId}: ${tables.length} table(s).`,
4022
+ content: listContent(
4023
+ "Routing tables",
4024
+ context.routerId,
4025
+ tables,
4026
+ tables.length,
4027
+ 0,
4028
+ (t) => compactFields(t, ["name", "fib", "disabled"])
4029
+ ),
3869
4030
  structuredContent: { routerId: context.routerId, tables, total: tables.length }
3870
4031
  };
3871
4032
  } catch (err) {
@@ -4001,7 +4162,14 @@ var listBgpPeersTool = {
4001
4162
  sessions = sessions.filter((s) => s.state === parsed.state);
4002
4163
  }
4003
4164
  return {
4004
- content: `BGP sessions on ${context.routerId}: ${sessions.length} session(s).`,
4165
+ content: listContent(
4166
+ "BGP sessions",
4167
+ context.routerId,
4168
+ sessions,
4169
+ sessions.length,
4170
+ 0,
4171
+ (s) => compactFields(s, ["name", "remote.address", "remote.as", "established", "uptime"])
4172
+ ),
4005
4173
  structuredContent: { routerId: context.routerId, sessions, total: sessions.length }
4006
4174
  };
4007
4175
  } catch (err) {
@@ -4036,7 +4204,14 @@ var listOspfNeighborsTool = {
4036
4204
  neighbors = neighbors.filter((n) => n.state === parsed.state);
4037
4205
  }
4038
4206
  return {
4039
- content: `OSPF neighbors on ${context.routerId}: ${neighbors.length} neighbor(s).`,
4207
+ content: listContent(
4208
+ "OSPF neighbors",
4209
+ context.routerId,
4210
+ neighbors,
4211
+ neighbors.length,
4212
+ 0,
4213
+ (n) => compactFields(n, ["instance", "router-id", "address", "state", "state-changes"])
4214
+ ),
4040
4215
  structuredContent: { routerId: context.routerId, neighbors, total: neighbors.length }
4041
4216
  };
4042
4217
  } catch (err) {
@@ -4080,7 +4255,14 @@ var listScriptsTool = {
4080
4255
  );
4081
4256
  }
4082
4257
  return {
4083
- content: `Scripts on ${context.routerId}: ${scripts.length} script(s).`,
4258
+ content: listContent(
4259
+ "Scripts",
4260
+ context.routerId,
4261
+ scripts,
4262
+ scripts.length,
4263
+ 0,
4264
+ (s) => compactFields(s, ["name", "owner", "policy", "run-count", "last-started"])
4265
+ ),
4084
4266
  structuredContent: { routerId: context.routerId, scripts, total: scripts.length }
4085
4267
  };
4086
4268
  } catch (err) {
@@ -4319,7 +4501,14 @@ var listScheduledJobsTool = {
4319
4501
  jobs = jobs.filter((j) => j.name === parsed.name);
4320
4502
  }
4321
4503
  return {
4322
- content: `Scheduled jobs on ${context.routerId}: ${jobs.length} job(s).`,
4504
+ content: listContent(
4505
+ "Scheduled jobs",
4506
+ context.routerId,
4507
+ jobs,
4508
+ jobs.length,
4509
+ 0,
4510
+ (j) => compactFields(j, ["name", "start-time", "interval", "on-event", "disabled"])
4511
+ ),
4323
4512
  structuredContent: { routerId: context.routerId, jobs, total: jobs.length }
4324
4513
  };
4325
4514
  } catch (err) {
@@ -4565,7 +4754,14 @@ var listPackagesTool = {
4565
4754
  packages = packages.filter((p) => p.name === parsed.name);
4566
4755
  }
4567
4756
  return {
4568
- content: `Packages on ${context.routerId}: ${packages.length} package(s).`,
4757
+ content: listContent(
4758
+ "Packages",
4759
+ context.routerId,
4760
+ packages,
4761
+ packages.length,
4762
+ 0,
4763
+ (p) => compactFields(p, ["name", "version", "disabled", "scheduled"])
4764
+ ),
4569
4765
  structuredContent: { routerId: context.routerId, packages, total: packages.length }
4570
4766
  };
4571
4767
  } catch (err) {
@@ -4687,7 +4883,14 @@ var listFilesTool = {
4687
4883
  files = files.filter((f) => f.type === parsed.type);
4688
4884
  }
4689
4885
  return {
4690
- content: `Files on ${context.routerId}: ${files.length} file(s).`,
4886
+ content: listContent(
4887
+ "Files",
4888
+ context.routerId,
4889
+ files,
4890
+ files.length,
4891
+ 0,
4892
+ (f) => compactFields(f, ["name", "type", "size", "creation-time"])
4893
+ ),
4691
4894
  structuredContent: { routerId: context.routerId, files, total: files.length }
4692
4895
  };
4693
4896
  } catch (err) {
@@ -4889,7 +5092,14 @@ var listContainersTool = {
4889
5092
  offset: void 0
4890
5093
  });
4891
5094
  return {
4892
- content: `Containers on ${context.routerId}: ${containers.length} container(s).`,
5095
+ content: listContent(
5096
+ "Containers",
5097
+ context.routerId,
5098
+ containers,
5099
+ containers.length,
5100
+ 0,
5101
+ (c) => compactFields(c, ["name", "tag", "status", "interface", "root-dir"])
5102
+ ),
4893
5103
  structuredContent: { routerId: context.routerId, containers, total: containers.length }
4894
5104
  };
4895
5105
  } catch (err) {
@@ -5555,7 +5765,14 @@ var listIpsecPeersTool = {
5555
5765
  ) : allPeers;
5556
5766
  const peers = filtered.slice(0, parsed.limit);
5557
5767
  return {
5558
- content: `IPSec peers on ${context.routerId}: ${peers.length} returned (${allPeers.length} total)`,
5768
+ content: listContent(
5769
+ "IPSec peers",
5770
+ context.routerId,
5771
+ peers,
5772
+ allPeers.length,
5773
+ 0,
5774
+ (p) => compactFields(p, ["name", "address", "profile", "exchange-mode", "disabled"])
5775
+ ),
5559
5776
  structuredContent: {
5560
5777
  routerId: context.routerId,
5561
5778
  peers,
@@ -5600,7 +5817,14 @@ var listIpsecPoliciesTool = {
5600
5817
  );
5601
5818
  const policies = filtered.slice(0, parsed.limit);
5602
5819
  return {
5603
- content: `IPSec policies on ${context.routerId}: ${policies.length} returned (${allPolicies.length} total)`,
5820
+ content: listContent(
5821
+ "IPSec policies",
5822
+ context.routerId,
5823
+ policies,
5824
+ allPolicies.length,
5825
+ 0,
5826
+ (p) => compactFields(p, ["src-address", "dst-address", "protocol", "action", "level", "disabled"])
5827
+ ),
5604
5828
  structuredContent: {
5605
5829
  routerId: context.routerId,
5606
5830
  policies,
@@ -5974,7 +6198,14 @@ var listCertificatesTool = {
5974
6198
  });
5975
6199
  const certs = filtered.slice(0, parsed.limit);
5976
6200
  return {
5977
- content: `Certificates on ${context.routerId}: ${certs.length} returned (${allCerts.length} total)`,
6201
+ content: listContent(
6202
+ "Certificates",
6203
+ context.routerId,
6204
+ certs,
6205
+ allCerts.length,
6206
+ 0,
6207
+ (c) => compactFields(c, ["name", "common-name", "invalid-after", "expired", "revoked"])
6208
+ ),
5978
6209
  structuredContent: {
5979
6210
  routerId: context.routerId,
5980
6211
  certificates: certs,
@@ -6133,7 +6364,14 @@ var listUsersTool = {
6133
6364
  const filtered = parsed.group ? safeUsers.filter((u) => u.group === parsed.group) : safeUsers;
6134
6365
  const users = filtered.slice(0, parsed.limit);
6135
6366
  return {
6136
- content: `Users on ${context.routerId}: ${users.length} returned (${allUsers.length} total)`,
6367
+ content: listContent(
6368
+ "Users",
6369
+ context.routerId,
6370
+ users,
6371
+ allUsers.length,
6372
+ 0,
6373
+ (u) => compactFields(u, ["name", "group", "address", "disabled", "comment"])
6374
+ ),
6137
6375
  structuredContent: {
6138
6376
  routerId: context.routerId,
6139
6377
  users,
@@ -6408,7 +6646,14 @@ var listUserGroupsTool = {
6408
6646
  });
6409
6647
  const groups = all.slice(0, parsed.limit);
6410
6648
  return {
6411
- content: `User groups on ${context.routerId}: ${groups.length} returned (${all.length} total)`,
6649
+ content: listContent(
6650
+ "User groups",
6651
+ context.routerId,
6652
+ groups,
6653
+ all.length,
6654
+ 0,
6655
+ (g) => compactFields(g, ["name", "policy", "comment"])
6656
+ ),
6412
6657
  structuredContent: {
6413
6658
  routerId: context.routerId,
6414
6659
  groups,
@@ -6620,7 +6865,14 @@ var listDhcpServersTool = {
6620
6865
  let filtered = parsed.interface ? allServers.filter((s) => s.interface === parsed.interface) : allServers;
6621
6866
  const { items: servers, total, hasMore } = paginate(filtered, parsed.offset, parsed.limit);
6622
6867
  return {
6623
- content: `DHCP servers on ${context.routerId}: ${total} total, showing ${servers.length} (offset ${parsed.offset})`,
6868
+ content: listContent(
6869
+ "DHCP servers",
6870
+ context.routerId,
6871
+ servers,
6872
+ total,
6873
+ parsed.offset,
6874
+ (s) => compactFields(s, ["name", "interface", "address-pool", "lease-time", "disabled"])
6875
+ ),
6624
6876
  structuredContent: {
6625
6877
  routerId: context.routerId,
6626
6878
  servers,
@@ -6853,7 +7105,14 @@ var listIpPoolsTool = {
6853
7105
  }
6854
7106
  const { items: pools, total, hasMore } = paginate(filtered, parsed.offset, parsed.limit);
6855
7107
  return {
6856
- content: `IP pools on ${context.routerId}: ${total} total, showing ${pools.length} (offset ${parsed.offset})`,
7108
+ content: listContent(
7109
+ "IP pools",
7110
+ context.routerId,
7111
+ pools,
7112
+ total,
7113
+ parsed.offset,
7114
+ (p) => compactFields(p, ["name", "ranges", "next-pool"])
7115
+ ),
6857
7116
  structuredContent: {
6858
7117
  routerId: context.routerId,
6859
7118
  pools,
@@ -7006,7 +7265,14 @@ var listQueuesTool = {
7006
7265
  ) : allQueues;
7007
7266
  const queues = filtered.slice(0, parsed.limit);
7008
7267
  return {
7009
- content: `Queues on ${context.routerId}: ${queues.length} returned (${allQueues.length} total)`,
7268
+ content: listContent(
7269
+ "Queues",
7270
+ context.routerId,
7271
+ queues,
7272
+ allQueues.length,
7273
+ 0,
7274
+ (q) => compactFields(q, ["name", "target", "max-limit", "limit-at", "disabled", "comment"])
7275
+ ),
7010
7276
  structuredContent: {
7011
7277
  routerId: context.routerId,
7012
7278
  queues,
@@ -7211,7 +7477,14 @@ var listVrrpInstancesTool = {
7211
7477
  ) : allInstances;
7212
7478
  const instances = filtered.slice(0, parsed.limit);
7213
7479
  return {
7214
- content: `VRRP instances on ${context.routerId}: ${instances.length} returned (${allInstances.length} total)`,
7480
+ content: listContent(
7481
+ "VRRP instances",
7482
+ context.routerId,
7483
+ instances,
7484
+ allInstances.length,
7485
+ 0,
7486
+ (i) => compactFields(i, ["name", "interface", "vrid", "priority", "disabled"])
7487
+ ),
7215
7488
  structuredContent: {
7216
7489
  routerId: context.routerId,
7217
7490
  instances,
@@ -7527,7 +7800,14 @@ var listNetwatchEntriesTool = {
7527
7800
  const filtered = allEntries.filter((e) => parsed.host ? (e.host ?? "").includes(parsed.host) : true).filter((e) => parsed.status ? e.status === parsed.status : true);
7528
7801
  const entries = filtered.slice(0, parsed.limit);
7529
7802
  return {
7530
- content: `Netwatch entries on ${context.routerId}: ${entries.length} returned (${allEntries.length} total)`,
7803
+ content: listContent(
7804
+ "Netwatch entries",
7805
+ context.routerId,
7806
+ entries,
7807
+ allEntries.length,
7808
+ 0,
7809
+ (e) => compactFields(e, ["host", "type", "status", "interval", "disabled", "comment"])
7810
+ ),
7531
7811
  structuredContent: {
7532
7812
  routerId: context.routerId,
7533
7813
  entries,
@@ -7724,7 +8004,14 @@ var listNeighborsTool = {
7724
8004
  ) : allNeighbors;
7725
8005
  const neighbors = filtered.slice(0, parsed.limit);
7726
8006
  return {
7727
- content: `Neighbors on ${context.routerId}: ${neighbors.length} returned (${allNeighbors.length} total)`,
8007
+ content: listContent(
8008
+ "Neighbors",
8009
+ context.routerId,
8010
+ neighbors,
8011
+ allNeighbors.length,
8012
+ 0,
8013
+ (n) => compactFields(n, ["interface", "address", "mac-address", "identity", "platform", "board"])
8014
+ ),
7728
8015
  structuredContent: {
7729
8016
  routerId: context.routerId,
7730
8017
  neighbors,
@@ -7768,7 +8055,14 @@ var listArpEntriesTool = {
7768
8055
  );
7769
8056
  const entries = filtered.slice(0, parsed.limit);
7770
8057
  return {
7771
- content: `ARP entries on ${context.routerId}: ${entries.length} returned (${allEntries.length} total)`,
8058
+ content: listContent(
8059
+ "ARP entries",
8060
+ context.routerId,
8061
+ entries,
8062
+ allEntries.length,
8063
+ 0,
8064
+ (e) => compactFields(e, ["address", "mac-address", "interface", "status", "complete", "dynamic"])
8065
+ ),
7772
8066
  structuredContent: {
7773
8067
  routerId: context.routerId,
7774
8068
  entries,
@@ -8069,7 +8363,14 @@ var listDhcpClientsTool = {
8069
8363
  }
8070
8364
  const { items: clients, total, hasMore } = paginate(filtered, parsed.offset, parsed.limit);
8071
8365
  return {
8072
- content: listSummary("DHCP clients", context.routerId, clients.length, total, parsed.offset),
8366
+ content: listContent(
8367
+ "DHCP clients",
8368
+ context.routerId,
8369
+ clients,
8370
+ total,
8371
+ parsed.offset,
8372
+ (c) => compactFields(c, ["interface", "status", "address", "gateway", "disabled"])
8373
+ ),
8073
8374
  structuredContent: {
8074
8375
  routerId: context.routerId,
8075
8376
  clients,
@@ -8255,7 +8556,14 @@ var listIpServicesTool = {
8255
8556
  });
8256
8557
  }
8257
8558
  return {
8258
- content: `IP services on ${context.routerId}: ${filtered.length} returned. Full records in structuredContent.`,
8559
+ content: listContent(
8560
+ "IP services",
8561
+ context.routerId,
8562
+ filtered,
8563
+ filtered.length,
8564
+ 0,
8565
+ (s) => compactFields(s, ["name", "port", "address", "certificate", "disabled"])
8566
+ ),
8259
8567
  structuredContent: {
8260
8568
  routerId: context.routerId,
8261
8569
  services: filtered,
@@ -8391,7 +8699,14 @@ var listPppoeClientsTool = {
8391
8699
  }
8392
8700
  const { items: clients, total, hasMore } = paginate(filtered, parsed.offset, parsed.limit);
8393
8701
  return {
8394
- content: listSummary("PPPoE clients", context.routerId, clients.length, total, parsed.offset),
8702
+ content: listContent(
8703
+ "PPPoE clients",
8704
+ context.routerId,
8705
+ clients,
8706
+ total,
8707
+ parsed.offset,
8708
+ (c) => compactFields(c, ["name", "interface", "status", "user", "disabled"])
8709
+ ),
8395
8710
  structuredContent: {
8396
8711
  routerId: context.routerId,
8397
8712
  clients,
@@ -8649,7 +8964,14 @@ var listOvpnClientsTool = {
8649
8964
  });
8650
8965
  const { items: clients, total, hasMore } = paginate(allClients, parsed.offset, parsed.limit);
8651
8966
  return {
8652
- content: `OpenVPN clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`,
8967
+ content: listContent(
8968
+ "OpenVPN clients",
8969
+ context.routerId,
8970
+ clients,
8971
+ total,
8972
+ parsed.offset,
8973
+ (c) => compactFields(c, ["name", "connect-to", "user", "profile", "disabled"])
8974
+ ),
8653
8975
  structuredContent: {
8654
8976
  routerId: context.routerId,
8655
8977
  clients,
@@ -9051,7 +9373,14 @@ var listPppProfilesTool = {
9051
9373
  const filtered = parsed.name ? all.filter((p) => p.name === parsed.name) : all;
9052
9374
  const profiles = filtered.slice(0, parsed.limit);
9053
9375
  return {
9054
- content: `PPP profiles on ${context.routerId}: ${profiles.length} returned`,
9376
+ content: listContent(
9377
+ "PPP profiles",
9378
+ context.routerId,
9379
+ profiles,
9380
+ all.length,
9381
+ 0,
9382
+ (p) => compactFields(p, ["name", "local-address", "remote-address", "dns-server", "rate-limit"])
9383
+ ),
9055
9384
  structuredContent: { routerId: context.routerId, profiles, total: all.length, returned: profiles.length }
9056
9385
  };
9057
9386
  } catch (err) {
@@ -9440,7 +9769,14 @@ var listLogRulesTool = {
9440
9769
  }
9441
9770
  const returned = rules.slice(0, parsed.limit);
9442
9771
  return {
9443
- content: `Log rules on ${context.routerId}: ${returned.length} rule(s) (${all.length} total).`,
9772
+ content: listContent(
9773
+ "Log rules",
9774
+ context.routerId,
9775
+ returned,
9776
+ all.length,
9777
+ 0,
9778
+ (r) => compactFields(r, ["topics", "action", "prefix", "disabled"])
9779
+ ),
9444
9780
  structuredContent: {
9445
9781
  routerId: context.routerId,
9446
9782
  rules: returned,
@@ -9631,7 +9967,14 @@ var listLogActionsTool = {
9631
9967
  }
9632
9968
  const returned = actions.slice(0, parsed.limit);
9633
9969
  return {
9634
- content: `Log actions on ${context.routerId}: ${returned.length} action(s) (${all.length} total).`,
9970
+ content: listContent(
9971
+ "Log actions",
9972
+ context.routerId,
9973
+ returned,
9974
+ all.length,
9975
+ 0,
9976
+ (a) => compactFields(a, ["name", "target", "type", "remote"])
9977
+ ),
9635
9978
  structuredContent: {
9636
9979
  routerId: context.routerId,
9637
9980
  actions: returned,
@@ -9898,7 +10241,20 @@ var listConnectionsTool = {
9898
10241
  const filtered = all.filter((c) => parsed.srcAddress ? (c["src-address"] ?? "").includes(parsed.srcAddress) : true).filter((c) => parsed.dstAddress ? (c["dst-address"] ?? "").includes(parsed.dstAddress) : true).filter((c) => parsed.protocol ? c.protocol === parsed.protocol : true);
9899
10242
  const connections = filtered.slice(0, parsed.limit);
9900
10243
  return {
9901
- content: `Active connections on ${context.routerId}: ${connections.length} returned (${all.length} total)`,
10244
+ content: listContent(
10245
+ "Active connections",
10246
+ context.routerId,
10247
+ connections,
10248
+ all.length,
10249
+ 0,
10250
+ (c) => compactFields(c, [
10251
+ "protocol",
10252
+ "src-address",
10253
+ "dst-address",
10254
+ "tcp-state",
10255
+ "connection-mark"
10256
+ ])
10257
+ ),
9902
10258
  structuredContent: { routerId: context.routerId, connections, total: all.length, returned: connections.length }
9903
10259
  };
9904
10260
  } catch (err) {
@@ -10021,7 +10377,14 @@ var listContainerEnvsTool = {
10021
10377
  const filtered = parsed.name ? all.filter((e) => e.name === parsed.name) : all;
10022
10378
  const envs = filtered.slice(0, parsed.limit);
10023
10379
  return {
10024
- content: `Container envs on ${context.routerId}: ${envs.length} returned`,
10380
+ content: listContent(
10381
+ "Container envs",
10382
+ context.routerId,
10383
+ envs,
10384
+ all.length,
10385
+ 0,
10386
+ (e) => compactFields(e, ["name", "key", "value"])
10387
+ ),
10025
10388
  structuredContent: { routerId: context.routerId, envs, total: all.length, returned: envs.length }
10026
10389
  };
10027
10390
  } catch (err) {
@@ -10158,7 +10521,14 @@ var listContainerMountsTool = {
10158
10521
  const filtered = parsed.name ? all.filter((m) => m.name === parsed.name) : all;
10159
10522
  const mounts = filtered.slice(0, parsed.limit);
10160
10523
  return {
10161
- content: `Container mounts on ${context.routerId}: ${mounts.length} returned`,
10524
+ content: listContent(
10525
+ "Container mounts",
10526
+ context.routerId,
10527
+ mounts,
10528
+ all.length,
10529
+ 0,
10530
+ (m) => compactFields(m, ["name", "src", "dst"])
10531
+ ),
10162
10532
  structuredContent: { routerId: context.routerId, mounts, total: all.length, returned: mounts.length }
10163
10533
  };
10164
10534
  } catch (err) {
@@ -10298,7 +10668,14 @@ var listInterfaceListsTool = {
10298
10668
  });
10299
10669
  const lists = all.slice(0, parsed.limit);
10300
10670
  return {
10301
- content: `Interface lists on ${context.routerId}: ${lists.length} returned`,
10671
+ content: listContent(
10672
+ "Interface lists",
10673
+ context.routerId,
10674
+ lists,
10675
+ all.length,
10676
+ 0,
10677
+ (l) => compactFields(l, ["name", "include", "exclude", "comment"])
10678
+ ),
10302
10679
  structuredContent: { routerId: context.routerId, lists, total: all.length, returned: lists.length }
10303
10680
  };
10304
10681
  } catch (err) {
@@ -10783,6 +11160,54 @@ var bulkExecuteInputSchema = z46.object({
10783
11160
  "Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
10784
11161
  )
10785
11162
  }).strict();
11163
+ var listRoutersInputSchema = z46.object({
11164
+ tags: z46.array(z46.string()).optional().describe('Only return routers having any of these tags (e.g. ["edge", "prod"])')
11165
+ }).strict();
11166
+ var listRoutersTool = {
11167
+ name: "list_routers",
11168
+ title: "List Routers",
11169
+ description: "List the routers configured in the registry (routers.yaml): id, host, port, TLS status, tags, ROS version, and which is the default. Read-only reflection of local config \u2014 no RouterOS API call, no credentials in the response. Use it to discover valid routerId values and tags for targeting other tools (including bulk_execute).",
11170
+ inputSchema: listRoutersInputSchema,
11171
+ annotations: {
11172
+ readOnlyHint: true,
11173
+ destructiveHint: false,
11174
+ idempotentHint: true,
11175
+ openWorldHint: false
11176
+ },
11177
+ skipRouterContext: true,
11178
+ async handler(params, context) {
11179
+ const parsed = listRoutersInputSchema.parse(params);
11180
+ const { identity, appConfig } = context;
11181
+ const defaultRouter = appConfig.defaultRouter;
11182
+ log47.info({ tags: parsed.tags, identityId: identity.id }, "Listing routers");
11183
+ let routers = context.routerRegistry.listRouters(parsed.tags);
11184
+ if (identity.allowedRouters.length > 0) {
11185
+ routers = routers.filter((r) => identity.allowedRouters.includes(r.id));
11186
+ }
11187
+ const soleDefaultId = defaultRouter === void 0 && routers.length === 1 ? routers[0].id : void 0;
11188
+ const rows = routers.map((r) => ({
11189
+ id: r.id,
11190
+ host: r.host,
11191
+ port: r.port,
11192
+ tlsEnabled: r.tls.enabled,
11193
+ tags: r.tags,
11194
+ rosVersion: r.rosVersion,
11195
+ isDefault: defaultRouter !== void 0 ? r.id === defaultRouter : r.id === soleDefaultId
11196
+ }));
11197
+ const total = rows.length;
11198
+ const header = `Routers: ${total === 0 ? "none" : `1-${total} of ${total}`}.`;
11199
+ const lines = rows.map(
11200
+ (r) => ` ${compactFields(
11201
+ { ...r, tags: r.tags.join(",") },
11202
+ ["id", "host", "port", "tlsEnabled", "tags", "rosVersion", "isDefault"]
11203
+ )}`
11204
+ );
11205
+ return {
11206
+ content: total === 0 ? header : [header, ...lines].join("\n"),
11207
+ structuredContent: { routers: rows, total, returned: total }
11208
+ };
11209
+ }
11210
+ };
10786
11211
  function createFleetTools(baseTools2) {
10787
11212
  const toolMap = new Map(baseTools2.map((t) => [t.name, t]));
10788
11213
  const checkRouterHealthTool = {
@@ -11089,7 +11514,7 @@ function createFleetTools(baseTools2) {
11089
11514
  };
11090
11515
  }
11091
11516
  };
11092
- return [checkRouterHealthTool, bulkExecuteTool];
11517
+ return [checkRouterHealthTool, bulkExecuteTool, listRoutersTool];
11093
11518
  }
11094
11519
 
11095
11520
  // src/domain/tools/index.ts
@@ -12121,7 +12546,7 @@ var ConnectionPool = class {
12121
12546
  };
12122
12547
 
12123
12548
  // src/version.ts
12124
- var VERSION = "1.6.0";
12549
+ var VERSION = "1.7.0";
12125
12550
 
12126
12551
  // src/mcp/server.ts
12127
12552
  var SERVER_INSTRUCTIONS = [
@@ -12532,7 +12957,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
12532
12957
  import { homedir as homedir2 } from "os";
12533
12958
  import * as net from "net";
12534
12959
  import chalk from "chalk";
12535
- var localVersion = "1.6.0";
12960
+ var localVersion = "1.7.0";
12536
12961
  var results = [];
12537
12962
  function ok(msg) {
12538
12963
  console.log(chalk.green("\u2705 ") + msg);
@@ -13244,7 +13669,7 @@ async function runInit() {
13244
13669
  }
13245
13670
 
13246
13671
  // src/cli/index.ts
13247
- var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.6.0");
13672
+ var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.7.0");
13248
13673
  program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
13249
13674
  program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
13250
13675
  program.command("init").description("Interactive setup wizard").action(runInit);