feat(app): desktop discovers and connects org-level MCP connections#2439
feat(app): desktop discovers and connects org-level MCP connections#2439benjaminshafii wants to merge 6 commits into
Conversation
Adds a "From your organization" section to Settings > Extensions > MCP, additive alongside the existing static Quick Connect grid. Sourced live from Den's /v1/mcp-connections?scope=usable (from PR #2406), it's the first piece of desktop-side consumption of that system. - den.ts: DenExternalMcpConnection type + listMcpConnections / startMcpConnectionConnect client methods, mirroring the existing listOrgLlmProviders pattern. - use-org-mcp-connections.ts: fetch-on-mount hook; connect() calls connect/start, opens the returned authorize URL via openDesktopUrl, then polls (mirrors the existing MCP OAuth poll-until-connected pattern) until the member's own connectedForMe flips true. - mcp-view.tsx: new McpOrgConnectionsSection using the existing ExtensionCard component unmodified. A pure resolveOrgMcpConnectionCardState helper (unit tested) decides card state: shared connections are shown as admin-managed and non-actionable; per_member connections show "Connect your account" until the calling member has their own token. - settings-route.tsx: wires the hook in and refreshes it from the existing Refresh button alongside the other cloud-sourced refreshes. Scope, deliberately: creating new org connections from the desktop is NOT included — POST /v1/mcp-connections is admin-only server-side, and den-web's admin dashboard already covers creation/management. This is consumption-only: discover what an admin published, connect your own account if needed. Local-only entries (OpenWork Browser, UI Control, local-command custom MCPs) are untouched since Den cannot spawn processes on a member's machine. Proven end-to-end with evals/flows/desktop-org-mcp-consolidation.flow.mjs (fraimz PASSED, 2 runs): a real click drives a real connect/start call, the OS browser it opens completes a real OAuth round trip against a mock IdP (witnessed via the mock server's own request log), and the SAME card flips from "Connect your account" to "Connected" without a page reload — proving the poll-driven UI update, not a manual refresh. Regression-checked against the pre-existing settings-extensions-mcp flow (still passing) to confirm the static Quick Connect grid is untouched. Depends on PR #2406 (External MCP Connections) for the underlying /v1/mcp-connections API — this branch is stacked on feat/external-mcp-connections.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…th docs + passing fraimz - Project org MCP connections as ExtensionItems: per-member grants appear in Marketplace as 'Connect your account', connected/shared ones land in My Extensions — no special org-only section. - Dedupe static Quick Connect suggestions only when an org equivalent renders, never hiding configured direct MCPs. - Den OAuth callback page now deep-links back to the OpenWork app. - Route mounted /workspace/:id/opencode clients' promptAsync/command through the OpenWork wrapper path. - Add tutorial docs (shared-mcp-connections.mdx) with validated screenshots. - Add desktop-org-mcp-demo fraimz flow (Marketplace -> real browser OAuth -> My Extensions -> Cloud Control -> real chat tool execution with external mock-server witness); old consolidation flow delegates to it.
fraimz — desktop org MCP consolidation, end-to-end proof (PASSED)
Result: PASSED — 8/8 steps, 0 failed ( Frame 1 — Admin publishes the connection in OpenWork CloudClaim: An org admin can publish an MCP connection (per-member credential mode, org-wide access) from the Cloud dashboard. (admin frame from the passing Frame 2 — Jordan finds it in the normal Extension MarketplaceClaim: The org connection appears as a regular catalog card in Frame 3 — Real browser OAuth round tripClaim: Clicking Frame 4 — The card moves to My Extensions as ConnectedClaim: After the browser sign-in completes, the same item appears in My Extensions as connected — no manual reload. Frame 5 — OpenWork Cloud Control is actually wired into the engineClaim: The desktop's Frame 6 — A real chat executes the org MCP toolClaim: In a fresh desktop chat, the agent discovers the org capability through OpenWork Cloud Control and executes it with Jordan's own connected account. Frame 7 — CleanupConnection deleted via the admin API; the run leaves no dangling org state. Reproduce locally# 1. real OAuth2 + MCP mock server (external witness)
PORT=3978 AUTO_APPROVE=1 node scripts/mock-oauth-mcp-server.mjs
# 2. run the proof (boots MySQL, den-api, dev Electron via CDP)
MOCK_OAUTH_MCP_URL=http://127.0.0.1:3978 pnpm fraimz --flow desktop-org-mcp-demo --stack denOther checks run on this commit:
Notable repair while getting this green (environment, not product)The final chat frame was blocked by a corrupted dev-profile workspace config ( One product fix rode along: mounted |
fraimz — Frame 3 hardened: independent Daytona cross-sandbox re-run of the real browser OAuth round trip (PASSED)The earlier fraimz proved Frame 3 from the mock IdP's request log only — it never confirmed a real OS browser window actually opened, and with Branch Frame 2 (re-confirmed) — org connection is an ordinary Marketplace cardAssertions (passed): card shows Frame 3a — consent copy before the clickAssertion (passed): modal renders Frame 3b — a real, separate Chromium window opened (the part log-only proof can't show)Claim: (Daytona display capture of the X11 framebuffer — not a CDP screenshot of the app.) Frame 3c — GET /authorize params, verified again cross-sandboxAssertions (passed): preceding Frame 3d — Approve clicked like a human, round trip completesAction: Frame 4 (re-confirmed) — card flips with no reload; static grid intact in the same shotAssertions (passed): no Regressions — none found
Reproduce# server sandbox (Den stack on this branch)
bash .devcontainer/test-server-on-daytona.sh feat/desktop-mcp-consolidation --name openwork-frame3-server
# seed org (needs pnpm --filter @openwork/email build first), then on the sandbox:
# DEN_DEMO_SEED_FETCH_GITHUB=0 pnpm exec tsx scripts/seed-demo-org.ts --reset
# mock IdP with a REAL consent screen (public preview URL as ISSUER)
HOST=0.0.0.0 PORT=3978 ISSUER=<preview-url-3978> AUTO_APPROVE=0 node scripts/mock-oauth-mcp-server.mjs
# electron sandbox against that Den
bash .devcontainer/test-on-daytona.sh feat/desktop-mcp-consolidation \
--den-base-url <DEN_WEB_URL> --den-api-base-url <DEN_API_URL> --artifacts-volume
# then: admin POST /v1/mcp-connections → desktop handoff sign-in as the member →
# Marketplace → Connect your account → wmctrl -l (new Chromium window) →
# xdotool click Approve → watch connectedForMe flip + the card update with no reload.Honest caveats (environment, not product)
|
…from this PR The 2-line opencode.ts change rerouted prompt/command for every openwork-mounted workspace — the one piece of this PR that changed behavior for users who never touch org MCP connections. It now ships separately as fix/opencode-openwork-wrapper-writes so this PR is purely additive and data-driven.
Hosted deployments can roll the desktop org-MCP experience out per org:
with DEN_MCP_CONNECTIONS_GATING_ENABLED=true, scope=usable returns an
empty list unless the org opted in via metadata mcpConnectionsEnabled:
true — byte-identical to an org with no published connections, on every
desktop version in the field (server-enforced, no client flag needed).
- Gating is off by default: local dev, evals, and self-hosted keep the
feature working out of the box (mirrors DEN_PLAN_GATING_ENABLED).
- scope=manageable, create, and access-grant routes stay available so
admins can stage connections before the flag flips.
- Pilot enable: UPDATE organization SET metadata = JSON_SET(
COALESCE(metadata,'{}'), '$.mcpConnectionsEnabled', CAST('true' AS JSON))
WHERE slug = '<org>';
- New-orgs-only default can later hook the same flag in org creation.
…can scan this file route-access-policy.test.ts parses route registrations with a scanner that understands strings but not comments; the apostrophe in "that's" opened a phantom string and made every subsequent route in this file unparseable (test failed with 'Unclosed route registration' on this branch while passing on dev).
Layer 0 — no deploy: scripts/mcp-connections-rollout.ts (den-api) flips the per-org metadata opt-in (status / enable / disable / dark), taking effect on the next desktop poll for every app version in the field. Layer 1 — code: scripts/revert-org-mcp-connections.sh builds a revert branch from the squash commits on dev (#2439, --all adds #2406), newest first, leaving the additive DB tables in place on purpose.
Staged rollout + rollback story (4 new commits, server-validated on Daytona)This PR now ships with rollout control and a layered rollback path, and the shared-path
Why this shape
Runtime validation (real Den stack on Daytona, branch @
|
Layer 0 — no deploy: scripts/mcp-connections-rollout.ts (den-api) flips the per-org metadata opt-in (status / enable / disable / dark), taking effect on the next desktop poll for every app version in the field. Layer 1 — code: scripts/revert-org-mcp-connections.sh builds a revert branch from the squash commits on dev (#2439, --all adds #2406), newest first, leaving the additive DB tables in place on purpose.
…staged rollout + rollback) (#2451) * feat(app): desktop discovers and connects org-level MCP connections Adds a "From your organization" section to Settings > Extensions > MCP, additive alongside the existing static Quick Connect grid. Sourced live from Den's /v1/mcp-connections?scope=usable (from PR #2406), it's the first piece of desktop-side consumption of that system. - den.ts: DenExternalMcpConnection type + listMcpConnections / startMcpConnectionConnect client methods, mirroring the existing listOrgLlmProviders pattern. - use-org-mcp-connections.ts: fetch-on-mount hook; connect() calls connect/start, opens the returned authorize URL via openDesktopUrl, then polls (mirrors the existing MCP OAuth poll-until-connected pattern) until the member's own connectedForMe flips true. - mcp-view.tsx: new McpOrgConnectionsSection using the existing ExtensionCard component unmodified. A pure resolveOrgMcpConnectionCardState helper (unit tested) decides card state: shared connections are shown as admin-managed and non-actionable; per_member connections show "Connect your account" until the calling member has their own token. - settings-route.tsx: wires the hook in and refreshes it from the existing Refresh button alongside the other cloud-sourced refreshes. Scope, deliberately: creating new org connections from the desktop is NOT included — POST /v1/mcp-connections is admin-only server-side, and den-web's admin dashboard already covers creation/management. This is consumption-only: discover what an admin published, connect your own account if needed. Local-only entries (OpenWork Browser, UI Control, local-command custom MCPs) are untouched since Den cannot spawn processes on a member's machine. Proven end-to-end with evals/flows/desktop-org-mcp-consolidation.flow.mjs (fraimz PASSED, 2 runs): a real click drives a real connect/start call, the OS browser it opens completes a real OAuth round trip against a mock IdP (witnessed via the mock server's own request log), and the SAME card flips from "Connect your account" to "Connected" without a page reload — proving the poll-driven UI update, not a manual refresh. Regression-checked against the pre-existing settings-extensions-mcp flow (still passing) to confirm the static Quick Connect grid is untouched. Depends on PR #2406 (External MCP Connections) for the underlying /v1/mcp-connections API — this branch is stacked on feat/external-mcp-connections. * feat(app): org MCP connections join the unified Extensions catalog with docs + passing fraimz - Project org MCP connections as ExtensionItems: per-member grants appear in Marketplace as 'Connect your account', connected/shared ones land in My Extensions — no special org-only section. - Dedupe static Quick Connect suggestions only when an org equivalent renders, never hiding configured direct MCPs. - Den OAuth callback page now deep-links back to the OpenWork app. - Route mounted /workspace/:id/opencode clients' promptAsync/command through the OpenWork wrapper path. - Add tutorial docs (shared-mcp-connections.mdx) with validated screenshots. - Add desktop-org-mcp-demo fraimz flow (Marketplace -> real browser OAuth -> My Extensions -> Cloud Control -> real chat tool execution with external mock-server witness); old consolidation flow delegates to it. * revert(app): drop the shared-path promptAsync/command wrapper change from this PR The 2-line opencode.ts change rerouted prompt/command for every openwork-mounted workspace — the one piece of this PR that changed behavior for users who never touch org MCP connections. It now ships separately as fix/opencode-openwork-wrapper-writes so this PR is purely additive and data-driven. * feat(server): staged-rollout gate for member-facing org MCP connections Hosted deployments can roll the desktop org-MCP experience out per org: with DEN_MCP_CONNECTIONS_GATING_ENABLED=true, scope=usable returns an empty list unless the org opted in via metadata mcpConnectionsEnabled: true — byte-identical to an org with no published connections, on every desktop version in the field (server-enforced, no client flag needed). - Gating is off by default: local dev, evals, and self-hosted keep the feature working out of the box (mirrors DEN_PLAN_GATING_ENABLED). - scope=manageable, create, and access-grant routes stay available so admins can stage connections before the flag flips. - Pilot enable: UPDATE organization SET metadata = JSON_SET( COALESCE(metadata,'{}'), '$.mcpConnectionsEnabled', CAST('true' AS JSON)) WHERE slug = '<org>'; - New-orgs-only default can later hook the same flag in org creation. * fix(server): reword connect/start comment so the route-policy parser can scan this file route-access-policy.test.ts parses route registrations with a scanner that understands strings but not comments; the apostrophe in "that's" opened a phantom string and made every subsequent route in this file unparseable (test failed with 'Unclosed route registration' on this branch while passing on dev). * chore(server): rollback tooling for the org MCP connections rollout Layer 0 — no deploy: scripts/mcp-connections-rollout.ts (den-api) flips the per-org metadata opt-in (status / enable / disable / dark), taking effect on the next desktop poll for every app version in the field. Layer 1 — code: scripts/revert-org-mcp-connections.sh builds a revert branch from the squash commits on dev (#2439, --all adds #2406), newest first, leaving the additive DB tables in place on purpose.
…nWork wrapper path (#2452) The read-side session overrides (list/get/messages/todo) already go through the OpenWork wrapper when the client is openwork-mounted; promptAsync and command still used the generated SDK call unless the caller passed reasoning_effort (a field the generated client drops). Make the write path consistent with the read path: openwork-mounted clients always use postSessionRequest, which preserves unknown body fields and passes the directory via header. Split out of #2439 so this shared-path change ships and bisects independently of the org MCP connections feature.










Summary
Stacked on #2406. Adds the first piece of desktop-side consumption of Den's
/v1/mcp-connectionssystem: a new "From your organization" section in Settings > Extensions > MCP, additive alongside the existing static Quick Connect grid.Scope, deliberately narrow (consumption only):
GET /v1/mcp-connections?scope=usable).shared-credential connections render as admin-managed, non-actionable.per_member-credential connections show Connect your account; clicking drives a realconnect/startcall and opens the returned authorize URL via the existingopenDesktopUrl.connectedForMeflips true, then the card updates in place — no page reload.Not in scope: creating new org connections from the desktop (that stays admin-only, server-enforced, and already covered by den-web's admin dashboard). Local-only entries (OpenWork Browser, UI Control, local-command custom MCPs) are untouched — Den cannot spawn processes on a member's machine, so those stay engine-direct.
Proof
evals/flows/desktop-org-mcp-consolidation.flow.mjs— fraimz PASSED, run twice for repeatability, against a real local stack (den-api + mock OAuth/MCP server + a real Electron desktop app driven via CDP):per_memberconnection the member has not yet connected (real HTTP, admin-only endpoint).connect/start; the OS browser it opens completes a real OAuth round trip against the mock IdP (witnessed via the mock server's own request log, not a client-side stub — Electron's contextBridge freezes exposed APIs, so this flow does not fakeshell.openExternal).Regression-checked against
evals/flows/settings-extensions-mcp.flow.mjs(still passing) to confirm the pre-existing static Quick Connect grid is untouched.Tests run
pnpm --filter @openwork/app typecheck— clean.bun test tests/(apps/app) — 98 pass, 0 fail (94 pre-existing + 4 new for the pure card-state helper).pnpm evals --flow desktop-org-mcp-consolidation— PASSED x2.pnpm evals --flow settings-extensions-mcp— PASSED (regression check).Notes for reviewer
feat/external-mcp-connections(feat(den): External MCP Connections — org-level, search/execute-routed, real OAuth #2406), since it needs the/v1/mcp-connections*routes that PR introduces. Rebase target should follow feat(den): External MCP Connections — org-level, search/execute-routed, real OAuth #2406's merge.0028_amused_mongu.sql,0029_wandering_medusa.sql) that existed in the repo but hadn't been run against the shared local MySQL instance used across worktrees — unrelated to this change, just a local-env gotcha worth knowing about.