@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/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.