Skip to content

Custom HolaApp polish (URL/OAuth) + runtime: agents-optional & single-flight authorize - #439

Merged
jeffreyliimerch merged 10 commits into
mainfrom
feat/custom-app-bare-url
Jul 26, 2026
Merged

Custom HolaApp polish (URL/OAuth) + runtime: agents-optional & single-flight authorize#439
jeffreyliimerch merged 10 commits into
mainfrom
feat/custom-app-bare-url

Conversation

@jeffreyliimerch

Copy link
Copy Markdown
Collaborator

Follow-up polish on the "Create your own HolaApp" feature, plus two runtime fixes surfaced while testing it (an OAuth-MCP custom app against Heygen/Linear).

Desktop — custom-app UX

  • Bare MCP URL — paste just https://mcp.heygen.com/mcp/v1/ (or any provider's "add custom connector" URL); no JSON blob required. The field placeholder/label now lead with the URL.
  • Auto-https:// — scheme-less URLs (heygen.com, mcp.heygen.com/mcp/v1/) are accepted and normalized (with a dotted-host guard so a stray word isn't mistaken for a URL). Applies to both the app URL and the MCP field.
  • Cancel = don't add — for a headerless (OAuth) MCP app, the app is only kept if sign-in succeeds. It's saved as a pending draft so the MCP can attach + authorize, then rolled back if you cancel or close the dialog.
  • No-churn retries — a cancelled sign-in keeps the same draft (same server id) so a retry reuses the SAME attached server instead of standing up a fresh one each click.
  • Real failure reasons — the add dialog now shows the runtime's actual authorize detail (e.g. EADDRINUSE, superseded by a newer sign-in attempt) instead of a generic "cancelled".
  • Orphaned-draft purge — a session killed mid-add no longer leaves stray entries: pending drafts are swept once at startup.

Runtime

  • agents is optional (workspace-runtime-plan.ts) — a workspace.yaml with no top-level agents block now defaults to the built-in single agent (holaboss / gpt-5.4, overridden per-session by the picker) instead of failing every run with missing object field 'agents'. A present-but-malformed agents is still rejected (existing tests stand; added a default-path test).
  • Single-flight authorize (mcp-authorize-host.ts) — the OAuth callback binds a FIXED loopback port, so a cancelled/abandoned sign-in (which waits out its full consent timeout) held the port and the retry died with EADDRINUSE 127.0.0.1:51703. The api-server now SIGKILLs any in-flight authorize child (awaiting its exit so the OS frees the port) before spawning a new one, and reports "superseded" for the one it killed.

Testing

  • turbo run typecheck green for holaboss-local and @holaboss/runtime-api-server.
  • workspace-runtime-plan.test.ts: 20/20 pass (added a test for the default-agent path).
  • Biome clean on the touched desktop feature files (electron main.ts intentionally not touched here).

🤖 Generated with Claude Code

jeffreyliimerch and others added 10 commits July 27, 2026 00:21
Many providers (HeyGen, …) give ONLY a remote MCP URL for an "add custom
connector" flow — e.g. https://mcp.heygen.com/mcp/v1/ — not a JSON blob.
parseCustomMcpConfig now accepts a bare http(s) URL as shorthand for
{ url }; no headers ⇒ treated as OAuth, so sign-in opens on add (same as
a headerless JSON config). Config field help updated to say "or just the
remote server URL".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MCP field's placeholder showed a full { mcpServers } JSON, so it read
as "JSON required" even though a bare URL works. Lead the placeholder with
a bare URL (…or a full JSON / npx mcp-remote config below), and drop
"config" from the label ("MCP server").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app was saved BEFORE the headerless-MCP OAuth ran, so cancelling the
sign-in still left the app added (openable, MCP unauthorized). Now a
headerless (OAuth) MCP app is only KEPT if sign-in succeeds: it's saved so
the MCP can attach + authorize, then rolled back (deleted → detached via
catalog refresh) if the user cancels or auth fails.

- runCustomAppOAuth returns whether the server ended up authorized.
- Cancelling the "Signing in…" state (Cancel / X / Esc / backdrop) aborts
  the add cleanly and rolls the pending app back.
- Static-token / no-MCP apps add immediately, as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cheme

The app URL and the MCP field both required an explicit http(s):// scheme.
Now a scheme-less URL (heygen.com, mcp.heygen.com/mcp/v1/) is accepted and
normalized to https://:

- normalizeRemoteUrl() prepends https:// when no scheme is present.
- bareUrlOrNull() accepts a scheme-less bare token only if it has a dotted
  host, so a stray word isn't misread as a URL; the app URL and JSON-config
  urls are normalized too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a cancelled sign-in the form stays pre-filled, but each retry minted
a NEW app id — churning the MCP attach/detach and standing up a fresh
server every time, so the retry's authorize raced/failed and showed the
cancelled error again.

Now the pending app is a single DRAFT reused across retries (same id →
same attached server, no attach/detach churn). It's committed on success
and discarded ONLY when the dialog is actually closed (Cancel / X / Esc /
backdrop) — cancel = don't add. Stops deleting + re-adding the app on each
cancel, which was the source of the churn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The custom-app add showed "Sign-in was cancelled" for every failure mode
(actual cancel, authorize error, server-never-registered), hiding what
went wrong. runCustomAppOAuth now returns the runtime's authorize `detail`
(and a distinct "didn't register in time" for a poll timeout), and the
dialog shows it: "Sign-in didn't complete: <reason>."

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent)

A workspace.yaml with no top-level `agents` block failed every run with
"missing object field 'agents'". This kept recurring: the desktop MCP
registry rewrites workspace.yaml, and a General/onboarding workspace may
never have had an `agents` block to begin with.

loadGeneralConfig now defaults a TRULY-ABSENT `agents` (undefined/null) to
the built-in single agent (holaboss / gpt-5.4 — the session picker
overrides the model). A PRESENT-but-malformed `agents` (a list, or the
legacy {general} shape) is still rejected, so both existing tests stand;
added a test for the default path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…NUSE

The OAuth callback binds a FIXED loopback port (51703). A cancelled/
abandoned sign-in leaves its authorize child running — it waits out the
full ~210s consent timeout still holding the port — so the retry's child
can't bind it and fails immediately with
"listen EADDRINUSE 127.0.0.1:51703".

authorizeMcpServerViaHost now tracks the in-flight child and SIGKILLs it
(awaiting its exit so the OS frees the port) before spawning the next one.
Only one authorize runs at a time, which matches the fixed-port design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etry

When single-flight SIGKILLs an in-flight authorize child to free the fixed
callback port for a newer attempt, its close now reports "superseded by a
newer sign-in attempt" instead of the cryptic "produced no result".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A headerless-MCP app is saved as a DRAFT (pending) during sign-in — kept
for retry, committed on success, discarded on close. But a session killed
mid-add leaked the draft into the sidebar/grid.

Mark drafts `pending`, drop the flag on a successful commit, and purge any
still-pending drafts once at startup (first catalog load), before the
app-owned sync re-attaches their MCPs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeffreyliimerch
jeffreyliimerch merged commit 74c7b6f into main Jul 26, 2026
11 checks passed
@jeffreyliimerch
jeffreyliimerch deleted the feat/custom-app-bare-url branch July 26, 2026 17:43
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