mikromcp 1.10.0 → 1.11.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 +1 -1
- package/config/routers.example.yaml +2 -0
- package/dist/main.js +94 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
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)
|
|
@@ -61,6 +61,8 @@ routers:
|
|
|
61
61
|
tags: ["datacenter", "core"]
|
|
62
62
|
rosVersion: "7.14"
|
|
63
63
|
# sshPort: 22 # SSH port (default 22)
|
|
64
|
+
# sshUsername: "automation" # Optional SSH/SFTP username; defaults to REST username
|
|
65
|
+
# sshPrivateKeyPath: "/home/user/.ssh/id_ed25519" # Absolute path; suppresses SSH/SFTP password auth
|
|
64
66
|
# sshFingerprint: "aabbcc..." # Pin SSH host key SHA256 fingerprint (hex)
|
|
65
67
|
# cmdAllow: # Strict allow-list for run_command (see above)
|
|
66
68
|
# - "/ip/route/print*"
|
package/dist/main.js
CHANGED
|
@@ -1705,13 +1705,51 @@ var firewallTools = [listFirewallRulesTool, manageFirewallRuleTool];
|
|
|
1705
1705
|
|
|
1706
1706
|
// src/domain/tools/diagnostic-tools.ts
|
|
1707
1707
|
import { z as z9 } from "zod";
|
|
1708
|
+
|
|
1709
|
+
// src/adapter/routeros-command.ts
|
|
1710
|
+
var ROUTEROS_COMMAND = /^\/[a-z][a-z0-9-]*(?: [a-z][a-z0-9-]*)+$/;
|
|
1711
|
+
var ROUTEROS_PARAMETER = /^[a-z][a-z0-9-]*$/;
|
|
1712
|
+
var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
1713
|
+
function isRouterOsCommandString(value) {
|
|
1714
|
+
return !CONTROL_CHARACTER.test(value);
|
|
1715
|
+
}
|
|
1716
|
+
function quoteRouterOsString(value) {
|
|
1717
|
+
if (!isRouterOsCommandString(value)) {
|
|
1718
|
+
throw new Error("RouterOS command values must not contain control characters");
|
|
1719
|
+
}
|
|
1720
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$");
|
|
1721
|
+
return `"${escaped}"`;
|
|
1722
|
+
}
|
|
1723
|
+
function buildRouterOsCommand(command, parameters) {
|
|
1724
|
+
if (!ROUTEROS_COMMAND.test(command)) {
|
|
1725
|
+
throw new Error("Invalid RouterOS command");
|
|
1726
|
+
}
|
|
1727
|
+
const parts = [command];
|
|
1728
|
+
for (const [name, value] of Object.entries(parameters)) {
|
|
1729
|
+
if (!ROUTEROS_PARAMETER.test(name)) {
|
|
1730
|
+
throw new Error("Invalid RouterOS parameter");
|
|
1731
|
+
}
|
|
1732
|
+
if (typeof value === "number") {
|
|
1733
|
+
if (!Number.isSafeInteger(value)) {
|
|
1734
|
+
throw new Error("RouterOS numeric command values must be safe integers");
|
|
1735
|
+
}
|
|
1736
|
+
parts.push(`${name}=${value}`);
|
|
1737
|
+
} else {
|
|
1738
|
+
parts.push(`${name}=${quoteRouterOsString(value)}`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
return parts.join(" ");
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// src/domain/tools/diagnostic-tools.ts
|
|
1708
1745
|
var log7 = createLogger("diagnostic-tools");
|
|
1746
|
+
var routerOsCommandString = z9.string().refine(isRouterOsCommandString, "Must not contain control characters");
|
|
1709
1747
|
var pingInputSchema = z9.object({
|
|
1710
1748
|
routerId,
|
|
1711
|
-
address:
|
|
1749
|
+
address: routerOsCommandString.describe("Target IP address or hostname to ping"),
|
|
1712
1750
|
count: z9.number().int().min(1).max(20).default(4).describe("Number of ICMP echo requests (1\u201320)"),
|
|
1713
1751
|
size: z9.number().int().min(14).max(65535).default(56).describe("Packet size in bytes (14\u201365535)"),
|
|
1714
|
-
routingTable:
|
|
1752
|
+
routingTable: routerOsCommandString.optional().describe("Routing table to use for the ping")
|
|
1715
1753
|
}).strict();
|
|
1716
1754
|
var pingTool = {
|
|
1717
1755
|
name: "ping",
|
|
@@ -1728,13 +1766,15 @@ var pingTool = {
|
|
|
1728
1766
|
const parsed = pingInputSchema.parse(params);
|
|
1729
1767
|
log7.info({ routerId: context.routerId, address: parsed.address }, "Pinging");
|
|
1730
1768
|
try {
|
|
1731
|
-
const
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
if (parsed.routingTable !== void 0)
|
|
1737
|
-
const output = await context.sshClient.execute(
|
|
1769
|
+
const parameters = {
|
|
1770
|
+
address: parsed.address,
|
|
1771
|
+
count: parsed.count,
|
|
1772
|
+
size: parsed.size
|
|
1773
|
+
};
|
|
1774
|
+
if (parsed.routingTable !== void 0) parameters["routing-table"] = parsed.routingTable;
|
|
1775
|
+
const output = await context.sshClient.execute(
|
|
1776
|
+
buildRouterOsCommand("/tool ping", parameters)
|
|
1777
|
+
);
|
|
1738
1778
|
const s = output.match(/sent=(\d+)/)?.[1] ?? String(parsed.count);
|
|
1739
1779
|
const r = output.match(/received=(\d+)/)?.[1] ?? "0";
|
|
1740
1780
|
const loss = output.match(/packet-loss=(\S+)/)?.[1] ?? "100%";
|
|
@@ -1762,7 +1802,7 @@ var pingTool = {
|
|
|
1762
1802
|
};
|
|
1763
1803
|
var tracerouteInputSchema = z9.object({
|
|
1764
1804
|
routerId,
|
|
1765
|
-
address:
|
|
1805
|
+
address: routerOsCommandString.describe("Target IP address or hostname to trace"),
|
|
1766
1806
|
count: z9.number().int().min(1).max(5).default(3).describe("Probes per hop (1\u20135)"),
|
|
1767
1807
|
maxHops: z9.number().int().min(1).max(30).default(15).describe("Maximum number of hops (1\u201330)")
|
|
1768
1808
|
}).strict();
|
|
@@ -1781,12 +1821,13 @@ var tracerouteTool = {
|
|
|
1781
1821
|
const parsed = tracerouteInputSchema.parse(params);
|
|
1782
1822
|
log7.info({ routerId: context.routerId, address: parsed.address }, "Tracerouting");
|
|
1783
1823
|
try {
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1824
|
+
const output = await context.sshClient.execute(
|
|
1825
|
+
buildRouterOsCommand("/tool traceroute", {
|
|
1826
|
+
address: parsed.address,
|
|
1827
|
+
count: parsed.count,
|
|
1828
|
+
"max-hops": parsed.maxHops
|
|
1829
|
+
})
|
|
1830
|
+
);
|
|
1790
1831
|
const hopLines = output.split("\n").filter((l) => /^\s*\d+\s+\S+/.test(l));
|
|
1791
1832
|
const hops = hopLines.map((line) => {
|
|
1792
1833
|
const cols = line.trim().split(/\s+/);
|
|
@@ -1817,10 +1858,10 @@ var tracerouteTool = {
|
|
|
1817
1858
|
};
|
|
1818
1859
|
var torchInputSchema = z9.object({
|
|
1819
1860
|
routerId,
|
|
1820
|
-
interface:
|
|
1861
|
+
interface: routerOsCommandString.describe("Interface name to monitor (e.g. ether1, bridge1)"),
|
|
1821
1862
|
duration: z9.number().int().min(1).max(30).default(5).describe("Capture duration in seconds (1\u201330)"),
|
|
1822
|
-
srcAddress:
|
|
1823
|
-
dstAddress:
|
|
1863
|
+
srcAddress: routerOsCommandString.optional().describe("Filter by source IP address"),
|
|
1864
|
+
dstAddress: routerOsCommandString.optional().describe("Filter by destination IP address")
|
|
1824
1865
|
}).strict();
|
|
1825
1866
|
function parseTorchOutput(output) {
|
|
1826
1867
|
const flows = [];
|
|
@@ -1853,10 +1894,13 @@ var torchTool = {
|
|
|
1853
1894
|
const parsed = torchInputSchema.parse(params);
|
|
1854
1895
|
log7.info({ routerId: context.routerId, interface: parsed.interface }, "Running torch");
|
|
1855
1896
|
try {
|
|
1856
|
-
const
|
|
1857
|
-
if (parsed.srcAddress !== void 0)
|
|
1858
|
-
if (parsed.dstAddress !== void 0)
|
|
1859
|
-
const raw = await context.sshClient.execute(
|
|
1897
|
+
const parameters = { interface: parsed.interface };
|
|
1898
|
+
if (parsed.srcAddress !== void 0) parameters["src-address"] = parsed.srcAddress;
|
|
1899
|
+
if (parsed.dstAddress !== void 0) parameters["dst-address"] = parsed.dstAddress;
|
|
1900
|
+
const raw = await context.sshClient.execute(
|
|
1901
|
+
buildRouterOsCommand("/tool torch", parameters),
|
|
1902
|
+
(parsed.duration + 1) * 1e3
|
|
1903
|
+
);
|
|
1860
1904
|
const clean = raw.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1861
1905
|
const flows = parseTorchOutput(clean);
|
|
1862
1906
|
const lines = [
|
|
@@ -5134,7 +5178,7 @@ var uploadFileInputSchema = z22.object({
|
|
|
5134
5178
|
routerId,
|
|
5135
5179
|
name: z22.string().describe("Target filename on the router (e.g. flash/my-script.rsc)"),
|
|
5136
5180
|
content: z22.string().describe("File content to upload (text)"),
|
|
5137
|
-
dryRun: z22.boolean().default(false).describe("Validate FTP connectivity without writing the file")
|
|
5181
|
+
dryRun: z22.boolean().default(false).describe("Validate SFTP or FTP connectivity without writing the target file")
|
|
5138
5182
|
}).strict();
|
|
5139
5183
|
var uploadFileTool = {
|
|
5140
5184
|
name: "upload_file",
|
|
@@ -13289,6 +13333,7 @@ function getCredentials(router) {
|
|
|
13289
13333
|
|
|
13290
13334
|
// src/adapter/ssh-client.ts
|
|
13291
13335
|
import { createHash as createHash2 } from "crypto";
|
|
13336
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
13292
13337
|
import { Client } from "ssh2";
|
|
13293
13338
|
var SshClient = class {
|
|
13294
13339
|
constructor(config, credentials, options) {
|
|
@@ -13304,6 +13349,7 @@ var SshClient = class {
|
|
|
13304
13349
|
async execute(command, overrideTimeoutMs) {
|
|
13305
13350
|
const timeoutMs = overrideTimeoutMs ?? this.commandTimeoutMs;
|
|
13306
13351
|
const maxOutputBytes = this.maxOutputBytes;
|
|
13352
|
+
const privateKey = this.config.sshPrivateKeyPath ? readFileSync3(this.config.sshPrivateKeyPath) : void 0;
|
|
13307
13353
|
return new Promise((resolve, reject) => {
|
|
13308
13354
|
const conn = new Client();
|
|
13309
13355
|
const chunks = [];
|
|
@@ -13369,10 +13415,11 @@ var SshClient = class {
|
|
|
13369
13415
|
const connectOptions = {
|
|
13370
13416
|
host: this.config.host,
|
|
13371
13417
|
port: this.config.sshPort ?? 22,
|
|
13372
|
-
username: this.credentials.username,
|
|
13373
|
-
password: this.credentials.password,
|
|
13418
|
+
username: this.config.sshUsername ?? this.credentials.username,
|
|
13374
13419
|
readyTimeout: 1e4
|
|
13375
13420
|
};
|
|
13421
|
+
if (privateKey) connectOptions.privateKey = privateKey;
|
|
13422
|
+
else connectOptions.password = this.credentials.password;
|
|
13376
13423
|
if (this.config.sshFingerprint) {
|
|
13377
13424
|
const expected = this.config.sshFingerprint.toLowerCase();
|
|
13378
13425
|
connectOptions.hostVerifier = (key2) => {
|
|
@@ -13434,6 +13481,7 @@ var FtpClient = class {
|
|
|
13434
13481
|
|
|
13435
13482
|
// src/adapter/sftp-client.ts
|
|
13436
13483
|
import { createHash as createHash3 } from "crypto";
|
|
13484
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
13437
13485
|
import { Client as Client3 } from "ssh2";
|
|
13438
13486
|
var SftpClient = class {
|
|
13439
13487
|
constructor(config, credentials) {
|
|
@@ -13443,6 +13491,7 @@ var SftpClient = class {
|
|
|
13443
13491
|
config;
|
|
13444
13492
|
credentials;
|
|
13445
13493
|
async upload(remoteName, content) {
|
|
13494
|
+
const privateKey = this.config.sshPrivateKeyPath ? readFileSync4(this.config.sshPrivateKeyPath) : void 0;
|
|
13446
13495
|
return new Promise((resolve, reject) => {
|
|
13447
13496
|
const conn = new Client3();
|
|
13448
13497
|
let settled = false;
|
|
@@ -13468,10 +13517,11 @@ var SftpClient = class {
|
|
|
13468
13517
|
const connectOptions = {
|
|
13469
13518
|
host: this.config.host,
|
|
13470
13519
|
port: this.config.sshPort ?? 22,
|
|
13471
|
-
username: this.credentials.username,
|
|
13472
|
-
password: this.credentials.password,
|
|
13520
|
+
username: this.config.sshUsername ?? this.credentials.username,
|
|
13473
13521
|
readyTimeout: 1e4
|
|
13474
13522
|
};
|
|
13523
|
+
if (privateKey) connectOptions.privateKey = privateKey;
|
|
13524
|
+
else connectOptions.password = this.credentials.password;
|
|
13475
13525
|
if (this.config.sshFingerprint) {
|
|
13476
13526
|
const expected = this.config.sshFingerprint.toLowerCase();
|
|
13477
13527
|
connectOptions.hostVerifier = (key2) => {
|
|
@@ -14754,7 +14804,8 @@ function registerAllTools(server, registry, pool, circuitBreakers, config, ident
|
|
|
14754
14804
|
}
|
|
14755
14805
|
|
|
14756
14806
|
// src/config/router-registry.ts
|
|
14757
|
-
import { readFileSync as
|
|
14807
|
+
import { readFileSync as readFileSync5, existsSync } from "fs";
|
|
14808
|
+
import { isAbsolute } from "path";
|
|
14758
14809
|
import { parse } from "yaml";
|
|
14759
14810
|
import { z as z49 } from "zod";
|
|
14760
14811
|
var log51 = createLogger("router-registry");
|
|
@@ -14781,6 +14832,8 @@ var RouterConfigSchema = z49.object({
|
|
|
14781
14832
|
tags: z49.array(z49.string()).default([]),
|
|
14782
14833
|
rosVersion: z49.string().min(1).optional(),
|
|
14783
14834
|
sshPort: z49.number().int().min(1).max(65535).optional(),
|
|
14835
|
+
sshUsername: z49.string().min(1).optional(),
|
|
14836
|
+
sshPrivateKeyPath: z49.string().min(1).refine(isAbsolute, "sshPrivateKeyPath must be absolute").optional(),
|
|
14784
14837
|
sshFingerprint: z49.string().optional(),
|
|
14785
14838
|
cmdAllow: z49.array(z49.string()).optional(),
|
|
14786
14839
|
cmdDeny: z49.array(z49.string()).optional(),
|
|
@@ -14832,7 +14885,7 @@ var RouterRegistry = class {
|
|
|
14832
14885
|
log51.warn({ configPath }, "Router config file not found; starting with empty registry");
|
|
14833
14886
|
return;
|
|
14834
14887
|
}
|
|
14835
|
-
const raw =
|
|
14888
|
+
const raw = readFileSync5(configPath, "utf-8");
|
|
14836
14889
|
const parsed = parse(raw);
|
|
14837
14890
|
const result = ConfigFileSchema.safeParse(parsed);
|
|
14838
14891
|
if (!result.success) {
|
|
@@ -14960,7 +15013,7 @@ var ConnectionPool = class {
|
|
|
14960
15013
|
};
|
|
14961
15014
|
|
|
14962
15015
|
// src/version.ts
|
|
14963
|
-
var VERSION = "1.
|
|
15016
|
+
var VERSION = "1.11.0";
|
|
14964
15017
|
|
|
14965
15018
|
// src/mcp/server.ts
|
|
14966
15019
|
var SERVER_INSTRUCTIONS = [
|
|
@@ -15267,7 +15320,7 @@ async function handleMcpRequest(pathname, req, res, url, port, maxBodyBytes, mak
|
|
|
15267
15320
|
}
|
|
15268
15321
|
|
|
15269
15322
|
// src/config/identity-registry.ts
|
|
15270
|
-
import { readFileSync as
|
|
15323
|
+
import { readFileSync as readFileSync6, existsSync as existsSync2 } from "fs";
|
|
15271
15324
|
import { createHash as createHash7 } from "crypto";
|
|
15272
15325
|
import { parse as parse2 } from "yaml";
|
|
15273
15326
|
import { z as z50 } from "zod";
|
|
@@ -15295,7 +15348,7 @@ var IdentityRegistry = class {
|
|
|
15295
15348
|
);
|
|
15296
15349
|
return;
|
|
15297
15350
|
}
|
|
15298
|
-
const raw =
|
|
15351
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
15299
15352
|
const parsed = parse2(raw);
|
|
15300
15353
|
const result = ConfigFileSchema2.safeParse(parsed);
|
|
15301
15354
|
if (!result.success) {
|
|
@@ -15434,11 +15487,11 @@ async function runServe() {
|
|
|
15434
15487
|
}
|
|
15435
15488
|
|
|
15436
15489
|
// src/cli/doctor.ts
|
|
15437
|
-
import { existsSync as existsSync3, readFileSync as
|
|
15490
|
+
import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
|
|
15438
15491
|
import { homedir as homedir2 } from "os";
|
|
15439
15492
|
import * as net from "net";
|
|
15440
15493
|
import chalk from "chalk";
|
|
15441
|
-
var localVersion = "1.
|
|
15494
|
+
var localVersion = "1.11.0";
|
|
15442
15495
|
var results = [];
|
|
15443
15496
|
function ok(msg) {
|
|
15444
15497
|
console.log(chalk.green("\u2705 ") + msg);
|
|
@@ -15651,7 +15704,7 @@ function checkClaudeDesktop() {
|
|
|
15651
15704
|
return;
|
|
15652
15705
|
}
|
|
15653
15706
|
try {
|
|
15654
|
-
const raw =
|
|
15707
|
+
const raw = readFileSync7(configPath, "utf-8");
|
|
15655
15708
|
const config = JSON.parse(raw);
|
|
15656
15709
|
const servers = config.mcpServers ?? {};
|
|
15657
15710
|
if ("mikromcp" in servers || "mikrotik-mcp-server" in servers) {
|
|
@@ -15750,7 +15803,7 @@ async function runDoctor() {
|
|
|
15750
15803
|
// src/cli/init.ts
|
|
15751
15804
|
import { input, confirm, select } from "@inquirer/prompts";
|
|
15752
15805
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15753
|
-
import { existsSync as existsSync4, readFileSync as
|
|
15806
|
+
import { existsSync as existsSync4, readFileSync as readFileSync8, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
15754
15807
|
import { homedir as homedir3 } from "os";
|
|
15755
15808
|
import { join as join4 } from "path";
|
|
15756
15809
|
import { parse as yamlParse, Document, visit } from "yaml";
|
|
@@ -15974,7 +16027,7 @@ function writeRoutersYaml(data) {
|
|
|
15974
16027
|
};
|
|
15975
16028
|
let parsed = { routers: {} };
|
|
15976
16029
|
if (existsSync4(filePath)) {
|
|
15977
|
-
const raw =
|
|
16030
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
15978
16031
|
parsed = yamlParse(raw) ?? { routers: {} };
|
|
15979
16032
|
if (!parsed.routers) parsed.routers = {};
|
|
15980
16033
|
}
|
|
@@ -15993,7 +16046,7 @@ function writeIdentitiesYaml(data) {
|
|
|
15993
16046
|
};
|
|
15994
16047
|
let parsed = { identities: {} };
|
|
15995
16048
|
if (existsSync4(filePath)) {
|
|
15996
|
-
const raw =
|
|
16049
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
15997
16050
|
parsed = yamlParse(raw) ?? { identities: {} };
|
|
15998
16051
|
if (!parsed.identities) parsed.identities = {};
|
|
15999
16052
|
}
|
|
@@ -16059,7 +16112,7 @@ function registerClaudeDesktop() {
|
|
|
16059
16112
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16060
16113
|
const backupPath = `${configPath}.backup-${timestamp}`;
|
|
16061
16114
|
copyFileSync(configPath, backupPath);
|
|
16062
|
-
const raw =
|
|
16115
|
+
const raw = readFileSync8(configPath, "utf-8");
|
|
16063
16116
|
const config = JSON.parse(raw);
|
|
16064
16117
|
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
16065
16118
|
config.mcpServers = {};
|
|
@@ -16168,7 +16221,7 @@ async function runInit() {
|
|
|
16168
16221
|
}
|
|
16169
16222
|
|
|
16170
16223
|
// src/cli/index.ts
|
|
16171
|
-
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.
|
|
16224
|
+
var program = new Command().name("mikromcp").description("MikroTik RouterOS MCP server").version("1.11.0");
|
|
16172
16225
|
program.command("serve", { isDefault: true }).description("Start the MCP server (default)").action(runServe);
|
|
16173
16226
|
program.command("doctor").description("Check config, connectivity, and permissions").action(runDoctor);
|
|
16174
16227
|
program.command("init").description("Interactive setup wizard").action(runInit);
|