mikromcp 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/main.js +795 -93
- 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
|
-
[](#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.
|
|
@@ -71,7 +71,7 @@ MikroMCP exists because raw router CLI access is the wrong abstraction for AI ag
|
|
|
71
71
|
|
|
72
72
|
### MCP Inspector
|
|
73
73
|
|
|
74
|
-

|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
@@ -285,7 +285,7 @@ All settings can be placed in `~/.mikromcp/.env` or passed as environment variab
|
|
|
285
285
|
|
|
286
286
|
## Available Tools
|
|
287
287
|
|
|
288
|
-
MikroMCP currently registers **
|
|
288
|
+
MikroMCP currently registers **88 MCP tools**.
|
|
289
289
|
|
|
290
290
|
| Area | Tools |
|
|
291
291
|
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -295,6 +295,7 @@ MikroMCP currently registers **82 MCP tools**.
|
|
|
295
295
|
| DHCP Servers & Pools | `list_dhcp_servers`, `manage_dhcp_server`, `list_ip_pools`, `manage_ip_pool` |
|
|
296
296
|
| DHCP Clients | `list_dhcp_clients`, `manage_dhcp_client` |
|
|
297
297
|
| IP Services | `list_ip_services`, `manage_ip_service` |
|
|
298
|
+
| PPPoE & OpenVPN | `list_pppoe_clients`, `manage_pppoe_client`, `list_ovpn_clients`, `manage_ovpn_client`, `get_ovpn_server`, `manage_ovpn_server` |
|
|
298
299
|
| Routing | `list_routes`, `manage_route`, `list_routing_rules`, `manage_routing_rule`, `list_routing_tables`, `manage_routing_table` |
|
|
299
300
|
| Routing protocols | `list_bgp_peers`, `list_ospf_neighbors` |
|
|
300
301
|
| Firewall | `list_firewall_rules`, `manage_firewall_rule`, `list_mangle_rules`, `manage_mangle_rule`, `list_address_list_entries`, `manage_address_list_entry` |
|
|
@@ -421,7 +422,7 @@ AI assistants can use MikroMCP to:
|
|
|
421
422
|
| [Configuration](https://github.com/AliKarami/MikroMCP/wiki/Configuration) | Router registry, credentials, all environment variables |
|
|
422
423
|
| [Connecting to Claude Desktop](https://github.com/AliKarami/MikroMCP/wiki/Connecting-to-Claude-Desktop) | Register MikroMCP in Claude Desktop |
|
|
423
424
|
| [Connecting to AI Assistants](https://github.com/AliKarami/MikroMCP/wiki/Connecting-to-AI-Assistants) | Claude Code, Cursor, Codex, HTTP/Docker/systemd |
|
|
424
|
-
| [Available Tools](https://github.com/AliKarami/MikroMCP/wiki/Available-Tools) | All
|
|
425
|
+
| [Available Tools](https://github.com/AliKarami/MikroMCP/wiki/Available-Tools) | All 88 tools — parameters and example prompts |
|
|
425
426
|
| [Architecture](https://github.com/AliKarami/MikroMCP/wiki/Architecture) | System layers, request pipeline, auth model |
|
|
426
427
|
| [Error Handling](https://github.com/AliKarami/MikroMCP/wiki/Error-Handling) | Error categories, retry engine, circuit breaker |
|
|
427
428
|
| [Running](https://github.com/AliKarami/MikroMCP/wiki/Running) | Run commands, HTTP transport, troubleshooting |
|
package/dist/main.js
CHANGED
|
@@ -60,7 +60,7 @@ function loadAppConfig() {
|
|
|
60
60
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
61
61
|
|
|
62
62
|
// src/mcp/tool-registry.ts
|
|
63
|
-
import { z as
|
|
63
|
+
import { z as z38 } from "zod";
|
|
64
64
|
|
|
65
65
|
// src/domain/tools/system-tools.ts
|
|
66
66
|
import { z } from "zod";
|
|
@@ -497,9 +497,9 @@ var listInterfacesTool = {
|
|
|
497
497
|
const paginated = interfaces.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
498
498
|
const results2 = paginated.map((iface) => {
|
|
499
499
|
const rec = iface;
|
|
500
|
-
const
|
|
500
|
+
const isRunning2 = rec.running === true || rec.running === "true";
|
|
501
501
|
const isDisabled = rec.disabled === true || rec.disabled === "true";
|
|
502
|
-
const enriched = { status: isDisabled ? "disabled" :
|
|
502
|
+
const enriched = { status: isDisabled ? "disabled" : isRunning2 ? "up" : "down" };
|
|
503
503
|
for (const [key2, value] of Object.entries(rec)) {
|
|
504
504
|
if (parsed.includeCounters || !COUNTER_PROPS.includes(key2)) {
|
|
505
505
|
enriched[key2] = value;
|
|
@@ -7775,8 +7775,708 @@ var manageIpServiceTool = {
|
|
|
7775
7775
|
};
|
|
7776
7776
|
var ipServiceTools = [listIpServicesTool, manageIpServiceTool];
|
|
7777
7777
|
|
|
7778
|
-
// src/domain/tools/
|
|
7778
|
+
// src/domain/tools/pppoe-tools.ts
|
|
7779
7779
|
import { z as z35 } from "zod";
|
|
7780
|
+
var log36 = createLogger("pppoe-tools");
|
|
7781
|
+
var PPPOE_PATH = "interface/pppoe-client";
|
|
7782
|
+
var listPppoeClientsInputSchema = z35.object({
|
|
7783
|
+
routerId: z35.string().describe("Target router identifier from the router registry"),
|
|
7784
|
+
interface: z35.string().optional().describe("Filter by parent interface name (exact match)"),
|
|
7785
|
+
status: z35.enum(["connected", "disconnected", "all"]).default("all").describe("Filter by running status"),
|
|
7786
|
+
limit: z35.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
|
|
7787
|
+
offset: z35.number().int().min(0).default(0).describe("Offset for pagination")
|
|
7788
|
+
}).strict();
|
|
7789
|
+
function isRunning(record) {
|
|
7790
|
+
return record.running === true || record.running === "true";
|
|
7791
|
+
}
|
|
7792
|
+
var listPppoeClientsTool = {
|
|
7793
|
+
name: "list_pppoe_clients",
|
|
7794
|
+
title: "List PPPoE Clients",
|
|
7795
|
+
description: "List PPPoE client interfaces on a MikroTik router. Shows name, parent interface, ISP username, and connection status.",
|
|
7796
|
+
inputSchema: listPppoeClientsInputSchema,
|
|
7797
|
+
annotations: {
|
|
7798
|
+
readOnlyHint: true,
|
|
7799
|
+
destructiveHint: false,
|
|
7800
|
+
idempotentHint: true,
|
|
7801
|
+
openWorldHint: false
|
|
7802
|
+
},
|
|
7803
|
+
async handler(params, context) {
|
|
7804
|
+
const parsed = listPppoeClientsInputSchema.parse(params);
|
|
7805
|
+
log36.info({ routerId: context.routerId }, "Listing PPPoE clients");
|
|
7806
|
+
try {
|
|
7807
|
+
const allClients = await context.routerClient.get(PPPOE_PATH, {
|
|
7808
|
+
limit: void 0,
|
|
7809
|
+
offset: void 0
|
|
7810
|
+
});
|
|
7811
|
+
let filtered = allClients;
|
|
7812
|
+
if (parsed.interface) {
|
|
7813
|
+
filtered = filtered.filter((c) => c.interface === parsed.interface);
|
|
7814
|
+
}
|
|
7815
|
+
if (parsed.status === "connected") {
|
|
7816
|
+
filtered = filtered.filter((c) => isRunning(c));
|
|
7817
|
+
} else if (parsed.status === "disconnected") {
|
|
7818
|
+
filtered = filtered.filter((c) => !isRunning(c));
|
|
7819
|
+
}
|
|
7820
|
+
const total = filtered.length;
|
|
7821
|
+
const clients = filtered.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
7822
|
+
const hasMore = parsed.offset + parsed.limit < total;
|
|
7823
|
+
const lines = [
|
|
7824
|
+
`PPPoE clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`
|
|
7825
|
+
];
|
|
7826
|
+
for (const c of clients) {
|
|
7827
|
+
const name = c.name ?? "unknown";
|
|
7828
|
+
const iface = c.interface ?? "unknown";
|
|
7829
|
+
const user = c.user ?? "-";
|
|
7830
|
+
const status = isRunning(c) ? "connected" : "disconnected";
|
|
7831
|
+
lines.push(` ${name} [${iface}] user=${user} ${status}`);
|
|
7832
|
+
}
|
|
7833
|
+
return {
|
|
7834
|
+
content: lines.join("\n"),
|
|
7835
|
+
structuredContent: {
|
|
7836
|
+
routerId: context.routerId,
|
|
7837
|
+
clients,
|
|
7838
|
+
total,
|
|
7839
|
+
hasMore,
|
|
7840
|
+
offset: parsed.offset,
|
|
7841
|
+
limit: parsed.limit
|
|
7842
|
+
}
|
|
7843
|
+
};
|
|
7844
|
+
} catch (err) {
|
|
7845
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_pppoe_clients" });
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
};
|
|
7849
|
+
var managePppoeClientInputSchema = z35.object({
|
|
7850
|
+
routerId: z35.string().describe("Target router identifier from the router registry"),
|
|
7851
|
+
action: z35.enum(["add", "update", "remove"]).describe("Action to perform"),
|
|
7852
|
+
name: z35.string().describe("PPPoE client interface name \u2014 idempotency key (e.g. pppoe-wan)"),
|
|
7853
|
+
interface: z35.string().optional().describe("Parent interface (required for add)"),
|
|
7854
|
+
user: z35.string().optional().describe("PPPoE username (required for add)"),
|
|
7855
|
+
password: z35.string().optional().describe("PPPoE password (never logged)"),
|
|
7856
|
+
serviceName: z35.string().optional().describe("PPPoE service name filter (leave empty to match any)"),
|
|
7857
|
+
addDefaultRoute: z35.boolean().optional().describe("Add default route via PPPoE (yes/no)"),
|
|
7858
|
+
dialOnDemand: z35.boolean().optional().describe("Dial on demand instead of always-on (yes/no)"),
|
|
7859
|
+
dryRun: z35.boolean().default(false).describe("Preview changes without applying")
|
|
7860
|
+
}).strict();
|
|
7861
|
+
var managePppoeClientTool = {
|
|
7862
|
+
name: "manage_pppoe_client",
|
|
7863
|
+
title: "Manage PPPoE Client",
|
|
7864
|
+
description: "Add, update, or remove a PPPoE client interface. Idempotent by name: add returns already_exists if same name+interface+user exists; throws CONFLICT if same name exists with different config. Update builds a diff of changed fields and returns no_change when nothing differs. Password is always written when provided because RouterOS does not expose it in GET.",
|
|
7865
|
+
inputSchema: managePppoeClientInputSchema,
|
|
7866
|
+
annotations: {
|
|
7867
|
+
readOnlyHint: false,
|
|
7868
|
+
destructiveHint: true,
|
|
7869
|
+
idempotentHint: true,
|
|
7870
|
+
openWorldHint: false
|
|
7871
|
+
},
|
|
7872
|
+
snapshotPaths: [PPPOE_PATH],
|
|
7873
|
+
async handler(params, context) {
|
|
7874
|
+
const parsed = managePppoeClientInputSchema.parse(params);
|
|
7875
|
+
log36.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing PPPoE client");
|
|
7876
|
+
try {
|
|
7877
|
+
const allClients = await context.routerClient.get(PPPOE_PATH, {
|
|
7878
|
+
limit: void 0,
|
|
7879
|
+
offset: void 0
|
|
7880
|
+
});
|
|
7881
|
+
const existing = allClients.find((c) => c.name === parsed.name);
|
|
7882
|
+
if (parsed.action === "add") {
|
|
7883
|
+
if (!parsed.interface) {
|
|
7884
|
+
throw new MikroMCPError({
|
|
7885
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
7886
|
+
code: "PPPOE_CLIENT_INTERFACE_REQUIRED",
|
|
7887
|
+
message: "interface is required for action add.",
|
|
7888
|
+
details: { name: parsed.name },
|
|
7889
|
+
recoverability: {
|
|
7890
|
+
retryable: false,
|
|
7891
|
+
suggestedAction: "Provide the parent interface (e.g. ether1).",
|
|
7892
|
+
alternativeTools: []
|
|
7893
|
+
}
|
|
7894
|
+
});
|
|
7895
|
+
}
|
|
7896
|
+
if (!parsed.user) {
|
|
7897
|
+
throw new MikroMCPError({
|
|
7898
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
7899
|
+
code: "PPPOE_CLIENT_USER_REQUIRED",
|
|
7900
|
+
message: "user is required for action add.",
|
|
7901
|
+
details: { name: parsed.name },
|
|
7902
|
+
recoverability: {
|
|
7903
|
+
retryable: false,
|
|
7904
|
+
suggestedAction: "Provide the PPPoE username.",
|
|
7905
|
+
alternativeTools: []
|
|
7906
|
+
}
|
|
7907
|
+
});
|
|
7908
|
+
}
|
|
7909
|
+
if (existing) {
|
|
7910
|
+
const sameConfig = existing.interface === parsed.interface && existing.user === parsed.user;
|
|
7911
|
+
if (sameConfig) {
|
|
7912
|
+
return {
|
|
7913
|
+
content: `PPPoE client "${parsed.name}" already exists with the same configuration. No changes made.`,
|
|
7914
|
+
structuredContent: { action: "already_exists", client: existing }
|
|
7915
|
+
};
|
|
7916
|
+
}
|
|
7917
|
+
throw new MikroMCPError({
|
|
7918
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
7919
|
+
code: "PPPOE_CLIENT_CONFLICT",
|
|
7920
|
+
message: `PPPoE client "${parsed.name}" already exists with a different configuration.`,
|
|
7921
|
+
details: {
|
|
7922
|
+
existing: { interface: existing.interface, user: existing.user },
|
|
7923
|
+
requested: { interface: parsed.interface, user: parsed.user }
|
|
7924
|
+
},
|
|
7925
|
+
recoverability: {
|
|
7926
|
+
retryable: false,
|
|
7927
|
+
suggestedAction: "Remove the existing client first or use action update.",
|
|
7928
|
+
alternativeTools: ["manage_pppoe_client"]
|
|
7929
|
+
}
|
|
7930
|
+
});
|
|
7931
|
+
}
|
|
7932
|
+
const diff = [
|
|
7933
|
+
{ property: "name", before: null, after: parsed.name },
|
|
7934
|
+
{ property: "interface", before: null, after: parsed.interface },
|
|
7935
|
+
{ property: "user", before: null, after: parsed.user }
|
|
7936
|
+
];
|
|
7937
|
+
if (parsed.serviceName !== void 0) {
|
|
7938
|
+
diff.push({ property: "service-name", before: null, after: parsed.serviceName });
|
|
7939
|
+
}
|
|
7940
|
+
if (parsed.addDefaultRoute !== void 0) {
|
|
7941
|
+
diff.push({ property: "add-default-route", before: null, after: parsed.addDefaultRoute ? "yes" : "no" });
|
|
7942
|
+
}
|
|
7943
|
+
if (parsed.dialOnDemand !== void 0) {
|
|
7944
|
+
diff.push({ property: "dial-on-demand", before: null, after: parsed.dialOnDemand ? "yes" : "no" });
|
|
7945
|
+
}
|
|
7946
|
+
if (parsed.dryRun) {
|
|
7947
|
+
return {
|
|
7948
|
+
content: `Dry run: Would create PPPoE client "${parsed.name}" on "${parsed.interface}".`,
|
|
7949
|
+
structuredContent: { action: "dry_run", diff }
|
|
7950
|
+
};
|
|
7951
|
+
}
|
|
7952
|
+
const body = {
|
|
7953
|
+
name: parsed.name,
|
|
7954
|
+
interface: parsed.interface,
|
|
7955
|
+
user: parsed.user
|
|
7956
|
+
};
|
|
7957
|
+
if (parsed.password !== void 0) body.password = parsed.password;
|
|
7958
|
+
if (parsed.serviceName !== void 0) body["service-name"] = parsed.serviceName;
|
|
7959
|
+
if (parsed.addDefaultRoute !== void 0) body["add-default-route"] = parsed.addDefaultRoute ? "yes" : "no";
|
|
7960
|
+
if (parsed.dialOnDemand !== void 0) body["dial-on-demand"] = parsed.dialOnDemand ? "yes" : "no";
|
|
7961
|
+
const created = await context.routerClient.create(PPPOE_PATH, body);
|
|
7962
|
+
log36.info({ name: parsed.name, id: created[".id"] }, "PPPoE client created");
|
|
7963
|
+
return {
|
|
7964
|
+
content: `Created PPPoE client "${parsed.name}" on "${parsed.interface}".`,
|
|
7965
|
+
structuredContent: { action: "created", client: created }
|
|
7966
|
+
};
|
|
7967
|
+
}
|
|
7968
|
+
if (parsed.action === "update") {
|
|
7969
|
+
if (!existing) {
|
|
7970
|
+
throw new MikroMCPError({
|
|
7971
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
7972
|
+
code: "PPPOE_CLIENT_NOT_FOUND",
|
|
7973
|
+
message: `PPPoE client "${parsed.name}" not found.`,
|
|
7974
|
+
details: { name: parsed.name },
|
|
7975
|
+
recoverability: {
|
|
7976
|
+
retryable: false,
|
|
7977
|
+
suggestedAction: "Verify with list_pppoe_clients.",
|
|
7978
|
+
alternativeTools: ["list_pppoe_clients"]
|
|
7979
|
+
}
|
|
7980
|
+
});
|
|
7981
|
+
}
|
|
7982
|
+
const updates = {};
|
|
7983
|
+
const diff = [];
|
|
7984
|
+
if (parsed.interface !== void 0 && existing.interface !== parsed.interface) {
|
|
7985
|
+
updates.interface = parsed.interface;
|
|
7986
|
+
diff.push({ property: "interface", before: existing.interface, after: parsed.interface });
|
|
7987
|
+
}
|
|
7988
|
+
if (parsed.user !== void 0 && existing.user !== parsed.user) {
|
|
7989
|
+
updates.user = parsed.user;
|
|
7990
|
+
diff.push({ property: "user", before: existing.user, after: parsed.user });
|
|
7991
|
+
}
|
|
7992
|
+
if (parsed.serviceName !== void 0 && existing["service-name"] !== parsed.serviceName) {
|
|
7993
|
+
updates["service-name"] = parsed.serviceName;
|
|
7994
|
+
diff.push({ property: "service-name", before: existing["service-name"] ?? null, after: parsed.serviceName });
|
|
7995
|
+
}
|
|
7996
|
+
if (parsed.addDefaultRoute !== void 0) {
|
|
7997
|
+
const desired = parsed.addDefaultRoute ? "yes" : "no";
|
|
7998
|
+
if (existing["add-default-route"] !== desired) {
|
|
7999
|
+
updates["add-default-route"] = desired;
|
|
8000
|
+
diff.push({ property: "add-default-route", before: existing["add-default-route"] ?? null, after: desired });
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
if (parsed.dialOnDemand !== void 0) {
|
|
8004
|
+
const desired = parsed.dialOnDemand ? "yes" : "no";
|
|
8005
|
+
if (existing["dial-on-demand"] !== desired) {
|
|
8006
|
+
updates["dial-on-demand"] = desired;
|
|
8007
|
+
diff.push({ property: "dial-on-demand", before: existing["dial-on-demand"] ?? null, after: desired });
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
if (parsed.password !== void 0) {
|
|
8011
|
+
updates.password = parsed.password;
|
|
8012
|
+
}
|
|
8013
|
+
const hasUpdates = Object.keys(updates).length > 0;
|
|
8014
|
+
if (!hasUpdates) {
|
|
8015
|
+
return {
|
|
8016
|
+
content: `PPPoE client "${parsed.name}" already matches the requested configuration. No changes made.`,
|
|
8017
|
+
structuredContent: { action: "no_change", client: existing }
|
|
8018
|
+
};
|
|
8019
|
+
}
|
|
8020
|
+
if (parsed.dryRun) {
|
|
8021
|
+
return {
|
|
8022
|
+
content: `Dry run: Would update PPPoE client "${parsed.name}".`,
|
|
8023
|
+
structuredContent: { action: "dry_run", diff }
|
|
8024
|
+
};
|
|
8025
|
+
}
|
|
8026
|
+
await context.routerClient.update(PPPOE_PATH, existing[".id"], updates);
|
|
8027
|
+
log36.info({ name: parsed.name }, "PPPoE client updated");
|
|
8028
|
+
return {
|
|
8029
|
+
content: `Updated PPPoE client "${parsed.name}".`,
|
|
8030
|
+
structuredContent: { action: "updated", name: parsed.name, diff }
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
if (!existing) {
|
|
8034
|
+
return {
|
|
8035
|
+
content: `PPPoE client "${parsed.name}" not found. Nothing to remove.`,
|
|
8036
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
8037
|
+
};
|
|
8038
|
+
}
|
|
8039
|
+
if (parsed.dryRun) {
|
|
8040
|
+
return {
|
|
8041
|
+
content: `Dry run: Would remove PPPoE client "${parsed.name}".`,
|
|
8042
|
+
structuredContent: {
|
|
8043
|
+
action: "dry_run",
|
|
8044
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
8045
|
+
}
|
|
8046
|
+
};
|
|
8047
|
+
}
|
|
8048
|
+
await context.routerClient.remove(PPPOE_PATH, existing[".id"]);
|
|
8049
|
+
log36.info({ name: parsed.name }, "PPPoE client removed");
|
|
8050
|
+
return {
|
|
8051
|
+
content: `Removed PPPoE client "${parsed.name}".`,
|
|
8052
|
+
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
8053
|
+
};
|
|
8054
|
+
} catch (err) {
|
|
8055
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8056
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_pppoe_client" });
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
};
|
|
8060
|
+
var pppoeTools = [listPppoeClientsTool, managePppoeClientTool];
|
|
8061
|
+
|
|
8062
|
+
// src/domain/tools/ovpn-tools.ts
|
|
8063
|
+
import { z as z36 } from "zod";
|
|
8064
|
+
var log37 = createLogger("ovpn-tools");
|
|
8065
|
+
var OVPN_CLIENT_PATH = "interface/ovpn-client";
|
|
8066
|
+
var OVPN_SERVER_PATH = "interface/ovpn-server/server";
|
|
8067
|
+
var listOvpnClientsInputSchema = z36.object({
|
|
8068
|
+
routerId: z36.string().describe("Target router identifier from the router registry"),
|
|
8069
|
+
limit: z36.number().int().min(1).max(500).default(100).describe("Maximum number of clients to return"),
|
|
8070
|
+
offset: z36.number().int().min(0).default(0).describe("Offset for pagination")
|
|
8071
|
+
}).strict();
|
|
8072
|
+
var listOvpnClientsTool = {
|
|
8073
|
+
name: "list_ovpn_clients",
|
|
8074
|
+
title: "List OpenVPN Clients",
|
|
8075
|
+
description: "List OpenVPN client interfaces on a MikroTik router. Shows name, remote server, and connection status.",
|
|
8076
|
+
inputSchema: listOvpnClientsInputSchema,
|
|
8077
|
+
annotations: {
|
|
8078
|
+
readOnlyHint: true,
|
|
8079
|
+
destructiveHint: false,
|
|
8080
|
+
idempotentHint: true,
|
|
8081
|
+
openWorldHint: false
|
|
8082
|
+
},
|
|
8083
|
+
async handler(params, context) {
|
|
8084
|
+
const parsed = listOvpnClientsInputSchema.parse(params);
|
|
8085
|
+
log37.info({ routerId: context.routerId }, "Listing OpenVPN clients");
|
|
8086
|
+
try {
|
|
8087
|
+
const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
|
|
8088
|
+
limit: void 0,
|
|
8089
|
+
offset: void 0
|
|
8090
|
+
});
|
|
8091
|
+
const total = allClients.length;
|
|
8092
|
+
const clients = allClients.slice(parsed.offset, parsed.offset + parsed.limit);
|
|
8093
|
+
const hasMore = parsed.offset + parsed.limit < total;
|
|
8094
|
+
return {
|
|
8095
|
+
content: `OpenVPN clients on ${context.routerId}: ${total} total, showing ${clients.length} (offset ${parsed.offset})`,
|
|
8096
|
+
structuredContent: {
|
|
8097
|
+
routerId: context.routerId,
|
|
8098
|
+
clients,
|
|
8099
|
+
total,
|
|
8100
|
+
hasMore,
|
|
8101
|
+
offset: parsed.offset,
|
|
8102
|
+
limit: parsed.limit
|
|
8103
|
+
}
|
|
8104
|
+
};
|
|
8105
|
+
} catch (err) {
|
|
8106
|
+
throw enrichError(err, { routerId: context.routerId, tool: "list_ovpn_clients" });
|
|
8107
|
+
}
|
|
8108
|
+
}
|
|
8109
|
+
};
|
|
8110
|
+
var manageOvpnClientInputSchema = z36.object({
|
|
8111
|
+
routerId: z36.string().describe("Target router identifier from the router registry"),
|
|
8112
|
+
action: z36.enum(["add", "update", "remove"]).describe("Action to perform"),
|
|
8113
|
+
name: z36.string().describe("OpenVPN client interface name \u2014 idempotency key"),
|
|
8114
|
+
connectTo: z36.string().optional().describe("Remote server address (required for add)"),
|
|
8115
|
+
port: z36.number().int().min(1).max(65535).optional().describe("Remote port number"),
|
|
8116
|
+
mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode"),
|
|
8117
|
+
protocol: z36.enum(["tcp-client", "udp"]).optional().describe("Transport protocol"),
|
|
8118
|
+
certificate: z36.string().optional().describe("Client certificate name"),
|
|
8119
|
+
user: z36.string().optional().describe("VPN username"),
|
|
8120
|
+
password: z36.string().optional().describe("VPN password (never logged)"),
|
|
8121
|
+
dryRun: z36.boolean().default(false).describe("Preview changes without applying")
|
|
8122
|
+
}).strict();
|
|
8123
|
+
var manageOvpnClientTool = {
|
|
8124
|
+
name: "manage_ovpn_client",
|
|
8125
|
+
title: "Manage OpenVPN Client",
|
|
8126
|
+
description: "Add, update, or remove an OpenVPN client interface. Idempotent by name: add returns already_exists if same name+connectTo exists; throws CONFLICT if same name exists with different connectTo. Update builds a diff of changed fields and returns no_change when nothing differs. Password is always written when provided because RouterOS does not expose it in GET.",
|
|
8127
|
+
inputSchema: manageOvpnClientInputSchema,
|
|
8128
|
+
annotations: {
|
|
8129
|
+
readOnlyHint: false,
|
|
8130
|
+
destructiveHint: true,
|
|
8131
|
+
idempotentHint: true,
|
|
8132
|
+
openWorldHint: false
|
|
8133
|
+
},
|
|
8134
|
+
snapshotPaths: [OVPN_CLIENT_PATH],
|
|
8135
|
+
async handler(params, context) {
|
|
8136
|
+
const parsed = manageOvpnClientInputSchema.parse(params);
|
|
8137
|
+
log37.info({ routerId: context.routerId, action: parsed.action, name: parsed.name }, "Managing OpenVPN client");
|
|
8138
|
+
try {
|
|
8139
|
+
const allClients = await context.routerClient.get(OVPN_CLIENT_PATH, {
|
|
8140
|
+
limit: void 0,
|
|
8141
|
+
offset: void 0
|
|
8142
|
+
});
|
|
8143
|
+
const existing = allClients.find((c) => c.name === parsed.name);
|
|
8144
|
+
if (parsed.action === "add") {
|
|
8145
|
+
if (!parsed.connectTo) {
|
|
8146
|
+
throw new MikroMCPError({
|
|
8147
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
8148
|
+
code: "OVPN_CLIENT_CONNECT_TO_REQUIRED",
|
|
8149
|
+
message: "connectTo is required for action add.",
|
|
8150
|
+
details: { name: parsed.name },
|
|
8151
|
+
recoverability: {
|
|
8152
|
+
retryable: false,
|
|
8153
|
+
suggestedAction: "Provide the remote server address.",
|
|
8154
|
+
alternativeTools: []
|
|
8155
|
+
}
|
|
8156
|
+
});
|
|
8157
|
+
}
|
|
8158
|
+
if (existing) {
|
|
8159
|
+
const sameConfig = existing["connect-to"] === parsed.connectTo;
|
|
8160
|
+
if (sameConfig) {
|
|
8161
|
+
return {
|
|
8162
|
+
content: `OpenVPN client "${parsed.name}" already exists with the same configuration. No changes made.`,
|
|
8163
|
+
structuredContent: { action: "already_exists", client: existing }
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8166
|
+
throw new MikroMCPError({
|
|
8167
|
+
category: "CONFLICT" /* CONFLICT */,
|
|
8168
|
+
code: "OVPN_CLIENT_CONFLICT",
|
|
8169
|
+
message: `OpenVPN client "${parsed.name}" already exists with a different configuration.`,
|
|
8170
|
+
details: {
|
|
8171
|
+
existing: { "connect-to": existing["connect-to"] },
|
|
8172
|
+
requested: { "connect-to": parsed.connectTo }
|
|
8173
|
+
},
|
|
8174
|
+
recoverability: {
|
|
8175
|
+
retryable: false,
|
|
8176
|
+
suggestedAction: "Remove the existing client first or use action update.",
|
|
8177
|
+
alternativeTools: ["manage_ovpn_client"]
|
|
8178
|
+
}
|
|
8179
|
+
});
|
|
8180
|
+
}
|
|
8181
|
+
const diff = [
|
|
8182
|
+
{ property: "name", before: null, after: parsed.name },
|
|
8183
|
+
{ property: "connect-to", before: null, after: parsed.connectTo }
|
|
8184
|
+
];
|
|
8185
|
+
if (parsed.dryRun) {
|
|
8186
|
+
return {
|
|
8187
|
+
content: `Dry run: Would create OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
|
|
8188
|
+
structuredContent: { action: "dry_run", diff }
|
|
8189
|
+
};
|
|
8190
|
+
}
|
|
8191
|
+
const body = {
|
|
8192
|
+
name: parsed.name,
|
|
8193
|
+
"connect-to": parsed.connectTo
|
|
8194
|
+
};
|
|
8195
|
+
if (parsed.port !== void 0) body.port = String(parsed.port);
|
|
8196
|
+
if (parsed.mode !== void 0) body.mode = parsed.mode;
|
|
8197
|
+
if (parsed.protocol !== void 0) body.protocol = parsed.protocol;
|
|
8198
|
+
if (parsed.certificate !== void 0) body.certificate = parsed.certificate;
|
|
8199
|
+
if (parsed.user !== void 0) body.user = parsed.user;
|
|
8200
|
+
if (parsed.password !== void 0) body.password = parsed.password;
|
|
8201
|
+
const created = await context.routerClient.create(OVPN_CLIENT_PATH, body);
|
|
8202
|
+
log37.info({ name: parsed.name, id: created[".id"] }, "OpenVPN client created");
|
|
8203
|
+
return {
|
|
8204
|
+
content: `Created OpenVPN client "${parsed.name}" connecting to "${parsed.connectTo}".`,
|
|
8205
|
+
structuredContent: { action: "created", client: created }
|
|
8206
|
+
};
|
|
8207
|
+
}
|
|
8208
|
+
if (parsed.action === "update") {
|
|
8209
|
+
if (!existing) {
|
|
8210
|
+
throw new MikroMCPError({
|
|
8211
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
8212
|
+
code: "OVPN_CLIENT_NOT_FOUND",
|
|
8213
|
+
message: `OpenVPN client "${parsed.name}" not found.`,
|
|
8214
|
+
details: { name: parsed.name },
|
|
8215
|
+
recoverability: {
|
|
8216
|
+
retryable: false,
|
|
8217
|
+
suggestedAction: "Verify with list_ovpn_clients.",
|
|
8218
|
+
alternativeTools: ["list_ovpn_clients"]
|
|
8219
|
+
}
|
|
8220
|
+
});
|
|
8221
|
+
}
|
|
8222
|
+
const updates = {};
|
|
8223
|
+
const diff = [];
|
|
8224
|
+
if (parsed.connectTo !== void 0 && existing["connect-to"] !== parsed.connectTo) {
|
|
8225
|
+
updates["connect-to"] = parsed.connectTo;
|
|
8226
|
+
diff.push({ property: "connect-to", before: existing["connect-to"] ?? null, after: parsed.connectTo });
|
|
8227
|
+
}
|
|
8228
|
+
if (parsed.port !== void 0 && existing.port !== String(parsed.port)) {
|
|
8229
|
+
updates.port = String(parsed.port);
|
|
8230
|
+
diff.push({ property: "port", before: existing.port ?? null, after: String(parsed.port) });
|
|
8231
|
+
}
|
|
8232
|
+
if (parsed.mode !== void 0 && existing.mode !== parsed.mode) {
|
|
8233
|
+
updates.mode = parsed.mode;
|
|
8234
|
+
diff.push({ property: "mode", before: existing.mode ?? null, after: parsed.mode });
|
|
8235
|
+
}
|
|
8236
|
+
if (parsed.protocol !== void 0 && existing.protocol !== parsed.protocol) {
|
|
8237
|
+
updates.protocol = parsed.protocol;
|
|
8238
|
+
diff.push({ property: "protocol", before: existing.protocol ?? null, after: parsed.protocol });
|
|
8239
|
+
}
|
|
8240
|
+
if (parsed.certificate !== void 0 && existing.certificate !== parsed.certificate) {
|
|
8241
|
+
updates.certificate = parsed.certificate;
|
|
8242
|
+
diff.push({ property: "certificate", before: existing.certificate ?? null, after: parsed.certificate });
|
|
8243
|
+
}
|
|
8244
|
+
if (parsed.user !== void 0 && existing.user !== parsed.user) {
|
|
8245
|
+
updates.user = parsed.user;
|
|
8246
|
+
diff.push({ property: "user", before: existing.user ?? null, after: parsed.user });
|
|
8247
|
+
}
|
|
8248
|
+
if (parsed.password !== void 0) {
|
|
8249
|
+
updates.password = parsed.password;
|
|
8250
|
+
}
|
|
8251
|
+
const hasUpdates = Object.keys(updates).length > 0;
|
|
8252
|
+
if (!hasUpdates) {
|
|
8253
|
+
return {
|
|
8254
|
+
content: `OpenVPN client "${parsed.name}" already matches the requested configuration. No changes made.`,
|
|
8255
|
+
structuredContent: { action: "no_change", client: existing }
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
if (parsed.dryRun) {
|
|
8259
|
+
return {
|
|
8260
|
+
content: `Dry run: Would update OpenVPN client "${parsed.name}".`,
|
|
8261
|
+
structuredContent: { action: "dry_run", diff }
|
|
8262
|
+
};
|
|
8263
|
+
}
|
|
8264
|
+
await context.routerClient.update(OVPN_CLIENT_PATH, existing[".id"], updates);
|
|
8265
|
+
log37.info({ name: parsed.name }, "OpenVPN client updated");
|
|
8266
|
+
return {
|
|
8267
|
+
content: `Updated OpenVPN client "${parsed.name}".`,
|
|
8268
|
+
structuredContent: { action: "updated", name: parsed.name, diff }
|
|
8269
|
+
};
|
|
8270
|
+
}
|
|
8271
|
+
if (!existing) {
|
|
8272
|
+
return {
|
|
8273
|
+
content: `OpenVPN client "${parsed.name}" not found. Nothing to remove.`,
|
|
8274
|
+
structuredContent: { action: "not_found", name: parsed.name }
|
|
8275
|
+
};
|
|
8276
|
+
}
|
|
8277
|
+
if (parsed.dryRun) {
|
|
8278
|
+
return {
|
|
8279
|
+
content: `Dry run: Would remove OpenVPN client "${parsed.name}".`,
|
|
8280
|
+
structuredContent: {
|
|
8281
|
+
action: "dry_run",
|
|
8282
|
+
diff: [{ property: "name", before: parsed.name, after: null }]
|
|
8283
|
+
}
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
await context.routerClient.remove(OVPN_CLIENT_PATH, existing[".id"]);
|
|
8287
|
+
log37.info({ name: parsed.name }, "OpenVPN client removed");
|
|
8288
|
+
return {
|
|
8289
|
+
content: `Removed OpenVPN client "${parsed.name}".`,
|
|
8290
|
+
structuredContent: { action: "removed", name: parsed.name, id: existing[".id"] }
|
|
8291
|
+
};
|
|
8292
|
+
} catch (err) {
|
|
8293
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8294
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_client" });
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
};
|
|
8298
|
+
var getOvpnServerInputSchema = z36.object({
|
|
8299
|
+
routerId: z36.string().describe("Target router identifier from the router registry")
|
|
8300
|
+
}).strict();
|
|
8301
|
+
function fetchOvpnServer(allRecords, routerId) {
|
|
8302
|
+
if (allRecords.length === 0) {
|
|
8303
|
+
throw new MikroMCPError({
|
|
8304
|
+
category: "NOT_FOUND" /* NOT_FOUND */,
|
|
8305
|
+
code: "OVPN_SERVER_NOT_FOUND",
|
|
8306
|
+
message: "OpenVPN server not found. The OpenVPN package may not be installed on this router.",
|
|
8307
|
+
details: { routerId },
|
|
8308
|
+
recoverability: {
|
|
8309
|
+
retryable: false,
|
|
8310
|
+
suggestedAction: "Install the OpenVPN package via manage_package.",
|
|
8311
|
+
alternativeTools: ["list_packages", "manage_package"]
|
|
8312
|
+
}
|
|
8313
|
+
});
|
|
8314
|
+
}
|
|
8315
|
+
return allRecords[0];
|
|
8316
|
+
}
|
|
8317
|
+
var getOvpnServerTool = {
|
|
8318
|
+
name: "get_ovpn_server",
|
|
8319
|
+
title: "Get OpenVPN Server",
|
|
8320
|
+
description: "Get the OpenVPN server configuration on a MikroTik router. Throws NOT_FOUND if the OpenVPN package is not installed.",
|
|
8321
|
+
inputSchema: getOvpnServerInputSchema,
|
|
8322
|
+
annotations: {
|
|
8323
|
+
readOnlyHint: true,
|
|
8324
|
+
destructiveHint: false,
|
|
8325
|
+
idempotentHint: true,
|
|
8326
|
+
openWorldHint: false
|
|
8327
|
+
},
|
|
8328
|
+
async handler(params, context) {
|
|
8329
|
+
const parsed = getOvpnServerInputSchema.parse(params);
|
|
8330
|
+
log37.info({ routerId: context.routerId }, "Getting OpenVPN server configuration");
|
|
8331
|
+
try {
|
|
8332
|
+
const records = await context.routerClient.get(OVPN_SERVER_PATH, {
|
|
8333
|
+
limit: void 0,
|
|
8334
|
+
offset: void 0
|
|
8335
|
+
});
|
|
8336
|
+
const server = fetchOvpnServer(records, parsed.routerId);
|
|
8337
|
+
return {
|
|
8338
|
+
content: `OpenVPN server on ${context.routerId}: enabled=${server.enabled}, port=${server.port}, protocol=${server.protocol}`,
|
|
8339
|
+
structuredContent: { routerId: context.routerId, server }
|
|
8340
|
+
};
|
|
8341
|
+
} catch (err) {
|
|
8342
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8343
|
+
throw enrichError(err, { routerId: context.routerId, tool: "get_ovpn_server" });
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
};
|
|
8347
|
+
var manageOvpnServerInputSchema = z36.object({
|
|
8348
|
+
routerId: z36.string().describe("Target router identifier from the router registry"),
|
|
8349
|
+
action: z36.enum(["enable", "disable", "set"]).describe("Action to perform"),
|
|
8350
|
+
port: z36.number().int().min(1).max(65535).optional().describe("Listening port (set action only)"),
|
|
8351
|
+
mode: z36.enum(["ip", "ethernet"]).optional().describe("Tunnel mode (set action only)"),
|
|
8352
|
+
protocol: z36.enum(["tcp-server", "udp"]).optional().describe("Transport protocol (set action only)"),
|
|
8353
|
+
certificate: z36.string().optional().describe("Server certificate name (set action only)"),
|
|
8354
|
+
cipher: z36.enum(["blowfish128", "aes128-cbc", "aes192-cbc", "aes256-cbc", "aes128-gcm", "aes256-gcm", "none"]).optional().describe("Encryption cipher (set action only)"),
|
|
8355
|
+
auth: z36.enum(["md5", "sha1", "sha256", "sha512", "null"]).optional().describe("Authentication algorithm (set action only)"),
|
|
8356
|
+
dryRun: z36.boolean().default(false).describe("Preview changes without applying")
|
|
8357
|
+
}).strict();
|
|
8358
|
+
var manageOvpnServerTool = {
|
|
8359
|
+
name: "manage_ovpn_server",
|
|
8360
|
+
title: "Manage OpenVPN Server",
|
|
8361
|
+
description: "Enable, disable, or configure the OpenVPN server on a MikroTik router. The server is a singleton \u2014 there is only one per router. Throws NOT_FOUND if the OpenVPN package is not installed. For set action, at least one configuration field must be provided.",
|
|
8362
|
+
inputSchema: manageOvpnServerInputSchema,
|
|
8363
|
+
annotations: {
|
|
8364
|
+
readOnlyHint: false,
|
|
8365
|
+
destructiveHint: true,
|
|
8366
|
+
idempotentHint: true,
|
|
8367
|
+
openWorldHint: false
|
|
8368
|
+
},
|
|
8369
|
+
snapshotPaths: [OVPN_SERVER_PATH],
|
|
8370
|
+
async handler(params, context) {
|
|
8371
|
+
const parsed = manageOvpnServerInputSchema.parse(params);
|
|
8372
|
+
log37.info({ routerId: context.routerId, action: parsed.action }, "Managing OpenVPN server");
|
|
8373
|
+
try {
|
|
8374
|
+
const records = await context.routerClient.get(OVPN_SERVER_PATH, {
|
|
8375
|
+
limit: void 0,
|
|
8376
|
+
offset: void 0
|
|
8377
|
+
});
|
|
8378
|
+
const server = fetchOvpnServer(records, parsed.routerId);
|
|
8379
|
+
const serverId = server[".id"];
|
|
8380
|
+
if (parsed.action === "enable" || parsed.action === "disable") {
|
|
8381
|
+
const desiredEnabled = parsed.action === "enable";
|
|
8382
|
+
const currentEnabled = server.enabled === "yes";
|
|
8383
|
+
if (desiredEnabled === currentEnabled) {
|
|
8384
|
+
return {
|
|
8385
|
+
content: `OpenVPN server on ${context.routerId} is already ${parsed.action}d. No changes made.`,
|
|
8386
|
+
structuredContent: { action: "no_change", server }
|
|
8387
|
+
};
|
|
8388
|
+
}
|
|
8389
|
+
if (parsed.dryRun) {
|
|
8390
|
+
return {
|
|
8391
|
+
content: `Dry run: Would ${parsed.action} OpenVPN server on ${context.routerId}.`,
|
|
8392
|
+
structuredContent: {
|
|
8393
|
+
action: "dry_run",
|
|
8394
|
+
diff: [{ property: "enabled", before: server.enabled, after: desiredEnabled ? "yes" : "no" }]
|
|
8395
|
+
}
|
|
8396
|
+
};
|
|
8397
|
+
}
|
|
8398
|
+
await context.routerClient.update(OVPN_SERVER_PATH, serverId, {
|
|
8399
|
+
enabled: desiredEnabled ? "yes" : "no"
|
|
8400
|
+
});
|
|
8401
|
+
log37.info({ routerId: context.routerId, action: parsed.action }, "OpenVPN server updated");
|
|
8402
|
+
return {
|
|
8403
|
+
content: `OpenVPN server on ${context.routerId} ${parsed.action}d.`,
|
|
8404
|
+
structuredContent: { action: parsed.action === "enable" ? "enabled" : "disabled", server }
|
|
8405
|
+
};
|
|
8406
|
+
}
|
|
8407
|
+
const hasFields = parsed.port !== void 0 || parsed.mode !== void 0 || parsed.protocol !== void 0 || parsed.certificate !== void 0 || parsed.cipher !== void 0 || parsed.auth !== void 0;
|
|
8408
|
+
if (!hasFields) {
|
|
8409
|
+
throw new MikroMCPError({
|
|
8410
|
+
category: "VALIDATION" /* VALIDATION */,
|
|
8411
|
+
code: "OVPN_SERVER_NO_FIELDS",
|
|
8412
|
+
message: "At least one field must be provided for action set.",
|
|
8413
|
+
details: {},
|
|
8414
|
+
recoverability: {
|
|
8415
|
+
retryable: false,
|
|
8416
|
+
suggestedAction: "Provide at least one of: port, mode, protocol, certificate, cipher, auth.",
|
|
8417
|
+
alternativeTools: []
|
|
8418
|
+
}
|
|
8419
|
+
});
|
|
8420
|
+
}
|
|
8421
|
+
const updates = {};
|
|
8422
|
+
const diff = [];
|
|
8423
|
+
if (parsed.port !== void 0 && server.port !== String(parsed.port)) {
|
|
8424
|
+
updates.port = String(parsed.port);
|
|
8425
|
+
diff.push({ property: "port", before: server.port ?? null, after: String(parsed.port) });
|
|
8426
|
+
}
|
|
8427
|
+
if (parsed.mode !== void 0 && server.mode !== parsed.mode) {
|
|
8428
|
+
updates.mode = parsed.mode;
|
|
8429
|
+
diff.push({ property: "mode", before: server.mode ?? null, after: parsed.mode });
|
|
8430
|
+
}
|
|
8431
|
+
if (parsed.protocol !== void 0 && server.protocol !== parsed.protocol) {
|
|
8432
|
+
updates.protocol = parsed.protocol;
|
|
8433
|
+
diff.push({ property: "protocol", before: server.protocol ?? null, after: parsed.protocol });
|
|
8434
|
+
}
|
|
8435
|
+
if (parsed.certificate !== void 0 && server.certificate !== parsed.certificate) {
|
|
8436
|
+
updates.certificate = parsed.certificate;
|
|
8437
|
+
diff.push({ property: "certificate", before: server.certificate ?? null, after: parsed.certificate });
|
|
8438
|
+
}
|
|
8439
|
+
if (parsed.cipher !== void 0 && server.cipher !== parsed.cipher) {
|
|
8440
|
+
updates.cipher = parsed.cipher;
|
|
8441
|
+
diff.push({ property: "cipher", before: server.cipher ?? null, after: parsed.cipher });
|
|
8442
|
+
}
|
|
8443
|
+
if (parsed.auth !== void 0 && server.auth !== parsed.auth) {
|
|
8444
|
+
updates.auth = parsed.auth;
|
|
8445
|
+
diff.push({ property: "auth", before: server.auth ?? null, after: parsed.auth });
|
|
8446
|
+
}
|
|
8447
|
+
if (Object.keys(updates).length === 0) {
|
|
8448
|
+
return {
|
|
8449
|
+
content: `OpenVPN server on ${context.routerId} already matches the requested configuration. No changes made.`,
|
|
8450
|
+
structuredContent: { action: "no_change", server }
|
|
8451
|
+
};
|
|
8452
|
+
}
|
|
8453
|
+
if (parsed.dryRun) {
|
|
8454
|
+
return {
|
|
8455
|
+
content: `Dry run: Would update OpenVPN server on ${context.routerId}.`,
|
|
8456
|
+
structuredContent: { action: "dry_run", diff }
|
|
8457
|
+
};
|
|
8458
|
+
}
|
|
8459
|
+
await context.routerClient.update(OVPN_SERVER_PATH, serverId, updates);
|
|
8460
|
+
log37.info({ routerId: context.routerId }, "OpenVPN server configuration updated");
|
|
8461
|
+
return {
|
|
8462
|
+
content: `Updated OpenVPN server configuration on ${context.routerId}.`,
|
|
8463
|
+
structuredContent: { action: "updated", diff }
|
|
8464
|
+
};
|
|
8465
|
+
} catch (err) {
|
|
8466
|
+
if (err instanceof MikroMCPError) throw err;
|
|
8467
|
+
throw enrichError(err, { routerId: context.routerId, tool: "manage_ovpn_server" });
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
};
|
|
8471
|
+
var ovpnTools = [
|
|
8472
|
+
listOvpnClientsTool,
|
|
8473
|
+
manageOvpnClientTool,
|
|
8474
|
+
getOvpnServerTool,
|
|
8475
|
+
manageOvpnServerTool
|
|
8476
|
+
];
|
|
8477
|
+
|
|
8478
|
+
// src/domain/tools/fleet-tools.ts
|
|
8479
|
+
import { z as z37 } from "zod";
|
|
7780
8480
|
|
|
7781
8481
|
// src/middleware/authz.ts
|
|
7782
8482
|
function matchesPattern(name, pattern) {
|
|
@@ -7944,7 +8644,7 @@ var SshClient = class {
|
|
|
7944
8644
|
// src/adapter/ftp-client.ts
|
|
7945
8645
|
import { Client as Client2 } from "basic-ftp";
|
|
7946
8646
|
import { Readable } from "stream";
|
|
7947
|
-
var
|
|
8647
|
+
var log38 = createLogger("ftp-client");
|
|
7948
8648
|
var FtpClient = class {
|
|
7949
8649
|
#host;
|
|
7950
8650
|
#port;
|
|
@@ -7957,7 +8657,7 @@ var FtpClient = class {
|
|
|
7957
8657
|
this.#password = password;
|
|
7958
8658
|
}
|
|
7959
8659
|
async upload(remoteName, content) {
|
|
7960
|
-
|
|
8660
|
+
log38.info({ host: this.#host, remoteName }, "Uploading file via FTP");
|
|
7961
8661
|
const client = new Client2();
|
|
7962
8662
|
try {
|
|
7963
8663
|
await client.access({
|
|
@@ -7967,7 +8667,7 @@ var FtpClient = class {
|
|
|
7967
8667
|
password: this.#password
|
|
7968
8668
|
});
|
|
7969
8669
|
await client.uploadFrom(Readable.from(Buffer.from(content, "utf-8")), remoteName);
|
|
7970
|
-
|
|
8670
|
+
log38.info({ host: this.#host, remoteName }, "FTP upload complete");
|
|
7971
8671
|
} finally {
|
|
7972
8672
|
client.close();
|
|
7973
8673
|
}
|
|
@@ -8075,17 +8775,17 @@ function checkFleetConfirmation(args, secret) {
|
|
|
8075
8775
|
}
|
|
8076
8776
|
|
|
8077
8777
|
// src/domain/tools/fleet-tools.ts
|
|
8078
|
-
var
|
|
8079
|
-
var checkHealthInputSchema =
|
|
8080
|
-
routerId:
|
|
8778
|
+
var log39 = createLogger("fleet-tools");
|
|
8779
|
+
var checkHealthInputSchema = z37.object({
|
|
8780
|
+
routerId: z37.string().describe("Target router to health-check")
|
|
8081
8781
|
}).strict();
|
|
8082
|
-
var bulkExecuteInputSchema =
|
|
8083
|
-
toolName:
|
|
8084
|
-
routerIds:
|
|
8085
|
-
tags:
|
|
8086
|
-
params:
|
|
8087
|
-
concurrency:
|
|
8088
|
-
confirmationToken:
|
|
8782
|
+
var bulkExecuteInputSchema = z37.object({
|
|
8783
|
+
toolName: z37.string().describe("Name of the tool to fan out (must be a single-router tool)"),
|
|
8784
|
+
routerIds: z37.array(z37.string()).optional().describe("Explicit list of router IDs to target"),
|
|
8785
|
+
tags: z37.array(z37.string()).optional().describe("Target all routers with ALL of these tags (mutually exclusive with routerIds)"),
|
|
8786
|
+
params: z37.record(z37.string(), z37.unknown()).describe("Params to pass to the tool (omit routerId \u2014 injected per router)"),
|
|
8787
|
+
concurrency: z37.number().int().min(1).max(20).default(5).describe("Max simultaneous router calls"),
|
|
8788
|
+
confirmationToken: z37.string().optional().describe(
|
|
8089
8789
|
"Fleet confirmation token from a prior APPROVAL_REQUIRED response. Required to fan out a destructive tool."
|
|
8090
8790
|
)
|
|
8091
8791
|
}).strict();
|
|
@@ -8104,7 +8804,7 @@ function createFleetTools(baseTools2) {
|
|
|
8104
8804
|
},
|
|
8105
8805
|
async handler(params, context) {
|
|
8106
8806
|
checkHealthInputSchema.parse(params);
|
|
8107
|
-
|
|
8807
|
+
log39.info({ routerId: context.routerId }, "Checking router health");
|
|
8108
8808
|
const startMs = Date.now();
|
|
8109
8809
|
try {
|
|
8110
8810
|
const resources = await context.routerClient.get("system/resource");
|
|
@@ -8166,7 +8866,7 @@ function createFleetTools(baseTools2) {
|
|
|
8166
8866
|
}
|
|
8167
8867
|
});
|
|
8168
8868
|
}
|
|
8169
|
-
|
|
8869
|
+
log39.info(
|
|
8170
8870
|
{ toolName: parsed.toolName, concurrency: parsed.concurrency },
|
|
8171
8871
|
"bulk_execute invoked"
|
|
8172
8872
|
);
|
|
@@ -8213,7 +8913,7 @@ function createFleetTools(baseTools2) {
|
|
|
8213
8913
|
try {
|
|
8214
8914
|
resolved.push(context.routerRegistry.getRouter(id));
|
|
8215
8915
|
} catch {
|
|
8216
|
-
|
|
8916
|
+
log39.warn({ routerId: id }, "bulk_execute: router not found");
|
|
8217
8917
|
preErrors.push({
|
|
8218
8918
|
routerId: id,
|
|
8219
8919
|
status: "error",
|
|
@@ -8391,7 +9091,9 @@ var baseTools = [
|
|
|
8391
9091
|
...networkServicesTools,
|
|
8392
9092
|
...vlanTools,
|
|
8393
9093
|
...dhcpClientTools,
|
|
8394
|
-
...ipServiceTools
|
|
9094
|
+
...ipServiceTools,
|
|
9095
|
+
...pppoeTools,
|
|
9096
|
+
...ovpnTools
|
|
8395
9097
|
];
|
|
8396
9098
|
var allTools = [
|
|
8397
9099
|
...baseTools,
|
|
@@ -8790,7 +9492,7 @@ var CircuitBreaker = class {
|
|
|
8790
9492
|
};
|
|
8791
9493
|
|
|
8792
9494
|
// src/mcp/tool-executor.ts
|
|
8793
|
-
var
|
|
9495
|
+
var log40 = createLogger("tool-executor");
|
|
8794
9496
|
async function executeToolCall(tool, args, deps) {
|
|
8795
9497
|
const { registry, pool, circuitBreakers, config, identityRegistry } = deps;
|
|
8796
9498
|
const correlationId = nanoid4();
|
|
@@ -8886,9 +9588,9 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8886
9588
|
try {
|
|
8887
9589
|
const meta = await takeSnapshot(toolContext.routerClient, routerId, path, config.snapshotDir);
|
|
8888
9590
|
snapshotIds.push(meta.id);
|
|
8889
|
-
|
|
9591
|
+
log40.debug({ snapshotId: meta.id, path }, "Snapshot taken");
|
|
8890
9592
|
} catch (err) {
|
|
8891
|
-
|
|
9593
|
+
log40.warn({ err, path, routerId }, "Snapshot failed \u2014 proceeding without snapshot");
|
|
8892
9594
|
}
|
|
8893
9595
|
}
|
|
8894
9596
|
}
|
|
@@ -8930,7 +9632,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8930
9632
|
durationMs: Date.now() - startMs
|
|
8931
9633
|
}, config.auditLogPath);
|
|
8932
9634
|
}
|
|
8933
|
-
|
|
9635
|
+
log40.info({ tool: tool.name, routerId, correlationId }, "Tool executed successfully");
|
|
8934
9636
|
recordToolCall(tool.name, "success");
|
|
8935
9637
|
return formatToolResult(result);
|
|
8936
9638
|
} catch (err) {
|
|
@@ -8939,7 +9641,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8939
9641
|
const failedRouterId = args.routerId;
|
|
8940
9642
|
if (failedRouterId) {
|
|
8941
9643
|
pool.removeClient(failedRouterId);
|
|
8942
|
-
|
|
9644
|
+
log40.info({ routerId: failedRouterId }, "Evicted pooled client after auth failure");
|
|
8943
9645
|
}
|
|
8944
9646
|
}
|
|
8945
9647
|
if (journalId) {
|
|
@@ -8969,7 +9671,7 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8969
9671
|
durationMs: Date.now() - startMs
|
|
8970
9672
|
}, config.auditLogPath);
|
|
8971
9673
|
}
|
|
8972
|
-
|
|
9674
|
+
log40.error({ err: error, tool: tool.name, correlationId }, "Tool execution failed");
|
|
8973
9675
|
recordToolCall(tool.name, "error");
|
|
8974
9676
|
return formatError(error);
|
|
8975
9677
|
}
|
|
@@ -8977,12 +9679,12 @@ async function executeToolCall(tool, args, deps) {
|
|
|
8977
9679
|
}
|
|
8978
9680
|
|
|
8979
9681
|
// src/mcp/tool-registry.ts
|
|
8980
|
-
var
|
|
9682
|
+
var log41 = createLogger("tool-registry");
|
|
8981
9683
|
function registerAllTools(server, registry, pool, circuitBreakers, config, identityRegistry) {
|
|
8982
9684
|
const deps = { registry, pool, circuitBreakers, config, identityRegistry };
|
|
8983
9685
|
for (const tool of allTools) {
|
|
8984
9686
|
const registrationSchema = tool.annotations.destructiveHint ? tool.inputSchema.extend({
|
|
8985
|
-
confirmationToken:
|
|
9687
|
+
confirmationToken: z38.string().optional().describe(
|
|
8986
9688
|
"Token from a prior APPROVAL_REQUIRED response. Re-submit the identical call with this token to confirm the destructive action."
|
|
8987
9689
|
)
|
|
8988
9690
|
}) : tool.inputSchema;
|
|
@@ -8996,56 +9698,56 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
|
|
|
8996
9698
|
},
|
|
8997
9699
|
(args) => executeToolCall(tool, args, deps)
|
|
8998
9700
|
);
|
|
8999
|
-
|
|
9701
|
+
log41.debug({ tool: tool.name }, "Registered tool");
|
|
9000
9702
|
}
|
|
9001
|
-
|
|
9703
|
+
log41.info({ count: allTools.length }, "All tools registered");
|
|
9002
9704
|
}
|
|
9003
9705
|
|
|
9004
9706
|
// src/config/router-registry.ts
|
|
9005
9707
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
9006
9708
|
import { parse } from "yaml";
|
|
9007
|
-
import { z as
|
|
9008
|
-
var
|
|
9009
|
-
var RouterConfigSchema =
|
|
9010
|
-
host:
|
|
9011
|
-
port:
|
|
9012
|
-
tls:
|
|
9013
|
-
enabled:
|
|
9014
|
-
rejectUnauthorized:
|
|
9015
|
-
ca:
|
|
9016
|
-
fingerprint:
|
|
9709
|
+
import { z as z39 } from "zod";
|
|
9710
|
+
var log42 = createLogger("router-registry");
|
|
9711
|
+
var RouterConfigSchema = z39.object({
|
|
9712
|
+
host: z39.string().min(1, "host is required"),
|
|
9713
|
+
port: z39.number().int().min(1).max(65535),
|
|
9714
|
+
tls: z39.object({
|
|
9715
|
+
enabled: z39.boolean(),
|
|
9716
|
+
rejectUnauthorized: z39.boolean(),
|
|
9717
|
+
ca: z39.string().optional(),
|
|
9718
|
+
fingerprint: z39.string().optional()
|
|
9017
9719
|
}).strict(),
|
|
9018
|
-
credentials:
|
|
9019
|
-
source:
|
|
9020
|
-
envPrefix:
|
|
9021
|
-
vaultPath:
|
|
9720
|
+
credentials: z39.object({
|
|
9721
|
+
source: z39.enum(["env", "vault"]),
|
|
9722
|
+
envPrefix: z39.string().optional(),
|
|
9723
|
+
vaultPath: z39.string().optional()
|
|
9022
9724
|
}).strict(),
|
|
9023
|
-
tags:
|
|
9024
|
-
rosVersion:
|
|
9025
|
-
sshPort:
|
|
9026
|
-
sshFingerprint:
|
|
9027
|
-
cmdAllow:
|
|
9028
|
-
cmdDeny:
|
|
9029
|
-
maintenanceWindows:
|
|
9030
|
-
|
|
9031
|
-
days:
|
|
9032
|
-
|
|
9725
|
+
tags: z39.array(z39.string()).default([]),
|
|
9726
|
+
rosVersion: z39.string().min(1),
|
|
9727
|
+
sshPort: z39.number().int().min(1).max(65535).optional(),
|
|
9728
|
+
sshFingerprint: z39.string().optional(),
|
|
9729
|
+
cmdAllow: z39.array(z39.string()).optional(),
|
|
9730
|
+
cmdDeny: z39.array(z39.string()).optional(),
|
|
9731
|
+
maintenanceWindows: z39.array(
|
|
9732
|
+
z39.object({
|
|
9733
|
+
days: z39.array(
|
|
9734
|
+
z39.enum(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
|
|
9033
9735
|
),
|
|
9034
|
-
startTime:
|
|
9035
|
-
endTime:
|
|
9036
|
-
timezone:
|
|
9736
|
+
startTime: z39.string().regex(/^\d{2}:\d{2}$/, "startTime must be HH:MM"),
|
|
9737
|
+
endTime: z39.string().regex(/^\d{2}:\d{2}$/, "endTime must be HH:MM"),
|
|
9738
|
+
timezone: z39.string().min(1)
|
|
9037
9739
|
}).strict()
|
|
9038
9740
|
).optional()
|
|
9039
9741
|
}).strict();
|
|
9040
|
-
var ConfigFileSchema =
|
|
9041
|
-
routers:
|
|
9742
|
+
var ConfigFileSchema = z39.object({
|
|
9743
|
+
routers: z39.record(z39.string(), RouterConfigSchema)
|
|
9042
9744
|
}).strict();
|
|
9043
9745
|
var RouterRegistry = class {
|
|
9044
9746
|
routers;
|
|
9045
9747
|
constructor(configPath) {
|
|
9046
9748
|
this.routers = /* @__PURE__ */ new Map();
|
|
9047
9749
|
if (!existsSync(configPath)) {
|
|
9048
|
-
|
|
9750
|
+
log42.warn({ configPath }, "Router config file not found; starting with empty registry");
|
|
9049
9751
|
return;
|
|
9050
9752
|
}
|
|
9051
9753
|
const raw = readFileSync2(configPath, "utf-8");
|
|
@@ -9059,14 +9761,14 @@ ${issues}`);
|
|
|
9059
9761
|
for (const [id, config] of Object.entries(result.data.routers)) {
|
|
9060
9762
|
const routerConfig = { ...config, id };
|
|
9061
9763
|
if (routerConfig.tls.enabled && !routerConfig.tls.rejectUnauthorized) {
|
|
9062
|
-
|
|
9764
|
+
log42.warn(
|
|
9063
9765
|
{ routerId: id },
|
|
9064
9766
|
"TLS certificate validation is DISABLED (rejectUnauthorized=false). Set tls.fingerprint to pin the server certificate instead."
|
|
9065
9767
|
);
|
|
9066
9768
|
}
|
|
9067
9769
|
this.routers.set(id, routerConfig);
|
|
9068
9770
|
}
|
|
9069
|
-
|
|
9771
|
+
log42.info({ count: this.routers.size }, "Loaded routers from config");
|
|
9070
9772
|
}
|
|
9071
9773
|
getRouter(id) {
|
|
9072
9774
|
const router = this.routers.get(id);
|
|
@@ -9351,7 +10053,7 @@ var ConnectionPool = class {
|
|
|
9351
10053
|
};
|
|
9352
10054
|
|
|
9353
10055
|
// src/version.ts
|
|
9354
|
-
var VERSION = "1.
|
|
10056
|
+
var VERSION = "1.3.0";
|
|
9355
10057
|
|
|
9356
10058
|
// src/mcp/server.ts
|
|
9357
10059
|
function createServerFactory(config, identityRegistry) {
|
|
@@ -9381,7 +10083,7 @@ import { randomUUID } from "crypto";
|
|
|
9381
10083
|
import { createServer } from "http";
|
|
9382
10084
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9383
10085
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
9384
|
-
var
|
|
10086
|
+
var log43 = createLogger("transport-http");
|
|
9385
10087
|
var BodyTooLargeError = class extends Error {
|
|
9386
10088
|
constructor() {
|
|
9387
10089
|
super("Request body exceeds maximum allowed size");
|
|
@@ -9473,7 +10175,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
9473
10175
|
return;
|
|
9474
10176
|
}
|
|
9475
10177
|
if (!checkRateLimit(ip)) {
|
|
9476
|
-
|
|
10178
|
+
log43.warn({ ip }, "Rate limit exceeded");
|
|
9477
10179
|
res.writeHead(429, { "Content-Type": "application/json" });
|
|
9478
10180
|
res.end(JSON.stringify({ error: "Too many requests. Retry after 60 seconds." }));
|
|
9479
10181
|
return;
|
|
@@ -9514,7 +10216,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
9514
10216
|
}
|
|
9515
10217
|
return;
|
|
9516
10218
|
}
|
|
9517
|
-
|
|
10219
|
+
log43.error({ err }, "HTTP transport error");
|
|
9518
10220
|
if (!res.headersSent) {
|
|
9519
10221
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
9520
10222
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
@@ -9524,7 +10226,7 @@ async function connectHttp(makeServer, config, identityRegistry) {
|
|
|
9524
10226
|
httpServer.on("close", () => clearInterval(sweepTimer));
|
|
9525
10227
|
await new Promise((resolve, reject) => {
|
|
9526
10228
|
httpServer.listen(port, bindHost, () => {
|
|
9527
|
-
|
|
10229
|
+
log43.info(
|
|
9528
10230
|
{ port, bindHost },
|
|
9529
10231
|
"HTTP transport listening (POST /mcp for Streamable HTTP, GET /sse for legacy SSE)"
|
|
9530
10232
|
);
|
|
@@ -9548,13 +10250,13 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9548
10250
|
sessionIdGenerator: () => randomUUID(),
|
|
9549
10251
|
onsessioninitialized: (id) => {
|
|
9550
10252
|
streamableSessions.set(id, transport2);
|
|
9551
|
-
|
|
10253
|
+
log43.info({ sessionId: id }, "Streamable HTTP session created");
|
|
9552
10254
|
}
|
|
9553
10255
|
});
|
|
9554
10256
|
transport2.onclose = () => {
|
|
9555
10257
|
if (transport2.sessionId) {
|
|
9556
10258
|
streamableSessions.delete(transport2.sessionId);
|
|
9557
|
-
|
|
10259
|
+
log43.info({ sessionId: transport2.sessionId }, "Streamable HTTP session closed");
|
|
9558
10260
|
}
|
|
9559
10261
|
};
|
|
9560
10262
|
await makeServer().connect(transport2);
|
|
@@ -9571,7 +10273,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9571
10273
|
sseTransport.onclose = () => {
|
|
9572
10274
|
sseTransports.delete(sseTransport.sessionId);
|
|
9573
10275
|
};
|
|
9574
|
-
|
|
10276
|
+
log43.info({ sessionId: sseTransport.sessionId }, "SSE client connected");
|
|
9575
10277
|
await sseTransport.start();
|
|
9576
10278
|
return;
|
|
9577
10279
|
}
|
|
@@ -9592,23 +10294,23 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
9592
10294
|
// src/config/identity-registry.ts
|
|
9593
10295
|
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
9594
10296
|
import { parse as parse2 } from "yaml";
|
|
9595
|
-
import { z as
|
|
10297
|
+
import { z as z40 } from "zod";
|
|
9596
10298
|
import bcrypt from "bcryptjs";
|
|
9597
|
-
var
|
|
9598
|
-
var IdentityConfigSchema =
|
|
9599
|
-
token:
|
|
9600
|
-
role:
|
|
9601
|
-
allowedRouters:
|
|
9602
|
-
allowedToolPatterns:
|
|
10299
|
+
var log44 = createLogger("identity-registry");
|
|
10300
|
+
var IdentityConfigSchema = z40.object({
|
|
10301
|
+
token: z40.string().min(1, "token hash is required"),
|
|
10302
|
+
role: z40.enum(["readonly", "operator", "admin", "superadmin"]),
|
|
10303
|
+
allowedRouters: z40.array(z40.string()).default([]),
|
|
10304
|
+
allowedToolPatterns: z40.array(z40.string()).default([])
|
|
9603
10305
|
}).strict();
|
|
9604
|
-
var ConfigFileSchema2 =
|
|
9605
|
-
identities:
|
|
10306
|
+
var ConfigFileSchema2 = z40.object({
|
|
10307
|
+
identities: z40.record(z40.string(), IdentityConfigSchema)
|
|
9606
10308
|
}).strict();
|
|
9607
10309
|
var IdentityRegistry = class {
|
|
9608
10310
|
identities = [];
|
|
9609
10311
|
constructor(configPath) {
|
|
9610
10312
|
if (!existsSync2(configPath)) {
|
|
9611
|
-
|
|
10313
|
+
log44.warn(
|
|
9612
10314
|
{ configPath },
|
|
9613
10315
|
"Identity config file not found; starting with empty registry (stdio callers do not require it)"
|
|
9614
10316
|
);
|
|
@@ -9631,7 +10333,7 @@ ${issues}`);
|
|
|
9631
10333
|
allowedToolPatterns: config.allowedToolPatterns
|
|
9632
10334
|
});
|
|
9633
10335
|
}
|
|
9634
|
-
|
|
10336
|
+
log44.info({ count: this.identities.length }, "Loaded identities from config");
|
|
9635
10337
|
}
|
|
9636
10338
|
getIdentities() {
|
|
9637
10339
|
return this.identities.map(({ id, role, allowedRouters, allowedToolPatterns }) => ({
|
|
@@ -9660,7 +10362,7 @@ ${issues}`);
|
|
|
9660
10362
|
// src/domain/snapshot/retention.ts
|
|
9661
10363
|
import { readdir, stat, unlink } from "fs/promises";
|
|
9662
10364
|
import { join as join3 } from "path";
|
|
9663
|
-
var
|
|
10365
|
+
var log45 = createLogger("snapshot-retention");
|
|
9664
10366
|
async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
9665
10367
|
const cutoffMs = Date.now() - maxAgeDays * 864e5;
|
|
9666
10368
|
let removed = 0;
|
|
@@ -9687,21 +10389,21 @@ async function pruneSnapshots(snapshotDir, maxAgeDays) {
|
|
|
9687
10389
|
removed++;
|
|
9688
10390
|
}
|
|
9689
10391
|
} catch (err) {
|
|
9690
|
-
|
|
10392
|
+
log45.warn({ err, filePath }, "Failed to evaluate snapshot for retention");
|
|
9691
10393
|
}
|
|
9692
10394
|
}
|
|
9693
10395
|
}
|
|
9694
10396
|
if (removed > 0) {
|
|
9695
|
-
|
|
10397
|
+
log45.info({ removed, maxAgeDays }, "Pruned expired snapshots");
|
|
9696
10398
|
}
|
|
9697
10399
|
return removed;
|
|
9698
10400
|
}
|
|
9699
10401
|
|
|
9700
10402
|
// src/cli/serve.ts
|
|
9701
|
-
var
|
|
10403
|
+
var log46 = createLogger("main");
|
|
9702
10404
|
async function runServe() {
|
|
9703
10405
|
const config = loadAppConfig();
|
|
9704
|
-
|
|
10406
|
+
log46.info({ transport: config.transport, logLevel: config.logLevel }, "Starting MikroMCP server");
|
|
9705
10407
|
void pruneSnapshots(config.snapshotDir, config.retention.snapshotMaxAgeDays);
|
|
9706
10408
|
const identityRegistry = new IdentityRegistry(config.identitiesPath);
|
|
9707
10409
|
if (config.transport === "http") {
|
|
@@ -9709,7 +10411,7 @@ async function runServe() {
|
|
|
9709
10411
|
(i) => i.role === "readonly" || i.role === "operator"
|
|
9710
10412
|
);
|
|
9711
10413
|
if (hasLimitedRoles && !config.confirmationSecret) {
|
|
9712
|
-
|
|
10414
|
+
log46.error(
|
|
9713
10415
|
"MIKROMCP_CONFIRMATION_SECRET is required in HTTP mode when identities with role readonly or operator are configured"
|
|
9714
10416
|
);
|
|
9715
10417
|
process.exit(1);
|
|
@@ -9719,7 +10421,7 @@ async function runServe() {
|
|
|
9719
10421
|
if (config.transport === "stdio") {
|
|
9720
10422
|
const stdioIdentity = getStdioIdentity(config.stdioIdentity, identityRegistry);
|
|
9721
10423
|
await withIdentity(stdioIdentity, () => connectStdio(makeServer()));
|
|
9722
|
-
|
|
10424
|
+
log46.info("MikroMCP server running via stdio");
|
|
9723
10425
|
} else if (config.transport === "http") {
|
|
9724
10426
|
await connectHttp(
|
|
9725
10427
|
makeServer,
|
|
@@ -9731,13 +10433,13 @@ async function runServe() {
|
|
|
9731
10433
|
},
|
|
9732
10434
|
identityRegistry
|
|
9733
10435
|
);
|
|
9734
|
-
|
|
10436
|
+
log46.info(
|
|
9735
10437
|
{ port: config.port, bindHost: config.bindHost },
|
|
9736
10438
|
"MikroMCP server running via HTTP/SSE"
|
|
9737
10439
|
);
|
|
9738
10440
|
}
|
|
9739
10441
|
const shutdown = () => {
|
|
9740
|
-
|
|
10442
|
+
log46.info("Shutting down MikroMCP server");
|
|
9741
10443
|
pool.closeAll();
|
|
9742
10444
|
process.exit(0);
|
|
9743
10445
|
};
|
|
@@ -9750,7 +10452,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
|
9750
10452
|
import { homedir as homedir2 } from "os";
|
|
9751
10453
|
import * as net from "net";
|
|
9752
10454
|
import chalk from "chalk";
|
|
9753
|
-
var localVersion = "1.
|
|
10455
|
+
var localVersion = "1.3.0";
|
|
9754
10456
|
var results = [];
|
|
9755
10457
|
function ok(msg) {
|
|
9756
10458
|
console.log(chalk.green("\u2705 ") + msg);
|
|
@@ -10402,7 +11104,7 @@ async function runInit() {
|
|
|
10402
11104
|
}
|
|
10403
11105
|
|
|
10404
11106
|
// src/cli/index.ts
|
|
10405
|
-
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.
|
|
11107
|
+
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.3.0");
|
|
10406
11108
|
program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
|
|
10407
11109
|
program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
|
|
10408
11110
|
program.command("init").description("Interactive setup wizard").action(runInit);
|