@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/rosetta",
3
- "version": "0.2.1",
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",
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:** Always set max_length (e.g., 30000) on the first call.
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. Parent sections automatically include all
186
- sub-section content, so requesting a top-level heading gives you everything
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. Call with max_length=30000 → get TOC if page is large
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
- .optional()
208
- .describe("Recommended: set to 30000. Max combined text+code length. If exceeded and page has sections, returns a TOC instead of truncated text. Omit only if you need the entire page."),
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
- const transport = new StdioServerTransport();
743
- await server.connect(transport);
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, sections: toc,
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, sections: toc,
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