Skip to content

tron automate: Obscura-first fetch router, and the hosted OpenMCP relay at tronbrowser.dev/mcp/tron - #97

Merged
ralyodio merged 2 commits into
mainfrom
feat/tron-automate
Sep 13, 2026
Merged

tron automate: Obscura-first fetch router, and the hosted OpenMCP relay at tronbrowser.dev/mcp/tron#97
ralyodio merged 2 commits into
mainfrom
feat/tron-automate

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

One MCP server with an engine switch, local and hosted.

Local: tron automate. Obscura renders first (its own engine + V8: tens of milliseconds and ~30 MB on a light page); the Chromium session takes over for JS-heavy pages, bot walls and logins, where a warm Chromium is 3 to 10x faster than Obscura. Measured on the same pages before building (Obscura 63 ms on example.com vs Chrome 774 ms cold; Chrome 1.1 s on a GitHub page vs Obscura 17 s).

  • tron automate (stdio MCP), tron automate serve (HTTP + OpenMCP descriptor), tron automate fetch <url>, tron automate status
  • fetch_page (markdown|text|links|html, engine=auto|obscura|chromium, budget, wall/empty detection) and engine_status; tron mcp gains fetch_page too
  • The installer downloads the pinned Obscura release next to the launcher (two ~100 MB binaries, so not in the tarball); ensure-obscura, TB_NO_OBSCURA_INSTALL, TRONBROWSER_OBSCURA_VERSION
  • DirectChromium: the SDK can spawn a Chromium binary itself (server, container, box without the launcher)

Hosted: tronbrowser.dev/mcp/tron. The same router in the API as a public OpenMCP relay, keyless and stateless: fetch_page + screenshot_page only, a fresh tab per call, public targets only (name and resolved address), 3 concurrent, 30/min per caller, 1 MiB body, 400k chars, 45 s Obscura budget. Descriptor at /.well-known/openmcp.json (static, served by Caddy, so the catalog marks it verified).

The Dockerfile moves to bookworm with Caddy copied in, builds the API as a pnpm workspace member, and bakes in ungoogled-chromium 152.0.7977.82-1 (portable Linux build) and Obscura 0.2.2. Caddy proxies /mcp/* to the API.

Why stdio for Obscura

Obscura's HTTP MCP transport answers the first request on a connection in under 60 ms and then stalls every later request on that keep-alive connection by ~4 s. Over stdio there is no stall. (The house relays that talk to Obscura over HTTP may be paying this per tool call; separate follow-up.)

Tests

pnpm lint, pnpm typecheck, pnpm test all green (sdk 50, api 79, web 20). Live checks on the dev box: tron automate fetch through both engines, tron automate serve handshake + descriptor, and services/api/scripts/mcp-smoke.mjs against the relay (Obscura fetch, Chromium fetch, links, private target refused, PNG screenshot, no tab left open).

After merge

  1. Railway builds the new image (bigger: Chromium + Obscura, ~1 GB) and deploys.
  2. Check https://tronbrowser.dev/mcp/tron and https://tronbrowser.dev/.well-known/openmcp.json.
  3. Register: openmcp add https://tronbrowser.dev (or POST https://openmcp.logicsrc.com/v1/relays {"url":"https://tronbrowser.dev"}).
  4. Tag a release so the installer and tron automate reach users.

🤖 Generated with Claude Code

https://claude.ai/code/session_018WGZpo8on6XLGYnccXV3dP

…sted OpenMCP relay at /mcp/tron

`tron automate` is one MCP server with an engine switch: Obscura (its own
renderer + V8, tens of ms and ~30 MB on a light page) renders first, and the
Chromium session takes over for JS-heavy pages, bot walls and logins, where a
warm Chromium is 3-10x faster. Measured on the same pages before deciding:
Obscura wins cold start and tiny pages, loses badly on GitHub/Wikipedia.

SDK (packages/sdk/src/mcp):
- obscura.ts: MCP client driving `obscura mcp` over stdio. Stdio on purpose:
  Obscura's HTTP transport stalls every request after the first on a
  keep-alive connection by ~4 s.
- automate.ts: fetch_page (markdown|text|links|html, engine=auto|obscura|
  chromium, budget, wall/empty detection with fallback) + engine_status; an
  in-page markdown walker for the Chromium path.
- chromium.ts: DirectChromium spawns a Chromium binary itself (headless,
  throwaway profile, kernel-picked DevTools port, isolated tab per call, idle
  shutdown, private-host resolver rules) for servers and boxes without the
  launcher.
- http.ts: Streamable-HTTP transport (plain JSON, batches, 202 for
  notifications, optional bearer) + the OpenMCP descriptor at
  /.well-known/openmcp.json.
- automate-bin.ts: `tron automate [mcp|serve|fetch|status]`; `tron mcp` gains
  fetch_page too.

CLI/installer (apps/web/public/install.sh): `tron automate` case; the installer
downloads the pinned Obscura release (two ~100 MB binaries, so not in the
tarball) next to the launcher on install and upgrade, `ensure-obscura`
subcommand, TB_NO_OBSCURA_INSTALL / TRONBROWSER_OBSCURA_VERSION.

Hosted relay (services/api/src/mcp): tronbrowser.dev/mcp/tron, keyless and
stateless (fetch_page + screenshot_page only), fresh tab per call, public
targets only (name and resolved address), 3 concurrent, 30/min per caller,
1 MiB body, 400k chars, 45 s Obscura budget. Static descriptor at
/.well-known/openmcp.json lists it on openmcp.logicsrc.com once registered.
Dockerfile moves to bookworm with Caddy copied in, builds the API as a pnpm
workspace member, and bakes in ungoogled-chromium 152.0.7977.82-1 (portable
Linux build) and Obscura 0.2.2. Caddy proxies /mcp/* to the API.

Tests: router (fallback on wall/error/timeout/missing, pinned engines, formats,
truncation), Obscura client, HTTP transport + descriptor, DirectChromium (live
half skips without a browser), CLI heredoc, installer hooks, relay guard
(private ranges, DNS rebinding, rate limiter) and routes. Smoke script:
services/api/scripts/mcp-smoke.mjs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WGZpo8on6XLGYnccXV3dP
info "Installed Obscura $OBSCURA_VERSION to $obdest"
return 0
fi
warn "Couldn't install Obscura; 'tron automate' will use Chromium for every page. Retry with: curl -fsSL $INSTALL_URL | sh -s -- ensure-obscura"
expect(src.startsWith('(() => {')).toBe(true);
expect(src.trimEnd().endsWith('})()')).toBe(true);
// It must parse as JavaScript; a syntax slip here breaks every Chromium markdown fetch.
expect(() => new Function('return ' + src)).not.toThrow();
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

51 finding(s)

MEDIUM: 32 | LOW: 19

Severity Rule Location
MEDIUM js-open-redirect apps/desktop/extensions/ai-sidebar/install-helper.js:156
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:34
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:57
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:237
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:266
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:336
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/options.js:305
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:77
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:165
MEDIUM sh-remote-script-execution apps/desktop/launcher/tronbrowser:122
MEDIUM sh-remote-script-execution apps/desktop/launcher/tronbrowser:432
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:77
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:227
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:569
MEDIUM js-unescaped-html-sink apps/web/public/app.js:29
MEDIUM js-unescaped-html-sink apps/web/public/dns.js:54
MEDIUM sh-remote-script-execution apps/web/public/install.sh:161
MEDIUM sh-remote-script-execution apps/web/public/install.sh:166
MEDIUM sh-remote-script-execution apps/web/public/install.sh:279
MEDIUM sh-remote-script-execution apps/web/public/install.sh:297
MEDIUM sh-remote-script-execution apps/web/public/install.sh:704
MEDIUM sh-remote-script-execution apps/web/public/install.sh:985
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:96
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:168
MEDIUM js-dynamic-code-execution packages/sdk/src/mcp/tools.ts:80
MEDIUM js-dynamic-code-execution packages/sdk/src/mcp/tools.ts:92
MEDIUM js-dynamic-code-execution packages/sdk/src/mcp/tools.ts:103
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:65
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:70
MEDIUM sql-template-interpolation services/api/src/store/db.ts:116
MEDIUM js-dynamic-code-execution services/api/src/store/scanner.ts:44
MEDIUM sh-predictable-temp-path start.sh:25
LOW js-dynamic-code-execution packages/agent-runtime/src/analyze/form-script.test.ts:6
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:26
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:49
LOW js-dynamic-code-execution packages/browser-core/src/automation/extract-script.test.ts:6
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:35
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:47
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:70
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:89
LOW js-dynamic-code-execution packages/browser-core/src/automation/snapshot-script.test.ts:11
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:24
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:63
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:47
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:170
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:216
LOW js-dynamic-code-execution packages/sdk/src/mcp/automate.test.ts:258
LOW secret-generic-api-key packages/storage/src/config.ts:51
LOW secret-generic-credential packages/storage/src/config.ts:51
LOW js-dynamic-code-execution services/api/src/store/scanner.test.ts:32

…and 1 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

…VE_TEST=1)

GitHub runners carry a Chrome that cannot start under the default sandbox, so
'binary present' picked the live half up in CI and it timed out. The live
test now runs only when asked for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WGZpo8on6XLGYnccXV3dP
@ralyodio
ralyodio merged commit 611d8e4 into main Sep 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants