Skip to content

Interactive MCP OAuth not wired on session-launch path: AgentServices omits McpOAuthOptions #1402

Description

@JoshuaRowePhantom

Summary

Opening an agent session whose definition references an MCP server with an oauth connection fails immediately with:

Failed to open MCP server 'github-oauth': System.InvalidOperationException:
Interactive OAuth is not configured for MCP server 'github-oauth'.
   at Phantom.Workspaces.Llm.Mcp.McpOAuthOptions.<>c__DisplayClass17_0.<CreateNotConfiguredDelegate>b__0(...) McpOAuthOptions.cs:line 68
   at ModelContextProtocol.Authentication.ClientOAuthProvider.InitiateAuthorizationCodeFlowAsync(...)
   at ModelContextProtocol.Authentication.ClientOAuthProvider.GetAccessTokenAsync(...)
   ...
   at Phantom.Workspaces.Llm.McpToolContextProvider.ProvideAIContextAsync(...) McpToolContextProvider.cs:line 62
   at Phantom.Workspaces.Llm.AgentChat.InitializeMcpRuntimeToolAsync(...) AgentChat.cs:line 2580

The interactive redirect handler + loopback listener (McpOAuthRedirectHandler, wired via McpOAuthComposition.CreateOptions) exists and works, but it is only threaded into AgentServices.McpOAuthOptions on the app-level AgentChatFactory path in App.axaml.cs. The GUI session-launch path (AgentSessionShortcutContext.CreateAgentServices, used when opening/resuming a session from a workspace) builds its own AgentServices that sets SecretProvider but omits McpOAuthOptions. The MCP transport factory therefore falls back to McpOAuthOptions.Default, whose redirect delegate is the "not configured" stub that throws. No browser is ever opened.

This was surfaced while OAuth-testing the remote GitHub MCP server: after setting a pre-registered clientId (which correctly gets past GitHub's "dynamic client registration not supported" failure), the flow reaches InitiateAuthorizationCodeFlowAsync, which invokes the redirect delegate — and hits the stub because the session's AgentServices has no McpOAuthOptions.

This is a sibling of #1401: both are cases where the session-launch path drops a host service that the app-level path provides. #1401 drops SecretProvider-driven materialization; this drops McpOAuthOptions.

Root Cause

Two AgentServices construction sites diverge.

1. App-level path — has McpOAuthOptions (features/Phantom.Workspaces/App.axaml.cs:313-316):

var mcpOAuthOptions = Services.Mcp.McpOAuthComposition.CreateOptions(secretProvider, platformStore);  // 313
var agentChatFactory = new AgentChatFactory(
    agentPersistenceStore,
    new AgentServices { SecretProvider = secretProvider, McpOAuthOptions = mcpOAuthOptions },          // 316
    foregroundScheduler);

But mcpOAuthOptions is not passed into ApplicationServices (App.axaml.cs:318-327 passes secretProvider: but no mcpOAuthOptions:), and ApplicationServices has no property to hold it.

2. Session-launch path — missing McpOAuthOptions (features/Phantom.Workspaces/ViewModels/AgentSessionShortcutContext.cs:74-85):

return new AgentServices
{
    AgentPersistenceStoreOverride = agentPersistenceStore,
    LoggerFactory = loggerFactory,
    ToolsetFactory = toolsetFactory,
    ToolResourceFactory = this.CreateToolResourceFactory(dataAccessLayer),
    AccountUpsertService = accountUpsertService,
    CurrentSessionContext = currentSessionContext,
    SecretProvider = mainWindowViewModel.ApplicationServices.SecretProvider,   // 84  <-- no McpOAuthOptions
};

3. The factory falls back to the failing default (features/Phantom.Workspaces.Llm.Core/Mcp/McpTransportFactory.cs):

internal static McpOAuthOptions ResolveOAuthOptions(AgentServices? services)
    => services?.McpOAuthOptions as McpOAuthOptions ?? McpOAuthOptions.Default;

McpOAuthOptions.Default resolves the redirect delegate to the stub (features/Phantom.Workspaces.Llm.Core/Mcp/McpOAuthOptions.cs:53-69):

public AuthorizationRedirectDelegate ResolveRedirectDelegate(string serverName)
    => this.RedirectDelegateProvider?.Invoke(serverName) ?? CreateNotConfiguredDelegate(serverName);

internal static AuthorizationRedirectDelegate CreateNotConfiguredDelegate(string serverName)
    => (_, _, _) => throw new InvalidOperationException(
        $"Interactive OAuth is not configured for MCP server '{serverName}'.");

ApplicationServices exposes SecretProvider (features/Phantom.Workspaces/Services/ApplicationServices.cs:98) but has no McpOAuthOptions member, so even though AgentSessionShortcutContext reads other services from mainWindowViewModel.ApplicationServices, there is nothing for it to forward.

Affected Files

File What it contributes
features/Phantom.Workspaces/ViewModels/AgentSessionShortcutContext.cs Session-launch AgentServices (74-85) sets SecretProvider but omits McpOAuthOptions — the direct cause.
features/Phantom.Workspaces/Services/ApplicationServices.cs Carries SecretProvider (98) for the shortcut context to forward, but has no McpOAuthOptions property or ctor parameter.
features/Phantom.Workspaces/App.axaml.cs Builds mcpOAuthOptions (313) and threads it only into the AgentChatFactory AgentServices (316); does not pass it into ApplicationServices (318-327).
features/Phantom.Workspaces.Llm.Core/Mcp/McpTransportFactory.cs ResolveOAuthOptions falls back to McpOAuthOptions.Default when AgentServices.McpOAuthOptions is null.
features/Phantom.Workspaces.Llm.Core/Mcp/McpOAuthOptions.cs Default + CreateNotConfiguredDelegate (53-69) — the throwing stub that surfaces the misconfiguration.
features/Phantom.Workspaces/Services/Mcp/McpOAuthComposition.cs The real options bundle (redirect delegate + loopback URI + token cache) that must reach the session path.
features/Phantom.Workspaces.Llm.Interfaces/AgentServices.cs object? McpOAuthOptions { get; init; } seam.

Design / Fix

Thread the single, process-wide McpOAuthOptions instance to every GUI launch path, exactly as SecretProvider already is.

  1. Carry McpOAuthOptions on ApplicationServices. Add an optional ctor parameter object? mcpOAuthOptions = null and a public object? McpOAuthOptions { get; } property (typed object? to match AgentServices.McpOAuthOptions and avoid a Core→host dependency).

  2. Populate it in App.axaml.cs. Pass the already-built mcpOAuthOptions (313) into the ApplicationServices constructor (318-327), alongside secretProvider:.

  3. Forward it in AgentSessionShortcutContext.CreateAgentServices. Add:

    McpOAuthOptions = mainWindowViewModel.ApplicationServices.McpOAuthOptions,

    so session launches reuse the same interactive redirect handler + loopback listener + token cache.

  4. Audit sibling launch sites. Any other GUI path that constructs AgentServices for a session (e.g. MainWindowViewModel, auto-resume via trusted executors) must likewise set McpOAuthOptions. Headless hosts (CLI / Web.Server / tests) intentionally leave it null and keep the failing default.

Reusing the single McpOAuthComposition.CreateOptions(...) instance is important: CreateLoopbackRedirectUri() reserves a port and the handler binds a listener; each session should share the one configured bundle rather than construct its own.

Note (separate, later failure): once interactive OAuth is wired, the GitHub remote MCP server will still require a clientSecret at token exchange — GitHub "does not distinguish between public and confidential clients," so PKCE alone does not authorize a secret-less client (see #1400). That is a distinct configuration concern, not part of this wiring fix.

Considered / Background

  • An alternative is to have AgentSessionShortcutContext call McpOAuthComposition.CreateOptions(...) itself, but that would allocate a second loopback listener/handler per session and duplicate host composition. Sharing the app-level instance via ApplicationServices is preferred and mirrors the existing SecretProvider wiring.

Expected Tests

Test Name Class What It Verifies
AgentSessionShortcutContext_CreateAgentServices_ThreadsMcpOAuthOptions AgentSessionShortcutContextTests The AgentServices built for a session launch has McpOAuthOptions set to the instance from ApplicationServices (not null).
ApplicationServices_McpOAuthOptions_ExposesInjectedInstance AgentSessionShortcutContextTests ApplicationServices returns the mcpOAuthOptions passed to its constructor.
CreateMcpTransport_OAuthWithConfiguredDelegate_DoesNotUseFailingDefault McpTransportFactoryTests With AgentServices.McpOAuthOptions carrying a RedirectDelegateProvider, the resolved AuthorizationRedirectDelegate is the injected one, not the throwing default.
CreateMcpTransport_OAuthWithoutConfiguredDelegate_UsesFailingDefaultDelegate McpTransportFactoryTests (Existing) Confirms the default remains the throwing stub for headless hosts — guards the intended fallback.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingdiagnosedRoot cause identifiedneeds-slow-testsRequires full test suite including slow Git tests at checkinverified-locallyImplementation has been verified locally

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions