Skip to content

fix(mcp): reproduce #86 — truthful logout, and a sign-in name that resolves - #87

Draft
christianhpoe wants to merge 2 commits into
devfrom
claude/issue-86-reproduce-fix-3hz9a9
Draft

fix(mcp): reproduce #86 — truthful logout, and a sign-in name that resolves#87
christianhpoe wants to merge 2 commits into
devfrom
claude/issue-86-reproduce-fix-3hz9a9

Conversation

@christianhpoe

@christianhpoe christianhpoe commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Issue #86 reproduces in full against the pinned engine (v1.17.18). Two defects, both fixed here.

  1. Logout confirms success when it cleared nothing. A server that accepts the unauthenticated MCP handshake reads as Ready without anyone signing in; "Log out" clears a credential that never existed, and the next launch silently restores Ready. Logout now says what actually happened.
  2. The sign-in modal could not resolve a connector's server name. It validated the display name instead of slugifying it, so any name with a space threw before the authorization URL was requested — no browser, no prompt.

Why

The reproduction

The reporter's server accepts the unauthenticated handshake and rejects only tools/call, with a 403 and an HTML body. All four reported symptoms follow from that, and all four reproduce:

Reported Reproduced
Connector is already "logged in" after install ✅ engine reports connected, auth store empty
No password prompt / no sign-in offered ✅ connect sees connected, never opens the modal
Tool calls fail 403 with an HTML body ✅ exactly
Still logged in after logout + restart ✅ returns to connected ~2s after restart
ARM A — log out, then restart
  after install:  status=connected  auth=<file missing>   <- Ready, never signed in
  after logout:   status=disabled   auth=<no entry>
  restarted: 3065 -> 3084 (new process, port released)
    t+2s          status=connected                        <- Ready again
ARM B — control, restart without logging out
    t+2s          status=connected

Arm B is the important one: the restart alone is sufficient, so the logout contributes nothing. Nothing stale is being restored — there was never anything to restore. The issue's own diagnosis ("reloading a stale or invalid token on startup") is not what happens; the auth store is empty throughout.

That makes the LegalWork defect narrower and more concrete than the title suggests: logoutMcpAuth reported success unconditionally. That false confirmation is what makes this read as a stale-session bug.

Detecting it

The engine offers no way to ask whether a server is authenticated — McpStatusConnected is bare, and no endpoint reports stored credentials. The signal is to reconnect once the credentials are gone, which is exactly what the next launch does, only now instead of later:

after logout after reconnect probe
anonymous-handshake server disabled connected → logout was a no-op
strict OAuth server, signed in disabled needs_auth → logout worked

Probing during logout also leaves the badge showing the truth immediately, instead of a Paused state that quietly flips to Ready on the next start — the very transition the reporter experienced.

The sign-in name

Connect slugifies the display name and writes that key into opencode.jsonc. Sign-in ran the display name through validateMcpServerName, whose job is to reject strings that are not already valid server names. Any character outside [A-Za-z0-9_-] threw server_name must be alphanumeric with '-' or '_', caught and shown as a generic failure — which is why it reads as the browser refusing to open. Nine remote catalog connectors are affected, path-dependent:

Connector Registered as Modal opened by connect "Log in" button
Microsoft SharePoint sharepoint throws (declares oauth: true) ok
Google Cloud Storage google-cloud-storage throws (declares oauth: true) throws
Thomson Reuters HighQ highq throws (declares oauth: true) ok
Google Workspace google-workspace throws, if the server challenges throws
CB Insights cbinsights throws, if the server challenges ok
Legal Data Hunter legaldatahunter throws, if the server challenges ok
Solve Intelligence solveintelligence throws, if the server challenges ok
TechGC (TopCounsel) techgc throws, if the server challenges ok
Dun & Bradstreet Risk Analytics dnb-risk throws, if the server challenges ok

LegalWork UI Control and Computer Use trip the same validator but are type: local, and authorizeMcp already refuses non-remote entries, so they never reach sign-in and are not counted.

This matters for #86 because sign-in is the remedy for an anonymously-connected server: the "Log in" button is the only way to force authentication. Whether the reporter hit it depends on their connector name, which the issue does not state — see below.

Issue

What is and is not established

  • The four reported symptoms reproduce, and the mechanism is confirmed with a control arm.
  • No stale-token restore exists. After disconnect + auth remove the entry is gone from mcp-auth.json and does not come back.
  • Whether the reporter also hit the sign-in bug is unproven. The issue says "IronCrow" in prose but references iron-crow_predict_101_rejection, implying a config key of iron-crow. That comes from a display name of either "Iron Crow" (breaks) or "iron-crow" (does not). "IronCrow" would have produced ironcrow. Circumstantial; not settled. Confirming the name as typed would close it.
  • The engine reporting connected for an anonymous handshake is engine behaviour and is not changed here. This PR makes LegalWork stop misreporting it.

Scope

  • New apps/app/src/app/mcp-auth-state.ts: detectReconnectWithoutAuth, the reconnect probe.
  • New apps/app/src/app/mcp-identity.ts: one derivation for a connector's server name, replacing three.
  • store.ts logoutMcpAuth reports the probe's verdict instead of unconditional success; authorizeMcp matches catalog entries on their identity key and carries the config key to the modal.
  • mcp-auth-modal.tsx resolves the server name from the entry rather than re-slugifying its display name.
  • constants.ts / mcp.ts delegate to the shared identity module.
  • New mcp.logout_no_credentials string (en; other locales fall back, as with existing keys).
  • Tests: mcp-auth-identity.test.ts (7), mcp-logout-truthfulness.test.ts (12).

Out of scope

  • Changing how the engine classifies an anonymous-handshake connection.
  • A persisted "authenticated" flag on the connector badge. The engine exposes no such signal, and caching an inference would be inventing information.

Testing

Ran

  • bun test tests/ (apps/app)
  • pnpm typecheck (apps/app)
  • node scripts/i18n-audit.mjs --ci
  • Reproduction and verification harnesses against the pinned engine and scripts/mock-oauth-mcp-server.mjs

Result

  • pass: 284 tests across 40 files, 0 fail; typecheck clean; i18n audit clean.
  • The identity tests were confirmed to be a real guard: reverting the resolver fails 6 of 7.

Manual verification

Both fixes were validated end to end against the real engine, not just unit tests.

Logout, two arms:

ARM 1: anonymous-handshake server (issue #86's shape)
  status on arrival: connected      <- Ready, never signed in
  status right after logout: disabled
  status after probe: connected
  -> "iron-crow connects without signing in, so there was nothing to log out of..."

ARM 2: strict OAuth server, signed in first (control)
  status after sign-in: connected
  status right after logout: disabled
  status after probe: needs_auth
  -> "Logged out of iron-crow."

Sign-in, a connector named Iron Crow:

before: sign-in threw before contacting the engine -> no OAuth browser
after:  engine returned an authorization URL -> browser opens
        status after sign-in: connected

A note on process, since it shaped the result: the first logout implementation watched for a spontaneous reconnect. It passed twelve unit tests and did nothing whatsoever against a real engine, because the server stays disabled until something reconnects it. The end-to-end arms above are what caught that, and are why the probe now asks for the reconnect rather than waiting for one.

Evidence

  • Console transcripts above, from harnesses run against the pinned engine binary, the repo's mock OAuth server, and a mock shaped like the reporter's (anonymous handshake, 403 HTML on tools/call).

Risk

  • The logout probe issues one extra POST /mcp/{name}/connect during logout. On a server that genuinely required credentials this lands on needs_auth, which is the correct post-logout state and more accurate than the previous disabled. A failed or unreadable probe falls back to the existing message, so an uncertain answer never accuses a working logout.
  • The identity change is confined to how one string is computed; keys written to opencode.jsonc are unchanged. getMcpIdentityKey now preserves underscores to match what connect writes, correcting a latent mismatch; no catalog entry uses one.

Rollback

  • Revert the two commits. Additive modules plus call-site edits; nothing persisted or migrated.

…rowser

Clicking "Log in" on an MCP connector often did nothing visible: no browser
window, no password prompt, no error anyone could act on. Issue #86 reports it
against a custom connector; it also affects eleven catalog entries, Microsoft
SharePoint and Google Workspace among them.

The connector's name was being derived three different ways. Connect slugifies
the display name and writes that key into opencode.jsonc. Sign-in instead ran
the *display* name through validateMcpServerName, which exists to reject strings
that are not already valid server names — so every name containing a space threw
"server_name must be alphanumeric" before the authorization URL was ever
requested. The throw was caught and shown as a generic failure, which is why the
symptom reads as the browser refusing to open rather than as a bug. Names with
an underscore failed differently: sign-in asked the engine for "iron-crow" while
the server was registered as "iron_crow", so the engine reported it missing.

The derivation now lives in one dependency-free module, and sign-in composes it
with the validator rather than substituting one for the other: a display name is
slugified, and only the resulting key is validated. authorizeMcp also pins the
opencode.jsonc key onto the entry it hands the modal, so a catalog entry's
display name can no longer stand in for the server's identity, and it matches
catalog entries on that key instead of on a slugified title.

Verified against the pinned opencode engine and the repo's mock OAuth server: a
connector named "Iron Crow" now returns an authorization URL and completes the
flow to connected, where before it failed without contacting the engine.

Closes #86

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EwVKKSbK7HTqnjeWmtPdK
@christianhpoe christianhpoe changed the title fix(mcp): let connectors with a space in their name reach the OAuth browser fix(mcp): resolve a connector's sign-in name from its config key, not its display name Aug 8, 2026
Reproduced from issue #86 against the pinned engine. A remote MCP server that
accepts the unauthenticated handshake and rejects only tools/call is reported by
the engine as plain `connected`, so the connector reads as Ready without anyone
signing in. "Log out" then clears a credential that never existed, the badge
drops to Paused, and the next launch silently restores Ready. From the outside
that is indistinguishable from a stale session surviving a logout, which is what
the issue describes. Verified with a control arm: restarting the engine without
logging out produces the same Ready state, so the logout contributes nothing.

The engine offers no way to ask whether a server is authenticated —
McpStatusConnected is bare and no endpoint reports stored credentials. The
signal is to reconnect once the credentials are gone, which is what the next
launch does anyway: a server that needed them comes back needs_auth, one that
never did comes back connected. Confirmed both ways against the real engine.
Probing during logout also leaves the badge showing the truth immediately rather
than a Paused state that flips to Ready on the next start.

Logout now says what happened instead of always claiming success. A first
attempt at this watched for a spontaneous reconnect instead of asking for one;
it passed its unit tests and did nothing at all against a real engine, because
the server stays disabled until something reconnects it.

Refs #86

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EwVKKSbK7HTqnjeWmtPdK
@christianhpoe christianhpoe changed the title fix(mcp): resolve a connector's sign-in name from its config key, not its display name fix(mcp): reproduce #86 — truthful logout, and a sign-in name that resolves Aug 8, 2026
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