@tikoci/rosetta 0.2.0 → 0.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 +185 -90
- package/bin/rosetta.js +10 -4
- package/package.json +4 -1
- package/src/db.ts +2 -15
- package/src/mcp.ts +162 -30
- package/src/paths.ts +92 -0
- package/src/query.test.ts +5 -4
- package/src/query.ts +44 -6
- package/src/release.test.ts +10 -8
- package/src/setup.ts +131 -42
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for searching [MikroTik RouterOS documentation](https://help.mikrotik.com/docs/spaces/ROS/overview). Gives your AI assistant searchable access to 317 documentation pages, 4,860 property definitions, 40,000-entry command tree, and 144 hardware product specs — with direct links to help.mikrotik.com.
|
|
4
4
|
|
|
5
|
-
Tested with **Claude Desktop**, **Claude Code**, **VS Code Copilot** (including Copilot CLI), and **
|
|
5
|
+
Tested with **Claude Desktop**, **Claude Code**, **VS Code Copilot** (including Copilot CLI), **Cursor**, and **OpenAI Codex** on macOS, Linux, and Windows.
|
|
6
6
|
|
|
7
7
|
## What is SQL-as-RAG?
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Most retrieval-augmented generation (RAG) systems use vector embeddings to searc
|
|
|
10
10
|
|
|
11
11
|
For structured technical documentation like RouterOS, full-text search with [BM25 ranking](https://www.sqlite.org/fts5.html#the_bm25_function) beats vector similarity. Technical terms like "dhcp-snooping" or "/ip/firewall/filter" are exact tokens — [porter stemming](https://www.sqlite.org/fts5.html#porter_tokenizer) and proximity matching handle the rest. No embedding pipeline, no vector database, no API keys. Just a single SQLite file that searches in milliseconds.
|
|
12
12
|
|
|
13
|
-
The data flows: **HTML docs → SQLite extraction → FTS5 indexes → MCP tools → your AI assistant.** The database is built once from MikroTik's official Confluence documentation export, then the MCP server exposes
|
|
13
|
+
The data flows: **HTML docs → SQLite extraction → FTS5 indexes → MCP tools → your AI assistant.** The database is built once from MikroTik's official Confluence documentation export, then the MCP server exposes 11 search tools over stdio or HTTP transport.
|
|
14
14
|
|
|
15
15
|
## What's Inside
|
|
16
16
|
|
|
@@ -24,45 +24,75 @@ The data flows: **HTML docs → SQLite extraction → FTS5 indexes → MCP tools
|
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
## MCP Discovery Status
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
- GitHub MCP Registry listing: planned
|
|
30
|
+
- Official MCP Registry publication: metadata is now prepared in `server.json`
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Local install remains the primary path today (`bunx @tikoci/rosetta`).
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|----------|------|
|
|
35
|
-
| macOS (Apple Silicon) | `rosetta-macos-arm64.zip` |
|
|
36
|
-
| macOS (Intel) | `rosetta-macos-x64.zip` |
|
|
37
|
-
| Windows | `rosetta-windows-x64.zip` |
|
|
38
|
-
| Linux | `rosetta-linux-x64.zip` |
|
|
34
|
+
When ready to publish to the official registry:
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
```sh
|
|
37
|
+
brew install mcp-publisher
|
|
38
|
+
mcp-publisher validate server.json
|
|
39
|
+
mcp-publisher login github
|
|
40
|
+
mcp-publisher publish server.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After publication, the server should be discoverable via:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.tikoci/rosetta"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Option A: Install with Bun (recommended)
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
Zero install, zero config, no binary signing issues. Requires [Bun](https://bun.sh/) — no Gatekeeper or SmartScreen warnings since there's no compiled binary to sign.
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
**1. Install Bun** (if you don't have it):
|
|
45
54
|
|
|
46
55
|
```sh
|
|
47
|
-
|
|
56
|
+
# macOS / Linux
|
|
57
|
+
curl -fsSL https://bun.sh/install | bash
|
|
58
|
+
|
|
59
|
+
# Windows
|
|
60
|
+
powershell -c "irm bun.sh/install.ps1 | iex"
|
|
48
61
|
```
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
**2. Configure your MCP client** with `bunx @tikoci/rosetta` as the command. No setup step needed — the database downloads automatically on first launch (~50 MB compressed).
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary><b>VS Code Copilot</b></summary>
|
|
67
|
+
|
|
68
|
+
Open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`), choose **"MCP: Add Server…"**, select **"Command (stdio)"**, enter `bunx` as the command, and `@tikoci/rosetta` as the argument.
|
|
69
|
+
|
|
70
|
+
Or add to User Settings JSON (`Cmd+Shift+P` → "Preferences: Open User Settings (JSON)"):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"mcp": {
|
|
74
|
+
"servers": {
|
|
75
|
+
"rosetta": {
|
|
76
|
+
"command": "bunx",
|
|
77
|
+
"args": ["@tikoci/rosetta"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
53
81
|
```
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
</details>
|
|
56
84
|
|
|
57
|
-
>
|
|
58
|
-
>
|
|
59
|
-
> **Windows SmartScreen:** If Windows warns about an unrecognized app, click **More info → Run anyway**.
|
|
85
|
+
<details>
|
|
86
|
+
<summary><b>Claude Code</b></summary>
|
|
60
87
|
|
|
61
|
-
|
|
88
|
+
```sh
|
|
89
|
+
claude mcp add rosetta -- bunx @tikoci/rosetta
|
|
90
|
+
```
|
|
62
91
|
|
|
63
|
-
|
|
92
|
+
</details>
|
|
64
93
|
|
|
65
|
-
|
|
94
|
+
<details>
|
|
95
|
+
<summary><b>Claude Desktop</b></summary>
|
|
66
96
|
|
|
67
97
|
Edit your Claude Desktop config file:
|
|
68
98
|
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
@@ -74,37 +104,97 @@ Add (or merge into existing config):
|
|
|
74
104
|
{
|
|
75
105
|
"mcpServers": {
|
|
76
106
|
"rosetta": {
|
|
77
|
-
"command": "
|
|
107
|
+
"command": "bunx",
|
|
108
|
+
"args": ["@tikoci/rosetta"]
|
|
78
109
|
}
|
|
79
110
|
}
|
|
80
111
|
}
|
|
81
112
|
```
|
|
82
113
|
|
|
83
|
-
|
|
114
|
+
> **PATH note:** Claude Desktop on macOS doesn't always inherit your shell PATH. If `bunx` isn't found, use the full path — typically `~/.bun/bin/bunx`. Run `which bunx` to find it, or use `bunx @tikoci/rosetta --setup` which prints the full-path config for you.
|
|
84
115
|
|
|
85
|
-
|
|
116
|
+
Then **restart Claude Desktop**.
|
|
86
117
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
<details>
|
|
121
|
+
<summary><b>GitHub Copilot CLI</b></summary>
|
|
122
|
+
|
|
123
|
+
Inside a `copilot` session, type `/mcp add` to open the interactive form:
|
|
124
|
+
|
|
125
|
+
- **Server Name:** `routeros-rosetta`
|
|
126
|
+
- **Server Type:** 2 (STDIO)
|
|
127
|
+
- **Command:** `bunx @tikoci/rosetta`
|
|
90
128
|
|
|
91
|
-
|
|
129
|
+
Press <kbd>Tab</kbd> to navigate fields, <kbd>Ctrl+S</kbd> to save.
|
|
92
130
|
|
|
93
|
-
|
|
131
|
+
</details>
|
|
94
132
|
|
|
95
|
-
|
|
133
|
+
<details>
|
|
134
|
+
<summary><b>Cursor</b></summary>
|
|
135
|
+
|
|
136
|
+
Open **Settings → MCP** and add a new server:
|
|
96
137
|
|
|
97
138
|
```json
|
|
98
|
-
|
|
99
|
-
"
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
100
141
|
"rosetta": {
|
|
101
|
-
"command": "
|
|
142
|
+
"command": "bunx",
|
|
143
|
+
"args": ["@tikoci/rosetta"]
|
|
102
144
|
}
|
|
103
145
|
}
|
|
104
146
|
}
|
|
105
147
|
```
|
|
106
148
|
|
|
107
|
-
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
|
+
<details>
|
|
152
|
+
<summary><b>OpenAI Codex</b></summary>
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
codex mcp add rosetta -- bunx @tikoci/rosetta
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
> **Note:** ChatGPT Apps require a remote HTTPS MCP endpoint and cannot use local stdio servers like this one. Codex (CLI and desktop app) supports stdio and works with `bunx`.
|
|
159
|
+
|
|
160
|
+
</details>
|
|
161
|
+
|
|
162
|
+
**That's it.** First launch takes a moment to download the database; subsequent starts are instant. The database is stored in `~/.rosetta/ros-help.db`.
|
|
163
|
+
|
|
164
|
+
> **Verify it works:** Run `bunx @tikoci/rosetta --setup` to see the database status and print config for all MCP clients.
|
|
165
|
+
>
|
|
166
|
+
> **Auto-update:** `bunx` checks the npm registry each session and uses the latest published version automatically. No manual update needed. (Note: the `~/.rosetta/ros-help.db` database persists across updates — it's re-downloaded only when missing or when you run `--setup --force`.)
|
|
167
|
+
|
|
168
|
+
### Option B: Pre-built binary (no runtime needed)
|
|
169
|
+
|
|
170
|
+
Download a compiled binary from [Releases](https://github.com/tikoci/rosetta/releases) — no Bun, Node.js, or other tools required.
|
|
171
|
+
|
|
172
|
+
**1. Download** the ZIP for your platform from the [latest release](https://github.com/tikoci/rosetta/releases/latest):
|
|
173
|
+
|
|
174
|
+
| Platform | File |
|
|
175
|
+
|----------|------|
|
|
176
|
+
| macOS (Apple Silicon) | `rosetta-macos-arm64.zip` |
|
|
177
|
+
| macOS (Intel) | `rosetta-macos-x64.zip` |
|
|
178
|
+
| Windows | `rosetta-windows-x64.zip` |
|
|
179
|
+
| Linux | `rosetta-linux-x64.zip` |
|
|
180
|
+
|
|
181
|
+
Extract the ZIP to a permanent location (e.g., `~/rosetta` or `C:\rosetta`).
|
|
182
|
+
|
|
183
|
+
**2. Run setup** to download the database and see MCP client config:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
./rosetta --setup
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
On Windows: `.\rosetta.exe --setup`
|
|
190
|
+
|
|
191
|
+
> **macOS Gatekeeper:** If macOS blocks the binary: `xattr -d com.apple.quarantine ./rosetta` or go to **System Settings → Privacy & Security → Allow Anyway**.
|
|
192
|
+
>
|
|
193
|
+
> **Windows SmartScreen:** Click **More info → Run anyway**.
|
|
194
|
+
|
|
195
|
+
**3. Configure your MCP client** using the config printed by `--setup`. It uses the full path to the binary — paste it into your MCP client's config as shown.
|
|
196
|
+
|
|
197
|
+
### Try It
|
|
108
198
|
|
|
109
199
|
Ask your AI assistant questions like:
|
|
110
200
|
|
|
@@ -117,7 +207,7 @@ Ask your AI assistant questions like:
|
|
|
117
207
|
|
|
118
208
|
## MCP Tools
|
|
119
209
|
|
|
120
|
-
The server provides
|
|
210
|
+
The server provides 11 tools, designed to work together:
|
|
121
211
|
|
|
122
212
|
| Tool | What it does |
|
|
123
213
|
|------|-------------|
|
|
@@ -127,6 +217,7 @@ The server provides 10 tools, designed to work together:
|
|
|
127
217
|
| `routeros_search_properties` | Search across 4,860 property names and descriptions |
|
|
128
218
|
| `routeros_command_tree` | Browse the `/ip/firewall/filter` style command hierarchy |
|
|
129
219
|
| `routeros_search_callouts` | Search warnings, notes, and tips across all pages |
|
|
220
|
+
| `routeros_search_changelogs` | Search parsed changelog entries — filter by version range, category, breaking changes |
|
|
130
221
|
| `routeros_command_version_check` | Check which RouterOS versions include a command |
|
|
131
222
|
| `routeros_device_lookup` | Hardware specs for 144 MikroTik products — filter by architecture, RAM, storage, PoE, wireless, LTE |
|
|
132
223
|
| `routeros_stats` | Database health: page/property/command counts, coverage stats |
|
|
@@ -134,78 +225,79 @@ The server provides 10 tools, designed to work together:
|
|
|
134
225
|
|
|
135
226
|
The AI assistant typically starts with `routeros_search`, then drills into specific pages, properties, or the command tree based on what it finds. Each tool's description includes workflow hints (e.g., "→ use `routeros_get_page` to read full content") and empty-result suggestions so the AI knows how to chain tools together — this is where most of the tuning effort goes.
|
|
136
227
|
|
|
137
|
-
##
|
|
228
|
+
## Troubleshooting
|
|
138
229
|
|
|
139
|
-
|
|
230
|
+
| Issue | Solution |
|
|
231
|
+
|-------|----------|
|
|
232
|
+
| **First launch is slow** | One-time database download (~50 MB). Subsequent starts are instant. |
|
|
233
|
+
| **`npx @tikoci/rosetta` fails** | This package requires Bun, not Node.js. Use `bunx` instead of `npx`. |
|
|
234
|
+
| **`npm install -g` then `rosetta` fails** | Global npm install works if Bun is on PATH — it delegates to `bun` at runtime. But prefer `bunx` — it's simpler and auto-updates. |
|
|
235
|
+
| **ChatGPT Apps can't connect with `bunx @tikoci/rosetta`** | Expected: ChatGPT Apps supports remote HTTPS MCP endpoints, not local stdio command launch. Use OpenAI Codex for local stdio, or deploy/tunnel a remote MCP URL for ChatGPT. |
|
|
236
|
+
| **Claude Desktop can't find `bunx`** | Claude Desktop on macOS may not inherit shell PATH. Use the full path to bunx (run `which bunx` to find it, typically `~/.bun/bin/bunx`). `bunx @tikoci/rosetta --setup` prints the full-path config. |
|
|
237
|
+
| **macOS Gatekeeper blocks binary** | Use `bunx` install (no Gatekeeper issues), or: `xattr -d com.apple.quarantine ./rosetta` |
|
|
238
|
+
| **Windows SmartScreen warning** | Use `bunx` install (no SmartScreen issues), or click **More info → Run anyway** |
|
|
239
|
+
| **How to update** | `bunx` always uses the latest published version. For binaries, re-download from [Releases](https://github.com/tikoci/rosetta/releases/latest). |
|
|
140
240
|
|
|
141
|
-
|
|
241
|
+
## HTTP Transport
|
|
142
242
|
|
|
143
|
-
|
|
144
|
-
# macOS / Linux
|
|
145
|
-
curl -fsSL https://bun.sh/install | bash
|
|
146
|
-
# or: brew install oven-sh/bun/bun
|
|
243
|
+
Most MCP clients use stdio (the default). Some — like the OpenAI platform and remote/LAN setups — require an HTTP endpoint instead. Rosetta supports the [MCP Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) via the `--http` flag:
|
|
147
244
|
|
|
148
|
-
|
|
149
|
-
|
|
245
|
+
```sh
|
|
246
|
+
rosetta --http # http://localhost:8080/mcp
|
|
247
|
+
rosetta --http --port 9090 # custom port
|
|
248
|
+
rosetta --http --host 0.0.0.0 # accessible from LAN
|
|
150
249
|
```
|
|
151
250
|
|
|
152
|
-
|
|
251
|
+
Then point your MCP client at the URL:
|
|
153
252
|
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
cd rosetta
|
|
157
|
-
bun install
|
|
253
|
+
```json
|
|
254
|
+
{ "url": "http://localhost:8080/mcp" }
|
|
158
255
|
```
|
|
159
256
|
|
|
160
|
-
|
|
257
|
+
**Key facts:**
|
|
161
258
|
|
|
162
|
-
|
|
259
|
+
- **Read-only** — the server queries a local SQLite database. It does not store data, accept uploads, or modify anything.
|
|
260
|
+
- **No authentication** — designed for local/trusted-network use. For public exposure, put it behind a reverse proxy (nginx, caddy) with TLS and auth.
|
|
261
|
+
- **TLS built-in** — for direct HTTPS without a proxy: `--tls-cert cert.pem --tls-key key.pem` (or `TLS_CERT_PATH` + `TLS_KEY_PATH` env vars)
|
|
262
|
+
- **Defaults to localhost** — binding to all interfaces (`--host 0.0.0.0`) requires an explicit flag and logs a warning.
|
|
263
|
+
- **Origin validation** — rejects cross-origin requests to prevent DNS rebinding attacks.
|
|
264
|
+
- **Stdio remains default** — `--http` is opt-in. Existing stdio configs are unaffected.
|
|
163
265
|
|
|
164
|
-
|
|
165
|
-
bun run src/mcp.ts --setup
|
|
166
|
-
```
|
|
266
|
+
The `PORT`, `HOST`, `TLS_CERT_PATH`, and `TLS_KEY_PATH` environment variables are supported (lower precedence than CLI flags).
|
|
167
267
|
|
|
168
|
-
|
|
268
|
+
## Container Images
|
|
169
269
|
|
|
170
|
-
|
|
270
|
+
Release CI publishes multi-arch OCI images to:
|
|
171
271
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"mcpServers": {
|
|
175
|
-
"rosetta": {
|
|
176
|
-
"command": "bun",
|
|
177
|
-
"args": ["run", "src/mcp.ts"],
|
|
178
|
-
"cwd": "/path/to/rosetta"
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
```
|
|
272
|
+
- Docker Hub: `ammo74/rosetta`
|
|
273
|
+
- GHCR: `ghcr.io/tikoci/rosetta`
|
|
183
274
|
|
|
184
|
-
|
|
275
|
+
Tags per release:
|
|
185
276
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
- `${version}` (example: `v0.2.1`)
|
|
278
|
+
- `latest`
|
|
279
|
+
- `sha-<12-char-commit>`
|
|
189
280
|
|
|
190
|
-
|
|
281
|
+
Container defaults:
|
|
191
282
|
|
|
192
|
-
|
|
283
|
+
- Starts in HTTP mode (`--http`) on `0.0.0.0`
|
|
284
|
+
- Uses `PORT` if set, otherwise 8080
|
|
285
|
+
- Uses HTTPS only when both `TLS_CERT_PATH` and `TLS_KEY_PATH` are set
|
|
193
286
|
|
|
194
|
-
|
|
287
|
+
Examples:
|
|
195
288
|
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
"servers": {
|
|
199
|
-
"rosetta": {
|
|
200
|
-
"command": "bun",
|
|
201
|
-
"args": ["run", "src/mcp.ts"],
|
|
202
|
-
"cwd": "/path/to/rosetta"
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
289
|
+
```sh
|
|
290
|
+
docker run --rm -p 8080:8080 ghcr.io/tikoci/rosetta:latest
|
|
206
291
|
```
|
|
207
292
|
|
|
208
|
-
|
|
293
|
+
```sh
|
|
294
|
+
docker run --rm -p 8443:8443 \
|
|
295
|
+
-e PORT=8443 \
|
|
296
|
+
-e TLS_CERT_PATH=/certs/cert.pem \
|
|
297
|
+
-e TLS_KEY_PATH=/certs/key.pem \
|
|
298
|
+
-v "$PWD/certs:/certs:ro" \
|
|
299
|
+
ghcr.io/tikoci/rosetta:latest
|
|
300
|
+
```
|
|
209
301
|
|
|
210
302
|
## Building from Source
|
|
211
303
|
|
|
@@ -267,11 +359,13 @@ make release VERSION=v0.1.0
|
|
|
267
359
|
|
|
268
360
|
This cross-compiles to macOS (arm64 + x64), Windows (x64), and Linux (x64), creates ZIP archives, compresses the database, tags the commit, and creates a GitHub Release with all artifacts.
|
|
269
361
|
|
|
362
|
+
The release workflow also publishes OCI images to Docker Hub (`ammo74/rosetta`) and GHCR (`ghcr.io/tikoci/rosetta`) using crane (no Docker daemon required in CI).
|
|
363
|
+
|
|
270
364
|
## Project Structure
|
|
271
365
|
|
|
272
366
|
```text
|
|
273
367
|
src/
|
|
274
|
-
├── mcp.ts # MCP server (
|
|
368
|
+
├── mcp.ts # MCP server (11 tools, stdio + HTTP) + CLI dispatch
|
|
275
369
|
├── setup.ts # --setup: DB download + MCP client config
|
|
276
370
|
├── query.ts # NL → FTS5 query planner, BM25 ranking
|
|
277
371
|
├── db.ts # SQLite schema, WAL mode, FTS5 triggers
|
|
@@ -285,6 +379,7 @@ src/
|
|
|
285
379
|
|
|
286
380
|
scripts/
|
|
287
381
|
└── build-release.ts # Cross-compile + package releases
|
|
382
|
+
└── container-entrypoint.sh # OCI image runtime entrypoint (HTTP default)
|
|
288
383
|
```
|
|
289
384
|
|
|
290
385
|
## Data Sources
|
package/bin/rosetta.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* bin/rosetta.js — Entry point for `
|
|
3
|
+
* bin/rosetta.js — Entry point for `bunx @tikoci/rosetta` (and npx fallback).
|
|
4
4
|
*
|
|
5
5
|
* The server uses bun:sqlite and other Bun APIs, so it requires the Bun runtime.
|
|
6
6
|
* When run under Bun (via bunx), this imports src/mcp.ts directly.
|
|
7
|
-
* When run under Node (via npx), this spawns `bun
|
|
7
|
+
* When run under Node (via npx), this spawns `bun` as a subprocess if available.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
@@ -17,17 +17,23 @@ if (typeof Bun !== "undefined") {
|
|
|
17
17
|
// Running under Bun — import TypeScript entry directly
|
|
18
18
|
await import(entry);
|
|
19
19
|
} else {
|
|
20
|
-
// Running under Node — delegate to Bun
|
|
20
|
+
// Running under Node — try to delegate to Bun, but warn the user
|
|
21
|
+
console.error("Note: rosetta requires the Bun runtime. Attempting to run via bun...");
|
|
22
|
+
console.error();
|
|
21
23
|
const { spawn } = await import("node:child_process");
|
|
22
24
|
const proc = spawn("bun", ["run", entry, ...process.argv.slice(2)], {
|
|
23
25
|
stdio: "inherit",
|
|
24
26
|
});
|
|
25
27
|
proc.on("error", (err) => {
|
|
26
28
|
if (err.code === "ENOENT") {
|
|
27
|
-
console.error("rosetta requires
|
|
29
|
+
console.error("rosetta requires Bun (bun:sqlite is not available in Node.js).\n");
|
|
30
|
+
console.error("Recommended: install Bun, then use bunx instead of npx:\n");
|
|
31
|
+
console.error(" curl -fsSL https://bun.sh/install | bash");
|
|
32
|
+
console.error(" bunx @tikoci/rosetta --setup\n");
|
|
28
33
|
console.error("Install Bun: https://bun.sh");
|
|
29
34
|
process.exit(1);
|
|
30
35
|
}
|
|
36
|
+
console.error(`Failed to start bun: ${err.message}`);
|
|
31
37
|
process.exit(1);
|
|
32
38
|
});
|
|
33
39
|
proc.on("exit", (code) => process.exit(code ?? 1));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tikoci/rosetta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "RouterOS documentation as SQLite FTS5 — RAG search + command glossary via MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
26
26
|
"zod": "^4.3.6"
|
|
27
27
|
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"bun": ">=1.1"
|
|
30
|
+
},
|
|
28
31
|
"devDependencies": {
|
|
29
32
|
"@biomejs/biome": "^2.4.7",
|
|
30
33
|
"@types/bun": "^1.3.10",
|
package/src/db.ts
CHANGED
|
@@ -20,22 +20,9 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import sqlite from "bun:sqlite";
|
|
23
|
-
import
|
|
23
|
+
import { resolveDbPath } from "./paths.ts";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Resolve the base directory for finding ros-help.db:
|
|
29
|
-
* - Compiled binary: directory containing the executable
|
|
30
|
-
* - Dev mode: project root (one level up from src/)
|
|
31
|
-
*/
|
|
32
|
-
const baseDir =
|
|
33
|
-
typeof IS_COMPILED !== "undefined" && IS_COMPILED
|
|
34
|
-
? path.dirname(process.execPath)
|
|
35
|
-
: path.resolve(import.meta.dirname, "..");
|
|
36
|
-
|
|
37
|
-
export const DB_PATH =
|
|
38
|
-
process.env.DB_PATH?.trim() || path.join(baseDir, "ros-help.db");
|
|
25
|
+
export const DB_PATH = resolveDbPath(import.meta.dirname);
|
|
39
26
|
|
|
40
27
|
export const db = new sqlite(DB_PATH);
|
|
41
28
|
|
package/src/mcp.ts
CHANGED
|
@@ -9,38 +9,63 @@
|
|
|
9
9
|
* --setup [--force] Download database + print MCP client config
|
|
10
10
|
* --version Print version
|
|
11
11
|
* --help Print usage
|
|
12
|
+
* --http Start with Streamable HTTP transport (instead of stdio)
|
|
13
|
+
* --port <N> HTTP listen port (default: 8080, env: PORT)
|
|
14
|
+
* --host <ADDR> HTTP bind address (default: localhost, env: HOST)
|
|
15
|
+
* --tls-cert <PATH> TLS certificate PEM file (enables HTTPS, env: TLS_CERT_PATH)
|
|
16
|
+
* --tls-key <PATH> TLS private key PEM file (requires --tls-cert, env: TLS_KEY_PATH)
|
|
12
17
|
* (default) Start MCP server (stdio transport)
|
|
13
18
|
*
|
|
14
19
|
* Environment variables:
|
|
15
20
|
* DB_PATH — absolute path to ros-help.db (default: next to binary or project root)
|
|
21
|
+
* PORT — HTTP listen port (lower precedence than --port)
|
|
22
|
+
* HOST — HTTP bind address (lower precedence than --host)
|
|
23
|
+
* TLS_CERT_PATH — TLS certificate path (lower precedence than --tls-cert)
|
|
24
|
+
* TLS_KEY_PATH — TLS private key path (lower precedence than --tls-key)
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
import { resolveVersion } from "./paths.ts";
|
|
28
|
+
|
|
29
|
+
const RESOLVED_VERSION = resolveVersion(import.meta.dirname);
|
|
20
30
|
|
|
21
31
|
// ── CLI dispatch (before MCP server init) ──
|
|
22
32
|
|
|
23
33
|
const args = process.argv.slice(2);
|
|
24
34
|
|
|
35
|
+
/** Extract the value following a named flag (e.g. --port 8080 → "8080") */
|
|
36
|
+
function getArg(name: string): string | undefined {
|
|
37
|
+
const idx = args.indexOf(name);
|
|
38
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
if (args.includes("--version") || args.includes("-v")) {
|
|
26
|
-
|
|
27
|
-
console.log(`rosetta ${ver}`);
|
|
42
|
+
console.log(`rosetta ${RESOLVED_VERSION}`);
|
|
28
43
|
process.exit(0);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
if (args.includes("--help") || args.includes("-h")) {
|
|
32
|
-
|
|
33
|
-
console.log(`rosetta ${ver} — MCP server for RouterOS documentation`);
|
|
47
|
+
console.log(`rosetta ${RESOLVED_VERSION} — MCP server for RouterOS documentation`);
|
|
34
48
|
console.log();
|
|
35
49
|
console.log("Usage:");
|
|
36
50
|
console.log(" rosetta Start MCP server (stdio transport)");
|
|
51
|
+
console.log(" rosetta --http Start with Streamable HTTP transport");
|
|
37
52
|
console.log(" rosetta --setup Download database + print MCP client config");
|
|
38
53
|
console.log(" rosetta --setup --force Re-download database");
|
|
39
54
|
console.log(" rosetta --version Print version");
|
|
40
55
|
console.log(" rosetta --help Print this help");
|
|
41
56
|
console.log();
|
|
57
|
+
console.log("HTTP options (require --http):");
|
|
58
|
+
console.log(" --port <N> Listen port (default: 8080, env: PORT)");
|
|
59
|
+
console.log(" --host <ADDR> Bind address (default: localhost, env: HOST)");
|
|
60
|
+
console.log(" --tls-cert <PATH> TLS certificate PEM file (env: TLS_CERT_PATH)");
|
|
61
|
+
console.log(" --tls-key <PATH> TLS private key PEM file (env: TLS_KEY_PATH)");
|
|
62
|
+
console.log();
|
|
42
63
|
console.log("Environment:");
|
|
43
64
|
console.log(" DB_PATH Absolute path to ros-help.db (optional)");
|
|
65
|
+
console.log(" PORT HTTP listen port (lower precedence than --port)");
|
|
66
|
+
console.log(" HOST HTTP bind address (lower precedence than --host)");
|
|
67
|
+
console.log(" TLS_CERT_PATH TLS certificate path (lower precedence than --tls-cert)");
|
|
68
|
+
console.log(" TLS_KEY_PATH TLS private key path (lower precedence than --tls-key)");
|
|
44
69
|
process.exit(0);
|
|
45
70
|
}
|
|
46
71
|
|
|
@@ -55,8 +80,9 @@ if (args.includes("--setup")) {
|
|
|
55
80
|
|
|
56
81
|
// ── MCP Server ──
|
|
57
82
|
|
|
83
|
+
const useHttp = args.includes("--http");
|
|
84
|
+
|
|
58
85
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
59
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
60
86
|
const { z } = await import("zod/v3");
|
|
61
87
|
|
|
62
88
|
// Dynamic imports — db.ts eagerly opens the DB file on import,
|
|
@@ -65,12 +91,8 @@ const { z } = await import("zod/v3");
|
|
|
65
91
|
//
|
|
66
92
|
// Check if DB has data BEFORE importing db.ts. If empty/missing,
|
|
67
93
|
// auto-download so db.ts opens the real database.
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
? (await import("node:path")).dirname(process.execPath)
|
|
71
|
-
: (await import("node:path")).resolve(import.meta.dirname, "..");
|
|
72
|
-
const _dbPath =
|
|
73
|
-
process.env.DB_PATH?.trim() || (await import("node:path")).join(_baseDir, "ros-help.db");
|
|
94
|
+
const { resolveDbPath } = await import("./paths.ts");
|
|
95
|
+
const _dbPath = resolveDbPath(import.meta.dirname);
|
|
74
96
|
|
|
75
97
|
const _pageCount = (() => {
|
|
76
98
|
try {
|
|
@@ -116,7 +138,7 @@ initDb();
|
|
|
116
138
|
|
|
117
139
|
const server = new McpServer({
|
|
118
140
|
name: "rosetta",
|
|
119
|
-
version:
|
|
141
|
+
version: RESOLVED_VERSION,
|
|
120
142
|
});
|
|
121
143
|
|
|
122
144
|
// ---- routeros_search ----
|
|
@@ -180,19 +202,17 @@ Use after routeros_search identifies a relevant page. Pass the numeric page ID
|
|
|
180
202
|
Returns: plain text, code blocks, and callout blocks (notes, warnings, info, tips).
|
|
181
203
|
Callouts contain crucial caveats and edge-case details — always review them.
|
|
182
204
|
|
|
183
|
-
**Large page handling:**
|
|
184
|
-
Some pages are 100K+ chars. When max_length is set and the page exceeds it,
|
|
205
|
+
**Large page handling:** max_length defaults to 16000. When page content exceeds it,
|
|
185
206
|
pages with sections return a **table of contents** instead of truncated text.
|
|
186
207
|
The TOC lists each section's heading, hierarchy level, character count, and
|
|
187
208
|
deep-link URL. Re-call with the section parameter to retrieve specific sections.
|
|
188
209
|
|
|
189
210
|
**Section parameter:** Pass a section heading or anchor_id (from the TOC)
|
|
190
|
-
to get that section's content.
|
|
191
|
-
|
|
192
|
-
under it.
|
|
211
|
+
to get that section's content. If a section is still too large, its sub-section
|
|
212
|
+
TOC is returned instead — request a more specific sub-section.
|
|
193
213
|
|
|
194
214
|
Recommended workflow for large pages:
|
|
195
|
-
1.
|
|
215
|
+
1. First call → get TOC if page is large (automatic with default max_length)
|
|
196
216
|
2. Review section headings to find the relevant section
|
|
197
217
|
3. Call again with section="Section Name" to get that section's content
|
|
198
218
|
|
|
@@ -209,12 +229,12 @@ Workflow — what to do with this content:
|
|
|
209
229
|
.number()
|
|
210
230
|
.int()
|
|
211
231
|
.min(1000)
|
|
212
|
-
.
|
|
213
|
-
.describe("
|
|
232
|
+
.default(16000)
|
|
233
|
+
.describe("Max combined text+code length (default: 16000). If exceeded and page has sections, returns a TOC instead of truncated text. Set higher (e.g. 50000) to get more content in one call."),
|
|
214
234
|
section: z
|
|
215
235
|
.string()
|
|
216
236
|
.optional()
|
|
217
|
-
.describe("Section heading or anchor_id from TOC. Returns only that section's content."),
|
|
237
|
+
.describe("Section heading or anchor_id from TOC. Returns only that section's content (also subject to max_length)."),
|
|
218
238
|
},
|
|
219
239
|
},
|
|
220
240
|
async ({ page, max_length, section }) => {
|
|
@@ -451,6 +471,29 @@ Examples:
|
|
|
451
471
|
|
|
452
472
|
// ---- routeros_search_changelogs ----
|
|
453
473
|
|
|
474
|
+
/** Group flat changelog results by version for compact output. */
|
|
475
|
+
function groupChangelogsByVersion(results: Array<{ version: string; released: string | null; category: string; is_breaking: number; description: string }>) {
|
|
476
|
+
const byVersion = new Map<string, { released: string | null; entries: Array<{ category: string; is_breaking: number; description: string }> }>();
|
|
477
|
+
for (const r of results) {
|
|
478
|
+
let group = byVersion.get(r.version);
|
|
479
|
+
if (!group) {
|
|
480
|
+
group = { released: r.released, entries: [] };
|
|
481
|
+
byVersion.set(r.version, group);
|
|
482
|
+
}
|
|
483
|
+
group.entries.push({ category: r.category, is_breaking: r.is_breaking, description: r.description });
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
total_entries: results.length,
|
|
487
|
+
versions: Array.from(byVersion.entries()).map(([version, { released, entries }]) => ({
|
|
488
|
+
version,
|
|
489
|
+
released,
|
|
490
|
+
entry_count: entries.length,
|
|
491
|
+
breaking_count: entries.filter(e => e.is_breaking).length,
|
|
492
|
+
entries,
|
|
493
|
+
})),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
454
497
|
server.registerTool(
|
|
455
498
|
"routeros_search_changelogs",
|
|
456
499
|
{
|
|
@@ -484,7 +527,7 @@ Coverage depends on which versions were extracted — typically matches ros_vers
|
|
|
484
527
|
from_version: z
|
|
485
528
|
.string()
|
|
486
529
|
.optional()
|
|
487
|
-
.describe("Start of version range,
|
|
530
|
+
.describe("Start of version range, EXCLUSIVE — returns changes AFTER this version (e.g., from_version='7.21.3' excludes 7.21.3 entries, includes 7.22+)"),
|
|
488
531
|
to_version: z
|
|
489
532
|
.string()
|
|
490
533
|
.optional()
|
|
@@ -501,10 +544,10 @@ Coverage depends on which versions were extracted — typically matches ros_vers
|
|
|
501
544
|
.number()
|
|
502
545
|
.int()
|
|
503
546
|
.min(1)
|
|
504
|
-
.max(
|
|
547
|
+
.max(500)
|
|
505
548
|
.optional()
|
|
506
|
-
.default(
|
|
507
|
-
.describe("Max results (default
|
|
549
|
+
.default(50)
|
|
550
|
+
.describe("Max results (default 50, max 500). Version-range queries often need higher limits."),
|
|
508
551
|
},
|
|
509
552
|
},
|
|
510
553
|
async ({ query, version, from_version, to_version, category, breaking_only, limit }) => {
|
|
@@ -535,8 +578,10 @@ Coverage depends on which versions were extracted — typically matches ros_vers
|
|
|
535
578
|
],
|
|
536
579
|
};
|
|
537
580
|
}
|
|
581
|
+
// Group by version for compact output — avoids repeating version/released on every entry
|
|
582
|
+
const grouped = groupChangelogsByVersion(results);
|
|
538
583
|
return {
|
|
539
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
584
|
+
content: [{ type: "text", text: JSON.stringify(grouped, null, 2) }],
|
|
540
585
|
};
|
|
541
586
|
},
|
|
542
587
|
);
|
|
@@ -719,7 +764,94 @@ Requires network access to upgrade.mikrotik.com.`,
|
|
|
719
764
|
|
|
720
765
|
// ---- Start ----
|
|
721
766
|
|
|
722
|
-
|
|
723
|
-
await
|
|
767
|
+
if (useHttp) {
|
|
768
|
+
const { existsSync } = await import("node:fs");
|
|
769
|
+
const { WebStandardStreamableHTTPServerTransport } = await import(
|
|
770
|
+
"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
const port = Number(getArg("--port") ?? process.env.PORT ?? 8080);
|
|
774
|
+
const hostname = getArg("--host") ?? process.env.HOST ?? "localhost";
|
|
775
|
+
const tlsCert = getArg("--tls-cert") ?? process.env.TLS_CERT_PATH;
|
|
776
|
+
const tlsKey = getArg("--tls-key") ?? process.env.TLS_KEY_PATH;
|
|
777
|
+
|
|
778
|
+
if ((tlsCert && !tlsKey) || (!tlsCert && tlsKey)) {
|
|
779
|
+
process.stderr.write(
|
|
780
|
+
"Error: TLS cert and key must both be provided (via flags or TLS_CERT_PATH/TLS_KEY_PATH)\n"
|
|
781
|
+
);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
if (tlsCert && !existsSync(tlsCert)) {
|
|
785
|
+
process.stderr.write(`Error: TLS certificate not found: ${tlsCert}\n`);
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
if (tlsKey && !existsSync(tlsKey)) {
|
|
789
|
+
process.stderr.write(`Error: TLS private key not found: ${tlsKey}\n`);
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const useTls = !!(tlsCert && tlsKey);
|
|
794
|
+
const scheme = useTls ? "https" : "http";
|
|
795
|
+
|
|
796
|
+
const httpTransport = new WebStandardStreamableHTTPServerTransport({
|
|
797
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
798
|
+
enableJsonResponse: false,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
await server.connect(httpTransport);
|
|
802
|
+
|
|
803
|
+
const isLAN = hostname === "0.0.0.0" || hostname === "::";
|
|
804
|
+
if (isLAN) {
|
|
805
|
+
process.stderr.write(
|
|
806
|
+
"Warning: Binding to all interfaces — the MCP server will be accessible from the network.\n"
|
|
807
|
+
);
|
|
808
|
+
if (!useTls) {
|
|
809
|
+
process.stderr.write(
|
|
810
|
+
" Consider using --tls-cert/--tls-key or a reverse proxy for production use.\n"
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
Bun.serve({
|
|
816
|
+
port,
|
|
817
|
+
hostname,
|
|
818
|
+
...(useTls && tlsCert && tlsKey
|
|
819
|
+
? { tls: { cert: Bun.file(tlsCert), key: Bun.file(tlsKey) } }
|
|
820
|
+
: {}),
|
|
821
|
+
async fetch(req: Request): Promise<Response> {
|
|
822
|
+
const url = new URL(req.url);
|
|
823
|
+
|
|
824
|
+
// DNS rebinding protection: reject browser-origin requests
|
|
825
|
+
const origin = req.headers.get("origin");
|
|
826
|
+
if (origin) {
|
|
827
|
+
try {
|
|
828
|
+
const originHost = new URL(origin).host;
|
|
829
|
+
const serverHost = `${hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname}:${port}`;
|
|
830
|
+
if (originHost !== serverHost && originHost !== `localhost:${port}` && originHost !== `127.0.0.1:${port}`) {
|
|
831
|
+
return new Response("Forbidden: Origin not allowed", { status: 403 });
|
|
832
|
+
}
|
|
833
|
+
} catch {
|
|
834
|
+
return new Response("Forbidden: Invalid Origin", { status: 403 });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (url.pathname === "/mcp") {
|
|
839
|
+
return httpTransport.handleRequest(req);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return new Response("Not Found", { status: 404 });
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
const displayHost = isLAN ? "localhost" : hostname;
|
|
847
|
+
process.stderr.write(`rosetta ${RESOLVED_VERSION} — Streamable HTTP\n`);
|
|
848
|
+
process.stderr.write(` ${scheme}://${displayHost}:${port}/mcp\n`);
|
|
849
|
+
} else {
|
|
850
|
+
const { StdioServerTransport } = await import(
|
|
851
|
+
"@modelcontextprotocol/sdk/server/stdio.js"
|
|
852
|
+
);
|
|
853
|
+
const transport = new StdioServerTransport();
|
|
854
|
+
await server.connect(transport);
|
|
855
|
+
}
|
|
724
856
|
|
|
725
857
|
})();
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths.ts — Shared DB path resolution for all entry points.
|
|
3
|
+
*
|
|
4
|
+
* Three modes:
|
|
5
|
+
* 1. Compiled binary (IS_COMPILED) → next to executable
|
|
6
|
+
* 2. Dev mode (.git exists in project root) → project root
|
|
7
|
+
* 3. Package mode (bunx / bun add -g) → ~/.rosetta/
|
|
8
|
+
*
|
|
9
|
+
* DB_PATH env var overrides all modes.
|
|
10
|
+
* This module must NOT import db.ts or bun:sqlite — it's used before the DB is opened.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
declare const IS_COMPILED: boolean;
|
|
18
|
+
declare const VERSION: string;
|
|
19
|
+
|
|
20
|
+
/** True when running as a compiled binary (bun build --compile) */
|
|
21
|
+
export function isCompiled(): boolean {
|
|
22
|
+
try {
|
|
23
|
+
return typeof IS_COMPILED !== "undefined" && IS_COMPILED;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when running from a git checkout (dev mode) */
|
|
30
|
+
function isDevMode(projectRoot: string): boolean {
|
|
31
|
+
return existsSync(path.join(projectRoot, ".git"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the directory where ros-help.db should live.
|
|
36
|
+
* - Compiled: directory containing the executable
|
|
37
|
+
* - Dev: project root (one level up from src/)
|
|
38
|
+
* - Package (bunx / global install): ~/.rosetta/
|
|
39
|
+
*/
|
|
40
|
+
export function resolveBaseDir(srcDir: string): string {
|
|
41
|
+
if (isCompiled()) {
|
|
42
|
+
return path.dirname(process.execPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const projectRoot = path.resolve(srcDir, "..");
|
|
46
|
+
if (isDevMode(projectRoot)) {
|
|
47
|
+
return projectRoot;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Package mode — stable user-local directory
|
|
51
|
+
const dataDir = path.join(homedir(), ".rosetta");
|
|
52
|
+
mkdirSync(dataDir, { recursive: true });
|
|
53
|
+
return dataDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the full path to ros-help.db.
|
|
58
|
+
* DB_PATH env var overrides all detection logic.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveDbPath(srcDir: string): string {
|
|
61
|
+
const envPath = process.env.DB_PATH?.trim();
|
|
62
|
+
if (envPath) return envPath;
|
|
63
|
+
return path.join(resolveBaseDir(srcDir), "ros-help.db");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Detect invocation mode: "compiled" | "dev" | "package" */
|
|
67
|
+
export type InvocationMode = "compiled" | "dev" | "package";
|
|
68
|
+
|
|
69
|
+
export function detectMode(srcDir: string): InvocationMode {
|
|
70
|
+
if (isCompiled()) return "compiled";
|
|
71
|
+
const projectRoot = path.resolve(srcDir, "..");
|
|
72
|
+
if (isDevMode(projectRoot)) return "dev";
|
|
73
|
+
return "package";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the version string.
|
|
78
|
+
* Compiled mode: injected at build time via --define.
|
|
79
|
+
* Dev/package mode: read from package.json.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveVersion(srcDir: string): string {
|
|
82
|
+
try {
|
|
83
|
+
if (typeof VERSION !== "undefined") return VERSION;
|
|
84
|
+
} catch {}
|
|
85
|
+
try {
|
|
86
|
+
const pkgPath = path.join(srcDir, "..", "package.json");
|
|
87
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
88
|
+
return pkg.version ?? "unknown";
|
|
89
|
+
} catch {
|
|
90
|
+
return "unknown";
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/query.test.ts
CHANGED
|
@@ -877,13 +877,13 @@ describe("searchChangelogs", () => {
|
|
|
877
877
|
}
|
|
878
878
|
});
|
|
879
879
|
|
|
880
|
-
test("version range filter works", () => {
|
|
881
|
-
const results = searchChangelogs("", { fromVersion: "7.
|
|
880
|
+
test("version range filter works (from_version exclusive, to_version inclusive)", () => {
|
|
881
|
+
const results = searchChangelogs("", { fromVersion: "7.21", toVersion: "7.22.1" });
|
|
882
882
|
expect(results.length).toBeGreaterThan(0);
|
|
883
883
|
for (const r of results) {
|
|
884
884
|
expect(["7.22", "7.22.1"]).toContain(r.version);
|
|
885
885
|
}
|
|
886
|
-
// Should not include 7.21
|
|
886
|
+
// Should not include 7.21 (from_version is exclusive)
|
|
887
887
|
expect(results.some((r) => r.version === "7.21")).toBe(false);
|
|
888
888
|
});
|
|
889
889
|
|
|
@@ -902,7 +902,8 @@ describe("searchChangelogs", () => {
|
|
|
902
902
|
});
|
|
903
903
|
|
|
904
904
|
test("FTS combined with version range", () => {
|
|
905
|
-
|
|
905
|
+
// from_version is exclusive, so use 7.21 to include 7.22
|
|
906
|
+
const results = searchChangelogs("MLAG", { fromVersion: "7.21", toVersion: "7.22" });
|
|
906
907
|
expect(results.length).toBe(1);
|
|
907
908
|
expect(results[0].category).toBe("bridge");
|
|
908
909
|
});
|
package/src/query.ts
CHANGED
|
@@ -184,6 +184,7 @@ export function getPage(idOrTitle: string | number, maxLength?: number, section?
|
|
|
184
184
|
word_count: number;
|
|
185
185
|
code_lines: number;
|
|
186
186
|
callouts: Array<{ type: string; content: string }>;
|
|
187
|
+
callout_summary?: { count: number; types: Record<string, number> };
|
|
187
188
|
truncated?: { text_total: number; code_total: number };
|
|
188
189
|
sections?: SectionTocEntry[];
|
|
189
190
|
section?: { heading: string; level: number; anchor_id: string };
|
|
@@ -221,11 +222,11 @@ export function getPage(idOrTitle: string | number, maxLength?: number, section?
|
|
|
221
222
|
|
|
222
223
|
const descendants = db
|
|
223
224
|
.prepare(
|
|
224
|
-
`SELECT heading, level, text, code, word_count
|
|
225
|
+
`SELECT heading, level, anchor_id, text, code, word_count
|
|
225
226
|
FROM sections WHERE page_id = ? AND sort_order > ? AND level > ? AND sort_order < ?
|
|
226
227
|
ORDER BY sort_order`,
|
|
227
228
|
)
|
|
228
|
-
.all(page.id, sec.sort_order, sec.level, upperBound) as Array<{ heading: string; level: number; text: string; code: string; word_count: number }>;
|
|
229
|
+
.all(page.id, sec.sort_order, sec.level, upperBound) as Array<{ heading: string; level: number; anchor_id: string; text: string; code: string; word_count: number }>;
|
|
229
230
|
|
|
230
231
|
let fullText = sec.text;
|
|
231
232
|
let fullCode = sec.code;
|
|
@@ -237,6 +238,34 @@ export function getPage(idOrTitle: string | number, maxLength?: number, section?
|
|
|
237
238
|
totalWords += child.word_count;
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
// If section+descendants exceed maxLength and there are subsections, return sub-TOC
|
|
242
|
+
if (maxLength && (fullText.length + fullCode.length) > maxLength && descendants.length > 0) {
|
|
243
|
+
const subToc: SectionTocEntry[] = [
|
|
244
|
+
{ heading: sec.heading, level: sec.level, anchor_id: sec.anchor_id, char_count: sec.text.length + sec.code.length, url: `${page.url}#${sec.anchor_id}` },
|
|
245
|
+
...descendants.map((d) => ({ heading: d.heading, level: d.level, anchor_id: d.anchor_id, char_count: d.text.length + d.code.length, url: `${page.url}#${d.anchor_id}` })),
|
|
246
|
+
];
|
|
247
|
+
const totalChars = fullText.length + fullCode.length;
|
|
248
|
+
return {
|
|
249
|
+
id: page.id, title: page.title, path: page.path, url: `${page.url}#${sec.anchor_id}`,
|
|
250
|
+
text: "", code: "",
|
|
251
|
+
word_count: totalWords, code_lines: 0,
|
|
252
|
+
callouts: [], callout_summary: calloutSummary(callouts),
|
|
253
|
+
sections: subToc,
|
|
254
|
+
section: { heading: sec.heading, level: sec.level, anchor_id: sec.anchor_id },
|
|
255
|
+
note: `Section "${sec.heading}" content (${totalChars} chars) exceeds max_length (${maxLength}). Showing ${subToc.length} sub-sections. Re-call with a more specific section heading or anchor_id.`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// If section exceeds maxLength but has no sub-sections, truncate
|
|
260
|
+
if (maxLength && (fullText.length + fullCode.length) > maxLength) {
|
|
261
|
+
const textTotal = fullText.length;
|
|
262
|
+
const codeTotal = fullCode.length;
|
|
263
|
+
const codeBudget = Math.min(codeTotal, Math.floor(maxLength * 0.2));
|
|
264
|
+
const textBudget = maxLength - codeBudget;
|
|
265
|
+
fullText = `${fullText.slice(0, textBudget)}\n\n[... truncated — ${textTotal} chars total, showing first ${textBudget}]`;
|
|
266
|
+
fullCode = codeTotal > codeBudget ? `${fullCode.slice(0, codeBudget)}\n# [... truncated — ${codeTotal} chars total]` : fullCode;
|
|
267
|
+
}
|
|
268
|
+
|
|
240
269
|
return {
|
|
241
270
|
id: page.id,
|
|
242
271
|
title: page.title,
|
|
@@ -258,7 +287,8 @@ export function getPage(idOrTitle: string | number, maxLength?: number, section?
|
|
|
258
287
|
id: page.id, title: page.title, path: page.path, url: page.url,
|
|
259
288
|
text: "", code: "",
|
|
260
289
|
word_count: page.word_count, code_lines: page.code_lines,
|
|
261
|
-
callouts,
|
|
290
|
+
callouts: [], callout_summary: calloutSummary(callouts),
|
|
291
|
+
sections: toc,
|
|
262
292
|
note: `Section "${section}" not found. ${toc.length} sections available — use a heading or anchor_id from the list.`,
|
|
263
293
|
};
|
|
264
294
|
}
|
|
@@ -284,7 +314,8 @@ export function getPage(idOrTitle: string | number, maxLength?: number, section?
|
|
|
284
314
|
id: page.id, title: page.title, path: page.path, url: page.url,
|
|
285
315
|
text: "", code: "",
|
|
286
316
|
word_count: page.word_count, code_lines: page.code_lines,
|
|
287
|
-
callouts,
|
|
317
|
+
callouts: [], callout_summary: calloutSummary(callouts),
|
|
318
|
+
sections: toc,
|
|
288
319
|
truncated: { text_total: text.length, code_total: code.length },
|
|
289
320
|
note: `Page content (${totalChars} chars) exceeds max_length (${maxLength}). Showing table of contents with ${toc.length} sections. Re-call with section parameter to retrieve specific sections.`,
|
|
290
321
|
};
|
|
@@ -303,6 +334,13 @@ export function getPage(idOrTitle: string | number, maxLength?: number, section?
|
|
|
303
334
|
return { id: page.id, title: page.title, path: page.path, url: page.url, text, code, word_count: page.word_count, code_lines: page.code_lines, callouts, ...(truncated ? { truncated } : {}) };
|
|
304
335
|
}
|
|
305
336
|
|
|
337
|
+
/** Build compact callout summary (count + type breakdown) for TOC-mode responses. */
|
|
338
|
+
function calloutSummary(callouts: Array<{ type: string; content: string }>): { count: number; types: Record<string, number> } {
|
|
339
|
+
const types: Record<string, number> = {};
|
|
340
|
+
for (const c of callouts) types[c.type] = (types[c.type] || 0) + 1;
|
|
341
|
+
return { count: callouts.length, types };
|
|
342
|
+
}
|
|
343
|
+
|
|
306
344
|
/** Build section TOC for a page. */
|
|
307
345
|
function getPageToc(pageId: number, pageUrl: string): SectionTocEntry[] {
|
|
308
346
|
const rows = db
|
|
@@ -825,14 +863,14 @@ function getChangelogVersions(): string[] {
|
|
|
825
863
|
return rows.map((r) => r.version).sort(compareVersions);
|
|
826
864
|
}
|
|
827
865
|
|
|
828
|
-
/** Filter versions to those within
|
|
866
|
+
/** Filter versions to those within (fromVersion, toVersion] range (fromVersion exclusive, toVersion inclusive). */
|
|
829
867
|
function filterVersionRange(
|
|
830
868
|
versions: string[],
|
|
831
869
|
fromVersion?: string,
|
|
832
870
|
toVersion?: string,
|
|
833
871
|
): string[] {
|
|
834
872
|
return versions.filter((v) => {
|
|
835
|
-
if (fromVersion && compareVersions(v, fromVersion)
|
|
873
|
+
if (fromVersion && compareVersions(v, fromVersion) <= 0) return false;
|
|
836
874
|
if (toVersion && compareVersions(v, toVersion) > 0) return false;
|
|
837
875
|
return true;
|
|
838
876
|
});
|
package/src/release.test.ts
CHANGED
|
@@ -88,25 +88,27 @@ describe("bin/rosetta.js", () => {
|
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
89
89
|
|
|
90
90
|
describe("build-time constants", () => {
|
|
91
|
-
test("mcp.ts
|
|
91
|
+
test("mcp.ts imports resolveVersion from paths.ts", () => {
|
|
92
92
|
const src = readText("src/mcp.ts");
|
|
93
|
-
expect(src).toContain("
|
|
93
|
+
expect(src).toContain("resolveVersion");
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test("mcp.ts
|
|
96
|
+
test("mcp.ts does not have hardcoded version fallback", () => {
|
|
97
97
|
const src = readText("src/mcp.ts");
|
|
98
|
-
expect(src).toContain("
|
|
98
|
+
expect(src).not.toContain('"0.2.0"');
|
|
99
|
+
expect(src).not.toContain("\"dev\"");
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
test("
|
|
102
|
-
const src = readText("src/
|
|
102
|
+
test("paths.ts declares IS_COMPILED and VERSION", () => {
|
|
103
|
+
const src = readText("src/paths.ts");
|
|
103
104
|
expect(src).toContain("declare const IS_COMPILED");
|
|
105
|
+
expect(src).toContain("declare const VERSION");
|
|
104
106
|
});
|
|
105
107
|
|
|
106
|
-
test("setup.ts declares REPO_URL and
|
|
108
|
+
test("setup.ts declares REPO_URL and imports resolveVersion", () => {
|
|
107
109
|
const src = readText("src/setup.ts");
|
|
108
110
|
expect(src).toContain("declare const REPO_URL");
|
|
109
|
-
expect(src).toContain("
|
|
111
|
+
expect(src).toContain("resolveVersion");
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
test("build script injects all three constants", () => {
|
package/src/setup.ts
CHANGED
|
@@ -1,51 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* setup.ts — Download the RouterOS documentation database and print MCP client config.
|
|
3
3
|
*
|
|
4
|
-
* Called by `rosetta --setup` (compiled binary)
|
|
4
|
+
* Called by `rosetta --setup` (compiled binary), `bunx @tikoci/rosetta --setup`,
|
|
5
|
+
* or `bun run src/setup.ts` (dev).
|
|
5
6
|
* Downloads ros-help.db.gz from the latest GitHub Release, decompresses it,
|
|
6
7
|
* validates the DB, and prints config snippets for each MCP client.
|
|
7
8
|
*/
|
|
8
9
|
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
9
11
|
import { existsSync, writeFileSync } from "node:fs";
|
|
10
|
-
import path from "node:path";
|
|
11
12
|
import { gunzipSync } from "bun";
|
|
13
|
+
import { detectMode, resolveBaseDir, resolveDbPath, resolveVersion } from "./paths.ts";
|
|
12
14
|
|
|
13
15
|
declare const REPO_URL: string;
|
|
14
|
-
declare const VERSION: string;
|
|
15
16
|
|
|
16
17
|
const GITHUB_REPO =
|
|
17
18
|
typeof REPO_URL !== "undefined" ? REPO_URL : "tikoci/rosetta";
|
|
18
|
-
const RELEASE_VERSION =
|
|
19
|
-
typeof VERSION !== "undefined" ? VERSION : "dev";
|
|
20
|
-
|
|
21
|
-
/** Where the binary (or dev project root) lives */
|
|
22
|
-
function getBaseDir(): string {
|
|
23
|
-
// If IS_COMPILED is defined, use the executable's directory
|
|
24
|
-
// Otherwise use project root (one level up from src/)
|
|
25
|
-
try {
|
|
26
|
-
// @ts-expect-error IS_COMPILED defined at build time
|
|
27
|
-
if (typeof IS_COMPILED !== "undefined" && IS_COMPILED) {
|
|
28
|
-
return path.dirname(process.execPath);
|
|
29
|
-
}
|
|
30
|
-
} catch {
|
|
31
|
-
// not compiled
|
|
32
|
-
}
|
|
33
|
-
return path.resolve(import.meta.dirname, "..");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The binary/script path for MCP config */
|
|
37
|
-
function getServerCommand(): string {
|
|
38
|
-
try {
|
|
39
|
-
// @ts-expect-error IS_COMPILED defined at build time
|
|
40
|
-
if (typeof IS_COMPILED !== "undefined" && IS_COMPILED) {
|
|
41
|
-
return process.execPath;
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
// not compiled
|
|
45
|
-
}
|
|
46
|
-
// Dev mode — bun run src/mcp.ts
|
|
47
|
-
return path.resolve(import.meta.dirname, "mcp.ts");
|
|
48
|
-
}
|
|
19
|
+
const RELEASE_VERSION = resolveVersion(import.meta.dirname);
|
|
49
20
|
|
|
50
21
|
/** Check if a DB file exists and has actual page data */
|
|
51
22
|
function dbHasData(dbPath: string): boolean {
|
|
@@ -90,10 +61,8 @@ export async function downloadDb(
|
|
|
90
61
|
}
|
|
91
62
|
|
|
92
63
|
export async function runSetup(force = false) {
|
|
93
|
-
const
|
|
94
|
-
const dbPath =
|
|
95
|
-
const serverCmd = getServerCommand();
|
|
96
|
-
const isCompiled = serverCmd === process.execPath;
|
|
64
|
+
const mode = detectMode(import.meta.dirname);
|
|
65
|
+
const dbPath = resolveDbPath(import.meta.dirname);
|
|
97
66
|
|
|
98
67
|
console.log(`rosetta ${RELEASE_VERSION}`);
|
|
99
68
|
console.log();
|
|
@@ -118,7 +87,8 @@ export async function runSetup(force = false) {
|
|
|
118
87
|
console.log(`✓ Database ready (${row.c} pages, ${cmdRow.c} commands)`);
|
|
119
88
|
} catch (e) {
|
|
120
89
|
console.error(`✗ Database validation failed: ${e}`);
|
|
121
|
-
|
|
90
|
+
const retryCmd = mode === "compiled" ? "rosetta" : mode === "package" ? "bunx @tikoci/rosetta" : "bun run src/setup.ts";
|
|
91
|
+
console.error(` Try re-downloading with: ${retryCmd} --setup --force`);
|
|
122
92
|
process.exit(1);
|
|
123
93
|
}
|
|
124
94
|
|
|
@@ -128,10 +98,21 @@ export async function runSetup(force = false) {
|
|
|
128
98
|
console.log("Configure your MCP client:");
|
|
129
99
|
console.log("─".repeat(60));
|
|
130
100
|
|
|
131
|
-
if (
|
|
132
|
-
printCompiledConfig(
|
|
101
|
+
if (mode === "compiled") {
|
|
102
|
+
printCompiledConfig(process.execPath);
|
|
103
|
+
} else if (mode === "package") {
|
|
104
|
+
printPackageConfig();
|
|
133
105
|
} else {
|
|
134
|
-
printDevConfig(
|
|
106
|
+
printDevConfig(resolveBaseDir(import.meta.dirname));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Try to resolve the absolute path to bunx (for clients that don't inherit PATH) */
|
|
111
|
+
function resolveBunxPath(): string | null {
|
|
112
|
+
try {
|
|
113
|
+
return execSync("which bunx", { encoding: "utf-8" }).trim() || null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
135
116
|
}
|
|
136
117
|
}
|
|
137
118
|
|
|
@@ -175,6 +156,82 @@ function printCompiledConfig(serverCmd: string) {
|
|
|
175
156
|
console.log(` }`);
|
|
176
157
|
console.log(` }`);
|
|
177
158
|
console.log();
|
|
159
|
+
|
|
160
|
+
// Copilot CLI
|
|
161
|
+
console.log("▸ GitHub Copilot CLI");
|
|
162
|
+
console.log(` Inside a copilot session, type /mcp add:`);
|
|
163
|
+
console.log(` Name: routeros-rosetta | Type: STDIO | Command: ${serverCmd}`);
|
|
164
|
+
console.log();
|
|
165
|
+
|
|
166
|
+
// OpenAI Codex
|
|
167
|
+
console.log("▸ OpenAI Codex");
|
|
168
|
+
console.log(` codex mcp add rosetta -- ${serverCmd}`);
|
|
169
|
+
console.log();
|
|
170
|
+
|
|
171
|
+
printHttpConfig(`${serverCmd} --http`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function printPackageConfig() {
|
|
175
|
+
// Resolve full path to bunx — Claude Desktop doesn't inherit shell PATH
|
|
176
|
+
const bunxFullPath = resolveBunxPath();
|
|
177
|
+
|
|
178
|
+
// Claude Desktop
|
|
179
|
+
const isMac = process.platform === "darwin";
|
|
180
|
+
const configPath = isMac
|
|
181
|
+
? "~/Library/Application\\ Support/Claude/claude_desktop_config.json"
|
|
182
|
+
: "%APPDATA%\\Claude\\claude_desktop_config.json";
|
|
183
|
+
|
|
184
|
+
const bunxCmd = bunxFullPath ? JSON.stringify(bunxFullPath) : "\"bunx\"";
|
|
185
|
+
console.log();
|
|
186
|
+
console.log("▸ Claude Desktop");
|
|
187
|
+
console.log(` Edit: ${configPath}`);
|
|
188
|
+
console.log();
|
|
189
|
+
console.log(` {`);
|
|
190
|
+
console.log(` "mcpServers": {`);
|
|
191
|
+
console.log(` "rosetta": {`);
|
|
192
|
+
console.log(` "command": ${bunxCmd},`);
|
|
193
|
+
console.log(` "args": ["@tikoci/rosetta"]`);
|
|
194
|
+
console.log(` }`);
|
|
195
|
+
console.log(` }`);
|
|
196
|
+
console.log(` }`);
|
|
197
|
+
console.log();
|
|
198
|
+
if (bunxFullPath) {
|
|
199
|
+
console.log(` Note: Full path used because Claude Desktop may not inherit shell PATH.`);
|
|
200
|
+
console.log();
|
|
201
|
+
}
|
|
202
|
+
console.log(` Then restart Claude Desktop.`);
|
|
203
|
+
|
|
204
|
+
// Claude Code (inherits PATH — short form is fine)
|
|
205
|
+
console.log();
|
|
206
|
+
console.log("▸ Claude Code");
|
|
207
|
+
console.log(` claude mcp add rosetta -- bunx @tikoci/rosetta`);
|
|
208
|
+
|
|
209
|
+
// VS Code Copilot (inherits PATH)
|
|
210
|
+
console.log();
|
|
211
|
+
console.log("▸ VS Code Copilot (User Settings JSON)");
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(` "mcp": {`);
|
|
214
|
+
console.log(` "servers": {`);
|
|
215
|
+
console.log(` "rosetta": {`);
|
|
216
|
+
console.log(` "command": "bunx",`);
|
|
217
|
+
console.log(` "args": ["@tikoci/rosetta"]`);
|
|
218
|
+
console.log(` }`);
|
|
219
|
+
console.log(` }`);
|
|
220
|
+
console.log(` }`);
|
|
221
|
+
console.log();
|
|
222
|
+
|
|
223
|
+
// Copilot CLI (inherits PATH)
|
|
224
|
+
console.log("▸ GitHub Copilot CLI");
|
|
225
|
+
console.log(` Inside a copilot session, type /mcp add:`);
|
|
226
|
+
console.log(` Name: routeros-rosetta | Type: STDIO | Command: bunx @tikoci/rosetta`);
|
|
227
|
+
console.log();
|
|
228
|
+
|
|
229
|
+
// OpenAI Codex (inherits PATH)
|
|
230
|
+
console.log("▸ OpenAI Codex");
|
|
231
|
+
console.log(` codex mcp add rosetta -- bunx @tikoci/rosetta`);
|
|
232
|
+
console.log();
|
|
233
|
+
|
|
234
|
+
printHttpConfig("bunx @tikoci/rosetta --http");
|
|
178
235
|
}
|
|
179
236
|
|
|
180
237
|
function printDevConfig(baseDir: string) {
|
|
@@ -212,6 +269,38 @@ function printDevConfig(baseDir: string) {
|
|
|
212
269
|
console.log("▸ VS Code Copilot");
|
|
213
270
|
console.log(` The repo includes .vscode/mcp.json — just open the folder in VS Code.`);
|
|
214
271
|
console.log();
|
|
272
|
+
|
|
273
|
+
// Copilot CLI
|
|
274
|
+
console.log("▸ GitHub Copilot CLI");
|
|
275
|
+
console.log(` Inside a copilot session, type /mcp add:`);
|
|
276
|
+
console.log(` Name: routeros-rosetta | Type: STDIO | Command: bun run src/mcp.ts`);
|
|
277
|
+
console.log();
|
|
278
|
+
|
|
279
|
+
// OpenAI Codex
|
|
280
|
+
console.log("▸ OpenAI Codex");
|
|
281
|
+
console.log(` codex mcp add rosetta -- bun run src/mcp.ts`);
|
|
282
|
+
console.log();
|
|
283
|
+
|
|
284
|
+
printHttpConfig(`bun run src/mcp.ts --http`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function printHttpConfig(startCmd: string) {
|
|
288
|
+
console.log("─".repeat(60));
|
|
289
|
+
console.log("Streamable HTTP transport (for HTTP-only MCP clients):");
|
|
290
|
+
console.log("─".repeat(60));
|
|
291
|
+
console.log();
|
|
292
|
+
console.log("▸ Start in HTTP mode");
|
|
293
|
+
console.log(` ${startCmd}`);
|
|
294
|
+
console.log(` ${startCmd} --port 9090`);
|
|
295
|
+
console.log(` ${startCmd} --host 0.0.0.0 # LAN access`);
|
|
296
|
+
console.log(` ${startCmd} --tls-cert cert.pem --tls-key key.pem # HTTPS`);
|
|
297
|
+
console.log();
|
|
298
|
+
console.log("▸ URL-based MCP clients (OpenAI, etc.)");
|
|
299
|
+
console.log(` { "url": "http://localhost:8080/mcp" }`);
|
|
300
|
+
console.log();
|
|
301
|
+
console.log(" For LAN access, replace localhost with the server's IP address.");
|
|
302
|
+
console.log(" Use a reverse proxy (nginx, caddy) for production HTTPS.");
|
|
303
|
+
console.log();
|
|
215
304
|
}
|
|
216
305
|
|
|
217
306
|
// Run directly
|