Repository navigation
Adding an OAuth MCP source: silent credential drop, refresh masks discovery failure, and addSource is the only path that actually (re)discovers tools #859
Description
Adding an OAuth MCP source: silent credential drop, refresh masks discovery failure, and addSource is the only path that actually (re)discovers tools
Summary
Connecting Notion (an OAuth-protected remote MCP source) through the agent-facing tools left the source in a broken "0 tools" state that was hard to diagnose. Three distinct problems compounded:
mcp.addSource/mcp.configureSourcesilently accept an invalidauthshape and returnok: truewhile creating no credential binding. No validation error.coreTools.sources.refreshreturns{ refreshed: true }even when discovery fails or finds 0 tools — it hides the discovery status/error thataddSourcedoes return.refreshdoes not re-run discovery against a binding that was added after the source was created. After the binding existed, everyrefreshstill reportedtoolCount: 0. Only re-runningmcp.addSourcewith the credential passed inline actually discovered the tools (14).
Net effect from the user's side: the connector first appeared as a source showing nothing, then showed up with 0 tools, and only after an addSource re-run did all tools appear. There was no surfaced error explaining any of this.
Environment
- Executor: local desktop runtime, single host, two credential scopes (personal + org).
- Source: Notion preset → official remote MCP server
https://mcp.notion.com/mcp. - Auth: OAuth 2.0 with dynamic client registration (DCR);
supportsDynamicRegistration: true. - Transport: streamable-http (server does not support SSE).
Reproduction (actual tool-call sequence)
executor.sources.list→ only built-inexecutor.executor.coreTools.sources.presets({ query: "notion" })→ Notion preset,pluginId: "mcp",transport: "remote".executor.mcp.probeEndpoint({ endpoint })→{ connected: false, requiresOAuth: true, supportsDynamicRegistration: true }.executor.coreTools.scopes.list+executor.coreTools.oauth.probe({ endpoint })→ DCR + auth-server metadata.executor.coreTools.oauth.start({ endpoint, credentialScope })→invalid_tool_arguments(missingconnectionId,pluginId,strategy). The tool description doesn't make these required fields obvious; neededdescribe.toolto find the schema. (Minor DX nit, see below.)executor.coreTools.oauth.start({ endpoint, credentialScope, connectionId, pluginId: "mcp", strategy: { kind: "dynamic-dcr" } })→authorizationUrl. User completed browser sign-in.executor.coreTools.connections.list→ connection present, valid token.- BUG Add organization billing model and members/billing console flows #1 —
executor.mcp.addSource({ ..., credentials: { scope, auth: { kind: "connection", connectionId } } })
→ok: true,toolCount: 0,discovery.status: "failed"(Failed connecting via sse).
Theauthfield only accepts{ kind: "none" }or{ oauth2?: HttpOAuthConfigureInput }.{ kind: "connection", ... }is the value shape used forheaders/queryParams, not forauth. It was silently dropped — no validation error — so no binding was created. - Re-ran step 8 with
remoteTransport: "streamable-http"(same wrongauthshape) →discovery.status: "failed"(Failed connecting via streamable-http), stillok: true. executor.coreTools.sources.bindings.list→[](confirms no binding was ever created in steps 8–9, despiteok: true).executor.mcp.configureSource({ source, scope, auth: { oauth2: { connection: { kind: "connection", connectionId } } } })→{ configured: true }.bindings.listnow shows the binding.- BUG Add organization billing model and members/billing console flows #2 + Add Cloudflare Workers sandbox runtime for isolated code execution #3 —
executor.coreTools.sources.refresh({ id, targetScope })→{ refreshed: true }.
Butexecutor.sources.liststill showsnotionwithtoolCount: 0. Repeatedrefreshcalls all returnrefreshed: true; tool count never changes. No discovery status/error is returned byrefreshat all. - Workaround / fix —
executor.mcp.addSource({ ..., remoteTransport: "streamable-http", credentials: { scope, auth: { oauth2: { connection: { kind: "connection", connectionId } } } } })
→{ toolCount: 14, discovery: { status: "ok" } }. Tools finally registered. - Verified:
notion.notion_search({ query: "a" })returned real workspace pages.
Expected vs. actual
| # | Expected | Actual |
|---|---|---|
| 1 | addSource/configureSource reject an invalid auth shape with a validation error |
Returns ok: true, silently drops the credential, creates no binding |
| 2 | refresh reports discovery status/error (like addSource's discovery field) |
Always returns { refreshed: true }, even on failure / 0 tools |
| 3 | refresh re-runs discovery using the current bindings |
Does not pick up a binding added after the source was created; addSource re-run is the only path that discovers |
Impact
- A source can sit in a permanently broken "0 tools" state with no surfaced error, and the documented recovery (
refresh) silently does nothing. - The only working recovery (re-
addSourcewith inline creds) is non-obvious and undocumented for the OAuth-after-create flow. - Hard to debug without dropping to
bindings.list/getSource/ repeateddescribe.tool.
Suggested fixes
- Validate the
authfield inmcp.addSource/mcp.configureSource(and the GraphQL/OpenAPI equivalents). Reject{ kind: "connection" }inauthwith a clear message pointing toauth.oauth2.connection. - Make
refreshreturn adiscoveryfield identical toaddSource(status,message,stage,toolCount). Never returnrefreshed: truewhen discovery failed or yielded 0 tools without flagging it. - Make
refreshactually re-run discovery against current bindings, so it's a true equivalent of add for the post-OAuth flow. If that's intentionally not the case, document it and have the connect flow tell the user to re-add. - Surface discovery errors in the UI next to the source (the connector showed "0 tools" with no error/badge).
Observability gap (the broader ask)
There's no audit trail of what the agent/runtime did during connect. We should register every tool call the runtime makes during a connect/discovery flow — tool path, redacted args, scope, outcome, and discovery status — and expose it (per-source activity log / debug panel / structured logs). With that, this whole sequence would have been one glance instead of a dozen probing calls. At minimum: log discovery attempts and their failure reasons against the source, and show them in the source detail view.
DX nits (low priority)
oauth.startrequiresconnectionId,pluginId,strategybut the tool description reads as ifendpoint+credentialScopesuffice. Consider documenting required fields or providing a higher-level "connect preset" helper that wires DCR end-to-end.sources.refreshtakes{ id, targetScope }while sibling tools take{ source: { id, scope } }. Inconsistent argument shapes across the source tools.
Activity
RhysSullivan commentedon Aug 28, 2026
Closing: the addSource/configureSource API this describes was replaced wholesale in #1370. Its successors cover each report here — addServer's auth input is a closed schema that rejects unknown shapes, connections.refresh returns the produced tools and health rather than a boolean, and refresh re-runs discovery with live credentials. The connect-activity-log idea from this thread is tracked in #1231.1
Footnotes
Sent from my Claude ↩