@tikoci/rosetta 0.4.0 → 0.4.2
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 +13 -1
- package/package.json +1 -1
- package/src/db.ts +39 -8
- package/src/extract-test-results.ts +86 -13
- package/src/mcp-http.test.ts +168 -23
- package/src/mcp.ts +241 -1
- package/src/paths.ts +8 -0
- package/src/query.test.ts +426 -13
- package/src/query.ts +420 -23
- package/src/setup.ts +7 -2
package/src/mcp.ts
CHANGED
|
@@ -99,7 +99,7 @@ const { z } = await import("zod/v3");
|
|
|
99
99
|
//
|
|
100
100
|
// Check if DB has data BEFORE importing db.ts. If empty/missing,
|
|
101
101
|
// auto-download so db.ts opens the real database.
|
|
102
|
-
const { resolveDbPath } = await import("./paths.ts");
|
|
102
|
+
const { resolveDbPath, SCHEMA_VERSION } = await import("./paths.ts");
|
|
103
103
|
const _dbPath = resolveDbPath(import.meta.dirname);
|
|
104
104
|
|
|
105
105
|
const _pageCount = (() => {
|
|
@@ -126,18 +126,50 @@ if (_pageCount === 0) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Check schema version — a bunx auto-update may bring a new code version whose
|
|
130
|
+
// schema is incompatible with the existing ~/.rosetta/ros-help.db.
|
|
131
|
+
// MUST be checked before importing db.ts, because initDb() stamps user_version.
|
|
132
|
+
const _dbSchemaVersion = (() => {
|
|
133
|
+
try {
|
|
134
|
+
const check = new (require("bun:sqlite").default)(_dbPath, { readonly: true });
|
|
135
|
+
const row = check.prepare("PRAGMA user_version").get() as { user_version: number };
|
|
136
|
+
check.close();
|
|
137
|
+
return row.user_version;
|
|
138
|
+
} catch {
|
|
139
|
+
return SCHEMA_VERSION; // unreadable — assume ok, initDb() will stamp it
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
|
|
143
|
+
if (_dbSchemaVersion !== SCHEMA_VERSION) {
|
|
144
|
+
const { downloadDb } = await import("./setup.ts");
|
|
145
|
+
const log = (msg: string) => process.stderr.write(`${msg}\n`);
|
|
146
|
+
log(`DB schema version mismatch (DB=${_dbSchemaVersion}, expected=${SCHEMA_VERSION}) — re-downloading updated database...`);
|
|
147
|
+
try {
|
|
148
|
+
await downloadDb(_dbPath, log);
|
|
149
|
+
log("Database updated successfully.");
|
|
150
|
+
} catch (e) {
|
|
151
|
+
log(`Auto-download failed: ${e}`);
|
|
152
|
+
log(`Run: ${process.argv[0]} --setup --force`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
// Now import db.ts (opens the DB) and query.ts
|
|
130
157
|
const { getDbStats, initDb } = await import("./db.ts");
|
|
131
158
|
const {
|
|
132
159
|
browseCommands,
|
|
133
160
|
browseCommandsAtVersion,
|
|
134
161
|
checkCommandVersions,
|
|
162
|
+
diffCommandVersions,
|
|
163
|
+
exportDevicesCsv,
|
|
164
|
+
exportDeviceTestsCsv,
|
|
135
165
|
fetchCurrentVersions,
|
|
136
166
|
getPage,
|
|
137
167
|
lookupProperty,
|
|
138
168
|
searchCallouts,
|
|
139
169
|
searchChangelogs,
|
|
140
170
|
searchDevices,
|
|
171
|
+
searchDeviceTests,
|
|
172
|
+
getTestResultMeta,
|
|
141
173
|
searchPages,
|
|
142
174
|
searchProperties,
|
|
143
175
|
} = await import("./query.ts");
|
|
@@ -153,6 +185,40 @@ const server = new McpServer({
|
|
|
153
185
|
version: RESOLVED_VERSION,
|
|
154
186
|
});
|
|
155
187
|
|
|
188
|
+
server.registerResource(
|
|
189
|
+
"device-test-results-csv",
|
|
190
|
+
"rosetta://datasets/device-test-results.csv",
|
|
191
|
+
{
|
|
192
|
+
title: "Device Test Results CSV",
|
|
193
|
+
description: "Full joined benchmark dataset as CSV for reporting and bulk export. Attach explicitly in clients that support MCP resources.",
|
|
194
|
+
mimeType: "text/csv",
|
|
195
|
+
},
|
|
196
|
+
async () => ({
|
|
197
|
+
contents: [{
|
|
198
|
+
uri: "rosetta://datasets/device-test-results.csv",
|
|
199
|
+
mimeType: "text/csv",
|
|
200
|
+
text: exportDeviceTestsCsv(),
|
|
201
|
+
}],
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
server.registerResource(
|
|
206
|
+
"devices-csv",
|
|
207
|
+
"rosetta://datasets/devices.csv",
|
|
208
|
+
{
|
|
209
|
+
title: "Devices CSV",
|
|
210
|
+
description: "Full device catalog as CSV, including normalized RAM and storage fields plus product and block diagram URLs.",
|
|
211
|
+
mimeType: "text/csv",
|
|
212
|
+
},
|
|
213
|
+
async () => ({
|
|
214
|
+
contents: [{
|
|
215
|
+
uri: "rosetta://datasets/devices.csv",
|
|
216
|
+
mimeType: "text/csv",
|
|
217
|
+
text: exportDevicesCsv(),
|
|
218
|
+
}],
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
156
222
|
// ---- routeros_search ----
|
|
157
223
|
|
|
158
224
|
server.registerTool(
|
|
@@ -633,6 +699,67 @@ Examples:
|
|
|
633
699
|
},
|
|
634
700
|
);
|
|
635
701
|
|
|
702
|
+
// ---- routeros_command_diff ----
|
|
703
|
+
|
|
704
|
+
server.registerTool(
|
|
705
|
+
"routeros_command_diff",
|
|
706
|
+
{
|
|
707
|
+
description: `Diff two RouterOS versions — which command paths were added or removed between them.
|
|
708
|
+
|
|
709
|
+
The most common RouterOS support query is "something broke after I upgraded." This tool
|
|
710
|
+
directly answers it by comparing the command tree between any two tracked versions.
|
|
711
|
+
|
|
712
|
+
Returns added[] (new in to_version) and removed[] (gone from to_version) with counts.
|
|
713
|
+
Use path_prefix to scope the diff to a subsystem (e.g., '/ip/firewall' or '/routing/bgp').
|
|
714
|
+
|
|
715
|
+
Command data covers 7.9–7.23beta2. Both versions must be in this range for complete results;
|
|
716
|
+
if a version is outside the range, a note warns that results may be incomplete.
|
|
717
|
+
|
|
718
|
+
**Typical workflow for upgrade breakage:**
|
|
719
|
+
1. routeros_command_diff from_version="7.15" to_version="7.22" path_prefix="/ip/firewall"
|
|
720
|
+
→ see which filter/mangle/nat commands changed
|
|
721
|
+
2. routeros_search_changelogs from_version="7.15" to_version="7.22" category="firewall"
|
|
722
|
+
→ read human-readable changelog entries for that subsystem
|
|
723
|
+
3. routeros_command_version_check for a specific path that looks suspicious
|
|
724
|
+
→ confirm exact version range for that command
|
|
725
|
+
|
|
726
|
+
**path_prefix tip:** Start broad (e.g., '/routing/bgp'), then narrow if the diff is large.
|
|
727
|
+
Without a prefix, a major-version diff can list hundreds of added paths.
|
|
728
|
+
|
|
729
|
+
→ routeros_search_changelogs: read what changed (descriptions, breaking flags)
|
|
730
|
+
→ routeros_command_version_check: check a specific command's full version history
|
|
731
|
+
→ routeros_command_tree: browse the current command hierarchy at a path`,
|
|
732
|
+
inputSchema: {
|
|
733
|
+
from_version: z
|
|
734
|
+
.string()
|
|
735
|
+
.describe("The older RouterOS version to diff from (e.g., '7.15', '7.9')"),
|
|
736
|
+
to_version: z
|
|
737
|
+
.string()
|
|
738
|
+
.describe("The newer RouterOS version to diff to (e.g., '7.22', '7.23beta2')"),
|
|
739
|
+
path_prefix: z
|
|
740
|
+
.string()
|
|
741
|
+
.optional()
|
|
742
|
+
.describe("Optional: scope the diff to a command subtree (e.g., '/ip/firewall', '/routing/bgp', '/interface/bridge')"),
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
async ({ from_version, to_version, path_prefix }) => {
|
|
746
|
+
const result = diffCommandVersions(from_version, to_version, path_prefix);
|
|
747
|
+
if (result.added_count === 0 && result.removed_count === 0) {
|
|
748
|
+
const hint = [
|
|
749
|
+
result.note ?? null,
|
|
750
|
+
"No differences found. Possible reasons:",
|
|
751
|
+
"- Both versions have identical command trees for this path",
|
|
752
|
+
"- One or both versions may not be in our tracked range (7.9–7.23beta2)",
|
|
753
|
+
"Use routeros_stats to see available version range, or try a different path_prefix.",
|
|
754
|
+
].filter(Boolean).join("\n");
|
|
755
|
+
return { content: [{ type: "text", text: hint }] };
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
759
|
+
};
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
|
|
636
763
|
// ---- routeros_device_lookup ----
|
|
637
764
|
|
|
638
765
|
server.registerTool(
|
|
@@ -672,6 +799,7 @@ Shows bus topology and per-port bandwidth limits — useful for understanding So
|
|
|
672
799
|
- SMIPS: lowest-end (hAP lite)
|
|
673
800
|
|
|
674
801
|
Workflow — combine with other tools:
|
|
802
|
+
→ routeros_search_tests: cross-device performance ranking (all 125 devices at once, e.g., 512B routing benchmark)
|
|
675
803
|
→ routeros_search: find documentation for features relevant to a device
|
|
676
804
|
→ routeros_command_tree: check commands available for a feature
|
|
677
805
|
→ routeros_current_versions: check latest firmware for the device
|
|
@@ -757,6 +885,118 @@ Data: 144 products, March 2026 snapshot. Not all MikroTik products ever made —
|
|
|
757
885
|
},
|
|
758
886
|
);
|
|
759
887
|
|
|
888
|
+
// ---- routeros_search_tests ----
|
|
889
|
+
|
|
890
|
+
server.registerTool(
|
|
891
|
+
"routeros_search_tests",
|
|
892
|
+
{
|
|
893
|
+
description: `Query device performance test results across all devices.
|
|
894
|
+
|
|
895
|
+
Returns throughput benchmarks from mikrotik.com product pages — one call replaces
|
|
896
|
+
what would otherwise require 125+ individual device lookups.
|
|
897
|
+
|
|
898
|
+
**Data:** 2,874 test results across 125 devices (March 2026).
|
|
899
|
+
- Ethernet: bridging/routing throughput at 64/512/1518 byte packets
|
|
900
|
+
- IPSec: tunnel throughput with AES/SHA cipher configurations
|
|
901
|
+
- Results include kpps (packets/sec) and Mbps
|
|
902
|
+
|
|
903
|
+
**Common queries:**
|
|
904
|
+
- Routing performance ranking: test_type="ethernet", mode="Routing", configuration="25 ip filter rules", packet_size=512
|
|
905
|
+
- Bridge performance: test_type="ethernet", mode="Bridging", configuration="25 bridge filter"
|
|
906
|
+
- IPSec throughput: test_type="ipsec", mode="Single tunnel", configuration="AES-128-CBC"
|
|
907
|
+
|
|
908
|
+
**Configuration matching:** Uses LIKE (substring) — "25 ip filter" matches "25 ip filter rules".
|
|
909
|
+
Note: some devices use slightly different names (e.g., "25 bridge filter" vs "25 bridge filter rules").
|
|
910
|
+
|
|
911
|
+
**Tip:** Call with no filters first to see available test_types, modes, configurations, and packet_sizes via the metadata field.
|
|
912
|
+
|
|
913
|
+
Results include product_name, product_code, architecture — use routeros_device_lookup for full specs (CPU, RAM, ports, etc.).
|
|
914
|
+
For bulk export/reporting, attach the MCP resource rosetta://datasets/device-test-results.csv in clients that support MCP resources.
|
|
915
|
+
|
|
916
|
+
Workflow:
|
|
917
|
+
→ routeros_device_lookup: get full specs (CPU, RAM, pricing) + block diagram for a specific device
|
|
918
|
+
→ routeros_search: find documentation about features relevant to the test type`,
|
|
919
|
+
inputSchema: {
|
|
920
|
+
test_type: z
|
|
921
|
+
.string()
|
|
922
|
+
.optional()
|
|
923
|
+
.describe("Filter: 'ethernet' or 'ipsec'"),
|
|
924
|
+
mode: z
|
|
925
|
+
.string()
|
|
926
|
+
.optional()
|
|
927
|
+
.describe("Filter: e.g., 'Routing', 'Bridging', 'Single tunnel', '256 tunnels'"),
|
|
928
|
+
configuration: z
|
|
929
|
+
.string()
|
|
930
|
+
.optional()
|
|
931
|
+
.describe("Filter (substring match): e.g., '25 ip filter rules', 'AES-128-CBC + SHA1', 'none (fast path)'"),
|
|
932
|
+
packet_size: z
|
|
933
|
+
.number()
|
|
934
|
+
.int()
|
|
935
|
+
.optional()
|
|
936
|
+
.describe("Filter: packet size in bytes (64, 512, 1400, 1518)"),
|
|
937
|
+
sort_by: z
|
|
938
|
+
.enum(["mbps", "kpps"])
|
|
939
|
+
.optional()
|
|
940
|
+
.default("mbps")
|
|
941
|
+
.describe("Sort results by throughput metric (default: mbps)"),
|
|
942
|
+
limit: z
|
|
943
|
+
.number()
|
|
944
|
+
.int()
|
|
945
|
+
.min(1)
|
|
946
|
+
.max(200)
|
|
947
|
+
.optional()
|
|
948
|
+
.default(50)
|
|
949
|
+
.describe("Max results (default 50, max 200)"),
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
async ({ test_type, mode, configuration, packet_size, sort_by, limit }) => {
|
|
953
|
+
const hasFilters = test_type || mode || configuration || packet_size;
|
|
954
|
+
|
|
955
|
+
if (!hasFilters) {
|
|
956
|
+
// Discovery mode: return available filter values
|
|
957
|
+
const meta = getTestResultMeta();
|
|
958
|
+
return {
|
|
959
|
+
content: [{
|
|
960
|
+
type: "text",
|
|
961
|
+
text: JSON.stringify({
|
|
962
|
+
message: "No filters provided. Here are the available values — use these to build your query:",
|
|
963
|
+
...meta,
|
|
964
|
+
hint: "Common query: test_type='ethernet', mode='Routing', configuration='25 ip filter rules', packet_size=512",
|
|
965
|
+
}, null, 2),
|
|
966
|
+
}],
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const result = searchDeviceTests(
|
|
971
|
+
{ test_type, mode, configuration, packet_size, sort_by },
|
|
972
|
+
limit,
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
if (result.results.length === 0) {
|
|
976
|
+
const hints = [
|
|
977
|
+
"Call with no filters to see available test types, modes, and configurations",
|
|
978
|
+
configuration ? `Try a shorter configuration substring (e.g., "25 ip filter" instead of the full string)` : null,
|
|
979
|
+
].filter(Boolean);
|
|
980
|
+
return {
|
|
981
|
+
content: [{
|
|
982
|
+
type: "text",
|
|
983
|
+
text: `No test results matched the filters.\n\nTry:\n${hints.map((h) => `- ${h}`).join("\n")}`,
|
|
984
|
+
}],
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return {
|
|
989
|
+
content: [{
|
|
990
|
+
type: "text",
|
|
991
|
+
text: JSON.stringify({
|
|
992
|
+
...result,
|
|
993
|
+
has_more: result.total > result.results.length,
|
|
994
|
+
}, null, 2),
|
|
995
|
+
}],
|
|
996
|
+
};
|
|
997
|
+
},
|
|
998
|
+
);
|
|
999
|
+
|
|
760
1000
|
// ---- routeros_current_versions ----
|
|
761
1001
|
|
|
762
1002
|
server.registerTool(
|
package/src/paths.ts
CHANGED
|
@@ -73,6 +73,14 @@ export function detectMode(srcDir: string): InvocationMode {
|
|
|
73
73
|
return "package";
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Schema version for ros-help.db.
|
|
78
|
+
* Increment when making destructive schema changes (DROP/RENAME table or column).
|
|
79
|
+
* Stamped into the DB via `PRAGMA user_version` by initDb() and checked at MCP
|
|
80
|
+
* startup to detect stale DBs for bunx users who auto-update the package.
|
|
81
|
+
*/
|
|
82
|
+
export const SCHEMA_VERSION = 1;
|
|
83
|
+
|
|
76
84
|
/**
|
|
77
85
|
* Resolve the version string.
|
|
78
86
|
* Compiled mode: injected at build time via --define.
|