@tikoci/rosetta 0.2.1 → 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 +88 -2
- package/package.json +1 -1
- package/src/mcp.ts +124 -12
- package/src/query.ts +42 -4
- package/src/setup.ts +25 -0
package/README.md
CHANGED
|
@@ -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 11 search tools over stdio transport.
|
|
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,6 +24,28 @@ The data flows: **HTML docs → SQLite extraction → FTS5 indexes → MCP tools
|
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
|
+
## MCP Discovery Status
|
|
28
|
+
|
|
29
|
+
- GitHub MCP Registry listing: planned
|
|
30
|
+
- Official MCP Registry publication: metadata is now prepared in `server.json`
|
|
31
|
+
|
|
32
|
+
Local install remains the primary path today (`bunx @tikoci/rosetta`).
|
|
33
|
+
|
|
34
|
+
When ready to publish to the official registry:
|
|
35
|
+
|
|
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
|
+
|
|
27
49
|
### Option A: Install with Bun (recommended)
|
|
28
50
|
|
|
29
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.
|
|
@@ -216,6 +238,67 @@ The AI assistant typically starts with `routeros_search`, then drills into speci
|
|
|
216
238
|
| **Windows SmartScreen warning** | Use `bunx` install (no SmartScreen issues), or click **More info → Run anyway** |
|
|
217
239
|
| **How to update** | `bunx` always uses the latest published version. For binaries, re-download from [Releases](https://github.com/tikoci/rosetta/releases/latest). |
|
|
218
240
|
|
|
241
|
+
## HTTP Transport
|
|
242
|
+
|
|
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:
|
|
244
|
+
|
|
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
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Then point your MCP client at the URL:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{ "url": "http://localhost:8080/mcp" }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Key facts:**
|
|
258
|
+
|
|
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.
|
|
265
|
+
|
|
266
|
+
The `PORT`, `HOST`, `TLS_CERT_PATH`, and `TLS_KEY_PATH` environment variables are supported (lower precedence than CLI flags).
|
|
267
|
+
|
|
268
|
+
## Container Images
|
|
269
|
+
|
|
270
|
+
Release CI publishes multi-arch OCI images to:
|
|
271
|
+
|
|
272
|
+
- Docker Hub: `ammo74/rosetta`
|
|
273
|
+
- GHCR: `ghcr.io/tikoci/rosetta`
|
|
274
|
+
|
|
275
|
+
Tags per release:
|
|
276
|
+
|
|
277
|
+
- `${version}` (example: `v0.2.1`)
|
|
278
|
+
- `latest`
|
|
279
|
+
- `sha-<12-char-commit>`
|
|
280
|
+
|
|
281
|
+
Container defaults:
|
|
282
|
+
|
|
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
|
|
286
|
+
|
|
287
|
+
Examples:
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
docker run --rm -p 8080:8080 ghcr.io/tikoci/rosetta:latest
|
|
291
|
+
```
|
|
292
|
+
|
|
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
|
+
```
|
|
301
|
+
|
|
219
302
|
## Building from Source
|
|
220
303
|
|
|
221
304
|
For contributors or when you have access to the MikroTik HTML documentation export.
|
|
@@ -276,11 +359,13 @@ make release VERSION=v0.1.0
|
|
|
276
359
|
|
|
277
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.
|
|
278
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
|
+
|
|
279
364
|
## Project Structure
|
|
280
365
|
|
|
281
366
|
```text
|
|
282
367
|
src/
|
|
283
|
-
├── mcp.ts # MCP server (11 tools, stdio) + CLI dispatch
|
|
368
|
+
├── mcp.ts # MCP server (11 tools, stdio + HTTP) + CLI dispatch
|
|
284
369
|
├── setup.ts # --setup: DB download + MCP client config
|
|
285
370
|
├── query.ts # NL → FTS5 query planner, BM25 ranking
|
|
286
371
|
├── db.ts # SQLite schema, WAL mode, FTS5 triggers
|
|
@@ -294,6 +379,7 @@ src/
|
|
|
294
379
|
|
|
295
380
|
scripts/
|
|
296
381
|
└── build-release.ts # Cross-compile + package releases
|
|
382
|
+
└── container-entrypoint.sh # OCI image runtime entrypoint (HTTP default)
|
|
297
383
|
```
|
|
298
384
|
|
|
299
385
|
## Data Sources
|
package/package.json
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -9,10 +9,19 @@
|
|
|
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
27
|
import { resolveVersion } from "./paths.ts";
|
|
@@ -23,6 +32,12 @@ const RESOLVED_VERSION = resolveVersion(import.meta.dirname);
|
|
|
23
32
|
|
|
24
33
|
const args = process.argv.slice(2);
|
|
25
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
|
+
|
|
26
41
|
if (args.includes("--version") || args.includes("-v")) {
|
|
27
42
|
console.log(`rosetta ${RESOLVED_VERSION}`);
|
|
28
43
|
process.exit(0);
|
|
@@ -33,13 +48,24 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
33
48
|
console.log();
|
|
34
49
|
console.log("Usage:");
|
|
35
50
|
console.log(" rosetta Start MCP server (stdio transport)");
|
|
51
|
+
console.log(" rosetta --http Start with Streamable HTTP transport");
|
|
36
52
|
console.log(" rosetta --setup Download database + print MCP client config");
|
|
37
53
|
console.log(" rosetta --setup --force Re-download database");
|
|
38
54
|
console.log(" rosetta --version Print version");
|
|
39
55
|
console.log(" rosetta --help Print this help");
|
|
40
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();
|
|
41
63
|
console.log("Environment:");
|
|
42
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)");
|
|
43
69
|
process.exit(0);
|
|
44
70
|
}
|
|
45
71
|
|
|
@@ -54,8 +80,9 @@ if (args.includes("--setup")) {
|
|
|
54
80
|
|
|
55
81
|
// ── MCP Server ──
|
|
56
82
|
|
|
83
|
+
const useHttp = args.includes("--http");
|
|
84
|
+
|
|
57
85
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
58
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
59
86
|
const { z } = await import("zod/v3");
|
|
60
87
|
|
|
61
88
|
// Dynamic imports — db.ts eagerly opens the DB file on import,
|
|
@@ -175,19 +202,17 @@ Use after routeros_search identifies a relevant page. Pass the numeric page ID
|
|
|
175
202
|
Returns: plain text, code blocks, and callout blocks (notes, warnings, info, tips).
|
|
176
203
|
Callouts contain crucial caveats and edge-case details — always review them.
|
|
177
204
|
|
|
178
|
-
**Large page handling:**
|
|
179
|
-
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,
|
|
180
206
|
pages with sections return a **table of contents** instead of truncated text.
|
|
181
207
|
The TOC lists each section's heading, hierarchy level, character count, and
|
|
182
208
|
deep-link URL. Re-call with the section parameter to retrieve specific sections.
|
|
183
209
|
|
|
184
210
|
**Section parameter:** Pass a section heading or anchor_id (from the TOC)
|
|
185
|
-
to get that section's content.
|
|
186
|
-
|
|
187
|
-
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.
|
|
188
213
|
|
|
189
214
|
Recommended workflow for large pages:
|
|
190
|
-
1.
|
|
215
|
+
1. First call → get TOC if page is large (automatic with default max_length)
|
|
191
216
|
2. Review section headings to find the relevant section
|
|
192
217
|
3. Call again with section="Section Name" to get that section's content
|
|
193
218
|
|
|
@@ -204,12 +229,12 @@ Workflow — what to do with this content:
|
|
|
204
229
|
.number()
|
|
205
230
|
.int()
|
|
206
231
|
.min(1000)
|
|
207
|
-
.
|
|
208
|
-
.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."),
|
|
209
234
|
section: z
|
|
210
235
|
.string()
|
|
211
236
|
.optional()
|
|
212
|
-
.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)."),
|
|
213
238
|
},
|
|
214
239
|
},
|
|
215
240
|
async ({ page, max_length, section }) => {
|
|
@@ -739,7 +764,94 @@ Requires network access to upgrade.mikrotik.com.`,
|
|
|
739
764
|
|
|
740
765
|
// ---- Start ----
|
|
741
766
|
|
|
742
|
-
|
|
743
|
-
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
|
+
}
|
|
744
856
|
|
|
745
857
|
})();
|
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
|
package/src/setup.ts
CHANGED
|
@@ -167,6 +167,8 @@ function printCompiledConfig(serverCmd: string) {
|
|
|
167
167
|
console.log("▸ OpenAI Codex");
|
|
168
168
|
console.log(` codex mcp add rosetta -- ${serverCmd}`);
|
|
169
169
|
console.log();
|
|
170
|
+
|
|
171
|
+
printHttpConfig(`${serverCmd} --http`);
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
function printPackageConfig() {
|
|
@@ -228,6 +230,8 @@ function printPackageConfig() {
|
|
|
228
230
|
console.log("▸ OpenAI Codex");
|
|
229
231
|
console.log(` codex mcp add rosetta -- bunx @tikoci/rosetta`);
|
|
230
232
|
console.log();
|
|
233
|
+
|
|
234
|
+
printHttpConfig("bunx @tikoci/rosetta --http");
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
function printDevConfig(baseDir: string) {
|
|
@@ -276,6 +280,27 @@ function printDevConfig(baseDir: string) {
|
|
|
276
280
|
console.log("▸ OpenAI Codex");
|
|
277
281
|
console.log(` codex mcp add rosetta -- bun run src/mcp.ts`);
|
|
278
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();
|
|
279
304
|
}
|
|
280
305
|
|
|
281
306
|
// Run directly
|