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.
-
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).
-
Populate it in App.axaml.cs. Pass the already-built mcpOAuthOptions (313) into the ApplicationServices constructor (318-327), alongside secretProvider:.
-
Forward it in AgentSessionShortcutContext.CreateAgentServices. Add:
McpOAuthOptions = mainWindowViewModel.ApplicationServices.McpOAuthOptions,
so session launches reuse the same interactive redirect handler + loopback listener + token cache.
-
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. |
Summary
Opening an agent session whose definition references an MCP server with an
oauthconnection fails immediately with:The interactive redirect handler + loopback listener (
McpOAuthRedirectHandler, wired viaMcpOAuthComposition.CreateOptions) exists and works, but it is only threaded intoAgentServices.McpOAuthOptionson the app-levelAgentChatFactorypath inApp.axaml.cs. The GUI session-launch path (AgentSessionShortcutContext.CreateAgentServices, used when opening/resuming a session from a workspace) builds its ownAgentServicesthat setsSecretProviderbut omitsMcpOAuthOptions. The MCP transport factory therefore falls back toMcpOAuthOptions.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 reachesInitiateAuthorizationCodeFlowAsync, which invokes the redirect delegate — and hits the stub because the session'sAgentServiceshas noMcpOAuthOptions.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 dropsMcpOAuthOptions.Root Cause
Two
AgentServicesconstruction sites diverge.1. App-level path — has
McpOAuthOptions(features/Phantom.Workspaces/App.axaml.cs:313-316):But
mcpOAuthOptionsis not passed intoApplicationServices(App.axaml.cs:318-327passessecretProvider:but nomcpOAuthOptions:), andApplicationServiceshas no property to hold it.2. Session-launch path — missing
McpOAuthOptions(features/Phantom.Workspaces/ViewModels/AgentSessionShortcutContext.cs:74-85):3. The factory falls back to the failing default (
features/Phantom.Workspaces.Llm.Core/Mcp/McpTransportFactory.cs):McpOAuthOptions.Defaultresolves the redirect delegate to the stub (features/Phantom.Workspaces.Llm.Core/Mcp/McpOAuthOptions.cs:53-69):ApplicationServicesexposesSecretProvider(features/Phantom.Workspaces/Services/ApplicationServices.cs:98) but has noMcpOAuthOptionsmember, so even thoughAgentSessionShortcutContextreads other services frommainWindowViewModel.ApplicationServices, there is nothing for it to forward.Affected Files
features/Phantom.Workspaces/ViewModels/AgentSessionShortcutContext.csAgentServices(74-85) setsSecretProviderbut omitsMcpOAuthOptions— the direct cause.features/Phantom.Workspaces/Services/ApplicationServices.csSecretProvider(98) for the shortcut context to forward, but has noMcpOAuthOptionsproperty or ctor parameter.features/Phantom.Workspaces/App.axaml.csmcpOAuthOptions(313) and threads it only into theAgentChatFactoryAgentServices(316); does not pass it intoApplicationServices(318-327).features/Phantom.Workspaces.Llm.Core/Mcp/McpTransportFactory.csResolveOAuthOptionsfalls back toMcpOAuthOptions.DefaultwhenAgentServices.McpOAuthOptionsis null.features/Phantom.Workspaces.Llm.Core/Mcp/McpOAuthOptions.csDefault+CreateNotConfiguredDelegate(53-69) — the throwing stub that surfaces the misconfiguration.features/Phantom.Workspaces/Services/Mcp/McpOAuthComposition.csfeatures/Phantom.Workspaces.Llm.Interfaces/AgentServices.csobject? McpOAuthOptions { get; init; }seam.Design / Fix
Thread the single, process-wide
McpOAuthOptionsinstance to every GUI launch path, exactly asSecretProvideralready is.Carry
McpOAuthOptionsonApplicationServices. Add an optional ctor parameterobject? mcpOAuthOptions = nulland a publicobject? McpOAuthOptions { get; }property (typedobject?to matchAgentServices.McpOAuthOptionsand avoid a Core→host dependency).Populate it in
App.axaml.cs. Pass the already-builtmcpOAuthOptions(313) into theApplicationServicesconstructor (318-327), alongsidesecretProvider:.Forward it in
AgentSessionShortcutContext.CreateAgentServices. Add:so session launches reuse the same interactive redirect handler + loopback listener + token cache.
Audit sibling launch sites. Any other GUI path that constructs
AgentServicesfor a session (e.g.MainWindowViewModel, auto-resume via trusted executors) must likewise setMcpOAuthOptions. 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.Considered / Background
AgentSessionShortcutContextcallMcpOAuthComposition.CreateOptions(...)itself, but that would allocate a second loopback listener/handler per session and duplicate host composition. Sharing the app-level instance viaApplicationServicesis preferred and mirrors the existingSecretProviderwiring.Expected Tests
AgentSessionShortcutContext_CreateAgentServices_ThreadsMcpOAuthOptionsAgentSessionShortcutContextTestsAgentServicesbuilt for a session launch hasMcpOAuthOptionsset to the instance fromApplicationServices(not null).ApplicationServices_McpOAuthOptions_ExposesInjectedInstanceAgentSessionShortcutContextTestsApplicationServicesreturns themcpOAuthOptionspassed to its constructor.CreateMcpTransport_OAuthWithConfiguredDelegate_DoesNotUseFailingDefaultMcpTransportFactoryTestsAgentServices.McpOAuthOptionscarrying aRedirectDelegateProvider, the resolvedAuthorizationRedirectDelegateis the injected one, not the throwing default.CreateMcpTransport_OAuthWithoutConfiguredDelegate_UsesFailingDefaultDelegateMcpTransportFactoryTests