Skip to content

Commit 6a24d89

Browse files
committed
Merge remote-tracking branch 'origin/main' into mackinnonbuck-sdk-github-telemetry-contract
# Conflicts: # go/client_test.go
2 parents c810cbd + 6189f84 commit 6a24d89

74 files changed

Lines changed: 6279 additions & 173 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎dotnet/src/Client.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ private CopilotSession InitializeSession(
631631
this);
632632
session.RegisterTools(config.Tools ?? []);
633633
session.RegisterPermissionHandler(config.OnPermissionRequest);
634+
session.RegisterMcpAuthHandler(config.OnMcpAuthRequest);
634635
session.RegisterCommands(config.Commands);
635636
session.RegisterElicitationHandler(config.OnElicitationRequest);
636637
session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest);
@@ -1082,6 +1083,11 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
10821083
$"session.create returned sessionId {response.SessionId} but the caller requested {localSessionId}.");
10831084
}
10841085

1086+
if (config.OnMcpAuthRequest is not null)
1087+
{
1088+
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
1089+
}
1090+
10851091
session.WorkspacePath = response.WorkspacePath;
10861092
session.SetCapabilities(response.Capabilities);
10871093
session.SetOpenCanvases(response.OpenCanvases);
@@ -1168,6 +1174,10 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
11681174
transformCallbacks,
11691175
hasHooks,
11701176
"CopilotClient.ResumeSessionAsync");
1177+
if (config.OnMcpAuthRequest is not null)
1178+
{
1179+
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
1180+
}
11711181

11721182
try
11731183
{

‎dotnet/src/Session.cs‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public sealed partial class CopilotSession : IAsyncDisposable
6363
private readonly CopilotClient _parentClient;
6464

6565
private volatile Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? _permissionHandler;
66+
private volatile Func<McpAuthContext, Task<McpAuthResult?>>? _mcpAuthHandler;
6667
private volatile Func<UserInputRequest, UserInputInvocation, Task<UserInputResponse>>? _userInputHandler;
6768
private volatile Func<ElicitationContext, Task<ElicitationResult>>? _elicitationHandler;
6869
private volatile Func<ExitPlanModeRequest, ExitPlanModeInvocation, Task<ExitPlanModeResult>>? _exitPlanModeHandler;
@@ -558,6 +559,11 @@ internal void RegisterPermissionHandler(Func<PermissionRequest, PermissionInvoca
558559
_permissionHandler = handler;
559560
}
560561

562+
internal void RegisterMcpAuthHandler(Func<McpAuthContext, Task<McpAuthResult?>>? handler)
563+
{
564+
_mcpAuthHandler = handler;
565+
}
566+
561567
/// <summary>
562568
/// Handles a permission request from the Copilot CLI.
563569
/// </summary>
@@ -633,6 +639,39 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent)
633639
break;
634640
}
635641

642+
case McpOauthRequiredEvent authEvent:
643+
{
644+
var data = authEvent.Data;
645+
if (string.IsNullOrEmpty(data.RequestId))
646+
return;
647+
648+
var handler = _mcpAuthHandler;
649+
if (handler is null)
650+
{
651+
if (_logger.IsEnabled(LogLevel.Warning))
652+
{
653+
_logger.LogWarning(
654+
"Received MCP OAuth request without a registered MCP auth handler. SessionId={SessionId}, RequestId={RequestId}",
655+
SessionId,
656+
data.RequestId);
657+
}
658+
return;
659+
}
660+
661+
await ExecuteMcpAuthAndRespondAsync(data.RequestId, new McpAuthContext
662+
{
663+
SessionId = SessionId,
664+
RequestId = data.RequestId,
665+
ServerName = data.ServerName,
666+
ServerUrl = data.ServerUrl,
667+
Reason = data.Reason,
668+
WwwAuthenticateParams = data.WwwAuthenticateParams,
669+
ResourceMetadata = data.ResourceMetadata,
670+
StaticClientConfig = data.StaticClientConfig
671+
}, handler);
672+
break;
673+
}
674+
636675
case CommandExecuteEvent cmdEvent:
637676
{
638677
var data = cmdEvent.Data;
@@ -702,6 +741,91 @@ await HandleElicitationRequestAsync(
702741
}
703742
}
704743

744+
private async Task ExecuteMcpAuthAndRespondAsync(
745+
string requestId,
746+
McpAuthContext context,
747+
Func<McpAuthContext, Task<McpAuthResult?>> handler)
748+
{
749+
try
750+
{
751+
var result = await handler(context);
752+
McpOauthPendingRequestResponse response =
753+
result is { Cancelled: false, Token: { } token }
754+
? new McpOauthPendingRequestResponseToken
755+
{
756+
AccessToken = token.AccessToken,
757+
TokenType = token.TokenType,
758+
ExpiresIn = token.ExpiresIn
759+
}
760+
: new McpOauthPendingRequestResponseCancelled();
761+
762+
await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, response);
763+
}
764+
catch (OperationCanceledException)
765+
{
766+
await TryCancelMcpAuthRequestAsync(requestId);
767+
}
768+
catch (ObjectDisposedException)
769+
{
770+
await TryCancelMcpAuthRequestAsync(requestId);
771+
}
772+
catch (InvalidOperationException)
773+
{
774+
await TryCancelMcpAuthRequestAsync(requestId);
775+
}
776+
catch (ArgumentException)
777+
{
778+
await TryCancelMcpAuthRequestAsync(requestId);
779+
}
780+
catch (NotSupportedException)
781+
{
782+
await TryCancelMcpAuthRequestAsync(requestId);
783+
}
784+
catch (JsonException)
785+
{
786+
await TryCancelMcpAuthRequestAsync(requestId);
787+
}
788+
catch (RemoteRpcException)
789+
{
790+
await TryCancelMcpAuthRequestAsync(requestId);
791+
}
792+
catch (IOException)
793+
{
794+
await TryCancelMcpAuthRequestAsync(requestId);
795+
}
796+
catch (Exception ex) when (IsRecoverableMcpAuthFailure(ex))
797+
{
798+
await TryCancelMcpAuthRequestAsync(requestId);
799+
}
800+
}
801+
802+
private static bool IsRecoverableMcpAuthFailure(Exception exception)
803+
=> exception is not OperationCanceledException
804+
and not OutOfMemoryException
805+
and not StackOverflowException
806+
and not AccessViolationException
807+
and not AppDomainUnloadedException;
808+
809+
private async Task TryCancelMcpAuthRequestAsync(string requestId)
810+
{
811+
try
812+
{
813+
await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, new McpOauthPendingRequestResponseCancelled());
814+
}
815+
catch (IOException)
816+
{
817+
// Connection lost — nothing we can do.
818+
}
819+
catch (ObjectDisposedException)
820+
{
821+
// Connection already disposed — nothing we can do.
822+
}
823+
catch (RemoteRpcException)
824+
{
825+
// The pending request may already be gone — nothing we can do.
826+
}
827+
}
828+
705829
/// <summary>
706830
/// Executes a tool handler and sends the result back via the HandlePendingToolCall RPC.
707831
/// </summary>

‎dotnet/src/Types.cs‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,72 @@ public sealed class ElicitationContext
11371137
public string? Url { get; set; }
11381138
}
11391139

1140+
/// <summary>
1141+
/// Context for an MCP OAuth request callback.
1142+
/// </summary>
1143+
[Experimental(Diagnostics.Experimental)]
1144+
public sealed class McpAuthContext
1145+
{
1146+
/// <summary>Identifier of the session that triggered the MCP OAuth request.</summary>
1147+
public string SessionId { get; set; } = string.Empty;
1148+
1149+
/// <summary>Identifier of the pending MCP OAuth request.</summary>
1150+
public string RequestId { get; set; } = string.Empty;
1151+
1152+
/// <summary>Display name of the MCP server that requires OAuth.</summary>
1153+
public string ServerName { get; set; } = string.Empty;
1154+
1155+
/// <summary>URL of the MCP server that requires OAuth.</summary>
1156+
public string ServerUrl { get; set; } = string.Empty;
1157+
1158+
/// <summary>Why the runtime is requesting host-provided OAuth credentials.</summary>
1159+
public McpOauthRequestReason Reason { get; set; }
1160+
1161+
/// <summary>Parsed WWW-Authenticate parameters from the MCP server, if available.</summary>
1162+
public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; }
1163+
1164+
/// <summary>Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available.</summary>
1165+
public string? ResourceMetadata { get; set; }
1166+
1167+
/// <summary>Static OAuth client configuration, if the server specifies one.</summary>
1168+
public McpOauthRequiredStaticClientConfig? StaticClientConfig { get; set; }
1169+
}
1170+
1171+
/// <summary>
1172+
/// Host-provided OAuth token data for a pending MCP OAuth request.
1173+
/// </summary>
1174+
[Experimental(Diagnostics.Experimental)]
1175+
public sealed class McpAuthToken
1176+
{
1177+
/// <summary>Access token acquired by the SDK host.</summary>
1178+
public required string AccessToken { get; set; }
1179+
1180+
/// <summary>OAuth token type. Defaults to Bearer when omitted.</summary>
1181+
public string? TokenType { get; set; }
1182+
1183+
/// <summary>Token lifetime in seconds, if known.</summary>
1184+
public long? ExpiresIn { get; set; }
1185+
}
1186+
1187+
/// <summary>
1188+
/// Result returned by an MCP auth request handler.
1189+
/// </summary>
1190+
[Experimental(Diagnostics.Experimental)]
1191+
public sealed class McpAuthResult
1192+
{
1193+
/// <summary>Whether the request should be cancelled instead of resolved with a token.</summary>
1194+
public bool Cancelled { get; set; }
1195+
1196+
/// <summary>Host-provided token data. Ignored when <see cref="Cancelled"/> is true.</summary>
1197+
public McpAuthToken? Token { get; set; }
1198+
1199+
/// <summary>Create a token result.</summary>
1200+
public static McpAuthResult FromToken(McpAuthToken token) => new() { Token = token };
1201+
1202+
/// <summary>Create a cancellation result.</summary>
1203+
public static McpAuthResult Cancel() => new() { Cancelled = true };
1204+
}
1205+
11401206
// ============================================================================
11411207
// Session Capabilities
11421208
// ============================================================================
@@ -2728,6 +2794,7 @@ protected SessionConfigBase(SessionConfigBase? other)
27282794
OnElicitationRequest = other.OnElicitationRequest;
27292795
OnEvent = other.OnEvent;
27302796
OnExitPlanModeRequest = other.OnExitPlanModeRequest;
2797+
OnMcpAuthRequest = other.OnMcpAuthRequest;
27312798
OnPermissionRequest = other.OnPermissionRequest;
27322799
OnUserInputRequest = other.OnUserInputRequest;
27332800
Provider = other.Provider;
@@ -3189,6 +3256,14 @@ protected SessionConfigBase(SessionConfigBase? other)
31893256
[JsonIgnore]
31903257
public ICanvasHandler? CanvasHandler { get; set; }
31913258
#pragma warning restore GHCP001
3259+
3260+
/// <summary>
3261+
/// Optional handler for MCP OAuth requests from MCP servers.
3262+
/// When provided, the SDK can satisfy MCP server OAuth requests with host-provided token data or cancellation.
3263+
/// </summary>
3264+
[Experimental(Diagnostics.Experimental)]
3265+
[JsonIgnore]
3266+
public Func<McpAuthContext, Task<McpAuthResult?>>? OnMcpAuthRequest { get; set; }
31923267
}
31933268

31943269
/// <summary>

0 commit comments

Comments
 (0)