Skip to content

Make the slack remote MCP server work#2512

Draft
dgageot wants to merge 3 commits intodocker:mainfrom
dgageot:oauth-slack
Draft

Make the slack remote MCP server work#2512
dgageot wants to merge 3 commits intodocker:mainfrom
dgageot:oauth-slack

Conversation

@dgageot
Copy link
Copy Markdown
Member

@dgageot dgageot commented Apr 26, 2026

No description provided.

dgageot added 3 commits April 25, 2026 12:58
Recovery messages (a previously-failed toolset is now available again)
were going through the same warning queue as real failures. After lazy-
init OAuth completed and the toolset reconnected, the user saw

    Some toolsets failed to initialize for agent 'root'.
    Details:
    - mcp(remote host=mcp.slack.com transport=streamable) is now available

The body says 'is now available' (success) but the framing says 'failed'
(failure), which reads like 'is NOT available'.

This change splits the agent's queue in two:

  - AddToolWarning / DrainWarnings: real failures (start failed,
    list failed, ...). Exported because the runtime emits these from
    the startup tool-loading path (added in a follow-up commit).
  - AddToolNotice / DrainNotices: positive, informational status
    updates. The 'now available' recovery path now uses this queue.

emitAgentWarnings drains both, formatting failures with the existing
'Some toolsets failed to initialize' banner and notices with a neutral
'Toolset status update' header so each reads correctly on its own.

Assisted-By: docker-agent
Running 'docker agent run ./examples/slack.yaml' could hang during
startup before the TUI was even ready: the runtime was eagerly starting
toolsets to populate sidebar tool counts, the remote MCP client got back
401 + WWW-Authenticate, and the OAuth flow tried to surface an
elicitation dialog. The TUI hadn't rendered yet, the elicitation
goroutine was blocked on a synchronous channel send, and Ctrl-C
couldn't reach it.

This change introduces a per-context flag that lets callers opt out of
interactive flows:

  - WithoutInteractivePrompts(ctx) marks a context as non-interactive.
  - The oauthTransport checks the flag before triggering the OAuth
    elicitation on a 401 and short-circuits with a recognisable
    AuthorizationRequiredError instead of blocking.
  - IsAuthorizationRequired(err) lets callers distinguish 'OAuth was
    deferred' from a real failure. Because the MCP SDK wraps transport
    errors with %v (not %w), enrichConnectError reads the deferred-auth
    flag back off the transport in remote.go and re-emits a clean
    AuthorizationRequiredError.

The runtime wraps the startup tool-loading context with this helper, so
toolsets that need OAuth are detected immediately and silently deferred
to the first RunStream where the user is interacting and a dialog can
be rendered. Real start failures still surface as warnings via the
agent's warning channel (a.AddToolWarning), and freshFailure is
preserved for the deferred-OAuth case so the *real* failure on the
eventual interactive retry isn't suppressed by the once-per-streak
guard.

Assisted-By: docker-agent
… re-auth on scope changes

Three related fixes to the remote MCP OAuth flow, motivated by the Slack
MCP server (https://mcp.slack.com/mcp).

- Accept Slack's oauth.v2.user.access response shape in addition to the
  standard RFC 6749 §5.1 flat shape. Slack nests the user token inside
  an authed_user object and signals application-level failures via HTTP
  200 + {"ok":false}. Previously the flat decoder silently produced an
  empty bearer, and every subsequent MCP request was rejected with 401.
  Decoding is factored out into parseTokenResponse, used by both
  ExchangeCodeForToken and RefreshAccessToken, and we now error out
  explicitly when no access_token can be located.

- Track the configured scope list on each stored token as a new
  RequestedScopes field. In getValidToken, if the configured scopes are
  no longer covered by the stored token, purge it from the store and
  return nil so the next request triggers a fresh OAuth flow. The scope
  list is preserved across silent refreshes, and legacy tokens without
  RequestedScopes are left untouched to avoid forcing re-auth on
  upgrade.

- Log the response body (capped at 2 KiB) when an authenticated retry
  after a successful OAuth flow is still rejected with a non-2xx. The
  MCP SDK only surfaces http.StatusText, which hides provider-specific
  error detail such as insufficient_scope. The body remains readable by
  downstream consumers, and enrichConnectError attaches the extracted
  message to the Connect error so callers see the real server-side
  cause (e.g. 'App is not enabled for Slack MCP server access').

Covered by new unit tests for Slack's nested and ok:false payloads,
post-OAuth retry body preservation, and the scope-invalidation path.

Assisted-By: docker-agent
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