mikromcp 1.14.0 → 1.15.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/README.md +5 -5
- package/dist/main.js +361 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
[](https://github.com/AliKarami/MikroMCP/actions/workflows/ci.yml)
|
|
14
14
|
[](https://github.com/AliKarami/MikroMCP/actions/workflows/release.yml)
|
|
15
|
-
[](package.json)
|
|
16
16
|
[](LICENSE)
|
|
17
17
|
[](package.json)
|
|
18
18
|
[](https://help.mikrotik.com/docs/display/ROS/REST+API)
|
|
19
19
|
[](https://modelcontextprotocol.io)
|
|
20
|
-
[](https://github.com/AliKarami/MikroMCP/wiki/Available-Tools)
|
|
21
21
|
[](https://glama.ai/mcp/servers/AliKarami/MikroMCP)
|
|
22
22
|
|
|
23
23
|
MikroMCP exists because raw router CLI access is the wrong abstraction for AI agents. RouterOS is powerful, but asking an LLM to improvise shell commands against production network gear is risky. MikroMCP gives agents a controlled tool surface: strict schemas, idempotent writes, dry-run previews, per-router circuit breakers, retry policies, RBAC, audit logs, snapshots, and rollback-aware change workflows.
|
|
@@ -54,7 +54,7 @@ That's the whole setup for a single-router stdio deployment. For standalone bina
|
|
|
54
54
|
| 🤖 **AI-agent fit** | Human-readable responses plus structured JSON content for reasoning, chaining, and automation; server advertises an `instructions` string on MCP initialize so clients self-configure; optional [`routerId` resolved via `MIKROMCP_DEFAULT_ROUTER`](https://github.com/AliKarami/MikroMCP/wiki/Configuration) for single-router setups; [usage skill](docs/wiki/Using-the-Skill.md) for safe, guided tool use in Claude Code |
|
|
55
55
|
| 🧩 **MCP compatibility** | stdio for desktop clients, Streamable HTTP and legacy SSE for remote or service-style clients |
|
|
56
56
|
|
|
57
|
-
**
|
|
57
|
+
**133 typed tools** in total — browse the full catalog with parameters, defaults, and copy-paste example prompts in **[Available Tools](https://github.com/AliKarami/MikroMCP/wiki/Available-Tools)**.
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
@@ -140,7 +140,7 @@ AI assistants can use MikroMCP to:
|
|
|
140
140
|
|
|
141
141
|
### What is MikroMCP?
|
|
142
142
|
|
|
143
|
-
MikroMCP is an open-source [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that exposes MikroTik RouterOS as
|
|
143
|
+
MikroMCP is an open-source [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that exposes MikroTik RouterOS as 133 typed, auditable tools — letting AI assistants inspect, diagnose, and safely operate routers in natural language instead of improvising CLI commands.
|
|
144
144
|
|
|
145
145
|
### MikroMCP vs RouterOS API
|
|
146
146
|
|
|
@@ -189,7 +189,7 @@ The README stays intentionally short. Everything below is documented in depth in
|
|
|
189
189
|
| [Connecting to Claude Desktop](https://github.com/AliKarami/MikroMCP/wiki/Connecting-to-Claude-Desktop) | Register MikroMCP in Claude Desktop |
|
|
190
190
|
| [Connecting to AI Assistants](https://github.com/AliKarami/MikroMCP/wiki/Connecting-to-AI-Assistants) | Claude Code, Cursor, Codex, HTTP/Docker/systemd |
|
|
191
191
|
| [Using the Skill](docs/wiki/Using-the-Skill.md) | Install the MikroMCP usage skill so your assistant drives the tools safely |
|
|
192
|
-
| [Available Tools](https://github.com/AliKarami/MikroMCP/wiki/Available-Tools) | All
|
|
192
|
+
| [Available Tools](https://github.com/AliKarami/MikroMCP/wiki/Available-Tools) | All 133 tools — parameters and example prompts |
|
|
193
193
|
| [Architecture](https://github.com/AliKarami/MikroMCP/wiki/Architecture) | System layers, request pipeline, auth model |
|
|
194
194
|
| [Error Handling](https://github.com/AliKarami/MikroMCP/wiki/Error-Handling) | Error categories, retry engine, circuit breaker |
|
|
195
195
|
| [Security](https://github.com/AliKarami/MikroMCP/wiki/Security) | Threat model, hardening checklist, vulnerability reporting |
|
package/dist/main.js
CHANGED
|
@@ -398,6 +398,13 @@ function loggableParams(tool, params) {
|
|
|
398
398
|
return { routerId: params.routerId, params: "<withheld: redaction failed>" };
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
|
+
function isEffectiveDryRun(tool, params) {
|
|
402
|
+
try {
|
|
403
|
+
return tool.inputSchema.parse(params).dryRun === true;
|
|
404
|
+
} catch {
|
|
405
|
+
return params.dryRun === true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
401
408
|
function snapshotPathsFor(tool, params) {
|
|
402
409
|
return typeof tool.snapshotPaths === "function" ? tool.snapshotPaths(params) : tool.snapshotPaths ?? [];
|
|
403
410
|
}
|
|
@@ -2666,7 +2673,7 @@ var runCommandInputSchema = z10.object({
|
|
|
2666
2673
|
var runCommandTool = {
|
|
2667
2674
|
name: "run_command",
|
|
2668
2675
|
title: "Run Command",
|
|
2669
|
-
description: "Execute an arbitrary RouterOS console command via SSH. Guarded by an allow/deny policy (built-in deny list blocks destructive commands; tighten via cmdAllow in routers.yaml or MIKROMCP_CMD_ALLOW). Prefer dedicated tools (reboot, etc.) where available. Output capped at 4000 characters.",
|
|
2676
|
+
description: "Execute an arbitrary RouterOS console command via SSH. Guarded by an allow/deny policy (built-in deny list blocks destructive commands; tighten via cmdAllow in routers.yaml or MIKROMCP_CMD_ALLOW). Prefer dedicated tools (reboot, etc.) where available. Output capped at 4000 characters. Journaled for audit like every write (see list_journal), but never snapshotted: arbitrary commands touch unknown config sections, so it cannot be rolled back via rollback_change \u2014 prefer a dedicated tool when one exists, and note the manual undo command in your own records.",
|
|
2670
2677
|
inputSchema: runCommandInputSchema,
|
|
2671
2678
|
annotations: {
|
|
2672
2679
|
readOnlyHint: false,
|
|
@@ -2725,6 +2732,26 @@ var systemOpsTools = [
|
|
|
2725
2732
|
|
|
2726
2733
|
// src/domain/tools/bridge-tools.ts
|
|
2727
2734
|
import { z as z11 } from "zod";
|
|
2735
|
+
|
|
2736
|
+
// src/domain/tools/interface-name.ts
|
|
2737
|
+
var NEW_INTERFACE_NAME_MAX = 64;
|
|
2738
|
+
var NEW_INTERFACE_NAME_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
2739
|
+
function assertNewInterfaceName(name, opts) {
|
|
2740
|
+
const tooLong = name.length > NEW_INTERFACE_NAME_MAX;
|
|
2741
|
+
if (!tooLong && NEW_INTERFACE_NAME_PATTERN.test(name)) return;
|
|
2742
|
+
throw new MikroMCPError({
|
|
2743
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
2744
|
+
code: opts.code,
|
|
2745
|
+
message: tooLong ? `${opts.kind} name is ${name.length} chars; ${opts.action} allows at most ${NEW_INTERFACE_NAME_MAX}.` : `${opts.kind} name "${name}" uses characters ${opts.action} does not allow (permitted: letters, digits, dot, hyphen, underscore).`,
|
|
2746
|
+
details: { name },
|
|
2747
|
+
recoverability: {
|
|
2748
|
+
retryable: false,
|
|
2749
|
+
suggestedAction: `Use a name of letters, digits, dot, hyphen or underscore, at most ${NEW_INTERFACE_NAME_MAX} chars.`
|
|
2750
|
+
}
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
// src/domain/tools/bridge-tools.ts
|
|
2728
2755
|
var log9 = createLogger("bridge-tools");
|
|
2729
2756
|
var listBridgesInputSchema = z11.object({
|
|
2730
2757
|
routerId,
|
|
@@ -2865,7 +2892,9 @@ var listBridgePortsTool = {
|
|
|
2865
2892
|
var manageBridgeInputSchema = z11.object({
|
|
2866
2893
|
routerId,
|
|
2867
2894
|
action: z11.enum(["create", "remove"]).describe("Action to perform"),
|
|
2868
|
-
name: z11.string().
|
|
2895
|
+
name: z11.string().min(1).describe(
|
|
2896
|
+
"Bridge interface name, taken verbatim for remove; create accepts letters, digits, dot, hyphen, underscore, up to 64 chars"
|
|
2897
|
+
),
|
|
2869
2898
|
comment: z11.string().max(255).optional().describe("Optional comment"),
|
|
2870
2899
|
disabled: z11.boolean().default(false).describe("Whether the bridge should be disabled"),
|
|
2871
2900
|
dryRun
|
|
@@ -2893,6 +2922,11 @@ var manageBridgeTool = {
|
|
|
2893
2922
|
filter: { name: parsed.name }
|
|
2894
2923
|
});
|
|
2895
2924
|
if (parsed.action === "create") {
|
|
2925
|
+
assertNewInterfaceName(parsed.name, {
|
|
2926
|
+
kind: "Bridge",
|
|
2927
|
+
action: "create",
|
|
2928
|
+
code: "BRIDGE_NAME_INVALID"
|
|
2929
|
+
});
|
|
2896
2930
|
if (existing.length > 0) {
|
|
2897
2931
|
return {
|
|
2898
2932
|
content: `Bridge "${parsed.name}" already exists. No changes made.`,
|
|
@@ -3270,6 +3304,7 @@ var wifiTools = [listWifiTool, listWifiClientsTool, manageWifiTool];
|
|
|
3270
3304
|
|
|
3271
3305
|
// src/domain/tools/wireguard-tools.ts
|
|
3272
3306
|
import { z as z13 } from "zod";
|
|
3307
|
+
import { isIP } from "net";
|
|
3273
3308
|
var log11 = createLogger("wireguard-tools");
|
|
3274
3309
|
var listWgInputSchema = z13.object({
|
|
3275
3310
|
routerId,
|
|
@@ -3363,6 +3398,7 @@ var listPeersTool = {
|
|
|
3363
3398
|
"interface",
|
|
3364
3399
|
"public-key",
|
|
3365
3400
|
"endpoint-address",
|
|
3401
|
+
"endpoint-port",
|
|
3366
3402
|
"allowed-address",
|
|
3367
3403
|
"disabled"
|
|
3368
3404
|
])
|
|
@@ -3381,20 +3417,71 @@ var listPeersTool = {
|
|
|
3381
3417
|
}
|
|
3382
3418
|
}
|
|
3383
3419
|
};
|
|
3420
|
+
function parsePeerEndpoint(endpoint) {
|
|
3421
|
+
if (/\s/.test(endpoint) || endpoint.includes("%")) return void 0;
|
|
3422
|
+
if (isIP(endpoint) === 6) return { "endpoint-address": endpoint };
|
|
3423
|
+
const ipv6 = /^\[([^\]]+)\](?::(\d+))?$/.exec(endpoint);
|
|
3424
|
+
const host = /^([^:]+)(?::(\d+))?$/.exec(endpoint);
|
|
3425
|
+
const match = ipv6 ?? host;
|
|
3426
|
+
if (!match) return void 0;
|
|
3427
|
+
const [, address, port] = match;
|
|
3428
|
+
if (ipv6) {
|
|
3429
|
+
if (isIP(address) !== 6) return void 0;
|
|
3430
|
+
} else if (isIP(address) !== 4) {
|
|
3431
|
+
const hostname = address.endsWith(".") ? address.slice(0, -1) : address;
|
|
3432
|
+
if (hostname.length > 253 || /^[\d.]+$/.test(hostname) || !hostname.split(".").every((label) => /^[a-z\d_](?:[a-z\d_-]{0,61}[a-z\d_])?$/i.test(label))) {
|
|
3433
|
+
return void 0;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
if (port !== void 0 && (Number(port) < 1 || Number(port) > 65535)) return void 0;
|
|
3437
|
+
return {
|
|
3438
|
+
"endpoint-address": address,
|
|
3439
|
+
...port === void 0 ? {} : { "endpoint-port": String(Number(port)) }
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
var peerEndpointSchema = z13.string().transform((endpoint, ctx) => {
|
|
3443
|
+
const fields = parsePeerEndpoint(endpoint);
|
|
3444
|
+
if (fields) return fields;
|
|
3445
|
+
ctx.addIssue({
|
|
3446
|
+
code: z13.ZodIssueCode.custom,
|
|
3447
|
+
message: "Use IPv4:port, hostname:port or [IPv6]:port with port 1\u201365535, or an address without a port"
|
|
3448
|
+
});
|
|
3449
|
+
return z13.NEVER;
|
|
3450
|
+
});
|
|
3451
|
+
function canonicalPeerAddress(value) {
|
|
3452
|
+
const address = String(value ?? "");
|
|
3453
|
+
if (isIP(address) === 6) return new URL(`http://[${address}]/`).hostname;
|
|
3454
|
+
return address.toLowerCase().replace(/\.$/, "");
|
|
3455
|
+
}
|
|
3456
|
+
function canonicalAllowedAddresses(value) {
|
|
3457
|
+
const addresses = String(value ?? "").split(",").map((item) => {
|
|
3458
|
+
const cidr2 = item.trim();
|
|
3459
|
+
const [address, prefix] = cidr2.split("/");
|
|
3460
|
+
const family = isIP(address);
|
|
3461
|
+
if (!family || prefix !== void 0 && !/^\d+$/.test(prefix)) return cidr2;
|
|
3462
|
+
const bits = family === 4 ? 32 : 128;
|
|
3463
|
+
const length = Number(prefix ?? bits);
|
|
3464
|
+
if (length > bits || cidr2.split("/").length > 2) return cidr2;
|
|
3465
|
+
return `${canonicalPeerAddress(address)}/${length}`;
|
|
3466
|
+
});
|
|
3467
|
+
return [...new Set(addresses)].sort().join(",");
|
|
3468
|
+
}
|
|
3384
3469
|
var managePeerInputSchema = z13.object({
|
|
3385
3470
|
routerId,
|
|
3386
3471
|
action: z13.enum(["add", "remove"]).describe("Action to perform"),
|
|
3387
3472
|
interface: z13.string().describe("WireGuard interface name (e.g. wg0)"),
|
|
3388
3473
|
publicKey: z13.string().min(1).describe("Peer public key in base64 format (44 characters)"),
|
|
3389
3474
|
allowedAddress: z13.string().optional().describe("Allowed IP address/CIDR for this peer (e.g. 10.0.0.2/32)"),
|
|
3390
|
-
endpoint: z13.string().optional().describe(
|
|
3475
|
+
endpoint: z13.string().optional().describe(
|
|
3476
|
+
"Add only: IPv4:port, hostname:port or [IPv6]:port (port 1\u201365535); hostname labels may contain underscores. Address-only IPv4/hostname/IPv6 (bare or bracketed) leaves the RouterOS port default unchanged, which may not allow initiation. Ignored for remove; omit for a roaming peer."
|
|
3477
|
+
),
|
|
3391
3478
|
comment: z13.string().max(255).optional().describe("Optional comment"),
|
|
3392
3479
|
dryRun
|
|
3393
3480
|
}).strict();
|
|
3394
3481
|
var managePeerTool = {
|
|
3395
3482
|
name: "manage_wireguard_peer",
|
|
3396
3483
|
title: "Manage WireGuard Peer",
|
|
3397
|
-
description: "Add or remove a WireGuard peer.
|
|
3484
|
+
description: "Add or remove a WireGuard peer. Add returns already_exists when the public key and explicitly requested endpoint/allowed addresses match; different settings produce CONFLICT without changing the peer. Omitted settings are left unchanged.",
|
|
3398
3485
|
inputSchema: managePeerInputSchema,
|
|
3399
3486
|
annotations: {
|
|
3400
3487
|
readOnlyHint: false,
|
|
@@ -3410,11 +3497,44 @@ var managePeerTool = {
|
|
|
3410
3497
|
"Managing WireGuard peer"
|
|
3411
3498
|
);
|
|
3412
3499
|
try {
|
|
3500
|
+
const endpointFields = parsed.action === "add" && parsed.endpoint !== void 0 ? peerEndpointSchema.parse(parsed.endpoint) : {};
|
|
3413
3501
|
const existing = await context.routerClient.get("interface/wireguard/peers", {
|
|
3414
3502
|
filter: { interface: parsed.interface, "public-key": parsed.publicKey }
|
|
3415
3503
|
});
|
|
3416
3504
|
if (parsed.action === "add") {
|
|
3417
3505
|
if (existing.length > 0) {
|
|
3506
|
+
const rec2 = existing[0];
|
|
3507
|
+
const requestedFields = {
|
|
3508
|
+
...endpointFields,
|
|
3509
|
+
...parsed.allowedAddress === void 0 ? {} : { "allowed-address": parsed.allowedAddress }
|
|
3510
|
+
};
|
|
3511
|
+
const mismatched = Object.entries(requestedFields).filter(([property, desired]) => {
|
|
3512
|
+
if (property === "endpoint-address") {
|
|
3513
|
+
return canonicalPeerAddress(rec2[property]) !== canonicalPeerAddress(desired);
|
|
3514
|
+
}
|
|
3515
|
+
if (property === "allowed-address") {
|
|
3516
|
+
return canonicalAllowedAddresses(rec2[property]) !== canonicalAllowedAddresses(desired);
|
|
3517
|
+
}
|
|
3518
|
+
return !sameValue(rec2[property], desired);
|
|
3519
|
+
});
|
|
3520
|
+
if (mismatched.length > 0) {
|
|
3521
|
+
throw new MikroMCPError({
|
|
3522
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
3523
|
+
code: "WIREGUARD_PEER_CONFLICT",
|
|
3524
|
+
message: `WireGuard peer already exists on "${parsed.interface}" with different requested settings.`,
|
|
3525
|
+
details: {
|
|
3526
|
+
existing: Object.fromEntries(
|
|
3527
|
+
mismatched.map(([property]) => [property, rec2[property]])
|
|
3528
|
+
),
|
|
3529
|
+
requested: Object.fromEntries(mismatched)
|
|
3530
|
+
},
|
|
3531
|
+
recoverability: {
|
|
3532
|
+
retryable: false,
|
|
3533
|
+
suggestedAction: "Inspect the peer with list_wireguard_peers, then remove it before adding different settings.",
|
|
3534
|
+
alternativeTools: ["list_wireguard_peers", "manage_wireguard_peer"]
|
|
3535
|
+
}
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3418
3538
|
return {
|
|
3419
3539
|
content: `WireGuard peer with this public key already exists on "${parsed.interface}". No changes made.`,
|
|
3420
3540
|
structuredContent: { action: "already_exists", peer: existing[0] }
|
|
@@ -3424,8 +3544,12 @@ var managePeerTool = {
|
|
|
3424
3544
|
const diff = [
|
|
3425
3545
|
{ property: "interface", before: null, after: parsed.interface },
|
|
3426
3546
|
{ property: "public-key", before: null, after: parsed.publicKey },
|
|
3427
|
-
...parsed.allowedAddress ? [{ property: "allowed-address", before: null, after: parsed.allowedAddress }] : [],
|
|
3428
|
-
...
|
|
3547
|
+
...parsed.allowedAddress !== void 0 ? [{ property: "allowed-address", before: null, after: parsed.allowedAddress }] : [],
|
|
3548
|
+
...Object.entries(endpointFields).map(([property, after]) => ({
|
|
3549
|
+
property,
|
|
3550
|
+
before: null,
|
|
3551
|
+
after
|
|
3552
|
+
}))
|
|
3429
3553
|
];
|
|
3430
3554
|
return {
|
|
3431
3555
|
content: `Dry run: Would add WireGuard peer on "${parsed.interface}".`,
|
|
@@ -3434,10 +3558,10 @@ var managePeerTool = {
|
|
|
3434
3558
|
}
|
|
3435
3559
|
const body = {
|
|
3436
3560
|
interface: parsed.interface,
|
|
3437
|
-
"public-key": parsed.publicKey
|
|
3561
|
+
"public-key": parsed.publicKey,
|
|
3562
|
+
...endpointFields
|
|
3438
3563
|
};
|
|
3439
|
-
if (parsed.allowedAddress) body["allowed-address"] = parsed.allowedAddress;
|
|
3440
|
-
if (parsed.endpoint) body["endpoint-address"] = parsed.endpoint;
|
|
3564
|
+
if (parsed.allowedAddress !== void 0) body["allowed-address"] = parsed.allowedAddress;
|
|
3441
3565
|
if (parsed.comment) body.comment = parsed.comment.replace(/[\x00-\x1f\x7f]/g, "");
|
|
3442
3566
|
const created = await context.routerClient.create("interface/wireguard/peers", body);
|
|
3443
3567
|
log11.info({ interface: parsed.interface, id: created[".id"] }, "WireGuard peer added");
|
|
@@ -8027,6 +8151,7 @@ function recordAttempt(args) {
|
|
|
8027
8151
|
routerId: args.routerId,
|
|
8028
8152
|
params: redactParams(args.params),
|
|
8029
8153
|
snapshotIds: args.snapshotIds,
|
|
8154
|
+
...args.dryRun ? { dryRun: true } : {},
|
|
8030
8155
|
phase: "attempt"
|
|
8031
8156
|
});
|
|
8032
8157
|
return id;
|
|
@@ -8921,6 +9046,47 @@ function unionVlanBlob(currentWire, targetWire) {
|
|
|
8921
9046
|
return dumpBlob([...rows.entries()].sort((a, b) => a[0] - b[0]).map(([, row]) => row));
|
|
8922
9047
|
}
|
|
8923
9048
|
|
|
9049
|
+
// src/middleware/authz.ts
|
|
9050
|
+
function matchesPattern(name, pattern) {
|
|
9051
|
+
return globMatch(pattern, name);
|
|
9052
|
+
}
|
|
9053
|
+
function checkAuthz(identity, toolName, routerId2) {
|
|
9054
|
+
if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId2)) {
|
|
9055
|
+
throw new MikroMCPError({
|
|
9056
|
+
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
9057
|
+
code: "ROUTER_NOT_ALLOWED",
|
|
9058
|
+
message: `Identity "${identity.id}" is not allowed to access router "${routerId2}".`,
|
|
9059
|
+
details: { identityId: identity.id, routerId: routerId2, allowedRouters: identity.allowedRouters },
|
|
9060
|
+
recoverability: {
|
|
9061
|
+
retryable: false,
|
|
9062
|
+
suggestedAction: `Add "${routerId2}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
|
|
9063
|
+
}
|
|
9064
|
+
});
|
|
9065
|
+
}
|
|
9066
|
+
checkToolAllowed(identity, toolName);
|
|
9067
|
+
}
|
|
9068
|
+
function checkToolAllowed(identity, toolName) {
|
|
9069
|
+
if (identity.allowedToolPatterns.length > 0) {
|
|
9070
|
+
const allowed = identity.allowedToolPatterns.some((p) => matchesPattern(toolName, p));
|
|
9071
|
+
if (!allowed) {
|
|
9072
|
+
throw new MikroMCPError({
|
|
9073
|
+
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
9074
|
+
code: "TOOL_NOT_ALLOWED",
|
|
9075
|
+
message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
|
|
9076
|
+
details: {
|
|
9077
|
+
identityId: identity.id,
|
|
9078
|
+
toolName,
|
|
9079
|
+
allowedToolPatterns: identity.allowedToolPatterns
|
|
9080
|
+
},
|
|
9081
|
+
recoverability: {
|
|
9082
|
+
retryable: false,
|
|
9083
|
+
suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
|
|
9084
|
+
}
|
|
9085
|
+
});
|
|
9086
|
+
}
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
|
|
8924
9090
|
// src/domain/tools/change-management-tools.ts
|
|
8925
9091
|
var log24 = createLogger("change-management");
|
|
8926
9092
|
var stepSchema = z25.object({
|
|
@@ -8936,6 +9102,116 @@ var applyPlanInputSchema = z25.object({
|
|
|
8936
9102
|
steps: z25.array(stepSchema).min(1).max(10).describe("Ordered list of write operations to apply in sequence"),
|
|
8937
9103
|
confirmationToken: z25.string().optional()
|
|
8938
9104
|
}).strict();
|
|
9105
|
+
function journalEntryWasDryRun(entry) {
|
|
9106
|
+
const params = entry.params ?? {};
|
|
9107
|
+
return entry.dryRun === true || params.dryRun === true;
|
|
9108
|
+
}
|
|
9109
|
+
var listJournalInputSchema = z25.object({
|
|
9110
|
+
routerId: z25.string().optional().describe("Only entries for this router id"),
|
|
9111
|
+
tool: z25.string().optional().describe("Only entries written by this tool"),
|
|
9112
|
+
limit: z25.number().int().min(1).max(500).default(20).describe("Max entries to return")
|
|
9113
|
+
}).strict();
|
|
9114
|
+
var listJournalTool = {
|
|
9115
|
+
name: "list_journal",
|
|
9116
|
+
title: "List Write Journal",
|
|
9117
|
+
description: "List recent entries from the server's write journal (write-journal.ndjson): every journaled write with its journalId, timestamp, tool, router, parameters (as redacted at write time), snapshot ids and outcome \u2014 newest first. Read-only. Use it to recover a lost journalId for rollback_change, or to audit what was written when. Only entries with rollbackable: true can be rolled back \u2014 dry runs and writes without snapshots (run_command, plan_changes) cannot.",
|
|
9118
|
+
inputSchema: listJournalInputSchema,
|
|
9119
|
+
annotations: {
|
|
9120
|
+
readOnlyHint: true,
|
|
9121
|
+
destructiveHint: false,
|
|
9122
|
+
idempotentHint: true,
|
|
9123
|
+
openWorldHint: false
|
|
9124
|
+
},
|
|
9125
|
+
skipRouterContext: true,
|
|
9126
|
+
async handler(params, context) {
|
|
9127
|
+
const parsed = listJournalInputSchema.parse(params);
|
|
9128
|
+
checkToolAllowed(context.identity, "list_journal");
|
|
9129
|
+
const journalPath = context.appConfig.journalPath;
|
|
9130
|
+
if (!journalPath) {
|
|
9131
|
+
return {
|
|
9132
|
+
content: "Write journal is disabled (no journal path configured on this server).",
|
|
9133
|
+
structuredContent: { journalEnabled: false, entries: [], total: 0, returned: 0 }
|
|
9134
|
+
};
|
|
9135
|
+
}
|
|
9136
|
+
let raw;
|
|
9137
|
+
try {
|
|
9138
|
+
raw = readFileSync2(journalPath, "utf-8");
|
|
9139
|
+
} catch (err) {
|
|
9140
|
+
if (err.code === "ENOENT") {
|
|
9141
|
+
raw = "";
|
|
9142
|
+
} else {
|
|
9143
|
+
throw new MikroMCPError({
|
|
9144
|
+
category: "INTERNAL" /* INTERNAL */,
|
|
9145
|
+
code: "JOURNAL_READ_FAILED",
|
|
9146
|
+
message: `Could not read the write journal: ${err.message}`,
|
|
9147
|
+
details: { journalPath },
|
|
9148
|
+
recoverability: {
|
|
9149
|
+
retryable: false,
|
|
9150
|
+
suggestedAction: "Check the file's permissions under the MikroMCP data directory."
|
|
9151
|
+
}
|
|
9152
|
+
});
|
|
9153
|
+
}
|
|
9154
|
+
}
|
|
9155
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
9156
|
+
const attempts = [];
|
|
9157
|
+
for (const line of raw.split("\n")) {
|
|
9158
|
+
if (!line.trim()) continue;
|
|
9159
|
+
let entry;
|
|
9160
|
+
try {
|
|
9161
|
+
entry = JSON.parse(line);
|
|
9162
|
+
} catch {
|
|
9163
|
+
continue;
|
|
9164
|
+
}
|
|
9165
|
+
if (entry.phase === "attempt") attempts.push(entry);
|
|
9166
|
+
else
|
|
9167
|
+
outcomes.set(entry.id, {
|
|
9168
|
+
phase: entry.phase,
|
|
9169
|
+
outcome: entry.outcome,
|
|
9170
|
+
durationMs: entry.durationMs
|
|
9171
|
+
});
|
|
9172
|
+
}
|
|
9173
|
+
let entries = attempts.map((a) => {
|
|
9174
|
+
const outcome = outcomes.get(a.id);
|
|
9175
|
+
const snapshotIds = Array.isArray(a.snapshotIds) ? a.snapshotIds : [];
|
|
9176
|
+
const dryRun2 = journalEntryWasDryRun(a);
|
|
9177
|
+
return {
|
|
9178
|
+
journalId: a.id,
|
|
9179
|
+
ts: a.ts,
|
|
9180
|
+
tool: a.tool,
|
|
9181
|
+
routerId: a.routerId,
|
|
9182
|
+
identityId: a.identityId,
|
|
9183
|
+
role: a.role,
|
|
9184
|
+
params: a.params ?? {},
|
|
9185
|
+
snapshotIds,
|
|
9186
|
+
dryRun: dryRun2,
|
|
9187
|
+
rollbackable: !dryRun2 && snapshotIds.length > 0,
|
|
9188
|
+
status: outcome ? outcome.phase : "unknown",
|
|
9189
|
+
outcomeCode: outcome?.outcome,
|
|
9190
|
+
durationMs: outcome?.durationMs
|
|
9191
|
+
};
|
|
9192
|
+
}).reverse();
|
|
9193
|
+
if (parsed.routerId) entries = entries.filter((e) => e.routerId === parsed.routerId);
|
|
9194
|
+
if (parsed.tool) entries = entries.filter((e) => e.tool === parsed.tool);
|
|
9195
|
+
const scope = context.identity.allowedRouters;
|
|
9196
|
+
if (scope.length > 0) entries = entries.filter((e) => scope.includes(e.routerId));
|
|
9197
|
+
const total = entries.length;
|
|
9198
|
+
const returned = entries.slice(0, parsed.limit);
|
|
9199
|
+
const header = total === 0 ? "Write journal: no entries." : `Write journal: ${returned.length} of ${total}, newest first.`;
|
|
9200
|
+
const lines = returned.map(
|
|
9201
|
+
(e) => ` ${String(e.journalId)} ${String(e.tool)} ${String(e.routerId)} \u2192 ${e.status}${e.outcomeCode ? ` (${e.outcomeCode})` : ""}${e.dryRun ? " dry-run" : e.rollbackable ? "" : " not-rollbackable"} [${String(e.ts)}]`
|
|
9202
|
+
);
|
|
9203
|
+
return {
|
|
9204
|
+
content: total === 0 ? header : [header, ...lines].join("\n"),
|
|
9205
|
+
structuredContent: {
|
|
9206
|
+
journalEnabled: true,
|
|
9207
|
+
entries: returned,
|
|
9208
|
+
total,
|
|
9209
|
+
returned: returned.length,
|
|
9210
|
+
note: "Pass the journalId of an entry with rollbackable: true to rollback_change to restore that write. Dry runs and entries without snapshots (e.g. run_command) cannot be rolled back."
|
|
9211
|
+
}
|
|
9212
|
+
};
|
|
9213
|
+
}
|
|
9214
|
+
};
|
|
8939
9215
|
var rollbackChangeInputSchema = z25.object({
|
|
8940
9216
|
routerId,
|
|
8941
9217
|
journalId: z25.string().describe("Journal entry ID from write-journal.ndjson to roll back"),
|
|
@@ -9156,7 +9432,8 @@ function createChangeManagementTools(baseTools2) {
|
|
|
9156
9432
|
tool: step.tool,
|
|
9157
9433
|
routerId: context.routerId,
|
|
9158
9434
|
params: loggableParams(tool, stepParams),
|
|
9159
|
-
snapshotIds
|
|
9435
|
+
snapshotIds,
|
|
9436
|
+
dryRun: isEffectiveDryRun(tool, stepParams)
|
|
9160
9437
|
});
|
|
9161
9438
|
const stepStartMs = Date.now();
|
|
9162
9439
|
try {
|
|
@@ -9223,7 +9500,10 @@ function createChangeManagementTools(baseTools2) {
|
|
|
9223
9500
|
}
|
|
9224
9501
|
});
|
|
9225
9502
|
}
|
|
9226
|
-
return {
|
|
9503
|
+
return {
|
|
9504
|
+
snapshotIds: entry.snapshotIds ?? [],
|
|
9505
|
+
dryRun: journalEntryWasDryRun(entry)
|
|
9506
|
+
};
|
|
9227
9507
|
}
|
|
9228
9508
|
const rollbackChangeTool = {
|
|
9229
9509
|
name: "rollback_change",
|
|
@@ -9242,7 +9522,16 @@ function createChangeManagementTools(baseTools2) {
|
|
|
9242
9522
|
const journalPath = context.appConfig.journalPath;
|
|
9243
9523
|
const snapshotDir = context.appConfig.snapshotDir;
|
|
9244
9524
|
log24.info({ routerId: context.routerId, journalId: parsed.journalId }, "Rolling back change");
|
|
9245
|
-
const { snapshotIds } = findJournalEntry(
|
|
9525
|
+
const { snapshotIds, dryRun: entryWasDryRun } = findJournalEntry(
|
|
9526
|
+
journalPath,
|
|
9527
|
+
parsed.journalId
|
|
9528
|
+
);
|
|
9529
|
+
if (entryWasDryRun) {
|
|
9530
|
+
return {
|
|
9531
|
+
content: `Journal entry "${parsed.journalId}" was a dry run \u2014 nothing was changed, so there is nothing to roll back.`,
|
|
9532
|
+
structuredContent: { action: "dry_run_entry", journalId: parsed.journalId }
|
|
9533
|
+
};
|
|
9534
|
+
}
|
|
9246
9535
|
if (snapshotIds.length === 0) {
|
|
9247
9536
|
return {
|
|
9248
9537
|
content: `Journal entry "${parsed.journalId}" has no associated snapshots \u2014 cannot rollback automatically.`,
|
|
@@ -9297,7 +9586,7 @@ ${warnings.map((w) => ` - ${w}`).join("\n")}` : "";
|
|
|
9297
9586
|
};
|
|
9298
9587
|
}
|
|
9299
9588
|
};
|
|
9300
|
-
return [planChangesTool, applyPlanTool, rollbackChangeTool];
|
|
9589
|
+
return [planChangesTool, applyPlanTool, rollbackChangeTool, listJournalTool];
|
|
9301
9590
|
}
|
|
9302
9591
|
|
|
9303
9592
|
// src/domain/tools/ipsec-tools.ts
|
|
@@ -11770,8 +12059,8 @@ function sanitizeComment3(comment) {
|
|
|
11770
12059
|
var manageVlanInputSchema = z35.object({
|
|
11771
12060
|
routerId,
|
|
11772
12061
|
action: z35.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
11773
|
-
name: z35.string().
|
|
11774
|
-
"VLAN interface name \u2014 idempotency key
|
|
12062
|
+
name: z35.string().min(1).describe(
|
|
12063
|
+
"VLAN interface name \u2014 idempotency key, taken verbatim for remove/enable/disable; add accepts letters, digits, dot, hyphen, underscore, up to 64 chars"
|
|
11775
12064
|
),
|
|
11776
12065
|
vlanId: z35.number().int().min(1).max(4094).optional().describe("VLAN ID (1-4094; required for add)"),
|
|
11777
12066
|
parentInterface: z35.string().optional().describe("Parent interface name (e.g., ether1, bridge1; required for add)"),
|
|
@@ -11808,6 +12097,11 @@ var manageVlanTool = {
|
|
|
11808
12097
|
(v) => sameValue(v.name, parsed.name)
|
|
11809
12098
|
);
|
|
11810
12099
|
if (parsed.action === "add") {
|
|
12100
|
+
assertNewInterfaceName(parsed.name, {
|
|
12101
|
+
kind: "VLAN",
|
|
12102
|
+
action: "add",
|
|
12103
|
+
code: "VLAN_NAME_INVALID"
|
|
12104
|
+
});
|
|
11811
12105
|
if (parsed.vlanId === void 0) {
|
|
11812
12106
|
throw new MikroMCPError({
|
|
11813
12107
|
category: "VALIDATION" /* VALIDATION */,
|
|
@@ -11952,9 +12246,12 @@ var vlanTools = [manageVlanTool];
|
|
|
11952
12246
|
// src/domain/tools/dhcp-client-tools.ts
|
|
11953
12247
|
import { z as z36 } from "zod";
|
|
11954
12248
|
var log35 = createLogger("dhcp-client-tools");
|
|
12249
|
+
function findClient(clients, target, byNameToo = true) {
|
|
12250
|
+
return clients.find((c) => sameValue(c.interface, target)) ?? (byNameToo ? clients.find((c) => sameValue(c.name, target)) : void 0);
|
|
12251
|
+
}
|
|
11955
12252
|
var listDhcpClientsInputSchema = z36.object({
|
|
11956
12253
|
routerId,
|
|
11957
|
-
interface: z36.string().optional().describe("Filter by interface name (exact match)"),
|
|
12254
|
+
interface: z36.string().optional().describe("Filter by interface or display name (exact match)"),
|
|
11958
12255
|
status: z36.enum(["bound", "searching", "requesting", "init", "all"]).default("all").describe("Filter by DHCP client status"),
|
|
11959
12256
|
limit,
|
|
11960
12257
|
offset
|
|
@@ -11980,7 +12277,9 @@ var listDhcpClientsTool = {
|
|
|
11980
12277
|
});
|
|
11981
12278
|
let filtered = allClients;
|
|
11982
12279
|
if (parsed.interface) {
|
|
11983
|
-
filtered = filtered.filter(
|
|
12280
|
+
filtered = filtered.filter(
|
|
12281
|
+
(c) => sameValue(c.interface, parsed.interface) || sameValue(c.name, parsed.interface)
|
|
12282
|
+
);
|
|
11984
12283
|
}
|
|
11985
12284
|
if (parsed.status !== "all") {
|
|
11986
12285
|
filtered = filtered.filter((c) => c.status === parsed.status);
|
|
@@ -11993,7 +12292,7 @@ var listDhcpClientsTool = {
|
|
|
11993
12292
|
clients,
|
|
11994
12293
|
total,
|
|
11995
12294
|
parsed.offset,
|
|
11996
|
-
(c) => compactFields(c, ["interface", "status", "address", "gateway", "disabled"])
|
|
12295
|
+
(c) => compactFields(c, ["interface", "name", "status", "address", "gateway", "disabled"])
|
|
11997
12296
|
),
|
|
11998
12297
|
structuredContent: {
|
|
11999
12298
|
routerId: context.routerId,
|
|
@@ -12012,7 +12311,9 @@ var listDhcpClientsTool = {
|
|
|
12012
12311
|
var manageDhcpClientInputSchema = z36.object({
|
|
12013
12312
|
routerId,
|
|
12014
12313
|
action: z36.enum(["add", "remove", "enable", "disable"]).describe("Action to perform"),
|
|
12015
|
-
interface: z36.string().describe(
|
|
12314
|
+
interface: z36.string().describe(
|
|
12315
|
+
"Interface name \u2014 idempotency key (e.g. ether1); for remove/enable/disable a client's display name also matches"
|
|
12316
|
+
),
|
|
12016
12317
|
usePeerDns: z36.boolean().default(true).describe("Use DNS servers provided by DHCP server (add only)"),
|
|
12017
12318
|
usePeerNtp: z36.boolean().default(false).describe("Use NTP servers provided by DHCP server (add only)"),
|
|
12018
12319
|
addDefaultRoute: z36.boolean().default(true).describe("Add default route from DHCP (add only)"),
|
|
@@ -12042,8 +12343,10 @@ var manageDhcpClientTool = {
|
|
|
12042
12343
|
limit: void 0,
|
|
12043
12344
|
offset: void 0
|
|
12044
12345
|
});
|
|
12045
|
-
const existing =
|
|
12046
|
-
|
|
12346
|
+
const existing = findClient(
|
|
12347
|
+
allClients,
|
|
12348
|
+
parsed.interface,
|
|
12349
|
+
parsed.action !== "add"
|
|
12047
12350
|
);
|
|
12048
12351
|
if (parsed.action === "add") {
|
|
12049
12352
|
if (existing) {
|
|
@@ -12084,9 +12387,19 @@ var manageDhcpClientTool = {
|
|
|
12084
12387
|
}
|
|
12085
12388
|
if (parsed.action === "remove") {
|
|
12086
12389
|
if (!existing) {
|
|
12390
|
+
const candidates = allClients.map((c) => ({
|
|
12391
|
+
interface: c.interface,
|
|
12392
|
+
name: c.name,
|
|
12393
|
+
status: c.status
|
|
12394
|
+
}));
|
|
12395
|
+
const names = candidates.map((c) => c.name ?? c.interface).slice(0, 5).join(", ");
|
|
12087
12396
|
return {
|
|
12088
|
-
content: `No DHCP client found on "${parsed.interface}". Nothing to remove
|
|
12089
|
-
structuredContent: {
|
|
12397
|
+
content: `No DHCP client found on "${parsed.interface}". Nothing to remove.` + (names ? ` Existing clients: ${names}.` : ""),
|
|
12398
|
+
structuredContent: {
|
|
12399
|
+
action: "not_found",
|
|
12400
|
+
interface: parsed.interface,
|
|
12401
|
+
clients: candidates
|
|
12402
|
+
}
|
|
12090
12403
|
};
|
|
12091
12404
|
}
|
|
12092
12405
|
if (parsed.dryRun) {
|
|
@@ -12094,7 +12407,7 @@ var manageDhcpClientTool = {
|
|
|
12094
12407
|
content: `Dry run: Would remove DHCP client on "${parsed.interface}".`,
|
|
12095
12408
|
structuredContent: {
|
|
12096
12409
|
action: "dry_run",
|
|
12097
|
-
diff: [{ property: "interface", before:
|
|
12410
|
+
diff: [{ property: "interface", before: existing.interface, after: null }]
|
|
12098
12411
|
}
|
|
12099
12412
|
};
|
|
12100
12413
|
}
|
|
@@ -12236,7 +12549,7 @@ var manageIpServiceInputSchema = z37.object({
|
|
|
12236
12549
|
var manageIpServiceTool = {
|
|
12237
12550
|
name: "manage_ip_service",
|
|
12238
12551
|
title: "Manage IP Service",
|
|
12239
|
-
description: "Enable or disable a RouterOS IP service (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp). Port number changes are intentionally not supported to prevent accidental lockout.",
|
|
12552
|
+
description: "Enable or disable a RouterOS IP service (api, api-ssl, ssh, telnet, www, www-ssl, winbox, ftp). Port number changes are intentionally not supported to prevent accidental lockout. MikroMCP reaches RouterOS through the REST API on www-ssl (or www for plaintext): disabling the service its connection uses cuts MikroMCP off from the router, including any later call to re-enable it.",
|
|
12240
12553
|
inputSchema: manageIpServiceInputSchema,
|
|
12241
12554
|
annotations: {
|
|
12242
12555
|
readOnlyHint: false,
|
|
@@ -16389,44 +16702,6 @@ var swosMaintenanceTools = [
|
|
|
16389
16702
|
// src/domain/tools/fleet-tools.ts
|
|
16390
16703
|
import { z as z50 } from "zod";
|
|
16391
16704
|
|
|
16392
|
-
// src/middleware/authz.ts
|
|
16393
|
-
function matchesPattern(name, pattern) {
|
|
16394
|
-
return globMatch(pattern, name);
|
|
16395
|
-
}
|
|
16396
|
-
function checkAuthz(identity, toolName, routerId2) {
|
|
16397
|
-
if (identity.allowedRouters.length > 0 && !identity.allowedRouters.includes(routerId2)) {
|
|
16398
|
-
throw new MikroMCPError({
|
|
16399
|
-
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
16400
|
-
code: "ROUTER_NOT_ALLOWED",
|
|
16401
|
-
message: `Identity "${identity.id}" is not allowed to access router "${routerId2}".`,
|
|
16402
|
-
details: { identityId: identity.id, routerId: routerId2, allowedRouters: identity.allowedRouters },
|
|
16403
|
-
recoverability: {
|
|
16404
|
-
retryable: false,
|
|
16405
|
-
suggestedAction: `Add "${routerId2}" to allowedRouters for identity "${identity.id}" in identities.yaml.`
|
|
16406
|
-
}
|
|
16407
|
-
});
|
|
16408
|
-
}
|
|
16409
|
-
if (identity.allowedToolPatterns.length > 0) {
|
|
16410
|
-
const allowed = identity.allowedToolPatterns.some((p) => matchesPattern(toolName, p));
|
|
16411
|
-
if (!allowed) {
|
|
16412
|
-
throw new MikroMCPError({
|
|
16413
|
-
category: "PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
16414
|
-
code: "TOOL_NOT_ALLOWED",
|
|
16415
|
-
message: `Identity "${identity.id}" is not allowed to call tool "${toolName}".`,
|
|
16416
|
-
details: {
|
|
16417
|
-
identityId: identity.id,
|
|
16418
|
-
toolName,
|
|
16419
|
-
allowedToolPatterns: identity.allowedToolPatterns
|
|
16420
|
-
},
|
|
16421
|
-
recoverability: {
|
|
16422
|
-
retryable: false,
|
|
16423
|
-
suggestedAction: `Add a matching pattern to allowedToolPatterns for identity "${identity.id}" in identities.yaml.`
|
|
16424
|
-
}
|
|
16425
|
-
});
|
|
16426
|
-
}
|
|
16427
|
-
}
|
|
16428
|
-
}
|
|
16429
|
-
|
|
16430
16705
|
// src/config/secrets.ts
|
|
16431
16706
|
function getCredentials(router) {
|
|
16432
16707
|
const { source, envPrefix, vaultPath } = router.credentials;
|
|
@@ -17320,6 +17595,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
17320
17595
|
}
|
|
17321
17596
|
}
|
|
17322
17597
|
toolContext.snapshots = snapshots;
|
|
17598
|
+
const dryRun2 = !tool.annotations.readOnlyHint && isEffectiveDryRun(tool, handlerArgs);
|
|
17323
17599
|
if (!tool.annotations.readOnlyHint && config.journalPath) {
|
|
17324
17600
|
const { confirmationToken: _token, ...journalArgs } = loggedArgs;
|
|
17325
17601
|
journalId = recordAttempt({
|
|
@@ -17329,7 +17605,8 @@ async function executeToolCall(tool, args, deps) {
|
|
|
17329
17605
|
tool: tool.name,
|
|
17330
17606
|
routerId: routerId2,
|
|
17331
17607
|
params: journalArgs,
|
|
17332
|
-
snapshotIds
|
|
17608
|
+
snapshotIds,
|
|
17609
|
+
dryRun: dryRun2
|
|
17333
17610
|
});
|
|
17334
17611
|
}
|
|
17335
17612
|
const runHandler = () => tool.handler(handlerArgs, toolContext);
|
|
@@ -17343,6 +17620,11 @@ async function executeToolCall(tool, args, deps) {
|
|
|
17343
17620
|
phase: "success",
|
|
17344
17621
|
durationMs: Date.now() - startMs
|
|
17345
17622
|
});
|
|
17623
|
+
if (!dryRun2 && snapshotIds.length > 0) {
|
|
17624
|
+
result.structuredContent = { ...result.structuredContent, journalId };
|
|
17625
|
+
result.content += `
|
|
17626
|
+
Journal ID: ${journalId} (pass to rollback_change to undo).`;
|
|
17627
|
+
}
|
|
17346
17628
|
}
|
|
17347
17629
|
if (shouldAudit) {
|
|
17348
17630
|
auditLog(
|
|
@@ -17520,7 +17802,7 @@ var listRoutersInputSchema = z50.object({
|
|
|
17520
17802
|
var listRoutersTool = {
|
|
17521
17803
|
name: "list_routers",
|
|
17522
17804
|
title: "List Routers",
|
|
17523
|
-
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_read and bulk_execute).",
|
|
17805
|
+
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. rosVersion is what routers.yaml states and may lag a router upgrade \u2014 ask get_system_status for the live version. Use it to discover valid routerId values and tags for targeting other tools (including bulk_read and bulk_execute).",
|
|
17524
17806
|
inputSchema: listRoutersInputSchema,
|
|
17525
17807
|
annotations: {
|
|
17526
17808
|
readOnlyHint: true,
|
|
@@ -17563,9 +17845,17 @@ var listRoutersTool = {
|
|
|
17563
17845
|
"isDefault"
|
|
17564
17846
|
])}`
|
|
17565
17847
|
);
|
|
17848
|
+
const hasRouterOS = rows.some((r) => r.deviceType === "routeros");
|
|
17566
17849
|
return {
|
|
17567
17850
|
content: total === 0 ? header : [header, ...lines].join("\n"),
|
|
17568
|
-
structuredContent: {
|
|
17851
|
+
structuredContent: {
|
|
17852
|
+
routers: rows,
|
|
17853
|
+
total,
|
|
17854
|
+
returned: total,
|
|
17855
|
+
...hasRouterOS ? {
|
|
17856
|
+
note: "rosVersion comes from routers.yaml and may be stale; use get_system_status for the live version."
|
|
17857
|
+
} : {}
|
|
17858
|
+
}
|
|
17569
17859
|
};
|
|
17570
17860
|
}
|
|
17571
17861
|
};
|
|
@@ -17724,7 +18014,8 @@ async function fanOut(req) {
|
|
|
17724
18014
|
tool: input2.toolName,
|
|
17725
18015
|
routerId: router.id,
|
|
17726
18016
|
params: loggedParams,
|
|
17727
|
-
snapshotIds
|
|
18017
|
+
snapshotIds,
|
|
18018
|
+
dryRun: isEffectiveDryRun(targetTool, toolParams)
|
|
17728
18019
|
});
|
|
17729
18020
|
}
|
|
17730
18021
|
const cb = getOrCreateBreaker(context.circuitBreakers, router.id, context.appConfig);
|
|
@@ -18308,7 +18599,7 @@ var ConnectionPool = class {
|
|
|
18308
18599
|
};
|
|
18309
18600
|
|
|
18310
18601
|
// src/version.ts
|
|
18311
|
-
var VERSION2 = "1.
|
|
18602
|
+
var VERSION2 = "1.15.0";
|
|
18312
18603
|
|
|
18313
18604
|
// src/mcp/server.ts
|
|
18314
18605
|
var SERVER_INSTRUCTIONS = [
|
|
@@ -18786,7 +19077,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
|
|
|
18786
19077
|
import { homedir as homedir2 } from "os";
|
|
18787
19078
|
import * as net from "net";
|
|
18788
19079
|
import chalk from "chalk";
|
|
18789
|
-
var localVersion = "1.
|
|
19080
|
+
var localVersion = "1.15.0";
|
|
18790
19081
|
var results = [];
|
|
18791
19082
|
function ok(msg) {
|
|
18792
19083
|
console.log(chalk.green("\u2705 ") + msg);
|
|
@@ -19602,7 +19893,7 @@ async function runInit() {
|
|
|
19602
19893
|
}
|
|
19603
19894
|
|
|
19604
19895
|
// src/cli/index.ts
|
|
19605
|
-
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.
|
|
19896
|
+
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.15.0");
|
|
19606
19897
|
program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
|
|
19607
19898
|
program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
|
|
19608
19899
|
program.command("init").description("Interactive setup wizard").action(runInit);
|