@usex/mikrotik-mcp 3.26.0 → 3.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +170 -24
- package/dist/index.d.ts +35 -18
- package/dist/index.js +130 -20
- package/dist/ui/observability.html +41 -41
- package/package.json +1 -1
- package/schemas/README.md +1 -1
- package/schemas/config.schema.json +17 -2
- package/schemas/tool-catalog.json +51 -11
- package/schemas/tools/disable_sstp_client.json +12 -0
- package/schemas/tools/enable_sstp_client.json +12 -0
- package/schemas/tools/sign_certificate.json +2 -6
package/dist/index.js
CHANGED
|
@@ -18,7 +18,8 @@ var McpServerSettingsSchema = z.object({
|
|
|
18
18
|
port: z.coerce.number().int().positive().default(8000),
|
|
19
19
|
allowedHosts: z.string().default(""),
|
|
20
20
|
allowedOrigins: z.string().default(""),
|
|
21
|
-
corsOrigins: z.string().default("")
|
|
21
|
+
corsOrigins: z.string().default(""),
|
|
22
|
+
toolPageSize: z.coerce.number().int().min(0).default(0)
|
|
22
23
|
});
|
|
23
24
|
var DeviceConfigSchema = z.object({
|
|
24
25
|
host: z.string().default("127.0.0.1"),
|
|
@@ -178,7 +179,8 @@ function loadConfig(argv = process.argv.slice(2)) {
|
|
|
178
179
|
port: pick("mcp-port", "MIKROTIK_MCP__PORT"),
|
|
179
180
|
allowedHosts: pick("mcp-allowed-hosts", "MIKROTIK_MCP__ALLOWED_HOSTS"),
|
|
180
181
|
allowedOrigins: pick("mcp-allowed-origins", "MIKROTIK_MCP__ALLOWED_ORIGINS"),
|
|
181
|
-
corsOrigins: pick("mcp-cors-origins", "MIKROTIK_MCP__CORS_ORIGINS")
|
|
182
|
+
corsOrigins: pick("mcp-cors-origins", "MIKROTIK_MCP__CORS_ORIGINS"),
|
|
183
|
+
toolPageSize: pick("tool-page-size", "MIKROTIK_MCP__TOOL_PAGE_SIZE")
|
|
182
184
|
};
|
|
183
185
|
const isTruthy = (v) => /^(1|true|yes|on)$/i.test(v ?? "");
|
|
184
186
|
const readOnly = isTruthy(pick("read-only", "MIKROTIK_READ_ONLY"));
|
|
@@ -261,6 +263,24 @@ function resolveDeviceName(name) {
|
|
|
261
263
|
}
|
|
262
264
|
return active.defaultDevice in active.devices ? active.defaultDevice : Object.keys(active.devices)[0] ?? active.defaultDevice;
|
|
263
265
|
}
|
|
266
|
+
function deviceTarget(dc) {
|
|
267
|
+
if (!dc)
|
|
268
|
+
return "?";
|
|
269
|
+
return dc.mac ? `MAC ${dc.mac}` : `${dc.host}:${dc.port ?? 22}`;
|
|
270
|
+
}
|
|
271
|
+
function deviceDirectory() {
|
|
272
|
+
return Object.entries(active.devices).map(([key, dc]) => ({
|
|
273
|
+
key,
|
|
274
|
+
label: dc.description?.trim() || undefined,
|
|
275
|
+
target: deviceTarget(dc),
|
|
276
|
+
isDefault: key === active.defaultDevice
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
function resolvedTarget(name) {
|
|
280
|
+
const key = resolveDeviceName(name);
|
|
281
|
+
const dc = active.devices[key];
|
|
282
|
+
return { key, label: dc?.description?.trim() || undefined, target: deviceTarget(dc) };
|
|
283
|
+
}
|
|
264
284
|
function getDevice(name) {
|
|
265
285
|
if (name && !(name in active.devices) && !deviceKeyForLabel(name)) {
|
|
266
286
|
throw new Error(`Unknown device '${name}'. Configured devices: ${Object.keys(active.devices).join(", ")}`);
|
|
@@ -1587,6 +1607,16 @@ function effectiveUi(def) {
|
|
|
1587
1607
|
}
|
|
1588
1608
|
return { ui: undefined, auto: false };
|
|
1589
1609
|
}
|
|
1610
|
+
function deviceSelectorDescription(selectorNames, directory) {
|
|
1611
|
+
if (directory && directory.length > 0) {
|
|
1612
|
+
const rows = directory.map((d) => `\u2022 ${d.key}${d.label && d.label !== d.key ? ` ("${d.label}")` : ""} \u2192 ${d.target}${d.isDefault ? " [default]" : ""}`).join(`
|
|
1613
|
+
`);
|
|
1614
|
+
return "Which configured MikroTik device to run this on. Pass the EXACT config key (or its label) " + "that matches the user's wording \u2014 these are different physical routers, so never substitute " + `one for another (e.g. "Ali Home" is NOT "home"). Configured devices:
|
|
1615
|
+
` + `${rows}
|
|
1616
|
+
` + "Omit only when the user did not name a device (uses the default).";
|
|
1617
|
+
}
|
|
1618
|
+
return `Which configured MikroTik device to run this on. One of: ${selectorNames.join(", ")} ` + "(a config key or its label). Omit to use the default device.";
|
|
1619
|
+
}
|
|
1590
1620
|
var READ = {
|
|
1591
1621
|
readOnlyHint: true,
|
|
1592
1622
|
idempotentHint: true,
|
|
@@ -1619,19 +1649,25 @@ function defineTool(def) {
|
|
|
1619
1649
|
inputSchema: def.inputSchema,
|
|
1620
1650
|
ui: def.ui,
|
|
1621
1651
|
register(server, opts = {}) {
|
|
1622
|
-
const { sendLog, deviceNames, deviceAliases } = opts;
|
|
1652
|
+
const { sendLog, deviceNames, deviceAliases, deviceDirectory: deviceDirectory2 } = opts;
|
|
1623
1653
|
const multiDevice = !!deviceNames && deviceNames.length > 1;
|
|
1624
1654
|
const { ui, auto } = effectiveUi(def);
|
|
1625
1655
|
const selectorNames = multiDevice && deviceNames ? [...new Set([...deviceNames, ...deviceAliases ?? []])] : [];
|
|
1626
1656
|
const inputSchema = multiDevice ? {
|
|
1627
1657
|
...def.inputSchema,
|
|
1628
|
-
device: z2.enum(selectorNames).optional().describe(
|
|
1658
|
+
device: z2.enum(selectorNames).optional().describe(deviceSelectorDescription(selectorNames, deviceDirectory2))
|
|
1629
1659
|
} : def.inputSchema;
|
|
1630
1660
|
const risk = riskOf(def.annotations);
|
|
1631
1661
|
const callback = async (args) => {
|
|
1632
1662
|
const { device, ...rest } = args;
|
|
1633
1663
|
const deviceName = typeof device === "string" ? device : undefined;
|
|
1634
1664
|
const ctx = createContext(sendLog, deviceName);
|
|
1665
|
+
const deviceStamp = multiDevice && risk !== "READ" ? (() => {
|
|
1666
|
+
const t = resolvedTarget(deviceName);
|
|
1667
|
+
const label = t.label && t.label !== t.key ? ` "${t.label}"` : "";
|
|
1668
|
+
const how = deviceName === undefined ? " \u2014 DEFAULT (no device specified)" : "";
|
|
1669
|
+
return `\u21B3 executed on device: ${t.key}${label}${how} \u2192 ${t.target}`;
|
|
1670
|
+
})() : null;
|
|
1635
1671
|
const startedAt = Date.now();
|
|
1636
1672
|
let outText = "";
|
|
1637
1673
|
let isErr = false;
|
|
@@ -1660,6 +1696,8 @@ function defineTool(def) {
|
|
|
1660
1696
|
result.structuredContent = out.structuredContent;
|
|
1661
1697
|
hasStructured = true;
|
|
1662
1698
|
}
|
|
1699
|
+
if (deviceStamp)
|
|
1700
|
+
result.content.push({ type: "text", text: deviceStamp });
|
|
1663
1701
|
return result;
|
|
1664
1702
|
} catch (e) {
|
|
1665
1703
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1668,7 +1706,10 @@ function defineTool(def) {
|
|
|
1668
1706
|
errMsg = msg;
|
|
1669
1707
|
outText = `Error: ${msg}`;
|
|
1670
1708
|
return {
|
|
1671
|
-
content: [
|
|
1709
|
+
content: [
|
|
1710
|
+
{ type: "text", text: `Error: ${msg}` },
|
|
1711
|
+
...deviceStamp ? [{ type: "text", text: deviceStamp }] : []
|
|
1712
|
+
],
|
|
1672
1713
|
isError: true
|
|
1673
1714
|
};
|
|
1674
1715
|
} finally {
|
|
@@ -1719,6 +1760,7 @@ function registerTools(server, modules, opts = {}) {
|
|
|
1719
1760
|
}
|
|
1720
1761
|
// src/server.ts
|
|
1721
1762
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1763
|
+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1722
1764
|
|
|
1723
1765
|
// src/core/ui-resources.ts
|
|
1724
1766
|
import { readFileSync as readFileSync3 } from "fs";
|
|
@@ -3827,19 +3869,28 @@ ${details}` : "Certificate creation completed but unable to verify.";
|
|
|
3827
3869
|
name: "sign_certificate",
|
|
3828
3870
|
title: "Sign Certificate",
|
|
3829
3871
|
annotations: WRITE,
|
|
3830
|
-
description: "Signs an existing certificate template (`/certificate sign`) to produce a valid certificate. " + "Omit `ca` to produce a self-signed certificate; supply the name of an existing CA certificate in `ca` " + "to issue it under that CA. " + "
|
|
3872
|
+
description: "Signs an existing certificate template (`/certificate sign`) to produce a valid certificate. " + "Omit `ca` to produce a self-signed certificate; supply the name of an existing CA certificate in `ca` " + "to issue it under that CA. " + "IMPORTANT: certificate stores are PER-DEVICE \u2014 the template (and the `ca`, if given) must live on the " + "SAME router this runs on. A 'CA not found' error almost always means the template or CA was created on a " + "different device; verify with list_certificates on this device first (and check the targeting). " + "Set the common name (CN) when you create the template (create_certificate), not here \u2014 `/certificate sign` " + "has no CN option. " + "May run for several seconds while the device generates key material. " + "Returns the signing operation output from the device.",
|
|
3831
3873
|
inputSchema: {
|
|
3832
|
-
name: z10.string().describe("Name of the certificate to sign"),
|
|
3833
|
-
ca: z10.string().optional().describe("Name of the CA certificate to sign with (omit to self-sign)")
|
|
3834
|
-
common_name: z10.string().optional().describe("Override the common name when signing")
|
|
3874
|
+
name: z10.string().describe("Name of the certificate template to sign"),
|
|
3875
|
+
ca: z10.string().optional().describe("Name of the CA certificate to sign with (must exist on THIS device; omit to self-sign)")
|
|
3835
3876
|
},
|
|
3836
3877
|
async handler(a, ctx) {
|
|
3837
|
-
ctx.info(`Signing certificate: name=${a.name}`);
|
|
3838
|
-
const
|
|
3878
|
+
ctx.info(`Signing certificate: name=${a.name}${a.ca ? ` ca=${a.ca}` : " (self-signed)"}`);
|
|
3879
|
+
const templateCount = await executeMikrotikCommand(`/certificate print count-only where name="${a.name}"`, ctx);
|
|
3880
|
+
if (templateCount.trim() === "0") {
|
|
3881
|
+
return `Certificate template '${a.name}' was not found on this device. Certificate stores are per-device \u2014 ` + `create the template here first (create_certificate), and make sure you are targeting the router that holds it ` + `(use list_certificates / list_mikrotik_devices to confirm).`;
|
|
3882
|
+
}
|
|
3883
|
+
if (a.ca) {
|
|
3884
|
+
const caCount = await executeMikrotikCommand(`/certificate print count-only where name="${a.ca}"`, ctx);
|
|
3885
|
+
if (caCount.trim() === "0") {
|
|
3886
|
+
return `CA certificate '${a.ca}' was not found on this device. The signing CA must exist on the SAME router as ` + `the template (certificate stores are per-device). Omit 'ca' to self-sign, or create/import the CA on this device first.`;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
const cmd = new Cmd("/certificate sign").raw(a.name).opt("ca", a.ca).build();
|
|
3839
3890
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
3840
3891
|
if (looksLikeError(result))
|
|
3841
3892
|
return `Failed to sign certificate: ${result}`;
|
|
3842
|
-
return `Signing certificate '${a.name}'
|
|
3893
|
+
return `Signing certificate '${a.name}'${a.ca ? ` with CA '${a.ca}'` : " (self-signed)"}\u2026
|
|
3843
3894
|
|
|
3844
3895
|
${result}`;
|
|
3845
3896
|
}
|
|
@@ -4710,7 +4761,7 @@ var deviceTools = [
|
|
|
4710
4761
|
name: "list_mikrotik_devices",
|
|
4711
4762
|
title: "List Configured MikroTik Devices",
|
|
4712
4763
|
annotations: READ,
|
|
4713
|
-
description: "List all MikroTik devices registered in this server's configuration (server-side metadata \u2014 no RouterOS command is run, no SSH connection is opened). " + "Use this to discover the exact device name strings required by the `device` argument on every other tool, especially when working across multiple routers (e.g. building a tunnel between two devices). " + "Returns each device's name, username, host, port, auth method (key / password / none), optional description, and which entry is the default. " + "Credentials and private-key material are never included in the output.",
|
|
4764
|
+
description: "List all MikroTik devices registered in this server's configuration (server-side metadata \u2014 no RouterOS command is run, no SSH connection is opened). " + "Use this to discover the exact device name strings required by the `device` argument on every other tool, especially when working across multiple routers (e.g. building a tunnel between two devices). " + "Returns each device's name, username, host, port, auth method (key / password / none), optional description, and which entry is the default. " + "This name \u2192 host:port mapping is FIXED for the life of the server process \u2014 it does NOT change mid-session, so a given device name always reaches the same physical router. " + "Call this to verify the exact target before any write/destructive change when several routers are configured. " + "Credentials and private-key material are never included in the output.",
|
|
4714
4765
|
handler(_a, ctx) {
|
|
4715
4766
|
ctx.info("Listing configured MikroTik devices");
|
|
4716
4767
|
const { names, default: def } = listDevices();
|
|
@@ -12060,7 +12111,7 @@ ${details}`;
|
|
|
12060
12111
|
},
|
|
12061
12112
|
async handler(a, ctx) {
|
|
12062
12113
|
ctx.info(`Creating L2TP client: name=${a.name}, connect_to=${a.connect_to}`);
|
|
12063
|
-
const cmd = new Cmd("/interface l2tp-client add").set("name", a.name).set("connect-to", a.connect_to).set("user", a.user).set("password", a.password).opt("profile", a.profile).bool("add-default-route", a.add_default_route).opt("default-route-distance", a.default_route_distance).opt("use-ipsec", a.use_ipsec).opt("ipsec-secret", a.ipsec_secret).opt("allow", a.allow).bool("use-peer-dns", a.use_peer_dns).bool("dial-on-demand", a.dial_on_demand).opt("keepalive-timeout", a.keepalive_timeout).opt("max-mtu", a.max_mtu).opt("max-mru", a.max_mru).opt("mrru", a.mrru).opt("comment", a.comment).
|
|
12114
|
+
const cmd = new Cmd("/interface l2tp-client add").set("name", a.name).set("connect-to", a.connect_to).set("user", a.user).set("password", a.password).opt("profile", a.profile).bool("add-default-route", a.add_default_route).opt("default-route-distance", a.default_route_distance).opt("use-ipsec", a.use_ipsec).opt("ipsec-secret", a.ipsec_secret).opt("allow", a.allow).bool("use-peer-dns", a.use_peer_dns).bool("dial-on-demand", a.dial_on_demand).opt("keepalive-timeout", a.keepalive_timeout).opt("max-mtu", a.max_mtu).opt("max-mru", a.max_mru).opt("mrru", a.mrru).opt("comment", a.comment).bool("disabled", a.disabled).build();
|
|
12064
12115
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
12065
12116
|
if (looksLikeError(result))
|
|
12066
12117
|
return `Failed to create L2TP client: ${redactSecrets(result)}`;
|
|
@@ -14523,7 +14574,7 @@ ${details}`;
|
|
|
14523
14574
|
},
|
|
14524
14575
|
async handler(a, ctx) {
|
|
14525
14576
|
ctx.info(`Adding OpenVPN server: name=${a.name}`);
|
|
14526
|
-
const cmd = new Cmd("/interface ovpn-server server add").set("name", a.name).opt("port", a.port).opt("protocol", a.protocol).opt("mode", a.mode).opt("netmask", a.netmask).opt("mac-address", a.mac_address).opt("certificate", a.certificate).opt("auth", a.auth).opt("cipher", a.cipher).opt("tls-version", a.tls_version).opt("max-mtu", a.max_mtu).opt("default-profile", a.default_profile).bool("require-client-certificate", a.require_client_certificate).opt("comment", a.comment).
|
|
14577
|
+
const cmd = new Cmd("/interface ovpn-server server add").set("name", a.name).opt("port", a.port).opt("protocol", a.protocol).opt("mode", a.mode).opt("netmask", a.netmask).opt("mac-address", a.mac_address).opt("certificate", a.certificate).opt("auth", a.auth).opt("cipher", a.cipher).opt("tls-version", a.tls_version).opt("max-mtu", a.max_mtu).opt("default-profile", a.default_profile).bool("require-client-certificate", a.require_client_certificate).opt("comment", a.comment).bool("disabled", a.disabled).build();
|
|
14527
14578
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
14528
14579
|
if (containsRawParserError(result)) {
|
|
14529
14580
|
return "Failed to add OpenVPN server: this RouterOS build does not support named OpenVPN" + " server instances (the multi-server model requires RouterOS 7.17+). Use set_ovpn_server" + ` to configure the single legacy server instead.
|
|
@@ -14676,7 +14727,7 @@ ${details}`;
|
|
|
14676
14727
|
},
|
|
14677
14728
|
async handler(a, ctx) {
|
|
14678
14729
|
ctx.info(`Creating OpenVPN client: name=${a.name}, connect_to=${a.connect_to}`);
|
|
14679
|
-
const cmd = new Cmd("/interface ovpn-client add").set("name", a.name).set("connect-to", a.connect_to).opt("port", a.port).opt("user", a.user).opt("password", a.password).opt("certificate", a.certificate).opt("cipher", a.cipher).opt("auth", a.auth).opt("tls-version", a.tls_version).opt("mode", a.mode).opt("protocol", a.protocol).opt("mac-address", a.mac_address).opt("max-mtu", a.max_mtu).opt("profile", a.profile).bool("add-default-route", a.add_default_route).bool("route-nopull", a.route_nopull).bool("use-peer-dns", a.use_peer_dns).bool("verify-server-certificate", a.verify_server_certificate).opt("comment", a.comment).
|
|
14730
|
+
const cmd = new Cmd("/interface ovpn-client add").set("name", a.name).set("connect-to", a.connect_to).opt("port", a.port).opt("user", a.user).opt("password", a.password).opt("certificate", a.certificate).opt("cipher", a.cipher).opt("auth", a.auth).opt("tls-version", a.tls_version).opt("mode", a.mode).opt("protocol", a.protocol).opt("mac-address", a.mac_address).opt("max-mtu", a.max_mtu).opt("profile", a.profile).bool("add-default-route", a.add_default_route).bool("route-nopull", a.route_nopull).bool("use-peer-dns", a.use_peer_dns).bool("verify-server-certificate", a.verify_server_certificate).opt("comment", a.comment).bool("disabled", a.disabled).build();
|
|
14680
14731
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
14681
14732
|
if (looksLikeError(result))
|
|
14682
14733
|
return `Failed to create OpenVPN client: ${redactSecrets(result)}`;
|
|
@@ -15215,7 +15266,7 @@ ${details}`;
|
|
|
15215
15266
|
},
|
|
15216
15267
|
async handler(a, ctx) {
|
|
15217
15268
|
ctx.info(`Creating PPTP client: name=${a.name}, connect_to=${a.connect_to}`);
|
|
15218
|
-
const cmd = new Cmd("/interface pptp-client add").set("name", a.name).set("connect-to", a.connect_to).set("user", a.user).set("password", a.password).opt("profile", a.profile).bool("add-default-route", a.add_default_route).opt("default-route-distance", a.default_route_distance).opt("allow", a.allow).bool("dial-on-demand", a.dial_on_demand).opt("max-mtu", a.max_mtu).opt("max-mru", a.max_mru).opt("mrru", a.mrru).opt("keepalive-timeout", a.keepalive_timeout).opt("comment", a.comment).
|
|
15269
|
+
const cmd = new Cmd("/interface pptp-client add").set("name", a.name).set("connect-to", a.connect_to).set("user", a.user).set("password", a.password).opt("profile", a.profile).bool("add-default-route", a.add_default_route).opt("default-route-distance", a.default_route_distance).opt("allow", a.allow).bool("dial-on-demand", a.dial_on_demand).opt("max-mtu", a.max_mtu).opt("max-mru", a.max_mru).opt("mrru", a.mrru).opt("keepalive-timeout", a.keepalive_timeout).opt("comment", a.comment).bool("disabled", a.disabled).build();
|
|
15219
15270
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
15220
15271
|
if (looksLikeError(result))
|
|
15221
15272
|
return `Failed to create PPTP client: ${redactSecrets(result)}`;
|
|
@@ -19785,7 +19836,7 @@ ${details}`;
|
|
|
19785
19836
|
async handler(a, ctx) {
|
|
19786
19837
|
ctx.info(`Creating SSTP client: name=${a.name}, connect_to=${a.connect_to}`);
|
|
19787
19838
|
const { host, port } = splitHostPort(a.connect_to);
|
|
19788
|
-
const cmd = new Cmd("/interface sstp-client add").set("name", a.name).set("connect-to", host).opt("port", a.port ?? port).set("user", a.user).set("password", a.password).opt("profile", a.profile).opt("certificate", a.certificate).bool("verify-server-certificate", a.verify_server_certificate).opt("authentication", a.authentication).opt("tls-version", a.tls_version).bool("pfs", a.pfs).bool("add-default-route", a.add_default_route).opt("default-route-distance", a.default_route_distance).bool("dial-on-demand", a.dial_on_demand).opt("max-mtu", a.max_mtu).opt("max-mru", a.max_mru).opt("mrru", a.mrru).opt("keepalive-timeout", a.keepalive_timeout).opt("http-proxy", a.http_proxy).opt("comment", a.comment).
|
|
19839
|
+
const cmd = new Cmd("/interface sstp-client add").set("name", a.name).set("connect-to", host).opt("port", a.port ?? port).set("user", a.user).set("password", a.password).opt("profile", a.profile).opt("certificate", a.certificate).bool("verify-server-certificate", a.verify_server_certificate).opt("authentication", a.authentication).opt("tls-version", a.tls_version).bool("pfs", a.pfs).bool("add-default-route", a.add_default_route).opt("default-route-distance", a.default_route_distance).bool("dial-on-demand", a.dial_on_demand).opt("max-mtu", a.max_mtu).opt("max-mru", a.max_mru).opt("mrru", a.mrru).opt("keepalive-timeout", a.keepalive_timeout).opt("http-proxy", a.http_proxy).opt("comment", a.comment).bool("disabled", a.disabled).build();
|
|
19789
19840
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
19790
19841
|
if (looksLikeError(result))
|
|
19791
19842
|
return `Failed to create SSTP client: ${redactSecrets(result)}`;
|
|
@@ -19832,7 +19883,7 @@ ${redactSecrets(result)}`;
|
|
|
19832
19883
|
name: "remove_sstp_client",
|
|
19833
19884
|
title: "Remove SSTP Client Interface",
|
|
19834
19885
|
annotations: DESTRUCTIVE,
|
|
19835
|
-
description: "Permanently delete an SSTP client tunnel interface (`/interface sstp-client remove [find name=...]`) by interface name. " + "First verifies the interface exists (count-only check), then removes it; the tunnel is torn down immediately and the action is irreversible. " + "Use `list_sstp_clients` to confirm the interface name before calling this tool. " + "For L2TP, OpenVPN, or PPTP client interfaces see their respective tool scopes (`create_l2tp_client`, `create_ovpn_client`, `create_pptp_client`). " + "To
|
|
19886
|
+
description: "Permanently delete an SSTP client tunnel interface (`/interface sstp-client remove [find name=...]`) by interface name. " + "First verifies the interface exists (count-only check), then removes it; the tunnel is torn down immediately and the action is irreversible. " + "Use `list_sstp_clients` to confirm the interface name before calling this tool. " + "For L2TP, OpenVPN, or PPTP client interfaces see their respective tool scopes (`create_l2tp_client`, `create_ovpn_client`, `create_pptp_client`). " + "To deactivate the interface without deleting it use `disable_sstp_client` (re-activate with `enable_sstp_client`). " + "Returns a confirmation message on success or a not-found message if the name does not exist.",
|
|
19836
19887
|
inputSchema: { name: z98.string() },
|
|
19837
19888
|
async handler(a, ctx) {
|
|
19838
19889
|
ctx.info(`Removing SSTP client: name=${a.name}`);
|
|
@@ -19844,6 +19895,40 @@ ${redactSecrets(result)}`;
|
|
|
19844
19895
|
return `Failed to remove SSTP client: ${result}`;
|
|
19845
19896
|
return `SSTP client '${a.name}' removed successfully.`;
|
|
19846
19897
|
}
|
|
19898
|
+
}),
|
|
19899
|
+
defineTool({
|
|
19900
|
+
name: "enable_sstp_client",
|
|
19901
|
+
title: "Enable SSTP Client Interface",
|
|
19902
|
+
annotations: WRITE_IDEMPOTENT,
|
|
19903
|
+
description: "Enables a disabled SSTP client interface (`/interface sstp-client enable [find name=...]`), " + "causing the router to dial out and attempt the TLS connection to the remote SSTP server. " + "Use to activate a tunnel that was created disabled or stopped with `disable_sstp_client`. " + "For L2TP/PPTP/OpenVPN tunnels use their own enable tools. " + "Identifies the interface by name \u2014 create one with `create_sstp_client`.",
|
|
19904
|
+
inputSchema: { name: z98.string() },
|
|
19905
|
+
async handler(a, ctx) {
|
|
19906
|
+
ctx.info(`Enabling SSTP client: name=${a.name}`);
|
|
19907
|
+
const count = await executeMikrotikCommand(`/interface sstp-client print count-only where name="${a.name}"`, ctx);
|
|
19908
|
+
if (count.trim() === "0")
|
|
19909
|
+
return `SSTP client '${a.name}' not found.`;
|
|
19910
|
+
const result = await executeMikrotikCommand(`/interface sstp-client enable [find name="${a.name}"]`, ctx);
|
|
19911
|
+
if (looksLikeError(result))
|
|
19912
|
+
return `Failed to enable SSTP client: ${result}`;
|
|
19913
|
+
return `SSTP client '${a.name}' enabled successfully.`;
|
|
19914
|
+
}
|
|
19915
|
+
}),
|
|
19916
|
+
defineTool({
|
|
19917
|
+
name: "disable_sstp_client",
|
|
19918
|
+
title: "Disable SSTP Client Interface",
|
|
19919
|
+
annotations: WRITE_IDEMPOTENT,
|
|
19920
|
+
description: "Disables an active SSTP client interface (`/interface sstp-client disable [find name=...]`), " + "tearing down the tunnel without removing its configuration. " + "Use to temporarily stop a tunnel while preserving its settings for later reuse. " + "To re-enable use `enable_sstp_client`; to permanently remove use `remove_sstp_client`.",
|
|
19921
|
+
inputSchema: { name: z98.string() },
|
|
19922
|
+
async handler(a, ctx) {
|
|
19923
|
+
ctx.info(`Disabling SSTP client: name=${a.name}`);
|
|
19924
|
+
const count = await executeMikrotikCommand(`/interface sstp-client print count-only where name="${a.name}"`, ctx);
|
|
19925
|
+
if (count.trim() === "0")
|
|
19926
|
+
return `SSTP client '${a.name}' not found.`;
|
|
19927
|
+
const result = await executeMikrotikCommand(`/interface sstp-client disable [find name="${a.name}"]`, ctx);
|
|
19928
|
+
if (looksLikeError(result))
|
|
19929
|
+
return `Failed to disable SSTP client: ${result}`;
|
|
19930
|
+
return `SSTP client '${a.name}' disabled successfully.`;
|
|
19931
|
+
}
|
|
19847
19932
|
})
|
|
19848
19933
|
];
|
|
19849
19934
|
|
|
@@ -24339,7 +24424,7 @@ function selectToolModules(filter = {}, catalog = moduleCatalog) {
|
|
|
24339
24424
|
// package.json
|
|
24340
24425
|
var package_default = {
|
|
24341
24426
|
name: "@usex/mikrotik-mcp",
|
|
24342
|
-
version: "3.
|
|
24427
|
+
version: "3.27.0",
|
|
24343
24428
|
description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
|
|
24344
24429
|
keywords: [
|
|
24345
24430
|
"ai",
|
|
@@ -24544,12 +24629,37 @@ function createServer(opts = {}) {
|
|
|
24544
24629
|
sendLog,
|
|
24545
24630
|
deviceNames: names,
|
|
24546
24631
|
deviceAliases: deviceLabels(),
|
|
24632
|
+
deviceDirectory: deviceDirectory(),
|
|
24547
24633
|
readOnly
|
|
24548
24634
|
});
|
|
24549
24635
|
const promptCount = registerPrompts(server);
|
|
24550
24636
|
const uiViewCount = registerUiResources(server);
|
|
24637
|
+
installToolPagination(server, getConfig().mcp.toolPageSize);
|
|
24551
24638
|
return { server, toolCount, promptCount, uiViewCount, readOnly };
|
|
24552
24639
|
}
|
|
24640
|
+
function installToolPagination(server, pageSize) {
|
|
24641
|
+
if (pageSize <= 0)
|
|
24642
|
+
return;
|
|
24643
|
+
const low = server.server;
|
|
24644
|
+
const sdkHandler = low._requestHandlers?.get("tools/list");
|
|
24645
|
+
if (typeof sdkHandler !== "function")
|
|
24646
|
+
return;
|
|
24647
|
+
let cache = null;
|
|
24648
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
|
|
24649
|
+
if (!cache)
|
|
24650
|
+
cache = (await sdkHandler(request, extra)).tools ?? [];
|
|
24651
|
+
const total = cache.length;
|
|
24652
|
+
const cursor = request.params?.cursor;
|
|
24653
|
+
let start = 0;
|
|
24654
|
+
if (typeof cursor === "string") {
|
|
24655
|
+
const n = Number.parseInt(cursor, 10);
|
|
24656
|
+
start = Number.isFinite(n) && n > 0 ? Math.min(n, total) : 0;
|
|
24657
|
+
}
|
|
24658
|
+
const tools = cache.slice(start, start + pageSize);
|
|
24659
|
+
const end = start + tools.length;
|
|
24660
|
+
return end < total ? { tools, nextCursor: String(end) } : { tools };
|
|
24661
|
+
});
|
|
24662
|
+
}
|
|
24553
24663
|
export {
|
|
24554
24664
|
setConfig,
|
|
24555
24665
|
resolveDeviceName,
|