You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adding an OAuth MCP source: silent credential drop, refresh masks discovery failure, and addSource is the only path that actually (re)discovers tools #859
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.configureSource silently accept an invalid auth shape and return ok: true while creating no credential binding. No validation error.
coreTools.sources.refresh returns { refreshed: true } even when discovery fails or finds 0 tools — it hides the discovery status/error that addSource does return.
refresh does not re-run discovery against a binding that was added after the source was created. After the binding existed, every refresh still reported toolCount: 0. Only re-running mcp.addSource with 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).
executor.coreTools.oauth.start({ endpoint, credentialScope }) → invalid_tool_arguments (missing connectionId, pluginId, strategy). The tool description doesn't make these required fields obvious; needed describe.tool to find the schema. (Minor DX nit, see below.)
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).
The auth field only accepts { kind: "none" } or { oauth2?: HttpOAuthConfigureInput }. { kind: "connection", ... } is the value shape used for headers/queryParams, not for auth. It was silently dropped — no validation error — so no binding was created.
Re-ran step 8 with remoteTransport: "streamable-http" (same wrong auth shape) → discovery.status: "failed" (Failed connecting via streamable-http), still ok: true.
executor.coreTools.sources.bindings.list → [] (confirms no binding was ever created in steps 8–9, despite ok: true).
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-addSource with inline creds) is non-obvious and undocumented for the OAuth-after-create flow.
Hard to debug without dropping to bindings.list / getSource / repeated describe.tool.
Suggested fixes
Validate the auth field in mcp.addSource / mcp.configureSource (and the GraphQL/OpenAPI equivalents). Reject { kind: "connection" } in auth with a clear message pointing to auth.oauth2.connection.
Make refresh return a discovery field identical to addSource (status, message, stage, toolCount). Never return refreshed: true when discovery failed or yielded 0 tools without flagging it.
Make refresh actually 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.start requires connectionId, pluginId, strategy but the tool description reads as if endpoint + credentialScope suffice. Consider documenting required fields or providing a higher-level "connect preset" helper that wires DCR end-to-end.
sources.refresh takes { id, targetScope } while sibling tools take { source: { id, scope } }. Inconsistent argument shapes across the source tools.
Adding an OAuth MCP source: silent credential drop,
refreshmasks discovery failure, andaddSourceis the only path that actually (re)discovers toolsSummary
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
addSourcere-run did all tools appear. There was no surfaced error explaining any of this.Environment
https://mcp.notion.com/mcp.supportsDynamicRegistration: true.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.executor.mcp.addSource({ ..., credentials: { scope, auth: { kind: "connection", connectionId } } })→
ok: true,toolCount: 0,discovery.status: "failed"(Failed connecting via sse).The
authfield 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.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.executor.coreTools.sources.refresh({ id, targetScope })→{ refreshed: true }.But
executor.sources.liststill showsnotionwithtoolCount: 0. Repeatedrefreshcalls all returnrefreshed: true; tool count never changes. No discovery status/error is returned byrefreshat all.executor.mcp.addSource({ ..., remoteTransport: "streamable-http", credentials: { scope, auth: { oauth2: { connection: { kind: "connection", connectionId } } } } })→
{ toolCount: 14, discovery: { status: "ok" } }. Tools finally registered.notion.notion_search({ query: "a" })returned real workspace pages.Expected vs. actual
addSource/configureSourcereject an invalidauthshape with a validation errorok: true, silently drops the credential, creates no bindingrefreshreports discovery status/error (likeaddSource'sdiscoveryfield){ refreshed: true }, even on failure / 0 toolsrefreshre-runs discovery using the current bindingsaddSourcere-run is the only path that discoversImpact
refresh) silently does nothing.addSourcewith inline creds) is non-obvious and undocumented for the OAuth-after-create flow.bindings.list/getSource/ repeateddescribe.tool.Suggested fixes
authfield inmcp.addSource/mcp.configureSource(and the GraphQL/OpenAPI equivalents). Reject{ kind: "connection" }inauthwith a clear message pointing toauth.oauth2.connection.refreshreturn adiscoveryfield identical toaddSource(status,message,stage,toolCount). Never returnrefreshed: truewhen discovery failed or yielded 0 tools without flagging it.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.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.