Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ private CopilotSession InitializeSession(
this);
session.RegisterTools(config.Tools ?? []);
session.RegisterPermissionHandler(config.OnPermissionRequest);
session.RegisterMcpAuthHandler(config.OnMcpAuthRequest);
session.RegisterCommands(config.Commands);
session.RegisterElicitationHandler(config.OnElicitationRequest);
session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest);
Expand Down Expand Up @@ -1080,6 +1081,11 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
$"session.create returned sessionId {response.SessionId} but the caller requested {localSessionId}.");
}

if (config.OnMcpAuthRequest is not null)
{
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
}

session.WorkspacePath = response.WorkspacePath;
session.SetCapabilities(response.Capabilities);
session.SetOpenCanvases(response.OpenCanvases);
Expand Down Expand Up @@ -1166,6 +1172,10 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
transformCallbacks,
hasHooks,
"CopilotClient.ResumeSessionAsync");
if (config.OnMcpAuthRequest is not null)
{
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
}

try
{
Expand Down
147 changes: 138 additions & 9 deletions dotnet/src/Generated/Rpc.cs

Large diffs are not rendered by default.

412 changes: 410 additions & 2 deletions dotnet/src/Generated/SessionEvents.cs

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public sealed partial class CopilotSession : IAsyncDisposable
private readonly CopilotClient _parentClient;

private volatile Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? _permissionHandler;
private volatile Func<McpAuthContext, Task<McpAuthResult?>>? _mcpAuthHandler;
private volatile Func<UserInputRequest, UserInputInvocation, Task<UserInputResponse>>? _userInputHandler;
private volatile Func<ElicitationContext, Task<ElicitationResult>>? _elicitationHandler;
private volatile Func<ExitPlanModeRequest, ExitPlanModeInvocation, Task<ExitPlanModeResult>>? _exitPlanModeHandler;
Expand Down Expand Up @@ -558,6 +559,11 @@ internal void RegisterPermissionHandler(Func<PermissionRequest, PermissionInvoca
_permissionHandler = handler;
}

internal void RegisterMcpAuthHandler(Func<McpAuthContext, Task<McpAuthResult?>>? handler)
{
_mcpAuthHandler = handler;
}

/// <summary>
/// Handles a permission request from the Copilot CLI.
/// </summary>
Expand Down Expand Up @@ -633,6 +639,39 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent)
break;
}

case McpOauthRequiredEvent authEvent:
{
var data = authEvent.Data;
if (string.IsNullOrEmpty(data.RequestId))
return;

var handler = _mcpAuthHandler;
if (handler is null)
{
if (_logger.IsEnabled(LogLevel.Warning))
{
_logger.LogWarning(
"Received MCP OAuth request without a registered MCP auth handler. SessionId={SessionId}, RequestId={RequestId}",
SessionId,
data.RequestId);
}
return;
}

await ExecuteMcpAuthAndRespondAsync(data.RequestId, new McpAuthContext
{
SessionId = SessionId,
RequestId = data.RequestId,
ServerName = data.ServerName,
ServerUrl = data.ServerUrl,
Reason = data.Reason,
WwwAuthenticateParams = data.WwwAuthenticateParams,
ResourceMetadata = data.ResourceMetadata,
StaticClientConfig = data.StaticClientConfig
}, handler);
break;
}

case CommandExecuteEvent cmdEvent:
{
var data = cmdEvent.Data;
Expand Down Expand Up @@ -702,6 +741,80 @@ await HandleElicitationRequestAsync(
}
}

private async Task ExecuteMcpAuthAndRespondAsync(
string requestId,
McpAuthContext context,
Func<McpAuthContext, Task<McpAuthResult?>> handler)
{
try
{
var result = await handler(context);
McpOauthPendingRequestResponse response =
result is { Cancelled: false, Token: { } token }
? new McpOauthPendingRequestResponseToken
{
AccessToken = token.AccessToken,
TokenType = token.TokenType,
ExpiresIn = token.ExpiresIn
}
: new McpOauthPendingRequestResponseCancelled();

await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, response);
}
catch (OperationCanceledException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
catch (ObjectDisposedException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
catch (InvalidOperationException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
catch (ArgumentException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
catch (NotSupportedException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
catch (JsonException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
catch (RemoteRpcException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
catch (IOException)
{
await TryCancelMcpAuthRequestAsync(requestId);
}
}

private async Task TryCancelMcpAuthRequestAsync(string requestId)
{
try
{
await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, new McpOauthPendingRequestResponseCancelled());
}
catch (IOException)
{
// Connection lost — nothing we can do.
}
catch (ObjectDisposedException)
{
// Connection already disposed — nothing we can do.
}
catch (RemoteRpcException)
{
// The pending request may already be gone — nothing we can do.
}
}

/// <summary>
/// Executes a tool handler and sends the result back via the HandlePendingToolCall RPC.
/// </summary>
Expand Down
75 changes: 75 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,72 @@ public sealed class ElicitationContext
public string? Url { get; set; }
}

/// <summary>
/// Context for an MCP OAuth request callback.
/// </summary>
[Experimental(Diagnostics.Experimental)]
public sealed class McpAuthContext
{
/// <summary>Identifier of the session that triggered the MCP OAuth request.</summary>
public string SessionId { get; set; } = string.Empty;

/// <summary>Identifier of the pending MCP OAuth request.</summary>
public string RequestId { get; set; } = string.Empty;

/// <summary>Display name of the MCP server that requires OAuth.</summary>
public string ServerName { get; set; } = string.Empty;

/// <summary>URL of the MCP server that requires OAuth.</summary>
public string ServerUrl { get; set; } = string.Empty;

/// <summary>Why the runtime is requesting host-provided OAuth credentials.</summary>
public McpOauthRequestReason Reason { get; set; }

/// <summary>Parsed WWW-Authenticate parameters from the MCP server, if available.</summary>
public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; }

/// <summary>Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available.</summary>
public string? ResourceMetadata { get; set; }

/// <summary>Static OAuth client configuration, if the server specifies one.</summary>
public McpOauthRequiredStaticClientConfig? StaticClientConfig { get; set; }
Comment thread
roji marked this conversation as resolved.
}

/// <summary>
/// Host-provided OAuth token data for a pending MCP OAuth request.
/// </summary>
[Experimental(Diagnostics.Experimental)]
public sealed class McpAuthToken
{
/// <summary>Access token acquired by the SDK host.</summary>
public required string AccessToken { get; set; }

/// <summary>OAuth token type. Defaults to Bearer when omitted.</summary>
public string? TokenType { get; set; }

/// <summary>Token lifetime in seconds, if known.</summary>
public long? ExpiresIn { get; set; }
}

/// <summary>
/// Result returned by an MCP auth request handler.
/// </summary>
[Experimental(Diagnostics.Experimental)]
public sealed class McpAuthResult
{
/// <summary>Whether the request should be cancelled instead of resolved with a token.</summary>
public bool Cancelled { get; set; }

/// <summary>Host-provided token data. Ignored when <see cref="Cancelled"/> is true.</summary>
public McpAuthToken? Token { get; set; }

/// <summary>Create a token result.</summary>
public static McpAuthResult FromToken(McpAuthToken token) => new() { Token = token };

/// <summary>Create a cancellation result.</summary>
public static McpAuthResult Cancel() => new() { Cancelled = true };
}

// ============================================================================
// Session Capabilities
// ============================================================================
Expand Down Expand Up @@ -2719,6 +2785,7 @@ protected SessionConfigBase(SessionConfigBase? other)
OnElicitationRequest = other.OnElicitationRequest;
OnEvent = other.OnEvent;
OnExitPlanModeRequest = other.OnExitPlanModeRequest;
OnMcpAuthRequest = other.OnMcpAuthRequest;
OnPermissionRequest = other.OnPermissionRequest;
OnUserInputRequest = other.OnUserInputRequest;
Provider = other.Provider;
Expand Down Expand Up @@ -3180,6 +3247,14 @@ protected SessionConfigBase(SessionConfigBase? other)
[JsonIgnore]
public ICanvasHandler? CanvasHandler { get; set; }
#pragma warning restore GHCP001

/// <summary>
/// Optional handler for MCP OAuth requests from MCP servers.
/// When provided, the SDK can satisfy MCP server OAuth requests with host-provided token data or cancellation.
/// </summary>
[Experimental(Diagnostics.Experimental)]
[JsonIgnore]
public Func<McpAuthContext, Task<McpAuthResult?>>? OnMcpAuthRequest { get; set; }
}

/// <summary>
Expand Down
Loading