Skip to content

Commit e3aae80

Browse files
rojiCopilot
andcommitted
Add MCP OAuth lifecycle SDK support
Expose host-delegated MCP OAuth handling across SDK languages, sync generated RPC and event models to the lifecycle contract, and add cross-language E2E coverage for initial auth, refresh, upscope, reauth, and cancellation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c638a5e commit e3aae80

88 files changed

Lines changed: 7621 additions & 92 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
@@ -630,6 +630,7 @@ private CopilotSession InitializeSession(
630630
this);
631631
session.RegisterTools(config.Tools ?? []);
632632
session.RegisterPermissionHandler(config.OnPermissionRequest);
633+
session.RegisterMcpAuthHandler(config.OnMcpAuthRequest);
633634
session.RegisterCommands(config.Commands);
634635
session.RegisterElicitationHandler(config.OnElicitationRequest);
635636
session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest);
@@ -1080,6 +1081,11 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
10801081
$"session.create returned sessionId {response.SessionId} but the caller requested {localSessionId}.");
10811082
}
10821083

1084+
if (config.OnMcpAuthRequest is not null)
1085+
{
1086+
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
1087+
}
1088+
10831089
session.WorkspacePath = response.WorkspacePath;
10841090
session.SetCapabilities(response.Capabilities);
10851091
session.SetOpenCanvases(response.OpenCanvases);
@@ -1166,6 +1172,10 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
11661172
transformCallbacks,
11671173
hasHooks,
11681174
"CopilotClient.ResumeSessionAsync");
1175+
if (config.OnMcpAuthRequest is not null)
1176+
{
1177+
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
1178+
}
11691179

11701180
try
11711181
{

dotnet/src/Generated/Rpc.cs

Lines changed: 138 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Generated/SessionEvents.cs

Lines changed: 405 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Session.cs

Lines changed: 113 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,80 @@ 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+
}
797+
798+
private async Task TryCancelMcpAuthRequestAsync(string requestId)
799+
{
800+
try
801+
{
802+
await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, new McpOauthPendingRequestResponseCancelled());
803+
}
804+
catch (IOException)
805+
{
806+
// Connection lost — nothing we can do.
807+
}
808+
catch (ObjectDisposedException)
809+
{
810+
// Connection already disposed — nothing we can do.
811+
}
812+
catch (RemoteRpcException)
813+
{
814+
// The pending request may already be gone — nothing we can do.
815+
}
816+
}
817+
705818
/// <summary>
706819
/// Executes a tool handler and sends the result back via the HandlePendingToolCall RPC.
707820
/// </summary>

dotnet/src/Types.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,72 @@ public sealed class ElicitationContext
11281128
public string? Url { get; set; }
11291129
}
11301130

1131+
/// <summary>
1132+
/// Context for an MCP OAuth request callback.
1133+
/// </summary>
1134+
[Experimental(Diagnostics.Experimental)]
1135+
public sealed class McpAuthContext
1136+
{
1137+
/// <summary>Identifier of the session that triggered the MCP OAuth request.</summary>
1138+
public string SessionId { get; set; } = string.Empty;
1139+
1140+
/// <summary>Identifier of the pending MCP OAuth request.</summary>
1141+
public string RequestId { get; set; } = string.Empty;
1142+
1143+
/// <summary>Display name of the MCP server that requires OAuth.</summary>
1144+
public string ServerName { get; set; } = string.Empty;
1145+
1146+
/// <summary>URL of the MCP server that requires OAuth.</summary>
1147+
public string ServerUrl { get; set; } = string.Empty;
1148+
1149+
/// <summary>Why the runtime is requesting host-provided OAuth credentials.</summary>
1150+
public McpOauthRequestReason Reason { get; set; }
1151+
1152+
/// <summary>Parsed WWW-Authenticate parameters from the MCP server, if available.</summary>
1153+
public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; }
1154+
1155+
/// <summary>Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available.</summary>
1156+
public string? ResourceMetadata { get; set; }
1157+
1158+
/// <summary>Static OAuth client configuration, if the server specifies one.</summary>
1159+
public McpOauthRequiredStaticClientConfig? StaticClientConfig { get; set; }
1160+
}
1161+
1162+
/// <summary>
1163+
/// Host-provided OAuth token data for a pending MCP OAuth request.
1164+
/// </summary>
1165+
[Experimental(Diagnostics.Experimental)]
1166+
public sealed class McpAuthToken
1167+
{
1168+
/// <summary>Access token acquired by the SDK host.</summary>
1169+
public required string AccessToken { get; set; }
1170+
1171+
/// <summary>OAuth token type. Defaults to Bearer when omitted.</summary>
1172+
public string? TokenType { get; set; }
1173+
1174+
/// <summary>Token lifetime in seconds, if known.</summary>
1175+
public long? ExpiresIn { get; set; }
1176+
}
1177+
1178+
/// <summary>
1179+
/// Result returned by an MCP auth request handler.
1180+
/// </summary>
1181+
[Experimental(Diagnostics.Experimental)]
1182+
public sealed class McpAuthResult
1183+
{
1184+
/// <summary>Whether the request should be cancelled instead of resolved with a token.</summary>
1185+
public bool Cancelled { get; set; }
1186+
1187+
/// <summary>Host-provided token data. Ignored when <see cref="Cancelled"/> is true.</summary>
1188+
public McpAuthToken? Token { get; set; }
1189+
1190+
/// <summary>Create a token result.</summary>
1191+
public static McpAuthResult FromToken(McpAuthToken token) => new() { Token = token };
1192+
1193+
/// <summary>Create a cancellation result.</summary>
1194+
public static McpAuthResult Cancel() => new() { Cancelled = true };
1195+
}
1196+
11311197
// ============================================================================
11321198
// Session Capabilities
11331199
// ============================================================================
@@ -2719,6 +2785,7 @@ protected SessionConfigBase(SessionConfigBase? other)
27192785
OnElicitationRequest = other.OnElicitationRequest;
27202786
OnEvent = other.OnEvent;
27212787
OnExitPlanModeRequest = other.OnExitPlanModeRequest;
2788+
OnMcpAuthRequest = other.OnMcpAuthRequest;
27222789
OnPermissionRequest = other.OnPermissionRequest;
27232790
OnUserInputRequest = other.OnUserInputRequest;
27242791
Provider = other.Provider;
@@ -3180,6 +3247,14 @@ protected SessionConfigBase(SessionConfigBase? other)
31803247
[JsonIgnore]
31813248
public ICanvasHandler? CanvasHandler { get; set; }
31823249
#pragma warning restore GHCP001
3250+
3251+
/// <summary>
3252+
/// Optional handler for MCP OAuth requests from MCP servers.
3253+
/// When provided, the SDK can satisfy MCP server OAuth requests with host-provided token data or cancellation.
3254+
/// </summary>
3255+
[Experimental(Diagnostics.Experimental)]
3256+
[JsonIgnore]
3257+
public Func<McpAuthContext, Task<McpAuthResult?>>? OnMcpAuthRequest { get; set; }
31833258
}
31843259

31853260
/// <summary>

0 commit comments

Comments
 (0)