Skip to content

feat(app): desktop discovers and connects org-level MCP connections#2439

Closed
benjaminshafii wants to merge 6 commits into
feat/external-mcp-connectionsfrom
feat/desktop-mcp-consolidation
Closed

feat(app): desktop discovers and connects org-level MCP connections#2439
benjaminshafii wants to merge 6 commits into
feat/external-mcp-connectionsfrom
feat/desktop-mcp-consolidation

Conversation

@benjaminshafii

Copy link
Copy Markdown
Member

Summary

Stacked on #2406. Adds the first piece of desktop-side consumption of Den's /v1/mcp-connections system: a new "From your organization" section in Settings > Extensions > MCP, additive alongside the existing static Quick Connect grid.

Scope, deliberately narrow (consumption only):

  • Discover org-published MCP connections the signed-in member has been granted (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 real connect/start call and opens the returned authorize URL via the existing openDesktopUrl.
  • Polls (mirrors the existing MCP OAuth poll-until-connected pattern) until the member's own connectedForMe flips 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):

  1. Admin publishes a per_member connection the member has not yet connected (real HTTP, admin-only endpoint).
  2. Desktop app boots, member signs into OpenWork Cloud via a real handoff grant.
  3. The "From your organization" section renders the connection with Connect your account — screenshot validated.
  4. A real click on the card drives 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 fake shell.openExternal).
  5. Without a page reload, the same card flips to Connected — screenshot validated, proving the poll-driven update (not a manual refresh).

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

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.
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment Jul 3, 2026 8:58pm
openwork-den Ready Ready Preview, Comment Jul 3, 2026 8:58pm
openwork-den-worker-proxy Ready Ready Preview, Comment Jul 3, 2026 8:58pm
openwork-landing Ready Ready Preview, Comment, Open in v0 Jul 3, 2026 8:58pm

…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.
@benjaminshafii

Copy link
Copy Markdown
Member Author

fraimz — desktop org MCP consolidation, end-to-end proof (PASSED)

evals/results/2026-07-03T05-42-22-363Z/fraimz.html — flow desktop-org-mcp-demo, driven via CDP against the real Electron app + local Den stack + real OAuth2/MCP mock server. Every frame below binds a claim → user action → observable assertion → validated screenshot.

Result: PASSED — 8/8 steps, 0 failed (pnpm fraimz --flow desktop-org-mcp-demo --stack den)


Frame 1 — Admin publishes the connection in OpenWork Cloud

Claim: An org admin can publish an MCP connection (per-member credential mode, org-wide access) from the Cloud dashboard.
Assertion: POST /v1/mcp-connections succeeds; the member's scope=usable list shows the connection with connectedForMe: false.

MCP Connections admin screen in OpenWork Cloud

(admin frame from the passing mcp-connections-member-scoped fraimz — the den-web side is proven separately by that flow)

Frame 2 — Jordan finds it in the normal Extension Marketplace

Claim: The org connection appears as a regular catalog card in Settings → Extensions → Marketplace — no special org-only section.
Action: Sign in via desktop handoff, open Extensions, switch to the Marketplace tab.
Assertions (all passed): URL hash includes /settings/extensions/mcp · visible text includes Extension Marketplace, the connection name, and Connect your account · rejected From your organization and Something went wrong.

Marketplace shows the org MCP connection with Connect your account

Frame 3 — Real browser OAuth round trip

Claim: Clicking Connect your account opens the system browser and completes a real OAuth flow via the Den callback.
Assertions (all passed): consent copy OpenWork stores this sign-in visible · the external mock IdP logged a real GET /authorize carrying signed state, a dynamically registered client_id, and a redirect_uri scoped to this exact connection id.

Frame 4 — The card moves to My Extensions as Connected

Claim: After the browser sign-in completes, the same item appears in My Extensions as connected — no manual reload.
Assertion (passed): the card for the connection shows Connected and no longer offers Connect your account.

The same card flipped to Connected after the browser sign-in

Frame 5 — OpenWork Cloud Control is actually wired into the engine

Claim: The desktop's openwork-cloud MCP is real runtime config, not just a UI badge.
Assertions (all passed): GET /workspace/:id/mcp returns openwork-cloudhttp://127.0.0.1:8790/mcp/agent with a Bearer header and oauth: false, and engineSync.status === "ok" · card shows Ready.

Frame 6 — A real chat executes the org MCP tool

Claim: In a fresh desktop chat, the agent discovers the org capability through OpenWork Cloud Control and executes it with Jordan's own connected account.
Action: New task → prompt asking for search_capabilities("echo") then execute_capability with a run-unique payload.
Assertions (all passed): assistant finished · the exact echo text org mcp desktop proof 1783057342353 appears in both the prompt and the tool result · external witness: the mock MCP server logged a fresh POST /mcp during the chat window.

The agent executing an org-shared MCP tool in chat

Frame 7 — Cleanup

Connection 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 den

Other checks run on this commit:

  • pnpm --filter @openwork/app typecheck — passed
  • bun test apps/app/tests/ — 102 pass / 0 fail
  • node --check on both flow files, git diff --check — clean
Notable repair while getting this green (environment, not product)

The final chat frame was blocked by a corrupted dev-profile workspace config ("provider": { , } in the starter workspace's opencode.jsonc), which the shared OpenCode sidecar cached — aborting every model run with MessageAbortedError and 400-ing MCP engine registration. Repairing the file and restarting the sidecar fixed it. The flow now asserts engineSync === "ok" so this failure mode surfaces with its actual payload instead of a vague timeout.

One product fix rode along: mounted /workspace/:id/opencode clients now route promptAsync/command through the OpenWork wrapper path (apps/app/src/app/lib/opencode.ts).

@benjaminshafii

Copy link
Copy Markdown
Member Author

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 AUTO_APPROVE=1 no consent screen was ever rendered or clicked. This run closes both gaps, on a stricter topology: two independent Daytona sandboxes (Den stack on one, the real Electron desktop on the other), so the OAuth redirect + token exchange had to cross the public internet. Mock IdP ran with AUTO_APPROVE=0, so the flow only completes if someone actually clicks Approve on the provider's own page — the closest available simulation of a live Notion/Linear MCP OAuth screen (same RFC 8414 discovery + RFC 7591 dynamic client registration + PKCE S256).

Branch feat/desktop-mcp-consolidation @ 3dcc34f · member jordan.demo@acme.test invited + accepted via the real invitation APIs · evidence images pinned at 81025ca (proof-only branch, PR diff untouched).


Frame 2 (re-confirmed) — org connection is an ordinary Marketplace card

Assertions (passed): card shows Team Knowledge Base · MCP · Available from your organization … Connect your account · no org-only section · scope=usable for Jordan shows connectedForMe: false before the click.

Marketplace card

Frame 3a — consent copy before the click

Assertion (passed): modal renders OpenWork stores this sign-in in the organization cloud… (the PR's verbatim consent-copy claim).

Consent modal

Frame 3b — a real, separate Chromium window opened (the part log-only proof can't show)

Claim: Connect your accountshell.openExternal → the OS default browser, for real.
Assertions (passed): wmctrl -l before the click: only OpenWork - Dev. Immediately after: a new top-level window Mock MCP OAuth - Chromium — a separate OS process, not an in-app dialog (contextBridge freezes the exposed API, so this can't be stubbed from the renderer). Behind it, the Electron modal shows Waiting for browser… — the app is genuinely polling.

Real Chromium consent window on the sandbox display

(Daytona display capture of the X11 framebuffer — not a CDP screenshot of the app.)

Frame 3c — GET /authorize params, verified again cross-sandbox

Assertions (passed): preceding POST /register issued client_id=mock-client-5c991ac5-… · GET /authorize carried that exact client_id + code_challenge_method=S256 + signed state · redirect_uri = …/v1/mcp-connections/emc_01kwmnbpn8fzy825dvh9pj0fyf/connect/callback — scoped to this exact connection id. state payload decodes to { organizationId, orgMembershipId: Jordan's own, providerId: this connection, nonce, exp } with a trailing signature segment.

Frame 3d — Approve clicked like a human, round trip completes

Action: xdotool click on the provider page's own Approve OpenWork button (window-manager driven — no CDP, no DOM events).
Assertions (passed): IdP log: POST /approve → 302 → Den re-discovery → POST /token (PKCE verifier checked by the IdP) · browser lands on Den's callback page, title Connected — OpenWork, offering the openwork:// deep link back to the app · server-side connectedForMe flips to true for Jordan.

Callback page: Connected — OpenWork

Frame 4 (re-confirmed) — card flips with no reload; static grid intact in the same shot

Assertions (passed): no location.reload() after the click — poll-driven flip to Team Knowledge Base · Connected · Connected with your own account. in My Extensions · same screenshot shows Notion / Linear / Sentry / Stripe / Context7 / OpenWork UI Control still rendering Tap to connect — the pre-existing Quick Connect grid is untouched.

Connected + static grid intact

Regressions — none found

  • node evals/runner/run.mjs --flow settings-extensions-mcp --cdp-url http://127.0.0.1:9825PASSED (5/5 steps), run against the same live session after the OAuth flow, not a fresh boot.
  • pnpm --filter @openwork/app typecheck — clean.
  • bun test tests/ (apps/app) — 102 pass / 0 fail (includes the 4 new resolveOrgMcpConnectionCardState cases).

Regression flow frame


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)
  • The final openwork:// deep link shows a generic Linux "Open xdg-open?" prompt — the sandbox has no default-handler registration for the custom scheme. The card flip is driven by the app's own poll of connectedForMe, independent of that dialog.
  • jordan.demo@acme.test was email-verified via a direct DB flag (no mail transport in the sandbox) — equivalent to the flow's own MARK_VERIFIED_CMD escape hatch; invitation + accept went through the real APIs.
  • Scope was Frame 3 + its regression surface; the chat-execution frame (Frame 6 in the original fraimz) was not re-run here.

…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.
@benjaminshafii

Copy link
Copy Markdown
Member Author

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 opencode.ts change has been split out (it was the only piece affecting users who never touch org MCP connections — it now lives on fix/opencode-openwork-wrapper-writes for its own PR, and this branch's opencode.ts is byte-identical to dev).

Commit What
385cbf5 revert(app): drop the shared-path promptAsync/command change from this PR
7b34dee feat(server): staged-rollout gate — DEN_MCP_CONNECTIONS_GATING_ENABLED + per-org metadata.mcpConnectionsEnabled, scope=usable returns [] for non-opted-in orgs
a2fe725 fix(server): comment apostrophe broke route-access-policy.test.ts's parser (failed on this branch, passes on dev — now fixed)
8ec624b chore(server): rollback tooling — scripts/mcp-connections-rollout.ts (status/enable/disable/dark) + scripts/revert-org-mcp-connections.sh

Why this shape

  • Gating off by default → local dev, evals, self-host completely unchanged (mirrors DEN_PLAN_GATING_ENABLED).
  • Server-enforced empty list (not a 402) → non-pilot orgs are byte-identical to "org with nothing published", works on every desktop version in the field, zero client flag code.
  • Admin scope=manageable / create / grants stay open → orgs can stage connections before the flag flips.

Runtime validation (real Den stack on Daytona, branch @ 8ec624b, curl-level)

  • A — default (gating unset): admin created a per-member connection; member scope=usable returns it. Out-of-the-box behavior unchanged. ✅
  • B — gated, not opted in: den-api restarted with DEN_MCP_CONNECTIONS_GATING_ENABLED=true → same member gets {"connections":[]}; admin manageable still shows the connection. ✅
  • C — ops script on the sandbox: enable acme-robotics-demo → member sees it again · disable[] · dark --yes[] + status shows off. ✅
  • Git-revert script: dry-run refuses pre-merge (correct), squash-SHA lookup proven against merged PRs (Disable password breach screening for self-hosted Helm #2445, Memory Bank v0 — Backend (den-db + den-api + agent prompt + eval) #2436).

Rollback runbook (fastest first)

  1. No deploy: pnpm --filter @openwork-ee/den-api exec tsx scripts/mcp-connections-rollout.ts disable <org> (or dark --yes) — takes effect on the next desktop poll, all app versions.
  2. Code revert: bash scripts/revert-org-mcp-connections.sh --execute [--all] — builds a revert branch from the squash commits (feat(app): desktop discovers and connects org-level MCP connections #2439 first, --all adds feat(den): External MCP Connections — org-level, search/execute-routed, real OAuth #2406), leaves the additive DB tables in place on purpose.

Prod rollout after merge

Set DEN_MCP_CONNECTIONS_GATING_ENABLED=true on hosted Den before/at deploy (without it, the feature is live for every org — intentional default for dev/self-host), then opt in pilots via the same script's enable <org-slug>.

Tests run on this branch

  • bun test ee/apps/den-api/test/ — 261 pass / 1 fail; the 1 fail (route-guard-policy uncovered-routes audit) reproduces identically on plain dev and den-api tests are not in CI.
  • New: external-mcp-rollout.test.ts 3/3 · @openwork/app typecheck clean · den-api tsc --noEmit clean · bun test apps/app/tests/ 102/0.

@benjaminshafii benjaminshafii deleted the branch feat/external-mcp-connections July 3, 2026 21:16
benjaminshafii added a commit that referenced this pull request Jul 3, 2026
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.
benjaminshafii added a commit that referenced this pull request Jul 3, 2026
…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.
benjaminshafii added a commit that referenced this pull request Jul 3, 2026
…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.
benjaminshafii added a commit that referenced this pull request Jul 3, 2026
#2453)

The desktop PR landed as #2451 (successor to #2439, which GitHub
auto-closed when its stacked base branch was deleted). Dry-run now
resolves both squash commits on dev:
  #2451 -> 726304c (desktop + rollout gate + tooling)
  #2406 -> 69c3469 (server External MCP Connections)
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