Skip to content

feat(mcp): add hosted HTTP server and installer wizard - #69

Merged
milstan merged 37 commits into
mainfrom
ArtyETH06/https-github.com-leadbay-product-issues-3651
May 30, 2026
Merged

feat(mcp): add hosted HTTP server and installer wizard#69
milstan merged 37 commits into
mainfrom
ArtyETH06/https-github.com-leadbay-product-issues-3651

Conversation

@ArtyETH06

@ArtyETH06 ArtyETH06 commented May 26, 2026

Copy link
Copy Markdown
Contributor

Why

This PR makes the Leadbay MCP easy to install locally and usable from hosted-MCP clients like ChatGPT Desktop. The main install path now uses Leadbay OAuth instead of asking users for email/password or making them paste tokens.

Supported paths:

  1. Guided Electron installer launched from npm on macOS, Windows, and Linux.
  2. CLI installer for terminal users and automation.
  3. Local config install for Claude Desktop, Claude Code, Cursor, and Codex.
  4. Hosted MCP URL for ChatGPT Desktop / remote-MCP clients.

What Changed

OAuth installer flow

  • The guided installer starts with Sign in with Leadbay.
  • OAuth opens in the browser, returns to the local installer, then the user chooses where to install.
  • Removed email/region/password fields from the GUI install flow.
  • Added leadbay-mcp install --oauth for terminal users.
  • Updated missing-auth hints and README docs to point to OAuth.

Installer wizard

  • Added Codex install support.
  • Detects OS and installed clients.
  • Only shows clients whose real app/binary is installed.
  • Supports Claude Code, Claude Desktop, Cursor, Codex, and ChatGPT Desktop.
  • Claude Desktop writes claude_desktop_config.json with npx -y @leadbay/mcp@latest.
  • ChatGPT Desktop is explicit manual setup via the hosted MCP URL.
  • Added Windows Store detection for ChatGPT Desktop (OpenAI.ChatGPT-Desktop).
  • Uninstaller only removes Leadbay-owned entries/blocks.
  • Config version pinned to @latest so installs always pick up the newest release.
npx -y -p @leadbay/mcp@latest installer
npx -y -p @leadbay/mcp@latest installer --uninstall

Installer logic moved into installer/

All installer logic has been extracted out of src/bin.ts into dedicated files under installer/:

File Contents
installer/install-shared.ts Client detection, DetectedClient type
installer/install-claude-code.ts Claude Code install/uninstall, runClaudeMcp
installer/install-json-config.ts JSON config install/uninstall, stripJsonMcpEntry
installer/install-codex.ts Codex TOML + shell exports install/uninstall
installer/install-dxt.ts DXT extension removal
installer/install-wizard.ts TUI wizard (parseInstallSelection, chooseInstallClients, …)
installer/installer-gui.ts HTTP GUI server
installer/installer-electron.ts Electron launcher

src/bin.ts is now a thin MCP server entrypoint + CLI dispatcher. It re-exports all installer symbols from the new files so existing test imports (../../src/bin.js) continue to work without any test file changes.

Config path fix (macOS spaces)

DetectedClient now carries a dedicated configPath field (the real filesystem path) alongside detail (the display string). Previously all callers used client.detail.split(" ")[0] which truncated macOS paths containing spaces (~/Library/Application Support/Claude/...~/Library/Application). All install and uninstall callsites now use client.configPath directly.

DXT extension removal on Claude Desktop

When Claude Desktop 2026 (with DXT support) is detected via its marker files (Claude Extensions/, extensions-installations.json, config.json with dxt:* keys), the installer now removes the Leadbay DXT extension and falls through to the normal claude_desktop_config.json write:

  1. Deletes Claude Extensions/local.dxt.leadbay.leadbay/ directory.
  2. Removes the local.dxt.leadbay.leadbay entry from extensions-installations.json.
  3. Writes mcpServers.leadbay to claude_desktop_config.json as usual.

Browser fallback uninstall fix

runBrowserFallback() in installer-electron.ts previously always launched the installer GUI, so --uninstall --browser and no-Electron uninstall flows opened the install wizard instead of removing config. Fixed to branch on --uninstall and call startUninstallerGui().

Legacy SSE double-write fix

/sse and /messages now return { "x-hono-already-sent": "1" } (the same sentinel as /mcp) instead of a bare new Response(null, { status: 200 }). The previous response caused Hono's Node adapter to attempt a second header write after SSEServerTransport and handlePostMessage had already written headers and body, producing Cannot write headers after they are sent to the client.

Hosted MCP server

  • Added a Hono HTTP server entrypoint for the MCP.
  • Exposes Streamable HTTP at POST /mcp.
  • Keeps legacy SSE support with GET /sse and POST /messages.
  • Adds GET /healthz for Fly health checks.
  • Resolves auth per request/session from Authorization: Bearer <token>.
  • Supports X-Leadbay-Region: us|fr.

How To Test Before Merge

Local installer app

pnpm --filter @leadbay/mcp installer
pnpm --filter @leadbay/mcp installer -- --uninstall

macOS path smoke test

On macOS, after install, verify ~/Library/Application Support/Claude/claude_desktop_config.json contains mcpServers.leadbay (not a truncated path like ~/Library/Application).

DXT removal

With a Claude Desktop 2026 machine (DXT markers present):

node packages/mcp/dist/bin.js installer
# Expected: "DXT extension removed; updated" for Claude Desktop
# extensions-installations.json should have no leadbay entry
# claude_desktop_config.json should have mcpServers.leadbay written

Uninstall via browser fallback

node packages/mcp/dist/installer-electron.js --uninstall --browser
# Expected: uninstaller GUI opens, not the installer wizard

Legacy SSE

curl -N http://localhost:<PORT>/sse -H "Authorization: Bearer $LEADBAY_TOKEN"
# Expected: stream stays open, no "Cannot write headers" error in server logs

Validation

  • pnpm -r build && pnpm -r test — 355 tests pass, no test file modifications
  • pnpm --filter @leadbay/mcp typecheck
  • node --check packages/mcp/dist/bin.js
  • node --check packages/mcp/dist/installer-electron.js
  • node --check packages/mcp/dist/installer-gui.js

Refs #3651.

closes https://github.com/leadbay/product/issues/3651

Adds a Hono-based HTTP entry alongside the existing stdio server so
Leadbay MCP can be deployed as a public endpoint (ChatGPT custom
connectors, web clients) without changing the tool catalog or auth model.

- New: packages/mcp/src/http-server.ts — Hono app exposing
  POST /mcp (Streamable HTTP, current MCP spec), GET /sse + POST /messages
  (legacy SSE), and GET /healthz for Fly liveness.
- New: packages/mcp/src/auth-http.ts — per-request bearer-token resolver.
  Mirrors resolveClientFromEnv's probe + broken-client logic without
  touching the stdio code path; each session gets its own LeadbayClient.
- New: Dockerfile (multi-stage, Node 22-slim, ~548 MB) + fly.toml
  (Paris primary, auto-stop, /healthz check).
- Adds hono + @hono/node-server runtime deps and leadbay-mcp-http bin.

Stdio path is unchanged. All 251 existing tests pass.
@ArtyETH06 ArtyETH06 self-assigned this May 26, 2026
@ArtyETH06 ArtyETH06 changed the title feat(mcp): self-hosted HTTP server for ChatGPT custom connectors feat(mcp): add hosted HTTP server and installer wizard May 27, 2026
@ArtyETH06

Copy link
Copy Markdown
Contributor Author

Installer status checkpoint:\n\n- Linux: command-only installer, no desktop artifact:\n\nbash\nnpx -y @leadbay/mcp@latest installer\n\n\n- macOS installer artifact: https://github.com/leadbay/leadclaw/actions/runs/26598777855/artifacts/7275983822\n- Windows installer artifact: https://github.com/leadbay/leadclaw/actions/runs/26598777855/artifacts/7276009042\n\nLatest checks are green: build-and-test, macOS installer, Windows installer.

ArtyETH06 and others added 10 commits May 28, 2026 16:59
…b.com-leadbay-product-issues-3651

# Conflicts:
#	README.md
#	packages/mcp/icon.png
- `installer --uninstall` opens the browser/Electron uninstall wizard
  (Linux only); only shows clients where Leadbay is already configured
- electron-main.cjs routes --uninstall to startUninstallerGui so the
  Electron window title and flow are correct
- Installer agent list now shows "install" (green) or "update" (amber)
  badge per client based on whether leadbay is already in their config
- Pure uninstall helpers exported from bin.ts: stripShellExportBlock,
  stripCodexBlock, stripJsonMcpEntry + async uninstallFrom* functions
- 12 unit tests covering all three strip functions
- README and --help updated: uninstall via `installer --uninstall`,
  LEADBAY_API_BASE_URL typo fixed to LEADBAY_BASE_URL,
  leadbay_recall_ordered_titles added to tools table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Resolve bin.ts conflict: keep OAuth bootstrap code from main, remove
  now-redundant broken-client.ts import (makeBrokenClient/ResolvedClient
  are now inlined in bin.ts by main); import LeadbayClient as value and
  LeadbayError as type from @leadbay/core
- Resolve packages/mcp/README.md conflict: keep installer GUI docs,
  add terminal-only install note (all platforms) and uninstall section
- installer-gui.ts: keep process alive after startInstallerGui resolves
  so the HTTP server doesn't exit before the browser connects
- README (root + mcp): clarify `install` works on all platforms,
  `installer --uninstall` is Linux-only for now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…+ README dev testing notes

electron-builder.cjs referenced dist/broken-client.js, dist/install-shared.js,
and dist/env.js which tsup never emits as separate files (everything is bundled
into bin.js / installer-gui.js). The missing files caused macOS to report the
DMG app as "damaged and can't be opened". Now only the two files that actually
exist in dist are listed.

Also adds local testing instructions to README: use installer-electron.js (not
installer-gui.js) to get the Electron window; installer-gui.js is the raw HTTP
server / browser fallback only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npx commands for install/uninstall are now shown together; local build
instructions explain to use installer-electron.js (Electron window) not
installer-gui.js (browser-only HTTP server fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…b.com-leadbay-product-issues-3651

# Conflicts:
#	pnpm-lock.yaml
@ArtyETH06
ArtyETH06 marked this pull request as ready for review May 29, 2026 04:40
@ArtyETH06
ArtyETH06 requested a review from milstan May 29, 2026 04:40
ArtyETH06 and others added 13 commits May 29, 2026 10:38
…anup

Security:
- Add Origin/Host validation to local installer GUI server (CSRF/DNS-rebinding)
- Add 1 MB body limit to /mcp and /messages endpoints (OOM prevention)
- Add 30-min TTL eviction for SSE sessions (memory leak on dropped connections)

Correctness:
- Fix CRLF regex in mergeShellExportBlock / stripShellExportBlock — token
  rotation and uninstall silently no-oped on Windows RC files with CRLF endings
- Make shell RC file writes atomic (tmp + renameSync) in both appendShellExports
  and uninstallShellExports
- Remove duplicate BrokenLeadbayClient / makeBrokenClient from bin.ts; import
  from broken-client.ts where they already lived

Tests (+21):
- test/unit/auth-http.test.ts — all 4 auth paths in resolveClientFromToken
- test/unit/shell-crlf.test.ts — CRLF handling in both shell block functions
- test/unit/installer-gui-unit.test.ts — sanitizeOutput token redaction + install
  negative paths

Docs:
- README: fix Linux installer note (command-only, no desktop window on Linux),
  remove duplicate installer section, split multi-command bash blocks 1-per-block
- packages/mcp/README: fix section numbering (was 3→4→3a), collapse release-note
  walls into one-line upgrade callout, move LEADBAY_TIMEOUT_MS into its table,
  clarify leadbay_enrich_contacts is a granular (advanced) tool, split all
  multi-command bash blocks 1-per-block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, SSE double-write, DXT removal

- Extract all install/uninstall logic from src/bin.ts into installer/:
  install-claude-code.ts, install-json-config.ts, install-codex.ts,
  install-dxt.ts, install-wizard.ts. bin.ts re-exports everything so
  existing test imports are unaffected.
- Add configPath field to DetectedClient; drop detail.split(" ")[0]
  pattern that truncated macOS paths with spaces.
- Fix /sse and /messages returning plain Response after SSEServerTransport
  already wrote headers — use x-hono-already-sent sentinel like /mcp.
- Remove Leadbay DXT extension (dir + registry) when detected, then write
  claude_desktop_config.json as the authoritative config source.
- Fix runBrowserFallback() to open uninstaller GUI when --uninstall is
  passed, matching the Electron main process behavior.
- Pin all npx install targets to @latest instead of @0.16.

Co-Authored-By: Claude <noreply@anthropic.com>
… for 0.16.0

Co-Authored-By: Claude <noreply@anthropic.com>
@milstan
milstan merged commit 54bcff4 into main May 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants