Skip to content

mcp: open shared dashboard on demand, drop the dead redirect#1333

Merged
Andrew Gazelka (andrewgazelka) merged 1 commit into
mainfrom
dashboard-launcher
Jun 18, 2026
Merged

mcp: open shared dashboard on demand, drop the dead redirect#1333
Andrew Gazelka (andrewgazelka) merged 1 commit into
mainfrom
dashboard-launcher

Conversation

@andrewgazelka

@andrewgazelka Andrew Gazelka (andrewgazelka) commented Jun 18, 2026

Copy link
Copy Markdown
Member

Problem

Opening the kernel's DASHBOARD_URL gave ERR_CONNECTION_REFUSED.

The per-session data API's / handler unconditionally 302'd to config.hub_url() -- a free port reserved at startup (_free_port()) but never bound, because the human dashboard hub is not auto-spawned per session by default (that was the deliberate fix for "a new dashboard URL per MCP connection"). So the courtesy redirect always pointed at a dead port.

There was also no easy way to just open the one shared dashboard.

Change (Python only -- no Rust rebuild)

  • Kill the dead redirect (dashboard.py): / redirects to the shared hub only when one is actually running (recorded in runtime_dir()/hub.json, confirmed live via a TCP probe in config.live_hub()), otherwise serves a small page naming ix-mcp dashboard. The probe runs via asyncio.to_thread so it never blocks the shared event loop.
  • Singleton launcher (cli.py ix-mcp dashboard): reuse a running hub or spawn one detached, then print the URL and open the browser. Repeated runs reuse the same hub -- no pile-up.
  • Bind like the data API: the hub binds IX_MCP_HOST / the tailnet IP / loopback (honoring an operator's loopback pin), never 0.0.0.0. Stable port 8080, override IX_DASH_HUB_PORT.
  • config.py: hub_state_path(), port_open(), live_hub().

Validation

  • nix build .#mcp and .#mcp.tests.dashboardLauncherSmoke (new) green; strictTypecheck + ruff clean.
  • New smoke covers live_hub (missing / stale-dead-port / live), the landing page, and the real aiohttp / handler (302 to live hub, 200 landing otherwise).
  • End-to-end: ix-mcp dashboard spawns once, reuses on repeat (single process), hub serves 200; bind honors IX_MCP_HOST.

Notes

  • To join from another machine, run on a host where Tailscale is up and IX_MCP_HOST is not pinned to loopback (the hub then binds the tailnet IP), or set IX_MCP_HOST/IX_DASH_HUB_PORT.
  • Out of scope: cleanup of stale ix_notebook_mcp serve sockets/processes from old sessions.

Reviewed by the code-reviewer agent; its correctness blocker (blocking socket call on the event loop) and security finding (wildcard bind) are both fixed above.

Authored by Claude (Sonnet) via Claude Code.

Note

Open a shared dashboard hub on demand from ix-mcp dashboard, dropping dead redirects

  • ix-mcp dashboard now spawns or reuses a single shared hub process, persisting state in hub.json (pid, host, port, url) and using an fcntl file lock to serialize concurrent launches.
  • New helpers in cli.py resolve the bind IP (no wildcard), stable port (default 8080 or IX_DASH_HUB_PORT), and IPv6 bracketing for host:port strings.
  • live_hub() in config.py validates a recorded hub by checking pid liveness and TCP reachability before trusting it.
  • The / index handler in dashboard.py now only redirects to a verified live hub; otherwise it serves a static landing page pointing users to ix-mcp dashboard.
  • A --no-open flag suppresses browser launch; log messages and docs updated to reference ix-mcp dashboard instead of nix run .#dashboard.

Macroscope summarized eac5f75.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Blast radius

63 of 1486 checks would rebuild between base 8bd7c19 and head 16b9b3a.

1 added, 0 removed

pie showData title Rebuilt checks by category
  "rust" : 42
  "image" : 15
  "site" : 2
  "agent" : 1
  "blast" : 1
  "eval" : 1
  "lint" : 1
Loading
flowchart LR
  c0["ix-notebook-mcp-module"]
  c1["blast-radius-test"]
  c2["agent-skills"]
  c3["lint"]
  c4["rust-mcp.viewSmoke"]
  c5["site-test"]
  c0 --> k0["agent-skills"]
  c0 --> k2["eval"]
  c0 --> k3["image-development-base"]
  c0 --> k4["image-kernel-dev"]
  c0 --> k5["image-minecraft"]
Loading
changed checks (62)
  • agent-skills
  • blast-radius-test
  • eval
  • image-development-base
  • image-kernel-dev
  • image-minecraft
  • image-minecraft-bedrock
  • image-minecraft-status
  • image-minecraft_1.21.11-fabric
  • image-minecraft_1.21.11-paper
  • image-minecraft_26.1.2-fabric
  • image-minecraft_26.1.2-paper
  • image-minecraft_26w17a-fabric
  • image-minestom
  • image-neovim-ci
  • image-remote-desktop
  • image-symphony-codex
  • image-test-cluster-bootstrap
  • lint
  • rust-mcp.apiSmoke
  • rust-mcp.astlogBundled
  • rust-mcp.beeperBundled
  • rust-mcp.bindDefaultSmoke
  • rust-mcp.bindingsSmoke
  • rust-mcp.browserSmoke
  • rust-mcp.browserVdomSmoke
  • rust-mcp.dataLibsBundled
  • rust-mcp.engineBundled
  • rust-mcp.evalSmoke
  • rust-mcp.exaBundled
  • rust-mcp.feedSmoke
  • rust-mcp.fffBundled
  • rust-mcp.fleetClusterSmoke
  • rust-mcp.fleetSmoke
  • rust-mcp.gmailLibsBundled
  • rust-mcp.googleAuthBundled
  • rust-mcp.htpyBundled
  • rust-mcp.iphoneBundled
  • rust-mcp.ixGoogleBundled
  • rust-mcp.linearBundled
  • rust-mcp.nixSmoke
  • rust-mcp.noxAutotriageBundled
  • rust-mcp.requirementsSmoke
  • rust-mcp.richSmoke
  • rust-mcp.runtimeSmoke
  • rust-mcp.searchBundled
  • rust-mcp.serverTools
  • rust-mcp.sessionIdentitySmoke
  • rust-mcp.sessionSmoke
  • rust-mcp.shSmoke
  • rust-mcp.slackBundled
  • rust-mcp.sshAuthSockSmoke
  • rust-mcp.strictTypecheck
  • rust-mcp.tuiBundled
  • rust-mcp.vdomPropertiesSmoke
  • rust-mcp.viewSmoke
  • rust-mcp.wedgeSmoke
  • rust-mcp.worktreeSmoke
  • rust-mcp.xBundled
  • rust-mcp.yieldSmoke
  • site-case-tests
  • site-test

@andrewgazelka Andrew Gazelka (andrewgazelka) force-pushed the dashboard-launcher branch 3 times, most recently from 9febb93 to 1803a2b Compare June 18, 2026 06:33

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review found issues in this pull request.

Verdict: patch is incorrect
Confidence: 0.86

The shared dashboard launcher has correctness gaps in its process lifecycle and liveness detection: it can leak detached dashboards on failed readiness, can treat an unrelated reused port as the singleton hub, and fails for hostname bind values accepted elsewhere by the CLI.

  • P1 packages/mcp/ix_notebook_mcp/cli.py:657 Timeout path leaks the detached dashboard process
  • P2 packages/mcp/ix_notebook_mcp/config.py:172 Stale state can be mistaken for a live dashboard after port reuse
  • P2 packages/mcp/ix_notebook_mcp/cli.py:633 Hostname IX_MCP_HOST values cannot start the Rust dashboard

Comment thread packages/mcp/ix_notebook_mcp/cli.py
Comment thread packages/mcp/ix_notebook_mcp/config.py
Comment thread packages/mcp/ix_notebook_mcp/cli.py Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review found issues in this pull request.

Verdict: patch is incorrect
Confidence: 0.88

The shared-dashboard launcher mostly follows the intended shape, but the liveness check can reuse stale state for an unrelated listener, and the new root handler breaks the documented auto-dashboard compatibility path.

  • P1 packages/mcp/ix_notebook_mcp/config.py:172 Stale hub state can be mistaken for a live dashboard
  • P2 packages/mcp/ix_notebook_mcp/dashboard.py:73 Auto-dashboard mode no longer redirects to its spawned hub

Comment thread packages/mcp/ix_notebook_mcp/config.py
Comment thread packages/mcp/ix_notebook_mcp/dashboard.py
@andrewgazelka Andrew Gazelka (andrewgazelka) added this pull request to the merge queue Jun 18, 2026
@andrewgazelka Andrew Gazelka (andrewgazelka) removed this pull request from the merge queue due to a manual request Jun 18, 2026
@andrewgazelka Andrew Gazelka (andrewgazelka) force-pushed the dashboard-launcher branch 3 times, most recently from da67a9c to 6091503 Compare June 18, 2026 07:30

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review found issues in this pull request.

Verdict: patch is incorrect
Confidence: 0.88

The shared dashboard launcher has binding/advertisement regressions: it can expose the global dashboard on wildcard interfaces and can publish an unreachable URL after falling back to loopback.

  • P1 packages/mcp/ix_notebook_mcp/cli.py:646 Shared dashboard can bind to every interface
  • P2 packages/mcp/ix_notebook_mcp/cli.py:661 Fallback bind still advertises the unusable requested host

Comment thread packages/mcp/ix_notebook_mcp/cli.py
Comment thread packages/mcp/ix_notebook_mcp/cli.py Outdated
@andrewgazelka Andrew Gazelka (andrewgazelka) force-pushed the dashboard-launcher branch 2 times, most recently from 72e39f2 to 5cd18d7 Compare June 18, 2026 07:42
@andrewgazelka Andrew Gazelka (andrewgazelka) added this pull request to the merge queue Jun 18, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review found issues in this pull request.

Verdict: patch is incorrect
Confidence: 0.78

The shared dashboard launcher mostly follows the intended singleton behavior, but it regresses configured IPv6 hosts by passing them through in a form the dashboard binary cannot parse.

  • P2 packages/mcp/ix_notebook_mcp/cli.py:625 IPv6 hosts are passed to the dashboard binary in an unparsable form

Comment thread packages/mcp/ix_notebook_mcp/cli.py
The per-session data API root unconditionally 302'd to config.hub_url(), a
free port reserved at startup but never bound when no per-server hub is
auto-spawned (the default). Opening DASHBOARD_URL therefore redirected
straight into a refused connection.

Now / redirects to the shared hub only when one is actually running (recorded
in runtime_dir()/hub.json, port-probed via config.live_hub off the event
loop), and otherwise serves a short page naming the command. Add a singleton
launcher: the dashboard subcommand reuses a running hub or spawns one detached
on a stable port (8080, IX_DASH_HUB_PORT) bound to the tailnet IP or
IX_MCP_HOST, else loopback (never 0.0.0.0), then opens it, so repeated runs
never pile up dashboards.
@andrewgazelka Andrew Gazelka (andrewgazelka) removed this pull request from the merge queue due to a manual request Jun 18, 2026
@andrewgazelka Andrew Gazelka (andrewgazelka) added this pull request to the merge queue Jun 18, 2026
Merged via the queue into main with commit 8b1cbb6 Jun 18, 2026
18 of 19 checks passed
@andrewgazelka Andrew Gazelka (andrewgazelka) deleted the dashboard-launcher branch June 18, 2026 08:03
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.

1 participant