Skip to content

✨ feat(connector): custom OAuth MCP connectors — onboarding, runtime execution & connector-first (LOBE-9983)#15546

Merged
ONLY-yours merged 12 commits into
canaryfrom
feat/connector-custom-oauth
Jun 8, 2026
Merged

✨ feat(connector): custom OAuth MCP connectors — onboarding, runtime execution & connector-first (LOBE-9983)#15546
ONLY-yours merged 12 commits into
canaryfrom
feat/connector-custom-oauth

Conversation

@ONLY-yours

Copy link
Copy Markdown
Member

💻 Change Type

  • ✨ feat
  • 🐛 fix

🔗 Related Issue

LOBE-9983 (Connectors system). Builds on the oidcConfig design from LOBE-9736 and the Connectors foundation in #15463.

🔀 Description of Change

#15463 shipped the Connectors data layer + tool-permission UI, but custom OAuth MCP connectors were not usable end-to-end: there was no OAuth flow, and the connector tools were never injected/executed at runtime. This PR completes the feature.

1. OAuth onboarding (Pre-registration + DCR)

  • + button in /settings/skill opens the Add modal.
  • connector.startOAuth: probe MCP URL → RFC 9728 protected-resource metadata → RFC 8414 AS metadata; DCR (RFC 7591) dynamic registration when no client_id, otherwise pre-registration; persist resolved oidcConfig; build a PKCE authorize URL with the state stashed single-use in Redis.
  • /oauth/connector/callback: consume state → exchange code → store encrypted tokens (KeyVaultsGateKeeper) → sync the tool list server-side → report back to the opener.
  • Default to the server-advertised scopes_supported when the user gives no scope.
  • Built on the @modelcontextprotocol/sdk OAuth helpers (discover / register / start / exchange / refresh).

2. Runtime execution — connector-first

  • Agent runtime (execAgent): resolve connectors with a KeyVaultsGateKeeper so OAuth tokens decrypt (otherwise tool calls 401); surface connectors in the agent-management availablePlugins (<connector_plugins>).
  • Classic client chat path (/webapi/chat is a proxy; tools are assembled client-side): inject connector manifests in createToolsEngine, drop plugins sharing a connector identifier (connector wins), and route connector tool calls to a new connector.callTool tRPC (resolve → hard-block disabled tools → refresh token → call the remote MCP with decrypted creds). LobeHub/Klavis skills keep their existing executor.
  • Chat-input skills picker now lists custom connectors.

3. Security / correctness

  • Strip decrypted credentials and oidcConfig.clientSecret from the list response.
  • Open the OAuth popup synchronously from the click (avoid popup-blocking) — codex P1.
  • Escape the callback postMessage payload for <script> context (</script> / U+2028-9 breakout) — codex P1.
  • Let /oauth/connector/callback bypass the SPA-rewrite + auth gate (it's a cross-site backend callback).

🧪 How to Test

  • Tested locally (type-check passes; manual OAuth + tool-call against Linear MCP)
  1. /settings/skill → Connectors → + → Name + Remote MCP server URL. Empty Client ID → DCR; filled → pre-registration (register the shown redirect URI).
  2. Authorize in the popup → connector connects and tools sync.
  3. Enable the connector for an agent and call its tools in chat — executes via connector.callTool with the stored token; disabled tools are hard-blocked.

📝 Additional Information

  • No DB migration: clientSecret is a new optional field inside the existing oidc_config jsonb column.
  • clientSecret is stored plaintext in oidcConfig by design; access/refresh tokens are encrypted in credentials.
  • Cloud deployment needs two thin bridges in the cloud repo (separate app dir + middleware override): a re-export of the callback route, and /oauth/connector in the middleware backendApiEndpoints + isPublicRoute allowlists.

ONLY-yours and others added 6 commits June 8, 2026 11:27
…BE-9983)

Connect the two OIDC schemes designed in LOBE-9736 (oidcConfig) end-to-end so
users can add a custom OAuth MCP server from /settings/skill. Until now the DB
schema, models, and tool-permission UI existed, but nothing ran the OAuth
authorization flow — syncTools only worked when a token already existed.

Flow (shared pipeline, branches only on where client_id comes from):
- Add modal (client_id present → Pre-registration; absent → DCR/RFC 7591)
- startOAuth: probe MCP URL → RFC 9728 protected-resource metadata → RFC 8414
  AS metadata; DCR-register the client when no client_id; persist resolved
  oidcConfig; build PKCE authorize URL, stash verifier in Redis keyed by state
- /oauth/connector/callback: consume state → exchange code → store encrypted
  tokens (KeyVaultsGateKeeper) + tokenExpiresAt + status=connected → postMessage
- syncTools lazily refreshes the access token before connecting

Built on @modelcontextprotocol/sdk OAuth helpers (discover/register/start/
exchange/refresh) — no hand-rolled protocol code.

Security:
- Wire KeyVaultsGateKeeper into ConnectorModel so OAuth tokens are encrypted at
  rest (previously the router passed no gatekeeper → plaintext)
- Strip decrypted credentials and oidcConfig.clientSecret from the list response

UI:
- "+" button in /settings/skill Connectors tab opens the Add modal
- SkillList surfaces custom connectors from the connector store
- Modal wires the client secret field, infers the scheme, and shows the
  redirect URI to register

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The authorize request sent an empty scope list, so providers that require a
scope (e.g. Linear MCP advertises scopes_supported ["read","write"]) issued a
useless token or rejected the flow. Default to the authorization server's
advertised scopes_supported when the user did not specify any, and use them for
both DCR registration and the authorize request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/oauth/connector/callback is a backend route handler reached via a cross-site
redirect from the OAuth provider, so the proxy middleware broke it two ways:

1. It was not in the backend passthrough list, so it got rewritten to the SPA /
   locale shell instead of running the route handler (307 → blank).
2. It was not in isPublicRoute, so BetterAuth treated it as protected; the
   cross-site top-level navigation doesn't reliably carry the SameSite session
   cookie, so it redirected to sign-in (307).

Add /oauth/connector to backendApiEndpoints and /oauth/connector/callback to
isPublicRoute (the handler validates its own single-use state, so it must not be
session-gated). Scoped so /oauth/callback/success|error SPA pages are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e wiring

Make custom OAuth MCP connectors actually callable, and sync their tools as
soon as authorization completes.

- callback: after token exchange, sync the tool list server-side via a shared
  syncConnectorToolsById — the connector is usable without a client round-trip
- sync.ts: extract buildConnectorMcpParams (http+auth / stdio), shared by
  syncTools and the new callTool
- connector router: add `callTool` (resolve connector, hard-block disabled
  tools, refresh token, call the remote MCP with decrypted credentials)
- aiAgent runtime: pass a KeyVaultsGateKeeper when resolving connectors so OAuth
  tokens decrypt (otherwise tool calls 401); surface connectors in the
  agent-management availablePlugins as a new 'connector' type
- AgentManagementContextInjector: render a <connector_plugins> section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The front-end chat orchestrates tools client-side (via /webapi/chat proxy),
separate from the server agent runtime. Connectors were invisible and
unexecutable there. Wire them in, connector-first.

- toolEngineering: build connector manifests from the store and inject them into
  createToolsEngine; drop plugins sharing a connector identifier (connector wins)
- buildClientConnectorManifests: store rows → type 'mcp' manifests (no token; the
  client has none) with permission → humanIntervention mapping
- mcpService.invokeMcpToolCall: route connector tool calls to connector.callTool
  before the plugin path (only connectors with a real MCP endpoint, so
  Lobehub/Klavis skills keep their executor)
- DeferredStoreInitialization: fetch connectors post-login so chat sees them
- AddConnectorModal: refresh after OAuth regardless of popup outcome
- chat-input skills picker: surface custom connectors in the auto group

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ML (codex P1)

- AddConnectorModal: open the OAuth popup synchronously inside the click handler
  (before any await), then navigate it to the authorize URL. Browsers block
  window.open once an async boundary is crossed, which left popup=null and the
  poll loop never resolving — the Add modal hung. Null popup now fails fast with
  a "allow popups" message.
- callback route: escape the postMessage payload for `<script>` context
  (`<`, `>`, `&`, U+2028/U+2029 → \uXXXX). A malicious OAuth server could put
  `</script>...` in the error param and execute script on the app origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lobehub Ready Ready Preview, Comment Jun 8, 2026 9:37am

Request Review

@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jun 8, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @ONLY-yours, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@dosubot dosubot Bot added feature:auth Authentication and authorization feature:mcp MCP relative issue feature:tool Tool calling and function execution labels Jun 8, 2026
… + tests

Address review: enforce the same constraints at the call site that the manifest
layer enforces, and stop swallowing OAuth failures.

- isEnabled on BOTH sides: invokeMcpToolCall only routes enabled connectors
  (a disabled connector no longer steals a same-name plugin's call), and the
  server rejects calls to a disabled connector. Matches buildClientConnectorManifests
  which only exposes enabled connectors.
- callTool requires the toolName to exist in the synced user_connector_tools
  list — unsynced / hand-crafted tool names are rejected instead of being
  forwarded blindly to the remote MCP.
- extract callConnectorToolById (typed ConnectorToolCallError → tRPC codes) so
  the gates are unit-testable.
- AddConnectorModal: distinguish success / provider-error (show the reason) /
  user-dismissed instead of collapsing every failure into a silent close.
- tests: exec gates (not-found / disabled connector / unknown tool / disabled
  tool / success / token-refresh) + buildClientConnectorManifests mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.15%. Comparing base (6f5a633) to head (eb989a0).
⚠️ Report is 26 commits behind head on canary.

Additional details and impacted files
@@             Coverage Diff             @@
##           canary   #15546       +/-   ##
===========================================
+ Coverage   70.64%   89.15%   +18.51%     
===========================================
  Files        3274      892     -2382     
  Lines      322959   108288   -214671     
  Branches    29419    10611    -18808     
===========================================
- Hits       228155    96545   -131610     
+ Misses      94621    11560    -83061     
  Partials      183      183               
Flag Coverage Δ
app ?
database 92.54% <ø> (ø)
packages/agent-manager-runtime 49.69% <ø> (ø)
packages/agent-runtime 81.04% <ø> (ø)
packages/builtin-tool-lobe-agent 18.52% <ø> (ø)
packages/context-engine 84.05% <0.00%> (-0.14%) ⬇️
packages/conversation-flow 91.29% <ø> (ø)
packages/device-gateway-client 90.51% <ø> (ø)
packages/eval-dataset-parser 95.15% <ø> (ø)
packages/eval-rubric 76.11% <ø> (ø)
packages/fetch-sse 87.28% <ø> (ø)
packages/file-loaders 87.89% <ø> (ø)
packages/memory-user-memory 74.99% <ø> (ø)
packages/model-bank 99.99% <ø> (ø)
packages/model-runtime 84.22% <ø> (ø)
packages/prompts 72.51% <ø> (ø)
packages/python-interpreter 92.90% <ø> (ø)
packages/ssrf-safe-fetch 0.00% <ø> (ø)
packages/types 35.38% <ø> (ø)
packages/utils 84.98% <ø> (ø)
packages/web-crawler 88.08% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Store ∅ <ø> (∅)
Services ∅ <ø> (∅)
Server ∅ <ø> (∅)
Libs ∅ <ø> (∅)
Utils 100.00% <ø> (+18.28%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ilure UX

Second review round.

- redirect URI: the modal showed a client-origin URI while the server sent an
  APP_URL one — register-vs-use mismatch broke the callback. Add a
  `connector.getRedirectUri` query (server source of truth) and show exactly
  that in the modal.
- execAgent: derive the plugin-override set from the connectors that ACTUALLY
  produce a manifest (enabled + with tools), not the raw endpoint-having set —
  a disabled / not-yet-synced same-named connector no longer evicts the plugin
  and leaves the runtime with no tools. Matches the client-chat behaviour.
- partial failure: when code exchange succeeds but the tool sync fails, the
  callback now reports `synced: false`; the modal shows "authorized but tools
  could not be synced" instead of a false "connected".

Tests: execAgent overlap regression (disabled / 0-tool keeps the plugin; real
tools replace it) + callback partial-failure (synced:false on sync error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 'connector'

The agent-management availablePlugins types describe a tool's SOURCE
(builtin / klavis / lobehub-skill); 'connector' named the storage system
instead. Once plugins migrate to the connector table everything is a connector,
so the source-based label is what matters. Rename to 'custom' to align with
ConnectorSourceType.custom (single source of truth); section is <custom_plugins>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gins

Community MCPs execute via the plugin path (not connector.callTool), so the
per-tool permissions a user sets in the new Connectors UI weren't surfaced:
needs_approval didn't trigger the approval prompt on either runtime. (disabled
was already hard-blocked at execution by ToolExecutionService and the mcp
router.)

- extract patchManifestWithPermissions into a pure, client-safe module
  (patchManifestPermissions.ts); connectorPermissionCheck.ts re-exports it.
- execAgent: also patch community-plugin manifests (pluginsWithoutConnectors)
  with their connector permissions, alongside lobehub/klavis.
- client createToolsEngine: patch community-plugin manifests with connector
  permissions from the store so needs_approval surfaces as humanIntervention
  in the classic chat path too.
- unit tests for the shared patch function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createToolsEngine now reads connectorSelectors.{customConnectors,connectorList};
toolEngineering/index.test.ts mocks getToolStoreState without `connectors`, so
the selectors hit `undefined.filter`. Guard with `?? []` (the real store always
seeds connectors:[] via initialState) and add connectors:[] to the test mock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ized slice

mcp.test.ts mocks the tool store without `connectors`, and invokeMcpToolCall
calls connectorByIdentifier → `s.connectors.find` threw. The previous fix only
guarded connectorList/customConnectors; harden all of them (find/filter) so any
partial-store mock is safe. The real store always seeds connectors:[].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ONLY-yours ONLY-yours merged commit ee65cf2 into canary Jun 8, 2026
37 of 43 checks passed
@ONLY-yours ONLY-yours deleted the feat/connector-custom-oauth branch June 8, 2026 10:01
ONLY-yours added a commit that referenced this pull request Jun 9, 2026
Reconcile the klavis→composio migration with the new Connectors system
(custom OAuth MCP connectors) that landed on canary via #15546.

Conflict resolutions (keep connector additions + apply composio rename):
- AgentManagementContextInjector: plugin type union → 'builtin' | 'composio' | 'lobehub-skill' | 'custom'
- contextEngineering: keep COMPOSIO_APP_TYPES + canary's isDesktop import
- useControls / SkillList: keep composioStore + connectorSelectors; drop renamed klavisStore;
  community OAuth connectors live only in the Connectors view (canary dedup, #15510)
- aiAgent: keep both ComposioService and deviceGateway imports

Follow-on fixes for the now-merged tree:
- execAgent.connectorOverlap.test: mock @/server/services/composio (klavis service removed)
- finish incomplete rename in some consumers: servers→composioServers, serverName→label;
  composio.createConnection now returns authConfigId for the optimistic store entry
@arvinxx arvinxx mentioned this pull request Jun 9, 2026
arvinxx added a commit that referenced this pull request Jun 10, 2026
# 🚀 LobeHub Release (20260610)

**Release Date:** June 10, 2026  
**Since v2.2.2:** 131 merged PRs · 13 contributors

> This weekly release strengthens agent collaboration across cloud,
desktop, CLI, and workspace flows, with steadier runtime behavior and a
broader foundation for workspace-scoped data.

---

## ✨ Highlights

- **Agent execution across devices** — Unifies per-device working
directories, project skill discovery, and sub-agent suspend/resume
behavior across server, QStash, and device RPC flows. (#15543, #15566,
#15481, #15620, #15591)
- **Connector and sandbox platform** — Expands connector permissions,
custom OAuth MCP connector onboarding, sandbox provider support, and
user-uploaded file sync into cloud sandbox runs. (#15463, #15546,
#15184, #15550)
- **Desktop and CLI reliability** — Fixes desktop cold-start,
auto-update, Windows build, CLI skill discovery, and `lh connect` agent
dispatch paths. (#15547, #15525, #15527, #15562, #15632, #15634)
- **Pages and sharing** — Refreshes topic sharing, improves Page Editor
layout behavior, and routes Page Agent tool execution through the
server-side editor path. (#15581, #15556, #15588, #15023, #15610)
- **Model availability and provider updates** — Adds user-scoped LobeHub
model availability, Claude Fable 5, Qwen thinking preservation, and
MiniMax M3 updates. (#15590, #15639, #13494, #15376)

---

## 🏗️ Core Product & Architecture

### Agent Runtime & Heterogeneous Agents

- Improves sub-agent lifecycle handling, including async suspend/resume,
queue-mode QStash resume delivery, and blocking nested sub-agent calls.
(#15481, #15620, #15575)
- Stabilizes heterogeneous agent ingestion and streaming with raw stream
dumps, per-turn usage, image forwarding on regenerate, and
duplicate-text fixes. (#15602, #15577, #15592, #15585)
- Adds execution-device and working-directory controls across device
RPC, legacy defaults, and remote-spawned Claude Code sessions. (#15543,
#15566, #15591, #15572)
- Improves runtime diagnostics and compatibility, including Gemini
multimodal output capture, abort stream semantics, and trace quality
analysis. (#15535, #13677, #15508)

---

## 📱 Platforms, Integrations & UX

### Connectors, Sandbox & Tools

- Ships API-level connector tool permissions, custom OAuth MCP connector
onboarding, and connector-first runtime execution. (#15463, #15546)
- Adds sandbox provider support, cloud sandbox file sync, and safer
external URL file input handling with SSRF validation. (#15184, #15550,
#12657)
- Improves tool visibility and execution with pinned app-fixed tools,
ANSI output rendering, gateway-tunneled MCP calls, and automatic
headless tool runs. (#15509, #15516, #15469, #15492)

### Desktop, CLI & Web UX

- Restores desktop startup and reload behavior, preserves IPC error
causes, and keeps the tab bar new-tab action visible across routes.
(#15547, #15597, #15638)
- Fixes desktop update and build stability for browser quit guards,
macOS update signing, and Windows Visual Studio detection. (#15525,
#15527, #15562)
- Shows the plan-limit upgrade UI on desktop builds. (#15628)
- Adds the Agent Run delivery checker and fixes CLI device dispatch plus
skill list/search output. (#15489, #15634, #15632)
- Refreshes onboarding, auth source preservation, topic UI states,
referral/Fable campaign copy, and chat-input control bar behavior.
(#15629, #15544, #15573, #15614, #15616, #15617, #15622, #15643)

---

## 🔒 Security, Reliability & Rollout Notes

- External URL file input now includes SSRF validation for safer Google
file handling. (#12657)
- Database workspace-scope migrations are part of this release;
self-hosted operators should run the normal migration path before
serving the updated app. (#15446, #15465, #15468, #15472)
- The release branch was re-cut from `canary` and includes the latest
`main` release-version commit so `v2.2.2` is the verified compare base.

---

## 👥 Contributors

@ONLY-yours, @sxjeru, @hardy-one, @xujingli, @hezhijie0327, @Coooolfan,
@arvinxx, @tjx666, @Innei, @rivertwilight, @rdmclin2, @cy948,
@AmAzing129

**Full Changelog**:
v2.2.2...release/weekly-20260610-recut-3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:auth Authentication and authorization feature:mcp MCP relative issue feature:tool Tool calling and function execution size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant