From bf1592a4af745af7786c80a99c59f607d7e5f718 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Fri, 28 Aug 2026 11:00:04 +0100 Subject: [PATCH 01/16] feat: add managed MCP session support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/mcp.md | 75 +++++- dotnet/src/Client.cs | 13 + dotnet/src/Generated/Rpc.cs | 23 ++ dotnet/src/Generated/SessionEvents.cs | 10 +- dotnet/src/Session.cs | 76 ++++++ dotnet/src/Types.cs | 71 ++++++ .../test/Unit/ClientSessionLifetimeTests.cs | 119 +++++++++ dotnet/test/Unit/SerializationTests.cs | 35 +++ go/client.go | 24 ++ go/client_test.go | 70 ++++++ go/rpc/zrpc.go | 44 +++- go/rpc/zrpc_encoding.go | 19 ++ go/rpc/zsession_events.go | 2 + go/session.go | 70 ++++++ go/session_test.go | 144 +++++++++++ go/types.go | 42 ++++ go/zsession_events.go | 1 + .../copilot/generated/McpServerSource.java | 6 +- .../generated/McpServersLoadedServer.java | 4 +- .../generated/rpc/ManagedMcpServerConfig.java | 36 +++ .../copilot/generated/rpc/McpServer.java | 4 +- .../generated/rpc/McpServerSource.java | 6 +- .../generated/rpc/SessionOpenOptions.java | 4 +- .../com/github/copilot/CopilotClient.java | 73 +++--- .../com/github/copilot/CopilotSession.java | 87 +++++++ .../github/copilot/SessionRequestBuilder.java | 8 + .../copilot/rpc/CreateSessionRequest.java | 16 ++ .../copilot/rpc/McpHeadersRefreshHandler.java | 27 ++ .../rpc/McpHeadersRefreshInvocation.java | 36 +++ .../copilot/rpc/McpHeadersRefreshRequest.java | 21 ++ .../copilot/rpc/McpHeadersRefreshResult.java | 59 +++++ .../copilot/rpc/ResumeSessionConfig.java | 50 ++++ .../copilot/rpc/ResumeSessionRequest.java | 16 ++ .../com/github/copilot/rpc/SessionConfig.java | 50 ++++ .../McpAuthInterestRegistrationTest.java | 39 +++ .../copilot/McpHeadersRefreshHandlerTest.java | 139 ++++++++++ .../copilot/SessionRequestBuilderTest.java | 26 ++ .../rpc/GeneratedRpcRecordsCoverageTest.java | 2 +- nodejs/src/client.ts | 16 ++ nodejs/src/generated/rpc.ts | 55 ++++ nodejs/src/generated/session-events.ts | 10 +- nodejs/src/index.ts | 4 + nodejs/src/session.ts | 70 ++++++ nodejs/src/types.ts | 50 ++++ nodejs/test/client.test.ts | 156 ++++++++++++ python/copilot/__init__.py | 10 + python/copilot/client.py | 58 +++++ python/copilot/generated/rpc.py | 82 +++++- python/copilot/generated/session_events.py | 9 +- python/copilot/session.py | 137 ++++++++++ python/test_client.py | 172 ++++++++++++- rust/src/generated/api_types.rs | 60 +++++ rust/src/generated/session_events.rs | 8 +- rust/src/handler.rs | 91 ++++++- rust/src/session.rs | 159 +++++++++++- rust/src/types.rs | 114 ++++++++- rust/src/wire.rs | 7 +- rust/tests/e2e/rpc_tasks_and_handlers.rs | 1 + rust/tests/session_test.rs | 238 +++++++++++++++++- 59 files changed, 2973 insertions(+), 81 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java create mode 100644 java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java diff --git a/docs/features/mcp.md b/docs/features/mcp.md index caac633275..31af134793 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -15,15 +15,86 @@ The Copilot SDK can integrate with **MCP servers** (Model Context Protocol) to e * Call external APIs * And much more -## Server types +## Server transports -The SDK supports two types of MCP servers: +The SDK supports two MCP transport families: | Type | Description | Use Case | |------|-------------|----------| | **Local/Stdio** | Runs as a subprocess, communicates via stdin/stdout | Local tools, file access, custom scripts | | **HTTP/SSE** | Remote server accessed via HTTP | Shared services, cloud-hosted tools | +Transport and configuration origin are separate concepts. A managed MCP server +uses the HTTP transport, but its configuration comes from a trusted host catalog +instead of user or workspace configuration. Session status and loaded-server +events report this distinction with `source: "managed"` and include the catalog +display name. + +## Managed MCP servers + +Managed MCP lets a trusted SDK host inject a catalog of non-secret hosted +servers for one session. The runtime keeps this catalog separate from +`mcpServers`, so existing local stdio, HTTP, SSE, and OAuth behavior remains +unchanged. + +The host supplies each server under a stable managed identity: + + + +```typescript +const session = await client.createSession({ + managedMcpServers: { + "github-enterprise": { + displayName: "GitHub Enterprise", + url: "https://mcp.example.com/", + tools: ["issues", "pull_requests"], + timeout: 30_000, + headersRefreshTtlMs: 60_000, + }, + }, + onMcpHeadersRefresh: async ({ serverName, serverUrl, reason }) => { + const credential = await broker.getCredential({ serverName, serverUrl, reason }); + return { + headers: { Authorization: credential.authorizationHeader }, + ttlMs: credential.expiresInMs, + }; + }, +}); +``` + +The corresponding configuration and callback names are: + +| SDK | Managed servers | Header refresh callback | +| --- | --- | --- | +| Node.js | `managedMcpServers` | `onMcpHeadersRefresh` | +| Python | `managed_mcp_servers` | `on_mcp_headers_refresh` | +| Go | `ManagedMCPServers` | `OnMCPHeadersRefresh` | +| .NET | `ManagedMcpServers` | `OnMcpHeadersRefresh` | +| Java | `setManagedMcpServers(...)` | `setOnMcpHeadersRefreshRequest(...)` | +| Rust | `with_managed_mcp_servers(...)` | `with_mcp_headers_handler(...)` | + +### Host responsibilities + +Managed MCP hosts must enforce these boundaries: + +* **Trusted catalog injection**: Only inject server identities, display metadata, + endpoints, and tool policy from a trusted catalog. Do not treat model output + or untrusted content as catalog configuration. +* **Memory-only credentials**: Keep access tokens and derived authorization + headers in memory. Do not place credentials in `managedMcpServers`, session + history, workspace state, or persistent MCP OAuth storage. +* **Expiry and revocation**: Set `ttlMs` to the remaining credential lifetime. + The runtime clamps it to `headersRefreshTtlMs`. Throw from the callback when + the broker denies access, fails, or reports revocation; the SDK forwards an + explicit broker error without converting it to a successful empty response. +* **Cold resume**: Re-supply both the managed catalog and the header refresh + callback when cold-resuming a session. Managed configuration and credentials + are not recovered from persisted session state. + +Returning no result from the callback reports that no dynamic headers are +available. Existing static `headers`, arbitrary HTTP servers, and MCP OAuth +handlers continue to use their current behavior. + ## Configuration ### Node.js / TypeScript diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f2da0a48f7..fecebec08d 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -825,6 +825,7 @@ private CopilotSession InitializeSession( config.OnPermissionRequest, config.EnableManagedSettings is true || config.ManagedSettings is not null); session.RegisterMcpAuthHandler(config.OnMcpAuthRequest); + session.RegisterMcpHeadersRefreshHandler(config.OnMcpHeadersRefresh); session.RegisterCommands(config.Commands); session.RegisterElicitationHandler(config.OnElicitationRequest); session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest); @@ -1206,6 +1207,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, + config.ManagedMcpServers, config.McpOAuthTokenStorage, "direct", config.CustomAgents, @@ -1307,6 +1309,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken); } + if (config.OnMcpHeadersRefresh is not null) + { + await session.Rpc.EventLog.RegisterInterestAsync("mcp.headers_refresh_required", cancellationToken); + } session.WorkspacePath = response.WorkspacePath; session.SetCapabilities(response.Capabilities); @@ -1456,6 +1462,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, + config.ManagedMcpServers, config.McpOAuthTokenStorage, "direct", config.CustomAgents, @@ -1514,6 +1521,10 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes { await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken); } + if (config.OnMcpHeadersRefresh is not null) + { + await session.Rpc.EventLog.RegisterInterestAsync("mcp.headers_refresh_required", cancellationToken); + } await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false); if (registrationId is not null) @@ -2884,6 +2895,7 @@ internal record CreateSessionRequest( bool? Streaming, bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, + [property: JsonPropertyName("managedMcpServers")] IDictionary? ManagedMcpServers, McpOAuthTokenStorageMode? McpOAuthTokenStorage, string? EnvValueMode, IList? CustomAgents, @@ -3011,6 +3023,7 @@ internal record ResumeSessionRequest( bool? Streaming, bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, + [property: JsonPropertyName("managedMcpServers")] IDictionary? ManagedMcpServers, McpOAuthTokenStorageMode? McpOAuthTokenStorage, string? EnvValueMode, IList? CustomAgents, diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index ba8b844d83..6607af8f28 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -9163,6 +9163,10 @@ public sealed class McpHostState [Experimental(Diagnostics.Experimental)] public sealed class McpServer { + /// Human-readable display name supplied by a managed server catalog. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + /// Error message if the server failed to connect. [JsonPropertyName("error")] public string? Error { get; set; } @@ -9918,6 +9922,7 @@ public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestHeaders), "headers")] [JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestNone), "none")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestError), "error")] public partial class McpHeadersHandlePendingHeadersRefreshRequest { /// The type discriminator. @@ -9937,6 +9942,11 @@ public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHe /// Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. [JsonPropertyName("headers")] public required IDictionary Headers { get; set; } + + /// Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ttlMs")] + public long? TtlMs { get; set; } } /// The none variant of . @@ -9948,6 +9958,19 @@ public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeade public override string Kind => "none"; } +/// The error variant of . +[Experimental(Diagnostics.Experimental)] +public partial class McpHeadersHandlePendingHeadersRefreshRequestError : McpHeadersHandlePendingHeadersRefreshRequest +{ + /// + [JsonIgnore] + public override string Kind => "error"; + + /// Host credential broker failure, denial, or revocation reason. + [JsonPropertyName("message")] + public required string Message { get; set; } +} + /// MCP headers refresh request id and the host response. [Experimental(Diagnostics.Experimental)] internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index bb2f3f9979..eb5452fc4e 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -9399,6 +9399,11 @@ public sealed partial class CustomAgentsUpdatedAgent /// Nested data type for McpServersLoadedServer. public sealed partial class McpServersLoadedServer { + /// Human-readable display name supplied by a managed server catalog. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + /// Error message if the server failed to connect. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("error")] @@ -13855,7 +13860,7 @@ public override void Write(Utf8JsonWriter writer, SkillSource value, JsonSeriali } } -/// Configuration source: user, workspace, plugin, or builtin. +/// Configuration source: user, workspace, plugin, builtin, or managed. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct McpServerSource : IEquatable @@ -13886,6 +13891,9 @@ public McpServerSource(string value) /// Server bundled with the runtime. public static McpServerSource Builtin { get; } = new("builtin"); + /// Server supplied by a trusted host-managed catalog. + public static McpServerSource Managed { get; } = new("managed"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(McpServerSource left, McpServerSource right) => left.Equals(right); diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 7d076d14b0..d1db689102 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -65,6 +65,7 @@ public sealed partial class CopilotSession : IAsyncDisposable private volatile Func>? _permissionHandler; private bool _managedSettingsEnabled; private volatile Func>? _mcpAuthHandler; + private volatile Func>? _mcpHeadersRefreshHandler; private volatile Func>? _userInputHandler; private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; @@ -590,6 +591,11 @@ internal void RegisterMcpAuthHandler(Func>? _mcpAuthHandler = handler; } + internal void RegisterMcpHeadersRefreshHandler(Func>? handler) + { + _mcpHeadersRefreshHandler = handler; + } + /// /// Handles a permission request from the Copilot CLI. /// @@ -699,6 +705,26 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) break; } + case McpHeadersRefreshRequiredEvent refreshEvent: + { + var data = refreshEvent.Data; + var handler = _mcpHeadersRefreshHandler; + if (string.IsNullOrEmpty(data.RequestId) || handler is null) + return; + + await ExecuteMcpHeadersRefreshAndRespondAsync( + data.RequestId, + new McpHeadersRefreshContext + { + SessionId = SessionId, + ServerName = data.ServerName, + ServerUrl = data.ServerUrl, + Reason = data.Reason + }, + handler); + break; + } + case CommandExecuteEvent cmdEvent: { var data = cmdEvent.Data; @@ -826,6 +852,56 @@ private async Task ExecuteMcpAuthAndRespondAsync( } } + private async Task ExecuteMcpHeadersRefreshAndRespondAsync( + string requestId, + McpHeadersRefreshContext context, + Func> handler) + { + McpHeadersHandlePendingHeadersRefreshRequest response; + try + { + var result = await handler(context); + response = result is null + ? new McpHeadersHandlePendingHeadersRefreshRequestNone() + : new McpHeadersHandlePendingHeadersRefreshRequestHeaders + { + Headers = result.Headers, + TtlMs = result.TtlMs + }; + } + catch (OperationCanceledException ex) + { + response = new McpHeadersHandlePendingHeadersRefreshRequestError + { + Message = ex.Message + }; + } + catch (Exception ex) when (IsRecoverableMcpAuthFailure(ex)) + { + response = new McpHeadersHandlePendingHeadersRefreshRequestError + { + Message = ex.Message + }; + } + + try + { + await Rpc.Mcp.Headers.HandlePendingHeadersRefreshRequestAsync(requestId, response); + } + 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. + } + } + private static bool IsRecoverableMcpAuthFailure(Exception exception) => exception is not OperationCanceledException and not OutOfMemoryException diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 6ed05e3064..6c3167284b 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1274,6 +1274,57 @@ public sealed class McpAuthResult public static McpAuthResult Cancel() => new() { Cancelled = true }; } +/// Context for a managed MCP dynamic-headers refresh callback. +[Experimental(Diagnostics.Experimental)] +public sealed class McpHeadersRefreshContext +{ + /// Identifier of the session that requested refreshed headers. + public string SessionId { get; set; } = string.Empty; + + /// Human-readable managed catalog display name. + public string ServerName { get; set; } = string.Empty; + + /// Hosted MCP streamable HTTP endpoint. + public string ServerUrl { get; set; } = string.Empty; + + /// Why the runtime invalidated or requested dynamic headers. + public McpHeadersRefreshRequiredReason Reason { get; set; } +} + +/// Dynamic headers and their optional credential-bounded cache lifetime. +[Experimental(Diagnostics.Experimental)] +public sealed class McpHeadersRefreshResult +{ + /// HTTP headers to overlay on requests to the managed MCP server. + public required IDictionary Headers { get; set; } + + /// Optional lifetime in milliseconds for the returned credentials. + public long? TtlMs { get; set; } +} + +/// +/// Non-secret hosted MCP server configuration supplied by a trusted managed catalog. +/// Credentials are provided only through . +/// +[Experimental(Diagnostics.Experimental)] +public sealed class ManagedMcpServerConfig +{ + /// Human-readable catalog display name. + public required string DisplayName { get; set; } + + /// Hosted MCP streamable HTTP endpoint. + public required string Url { get; set; } + + /// Tools to include. The runtime includes all tools when omitted. + public IList? Tools { get; set; } + + /// Timeout in milliseconds for tool discovery and tool calls. + public long? Timeout { get; set; } + + /// Maximum dynamic-header cache lifetime in milliseconds. + public long? HeadersRefreshTtlMs { get; set; } +} + // ============================================================================ // Session Capabilities // ============================================================================ @@ -3182,6 +3233,9 @@ protected SessionConfigBase(SessionConfigBase? other) ? new Dictionary(dict, dict.Comparer) : new Dictionary(other.McpServers)) : null; + ManagedMcpServers = other.ManagedMcpServers is not null + ? new Dictionary(other.ManagedMcpServers) + : null; McpOAuthTokenStorage = other.McpOAuthTokenStorage; Model = other.Model; ModelCapabilities = other.ModelCapabilities; @@ -3190,6 +3244,7 @@ protected SessionConfigBase(SessionConfigBase? other) OnEvent = other.OnEvent; OnExitPlanModeRequest = other.OnExitPlanModeRequest; OnMcpAuthRequest = other.OnMcpAuthRequest; + OnMcpHeadersRefresh = other.OnMcpHeadersRefresh; OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; @@ -3554,6 +3609,13 @@ protected SessionConfigBase(SessionConfigBase? other) /// public IDictionary? McpServers { get; set; } + /// + /// Non-secret hosted MCP servers injected from a trusted managed catalog. + /// Keys are stable managed identities. Re-supply this map on cold resume. + /// + [Experimental(Diagnostics.Experimental)] + public IDictionary? ManagedMcpServers { get; set; } + /// /// Controls how MCP OAuth tokens are stored for this session. /// Default: for safe multitenant behavior. @@ -3785,6 +3847,15 @@ protected SessionConfigBase(SessionConfigBase? other) [Experimental(Diagnostics.Experimental)] [JsonIgnore] public Func>? OnMcpAuthRequest { get; set; } + + /// + /// Supplies short-lived HTTP headers for managed MCP servers. Returning + /// reports no headers; exceptions are sent to the + /// runtime as explicit credential-broker errors. + /// + [Experimental(Diagnostics.Experimental)] + [JsonIgnore] + public Func>? OnMcpHeadersRefresh { get; set; } } /// diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index b61546c650..6eb55f396d 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -814,6 +814,121 @@ public async Task McpAuth_Handler_Exception_Cancels_Pending_Request() Assert.Equal("cancelled", request.Params.GetProperty("result").GetProperty("kind").GetString()); } + [Theory] + [InlineData(null)] + [InlineData(5000L)] + public async Task McpHeadersRefresh_Handler_Sends_Headers_With_Optional_Ttl(long? ttlMs) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + ManagedMcpServers = new Dictionary + { + ["github"] = new() + { + DisplayName = "GitHub", + Url = "https://example.com/mcp", + HeadersRefreshTtlMs = 60_000 + } + }, + OnMcpHeadersRefresh = context => + { + Assert.Equal("managed-session-name", context.ServerName); + Assert.Equal("https://example.com/mcp", context.ServerUrl); + Assert.Equal(McpHeadersRefreshRequiredReason.Startup, context.Reason); + return Task.FromResult(new() + { + Headers = new Dictionary { ["Authorization"] = "Bearer short-lived" }, + TtlMs = ttlMs + }); + } + }); + + DispatchEvent(session, new McpHeadersRefreshRequiredEvent + { + Data = new McpHeadersRefreshRequiredData + { + RequestId = "mcp-refresh-1", + ServerName = "managed-session-name", + ServerUrl = "https://example.com/mcp", + Reason = McpHeadersRefreshRequiredReason.Startup + } + }); + + var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); + var result = request.Params.GetProperty("result"); + Assert.Equal("headers", result.GetProperty("kind").GetString()); + Assert.Equal("Bearer short-lived", result.GetProperty("headers").GetProperty("Authorization").GetString()); + Assert.Equal(ttlMs.HasValue, result.TryGetProperty("ttlMs", out var ttl)); + if (ttlMs.HasValue) + Assert.Equal(ttlMs.Value, ttl.GetInt64()); + + var create = server.Requests.First(request => request.Method == "session.create"); + Assert.Equal("GitHub", create.Params.GetProperty("managedMcpServers").GetProperty("github").GetProperty("displayName").GetString()); + Assert.Contains( + server.Requests, + request => request.Method == "session.eventLog.registerInterest" + && request.Params.GetProperty("eventType").GetString() == "mcp.headers_refresh_required"); + } + + [Fact] + public async Task McpHeadersRefresh_Handler_Exception_Sends_Explicit_Broker_Error() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + OnMcpHeadersRefresh = _ => throw new ApplicationException("credential revoked") + }); + + DispatchEvent(session, new McpHeadersRefreshRequiredEvent + { + Data = new McpHeadersRefreshRequiredData + { + RequestId = "mcp-refresh-error", + ServerName = "GitHub", + ServerUrl = "https://example.com/mcp", + Reason = McpHeadersRefreshRequiredReason.AuthFailed + } + }); + + var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); + var result = request.Params.GetProperty("result"); + Assert.Equal("error", result.GetProperty("kind").GetString()); + Assert.Equal("credential revoked", result.GetProperty("message").GetString()); + } + + [Fact] + public async Task McpHeadersRefresh_Handler_Cancellation_Sends_Explicit_Broker_Error() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + OnMcpHeadersRefresh = _ => Task.FromCanceled(new CancellationToken(true)) + }); + + DispatchEvent(session, new McpHeadersRefreshRequiredEvent + { + Data = new McpHeadersRefreshRequiredData + { + RequestId = "mcp-refresh-cancelled", + ServerName = "GitHub", + ServerUrl = "https://example.com/mcp", + Reason = McpHeadersRefreshRequiredReason.AuthFailed + } + }); + + var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); + var result = request.Params.GetProperty("result"); + Assert.Equal("error", result.GetProperty("kind").GetString()); + Assert.False(string.IsNullOrWhiteSpace(result.GetProperty("message").GetString())); + } + [Fact] public async Task Generated_Session_Rpc_Throws_When_Session_Disposed() { @@ -1527,6 +1642,10 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { ["success"] = true }, + "session.mcp.headers.handlePendingHeadersRefreshRequest" => new Dictionary + { + ["success"] = true + }, "session.permissions.handlePendingPermissionRequest" => new Dictionary { ["success"] = true diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 6edf168093..d2c3377ff8 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -17,6 +17,41 @@ namespace GitHub.Copilot.Test.Unit; /// public class SerializationTests { + [Fact] + public void SessionRequests_Serialize_ManagedMcpServers_On_Create_And_Resume() + { + var options = GetSerializerOptions(); + var managed = new Dictionary + { + ["github"] = new() + { + DisplayName = "GitHub", + Url = "https://example.com/mcp", + Tools = ["issues"], + Timeout = 30_000, + HeadersRefreshTtlMs = 60_000 + } + }; + + foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" }) + { + var requestType = GetNestedType(typeof(CopilotClient), requestName); + var request = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("ManagedMcpServers", managed)); + var json = JsonSerializer.Serialize(request, requestType, options); + using var document = JsonDocument.Parse(json); + var server = document.RootElement + .GetProperty("managedMcpServers") + .GetProperty("github"); + + Assert.Equal("GitHub", server.GetProperty("displayName").GetString()); + Assert.Equal("https://example.com/mcp", server.GetProperty("url").GetString()); + Assert.Equal(60_000, server.GetProperty("headersRefreshTtlMs").GetInt64()); + } + } + [Fact] public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() { diff --git a/go/client.go b/go/client.go index 4e44696a55..995f7595f6 100644 --- a/go/client.go +++ b/go/client.go @@ -855,6 +855,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.WorkingDirectory = config.WorkingDirectory req.AdditionalDirectories = config.AdditionalDirectories req.MCPServers = config.MCPServers + req.ManagedMCPServers = config.ManagedMCPServers req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.EnvValueMode = "direct" req.CustomAgents = config.CustomAgents @@ -975,6 +976,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses s.registerTools(config.Tools) s.registerPermissionHandler(config.OnPermissionRequest) s.registerMCPAuthHandler(config.OnMCPAuthRequest) + s.registerMCPHeadersRefreshHandler(config.OnMCPHeadersRefresh) if config.OnUserInputRequest != nil { s.registerUserInputHandler(config.OnUserInputRequest) } @@ -1114,6 +1116,17 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return nil, err } } + if config.OnMCPHeadersRefresh != nil { + if _, err := c.client.Request(ctx, "session.eventLog.registerInterest", map[string]any{ + "sessionId": session.SessionID, + "eventType": "mcp.headers_refresh_required", + }); err != nil { + c.sessionsMux.Lock() + delete(c.sessions, registeredSessionID) + c.sessionsMux.Unlock() + return nil, err + } + } session.workspacePath = response.WorkspacePath session.setCapabilities(response.Capabilities) @@ -1261,6 +1274,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } req.ContinuePendingWork = config.ContinuePendingWork req.MCPServers = config.MCPServers + req.ManagedMCPServers = config.ManagedMCPServers req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.EnvValueMode = "direct" req.CustomAgents = config.CustomAgents @@ -1332,6 +1346,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, session.registerTools(config.Tools) session.registerPermissionHandler(config.OnPermissionRequest) session.registerMCPAuthHandler(config.OnMCPAuthRequest) + session.registerMCPHeadersRefreshHandler(config.OnMCPHeadersRefresh) if config.OnUserInputRequest != nil { session.registerUserInputHandler(config.OnUserInputRequest) } @@ -1416,6 +1431,15 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, return nil, err } } + if config.OnMCPHeadersRefresh != nil { + if _, err := c.client.Request(ctx, "session.eventLog.registerInterest", map[string]any{ + "sessionId": sessionID, + "eventType": "mcp.headers_refresh_required", + }); err != nil { + restoreReplacedSession() + return nil, err + } + } session.workspacePath = response.WorkspacePath session.setCapabilities(response.Capabilities) diff --git a/go/client_test.go b/go/client_test.go index c6ab0808cb..80706fbbd3 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -2139,6 +2139,76 @@ func TestClient_MCPAuthInterestRegistration(t *testing.T) { }) } +func TestClient_ManagedMCPCreateAndResume(t *testing.T) { + client, requests, cleanup := newInMemoryClient(t) + defer cleanup() + + ttl := int64(60_000) + managed := map[string]ManagedMCPServerConfig{ + "github": { + DisplayName: "GitHub", + URL: "https://example.com/mcp", + Tools: []string{"issues"}, + HeadersRefreshTtlMs: &ttl, + }, + } + handler := func(MCPHeadersRefreshRequest, MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { + return nil, nil + } + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + ManagedMCPServers: managed, + OnMCPHeadersRefresh: handler, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + + create, ok := findRequest(requests.snapshot(), "session.create") + if !ok { + t.Fatal("missing session.create request") + } + managedWire, ok := create.Params["managedMcpServers"].(map[string]any) + if !ok { + t.Fatalf("unexpected managedMcpServers: %#v", create.Params["managedMcpServers"]) + } + github, ok := managedWire["github"].(map[string]any) + if !ok || github["displayName"] != "GitHub" || github["headersRefreshTtlMs"] != float64(60_000) { + t.Fatalf("unexpected managed server payload: %#v", managedWire) + } + if !hasEventInterest(requests.snapshot(), "mcp.headers_refresh_required") { + t.Fatal("missing managed MCP headers refresh interest on create") + } + + requests.clear() + resumed, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{ + ManagedMCPServers: managed, + OnMCPHeadersRefresh: handler, + }) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + defer resumed.Disconnect() + + resume, ok := findRequest(requests.snapshot(), "session.resume") + if !ok || resume.Params["managedMcpServers"] == nil { + t.Fatalf("managed MCP servers missing from resume: %#v", resume.Params) + } + if !hasEventInterest(requests.snapshot(), "mcp.headers_refresh_required") { + t.Fatal("missing managed MCP headers refresh interest on resume") + } +} + +func hasEventInterest(requests []recordedRequest, eventType string) bool { + for _, request := range requests { + if request.Method == "session.eventLog.registerInterest" && request.Params["eventType"] == eventType { + return true + } + } + return false +} + func findRequest(requests []recordedRequest, method string) (recordedRequest, bool) { for _, r := range requests { if r.Method == method { diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 2b3943988d..e644a814d8 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -4630,6 +4630,23 @@ type LspInitializeRequest struct { WorkingDirectory *string `json:"workingDirectory,omitempty"` } +// Non-secret host-managed HTTP MCP server configuration. The containing map key is the +// stable managed identity; credentials are supplied dynamically by the host. +// Experimental: ManagedMCPServerConfig is part of an experimental API and may change or be +// removed. +type ManagedMCPServerConfig struct { + // Human-readable catalog display name. + DisplayName string `json:"displayName"` + // Maximum dynamic-header cache lifetime in milliseconds. + HeadersRefreshTtlMs *int64 `json:"headersRefreshTtlMs,omitempty"` + // Timeout in milliseconds for tool discovery and tool calls. + Timeout *int64 `json:"timeout,omitempty"` + // Tools to include. Defaults to all tools when omitted. + Tools []string `json:"tools,omitzero"` + // Hosted MCP streamable HTTP endpoint. + URL string `json:"url"` +} + // Validated device-managed settings discovered before a session exists. // Experimental: ManagedSettingsReadResult is part of an experimental API and may change or // be removed. @@ -5134,10 +5151,24 @@ func (r RawMCPHeadersHandlePendingHeadersRefreshRequestData) Kind() MCPHeadersHa return r.Discriminator } +type MCPHeadersHandlePendingHeadersRefreshRequestError struct { + // Host credential broker failure, denial, or revocation reason. + Message string `json:"message"` +} + +func (MCPHeadersHandlePendingHeadersRefreshRequestError) mcpHeadersHandlePendingHeadersRefreshRequest() { +} +func (MCPHeadersHandlePendingHeadersRefreshRequestError) Kind() MCPHeadersHandlePendingHeadersRefreshRequestKind { + return MCPHeadersHandlePendingHeadersRefreshRequestKindError +} + type MCPHeadersHandlePendingHeadersRefreshRequestHeaders struct { // Headers to overlay onto the MCP request. Dynamic headers override static config headers // but do not replace SDK-managed request headers. Headers map[string]string `json:"headers"` + // Optional lifetime in milliseconds for these returned headers. The runtime clamps its + // configured cache lifetime to this value. + TtlMs *int64 `json:"ttlMs,omitempty"` } func (MCPHeadersHandlePendingHeadersRefreshRequestHeaders) mcpHeadersHandlePendingHeadersRefreshRequest() { @@ -6328,6 +6359,8 @@ func (MCPServerConfigStdio) mcpSerializableServerConfig() {} // MCP server status entry, including config source/plugin source and any connection error. // Experimental: MCPServer is part of an experimental API and may change or be removed. type MCPServer struct { + // Human-readable display name supplied by a managed server catalog. + DisplayName *string `json:"displayName,omitempty"` // Error message if the server failed to connect Error *string `json:"error,omitempty"` // Server name (config key) @@ -11931,6 +11964,12 @@ type SessionOpenOptions struct { LogInteractiveShells *bool `json:"logInteractiveShells,omitempty"` // Identifier sent to LSP-style integrations. LspClientName *string `json:"lspClientName,omitempty"` + // Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed + // provenance is runtime-established from this separate field and credentials are supplied + // through dynamic-header refresh. + // Experimental: ManagedMCPServers is part of an experimental API and may change or be + // removed. + ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitzero"` // Permissions-only enterprise policy injected by the SDK host at session create or resume. // Composes restrictively with self-fetched and device policy and is not persisted. ManagedSettings *SessionManagedSettings `json:"managedSettings,omitempty"` @@ -16947,6 +16986,7 @@ const ( type MCPHeadersHandlePendingHeadersRefreshRequestKind string const ( + MCPHeadersHandlePendingHeadersRefreshRequestKindError MCPHeadersHandlePendingHeadersRefreshRequestKind = "error" MCPHeadersHandlePendingHeadersRefreshRequestKindHeaders MCPHeadersHandlePendingHeadersRefreshRequestKind = "headers" MCPHeadersHandlePendingHeadersRefreshRequestKindNone MCPHeadersHandlePendingHeadersRefreshRequestKind = "none" ) @@ -17337,13 +17377,15 @@ const ( MCPServerConfigStdioTypeStdio MCPServerConfigStdioType = "stdio" ) -// Configuration source: user, workspace, plugin, or builtin +// Configuration source: user, workspace, plugin, builtin, or managed // Experimental: MCPServerSource is part of an experimental API and may change or be removed. type MCPServerSource string const ( // Server bundled with the runtime. MCPServerSourceBuiltin MCPServerSource = "builtin" + // Server supplied by a trusted host-managed catalog. + MCPServerSourceManaged MCPServerSource = "managed" // Server contributed by an installed plugin. MCPServerSourcePlugin MCPServerSource = "plugin" // Server configured in the user's global MCP configuration. diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 78788660f5..0722865974 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -2222,6 +2222,12 @@ func unmarshalMCPHeadersHandlePendingHeadersRefreshRequest(data []byte) (MCPHead } switch raw.Kind { + case MCPHeadersHandlePendingHeadersRefreshRequestKindError: + var d MCPHeadersHandlePendingHeadersRefreshRequestError + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case MCPHeadersHandlePendingHeadersRefreshRequestKindHeaders: var d MCPHeadersHandlePendingHeadersRefreshRequestHeaders if err := json.Unmarshal(data, &d); err != nil { @@ -2250,6 +2256,17 @@ func (r RawMCPHeadersHandlePendingHeadersRefreshRequestData) MarshalJSON() ([]by }) } +func (r MCPHeadersHandlePendingHeadersRefreshRequestError) MarshalJSON() ([]byte, error) { + type alias MCPHeadersHandlePendingHeadersRefreshRequestError + return json.Marshal(struct { + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func (r MCPHeadersHandlePendingHeadersRefreshRequestHeaders) MarshalJSON() ([]byte, error) { type alias MCPHeadersHandlePendingHeadersRefreshRequestHeaders return json.Marshal(struct { @@ -5397,6 +5414,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { IsExperimentalMode *bool `json:"isExperimentalMode,omitempty"` LogInteractiveShells *bool `json:"logInteractiveShells,omitempty"` LspClientName *string `json:"lspClientName,omitempty"` + ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitzero"` ManagedSettings *SessionManagedSettings `json:"managedSettings,omitempty"` MaxInlineBinaryBytes *int64 `json:"maxInlineBinaryBytes,omitempty"` Memory *MemoryConfiguration `json:"memory,omitempty"` @@ -5477,6 +5495,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.IsExperimentalMode = raw.IsExperimentalMode r.LogInteractiveShells = raw.LogInteractiveShells r.LspClientName = raw.LspClientName + r.ManagedMCPServers = raw.ManagedMCPServers r.ManagedSettings = raw.ManagedSettings r.MaxInlineBinaryBytes = raw.MaxInlineBinaryBytes r.Memory = raw.Memory diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 2abfd75a66..fca45e10fa 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -2931,6 +2931,8 @@ type MCPOauthWwwAuthenticateParams struct { // A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata. type MCPServersLoadedServer struct { + // Human-readable display name supplied by a managed server catalog. + DisplayName *string `json:"displayName,omitempty"` // Error message if the server failed to connect Error *string `json:"error,omitempty"` // Server name (config key) diff --git a/go/session.go b/go/session.go index 5d45d19ef2..229faf73cc 100644 --- a/go/session.go +++ b/go/session.go @@ -70,6 +70,8 @@ type Session struct { managedSettings bool mcpAuthHandler MCPAuthHandler mcpAuthMu sync.RWMutex + mcpHeadersRefreshHandler MCPHeadersRefreshHandler + mcpHeadersRefreshMu sync.RWMutex userInputHandler UserInputHandler userInputMux sync.RWMutex exitPlanModeHandler ExitPlanModeRequestHandler @@ -979,6 +981,18 @@ func (s *Session) getMCPAuthHandler() MCPAuthHandler { return s.mcpAuthHandler } +func (s *Session) registerMCPHeadersRefreshHandler(handler MCPHeadersRefreshHandler) { + s.mcpHeadersRefreshMu.Lock() + defer s.mcpHeadersRefreshMu.Unlock() + s.mcpHeadersRefreshHandler = handler +} + +func (s *Session) getMCPHeadersRefreshHandler() MCPHeadersRefreshHandler { + s.mcpHeadersRefreshMu.RLock() + defer s.mcpHeadersRefreshMu.RUnlock() + return s.mcpHeadersRefreshHandler +} + func (s *Session) handleMCPAuthRequest(request MCPAuthRequest) { handler := s.getMCPAuthHandler() if handler == nil { @@ -1506,6 +1520,21 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } s.handleMCPAuthRequest(request) + case *MCPHeadersRefreshRequiredData: + handler := s.getMCPHeadersRefreshHandler() + if d.RequestID == "" || handler == nil { + return + } + s.executeMCPHeadersRefreshAndRespond( + d.RequestID, + MCPHeadersRefreshRequest{ + ServerName: d.ServerName, + ServerURL: d.ServerURL, + Reason: d.Reason, + }, + handler, + ) + case *CommandExecuteData: s.executeCommandAndRespond(d.RequestID, d.CommandName, d.Command, d.Args) @@ -1670,6 +1699,47 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques }) } +func (s *Session) executeMCPHeadersRefreshAndRespond( + requestID string, + request MCPHeadersRefreshRequest, + handler MCPHeadersRefreshHandler, +) { + var wireResult rpc.MCPHeadersHandlePendingHeadersRefreshRequest + result, err := handler(request, MCPHeadersRefreshInvocation{SessionID: s.SessionID}) + switch { + case err != nil: + log.Printf( + "MCP headers refresh failed: session_id=%s server_name=%s error=%v", + s.SessionID, + request.ServerName, + err, + ) + wireResult = rpc.MCPHeadersHandlePendingHeadersRefreshRequestError{Message: err.Error()} + case result == nil: + wireResult = rpc.MCPHeadersHandlePendingHeadersRefreshRequestNone{} + default: + wireResult = rpc.MCPHeadersHandlePendingHeadersRefreshRequestHeaders{ + Headers: result.Headers, + TtlMs: result.TTLMS, + } + } + + if _, rpcErr := s.RPC.MCP.Headers().HandlePendingHeadersRefreshRequest( + context.Background(), + &rpc.MCPHeadersHandlePendingHeadersRefreshRequestRequest{ + RequestID: requestID, + Result: wireResult, + }, + ); rpcErr != nil { + log.Printf( + "failed to send MCP headers refresh response: session_id=%s request_id=%s error=%v", + s.SessionID, + requestID, + rpcErr, + ) + } +} + // GetEvents retrieves all events from this session's history. // // This returns the complete conversation history including user messages, diff --git a/go/session_test.go b/go/session_test.go index 74e212c418..547bb769dd 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" "io" "strconv" @@ -203,6 +204,149 @@ func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { } } +func TestSession_MCPHeadersRefreshResponses(t *testing.T) { + ttl := int64(5_000) + tests := []struct { + name string + result *MCPHeadersRefreshResult + err error + expected map[string]any + }{ + { + name: "headers without ttl", + result: &MCPHeadersRefreshResult{Headers: map[string]string{"Authorization": "Bearer first"}}, + expected: map[string]any{ + "kind": "headers", + "headers": map[string]any{"Authorization": "Bearer first"}, + }, + }, + { + name: "headers with ttl", + result: &MCPHeadersRefreshResult{ + Headers: map[string]string{"Authorization": "Bearer refreshed"}, + TTLMS: &ttl, + }, + expected: map[string]any{ + "kind": "headers", + "headers": map[string]any{"Authorization": "Bearer refreshed"}, + "ttlMs": float64(5_000), + }, + }, + { + name: "no headers", + expected: map[string]any{"kind": "none"}, + }, + { + name: "broker error", + err: errors.New("credential revoked"), + expected: map[string]any{"kind": "error", "message": "credential revoked"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + defer stdinR.Close() + defer stdinW.Close() + defer stdoutR.Close() + defer stdoutW.Close() + + client := jsonrpc2.NewClient(stdinW, stdoutR) + client.Start() + defer client.Stop() + + paramsCh := make(chan map[string]any, 1) + errCh := make(chan error, 1) + go func() { + frame, err := readTestJSONRPCFrame(stdinR) + if err != nil { + errCh <- err + return + } + var request struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` + } + if err := json.Unmarshal(frame, &request); err != nil { + errCh <- err + return + } + if request.Method != "session.mcp.headers.handlePendingHeadersRefreshRequest" { + errCh <- fmt.Errorf("unexpected method %s", request.Method) + return + } + paramsCh <- request.Params + response, err := json.Marshal(map[string]any{ + "jsonrpc": "2.0", + "id": json.RawMessage(request.ID), + "result": map[string]any{"success": true}, + }) + if err != nil { + errCh <- err + return + } + _, err = fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(response), response) + if err != nil { + errCh <- err + } + }() + + session := &Session{ + SessionID: "session-1", + client: client, + RPC: rpc.NewSessionRPC(client, "session-1"), + } + session.registerMCPHeadersRefreshHandler( + func(request MCPHeadersRefreshRequest, invocation MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { + if request.ServerName != "GitHub" || request.Reason != MCPHeadersRefreshRequiredReasonStartup { + t.Fatalf("unexpected request: %#v", request) + } + if invocation.SessionID != "session-1" { + t.Fatalf("unexpected invocation: %#v", invocation) + } + return tt.result, tt.err + }, + ) + session.handleBroadcastEvent(SessionEvent{ + Data: &MCPHeadersRefreshRequiredData{ + RequestID: "refresh-1", + ServerName: "GitHub", + ServerURL: "https://example.com/mcp", + Reason: MCPHeadersRefreshRequiredReasonStartup, + }, + }) + + select { + case params := <-paramsCh: + if params["requestId"] != "refresh-1" { + t.Fatalf("unexpected requestId: %v", params["requestId"]) + } + if !mapsEqual(params["result"], tt.expected) { + t.Fatalf("unexpected result: %#v", params["result"]) + } + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for MCP headers refresh response") + } + }) + } +} + +func mapsEqual(actual any, expected map[string]any) bool { + actualJSON, err := json.Marshal(actual) + if err != nil { + return false + } + expectedJSON, err := json.Marshal(expected) + if err != nil { + return false + } + return string(actualJSON) == string(expectedJSON) +} + func TestMCPAuthRequestAllowsMissingOptionalMetadata(t *testing.T) { request := MCPAuthRequest{RequestID: "oauth-request"} if request.ResourceMetadata != nil { diff --git a/go/types.go b/go/types.go index 1d98e06158..5f1ba06a55 100644 --- a/go/types.go +++ b/go/types.go @@ -500,6 +500,36 @@ type MCPAuthInvocation struct { // MCPAuthHandler handles MCP OAuth requests from the runtime. type MCPAuthHandler func(request MCPAuthRequest, invocation MCPAuthInvocation) (*MCPAuthResult, error) +// ManagedMCPServerConfig is a non-secret hosted MCP server from a trusted +// managed catalog. Credentials are supplied only through [MCPHeadersRefreshHandler]. +// +// Experimental: ManagedMCPServerConfig is part of an experimental API and may change. +type ManagedMCPServerConfig = rpc.ManagedMCPServerConfig + +// MCPHeadersRefreshRequest identifies a managed MCP server whose short-lived +// HTTP headers need refreshing. +type MCPHeadersRefreshRequest struct { + ServerName string + ServerURL string + Reason MCPHeadersRefreshRequiredReason +} + +// MCPHeadersRefreshResult contains dynamic headers and their optional +// credential-bounded cache lifetime. A nil result means no headers are available. +type MCPHeadersRefreshResult struct { + Headers map[string]string + TTLMS *int64 +} + +// MCPHeadersRefreshInvocation provides context for a managed MCP headers refresh. +type MCPHeadersRefreshInvocation struct { + SessionID string +} + +// MCPHeadersRefreshHandler supplies short-lived HTTP headers for managed MCP +// servers. Returning an error sends an explicit broker error to the runtime. +type MCPHeadersRefreshHandler func(request MCPHeadersRefreshRequest, invocation MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) + // UserInputRequest represents a request for user input from the agent type UserInputRequest struct { Question string @@ -1337,6 +1367,8 @@ type SessionConfig struct { // When provided, the SDK can satisfy MCP server OAuth requests with host-provided // token data or cancellation. OnMCPAuthRequest MCPAuthHandler + // OnMCPHeadersRefresh supplies short-lived HTTP headers for ManagedMCPServers. + OnMCPHeadersRefresh MCPHeadersRefreshHandler // GitHubTokenProvider acquires session-scoped GitHub tokens on demand. It // cannot be combined with GitHubToken. GitHubTokenProvider GitHubTokenProvider @@ -1420,6 +1452,9 @@ type SessionConfig struct { ModelCapabilities *rpc.ModelCapabilitiesOverride // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig + // ManagedMCPServers contains non-secret hosted MCP servers keyed by stable + // managed identity. Re-supply this map when cold-resuming a session. + ManagedMCPServers map[string]ManagedMCPServerConfig // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -1914,6 +1949,8 @@ type ResumeSessionConfig struct { // OnMCPAuthRequest is an optional handler for MCP OAuth requests from MCP servers. // See SessionConfig.OnMCPAuthRequest. OnMCPAuthRequest MCPAuthHandler + // OnMCPHeadersRefresh supplies short-lived HTTP headers for ManagedMCPServers. + OnMCPHeadersRefresh MCPHeadersRefreshHandler // OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool) OnUserInputRequest UserInputHandler // Hooks configures hook handlers for session lifecycle events @@ -1970,6 +2007,9 @@ type ResumeSessionConfig struct { IncludeSubAgentStreamingEvents *bool // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig + // ManagedMCPServers contains non-secret hosted MCP servers keyed by stable + // managed identity. Re-supply this map when cold-resuming a session. + ManagedMCPServers map[string]ManagedMCPServerConfig // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -2518,6 +2558,7 @@ type createSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` @@ -2627,6 +2668,7 @@ type resumeSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` diff --git a/go/zsession_events.go b/go/zsession_events.go index 11a1bd666f..f2b53fc658 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -523,6 +523,7 @@ const ( MCPOauthRequestReasonUpscope = rpc.MCPOauthRequestReasonUpscope MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials = rpc.MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials MCPServerSourceBuiltin = rpc.MCPServerSourceBuiltin + MCPServerSourceManaged = rpc.MCPServerSourceManaged MCPServerSourcePlugin = rpc.MCPServerSourcePlugin MCPServerSourceUser = rpc.MCPServerSourceUser MCPServerSourceWorkspace = rpc.MCPServerSourceWorkspace diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java index 63514743ab..5ea2c39f51 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, or builtin + * Configuration source: user, workspace, plugin, builtin, or managed * * @since 1.0.0 */ @@ -23,7 +23,9 @@ public enum McpServerSource { /** The {@code plugin} variant. */ PLUGIN("plugin"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code managed} variant. */ + MANAGED("managed"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java index c4567f30fa..bc840d90bd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java @@ -34,6 +34,8 @@ public record McpServersLoadedServer( /** Name of the plugin that supplied the effective MCP server config, only when source is plugin */ @JsonProperty("pluginName") String pluginName, /** Version of the plugin that supplied the effective MCP server config, only when source is plugin */ - @JsonProperty("pluginVersion") String pluginVersion + @JsonProperty("pluginVersion") String pluginVersion, + /** Human-readable display name supplied by a managed server catalog. */ + @JsonProperty("displayName") String displayName ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java new file mode 100644 index 0000000000..4a202063af --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedMcpServerConfig( + /** Human-readable catalog display name. */ + @JsonProperty("displayName") String displayName, + /** Hosted MCP streamable HTTP endpoint. */ + @JsonProperty("url") String url, + /** Tools to include. Defaults to all tools when omitted. */ + @JsonProperty("tools") List tools, + /** Timeout in milliseconds for tool discovery and tool calls. */ + @JsonProperty("timeout") Long timeout, + /** Maximum dynamic-header cache lifetime in milliseconds. */ + @JsonProperty("headersRefreshTtlMs") Long headersRefreshTtlMs +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java index 14a9118d0b..8251c1f1fb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java @@ -32,6 +32,8 @@ public record McpServer( /** Plugin version that provided this server, when source is plugin. */ @JsonProperty("sourcePluginVersion") String sourcePluginVersion, /** Error message if the server failed to connect */ - @JsonProperty("error") String error + @JsonProperty("error") String error, + /** Human-readable display name supplied by a managed server catalog. */ + @JsonProperty("displayName") String displayName ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java index f709df96dd..72b1503d59 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, or builtin + * Configuration source: user, workspace, plugin, builtin, or managed * * @since 1.0.0 */ @@ -23,7 +23,9 @@ public enum McpServerSource { /** The {@code plugin} variant. */ PLUGIN("plugin"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code managed} variant. */ + MANAGED("managed"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index 34706a68e1..8421bb2666 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -160,6 +160,8 @@ public record SessionOpenOptions( /** Memory configuration for this session. */ @JsonProperty("memory") MemoryConfiguration memory, /** Capabilities enabled for this session. */ - @JsonProperty("sessionCapabilities") List sessionCapabilities + @JsonProperty("sessionCapabilities") List sessionCapabilities, + /** Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed provenance is runtime-established from this separate field and credentials are supplied through dynamic-header refresh. */ + @JsonProperty("managedMcpServers") Map managedMcpServers ) { } diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index ea2b0b67dd..04c4bec4cc 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -1018,32 +1018,29 @@ public CompletableFuture createSession(SessionConfig config) { session.setGitHubTokenProviderRegistration(tokenRegistration); } registeredIdHolder[0] = returnedId; - CompletableFuture interest = config.getOnMcpAuthRequest() != null - ? session.getRpc().eventLog.registerInterest( - new SessionEventLogRegisterInterestParams(returnedId, "mcp.oauth_required")) - : CompletableFuture.completedFuture(null); + CompletableFuture interests = registerMcpEventInterests(session, returnedId, + config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefreshRequest() != null); session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); session.setOpenCanvases(response.openCanvases()); - return interest.thenCompose(interestResult -> { - logMcpAuthInterestRegistration(interestResult); - return updateSessionOptionsForMode(session, config.getSkipCustomInstructions().orElse(null), - config.getCustomAgentsLocalOnly().orElse(null), - config.getCoauthorEnabled().orElse(null), - config.getManageScheduleEnabled().orElse(null), config.getIncludedBuiltinSkills()); - }).thenApply(v -> { - if (tokenRegistration != null) { - tokenRegistration.claim(session.getSessionId()); - } else { - gitHubTokenProviders.retire(session.getSessionId()); - } - LoggingHelpers.logTiming(LOG, Level.FINE, - "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" - + session.getSessionId(), - totalNanos); - return session; - }); + return interests.thenCompose(v -> updateSessionOptionsForMode(session, + config.getSkipCustomInstructions().orElse(null), + config.getCustomAgentsLocalOnly().orElse(null), + config.getCoauthorEnabled().orElse(null), + config.getManageScheduleEnabled().orElse(null), config.getIncludedBuiltinSkills())) + .thenApply(v -> { + if (tokenRegistration != null) { + tokenRegistration.claim(session.getSessionId()); + } else { + gitHubTokenProviders.retire(session.getSessionId()); + } + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" + + session.getSessionId(), + totalNanos); + return session; + }); }).exceptionally(ex -> { if (registeredIdHolder[0] != null) { sessions.remove(registeredIdHolder[0]); @@ -1060,9 +1057,26 @@ public CompletableFuture createSession(SessionConfig config) { }); } - private static void logMcpAuthInterestRegistration(Object interestResult) { + private static CompletableFuture registerMcpEventInterests(CopilotSession session, String sessionId, + boolean auth, boolean headersRefresh) { + CompletableFuture interests = CompletableFuture.completedFuture(null); + if (auth) { + interests = interests.thenCompose(v -> session.getRpc().eventLog + .registerInterest(new SessionEventLogRegisterInterestParams(sessionId, "mcp.oauth_required")) + .thenAccept(result -> logMcpInterestRegistration(result, "OAuth"))); + } + if (headersRefresh) { + interests = interests.thenCompose(v -> session.getRpc().eventLog + .registerInterest( + new SessionEventLogRegisterInterestParams(sessionId, "mcp.headers_refresh_required")) + .thenAccept(result -> logMcpInterestRegistration(result, "headers refresh"))); + } + return interests; + } + + private static void logMcpInterestRegistration(Object interestResult, String eventName) { if (interestResult != null && LOG.isLoggable(Level.FINEST)) { - LOG.finest("MCP OAuth event interest registered"); + LOG.finest("MCP " + eventName + " event interest registered"); } } @@ -1188,14 +1202,9 @@ public CompletableFuture resumeSession(String sessionId, ResumeS rpcNanos); String returnedId = response.sessionId(); String interestSessionId = returnedId != null ? returnedId : sessionId; - CompletableFuture interest = config.getOnMcpAuthRequest() != null - ? session.getRpc().eventLog.registerInterest(new SessionEventLogRegisterInterestParams( - interestSessionId, "mcp.oauth_required")) - : CompletableFuture.completedFuture(null); - return interest.thenApply(interestResult -> { - logMcpAuthInterestRegistration(interestResult); - return response; - }); + return registerMcpEventInterests(session, interestSessionId, + config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefreshRequest() != null) + .thenApply(v -> response); }).thenCompose(response -> { session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index f3a35967d3..83beae4298 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -14,6 +14,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; +import java.util.concurrent.CompletionException; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -34,6 +35,7 @@ import com.github.copilot.generated.rpc.SessionLogParams; import com.github.copilot.generated.rpc.SessionLogLevel; import com.github.copilot.generated.rpc.SessionMcpOauthHandlePendingRequestParams; +import com.github.copilot.generated.rpc.SessionMcpHeadersHandlePendingHeadersRefreshRequestParams; import com.github.copilot.generated.rpc.ModelCapabilitiesOverride; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideLimits; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideSupports; @@ -50,6 +52,7 @@ import com.github.copilot.generated.CommandExecuteEvent; import com.github.copilot.generated.ElicitationRequestedEvent; import com.github.copilot.generated.ExternalToolRequestedEvent; +import com.github.copilot.generated.McpHeadersRefreshRequiredEvent; import com.github.copilot.generated.McpOauthRequiredEvent; import com.github.copilot.generated.PermissionRequestedEvent; import com.github.copilot.generated.SessionCanvasClosedEvent; @@ -87,6 +90,10 @@ import com.github.copilot.rpc.McpAuthInvocation; import com.github.copilot.rpc.McpAuthRequest; import com.github.copilot.rpc.McpAuthResult; +import com.github.copilot.rpc.McpHeadersRefreshHandler; +import com.github.copilot.rpc.McpHeadersRefreshInvocation; +import com.github.copilot.rpc.McpHeadersRefreshRequest; +import com.github.copilot.rpc.McpHeadersRefreshResult; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.PermissionInvocation; import com.github.copilot.rpc.PermissionRequest; @@ -189,6 +196,7 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference permissionHandler = new AtomicReference<>(); private volatile boolean managedSettingsEnabled; private final AtomicReference mcpAuthHandler = new AtomicReference<>(); + private final AtomicReference mcpHeadersRefreshHandler = new AtomicReference<>(); private final AtomicReference userInputHandler = new AtomicReference<>(); private final AtomicReference elicitationHandler = new AtomicReference<>(); private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); @@ -885,6 +893,17 @@ private void handleBroadcastEventAsync(SessionEvent event) { executeMcpAuthAndRespondAsync(new McpAuthRequest(data.requestId(), data.serverName(), data.serverUrl(), data.reason(), data.wwwAuthenticateParams(), data.resourceMetadata(), data.staticClientConfig()), handler); + } else if (event instanceof McpHeadersRefreshRequiredEvent headersEvent) { + var data = headersEvent.getData(); + if (data == null || data.requestId() == null) { + return; + } + McpHeadersRefreshHandler handler = mcpHeadersRefreshHandler.get(); + if (handler == null) { + return; + } + executeMcpHeadersRefreshAndRespondAsync(data.requestId(), + new McpHeadersRefreshRequest(data.serverName(), data.serverUrl(), data.reason()), handler); } else if (event instanceof CommandExecuteEvent cmdEvent) { var data = cmdEvent.getData(); if (data == null || data.requestId() == null || data.commandName() == null) { @@ -1135,6 +1154,70 @@ private void sendMcpAuthResponse(String requestId, McpAuthResult result) { } } + private void executeMcpHeadersRefreshAndRespondAsync(String requestId, McpHeadersRefreshRequest request, + McpHeadersRefreshHandler handler) { + Runnable task = () -> { + try { + var invocation = new McpHeadersRefreshInvocation().setSessionId(sessionId); + handler.handle(request, invocation) + .thenAccept(result -> sendMcpHeadersRefreshResponse(requestId, result)).exceptionally(ex -> { + sendMcpHeadersRefreshError(requestId, ex); + return null; + }); + } catch (Exception e) { + sendMcpHeadersRefreshError(requestId, e); + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, + "Executor rejected MCP headers refresh task for requestId=" + requestId + "; running inline", e); + task.run(); + } + } + + private void sendMcpHeadersRefreshResponse(String requestId, McpHeadersRefreshResult result) { + Map response; + if (result == null || result.headers() == null) { + response = Map.of("kind", "none"); + } else { + var headersResponse = new java.util.HashMap(); + headersResponse.put("kind", "headers"); + headersResponse.put("headers", result.headers()); + if (result.ttlMs() != null) { + headersResponse.put("ttlMs", result.ttlMs()); + } + response = headersResponse; + } + sendMcpHeadersRefreshResponse(requestId, response); + } + + private void sendMcpHeadersRefreshError(String requestId, Throwable error) { + sendMcpHeadersRefreshResponse(requestId, Map.of("kind", "error", "message", errorMessage(error))); + } + + private void sendMcpHeadersRefreshResponse(String requestId, Map response) { + try { + getRpc().mcp.headers.handlePendingHeadersRefreshRequest( + new SessionMcpHeadersHandlePendingHeadersRefreshRequestParams(sessionId, requestId, response)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending MCP headers refresh response for requestId=" + requestId, e); + } + } + + private static String errorMessage(Throwable error) { + Throwable current = error; + while (current instanceof CompletionException && current.getCause() != null) { + current = current.getCause(); + } + return current.getMessage() != null ? current.getMessage() : current.toString(); + } + /** * Registers custom tool handlers for this session. *

@@ -1407,6 +1490,10 @@ void registerMcpAuthHandler(McpAuthHandler handler) { mcpAuthHandler.set(handler); } + void registerMcpHeadersRefreshHandler(McpHeadersRefreshHandler handler) { + mcpHeadersRefreshHandler.set(handler); + } + /** * Handles a permission request from the Copilot CLI. *

diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index 4254c04ec4..697512c877 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -147,6 +147,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); + request.setManagedMcpServers(config.getManagedMcpServers()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setCustomAgents(config.getCustomAgents()); request.setCustomAgentsLocalOnly( @@ -300,6 +301,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); + request.setManagedMcpServers(config.getManagedMcpServers()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setCustomAgents(config.getCustomAgents()); request.setCustomAgentsLocalOnly( @@ -383,6 +385,9 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getOnMcpAuthRequest() != null) { session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); } + if (config.getOnMcpHeadersRefreshRequest() != null) { + session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefreshRequest()); + } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } @@ -435,6 +440,9 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getOnMcpAuthRequest() != null) { session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); } + if (config.getOnMcpHeadersRefreshRequest() != null) { + session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefreshRequest()); + } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 403893987d..3ab3da0616 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -113,6 +114,9 @@ public final class CreateSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; + @JsonProperty("managedMcpServers") + private Map managedMcpServers; + @JsonProperty("mcpOAuthTokenStorage") private String mcpOAuthTokenStorage; @@ -575,6 +579,18 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } + /** Gets managed MCP servers. @return the managed servers map */ + public Map getManagedMcpServers() { + return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + } + + /** + * Sets managed MCP servers. @param managedMcpServers the managed servers map + */ + public void setManagedMcpServers(Map managedMcpServers) { + this.managedMcpServers = managedMcpServers; + } + /** Gets MCP OAuth token storage mode. @return the storage mode */ public String getMcpOAuthTokenStorage() { return mcpOAuthTokenStorage; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java new file mode 100644 index 0000000000..cf357b6bd0 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.concurrent.CompletableFuture; + +/** + * Supplies dynamic headers for a host-managed MCP server. + * + * @since 1.0.0 + */ +@FunctionalInterface +public interface McpHeadersRefreshHandler { + /** + * Handles a dynamic-header refresh request. + * + * @param request + * the managed MCP server details + * @param invocation + * the invocation context with session information + * @return a future resolving to headers or an explicit no-result response + */ + CompletableFuture handle(McpHeadersRefreshRequest request, + McpHeadersRefreshInvocation invocation); +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java new file mode 100644 index 0000000000..f69b78aee2 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +/** + * Context for a managed MCP dynamic-header refresh invocation. + * + * @since 1.0.0 + */ +public class McpHeadersRefreshInvocation { + + private String sessionId; + + /** + * Gets the session ID. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public McpHeadersRefreshInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java new file mode 100644 index 0000000000..254a04376d --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.github.copilot.generated.McpHeadersRefreshRequiredReason; + +/** + * Request to refresh dynamic headers for a host-managed MCP server. + * + * @param serverName + * display name of the managed MCP server + * @param serverUrl + * URL of the managed MCP server + * @param reason + * reason the headers must be refreshed + * @since 1.0.0 + */ +public record McpHeadersRefreshRequest(String serverName, String serverUrl, McpHeadersRefreshRequiredReason reason) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java new file mode 100644 index 0000000000..a86737b76e --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.Map; + +/** + * Dynamic headers returned for a host-managed MCP server. + * + * @param headers + * headers to use, or {@code null} for no result + * @param ttlMs + * optional cache lifetime in milliseconds + * @since 1.0.0 + */ +public record McpHeadersRefreshResult(Map headers, Long ttlMs) { + + /** + * Creates a defensive copy of the supplied headers. + */ + public McpHeadersRefreshResult { + headers = headers == null ? null : Map.copyOf(headers); + } + + /** + * Creates a result with headers and no handler-specific cache lifetime. + * + * @param headers + * headers to use + * @return a headers result + */ + public static McpHeadersRefreshResult withHeaders(Map headers) { + return new McpHeadersRefreshResult(headers, null); + } + + /** + * Creates a result with headers and a cache lifetime. + * + * @param headers + * headers to use + * @param ttlMs + * cache lifetime in milliseconds + * @return a headers result + */ + public static McpHeadersRefreshResult withHeaders(Map headers, long ttlMs) { + return new McpHeadersRefreshResult(headers, ttlMs); + } + + /** + * Creates an explicit no-result response. + * + * @return a no-result response + */ + public static McpHeadersRefreshResult none() { + return new McpHeadersRefreshResult(null, null); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index a55c3454e7..f8a8ccef8f 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -16,6 +16,7 @@ import com.github.copilot.CopilotExperimental; import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -66,6 +67,7 @@ public class ResumeSessionConfig { private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; + private McpHeadersRefreshHandler onMcpHeadersRefreshRequest; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -84,6 +86,7 @@ public class ResumeSessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; + private Map managedMcpServers; private String mcpOAuthTokenStorage; private List customAgents; private DefaultAgentConfig defaultAgent; @@ -837,6 +840,28 @@ public ResumeSessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) return this; } + /** + * Gets the managed MCP dynamic-header refresh handler. + * + * @return the handler, or {@code null} if not set + */ + @JsonIgnore + public McpHeadersRefreshHandler getOnMcpHeadersRefreshRequest() { + return onMcpHeadersRefreshRequest; + } + + /** + * Sets the managed MCP dynamic-header refresh handler. + * + * @param onMcpHeadersRefreshRequest + * the handler + * @return this config instance for method chaining + */ + public ResumeSessionConfig setOnMcpHeadersRefreshRequest(McpHeadersRefreshHandler onMcpHeadersRefreshRequest) { + this.onMcpHeadersRefreshRequest = onMcpHeadersRefreshRequest; + return this; + } + /** * Gets the user input request handler. * @@ -1369,6 +1394,27 @@ public ResumeSessionConfig setMcpServers(Map mcpServers return this; } + /** + * Gets host-managed HTTP MCP server configurations. + * + * @return the managed MCP servers map + */ + public Map getManagedMcpServers() { + return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + } + + /** + * Sets host-managed HTTP MCP server configurations for a cold resume. + * + * @param managedMcpServers + * non-secret server configurations keyed by stable managed identity + * @return this config instance for method chaining + */ + public ResumeSessionConfig setManagedMcpServers(Map managedMcpServers) { + this.managedMcpServers = managedMcpServers; + return this; + } + /** * Gets the MCP OAuth token storage mode. * @@ -2074,6 +2120,9 @@ public ResumeSessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.managedMcpServers = this.managedMcpServers != null + ? new java.util.HashMap<>(this.managedMcpServers) + : null; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; copy.defaultAgent = this.defaultAgent; copy.agent = this.agent; @@ -2095,6 +2144,7 @@ public ResumeSessionConfig clone() { copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; copy.onMcpAuthRequest = this.onMcpAuthRequest; + copy.onMcpHeadersRefreshRequest = this.onMcpHeadersRefreshRequest; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 9b8e897fda..fbb431223f 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -156,6 +157,9 @@ public final class ResumeSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; + @JsonProperty("managedMcpServers") + private Map managedMcpServers; + @JsonProperty("mcpOAuthTokenStorage") private String mcpOAuthTokenStorage; @@ -801,6 +805,18 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } + /** Gets managed MCP servers. @return the managed servers map */ + public Map getManagedMcpServers() { + return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + } + + /** + * Sets managed MCP servers. @param managedMcpServers the managed servers map + */ + public void setManagedMcpServers(Map managedMcpServers) { + this.managedMcpServers = managedMcpServers; + } + /** Gets MCP OAuth token storage mode. @return the storage mode */ public String getMcpOAuthTokenStorage() { return mcpOAuthTokenStorage; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index 9f6ddb5efa..bcb5f3ed94 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -16,6 +16,7 @@ import com.github.copilot.CopilotExperimental; import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -66,6 +67,7 @@ public class SessionConfig { private Boolean manageScheduleEnabled; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; + private McpHeadersRefreshHandler onMcpHeadersRefreshRequest; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -73,6 +75,7 @@ public class SessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; + private Map managedMcpServers; private String mcpOAuthTokenStorage; private List customAgents; private DefaultAgentConfig defaultAgent; @@ -882,6 +885,28 @@ public SessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) { return this; } + /** + * Gets the managed MCP dynamic-header refresh handler. + * + * @return the handler, or {@code null} if not set + */ + @JsonIgnore + public McpHeadersRefreshHandler getOnMcpHeadersRefreshRequest() { + return onMcpHeadersRefreshRequest; + } + + /** + * Sets the managed MCP dynamic-header refresh handler. + * + * @param onMcpHeadersRefreshRequest + * the handler + * @return this config instance for method chaining + */ + public SessionConfig setOnMcpHeadersRefreshRequest(McpHeadersRefreshHandler onMcpHeadersRefreshRequest) { + this.onMcpHeadersRefreshRequest = onMcpHeadersRefreshRequest; + return this; + } + /** * Gets the user input request handler. * @@ -1021,6 +1046,27 @@ public SessionConfig setMcpServers(Map mcpServers) { return this; } + /** + * Gets host-managed HTTP MCP server configurations. + * + * @return the managed MCP servers map + */ + public Map getManagedMcpServers() { + return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + } + + /** + * Sets host-managed HTTP MCP server configurations. + * + * @param managedMcpServers + * non-secret server configurations keyed by stable managed identity + * @return this config instance for method chaining + */ + public SessionConfig setManagedMcpServers(Map managedMcpServers) { + this.managedMcpServers = managedMcpServers; + return this; + } + /** * Gets the MCP OAuth token storage mode. * @@ -2203,6 +2249,9 @@ public SessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.managedMcpServers = this.managedMcpServers != null + ? new java.util.HashMap<>(this.managedMcpServers) + : null; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; copy.defaultAgent = this.defaultAgent; copy.agent = this.agent; @@ -2235,6 +2284,7 @@ public SessionConfig clone() { copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; copy.onMcpAuthRequest = this.onMcpAuthRequest; + copy.onMcpHeadersRefreshRequest = this.onMcpHeadersRefreshRequest; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java index 06ac08a2a4..192962c046 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java @@ -23,6 +23,7 @@ import com.github.copilot.rpc.CloudSessionRepository; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.McpAuthResult; +import com.github.copilot.rpc.McpHeadersRefreshResult; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.SessionConfig; @@ -176,6 +177,44 @@ void resumeSessionRegistersMcpAuthInterestOnlyWhenHandlerConfigured() throws Exc } } + @Test + void createAndResumeRegisterManagedMcpHeadersRefreshInterest() throws Exception { + try (var server = new RecordingRuntime(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { + try (var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnMcpHeadersRefreshRequest((request, invocation) -> java.util.concurrent.CompletableFuture + .completedFuture(McpHeadersRefreshResult.none()))) + .get()) { + assertNotNull(session); + } + + List createRequests = server.requests().stream() + .filter(request -> "session.create".equals(request.method()) + || "session.eventLog.registerInterest".equals(request.method())) + .toList(); + assertEquals("session.create", createRequests.get(0).method()); + assertEquals("session.eventLog.registerInterest", createRequests.get(1).method()); + assertEquals("mcp.headers_refresh_required", createRequests.get(1).params().path("eventType").asText()); + + server.clearRequests(); + + try (var session = client.resumeSession("managed-session", + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnMcpHeadersRefreshRequest( + (request, invocation) -> java.util.concurrent.CompletableFuture + .completedFuture(McpHeadersRefreshResult.none()))) + .get()) { + assertNotNull(session); + } + + List resumeRequests = server.requests(); + assertEquals("session.resume", resumeRequests.get(0).method()); + assertEquals("session.eventLog.registerInterest", resumeRequests.get(1).method()); + assertEquals("mcp.headers_refresh_required", resumeRequests.get(1).params().path("eventType").asText()); + } + } + private static void assertNoMcpAuthInterest(List requests) { assertFalse(requests.stream().anyMatch(request -> "session.eventLog.registerInterest".equals(request.method()) && "mcp.oauth_required".equals(request.params().path("eventType").asText()))); diff --git a/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java b/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java new file mode 100644 index 0000000000..d9a97a550f --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.copilot.generated.McpHeadersRefreshRequiredEvent; +import com.github.copilot.generated.McpHeadersRefreshRequiredReason; +import com.github.copilot.generated.rpc.SessionMcpHeadersHandlePendingHeadersRefreshRequestResult; +import com.github.copilot.rpc.McpHeadersRefreshHandler; +import com.github.copilot.rpc.McpHeadersRefreshResult; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.SessionConfig; + +class McpHeadersRefreshHandlerTest { + + private static final String METHOD = "session.mcp.headers.handlePendingHeadersRefreshRequest"; + + @Test + void createHandlerDispatchesHeadersWithoutTtl() { + McpHeadersRefreshHandler handler = (request, invocation) -> { + assertEquals("managed-server", request.serverName()); + assertEquals("https://mcp.example.com", request.serverUrl()); + assertEquals(McpHeadersRefreshRequiredReason.STARTUP, request.reason()); + assertEquals("session-1", invocation.getSessionId()); + return CompletableFuture + .completedFuture(McpHeadersRefreshResult.withHeaders(Map.of("Authorization", "Bearer dynamic"))); + }; + + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + + assertEquals("headers", params.path("result").path("kind").asText()); + assertEquals("Bearer dynamic", params.path("result").path("headers").path("Authorization").asText()); + assertFalse(params.path("result").has("ttlMs")); + } + + @Test + void resumeHandlerDispatchesHeadersWithTtl() { + McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture + .completedFuture(McpHeadersRefreshResult.withHeaders(Map.of("X-Dynamic", "value"), 60_000L)); + + JsonNode params = dispatch(new ResumeSessionConfig().setOnMcpHeadersRefreshRequest(handler)); + + assertEquals("headers", params.path("result").path("kind").asText()); + assertEquals("value", params.path("result").path("headers").path("X-Dynamic").asText()); + assertEquals(60_000L, params.path("result").path("ttlMs").asLong()); + } + + @Test + void handlerCanReturnExplicitNone() { + McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture + .completedFuture(McpHeadersRefreshResult.none()); + + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + + assertEquals("none", params.path("result").path("kind").asText()); + assertEquals(1, params.path("result").size()); + } + + @Test + void handlerFailureReturnsExplicitErrorMessage() { + McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture + .failedFuture(new IllegalStateException("header provider unavailable")); + + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + + assertEquals("error", params.path("result").path("kind").asText()); + assertEquals("header provider unavailable", params.path("result").path("message").asText()); + } + + @Test + void handlerExceptionReturnsExplicitErrorMessage() { + McpHeadersRefreshHandler handler = (request, invocation) -> { + throw new IllegalArgumentException("invalid header configuration"); + }; + + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + + assertEquals("error", params.path("result").path("kind").asText()); + assertEquals("invalid header configuration", params.path("result").path("message").asText()); + } + + private JsonNode dispatch(SessionConfig config) { + var rpc = rpc(); + var session = new CopilotSession("session-1", rpc); + session.setExecutor(Runnable::run); + SessionRequestBuilder.configureSession(session, config); + return dispatchAndCapture(session, rpc); + } + + private JsonNode dispatch(ResumeSessionConfig config) { + var rpc = rpc(); + var session = new CopilotSession("session-1", rpc); + session.setExecutor(Runnable::run); + SessionRequestBuilder.configureSession(session, config); + return dispatchAndCapture(session, rpc); + } + + private JsonRpcClient rpc() { + var rpc = mock(JsonRpcClient.class); + when(rpc.invoke(eq(METHOD), any(), eq(SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class))) + .thenReturn(CompletableFuture + .completedFuture(new SessionMcpHeadersHandlePendingHeadersRefreshRequestResult(true))); + return rpc; + } + + private JsonNode dispatchAndCapture(CopilotSession session, JsonRpcClient rpc) { + var event = new McpHeadersRefreshRequiredEvent(); + event.setData(new McpHeadersRefreshRequiredEvent.McpHeadersRefreshRequiredEventData("headers-request", + "managed-server", "https://mcp.example.com", McpHeadersRefreshRequiredReason.STARTUP)); + + session.dispatchEvent(event); + + var paramsCaptor = ArgumentCaptor.forClass(Object.class); + verify(rpc).invoke(eq(METHOD), paramsCaptor.capture(), + eq(SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class)); + JsonNode params = JsonRpcClient.getObjectMapper().valueToTree(paramsCaptor.getValue()); + assertEquals("session-1", params.path("sessionId").asText()); + assertEquals("headers-request", params.path("requestId").asText()); + assertNull(params.get("unexpected")); + return params; + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 9d76d18ee2..0f0460c595 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; +import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; import com.github.copilot.rpc.AutoModeSwitchResponse; import com.github.copilot.rpc.CloudSessionOptions; @@ -1096,4 +1097,29 @@ void githubMcpToolConfigIsMappedAndSerializedForCreateAndResume() throws Excepti mapper.writeValueAsString(SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "session-2")) .contains("\"githubMcpToolConfig\"")); } + + @Test + void managedMcpServersAreMappedAndSerializedForCreateAndColdResume() throws Exception { + var managedServer = new ManagedMcpServerConfig("Managed catalog server", "https://mcp.example.com", + List.of("search"), 15_000L, 300_000L); + var managedServers = Map.of("catalog-server", managedServer); + + var createRequest = SessionRequestBuilder + .buildCreateRequest(new SessionConfig().setManagedMcpServers(managedServers), "session-1"); + var resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", + new ResumeSessionConfig().setManagedMcpServers(managedServers)); + + assertEquals(managedServers, createRequest.getManagedMcpServers()); + assertEquals(managedServers, resumeRequest.getManagedMcpServers()); + + var mapper = JsonRpcClient.getObjectMapper(); + var createJson = mapper.readTree(mapper.writeValueAsBytes(createRequest)); + var resumeJson = mapper.readTree(mapper.writeValueAsBytes(resumeRequest)); + assertEquals("Managed catalog server", + createJson.path("managedMcpServers").path("catalog-server").path("displayName").asText()); + assertEquals(300_000L, + createJson.path("managedMcpServers").path("catalog-server").path("headersRefreshTtlMs").asLong()); + assertEquals("https://mcp.example.com", + resumeJson.path("managedMcpServers").path("catalog-server").path("url").asText()); + } } diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 602089d012..034b5be792 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -637,7 +637,7 @@ void sessionLogResult_record() { @Test void sessionMcpListResult_nested() { - var server = new McpServer("my-mcp", McpServerStatus.CONNECTED, McpServerSource.USER, null, null, null); + var server = new McpServer("my-mcp", McpServerStatus.CONNECTED, McpServerSource.USER, null, null, null, null); var result = new SessionMcpListResult(List.of(server), null); assertEquals(1, result.servers().size()); assertEquals("my-mcp", result.servers().get(0).name()); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 9b853aa597..679cfd1153 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1565,6 +1565,7 @@ export class CopilotClient { this.onGetTraceContext, { mcpAuthHandler: config.onMcpAuthRequest, + mcpHeadersRefreshHandler: config.onMcpHeadersRefresh, managedSettingsEnabled: config.enableManagedSettings === true || config.managedSettings !== undefined, @@ -1693,6 +1694,7 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), + managedMcpServers: config.managedMcpServers, mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, envValueMode: "direct", customAgents: toWireCustomAgents(config.customAgents), @@ -1755,6 +1757,12 @@ export class CopilotClient { eventType: "mcp.oauth_required", }); } + if (config.onMcpHeadersRefresh) { + await this.connection!.sendRequest("session.eventLog.registerInterest", { + sessionId: returnedSessionId, + eventType: "mcp.headers_refresh_required", + }); + } session["_workspacePath"] = workspacePath; session.setCapabilities(capabilities); @@ -1833,6 +1841,7 @@ export class CopilotClient { this.onGetTraceContext, { mcpAuthHandler: config.onMcpAuthRequest, + mcpHeadersRefreshHandler: config.onMcpHeadersRefresh, managedSettingsEnabled: config.enableManagedSettings === true || config.managedSettings !== undefined, } @@ -1971,6 +1980,7 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), + managedMcpServers: config.managedMcpServers, mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, envValueMode: "direct", customAgents: toWireCustomAgents(config.customAgents), @@ -2033,6 +2043,12 @@ export class CopilotClient { eventType: "mcp.oauth_required", }); } + if (config.onMcpHeadersRefresh) { + await this.connection!.sendRequest("session.eventLog.registerInterest", { + sessionId, + eventType: "mcp.headers_refresh_required", + }); + } await this.updateSessionOptionsForMode(session, config); this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId); diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 9e7304bde2..8ced8be652 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -1694,6 +1694,10 @@ export type McpHeadersHandlePendingHeadersRefreshRequest = headers: { [k: string]: string | undefined; }; + /** + * Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value. + */ + ttlMs?: number; /** * Headers-refresh response variant discriminator. */ @@ -1704,6 +1708,16 @@ export type McpHeadersHandlePendingHeadersRefreshRequest = * Headers-refresh response variant discriminator. */ kind: "none"; + } + | { + /** + * Host credential broker failure, denial, or revocation reason. + */ + message: string; + /** + * Headers-refresh response variant discriminator. + */ + kind: "error"; }; /** * One eligible way to run the server, represented as a tagged package or remote variant so package identity and endpoint states cannot contradict the install method. @@ -11564,6 +11578,10 @@ export interface McpServer { * Error message if the server failed to connect */ error?: string; + /** + * Human-readable display name supplied by a managed server catalog. + */ + displayName?: string; } /** * In-process MCP server configuration used by embedded SDK clients. @@ -18293,6 +18311,14 @@ export interface SessionOpenOptions { * Capabilities enabled for this session. */ sessionCapabilities?: SessionCapability[]; + /** + * Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed provenance is runtime-established from this separate field and credentials are supplied through dynamic-header refresh. + * + * @experimental + */ + managedMcpServers?: { + [k: string]: ManagedMcpServerConfig | undefined; + }; } /** * Per-session settings for built-in shell tools. @@ -18439,6 +18465,35 @@ export interface SessionOpenOptionsAdditionalContentExclusionPolicyRuleSource { */ type: string; } +/** + * Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedMcpServerConfig". + */ +/** @experimental */ +export interface ManagedMcpServerConfig { + /** + * Human-readable catalog display name. + */ + displayName: string; + /** + * Hosted MCP streamable HTTP endpoint. + */ + url: string; + /** + * Tools to include. Defaults to all tools when omitted. + */ + tools?: string[]; + /** + * Timeout in milliseconds for tool discovery and tool calls. + */ + timeout?: number; + /** + * Maximum dynamic-header cache lifetime in milliseconds. + */ + headersRefreshTtlMs?: number; +} /** * Parameters for creating a new local session. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 473a2dbe16..c2d51f1249 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -914,7 +914,7 @@ export type SkillSource = /** Skill bundled with the runtime. */ | "builtin"; /** - * Configuration source: user, workspace, plugin, or builtin + * Configuration source: user, workspace, plugin, builtin, or managed */ export type McpServerSource = /** Server configured in the user's global MCP configuration. */ @@ -924,7 +924,9 @@ export type McpServerSource = /** Server contributed by an installed plugin. */ | "plugin" /** Server bundled with the runtime. */ - | "builtin"; + | "builtin" + /** Server supplied by a trusted host-managed catalog. */ + | "managed"; /** * Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured */ @@ -9935,6 +9937,10 @@ export interface McpServersLoadedData { * A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata. */ export interface McpServersLoadedServer { + /** + * Human-readable display name supplied by a managed server catalog. + */ + displayName?: string; /** * Error message if the server failed to connect */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 9d55ab1d10..054017332b 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -111,6 +111,10 @@ export type { MCPStdioServerConfig, MCPHTTPServerConfig, MCPServerConfig, + ManagedMcpServerConfig, + McpHeadersRefreshHandler, + McpHeadersRefreshRequest, + McpHeadersRefreshResult, DefaultAgentConfig, BearerTokenProvider, MessageOptions, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 65ff00921c..f1efc51a10 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -15,6 +15,7 @@ import type { ClientSessionApiHandlers, CanvasActionInvokeResult, CurrentToolMetadata, + McpHeadersHandlePendingHeadersRefreshRequest, McpOauthPendingRequestResponse, FactoryLogLine, FactoryRunResult as WireFactoryRunResult, @@ -40,6 +41,9 @@ import type { MessageOptions, McpAuthHandler, McpAuthRequest, + McpHeadersRefreshHandler, + McpHeadersRefreshRequest, + McpHeadersRefreshResult, PermissionHandler, PermissionRequest, PermissionRequestResult, @@ -157,6 +161,17 @@ function isOpenCanvasInstance(value: unknown): value is OpenCanvasInstance { ); } +function isMcpHeadersRefreshResult( + value: McpHeadersRefreshResult | Record +): value is McpHeadersRefreshResult { + const headers = (value as { headers?: unknown }).headers; + return ( + typeof headers === "object" && + headers !== null && + Object.values(headers).every((header) => typeof header === "string") + ); +} + const FACTORY_LOG_FLUSH_DELAY_MS = 10; const MAX_FACTORY_FANOUT_ITEMS = 4096; @@ -426,6 +441,7 @@ export class CopilotSession { private factoryAbortControllers = new Map>(); private permissionHandler?: PermissionHandler; private mcpAuthHandler?: McpAuthHandler; + private mcpHeadersRefreshHandler?: McpHeadersRefreshHandler; private userInputHandler?: UserInputHandler; private elicitationHandler?: ElicitationHandler; private exitPlanModeHandler?: ExitPlanModeHandler; @@ -620,12 +636,14 @@ export class CopilotSession { traceContextProvider?: TraceContextProvider, options?: { mcpAuthHandler?: McpAuthHandler; + mcpHeadersRefreshHandler?: McpHeadersRefreshHandler; managedSettingsEnabled?: boolean; onDisconnected?: () => void; } ) { this.traceContextProvider = traceContextProvider; this.mcpAuthHandler = options?.mcpAuthHandler; + this.mcpHeadersRefreshHandler = options?.mcpHeadersRefreshHandler; this.managedSettingsEnabled = options?.managedSettingsEnabled === true; this.onDisconnected = options?.onDisconnected; } @@ -1009,6 +1027,16 @@ export class CopilotSession { return; } void this._executeMcpAuthAndRespond(data); + } else if (event.type === "mcp.headers_refresh_required") { + const { requestId, serverName, serverUrl, reason } = event.data; + if (!requestId || !this.mcpHeadersRefreshHandler) { + return; + } + void this._executeMcpHeadersRefreshAndRespond(requestId, { + serverName, + serverUrl, + reason, + }); } else if (event.type === "command.execute") { const { requestId, commandName, command, args } = event.data as { requestId: string; @@ -1230,6 +1258,48 @@ export class CopilotSession { } } + /** + * Executes a managed MCP headers refresh handler and sends the result via RPC. + * @internal + */ + private async _executeMcpHeadersRefreshAndRespond( + requestId: string, + request: McpHeadersRefreshRequest + ): Promise { + let result: McpHeadersHandlePendingHeadersRefreshRequest; + try { + const response = await this.mcpHeadersRefreshHandler!(request, { + sessionId: this.sessionId, + }); + if (response === undefined) { + result = { kind: "none" }; + } else if (isMcpHeadersRefreshResult(response)) { + result = { + kind: "headers", + headers: response.headers, + ...(response.ttlMs === undefined ? {} : { ttlMs: response.ttlMs }), + }; + } else { + result = { kind: "headers", headers: response }; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`MCP headers refresh failed for '${request.serverName}': ${message}`); + result = { kind: "error", message }; + } + + try { + await this.rpc.mcp.headers.handlePendingHeadersRefreshRequest({ + requestId, + result, + }); + } catch (rpcError) { + if (!(rpcError instanceof ConnectionError || rpcError instanceof ResponseError)) { + throw rpcError; + } + } + } + /** * Executes a command handler and sends the result back via RPC. * @internal diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 616e15a467..5e13274b7a 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -14,6 +14,7 @@ import type { PermissionRequest as GeneratedPermissionRequest, PermissionRequestedData as GeneratedPermissionRequestedData, PermissionRequestedEvent as GeneratedPermissionRequestedEvent, + McpHeadersRefreshRequiredReason, ReasoningSummary, SessionLimitsConfig, SessionEvent as GeneratedSessionEvent, @@ -24,6 +25,7 @@ import type { GitHubTokenAcquireRequest, GitHubTokenAcquireResult, GitHubTelemetryNotification, + ManagedMcpServerConfig, ModelBillingTokenPrices, OpenCanvasInstance, RemoteSessionMode, @@ -32,6 +34,7 @@ import type { import type { ToolSet } from "./toolSet.js"; export type { RemoteSessionMode } from "./generated/rpc.js"; export type { CurrentToolMetadata } from "./generated/rpc.js"; +export type { ManagedMcpServerConfig } from "./generated/rpc.js"; export type { GitHubTokenAcquireReason, GitHubTokenAcquireResult, @@ -2031,6 +2034,38 @@ export type McpAuthHandler = ( | undefined | Promise; +/** MCP server whose short-lived HTTP headers must be refreshed by the host. */ +export interface McpHeadersRefreshRequest { + /** Human-readable managed catalog display name. */ + serverName: string; + /** Hosted MCP streamable HTTP endpoint. */ + serverUrl: string; + /** Why the runtime invalidated or requested dynamic headers. */ + reason: McpHeadersRefreshRequiredReason; +} + +/** Dynamic headers and their optional credential-bounded cache lifetime. */ +export interface McpHeadersRefreshResult { + headers: Record; + ttlMs?: number; +} + +/** + * Callback invoked when a managed MCP server needs fresh HTTP headers. + * + * Return a bare header map or a structured result with `ttlMs`. Returning + * `undefined` reports that no headers are available. Throwing reports an + * explicit credential-broker error to the runtime. + */ +export type McpHeadersRefreshHandler = ( + request: McpHeadersRefreshRequest, + context: { sessionId: string } +) => + | McpHeadersRefreshResult + | Record + | undefined + | Promise | undefined>; + /** * Stable extension identity for session participants that provide canvases. */ @@ -2554,6 +2589,12 @@ export interface SessionConfigBase { */ onMcpAuthRequest?: McpAuthHandler; + /** + * Supplies short-lived HTTP headers for managed MCP servers. + * Re-register this handler when cold-resuming a session. + */ + onMcpHeadersRefresh?: McpHeadersRefreshHandler; + /** * Handler for user input requests from the agent. * When provided, enables the ask_user tool allowing the agent to ask questions. @@ -2669,6 +2710,15 @@ export interface SessionConfigBase { */ mcpServers?: Record; + /** + * Non-secret hosted MCP servers injected from a trusted managed catalog. + * Keys are stable managed identities. Credentials must be supplied through + * {@link onMcpHeadersRefresh}, never stored in this configuration. + * + * @experimental + */ + managedMcpServers?: Record; + /** * Custom agent configurations for the session. */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 3ffda2fa71..a38804da1c 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -272,6 +272,162 @@ describe("CopilotClient", () => { }); }); + it.each([ + { + name: "bare headers without ttlMs", + response: { Authorization: "Bearer short-lived" }, + expected: { + kind: "headers", + headers: { Authorization: "Bearer short-lived" }, + }, + }, + { + name: "structured headers with ttlMs", + response: { + headers: { Authorization: "Bearer short-lived" }, + ttlMs: 5_000, + }, + expected: { + kind: "headers", + headers: { Authorization: "Bearer short-lived" }, + ttlMs: 5_000, + }, + }, + ])("responds to managed MCP header refresh with $name", async ({ response, expected }) => { + const sendRequest = vi.fn(async () => ({ success: true })); + const session = new CopilotSession( + "session-1", + { sendRequest } as any, + undefined, + undefined, + { mcpHeadersRefreshHandler: async () => response } + ); + + await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { + serverName: "Managed GitHub", + serverUrl: "https://example.com/mcp", + reason: "startup", + }); + + expect(sendRequest).toHaveBeenCalledWith( + "session.mcp.headers.handlePendingHeadersRefreshRequest", + { + sessionId: "session-1", + requestId: "refresh-request", + result: expected, + } + ); + }); + + it("reports no managed MCP headers when the handler returns undefined", async () => { + const sendRequest = vi.fn(async () => ({ success: true })); + const session = new CopilotSession( + "session-1", + { sendRequest } as any, + undefined, + undefined, + { mcpHeadersRefreshHandler: () => undefined } + ); + + await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { + serverName: "Managed GitHub", + serverUrl: "https://example.com/mcp", + reason: "ttl-expired", + }); + + expect(sendRequest).toHaveBeenCalledWith( + "session.mcp.headers.handlePendingHeadersRefreshRequest", + { + sessionId: "session-1", + requestId: "refresh-request", + result: { kind: "none" }, + } + ); + }); + + it("propagates managed MCP credential broker failures explicitly", async () => { + const sendRequest = vi.fn(async () => ({ success: true })); + const warning = vi.spyOn(console, "warn").mockImplementation(() => {}); + const session = new CopilotSession( + "session-1", + { sendRequest } as any, + undefined, + undefined, + { + mcpHeadersRefreshHandler: () => { + throw new Error("credential revoked"); + }, + } + ); + + await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { + serverName: "Managed GitHub", + serverUrl: "https://example.com/mcp", + reason: "auth-failed", + }); + + expect(sendRequest).toHaveBeenCalledWith( + "session.mcp.headers.handlePendingHeadersRefreshRequest", + { + sessionId: "session-1", + requestId: "refresh-request", + result: { kind: "error", message: "credential revoked" }, + } + ); + warning.mockRestore(); + }); + + it("forwards managed MCP servers and refresh interest on create and resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create" || method === "session.resume") { + return { sessionId: params.sessionId }; + } + if (method === "session.eventLog.registerInterest") { + return { id: "interest-1" }; + } + throw new Error(`Unexpected method: ${method}`); + }); + const managedMcpServers = { + github: { + displayName: "GitHub", + url: "https://api.example.com/mcp", + tools: ["issues"], + timeout: 30_000, + headersRefreshTtlMs: 60_000, + }, + }; + const onMcpHeadersRefresh = () => ({ Authorization: "Bearer short-lived" }); + + const session = await client.createSession({ + managedMcpServers, + onMcpHeadersRefresh, + }); + await client.resumeSession(session.sessionId, { + managedMcpServers, + onMcpHeadersRefresh, + }); + + for (const method of ["session.create", "session.resume"]) { + expect(spy.mock.calls.find(([called]) => called === method)![1]).toMatchObject({ + managedMcpServers, + }); + } + expect(spy).toHaveBeenCalledTimes(4); + expect( + spy.mock.calls.filter( + ([method, params]) => + method === "session.eventLog.registerInterest" && + params.eventType === "mcp.headers_refresh_required" + ) + ).toHaveLength(2); + }); + it("forwards GitHub MCP tool config on create and resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 608dacf253..6735d8dcc4 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -134,6 +134,7 @@ InfiniteSessionConfig, InputOptions, LargeToolOutputConfig, + ManagedMCPServerConfig, McpAuthContext, McpAuthHandler, McpAuthRequest, @@ -141,6 +142,10 @@ McpAuthStaticClientConfig, McpAuthToken, McpAuthWwwAuthenticateParams, + McpHeadersRefreshContext, + McpHeadersRefreshHandler, + McpHeadersRefreshRequest, + McpHeadersRefreshResult, MCPHTTPServerConfig, MCPServerConfig, MCPStdioServerConfig, @@ -300,6 +305,11 @@ "McpAuthStaticClientConfig", "McpAuthToken", "McpAuthWwwAuthenticateParams", + "McpHeadersRefreshContext", + "McpHeadersRefreshHandler", + "McpHeadersRefreshRequest", + "McpHeadersRefreshResult", + "ManagedMCPServerConfig", "ManagedSettings", "ManagedSettingsPermissions", "ModelBilling", diff --git a/python/copilot/client.py b/python/copilot/client.py index 271fad626c..e9bdc034c9 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -102,7 +102,9 @@ GitHubMcpToolConfig, InfiniteSessionConfig, LargeToolOutputConfig, + ManagedMCPServerConfig, McpAuthHandler, + McpHeadersRefreshHandler, MCPServerConfig, MemoryConfiguration, ModelCapabilitiesOverride, @@ -417,6 +419,26 @@ def _mcp_servers_to_wire( return wire +def _managed_mcp_servers_to_wire( + servers: dict[str, ManagedMCPServerConfig], +) -> dict[str, Any]: + """Convert managed MCP configuration keys to the JSON-RPC wire casing.""" + wire: dict[str, Any] = {} + for name, config in servers.items(): + server: dict[str, Any] = { + "displayName": config["display_name"], + "url": config["url"], + } + if "tools" in config: + server["tools"] = config["tools"] + if "timeout" in config: + server["timeout"] = config["timeout"] + if "headers_refresh_ttl_ms" in config: + server["headersRefreshTtlMs"] = config["headers_refresh_ttl_ms"] + wire[name] = server + return wire + + def _large_output_to_wire(config: Mapping[str, Any]) -> dict[str, Any]: """Convert a ``LargeToolOutputConfig`` mapping to wire format.""" wire: dict[str, Any] = {} @@ -2229,6 +2251,7 @@ async def create_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, + managed_mcp_servers: dict[str, ManagedMCPServerConfig] | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, custom_agents: list[CustomAgentConfig] | None = None, @@ -2256,6 +2279,7 @@ async def create_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, on_mcp_auth_request: McpAuthHandler | None = None, + on_mcp_headers_refresh: McpHeadersRefreshHandler | None = None, enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, @@ -2353,6 +2377,12 @@ async def create_session( ``agentId`` set). When False, only non-streaming sub-agent events and ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. + managed_mcp_servers: Non-secret hosted MCP servers from a trusted + managed catalog, keyed by stable managed identity. Re-supply on + cold resume. + on_mcp_headers_refresh: Supplies short-lived HTTP headers for managed + MCP servers. Exceptions are reported to the runtime as explicit + credential-broker errors. mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored. ``"persistent"`` uses the OS keychain (shared across sessions). ``"in-memory"`` stores tokens in memory (discarded on session end). @@ -2650,6 +2680,8 @@ async def create_session( # Add MCP servers configuration if provided if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) + if managed_mcp_servers is not None: + payload["managedMcpServers"] = _managed_mcp_servers_to_wire(managed_mcp_servers) # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: @@ -2820,6 +2852,7 @@ def _initialize_session(sid: str) -> CopilotSession: s._register_commands(commands) s._register_permission_handler(on_permission_request) s._register_mcp_auth_handler(on_mcp_auth_request) + s._register_mcp_headers_refresh_handler(on_mcp_headers_refresh) if on_user_input_request: s._register_user_input_handler(on_user_input_request) if on_elicitation_request: @@ -2912,6 +2945,14 @@ def _register_inline(raw_response: Any) -> None: "session.eventLog.registerInterest", {"sessionId": session.session_id, "eventType": "mcp.oauth_required"}, ) + if on_mcp_headers_refresh is not None: + await self._client.request( + "session.eventLog.registerInterest", + { + "sessionId": session.session_id, + "eventType": "mcp.headers_refresh_required", + }, + ) session._workspace_path = response.get("workspacePath") capabilities = response.get("capabilities") session._set_capabilities(capabilities) @@ -2990,6 +3031,7 @@ async def resume_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, + managed_mcp_servers: dict[str, ManagedMCPServerConfig] | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, custom_agents: list[CustomAgentConfig] | None = None, @@ -3017,6 +3059,7 @@ async def resume_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, on_mcp_auth_request: McpAuthHandler | None = None, + on_mcp_headers_refresh: McpHeadersRefreshHandler | None = None, enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, @@ -3116,6 +3159,10 @@ async def resume_session( ``agentId`` set). When False, only non-streaming sub-agent events and ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. + managed_mcp_servers: Non-secret hosted MCP servers from a trusted + managed catalog. Re-supply them on cold resume. + on_mcp_headers_refresh: Supplies short-lived HTTP headers for managed + MCP servers. mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored. ``"persistent"`` uses the OS keychain (shared across sessions). ``"in-memory"`` stores tokens in memory (discarded on session end). @@ -3409,6 +3456,8 @@ async def resume_session( # TODO: disable_resume is not a keyword arg yet; keeping for future use if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) + if managed_mcp_servers is not None: + payload["managedMcpServers"] = _managed_mcp_servers_to_wire(managed_mcp_servers) # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: @@ -3516,6 +3565,7 @@ async def resume_session( session._register_commands(commands) session._register_permission_handler(on_permission_request) session._register_mcp_auth_handler(on_mcp_auth_request) + session._register_mcp_headers_refresh_handler(on_mcp_headers_refresh) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if on_elicitation_request: @@ -3581,6 +3631,14 @@ async def resume_session( "session.eventLog.registerInterest", {"sessionId": session.session_id, "eventType": "mcp.oauth_required"}, ) + if on_mcp_headers_refresh is not None: + await self._client.request( + "session.eventLog.registerInterest", + { + "sessionId": session.session_id, + "eventType": "mcp.headers_refresh_required", + }, + ) except BaseException as exc: with self._sessions_lock: self._sessions.pop(session_id, None) diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 242e4041ed..6c8732e9bb 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -4464,6 +4464,49 @@ def to_dict(self) -> dict: result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedMCPServerConfig: + """Non-secret host-managed HTTP MCP server configuration. The containing map key is the + stable managed identity; credentials are supplied dynamically by the host. + """ + display_name: str + """Human-readable catalog display name.""" + + url: str + """Hosted MCP streamable HTTP endpoint.""" + + headers_refresh_ttl_ms: int | None = None + """Maximum dynamic-header cache lifetime in milliseconds.""" + + timeout: int | None = None + """Timeout in milliseconds for tool discovery and tool calls.""" + + tools: list[str] | None = None + """Tools to include. Defaults to all tools when omitted.""" + + @staticmethod + def from_dict(obj: Any) -> 'ManagedMCPServerConfig': + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + url = from_str(obj.get("url")) + headers_refresh_ttl_ms = from_union([from_int, from_none], obj.get("headersRefreshTtlMs")) + timeout = from_union([from_int, from_none], obj.get("timeout")) + tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools")) + return ManagedMCPServerConfig(display_name, url, headers_refresh_ttl_ms, timeout, tools) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["url"] = from_str(self.url) + if self.headers_refresh_ttl_ms is not None: + result["headersRefreshTtlMs"] = from_union([from_int, from_none], self.headers_refresh_ttl_ms) + if self.timeout is not None: + result["timeout"] = from_union([from_int, from_none], self.timeout) + if self.tools is not None: + result["tools"] = from_union([lambda x: from_list(from_str, x), from_none], self.tools) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ManagedSettingsReadResult: @@ -5182,6 +5225,7 @@ def to_dict(self) -> dict: return result class MCPHeadersHandlePendingHeadersRefreshRequestKind(Enum): + ERROR = "error" HEADERS = "headers" NONE = "none" @@ -18525,19 +18569,31 @@ class MCPHeadersHandlePendingHeadersRefreshRequest: """Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. """ + ttl_ms: int | None = None + """Optional lifetime in milliseconds for these returned headers. The runtime clamps its + configured cache lifetime to this value. + """ + message: str | None = None + """Host credential broker failure, denial, or revocation reason.""" @staticmethod def from_dict(obj: Any) -> 'MCPHeadersHandlePendingHeadersRefreshRequest': assert isinstance(obj, dict) kind = MCPHeadersHandlePendingHeadersRefreshRequestKind(obj.get("kind")) headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("headers")) - return MCPHeadersHandlePendingHeadersRefreshRequest(kind, headers) + ttl_ms = from_union([from_int, from_none], obj.get("ttlMs")) + message = from_union([from_str, from_none], obj.get("message")) + return MCPHeadersHandlePendingHeadersRefreshRequest(kind, headers, ttl_ms, message) def to_dict(self) -> dict: result: dict = {} result["kind"] = to_enum(MCPHeadersHandlePendingHeadersRefreshRequestKind, self.kind) if self.headers is not None: result["headers"] = from_union([lambda x: from_dict(from_str, x), from_none], self.headers) + if self.ttl_ms is not None: + result["ttlMs"] = from_union([from_int, from_none], self.ttl_ms) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -21235,6 +21291,9 @@ class MCPServer: """Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured """ + display_name: str | None = None + """Human-readable display name supplied by a managed server catalog.""" + error: str | None = None """Error message if the server failed to connect""" @@ -21252,16 +21311,19 @@ def from_dict(obj: Any) -> 'MCPServer': assert isinstance(obj, dict) name = from_str(obj.get("name")) status = McpServerStatus(obj.get("status")) + display_name = from_union([from_str, from_none], obj.get("displayName")) error = from_union([from_str, from_none], obj.get("error")) source = from_union([McpServerSource, from_none], obj.get("source")) source_plugin = from_union([from_str, from_none], obj.get("sourcePlugin")) source_plugin_version = from_union([from_str, from_none], obj.get("sourcePluginVersion")) - return MCPServer(name, status, error, source, source_plugin, source_plugin_version) + return MCPServer(name, status, display_name, error, source, source_plugin, source_plugin_version) def to_dict(self) -> dict: result: dict = {} result["name"] = from_str(self.name) result["status"] = to_enum(McpServerStatus, self.status) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) if self.source is not None: @@ -30782,6 +30844,11 @@ class SessionOpenOptions: lsp_client_name: str | None = None """Identifier sent to LSP-style integrations.""" + managed_mcp_servers: dict[str, ManagedMCPServerConfig] | None = None + """Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed + provenance is runtime-established from this separate field and credentials are supplied + through dynamic-header refresh. + """ managed_settings: SessionManagedSettings | None = None """Permissions-only enterprise policy injected by the SDK host at session create or resume. Composes restrictively with self-fetched and device policy and is not persisted. @@ -30920,6 +30987,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': is_experimental_mode = from_union([from_bool, from_none], obj.get("isExperimentalMode")) log_interactive_shells = from_union([from_bool, from_none], obj.get("logInteractiveShells")) lsp_client_name = from_union([from_str, from_none], obj.get("lspClientName")) + managed_mcp_servers = from_union([lambda x: from_dict(ManagedMCPServerConfig.from_dict, x), from_none], obj.get("managedMcpServers")) managed_settings = from_union([SessionManagedSettings.from_dict, from_none], obj.get("managedSettings")) max_inline_binary_bytes = from_union([from_int, from_none], obj.get("maxInlineBinaryBytes")) memory = from_union([MemoryConfiguration.from_dict, from_none], obj.get("memory")) @@ -30949,7 +31017,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) working_directory_context = from_union([SessionContext.from_dict, from_none], obj.get("workingDirectoryContext")) - return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) + return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_mcp_servers, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) def to_dict(self) -> dict: result: dict = {} @@ -31033,6 +31101,8 @@ def to_dict(self) -> dict: result["logInteractiveShells"] = from_union([from_bool, from_none], self.log_interactive_shells) if self.lsp_client_name is not None: result["lspClientName"] = from_union([from_str, from_none], self.lsp_client_name) + if self.managed_mcp_servers is not None: + result["managedMcpServers"] = from_union([lambda x: from_dict(lambda x: to_class(ManagedMCPServerConfig, x), x), from_none], self.managed_mcp_servers) if self.managed_settings is not None: result["managedSettings"] = from_union([lambda x: to_class(SessionManagedSettings, x), from_none], self.managed_settings) if self.max_inline_binary_bytes is not None: @@ -35267,6 +35337,7 @@ class RPC: log_request: LogRequest log_result: LogResult lsp_initialize_request: LspInitializeRequest + managed_mcp_server_config: ManagedMCPServerConfig managed_settings_read_result: ManagedSettingsReadResult marketplace_add_result: MarketplaceAddResult marketplace_browse_result: MarketplaceBrowseResult @@ -36447,6 +36518,7 @@ def from_dict(obj: Any) -> 'RPC': log_request = LogRequest.from_dict(obj.get("LogRequest")) log_result = LogResult.from_dict(obj.get("LogResult")) lsp_initialize_request = LspInitializeRequest.from_dict(obj.get("LspInitializeRequest")) + managed_mcp_server_config = ManagedMCPServerConfig.from_dict(obj.get("ManagedMcpServerConfig")) managed_settings_read_result = ManagedSettingsReadResult.from_dict(obj.get("ManagedSettingsReadResult")) marketplace_add_result = MarketplaceAddResult.from_dict(obj.get("MarketplaceAddResult")) marketplace_browse_result = MarketplaceBrowseResult.from_dict(obj.get("MarketplaceBrowseResult")) @@ -37291,7 +37363,7 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -37627,6 +37699,7 @@ def to_dict(self) -> dict: result["LogRequest"] = to_class(LogRequest, self.log_request) result["LogResult"] = to_class(LogResult, self.log_result) result["LspInitializeRequest"] = to_class(LspInitializeRequest, self.lsp_initialize_request) + result["ManagedMcpServerConfig"] = to_class(ManagedMCPServerConfig, self.managed_mcp_server_config) result["ManagedSettingsReadResult"] = to_class(ManagedSettingsReadResult, self.managed_settings_read_result) result["MarketplaceAddResult"] = to_class(MarketplaceAddResult, self.marketplace_add_result) result["MarketplaceBrowseResult"] = to_class(MarketplaceBrowseResult, self.marketplace_browse_result) @@ -42226,6 +42299,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "MCPToolUIVisibility", "MCPTools", "MCPUnregisterExternalClientRequest", + "ManagedMCPServerConfig", "ManagedSettingsReadResult", "MarketplaceAddResult", "MarketplaceBrowseResult", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index d4af5f2735..7db3cbd21d 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -4413,6 +4413,7 @@ class McpServersLoadedServer: "A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata." name: str status: McpServerStatus + display_name: str | None = None error: str | None = None plugin_name: str | None = None plugin_version: str | None = None @@ -4424,6 +4425,7 @@ def from_dict(obj: Any) -> "McpServersLoadedServer": assert isinstance(obj, dict) name = from_str(obj.get("name")) status = parse_enum(McpServerStatus, obj.get("status")) + display_name = from_union([from_none, from_str], obj.get("displayName")) error = from_union([from_none, from_str], obj.get("error")) plugin_name = from_union([from_none, from_str], obj.get("pluginName")) plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) @@ -4432,6 +4434,7 @@ def from_dict(obj: Any) -> "McpServersLoadedServer": return McpServersLoadedServer( name=name, status=status, + display_name=display_name, error=error, plugin_name=plugin_name, plugin_version=plugin_version, @@ -4443,6 +4446,8 @@ def to_dict(self) -> dict: result: dict = {} result["name"] = from_str(self.name) result["status"] = to_enum(McpServerStatus, self.status) + if self.display_name is not None: + result["displayName"] = from_union([from_none, from_str], self.display_name) if self.error is not None: result["error"] = from_union([from_none, from_str], self.error) if self.plugin_name is not None: @@ -11048,7 +11053,7 @@ class McpOauthRequestReason(Enum): class McpServerSource(Enum): - "Configuration source: user, workspace, plugin, or builtin" + "Configuration source: user, workspace, plugin, builtin, or managed" # Server configured in the user's global MCP configuration. USER = "user" # Server configured by the current workspace. @@ -11057,6 +11062,8 @@ class McpServerSource(Enum): PLUGIN = "plugin" # Server bundled with the runtime. BUILTIN = "builtin" + # Server supplied by a trusted host-managed catalog. + MANAGED = "managed" class McpServerStatus(Enum): diff --git a/python/copilot/session.py b/python/copilot/session.py index 78afdde139..2ee1e93032 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -37,6 +37,9 @@ GitHubTokenAcquireResultKind, HandlePendingToolCallRequest, LogRequest, + MCPHeadersHandlePendingHeadersRefreshRequest, + MCPHeadersHandlePendingHeadersRefreshRequestKind, + MCPHeadersHandlePendingHeadersRefreshRequestRequest, MCPOauthHandlePendingRequest, MCPOauthPendingRequestResponse, ModelSwitchToRequest, @@ -70,6 +73,7 @@ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, + McpHeadersRefreshRequiredData, McpOauthRequiredData, PermissionRequest, PermissionRequestedData, @@ -498,6 +502,41 @@ class McpAuthContext(TypedDict): ] +class McpHeadersRefreshRequest(TypedDict): + """Managed MCP server whose short-lived HTTP headers need refreshing.""" + + server_name: str + server_url: str + reason: Literal["startup", "ttl-expired", "auth-failed"] + + +class McpHeadersRefreshResult(TypedDict): + """Dynamic headers and their optional credential-bounded cache lifetime.""" + + headers: dict[str, str] + ttl_ms: NotRequired[int] + + +class McpHeadersRefreshContext(TypedDict): + """Context for a managed MCP headers refresh handler invocation.""" + + session_id: str + + +McpHeadersRefreshHandlerResult = McpHeadersRefreshResult | dict[str, str] | None +McpHeadersRefreshHandler = Callable[ + [McpHeadersRefreshRequest, McpHeadersRefreshContext], + McpHeadersRefreshHandlerResult | Awaitable[McpHeadersRefreshHandlerResult], +] + + +def _is_mcp_headers_refresh_result( + value: McpHeadersRefreshResult | dict[str, str], +) -> bool: + headers = value.get("headers") + return isinstance(headers, dict) and all(isinstance(header, str) for header in headers.values()) + + # ============================================================================ # User Input Request Types # ============================================================================ @@ -1164,6 +1203,16 @@ class MCPHTTPServerConfig(TypedDict, total=False): MCPServerConfig = MCPStdioServerConfig | MCPHTTPServerConfig +class ManagedMCPServerConfig(TypedDict, total=False): + """Non-secret hosted MCP server from a trusted managed catalog.""" + + display_name: Required[str] + url: Required[str] + tools: list[str] + timeout: int + headers_refresh_ttl_ms: int + + class GitHubMcpToolConfig(TypedDict, total=False): """Configuration for the built-in GitHub MCP server. @@ -1578,6 +1627,8 @@ def __init__( self._permission_handler_lock = threading.Lock() self._mcp_auth_handler: McpAuthHandler | None = None self._mcp_auth_handler_lock = threading.Lock() + self._mcp_headers_refresh_handler: McpHeadersRefreshHandler | None = None + self._mcp_headers_refresh_handler_lock = threading.Lock() self._user_input_handler: UserInputHandler | None = None self._user_input_handler_lock = threading.Lock() self._exit_plan_mode_handler: ExitPlanModeHandler | None = None @@ -2030,6 +2081,23 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: request["staticClientConfig"] = static_client_config asyncio.ensure_future(self._execute_mcp_auth_and_respond(request, handler)) + case McpHeadersRefreshRequiredData() as data: + with self._mcp_headers_refresh_handler_lock: + handler = self._mcp_headers_refresh_handler + if not data.request_id or not handler: + return + asyncio.ensure_future( + self._execute_mcp_headers_refresh_and_respond( + data.request_id, + { + "server_name": data.server_name, + "server_url": data.server_url, + "reason": data.reason.value, + }, + handler, + ) + ) + case CommandExecuteData() as data: request_id = data.request_id command_name = data.command_name @@ -2320,6 +2388,68 @@ async def _execute_mcp_auth_and_respond( except (JsonRpcError, ProcessExitedError, OSError): pass # Connection lost or RPC error — nothing we can do + async def _execute_mcp_headers_refresh_and_respond( + self, + request_id: str, + request: McpHeadersRefreshRequest, + handler: McpHeadersRefreshHandler, + ) -> None: + """Execute a managed MCP headers refresh handler and respond via RPC.""" + try: + maybe_result = handler(request, {"session_id": self.session_id}) + if inspect.isawaitable(maybe_result): + result = cast(McpHeadersRefreshHandlerResult, await maybe_result) + else: + result = maybe_result + + if result is None: + rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( + kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.NONE + ) + elif _is_mcp_headers_refresh_result(result): + structured = cast(McpHeadersRefreshResult, result) + rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( + kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.HEADERS, + headers=structured["headers"], + ttl_ms=structured.get("ttl_ms"), + ) + else: + rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( + kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.HEADERS, + headers=cast(dict[str, str], result), + ) + except asyncio.CancelledError as exc: + message = str(exc) or "MCP headers refresh cancelled" + logger.warning( + "MCP headers refresh cancelled for %r: %s", + request["server_name"], + message, + ) + rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( + kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.ERROR, + message=message, + ) + except Exception as exc: + logger.warning( + "MCP headers refresh failed for %r: %s", + request["server_name"], + exc, + ) + rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( + kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.ERROR, + message=str(exc), + ) + + try: + await self.rpc.mcp.headers.handle_pending_headers_refresh_request( + MCPHeadersHandlePendingHeadersRefreshRequestRequest( + request_id=request_id, + result=rpc_result, + ) + ) + except (JsonRpcError, ProcessExitedError, OSError): + pass + async def _execute_command_and_respond( self, request_id: str, @@ -2509,6 +2639,13 @@ def _register_mcp_auth_handler(self, handler: McpAuthHandler | None) -> None: with self._mcp_auth_handler_lock: self._mcp_auth_handler = handler + def _register_mcp_headers_refresh_handler( + self, handler: McpHeadersRefreshHandler | None + ) -> None: + """Register the managed MCP dynamic-headers handler for this session.""" + with self._mcp_headers_refresh_handler_lock: + self._mcp_headers_refresh_handler = handler + def _register_exit_plan_mode_handler(self, handler: ExitPlanModeHandler | None) -> None: """Register the exit-plan-mode handler for this session.""" with self._exit_plan_mode_handler_lock: diff --git a/python/test_client.py b/python/test_client.py index a33f0ecd60..ed2151ea40 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -19,6 +19,7 @@ CopilotClient, DisableBypassPermissionsModes, ExtensionInfo, + ManagedMCPServerConfig, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, RuntimeConnection, @@ -38,8 +39,10 @@ ModelLimits, ModelSupports, ) -from copilot.session import PermissionHandler +from copilot.session import CopilotSession, PermissionHandler from copilot.session_events import ( + McpHeadersRefreshRequiredData, + McpHeadersRefreshRequiredReason, McpOauthRequestReason, McpOauthRequiredData, McpOauthRequiredStaticClientConfig, @@ -552,6 +555,173 @@ def handle_mcp_auth_request(request, invocation): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_managed_mcp_headers_refresh_dispatches_ttl_and_broker_errors(self): + transport = Mock() + transport.request = AsyncMock(return_value={"success": True}) + session = CopilotSession("managed-session", transport) + responses = [ + {"Authorization": "Bearer short-lived"}, + { + "headers": {"Authorization": "Bearer refreshed"}, + "ttl_ms": 5_000, + }, + None, + ] + + async def handle_refresh(request, context): + assert request == { + "server_name": "GitHub", + "server_url": "https://example.com/mcp", + "reason": "startup", + } + assert context == {"session_id": "managed-session"} + return responses.pop(0) + + session._register_mcp_headers_refresh_handler(handle_refresh) + event = SessionEvent( + data=McpHeadersRefreshRequiredData( + request_id="refresh-1", + server_name="GitHub", + server_url="https://example.com/mcp", + reason=McpHeadersRefreshRequiredReason.STARTUP, + ), + id="evt-refresh", + timestamp="2026-01-01T00:00:00Z", + type=SessionEventType.MCP_HEADERS_REFRESH_REQUIRED, + ephemeral=True, + parent_id=None, + ) + session._dispatch_event(event) + for _ in range(200): + if transport.request.await_count == 1: + break + await asyncio.sleep(0.005) + + await session._execute_mcp_headers_refresh_and_respond( + "refresh-2", + { + "server_name": "GitHub", + "server_url": "https://example.com/mcp", + "reason": "startup", + }, + handle_refresh, + ) + await session._execute_mcp_headers_refresh_and_respond( + "refresh-3", + { + "server_name": "GitHub", + "server_url": "https://example.com/mcp", + "reason": "startup", + }, + handle_refresh, + ) + + def fail_refresh(_request, _context): + raise RuntimeError("credential revoked") + + await session._execute_mcp_headers_refresh_and_respond( + "refresh-4", + { + "server_name": "GitHub", + "server_url": "https://example.com/mcp", + "reason": "auth-failed", + }, + fail_refresh, + ) + await session._execute_mcp_headers_refresh_and_respond( + "refresh-5", + { + "server_name": "GitHub", + "server_url": "https://example.com/mcp", + "reason": "auth-failed", + }, + lambda _request, _context: (_ for _ in ()).throw(asyncio.CancelledError()), + ) + + results = [call.args[1]["result"] for call in transport.request.await_args_list] + assert results == [ + { + "kind": "headers", + "headers": {"Authorization": "Bearer short-lived"}, + }, + { + "kind": "headers", + "headers": {"Authorization": "Bearer refreshed"}, + "ttlMs": 5_000, + }, + {"kind": "none"}, + {"kind": "error", "message": "credential revoked"}, + {"kind": "error", "message": "MCP headers refresh cancelled"}, + ] + + @pytest.mark.asyncio + async def test_create_and_resume_forward_managed_mcp_servers(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + + async def mock_request(method, params, **kwargs): + captured.append((method, params)) + if method in ("session.create", "session.resume"): + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + if method == "session.eventLog.registerInterest": + return {"id": "interest-1"} + return {} + + client._client.request = mock_request + managed: dict[str, ManagedMCPServerConfig] = { + "github": { + "display_name": "GitHub", + "url": "https://example.com/mcp", + "tools": ["issues"], + "timeout": 30_000, + "headers_refresh_ttl_ms": 60_000, + } + } + + session = await client.create_session( + managed_mcp_servers=managed, + on_mcp_headers_refresh=lambda _request, _context: None, + ) + await client.resume_session( + session.session_id, + managed_mcp_servers=managed, + on_mcp_headers_refresh=lambda _request, _context: None, + ) + + requests = { + method: params + for method, params in captured + if method in ("session.create", "session.resume") + } + expected = { + "github": { + "displayName": "GitHub", + "url": "https://example.com/mcp", + "tools": ["issues"], + "timeout": 30_000, + "headersRefreshTtlMs": 60_000, + } + } + assert requests["session.create"]["managedMcpServers"] == expected + assert requests["session.resume"]["managedMcpServers"] == expected + assert ( + sum( + method == "session.eventLog.registerInterest" + and params["eventType"] == "mcp.headers_refresh_required" + for method, params in captured + ) + == 2 + ) + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_session_forwards_cloud_options(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index bdf291e599..d98ff0144c 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -7735,6 +7735,9 @@ pub struct McpHeadersHandlePendingHeadersRefreshRequestHeaders { pub headers: HashMap, /// Headers-refresh response variant discriminator. pub kind: McpHeadersHandlePendingHeadersRefreshRequestHeadersKind, + /// Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value. + #[serde(skip_serializing_if = "Option::is_none")] + pub ttl_ms: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -7744,6 +7747,15 @@ pub struct McpHeadersHandlePendingHeadersRefreshRequestNone { pub kind: McpHeadersHandlePendingHeadersRefreshRequestNoneKind, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpHeadersHandlePendingHeadersRefreshRequestError { + /// Headers-refresh response variant discriminator. + pub kind: McpHeadersHandlePendingHeadersRefreshRequestErrorKind, + /// Host credential broker failure, denial, or revocation reason. + pub message: String, +} + /// MCP headers refresh request id and the host response. /// ///

@@ -8967,6 +8979,9 @@ pub struct McpSamplingExecutionResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpServer { + /// Human-readable display name supplied by a managed server catalog. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, /// Error message if the server failed to connect #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, @@ -16474,6 +16489,32 @@ pub struct SessionOpenOptionsAdditionalContentExclusionPolicy { pub scope: SessionOpenOptionsAdditionalContentExclusionPolicyScope, } +/// Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ManagedMcpServerConfig { + /// Human-readable catalog display name. + pub display_name: String, + /// Maximum dynamic-header cache lifetime in milliseconds. + #[serde(skip_serializing_if = "Option::is_none")] + pub headers_refresh_ttl_ms: Option, + /// Timeout in milliseconds for tool discovery and tool calls. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + /// Tools to include. Defaults to all tools when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option>, + /// Hosted MCP streamable HTTP endpoint. + pub url: String, +} + /// Command-scoped GitHub credential injection for the shell commands an agent runs. /// /// Each channel is opt-in and independent, and injection is scoped to the individual command @@ -16720,6 +16761,16 @@ pub struct SessionOpenOptions { /// Identifier sent to LSP-style integrations. #[serde(skip_serializing_if = "Option::is_none")] pub lsp_client_name: Option, + /// Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed provenance is runtime-established from this separate field and credentials are supplied through dynamic-header refresh. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub managed_mcp_servers: Option>, /// Permissions-only enterprise policy injected by the SDK host at session create or resume. Composes restrictively with self-fetched and device policy and is not persisted. #[serde(skip_serializing_if = "Option::is_none")] pub managed_settings: Option, @@ -29828,6 +29879,14 @@ pub enum McpHeadersHandlePendingHeadersRefreshRequestNoneKind { None, } +/// Headers-refresh response variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpHeadersHandlePendingHeadersRefreshRequestErrorKind { + #[serde(rename = "error")] + #[default] + Error, +} + /// Host response: supply dynamic headers or decline this refresh. /// ///
@@ -29841,6 +29900,7 @@ pub enum McpHeadersHandlePendingHeadersRefreshRequestNoneKind { pub enum McpHeadersHandlePendingHeadersRefreshRequest { Headers(McpHeadersHandlePendingHeadersRefreshRequestHeaders), None(McpHeadersHandlePendingHeadersRefreshRequestNone), + Error(McpHeadersHandlePendingHeadersRefreshRequestError), } /// Whether a planned configuration change would create or modify an entry diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index a5b123d3b5..848fe0e401 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -5350,6 +5350,9 @@ pub struct SessionCustomAgentsUpdatedData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpServersLoadedServer { + /// Human-readable display name supplied by a managed server catalog. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, /// Error message if the server failed to connect #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, @@ -7423,7 +7426,7 @@ pub enum SkillSource { Unknown, } -/// Configuration source: user, workspace, plugin, or builtin +/// Configuration source: user, workspace, plugin, builtin, or managed #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpServerSource { /// Server configured in the user's global MCP configuration. @@ -7438,6 +7441,9 @@ pub enum McpServerSource { /// Server bundled with the runtime. #[serde(rename = "builtin")] Builtin, + /// Server supplied by a trusted host-managed catalog. + #[serde(rename = "managed")] + Managed, /// Unknown variant for forward compatibility. #[default] #[serde(other)] diff --git a/rust/src/handler.rs b/rust/src/handler.rs index e036b75a10..fd112dc45e 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -2,7 +2,7 @@ //! //! Each callback the CLI may dispatch (permission requests, elicitation //! prompts, user-input questions, exit-plan-mode prompts, -//! auto-mode-switch prompts) has its own focused trait with a single +//! auto-mode-switch prompts, MCP credential requests) has its own focused trait with a single //! `handle` method. //! //! Handlers are **optional**: install only the ones the application cares @@ -15,17 +15,27 @@ //! [`Tool::with_handler`](crate::types::Tool::with_handler) on entries passed to //! [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools). +use std::collections::HashMap; + use async_trait::async_trait; use serde::{Deserialize, Serialize}; use crate::generated::api_types::{ - McpOauthPendingRequestResponse, McpOauthPendingRequestResponseCancelled, - McpOauthPendingRequestResponseCancelledKind, McpOauthPendingRequestResponseToken, - McpOauthPendingRequestResponseTokenKind, PermissionDecision, PermissionDecisionApproveOnce, - PermissionDecisionContext, PermissionDecisionReject, PermissionDecisionUserNotAvailable, + McpHeadersHandlePendingHeadersRefreshRequest, + McpHeadersHandlePendingHeadersRefreshRequestError, + McpHeadersHandlePendingHeadersRefreshRequestErrorKind, + McpHeadersHandlePendingHeadersRefreshRequestHeaders, + McpHeadersHandlePendingHeadersRefreshRequestHeadersKind, + McpHeadersHandlePendingHeadersRefreshRequestNone, + McpHeadersHandlePendingHeadersRefreshRequestNoneKind, McpOauthPendingRequestResponse, + McpOauthPendingRequestResponseCancelled, McpOauthPendingRequestResponseCancelledKind, + McpOauthPendingRequestResponseToken, McpOauthPendingRequestResponseTokenKind, + PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionContext, + PermissionDecisionReject, PermissionDecisionUserNotAvailable, }; use crate::session_events::{ - McpOauthRequestReason, McpOauthRequiredStaticClientConfig, McpOauthWWWAuthenticateParams, + McpHeadersRefreshRequiredReason, McpOauthRequestReason, McpOauthRequiredStaticClientConfig, + McpOauthWWWAuthenticateParams, }; use crate::types::{ ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId, @@ -294,6 +304,75 @@ pub trait McpAuthHandler: Send + Sync + 'static { ) -> McpAuthResult; } +/// Dynamic-header request for a host-managed MCP server. +#[derive(Debug, Clone)] +pub struct McpHeadersRefreshRequest { + /// Display name of the MCP server that requires headers. + pub server_name: String, + /// URL of the MCP server that requires headers. + pub server_url: String, + /// Why the runtime is requesting fresh headers. + pub reason: McpHeadersRefreshRequiredReason, +} + +/// Result returned by an [`McpHeadersRefreshHandler`]. +#[derive(Debug, Clone)] +pub enum McpHeadersRefreshResult { + /// Supplies dynamic HTTP headers for the managed MCP server. + Headers { + /// Headers to overlay onto MCP requests. + headers: HashMap, + /// Optional lifetime in milliseconds for the returned headers. + ttl_ms: Option, + }, + /// Indicates that no dynamic headers are available. + None, +} + +impl McpHeadersRefreshResult { + pub(crate) fn into_wire(self) -> McpHeadersHandlePendingHeadersRefreshRequest { + match self { + Self::Headers { headers, ttl_ms } => { + McpHeadersHandlePendingHeadersRefreshRequest::Headers( + McpHeadersHandlePendingHeadersRefreshRequestHeaders { + headers, + kind: McpHeadersHandlePendingHeadersRefreshRequestHeadersKind::Headers, + ttl_ms, + }, + ) + } + Self::None => McpHeadersHandlePendingHeadersRefreshRequest::None( + McpHeadersHandlePendingHeadersRefreshRequestNone { + kind: McpHeadersHandlePendingHeadersRefreshRequestNoneKind::None, + }, + ), + } + } +} + +pub(crate) fn mcp_headers_error_result( + message: impl Into, +) -> McpHeadersHandlePendingHeadersRefreshRequest { + McpHeadersHandlePendingHeadersRefreshRequest::Error( + McpHeadersHandlePendingHeadersRefreshRequestError { + kind: McpHeadersHandlePendingHeadersRefreshRequestErrorKind::Error, + message: message.into(), + }, + ) +} + +/// Handler for dynamic headers requested by host-managed MCP servers. +#[async_trait] +pub trait McpHeadersRefreshHandler: Send + Sync + 'static { + /// Resolve a managed MCP headers refresh request. + async fn handle( + &self, + session_id: SessionId, + request_id: RequestId, + request: McpHeadersRefreshRequest, + ) -> Result; +} + /// Handler for `user_input.requested` events from the `ask_user` tool. /// /// When unset, `requestUserInput: false` goes on the wire and the diff --git a/rust/src/session.rs b/rust/src/session.rs index b9d2173055..87a4a5ca30 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -12,17 +12,20 @@ use tracing::{Instrument, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, OpenCanvasInstance, PermissionDecisionRequest, - RegisterEventInterestParams, ToolsGetCurrentMetadataResult, rpc_methods, + LogRequest, McpHeadersHandlePendingHeadersRefreshRequestRequest, ModelSwitchToRequest, + OpenCanvasInstance, PermissionDecisionRequest, RegisterEventInterestParams, + ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ - CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, - SessionCanvasClosedData, SessionErrorData, SessionEventType, SessionIdleData, SessionMode, + CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, + McpHeadersRefreshRequiredData, McpOauthRequiredData, SessionCanvasClosedData, SessionErrorData, + SessionEventType, SessionIdleData, SessionMode, }; use crate::handler::{ AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler, - McpAuthHandler, McpAuthRequest, McpAuthResult, PermissionHandler, PermissionResult, - UserInputHandler, UserInputResponse, + McpAuthHandler, McpAuthRequest, McpAuthResult, McpHeadersRefreshHandler, + McpHeadersRefreshRequest, PermissionHandler, PermissionResult, UserInputHandler, + UserInputResponse, mcp_headers_error_result, }; use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; @@ -60,6 +63,7 @@ pub(crate) struct SessionHandlers { pub managed_settings_enabled: bool, pub elicitation: Option>, pub mcp_auth: Option>, + pub mcp_headers: Option>, pub user_input: Option>, pub exit_plan_mode: Option>, pub auto_mode_switch: Option>, @@ -924,6 +928,7 @@ impl Client { ), elicitation: runtime.elicitation_handler.take(), mcp_auth: runtime.mcp_auth_handler.take(), + mcp_headers: runtime.mcp_headers_handler.take(), user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), @@ -946,6 +951,7 @@ impl Client { .as_ref() .map(|registration| registration.id().to_string()); let has_mcp_auth_handler = handlers.mcp_auth.is_some(); + let has_mcp_headers_handler = handlers.mcp_headers.is_some(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into()); } @@ -1072,6 +1078,8 @@ impl Client { event_tx.clone(), shutdown.clone(), ); + let mut registration = + PendingSessionRegistration::new(self.clone(), session_id.clone(), shutdown.clone()); tracing::debug!( elapsed_ms = setup_start.elapsed().as_millis(), session_id = %session_id, @@ -1081,8 +1089,16 @@ impl Client { "Client::create_session local setup complete" ); *capabilities.write() = create_result.capabilities.unwrap_or_default(); - if has_mcp_auth_handler { - register_mcp_auth_interest(self, &session_id).await?; + if let Err(error) = register_mcp_handler_interests( + self, + &session_id, + has_mcp_auth_handler, + has_mcp_headers_handler, + ) + .await + { + registration.cleanup(event_loop).await; + return Err(error); } tracing::debug!( @@ -1090,6 +1106,7 @@ impl Client { session_id = %session_id, "Client::create_session complete" ); + registration.disarm(); let session = Session { id: session_id, cwd: self.cwd().clone(), @@ -1212,6 +1229,7 @@ impl Client { ), elicitation: runtime.elicitation_handler.take(), mcp_auth: runtime.mcp_auth_handler.take(), + mcp_headers: runtime.mcp_headers_handler.take(), user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), @@ -1234,6 +1252,7 @@ impl Client { .as_ref() .map(|registration| registration.id().to_string()); let has_mcp_auth_handler = handlers.mcp_auth.is_some(); + let has_mcp_headers_handler = handlers.mcp_headers.is_some(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into()); } @@ -1320,8 +1339,16 @@ impl Client { }) .into()); } - if has_mcp_auth_handler { - register_mcp_auth_interest(self, &session_id).await?; + if let Err(error) = register_mcp_handler_interests( + self, + &session_id, + has_mcp_auth_handler, + has_mcp_headers_handler, + ) + .await + { + registration.cleanup(event_loop).await; + return Err(error); } // Reload skills after resume (best-effort). @@ -1677,9 +1704,13 @@ fn permission_response_params( Some(params) } -async fn register_mcp_auth_interest(client: &Client, session_id: &SessionId) -> Result<(), Error> { +async fn register_mcp_event_interest( + client: &Client, + session_id: &SessionId, + event_type: &str, +) -> Result<(), Error> { let mut params = serde_json::to_value(RegisterEventInterestParams { - event_type: "mcp.oauth_required".to_string(), + event_type: event_type.to_string(), })?; params["sessionId"] = Value::String(session_id.to_string()); client @@ -1688,6 +1719,21 @@ async fn register_mcp_auth_interest(client: &Client, session_id: &SessionId) -> Ok(()) } +async fn register_mcp_handler_interests( + client: &Client, + session_id: &SessionId, + has_auth_handler: bool, + has_headers_handler: bool, +) -> Result<(), Error> { + if has_auth_handler { + register_mcp_event_interest(client, session_id, "mcp.oauth_required").await?; + } + if has_headers_handler { + register_mcp_event_interest(client, session_id, "mcp.headers_refresh_required").await?; + } + Ok(()) +} + fn tool_failure_result(message: impl Into) -> ToolResult { let message = message.into(); ToolResult::Expanded(ToolResultExpanded { @@ -2266,6 +2312,95 @@ async fn handle_notification( .instrument(span), ); } + SessionEventType::McpHeadersRefreshRequired => { + let Some(mcp_headers_handler) = handlers.mcp_headers.clone() else { + warn!( + session_id = %session_id, + "received MCP headers refresh request without a registered handler" + ); + return; + }; + let data: McpHeadersRefreshRequiredData = + match serde_json::from_value(notification.event.data.clone()) { + Ok(data) => data, + Err(error) => { + warn!(error = %error, "failed to deserialize MCP headers refresh request"); + return; + } + }; + let request_id = data.request_id; + let request = McpHeadersRefreshRequest { + server_name: data.server_name, + server_url: data.server_url, + reason: data.reason, + }; + let client = client.clone(); + let sid = session_id.clone(); + let span = tracing::error_span!( + "mcp_headers_request_handler", + session_id = %sid, + request_id = %request_id + ); + tokio::spawn( + async move { + let handler_task = tokio::spawn({ + let sid = sid.clone(); + let request_id = request_id.clone(); + let span = tracing::error_span!( + "mcp_headers_callback", + session_id = %sid, + request_id = %request_id + ); + async move { + let handler_start = Instant::now(); + let result = mcp_headers_handler + .handle(sid.clone(), request_id.clone(), request) + .await; + tracing::debug!( + elapsed_ms = handler_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + "McpHeadersRefreshHandler::handle dispatch" + ); + result + } + .instrument(span) + }); + let result = match handler_task.await { + Ok(Ok(result)) => result.into_wire(), + Ok(Err(error)) => mcp_headers_error_result(error.to_string()), + Err(error) => mcp_headers_error_result(error.to_string()), + }; + let params = McpHeadersHandlePendingHeadersRefreshRequestRequest { + request_id: request_id.clone(), + result, + }; + let mut wire_params = match serde_json::to_value(params) { + Ok(params) => params, + Err(error) => { + warn!(error = %error, "failed to serialize MCP headers response"); + return; + } + }; + wire_params["sessionId"] = Value::String(sid.to_string()); + if let Err(error) = client + .call( + rpc_methods::SESSION_MCP_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST, + Some(wire_params), + ) + .await + { + warn!( + error = %error, + session_id = %sid, + request_id = %request_id, + "failed to send MCP headers response" + ); + } + } + .instrument(span), + ); + } SessionEventType::CommandExecute => { let data: CommandExecuteData = match serde_json::from_value(notification.event.data.clone()) { diff --git a/rust/src/types.rs b/rust/src/types.rs index 6e451eb452..df5ab6de0a 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -20,6 +20,7 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage, CopilotWebSocketResponse, WebSocketTransform, forward_http, }; +pub use crate::generated::api_types::ManagedMcpServerConfig; use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; use crate::generated::session_events::ReasoningSummary; /// Context window tier for models that support tiered context windows. @@ -27,7 +28,7 @@ pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig}; use crate::github_token::GitHubTokenProvider; use crate::handler::{ AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler, - PermissionHandler, UserInputHandler, + McpHeadersRefreshHandler, PermissionHandler, UserInputHandler, }; use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; @@ -1962,6 +1963,11 @@ pub struct SessionConfig { pub included_builtin_skills: Option>, /// MCP server configurations passed through to the CLI. pub mcp_servers: Option>, + /// Non-secret host-managed HTTP MCP servers keyed by stable managed identity. + /// + /// Credentials are supplied dynamically through + /// [`McpHeadersRefreshHandler`](crate::handler::McpHeadersRefreshHandler). + pub managed_mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// /// - `"persistent"` — tokens are stored in the OS keychain (shared across sessions). @@ -2181,6 +2187,8 @@ pub struct SessionConfig { /// Optional MCP OAuth request handler. When set, the SDK can satisfy MCP /// server OAuth requests with host-acquired token data or cancellation. pub mcp_auth_handler: Option>, + /// Optional dynamic-header handler for host-managed MCP servers. + pub mcp_headers_handler: Option>, /// Optional user-input handler. When `None`, /// `requestUserInput: false` goes on the wire and the `ask_user` /// tool is disabled. @@ -2254,6 +2262,7 @@ impl std::fmt::Debug for SessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) + .field("managed_mcp_servers", &self.managed_mcp_servers) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field("embedding_cache_storage", &self.embedding_cache_storage) .field("enable_config_discovery", &self.enable_config_discovery) @@ -2338,6 +2347,10 @@ impl std::fmt::Debug for SessionConfig { "mcp_auth_handler", &self.mcp_auth_handler.as_ref().map(|_| ""), ) + .field( + "mcp_headers_handler", + &self.mcp_headers_handler.as_ref().map(|_| ""), + ) .field( "user_input_handler", &self.user_input_handler.as_ref().map(|_| ""), @@ -2391,6 +2404,7 @@ impl Default for SessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, + managed_mcp_servers: None, mcp_oauth_token_storage: None, enable_config_discovery: None, skip_embedding_retrieval: None, @@ -2441,6 +2455,7 @@ impl Default for SessionConfig { permission_handler: None, elicitation_handler: None, mcp_auth_handler: None, + mcp_headers_handler: None, user_input_handler: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, @@ -2466,6 +2481,7 @@ pub(crate) struct SessionConfigRuntime { pub permission_policy: Option, pub elicitation_handler: Option>, pub mcp_auth_handler: Option>, + pub mcp_headers_handler: Option>, pub user_input_handler: Option>, pub exit_plan_mode_handler: Option>, pub auto_mode_switch_handler: Option>, @@ -2557,6 +2573,7 @@ impl SessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, + managed_mcp_servers: self.managed_mcp_servers, mcp_oauth_token_storage: self.mcp_oauth_token_storage, embedding_cache_storage: self.embedding_cache_storage, env_value_mode: "direct", @@ -2619,6 +2636,7 @@ impl SessionConfig { permission_policy: self.permission_policy, elicitation_handler: self.elicitation_handler, mcp_auth_handler: self.mcp_auth_handler, + mcp_headers_handler: self.mcp_headers_handler, user_input_handler: self.user_input_handler, exit_plan_mode_handler: self.exit_plan_mode_handler, auto_mode_switch_handler: self.auto_mode_switch_handler, @@ -2656,6 +2674,12 @@ impl SessionConfig { self } + /// Install an [`McpHeadersRefreshHandler`] for host-managed MCP dynamic headers. + pub fn with_mcp_headers_handler(mut self, handler: Arc) -> Self { + self.mcp_headers_handler = Some(handler); + self + } + /// Install a [`UserInputHandler`]. Required for the `ask_user` tool /// to be enabled. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { @@ -2880,6 +2904,15 @@ impl SessionConfig { self } + /// Set host-managed HTTP MCP server configurations. + pub fn with_managed_mcp_servers( + mut self, + servers: HashMap, + ) -> Self { + self.managed_mcp_servers = Some(servers); + self + } + /// Set MCP OAuth token storage mode. /// /// - `"persistent"` — tokens stored in the OS keychain. @@ -3342,6 +3375,8 @@ pub struct ResumeSessionConfig { pub included_builtin_skills: Option>, /// Re-supply MCP servers so they remain available after app restart. pub mcp_servers: Option>, + /// Re-supply host-managed HTTP MCP servers after app restart. + pub managed_mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// See [`SessionConfig::mcp_oauth_token_storage`] for details. pub mcp_oauth_token_storage: Option, @@ -3506,6 +3541,8 @@ pub struct ResumeSessionConfig { pub elicitation_handler: Option>, /// Optional MCP OAuth handler. See [`SessionConfig::mcp_auth_handler`]. pub mcp_auth_handler: Option>, + /// Optional managed MCP dynamic-header handler. + pub mcp_headers_handler: Option>, /// Optional user-input handler. See /// [`SessionConfig::user_input_handler`]. pub user_input_handler: Option>, @@ -3564,6 +3601,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) + .field("managed_mcp_servers", &self.managed_mcp_servers) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field("embedding_cache_storage", &self.embedding_cache_storage) .field("enable_config_discovery", &self.enable_config_discovery) @@ -3643,6 +3681,10 @@ impl std::fmt::Debug for ResumeSessionConfig { "elicitation_handler", &self.elicitation_handler.as_ref().map(|_| ""), ) + .field( + "mcp_headers_handler", + &self.mcp_headers_handler.as_ref().map(|_| ""), + ) .field( "user_input_handler", &self.user_input_handler.as_ref().map(|_| ""), @@ -3743,6 +3785,7 @@ impl ResumeSessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, + managed_mcp_servers: self.managed_mcp_servers, mcp_oauth_token_storage: self.mcp_oauth_token_storage, embedding_cache_storage: self.embedding_cache_storage, env_value_mode: "direct", @@ -3806,6 +3849,7 @@ impl ResumeSessionConfig { permission_policy: self.permission_policy, elicitation_handler: self.elicitation_handler, mcp_auth_handler: self.mcp_auth_handler, + mcp_headers_handler: self.mcp_headers_handler, user_input_handler: self.user_input_handler, exit_plan_mode_handler: self.exit_plan_mode_handler, auto_mode_switch_handler: self.auto_mode_switch_handler, @@ -3850,6 +3894,7 @@ impl ResumeSessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, + managed_mcp_servers: None, mcp_oauth_token_storage: None, enable_config_discovery: None, skip_embedding_retrieval: None, @@ -3901,6 +3946,7 @@ impl ResumeSessionConfig { permission_handler: None, elicitation_handler: None, mcp_auth_handler: None, + mcp_headers_handler: None, user_input_handler: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, @@ -3933,6 +3979,12 @@ impl ResumeSessionConfig { self } + /// Install an [`McpHeadersRefreshHandler`] for host-managed MCP dynamic headers. + pub fn with_mcp_headers_handler(mut self, handler: Arc) -> Self { + self.mcp_headers_handler = Some(handler); + self + } + /// Install a [`UserInputHandler`] for the resumed session. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { self.user_input_handler = Some(handler); @@ -4149,6 +4201,15 @@ impl ResumeSessionConfig { self } + /// Re-supply host-managed HTTP MCP server configurations on resume. + pub fn with_managed_mcp_servers( + mut self, + servers: HashMap, + ) -> Self { + self.managed_mcp_servers = Some(servers); + self + } + /// Set MCP OAuth token storage mode on resume. /// See [`SessionConfig::with_mcp_oauth_token_storage`] for details. pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into) -> Self { @@ -5969,11 +6030,12 @@ mod tests { AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, - InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, - MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, - ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, - SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, + InfiniteSessionConfig, LargeToolOutputConfig, ManagedMcpServerConfig, McpServerConfig, + McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, + PermissionResponseCapability, ProviderConfig, ProviderModelConfig, ReasoningSummary, + ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool, + ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse, + ensure_attachment_display_names, }; use crate::generated::session_events::TypedSessionEvent; @@ -6257,6 +6319,46 @@ mod tests { assert!(!wire.request_mcp_apps); } + #[test] + fn managed_mcp_servers_serialize_on_create_and_resume() { + let servers = HashMap::from([( + "catalog-id".to_string(), + ManagedMcpServerConfig { + display_name: "Managed server".to_string(), + headers_refresh_ttl_ms: Some(30_000), + timeout: Some(15_000), + tools: Some(vec!["search".to_string()]), + url: "https://mcp.example.test".to_string(), + }, + )]); + + let (create_wire, _) = SessionConfig::default() + .with_managed_mcp_servers(servers.clone()) + .into_wire(Some(SessionId::from("managed-create"))) + .expect("create config has no duplicate handlers"); + let create_json = serde_json::to_value(create_wire).unwrap(); + assert_eq!( + create_json["managedMcpServers"]["catalog-id"], + json!({ + "displayName": "Managed server", + "headersRefreshTtlMs": 30_000, + "timeout": 15_000, + "tools": ["search"], + "url": "https://mcp.example.test" + }) + ); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("managed-resume")) + .with_managed_mcp_servers(servers) + .into_wire() + .expect("resume config has no duplicate handlers"); + let resume_json = serde_json::to_value(resume_wire).unwrap(); + assert_eq!( + resume_json["managedMcpServers"]["catalog-id"]["displayName"], + "Managed server" + ); + } + #[test] fn custom_agents_local_only_serializes_on_create_and_resume() { let (create_wire, _) = SessionConfig::default() diff --git a/rust/src/wire.rs b/rust/src/wire.rs index f7de33839c..1ecad408e9 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -13,6 +13,7 @@ //! configs hold trait-object handlers, the wire structs hold only the //! plain data the runtime needs. +use std::collections::HashMap; use std::path::PathBuf; use indexmap::IndexMap; @@ -20,7 +21,7 @@ use serde::Serialize; use crate::canvas::CanvasDeclaration; use crate::generated::api_types::{ - ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, + ManagedMcpServerConfig, ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ @@ -89,6 +90,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub managed_mcp_servers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub embedding_cache_storage: Option, @@ -244,6 +247,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub managed_mcp_servers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub embedding_cache_storage: Option, diff --git a/rust/tests/e2e/rpc_tasks_and_handlers.rs b/rust/tests/e2e/rpc_tasks_and_handlers.rs index 540238ddd0..d0de7a237c 100644 --- a/rust/tests/e2e/rpc_tasks_and_handlers.rs +++ b/rust/tests/e2e/rpc_tasks_and_handlers.rs @@ -428,6 +428,7 @@ async fn should_return_expected_results_for_missing_pending_handler_requestids() "missing".to_string(), )]), kind: McpHeadersHandlePendingHeadersRefreshRequestHeadersKind::Headers, + ttl_ms: None, }, ), }, diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index a51d61910d..45b7cf503f 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -1,5 +1,6 @@ #![allow(clippy::unwrap_used)] +use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -13,15 +14,16 @@ use github_copilot_sdk::github_token::{ use github_copilot_sdk::handler::{ ApproveAllHandler, AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler, ExitPlanModeResult, McpAuthHandler, McpAuthRequest, McpAuthResult, - PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, + McpHeadersRefreshHandler, McpHeadersRefreshRequest, McpHeadersRefreshResult, PermissionHandler, + PermissionResult, UserInputHandler, UserInputResponse, }; use github_copilot_sdk::rpc::{ CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, OpenCanvasInstance, }; use github_copilot_sdk::session_events::{ - ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, - SessionManagedSettingsResolvedData, + ManagedSettingsResolvedSource, McpHeadersRefreshRequiredReason, McpOauthRequiredData, + ReasoningSummary, SessionLimitsConfig, SessionManagedSettingsResolvedData, }; use github_copilot_sdk::types::{ CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, CommandContext, @@ -42,6 +44,10 @@ struct TestCanvasHandler; struct CancelMcpAuthHandler; +struct TestMcpHeadersHandler { + calls: tokio::sync::mpsc::UnboundedSender<(SessionId, RequestId, McpHeadersRefreshRequest)>, +} + struct ContextualApproveHandler; #[async_trait] @@ -73,6 +79,41 @@ impl McpAuthHandler for CancelMcpAuthHandler { } } +#[async_trait] +impl McpHeadersRefreshHandler for TestMcpHeadersHandler { + async fn handle( + &self, + session_id: SessionId, + request_id: RequestId, + request: McpHeadersRefreshRequest, + ) -> Result { + self.calls + .send((session_id, request_id, request.clone())) + .unwrap(); + match request.server_name.as_str() { + "ttl-absent" => Ok(McpHeadersRefreshResult::Headers { + headers: HashMap::from([( + "Authorization".to_string(), + "Bearer absent".to_string(), + )]), + ttl_ms: None, + }), + "ttl-present" => Ok(McpHeadersRefreshResult::Headers { + headers: HashMap::from([( + "Authorization".to_string(), + "Bearer present".to_string(), + )]), + ttl_ms: Some(12_345), + }), + "none" => Ok(McpHeadersRefreshResult::None), + _ => Err(github_copilot_sdk::Error::with_message( + ErrorKind::InvalidConfig, + "credential broker unavailable", + )), + } + } +} + #[async_trait] impl CanvasHandler for TestCanvasHandler { async fn on_open( @@ -741,6 +782,197 @@ async fn resume_session_registers_mcp_auth_interest_only_with_handler() { let _session = timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn managed_mcp_headers_handler_dispatches_all_results() { + let (calls_tx, mut calls_rx) = tokio::sync::mpsc::unbounded_channel(); + let handler = Arc::new(TestMcpHeadersHandler { calls: calls_tx }); + let (client, server_read, server_write) = make_client(); + let mut server = FakeServer { + read: server_read, + write: server_write, + session_id: "managed-mcp-session".to_string(), + }; + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("managed-mcp-session") + .with_mcp_headers_handler(handler), + ) + .await + .unwrap() + } + }); + + let create_request = server.read_request().await; + assert_eq!(create_request["method"], "session.create"); + server + .respond( + &create_request, + serde_json::json!({ + "sessionId": "managed-mcp-session", + "workspacePath": "/workspace" + }), + ) + .await; + let interest_request = server.read_request().await; + assert_eq!( + interest_request["method"], + "session.eventLog.registerInterest" + ); + assert_eq!( + interest_request["params"]["eventType"], + "mcp.headers_refresh_required" + ); + server + .respond(&interest_request, serde_json::json!({ "id": "interest-1" })) + .await; + let _session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); + + let cases = [ + ( + "ttl-absent", + "startup", + McpHeadersRefreshRequiredReason::Startup, + serde_json::json!({ + "kind": "headers", + "headers": { "Authorization": "Bearer absent" } + }), + ), + ( + "ttl-present", + "ttl-expired", + McpHeadersRefreshRequiredReason::TtlExpired, + serde_json::json!({ + "kind": "headers", + "headers": { "Authorization": "Bearer present" }, + "ttlMs": 12_345 + }), + ), + ( + "none", + "auth-failed", + McpHeadersRefreshRequiredReason::AuthFailed, + serde_json::json!({ "kind": "none" }), + ), + ( + "error", + "startup", + McpHeadersRefreshRequiredReason::Startup, + serde_json::json!({ + "kind": "error", + "message": "credential broker unavailable" + }), + ), + ]; + + for (index, (server_name, reason, expected_reason, expected_result)) in + cases.into_iter().enumerate() + { + let request_id = format!("headers-{index}"); + let server_url = format!("https://{server_name}.example.test/mcp"); + server + .send_event( + "mcp.headers_refresh_required", + serde_json::json!({ + "requestId": request_id, + "serverName": server_name, + "serverUrl": server_url, + "reason": reason + }), + ) + .await; + + let response_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!( + response_request["method"], + "session.mcp.headers.handlePendingHeadersRefreshRequest" + ); + assert_eq!(response_request["params"]["requestId"], request_id); + assert_eq!(response_request["params"]["result"], expected_result); + + let (received_session_id, received_request_id, received_request) = + timeout(TIMEOUT, calls_rx.recv()).await.unwrap().unwrap(); + assert_eq!(received_session_id.as_str(), "managed-mcp-session"); + assert_eq!(received_request_id, request_id); + assert_eq!(received_request.server_name, server_name); + assert_eq!(received_request.server_url, server_url); + assert_eq!(received_request.reason, expected_reason); + + server + .respond(&response_request, serde_json::json!({ "success": true })) + .await; + } +} + +#[tokio::test] +async fn managed_mcp_interest_failure_cleans_up_session_dispatch() { + let (calls_tx, mut calls_rx) = tokio::sync::mpsc::unbounded_channel(); + let handler = Arc::new(TestMcpHeadersHandler { calls: calls_tx }); + let (client, server_read, server_write) = make_client(); + let mut server = FakeServer { + read: server_read, + write: server_write, + session_id: "managed-mcp-interest-failure".to_string(), + }; + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_session_id("managed-mcp-interest-failure") + .with_mcp_headers_handler(handler), + ) + .await + } + }); + + let create_request = server.read_request().await; + server + .respond( + &create_request, + serde_json::json!({ + "sessionId": "managed-mcp-interest-failure", + "workspacePath": "/workspace" + }), + ) + .await; + let interest_request = server.read_request().await; + assert_eq!( + interest_request["method"], + "session.eventLog.registerInterest" + ); + assert_eq!( + interest_request["params"]["eventType"], + "mcp.headers_refresh_required" + ); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": interest_request["id"], + "error": { "code": -32603, "message": "registration failed" } + }); + write_framed(&mut server.write, &serde_json::to_vec(&response).unwrap()).await; + + let result = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); + assert!(result.is_err()); + + server + .send_event( + "mcp.headers_refresh_required", + serde_json::json!({ + "requestId": "stale-request", + "serverName": "ttl-absent", + "serverUrl": "https://example.test/mcp", + "reason": "startup" + }), + ) + .await; + assert!(timeout(TIMEOUT, calls_rx.recv()).await.unwrap().is_none()); +} + async fn server_respond_create( writer: &mut (impl AsyncWrite + Unpin), request: &Value, From baea1f5b9209a52066ef8a383257b01fde4e0a85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:23:24 +0000 Subject: [PATCH 02/16] Regenerate Java codegen output Auto-committed by java-codegen-check workflow. --- .../copilot/generated/McpServerSource.java | 6 ++-- .../generated/McpServersLoadedServer.java | 4 +-- .../generated/rpc/ManagedMcpServerConfig.java | 36 ------------------- .../copilot/generated/rpc/McpServer.java | 4 +-- .../generated/rpc/McpServerSource.java | 6 ++-- .../generated/rpc/SessionOpenOptions.java | 4 +-- 6 files changed, 7 insertions(+), 53 deletions(-) delete mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java index 5ea2c39f51..63514743ab 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, builtin, or managed + * Configuration source: user, workspace, plugin, or builtin * * @since 1.0.0 */ @@ -23,9 +23,7 @@ public enum McpServerSource { /** The {@code plugin} variant. */ PLUGIN("plugin"), /** The {@code builtin} variant. */ - BUILTIN("builtin"), - /** The {@code managed} variant. */ - MANAGED("managed"); + BUILTIN("builtin"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java index bc840d90bd..c4567f30fa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java @@ -34,8 +34,6 @@ public record McpServersLoadedServer( /** Name of the plugin that supplied the effective MCP server config, only when source is plugin */ @JsonProperty("pluginName") String pluginName, /** Version of the plugin that supplied the effective MCP server config, only when source is plugin */ - @JsonProperty("pluginVersion") String pluginVersion, - /** Human-readable display name supplied by a managed server catalog. */ - @JsonProperty("displayName") String displayName + @JsonProperty("pluginVersion") String pluginVersion ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java deleted file mode 100644 index 4a202063af..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import javax.annotation.processing.Generated; - -/** - * Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record ManagedMcpServerConfig( - /** Human-readable catalog display name. */ - @JsonProperty("displayName") String displayName, - /** Hosted MCP streamable HTTP endpoint. */ - @JsonProperty("url") String url, - /** Tools to include. Defaults to all tools when omitted. */ - @JsonProperty("tools") List tools, - /** Timeout in milliseconds for tool discovery and tool calls. */ - @JsonProperty("timeout") Long timeout, - /** Maximum dynamic-header cache lifetime in milliseconds. */ - @JsonProperty("headersRefreshTtlMs") Long headersRefreshTtlMs -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java index 8251c1f1fb..14a9118d0b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java @@ -32,8 +32,6 @@ public record McpServer( /** Plugin version that provided this server, when source is plugin. */ @JsonProperty("sourcePluginVersion") String sourcePluginVersion, /** Error message if the server failed to connect */ - @JsonProperty("error") String error, - /** Human-readable display name supplied by a managed server catalog. */ - @JsonProperty("displayName") String displayName + @JsonProperty("error") String error ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java index 72b1503d59..f709df96dd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, builtin, or managed + * Configuration source: user, workspace, plugin, or builtin * * @since 1.0.0 */ @@ -23,9 +23,7 @@ public enum McpServerSource { /** The {@code plugin} variant. */ PLUGIN("plugin"), /** The {@code builtin} variant. */ - BUILTIN("builtin"), - /** The {@code managed} variant. */ - MANAGED("managed"); + BUILTIN("builtin"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index 8421bb2666..34706a68e1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -160,8 +160,6 @@ public record SessionOpenOptions( /** Memory configuration for this session. */ @JsonProperty("memory") MemoryConfiguration memory, /** Capabilities enabled for this session. */ - @JsonProperty("sessionCapabilities") List sessionCapabilities, - /** Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed provenance is runtime-established from this separate field and credentials are supplied through dynamic-header refresh. */ - @JsonProperty("managedMcpServers") Map managedMcpServers + @JsonProperty("sessionCapabilities") List sessionCapabilities ) { } From 6039d986490c28047711e6ab0a2e5cf0d8136004 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Tue, 1 Sep 2026 18:54:34 +0100 Subject: [PATCH 03/16] Make managed MCP codegen reproducible Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1aa90135-4c64-4faa-8006-b5605338176a --- java/scripts/codegen/java.ts | 17 +- .../copilot/generated/McpServerSource.java | 6 +- .../generated/McpServersLoadedServer.java | 4 +- .../generated/rpc/ManagedMcpServerConfig.java | 36 +++ .../copilot/generated/rpc/McpServer.java | 4 +- .../generated/rpc/McpServerSource.java | 6 +- .../generated/rpc/SessionOpenOptions.java | 4 +- nodejs/test/shared-codegen.test.ts | 53 ++++ .../codegen/managed-mcp-schema-overlay.json | 243 ++++++++++++++++++ scripts/codegen/managedMcpSchemaOverlay.ts | 207 +++++++++++++++ scripts/codegen/rust.ts | 12 +- scripts/codegen/utils.ts | 6 +- 12 files changed, 585 insertions(+), 13 deletions(-) create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java create mode 100644 scripts/codegen/managed-mcp-schema-overlay.json create mode 100644 scripts/codegen/managedMcpSchemaOverlay.ts diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 785049afa1..565b7d7d99 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -12,6 +12,8 @@ import type { JSONSchema7 } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; +import { applyManagedMcpSchemaOverlay } from "../../../scripts/codegen/managedMcpSchemaOverlay.js"; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -786,7 +788,12 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { async function generateSessionEvents(schemaPath: string): Promise { console.log("\n📋 Generating session event classes..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent) as JSONSchema7); + const schema = normalizeSchemaBrandCasing( + applyManagedMcpSchemaOverlay( + JSON.parse(schemaContent) as JSONSchema7, + path.basename(schemaPath) + ) + ); // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; @@ -1380,7 +1387,9 @@ function generateRpcClass( async function generateRpcTypes(schemaPath: string): Promise { console.log("\n🔌 Generating RPC types..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent)) as Record & { + const schema = normalizeSchemaBrandCasing( + applyManagedMcpSchemaOverlay(JSON.parse(schemaContent), path.basename(schemaPath)) + ) as Record & { server?: Record; session?: Record; clientSession?: Record; @@ -2235,7 +2244,9 @@ async function generateRpcWrappers(schemaPath: string): Promise { console.log("\n🔧 Generating RPC wrapper classes..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent)) as { + const schema = normalizeSchemaBrandCasing( + applyManagedMcpSchemaOverlay(JSON.parse(schemaContent), path.basename(schemaPath)) + ) as { server?: Record; session?: Record; clientSession?: Record; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java index 63514743ab..5ea2c39f51 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, or builtin + * Configuration source: user, workspace, plugin, builtin, or managed * * @since 1.0.0 */ @@ -23,7 +23,9 @@ public enum McpServerSource { /** The {@code plugin} variant. */ PLUGIN("plugin"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code managed} variant. */ + MANAGED("managed"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java index c4567f30fa..bc840d90bd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java @@ -34,6 +34,8 @@ public record McpServersLoadedServer( /** Name of the plugin that supplied the effective MCP server config, only when source is plugin */ @JsonProperty("pluginName") String pluginName, /** Version of the plugin that supplied the effective MCP server config, only when source is plugin */ - @JsonProperty("pluginVersion") String pluginVersion + @JsonProperty("pluginVersion") String pluginVersion, + /** Human-readable display name supplied by a managed server catalog. */ + @JsonProperty("displayName") String displayName ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java new file mode 100644 index 0000000000..4a202063af --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedMcpServerConfig.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedMcpServerConfig( + /** Human-readable catalog display name. */ + @JsonProperty("displayName") String displayName, + /** Hosted MCP streamable HTTP endpoint. */ + @JsonProperty("url") String url, + /** Tools to include. Defaults to all tools when omitted. */ + @JsonProperty("tools") List tools, + /** Timeout in milliseconds for tool discovery and tool calls. */ + @JsonProperty("timeout") Long timeout, + /** Maximum dynamic-header cache lifetime in milliseconds. */ + @JsonProperty("headersRefreshTtlMs") Long headersRefreshTtlMs +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java index 14a9118d0b..8251c1f1fb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java @@ -32,6 +32,8 @@ public record McpServer( /** Plugin version that provided this server, when source is plugin. */ @JsonProperty("sourcePluginVersion") String sourcePluginVersion, /** Error message if the server failed to connect */ - @JsonProperty("error") String error + @JsonProperty("error") String error, + /** Human-readable display name supplied by a managed server catalog. */ + @JsonProperty("displayName") String displayName ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java index f709df96dd..72b1503d59 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Configuration source: user, workspace, plugin, or builtin + * Configuration source: user, workspace, plugin, builtin, or managed * * @since 1.0.0 */ @@ -23,7 +23,9 @@ public enum McpServerSource { /** The {@code plugin} variant. */ PLUGIN("plugin"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code managed} variant. */ + MANAGED("managed"); private final String value; McpServerSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index 34706a68e1..8421bb2666 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -160,6 +160,8 @@ public record SessionOpenOptions( /** Memory configuration for this session. */ @JsonProperty("memory") MemoryConfiguration memory, /** Capabilities enabled for this session. */ - @JsonProperty("sessionCapabilities") List sessionCapabilities + @JsonProperty("sessionCapabilities") List sessionCapabilities, + /** Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed provenance is runtime-established from this separate field and credentials are supplied through dynamic-header refresh. */ + @JsonProperty("managedMcpServers") Map managedMcpServers ) { } diff --git a/nodejs/test/shared-codegen.test.ts b/nodejs/test/shared-codegen.test.ts index 54f9d39e94..17d9b91cbe 100644 --- a/nodejs/test/shared-codegen.test.ts +++ b/nodejs/test/shared-codegen.test.ts @@ -1,3 +1,4 @@ +import { readFile } from "node:fs/promises"; import type { JSONSchema7 } from "json-schema"; import { describe, expect, it } from "vitest"; @@ -6,11 +7,13 @@ import { collectExperimentalOnlyRpcReferencedDefinitionNames, collectReachableDefinitionNames, findSharedSchemaDefinitions, + getApiSchemaPath, getEnumValueDescriptions, inlineExternalSchemaDefinitions, isIntegerSchemaBoundedToInt32, rewriteSharedDefinitionReferences, } from "../../scripts/codegen/utils.ts"; +import { applyManagedMcpSchemaOverlay } from "../../scripts/codegen/managedMcpSchemaOverlay.ts"; describe("shared schema definition codegen utilities", () => { it("detects integer schemas bounded to the 32-bit signed range", () => { @@ -60,6 +63,56 @@ describe("shared schema definition codegen utilities", () => { ).toBe(false); }); + describe("managed MCP schema overlay", () => { + async function loadPinnedApiSchema(): Promise<{ + definitions: Record; + }> { + return JSON.parse(await readFile(await getApiSchemaPath(), "utf8")) as { + definitions: Record; + }; + } + + it("adds the runtime contract to the known pre-publication API shape", async () => { + const schema = await loadPinnedApiSchema(); + + applyManagedMcpSchemaOverlay(schema, "api.schema.json"); + + expect(schema.definitions).toHaveProperty("ManagedMcpServerConfig"); + expect( + (schema.definitions.SessionOpenOptions as JSONSchema7).properties + ).toHaveProperty("managedMcpServers"); + expect((schema.definitions.McpServer as JSONSchema7).properties).toHaveProperty( + "displayName" + ); + expect((schema.definitions.McpServerSource as JSONSchema7).enum).toContain("managed"); + expect( + (schema.definitions.McpHeadersHandlePendingHeadersRefreshRequest as JSONSchema7) + .anyOf + ).toHaveLength(3); + }); + + it("rejects an unknown upstream shape instead of masking it", async () => { + const schema = await loadPinnedApiSchema(); + const refresh = schema.definitions + .McpHeadersHandlePendingHeadersRefreshRequest as JSONSchema7; + const headersVariant = refresh.anyOf?.[0] as JSONSchema7; + headersVariant.required = [...(headersVariant.required ?? []), "futureField"]; + + expect(() => applyManagedMcpSchemaOverlay(schema, "api.schema.json")).toThrow( + "unknown upstream shape" + ); + }); + + it("rejects a missing legacy node instead of recreating it", async () => { + const schema = await loadPinnedApiSchema(); + delete schema.definitions.McpServerSource; + + expect(() => applyManagedMcpSchemaOverlay(schema, "api.schema.json")).toThrow( + "expected an upstream node" + ); + }); + }); + it("extracts non-empty enum value descriptions from schema extensions", () => { expect( getEnumValueDescriptions({ diff --git a/scripts/codegen/managed-mcp-schema-overlay.json b/scripts/codegen/managed-mcp-schema-overlay.json new file mode 100644 index 0000000000..db413e58ef --- /dev/null +++ b/scripts/codegen/managed-mcp-schema-overlay.json @@ -0,0 +1,243 @@ +{ + "api.schema.json": { + "ManagedMcpServerConfig": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "description": "Human-readable catalog display name." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Hosted MCP streamable HTTP endpoint." + }, + "tools": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tools to include. Defaults to all tools when omitted." + }, + "timeout": { + "type": "integer", + "exclusiveMinimum": 0, + "format": "duration", + "description": "Timeout in milliseconds for tool discovery and tool calls." + }, + "headersRefreshTtlMs": { + "type": "integer", + "minimum": 0, + "description": "Maximum dynamic-header cache lifetime in milliseconds." + } + }, + "required": [ + "displayName", + "url" + ], + "additionalProperties": false, + "description": "Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host.", + "title": "ManagedMcpServerConfig" + }, + "McpHeadersHandlePendingHeadersRefreshRequest": { + "anyOf": [ + { + "type": "object", + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers." + }, + "ttlMs": { + "type": "integer", + "minimum": 0, + "description": "Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value." + }, + "kind": { + "type": "string", + "const": "headers", + "description": "Headers-refresh response variant discriminator." + } + }, + "required": [ + "kind", + "headers" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "none", + "description": "Headers-refresh response variant discriminator." + } + }, + "required": [ + "kind" + ] + }, + { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Host credential broker failure, denial, or revocation reason." + }, + "kind": { + "type": "string", + "const": "error", + "description": "Headers-refresh response variant discriminator." + } + }, + "required": [ + "kind", + "message" + ] + } + ], + "description": "Host response: supply dynamic headers or decline this refresh.", + "title": "McpHeadersHandlePendingHeadersRefreshRequest" + }, + "managedMcpServers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ManagedMcpServerConfig", + "description": "Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host." + }, + "description": "Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed provenance is runtime-established from this separate field and credentials are supplied through dynamic-header refresh.", + "stability": "experimental" + }, + "McpServerSource": { + "type": "string", + "enum": [ + "user", + "workspace", + "plugin", + "builtin", + "managed" + ], + "description": "Configuration source: user, workspace, plugin, builtin, or managed", + "title": "McpServerSource", + "x-enumDescriptions": { + "user": "Server configured in the user's global MCP configuration.", + "workspace": "Server configured by the current workspace.", + "plugin": "Server contributed by an installed plugin.", + "builtin": "Server bundled with the runtime.", + "managed": "Server supplied by a trusted host-managed catalog." + } + }, + "mcpServerDisplayName": { + "type": "string", + "description": "Human-readable display name supplied by a managed server catalog." + } + }, + "session-events.schema.json": { + "McpServerSource": { + "type": "string", + "enum": [ + "user", + "workspace", + "plugin", + "builtin", + "managed" + ], + "description": "Configuration source: user, workspace, plugin, builtin, or managed", + "title": "McpServerSource", + "x-enumDescriptions": { + "user": "Server configured in the user's global MCP configuration.", + "workspace": "Server configured by the current workspace.", + "plugin": "Server contributed by an installed plugin.", + "builtin": "Server bundled with the runtime.", + "managed": "Server supplied by a trusted host-managed catalog." + } + }, + "mcpServersLoadedDisplayName": { + "type": "string", + "description": "Human-readable display name supplied by a managed server catalog." + } + }, + "legacy": { + "api.schema.json": { + "McpHeadersHandlePendingHeadersRefreshRequest": { + "anyOf": [ + { + "type": "object", + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers." + }, + "kind": { + "type": "string", + "const": "headers", + "description": "Headers-refresh response variant discriminator." + } + }, + "required": [ + "kind", + "headers" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "none", + "description": "Headers-refresh response variant discriminator." + } + }, + "required": [ + "kind" + ] + } + ], + "description": "Host response: supply dynamic headers or decline this refresh.", + "title": "McpHeadersHandlePendingHeadersRefreshRequest" + }, + "McpServerSource": { + "type": "string", + "enum": [ + "user", + "workspace", + "plugin", + "builtin" + ], + "description": "Configuration source: user, workspace, plugin, or builtin", + "title": "McpServerSource", + "x-enumDescriptions": { + "user": "Server configured in the user's global MCP configuration.", + "workspace": "Server configured by the current workspace.", + "plugin": "Server contributed by an installed plugin.", + "builtin": "Server bundled with the runtime." + } + } + }, + "session-events.schema.json": { + "McpServerSource": { + "type": "string", + "enum": [ + "user", + "workspace", + "plugin", + "builtin" + ], + "description": "Configuration source: user, workspace, plugin, or builtin", + "title": "McpServerSource", + "x-enumDescriptions": { + "user": "Server configured in the user's global MCP configuration.", + "workspace": "Server configured by the current workspace.", + "plugin": "Server contributed by an installed plugin.", + "builtin": "Server bundled with the runtime." + } + } + } + } +} diff --git a/scripts/codegen/managedMcpSchemaOverlay.ts b/scripts/codegen/managedMcpSchemaOverlay.ts new file mode 100644 index 0000000000..8b68861938 --- /dev/null +++ b/scripts/codegen/managedMcpSchemaOverlay.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import fs from "fs"; + +type SchemaNode = Record; + +interface ManagedMcpApiOverlay { + ManagedMcpServerConfig: SchemaNode; + McpHeadersHandlePendingHeadersRefreshRequest: SchemaNode; + managedMcpServers: SchemaNode; + McpServerSource: SchemaNode; + mcpServerDisplayName: SchemaNode; +} + +interface ManagedMcpEventOverlay { + McpServerSource: SchemaNode; + mcpServersLoadedDisplayName: SchemaNode; +} + +interface ManagedMcpSchemaOverlay { + "api.schema.json": ManagedMcpApiOverlay; + "session-events.schema.json": ManagedMcpEventOverlay; + legacy: { + "api.schema.json": { + McpHeadersHandlePendingHeadersRefreshRequest: SchemaNode; + McpServerSource: SchemaNode; + }; + "session-events.schema.json": { + McpServerSource: SchemaNode; + }; + }; +} + +const overlay = JSON.parse( + fs.readFileSync( + new URL("./managed-mcp-schema-overlay.json", import.meta.url), + "utf8", + ), +) as ManagedMcpSchemaOverlay; + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (value !== null && typeof value === "object") { + return `{${Object.entries(value as SchemaNode) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function installNode( + parent: SchemaNode, + key: string, + desired: SchemaNode, + isKnownLegacy: (current: unknown) => boolean, + allowMissing: boolean, +): void { + const current = parent[key]; + if (current === undefined) { + if (!allowMissing) { + throw new Error( + `Managed MCP schema overlay expected an upstream node at ${key}. ` + + "Update or remove scripts/codegen/managed-mcp-schema-overlay.json.", + ); + } + parent[key] = clone(desired); + return; + } + if (stableStringify(current) === stableStringify(desired)) { + parent[key] = clone(desired); + return; + } + if (!isKnownLegacy(current)) { + throw new Error( + `Managed MCP schema overlay conflicts with an unknown upstream shape at ${key}. ` + + "Update or remove scripts/codegen/managed-mcp-schema-overlay.json.", + ); + } + parent[key] = clone(desired); +} + +function matches(expected: unknown): (current: unknown) => boolean { + return (current) => stableStringify(current) === stableStringify(expected); +} + +function requireDefinitions(schema: SchemaNode): SchemaNode { + const definitions = schema.definitions; + if ( + definitions === null || + typeof definitions !== "object" || + Array.isArray(definitions) + ) { + throw new Error( + "Managed MCP schema overlay requires a definitions object.", + ); + } + return definitions as SchemaNode; +} + +function requireProperties( + definitions: SchemaNode, + definitionName: string, +): SchemaNode { + const definition = definitions[definitionName] as SchemaNode | undefined; + const properties = definition?.properties; + if ( + properties === null || + typeof properties !== "object" || + Array.isArray(properties) + ) { + throw new Error( + `Managed MCP schema overlay requires ${definitionName}.properties to be an object.`, + ); + } + return properties as SchemaNode; +} + +function applyApiOverlay(schema: SchemaNode): void { + const definitions = requireDefinitions(schema); + const api = overlay["api.schema.json"]; + const legacy = overlay.legacy["api.schema.json"]; + installNode( + definitions, + "ManagedMcpServerConfig", + api.ManagedMcpServerConfig, + () => false, + true, + ); + installNode( + definitions, + "McpHeadersHandlePendingHeadersRefreshRequest", + api.McpHeadersHandlePendingHeadersRefreshRequest, + matches(legacy.McpHeadersHandlePendingHeadersRefreshRequest), + false, + ); + installNode( + definitions, + "McpServerSource", + api.McpServerSource, + matches(legacy.McpServerSource), + false, + ); + installNode( + requireProperties(definitions, "SessionOpenOptions"), + "managedMcpServers", + api.managedMcpServers, + () => false, + true, + ); + installNode( + requireProperties(definitions, "McpServer"), + "displayName", + api.mcpServerDisplayName, + () => false, + true, + ); +} + +function applySessionEventsOverlay(schema: SchemaNode): void { + const definitions = requireDefinitions(schema); + const events = overlay["session-events.schema.json"]; + const legacy = overlay.legacy["session-events.schema.json"]; + installNode( + definitions, + "McpServerSource", + events.McpServerSource, + matches(legacy.McpServerSource), + false, + ); + installNode( + requireProperties(definitions, "McpServersLoadedServer"), + "displayName", + events.mcpServersLoadedDisplayName, + () => false, + true, + ); +} + +/** + * Applies the additive managed MCP contract from copilot-agent-runtime#17210. + * + * Remove this overlay after the pinned @github/copilot package publishes the + * same schema. Unknown upstream shapes fail instead of being silently replaced. + */ +export function applyManagedMcpSchemaOverlay( + schema: T, + fileName: string, +): T { + if (schema === null || typeof schema !== "object" || Array.isArray(schema)) { + throw new Error(`Managed MCP schema overlay cannot process ${fileName}.`); + } + if (fileName === "api.schema.json") { + applyApiOverlay(schema as SchemaNode); + } else if (fileName === "session-events.schema.json") { + applySessionEventsOverlay(schema as SchemaNode); + } + return schema; +} diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 0feec5e98a..63012b5834 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -16,6 +16,8 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; + +import { applyManagedMcpSchemaOverlay } from "./managedMcpSchemaOverlay.js"; import { addManagedApprovalRequiredToPermissionRequests, type ApiSchema, @@ -2220,10 +2222,16 @@ async function generate(): Promise { const apiSchemaPath = await getApiSchemaPath(schemaArgs.apiSchemaPath); const sessionEventsRaw = normalizeSchemaBrandCasing( - JSON.parse(await fs.readFile(sessionEventsSchemaPath, "utf-8")), + applyManagedMcpSchemaOverlay( + JSON.parse(await fs.readFile(sessionEventsSchemaPath, "utf-8")), + path.basename(sessionEventsSchemaPath), + ), ); const apiRaw = normalizeSchemaBrandCasing( - JSON.parse(await fs.readFile(apiSchemaPath, "utf-8")) as ApiSchema, + applyManagedMcpSchemaOverlay( + JSON.parse(await fs.readFile(apiSchemaPath, "utf-8")) as ApiSchema, + path.basename(apiSchemaPath), + ), ); const sessionEventsSchema = propagateInternalVisibility( diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 590213d72b..9a7454ad86 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -13,6 +13,8 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; +import { applyManagedMcpSchemaOverlay } from "./managedMcpSchemaOverlay.js"; + export const execFileAsync = promisify(execFile); const __filename = fileURLToPath(import.meta.url); @@ -185,7 +187,9 @@ function renameBrandDefinitionKeys(defs: Record): void { /** Load a JSON schema file and normalize GitHub brand casing in titles, refs, and definition keys. */ export async function loadSchemaJson(filePath: string): Promise { const parsed = JSON.parse(await fs.readFile(filePath, "utf-8")) as T; - return normalizeSchemaBrandCasing(parsed); + return normalizeSchemaBrandCasing( + applyManagedMcpSchemaOverlay(parsed, path.basename(filePath)) + ); } // ── Schema processing ─────────────────────────────────────────────────────── From 5d2ea30b3b2ba6b7f2db5e16f386d6c14ceb98b0 Mon Sep 17 00:00:00 2001 From: Alberto Gimeno Date: Tue, 1 Sep 2026 14:29:01 +0200 Subject: [PATCH 04/16] Notify hooks after response delivery Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/src/hooks.rs | 30 +++++++++++++++++++++++++++++- rust/src/session.rs | 18 ++++++++++++++++-- rust/tests/session_test.rs | 28 ++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs index 4986d6cb18..05481f1aea 100644 --- a/rust/src/hooks.rs +++ b/rust/src/hooks.rs @@ -19,6 +19,19 @@ use crate::types::SessionId; pub struct HookContext { /// The session this hook was triggered in. pub session_id: SessionId, + /// JSON-RPC request ID for this hook invocation. + pub request_id: u64, +} + +/// Hook response that was successfully written back to the CLI. +#[derive(Debug, Clone)] +pub struct HookResponseSent { + /// The session this hook was triggered in. + pub session_id: SessionId, + /// JSON-RPC request ID for this hook invocation. + pub request_id: u64, + /// Runtime hook type, such as `userPromptSubmitted` or `preToolUse`. + pub hook_type: String, } /// Input for the `preToolUse` hook — received before a tool executes. @@ -570,6 +583,9 @@ pub trait SessionHooks: Send + Sync + 'static { } } + /// Called after a hook response is successfully written back to the CLI. + async fn on_hook_response_sent(&self, _response: HookResponseSent) {} + /// Called before a tool executes. Return `Some(output)` to approve/deny /// or modify the call, or `None` (default) to pass through unchanged. async fn on_pre_tool_use( @@ -680,14 +696,26 @@ pub trait SessionHooks: Send + Sync + 'static { /// Returns `Ok(Value)` shaped like `{ "output": ... }` on success. /// If no hook is registered ([`HookOutput::None`]), the output is an empty /// object: `{ "output": {} }`. -pub(crate) async fn dispatch_hook( +#[cfg(test)] +async fn dispatch_hook( + hooks: &dyn SessionHooks, + session_id: &SessionId, + hook_type: &str, + raw_input: Value, +) -> Result { + dispatch_hook_for_request(hooks, session_id, 0, hook_type, raw_input).await +} + +pub(crate) async fn dispatch_hook_for_request( hooks: &dyn SessionHooks, session_id: &SessionId, + request_id: u64, hook_type: &str, raw_input: Value, ) -> Result { let ctx = HookContext { session_id: session_id.clone(), + request_id, }; let event = match hook_type { diff --git a/rust/src/session.rs b/rust/src/session.rs index cc383a29a9..ee25a3078b 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -2543,7 +2543,11 @@ async fn handle_request( .unwrap_or(Value::Object(Default::default())); let rpc_result = if let Some(hooks) = hooks { - match crate::hooks::dispatch_hook(hooks, &sid, hook_type, input).await { + match crate::hooks::dispatch_hook_for_request( + hooks, &sid, request.id, hook_type, input, + ) + .await + { Ok(output) => output, Err(e) => { warn!(error = %e, hook_type = hook_type, "hook dispatch failed"); @@ -2560,7 +2564,17 @@ async fn handle_request( result: Some(rpc_result), error: None, }; - let _ = client.send_response(&rpc_response).await; + if client.send_response(&rpc_response).await.is_ok() + && let Some(hooks) = hooks + { + hooks + .on_hook_response_sent(crate::hooks::HookResponseSent { + session_id: sid, + request_id: request.id, + hook_type: hook_type.to_string(), + }) + .await; + } } "userInput.request" => { diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 6957237744..b8090ee267 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -4750,14 +4750,19 @@ async fn create_session_pair_with_hooks( #[tokio::test] async fn hooks_invoke_dispatches_to_session_hooks() { - use github_copilot_sdk::hooks::{HookEvent, HookOutput, PreToolUseOutput, SessionHooks}; + use github_copilot_sdk::hooks::{ + HookEvent, HookOutput, HookResponseSent, PreToolUseOutput, SessionHooks, + }; - struct PolicyHooks; + struct PolicyHooks { + response_sent: tokio::sync::mpsc::UnboundedSender, + } #[async_trait] impl SessionHooks for PolicyHooks { async fn on_hook(&self, event: HookEvent) -> HookOutput { match event { - HookEvent::PreToolUse { input, .. } => { + HookEvent::PreToolUse { input, ctx } => { + assert_eq!(ctx.request_id, 300); if input.tool_name == "rm" { HookOutput::PreToolUse(PreToolUseOutput { permission_decision: Some("deny".to_string()), @@ -4771,9 +4776,18 @@ async fn hooks_invoke_dispatches_to_session_hooks() { _ => HookOutput::None, } } + + async fn on_hook_response_sent(&self, response: HookResponseSent) { + self.response_sent.send(response).unwrap(); + } } - let (_session, mut server) = create_session_pair_with_hooks(Arc::new(PolicyHooks)).await; + let (response_sent_tx, mut response_sent_rx) = + tokio::sync::mpsc::unbounded_channel::(); + let (_session, mut server) = create_session_pair_with_hooks(Arc::new(PolicyHooks { + response_sent: response_sent_tx, + })) + .await; // Send a hooks.invoke request for a denied tool server @@ -4801,6 +4815,12 @@ async fn hooks_invoke_dispatches_to_session_hooks() { response["result"]["output"]["permissionDecisionReason"], "destructive" ); + let response_sent = timeout(TIMEOUT, response_sent_rx.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(response_sent.request_id, 300); + assert_eq!(response_sent.hook_type, "preToolUse"); } #[tokio::test] From f864677c1faea171bafd3005cc4313f29c845b21 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Wed, 2 Sep 2026 15:32:48 +0100 Subject: [PATCH 05/16] Make managed MCP startup vendoring-safe Start session dispatch before local session.create completes so managed MCP startup header requests cannot deadlock creation. Ship the in-process CLI integrity snapshot for vendored consumers and keep no-bundle builds warning-free. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/.gitignore | 1 - rust/cli-version-in-process.txt | 11 +++ rust/src/resolve.rs | 1 + rust/src/session.rs | 128 +++++++++++++++++------------- rust/tests/cli_resolution_test.rs | 8 +- rust/tests/session_test.rs | 47 +++++++++++ 6 files changed, 138 insertions(+), 58 deletions(-) create mode 100644 rust/cli-version-in-process.txt diff --git a/rust/.gitignore b/rust/.gitignore index c149fa3946..c4095ffc0f 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,4 +1,3 @@ /target Cargo.lock.bak cli-version.txt -cli-version-in-process.txt diff --git a/rust/cli-version-in-process.txt b/rust/cli-version-in-process.txt new file mode 100644 index 0000000000..2da7c3210c --- /dev/null +++ b/rust/cli-version-in-process.txt @@ -0,0 +1,11 @@ +# Auto-generated by rust/scripts/snapshot-bundled-in-process-version.sh +# Do not edit. Regenerated by the publish workflow on every release. +version=1.0.83-1 +copilot-darwin-arm64=sha512-gxYJEd4yoIBIQHRmJw6ZegSkR6VuNm7AJVdAdARcU0SS68VZjtZFT29luThAp75Pxp37S5w+3A/jQNtxGyHizw== +copilot-darwin-x64=sha512-PL63+Ib1RH+Bpu2UYJ9E8tmD4NZD/YlhD25F1Q9Rb86AdgLEoRYlxjNxXXF9P9dMmJ/u5zm0rUtWIRxv/FRbPg== +copilot-linux-arm64=sha512-7c4RVEuXQu9zepAWoXwkizpESztUbWUR1hLPl+o+19L+D8fXm+5XQRH5E22Ia7P7/xHW3Ml2o2x5cXFq0k7/7w== +copilot-linux-x64=sha512-Be0zUVysLYV66pnOPhMxpShQ8Ox5V07qcsSOoNzG7iwpzkfZuf70U9CHk/fnI//mWwmZR+caPUfVM9BpwrdNnA== +copilot-linuxmusl-arm64=sha512-PZNP8vZZJU8yIc3dNjGUBUCFPNjqZs8FPZLDxvL6XdHM7Ep4cLBa+QdbRsw+nZONIaG0GtQf26eqk5ymYYpUPA== +copilot-linuxmusl-x64=sha512-uKMhGtg5K07nA5/pxDSq8S5F/I+sMgVhUXg90Y4BI9trOidgxpXpCVyh4TeRNj2ENNugPvATdfGc6eFPsjmlgQ== +copilot-win32-arm64=sha512-vMxHTmv3SotBuNI4LKcIFckd+n0OvhTik27qjsqOnVqFpvmHDghSsa8YPxySyxUQIE35z5pJaivVBK3dfEAY7w== +copilot-win32-x64=sha512-2l0VVTazW3/+p94ZQ5sAPCj1iH7KWxJAYh4yXWfvpsB/1JEEmyVgpLei1MeaMFVdtp/hi9bGprZ9+zWNlHh5hQ== diff --git a/rust/src/resolve.rs b/rust/src/resolve.rs index d8b996a11a..5fcc11b0c4 100644 --- a/rust/src/resolve.rs +++ b/rust/src/resolve.rs @@ -147,6 +147,7 @@ fn extracted_program(_use_runtime_wrapper: bool) -> Option { None } +#[cfg(any(feature = "bundled-cli", has_extracted_cli, test))] fn validate_runtime_pair(wrapper: &Path) -> Result<(), Error> { let wrapper_valid = wrapper .metadata() diff --git a/rust/src/session.rs b/rust/src/session.rs index ee25a3078b..cb0223fec6 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -113,6 +113,9 @@ struct PendingSessionRegistration { disarmed: bool, } +type CreateSessionRegistrationStash = + Arc)>>>; + impl PendingSessionRegistration { fn new(client: Client, session_id: SessionId, shutdown: CancellationToken) -> Self { Self { @@ -982,15 +985,37 @@ impl Client { // the session synchronously the instant the response arrives. // For non-cloud sessions, register up-front so the CLI can issue // session-scoped requests during session.create processing. - let inline_stash: Arc< - ParkingLotMutex>, - > = Arc::new(ParkingLotMutex::new(None)); + let inline_stash: CreateSessionRegistrationStash = Arc::new(ParkingLotMutex::new(None)); + let mut event_loop = None; + let mut registration = None; let inline_callback: Option = if let Some(ref sid) = local_session_id { let channels = self.register_session(sid); - *inline_stash.lock() = Some((sid.clone(), channels)); + event_loop = Some(spawn_event_loop( + sid.clone(), + self.clone(), + handlers.clone(), + hooks.clone(), + transforms.clone(), + command_handlers.clone(), + canvas_handler.clone(), + session_fs_provider.clone(), + bearer_token_providers.clone(), + channels, + idle_waiter.clone(), + capabilities.clone(), + open_canvases.clone(), + event_tx.clone(), + shutdown.clone(), + )); + registration = Some(PendingSessionRegistration::new( + self.clone(), + sid.clone(), + shutdown.clone(), + )); + *inline_stash.lock() = Some((sid.clone(), None)); None } else { let client = self.clone(); @@ -1012,19 +1037,36 @@ impl Client { .into()); } let channels = client.register_session(&parsed.session_id); - *stash.lock() = Some((parsed.session_id, channels)); + *stash.lock() = Some((parsed.session_id, Some(channels))); Ok(()) })) }; let rpc_start = Instant::now(); - let result = match self - .call_with_inline_callback("session.create", Some(params), inline_callback) - .await - { - Ok(result) => result, + let create_result: Result = async { + let result = self + .call_with_inline_callback("session.create", Some(params), inline_callback) + .await?; + let create_result: CreateSessionResult = serde_json::from_value(result)?; + if let Some(ref requested) = local_session_id + && create_result.session_id != *requested + { + return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch { + requested: requested.clone(), + returned: create_result.session_id, + }) + .into()); + } + Ok(create_result) + } + .await; + let create_result = match create_result { + Ok(create_result) => create_result, Err(error) => { - if let Some((id, _channels)) = inline_stash.lock().take() { + let pending = (registration.take(), event_loop.take()); + if let (Some(registration), Some(event_loop)) = pending { + registration.cleanup(event_loop).await; + } else if let Some((id, _channels)) = inline_stash.lock().take() { self.unregister_session(&id); } return Err(error); @@ -1034,52 +1076,32 @@ impl Client { elapsed_ms = rpc_start.elapsed().as_millis(), "Client::create_session session creation request completed successfully" ); - let create_result: CreateSessionResult = match serde_json::from_value(result) { - Ok(result) => result, - Err(error) => { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } - return Err(error.into()); - } - }; - - if let Some(ref requested) = local_session_id - && create_result.session_id != *requested - { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } - return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch { - requested: requested.clone(), - returned: create_result.session_id.clone(), - }) - .into()); - } - let (session_id, channels) = inline_stash .lock() .take() .expect("session registration must have populated stash on success"); - let event_loop = spawn_event_loop( - session_id.clone(), - self.clone(), - handlers, - hooks, - transforms, - command_handlers, - canvas_handler, - session_fs_provider, - bearer_token_providers, - channels, - idle_waiter.clone(), - capabilities.clone(), - open_canvases.clone(), - event_tx.clone(), - shutdown.clone(), - ); - let mut registration = - PendingSessionRegistration::new(self.clone(), session_id.clone(), shutdown.clone()); + let event_loop = event_loop.unwrap_or_else(|| { + spawn_event_loop( + session_id.clone(), + self.clone(), + handlers, + hooks, + transforms, + command_handlers, + canvas_handler, + session_fs_provider, + bearer_token_providers, + channels.expect("server-generated session must retain its channels"), + idle_waiter.clone(), + capabilities.clone(), + open_canvases.clone(), + event_tx.clone(), + shutdown.clone(), + ) + }); + let mut registration = registration.unwrap_or_else(|| { + PendingSessionRegistration::new(self.clone(), session_id.clone(), shutdown.clone()) + }); tracing::debug!( elapsed_ms = setup_start.elapsed().as_millis(), session_id = %session_id, diff --git a/rust/tests/cli_resolution_test.rs b/rust/tests/cli_resolution_test.rs index 75773a0c0d..711e79caf6 100644 --- a/rust/tests/cli_resolution_test.rs +++ b/rust/tests/cli_resolution_test.rs @@ -205,10 +205,10 @@ async fn extract_dir_runtime_override_is_honored() { #[test] fn pin_file_when_present_is_well_formed() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let (filename, value_prefix) = if cfg!(feature = "bundled-in-process") { - ("cli-version-in-process.txt", Some("sha512-")) + let (filename, value_prefix, expected_package_count) = if cfg!(feature = "bundled-in-process") { + ("cli-version-in-process.txt", Some("sha512-"), 8) } else { - ("cli-version.txt", None) + ("cli-version.txt", None, 6) }; let pin = PathBuf::from(manifest_dir).join(filename); if !pin.is_file() { @@ -250,7 +250,7 @@ fn pin_file_when_present_is_well_formed() { } } assert!(saw_version, "{filename} missing `version=` line"); - assert_eq!(package_count, 6); + assert_eq!(package_count, expected_package_count); } /// With `bundled-cli` on AND a supported target, `install_bundled_cli` diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index b8090ee267..9555d25d6c 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -980,6 +980,53 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { let create_request = server.read_request().await; assert_eq!(create_request["method"], "session.create"); + server + .send_event( + "mcp.headers_refresh_required", + serde_json::json!({ + "requestId": "headers-before-create", + "serverName": "ttl-absent", + "serverUrl": "https://ttl-absent.example.test/mcp", + "reason": "startup" + }), + ) + .await; + let response_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!( + response_request["method"], + "session.mcp.headers.handlePendingHeadersRefreshRequest" + ); + assert_eq!( + response_request["params"]["sessionId"], + "managed-mcp-session" + ); + assert_eq!( + response_request["params"]["requestId"], + "headers-before-create" + ); + assert_eq!(response_request["params"]["result"]["kind"], "headers"); + assert!( + response_request["params"]["result"]["headers"]["Authorization"] + .as_str() + .is_some() + ); + let (received_session_id, received_request_id, received_request) = + timeout(TIMEOUT, calls_rx.recv()).await.unwrap().unwrap(); + assert_eq!(received_session_id.as_str(), "managed-mcp-session"); + assert_eq!(received_request_id, "headers-before-create"); + assert_eq!(received_request.server_name, "ttl-absent"); + assert_eq!( + received_request.server_url, + "https://ttl-absent.example.test/mcp" + ); + assert_eq!( + received_request.reason, + McpHeadersRefreshRequiredReason::Startup + ); + server + .respond(&response_request, serde_json::json!({ "success": true })) + .await; + server .respond( &create_request, From a0793ab6e69d399ed958c103da81bd1ee987b8d7 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Wed, 2 Sep 2026 23:48:02 +0100 Subject: [PATCH 06/16] Fix managed MCP required checks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1aa90135-4c64-4faa-8006-b5605338176a --- python/copilot/session.py | 2 +- rust/src/types.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/copilot/session.py b/python/copilot/session.py index 1f82b541b9..cb64565d41 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -2448,7 +2448,7 @@ async def _execute_mcp_headers_refresh_and_respond( ) ) except (JsonRpcError, ProcessExitedError, OSError): - pass + pass # The runtime connection is gone, so the pending request cannot be answered. async def _execute_command_and_respond( self, diff --git a/rust/src/types.rs b/rust/src/types.rs index 95b423e923..8a7a313e24 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -2001,7 +2001,7 @@ pub struct SessionConfig { /// Non-secret host-managed HTTP MCP servers keyed by stable managed identity. /// /// Credentials are supplied dynamically through - /// [`McpHeadersRefreshHandler`](crate::handler::McpHeadersRefreshHandler). + /// [`McpHeadersRefreshHandler`]. pub managed_mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// From ce609cbf8281bd9e8883b1980763c4e83ebb5ba0 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Tue, 15 Sep 2026 13:41:19 +0100 Subject: [PATCH 07/16] Reject partial MCP schemas and recover header callback panics Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- go/session.go | 9 +- go/session_test.go | 97 +++++++++++++ go/types.go | 1 + nodejs/test/shared-codegen.test.ts | 134 +++++++++++++++++- scripts/codegen/managedMcpSchemaOverlay.ts | 151 ++++++++++----------- 5 files changed, 313 insertions(+), 79 deletions(-) diff --git a/go/session.go b/go/session.go index 872ef97a1e..27f6ad142f 100644 --- a/go/session.go +++ b/go/session.go @@ -1811,7 +1811,14 @@ func (s *Session) executeMCPHeadersRefreshAndRespond( handler MCPHeadersRefreshHandler, ) { var wireResult rpc.MCPHeadersHandlePendingHeadersRefreshRequest - result, err := handler(request, MCPHeadersRefreshInvocation{SessionID: s.SessionID}) + result, err := func() (result *MCPHeadersRefreshResult, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("MCP headers refresh handler panic: %v", r) + } + }() + return handler(request, MCPHeadersRefreshInvocation{SessionID: s.SessionID}) + }() switch { case err != nil: log.Printf( diff --git a/go/session_test.go b/go/session_test.go index b41b5bd287..416927c59f 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -459,6 +459,103 @@ func TestSession_MCPHeadersRefreshResponses(t *testing.T) { } } +func TestSession_MCPHeadersRefreshPanicDoesNotStopDispatch(t *testing.T) { + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + defer stdinR.Close() + defer stdinW.Close() + defer stdoutR.Close() + defer stdoutW.Close() + + client := jsonrpc2.NewClient(stdinW, stdoutR) + client.Start() + defer client.Stop() + + paramsCh := make(chan map[string]any, 2) + errCh := make(chan error, 1) + go func() { + for range 2 { + frame, err := readTestJSONRPCFrame(stdinR) + if err != nil { + errCh <- err + return + } + var request struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` + } + if err := json.Unmarshal(frame, &request); err != nil { + errCh <- err + return + } + if request.Method != "session.mcp.headers.handlePendingHeadersRefreshRequest" { + errCh <- fmt.Errorf("unexpected method %s", request.Method) + return + } + response, err := json.Marshal(map[string]any{ + "jsonrpc": "2.0", + "id": request.ID, + "result": map[string]any{"success": true}, + }) + if err != nil { + errCh <- err + return + } + if _, err := fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(response), response); err != nil { + errCh <- err + return + } + paramsCh <- request.Params + } + }() + + session, cleanup := newTestSession() + defer cleanup() + session.SessionID = "session-panic" + session.client = client + session.RPC = rpc.NewSessionRPC(client, session.SessionID) + var calls atomic.Int32 + session.registerMCPHeadersRefreshHandler( + func(MCPHeadersRefreshRequest, MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { + if calls.Add(1) == 1 { + panic("credential broker crashed") + } + return &MCPHeadersRefreshResult{Headers: map[string]string{"Authorization": "refreshed"}}, nil + }, + ) + + for i, expected := range []map[string]any{ + {"kind": "error", "message": "MCP headers refresh handler panic: credential broker crashed"}, + {"kind": "headers", "headers": map[string]any{"Authorization": "refreshed"}}, + } { + requestID := fmt.Sprintf("refresh-%d", i) + session.dispatchEvent(SessionEvent{Data: &MCPHeadersRefreshRequiredData{ + RequestID: requestID, + ServerName: "GitHub", + ServerURL: "https://example.com/mcp", + Reason: MCPHeadersRefreshRequiredReasonStartup, + }}) + + select { + case params := <-paramsCh: + if params["sessionId"] != session.SessionID || params["requestId"] != requestID { + t.Fatalf("unexpected response correlation: %#v", params) + } + if !mapsEqual(params["result"], expected) { + t.Fatalf("unexpected result: %#v", params["result"]) + } + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for MCP headers refresh response") + } + } + if got := calls.Load(); got != 2 { + t.Fatalf("expected two callback invocations, got %d", got) + } +} + func mapsEqual(actual any, expected map[string]any) bool { actualJSON, err := json.Marshal(actual) if err != nil { diff --git a/go/types.go b/go/types.go index 4860afa816..5b24f70315 100644 --- a/go/types.go +++ b/go/types.go @@ -583,6 +583,7 @@ type MCPHeadersRefreshInvocation struct { // MCPHeadersRefreshHandler supplies short-lived HTTP headers for managed MCP // servers. Returning an error sends an explicit broker error to the runtime. +// Handler panics are recovered and also sent as explicit broker errors. type MCPHeadersRefreshHandler func(request MCPHeadersRefreshRequest, invocation MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) // UserInputRequest represents a request for user input from the agent diff --git a/nodejs/test/shared-codegen.test.ts b/nodejs/test/shared-codegen.test.ts index 17d9b91cbe..015358e431 100644 --- a/nodejs/test/shared-codegen.test.ts +++ b/nodejs/test/shared-codegen.test.ts @@ -9,6 +9,7 @@ import { findSharedSchemaDefinitions, getApiSchemaPath, getEnumValueDescriptions, + getSessionEventsSchemaPath, inlineExternalSchemaDefinitions, isIntegerSchemaBoundedToInt32, rewriteSharedDefinitionReferences, @@ -72,7 +73,7 @@ describe("shared schema definition codegen utilities", () => { }; } - it("adds the runtime contract to the known pre-publication API shape", async () => { + it("accepts the pinned API schema", async () => { const schema = await loadPinnedApiSchema(); applyManagedMcpSchemaOverlay(schema, "api.schema.json"); @@ -91,16 +92,31 @@ describe("shared schema definition codegen utilities", () => { ).toHaveLength(3); }); + it("accepts the pinned session-event schema", async () => { + const schema = JSON.parse( + await readFile(await getSessionEventsSchemaPath(), "utf8") + ) as { definitions: Record }; + + applyManagedMcpSchemaOverlay(schema, "session-events.schema.json"); + + expect(schema.definitions.McpServerSource.enum).toContain("managed"); + expect(schema.definitions.McpServersLoadedServer.properties).toHaveProperty( + "displayName" + ); + }); + it("rejects an unknown upstream shape instead of masking it", async () => { const schema = await loadPinnedApiSchema(); const refresh = schema.definitions .McpHeadersHandlePendingHeadersRefreshRequest as JSONSchema7; const headersVariant = refresh.anyOf?.[0] as JSONSchema7; headersVariant.required = [...(headersVariant.required ?? []), "futureField"]; + const before = structuredClone(schema); expect(() => applyManagedMcpSchemaOverlay(schema, "api.schema.json")).toThrow( "unknown upstream shape" ); + expect(schema).toEqual(before); }); it("rejects a missing legacy node instead of recreating it", async () => { @@ -111,6 +127,122 @@ describe("shared schema definition codegen utilities", () => { "expected an upstream node" ); }); + + const contracts = [ + { + fileName: "api.schema.json", + nodes: [ + { definition: "ManagedMcpServerConfig", overlayKey: "ManagedMcpServerConfig" }, + { + definition: "McpHeadersHandlePendingHeadersRefreshRequest", + overlayKey: "McpHeadersHandlePendingHeadersRefreshRequest", + }, + { definition: "McpServerSource", overlayKey: "McpServerSource" }, + { + definition: "SessionOpenOptions", + property: "managedMcpServers", + overlayKey: "managedMcpServers", + }, + { + definition: "McpServer", + property: "displayName", + overlayKey: "mcpServerDisplayName", + }, + ], + }, + { + fileName: "session-events.schema.json", + nodes: [ + { definition: "McpServerSource", overlayKey: "McpServerSource" }, + { + definition: "McpServersLoadedServer", + property: "displayName", + overlayKey: "mcpServersLoadedDisplayName", + }, + ], + }, + ]; + + for (const { fileName, nodes } of contracts) { + async function fixture(targetMask: number) { + const overlay = JSON.parse( + await readFile( + new URL( + "../../scripts/codegen/managed-mcp-schema-overlay.json", + import.meta.url + ), + "utf8" + ) + ) as Record> & { + legacy: Record>; + }; + const schema: { definitions: Record } = { + definitions: { Unrelated: { type: "string" } }, + }; + for (const [index, { definition, property, overlayKey }] of nodes.entries()) { + const parent = property + ? (schema.definitions[definition] = { properties: {} }).properties! + : schema.definitions; + const value = + targetMask & (1 << index) + ? overlay[fileName][overlayKey] + : overlay.legacy[fileName][overlayKey]; + if (value !== undefined) { + parent[property ?? definition] = value; + } + } + return schema; + } + + const targetMask = (1 << nodes.length) - 1; + + it(`upgrades the complete legacy ${fileName} contract and is idempotent`, async () => { + const schema = await fixture(0); + const target = await fixture(targetMask); + + expect(applyManagedMcpSchemaOverlay(schema, fileName)).toEqual(target); + expect(applyManagedMcpSchemaOverlay(schema, fileName)).toEqual(target); + }); + + it(`accepts the complete upstream ${fileName} target without mutation`, async () => { + const schema = await fixture(targetMask); + const before = structuredClone(schema); + + expect(applyManagedMcpSchemaOverlay(schema, fileName)).toBe(schema); + expect(schema).toEqual(before); + }); + + it.each(Array.from({ length: targetMask - 1 }, (_, index) => index + 1))( + `rejects partial ${fileName} contract %i without mutation`, + async (mask) => { + const schema = await fixture(mask); + const before = structuredClone(schema); + + expect(() => applyManagedMcpSchemaOverlay(schema, fileName)).toThrow( + "unknown upstream shape or partial contract" + ); + expect(schema).toEqual(before); + } + ); + + it.each(nodes)( + `rejects missing ${fileName} target node $overlayKey without mutation`, + async ({ definition, property }) => { + const schema = await fixture(targetMask); + if (property) { + delete schema.definitions[definition].properties![property]; + } else { + delete schema.definitions[definition]; + } + const before = structuredClone(schema); + + expect(() => applyManagedMcpSchemaOverlay(schema, fileName)).toThrow( + "Managed MCP schema overlay" + ); + expect(schema).toEqual(before); + } + ); + } }); it("extracts non-empty enum value descriptions from schema extensions", () => { diff --git a/scripts/codegen/managedMcpSchemaOverlay.ts b/scripts/codegen/managedMcpSchemaOverlay.ts index 8b68861938..cae15e33c8 100644 --- a/scripts/codegen/managedMcpSchemaOverlay.ts +++ b/scripts/codegen/managedMcpSchemaOverlay.ts @@ -57,39 +57,42 @@ function clone(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } -function installNode( - parent: SchemaNode, - key: string, - desired: SchemaNode, - isKnownLegacy: (current: unknown) => boolean, - allowMissing: boolean, -): void { - const current = parent[key]; - if (current === undefined) { - if (!allowMissing) { - throw new Error( - `Managed MCP schema overlay expected an upstream node at ${key}. ` + - "Update or remove scripts/codegen/managed-mcp-schema-overlay.json.", - ); - } - parent[key] = clone(desired); - return; - } - if (stableStringify(current) === stableStringify(desired)) { - parent[key] = clone(desired); +interface ContractNode { + parent: SchemaNode; + key: string; + desired: SchemaNode; + legacy?: SchemaNode; +} + +function installContract(nodes: ContractNode[]): void { + if ( + nodes.every( + ({ parent, key, desired }) => + stableStringify(parent[key]) === stableStringify(desired), + ) + ) { return; } - if (!isKnownLegacy(current)) { + if ( + !nodes.every( + ({ parent, key, legacy }) => + stableStringify(parent[key]) === stableStringify(legacy), + ) + ) { + const missing = nodes.find( + ({ parent, key, legacy }) => + parent[key] === undefined && legacy !== undefined, + ); throw new Error( - `Managed MCP schema overlay conflicts with an unknown upstream shape at ${key}. ` + + (missing + ? `Managed MCP schema overlay expected an upstream node at ${missing.key}. ` + : "Managed MCP schema overlay conflicts with an unknown upstream shape or partial contract. ") + "Update or remove scripts/codegen/managed-mcp-schema-overlay.json.", ); } - parent[key] = clone(desired); -} - -function matches(expected: unknown): (current: unknown) => boolean { - return (current) => stableStringify(current) === stableStringify(expected); + for (const { parent, key, desired } of nodes) { + parent[key] = clone(desired); + } } function requireDefinitions(schema: SchemaNode): SchemaNode { @@ -128,68 +131,62 @@ function applyApiOverlay(schema: SchemaNode): void { const definitions = requireDefinitions(schema); const api = overlay["api.schema.json"]; const legacy = overlay.legacy["api.schema.json"]; - installNode( - definitions, - "ManagedMcpServerConfig", - api.ManagedMcpServerConfig, - () => false, - true, - ); - installNode( - definitions, - "McpHeadersHandlePendingHeadersRefreshRequest", - api.McpHeadersHandlePendingHeadersRefreshRequest, - matches(legacy.McpHeadersHandlePendingHeadersRefreshRequest), - false, - ); - installNode( - definitions, - "McpServerSource", - api.McpServerSource, - matches(legacy.McpServerSource), - false, - ); - installNode( - requireProperties(definitions, "SessionOpenOptions"), - "managedMcpServers", - api.managedMcpServers, - () => false, - true, - ); - installNode( - requireProperties(definitions, "McpServer"), - "displayName", - api.mcpServerDisplayName, - () => false, - true, - ); + installContract([ + { + parent: definitions, + key: "ManagedMcpServerConfig", + desired: api.ManagedMcpServerConfig, + }, + { + parent: definitions, + key: "McpHeadersHandlePendingHeadersRefreshRequest", + desired: api.McpHeadersHandlePendingHeadersRefreshRequest, + legacy: legacy.McpHeadersHandlePendingHeadersRefreshRequest, + }, + { + parent: definitions, + key: "McpServerSource", + desired: api.McpServerSource, + legacy: legacy.McpServerSource, + }, + { + parent: requireProperties(definitions, "SessionOpenOptions"), + key: "managedMcpServers", + desired: api.managedMcpServers, + }, + { + parent: requireProperties(definitions, "McpServer"), + key: "displayName", + desired: api.mcpServerDisplayName, + }, + ]); } function applySessionEventsOverlay(schema: SchemaNode): void { const definitions = requireDefinitions(schema); const events = overlay["session-events.schema.json"]; const legacy = overlay.legacy["session-events.schema.json"]; - installNode( - definitions, - "McpServerSource", - events.McpServerSource, - matches(legacy.McpServerSource), - false, - ); - installNode( - requireProperties(definitions, "McpServersLoadedServer"), - "displayName", - events.mcpServersLoadedDisplayName, - () => false, - true, - ); + installContract([ + { + parent: definitions, + key: "McpServerSource", + desired: events.McpServerSource, + legacy: legacy.McpServerSource, + }, + { + parent: requireProperties(definitions, "McpServersLoadedServer"), + key: "displayName", + desired: events.mcpServersLoadedDisplayName, + }, + ]); } /** * Applies the additive managed MCP contract from copilot-agent-runtime#17210. * * Remove this overlay after the pinned @github/copilot package publishes the - * same schema. Unknown upstream shapes fail instead of being silently replaced. + * same schema. Only complete legacy or target contracts are accepted; validation + * happens before mutation so unknown or partially published shapes fail closed. */ export function applyManagedMcpSchemaOverlay( schema: T, From 2b5e165690564e397b555b840402a63430d081f7 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Tue, 15 Sep 2026 13:41:44 +0100 Subject: [PATCH 08/16] Align Java managed MCP callback naming and experimental opt-in Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/mcp.md | 2 +- .../com/github/copilot/CopilotClient.java | 4 +- .../github/copilot/SessionRequestBuilder.java | 8 +-- .../copilot/rpc/ResumeSessionConfig.java | 16 +++--- .../com/github/copilot/rpc/SessionConfig.java | 16 +++--- .../CopilotExperimentalProcessorTest.java | 52 +++++++++++++++++++ .../McpAuthInterestRegistrationTest.java | 13 +++-- .../copilot/McpHeadersRefreshHandlerTest.java | 43 +++++++++++++-- 8 files changed, 121 insertions(+), 33 deletions(-) diff --git a/docs/features/mcp.md b/docs/features/mcp.md index 31af134793..b4d9ea0cc8 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -70,7 +70,7 @@ The corresponding configuration and callback names are: | Python | `managed_mcp_servers` | `on_mcp_headers_refresh` | | Go | `ManagedMCPServers` | `OnMCPHeadersRefresh` | | .NET | `ManagedMcpServers` | `OnMcpHeadersRefresh` | -| Java | `setManagedMcpServers(...)` | `setOnMcpHeadersRefreshRequest(...)` | +| Java | `setManagedMcpServers(...)` | `setOnMcpHeadersRefresh(...)` | | Rust | `with_managed_mcp_servers(...)` | `with_mcp_headers_handler(...)` | ### Host responsibilities diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index 3d81e44248..dfdac9eed9 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -1027,7 +1027,7 @@ public CompletableFuture createSession(SessionConfig config) { } registeredIdHolder[0] = returnedId; CompletableFuture interests = registerMcpEventInterests(session, returnedId, - config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefreshRequest() != null); + config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefresh() != null); session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); session.setOpenCanvases(response.openCanvases()); @@ -1214,7 +1214,7 @@ public CompletableFuture resumeSession(String sessionId, ResumeS String returnedId = response.sessionId(); String interestSessionId = returnedId != null ? returnedId : sessionId; return registerMcpEventInterests(session, interestSessionId, - config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefreshRequest() != null) + config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefresh() != null) .thenApply(v -> response); }).thenCompose(response -> { session.setWorkspacePath(response.workspacePath()); diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index 9139a491fc..9d1506d533 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -391,8 +391,8 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getOnMcpAuthRequest() != null) { session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); } - if (config.getOnMcpHeadersRefreshRequest() != null) { - session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefreshRequest()); + if (config.getOnMcpHeadersRefresh() != null) { + session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefresh()); } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); @@ -446,8 +446,8 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getOnMcpAuthRequest() != null) { session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); } - if (config.getOnMcpHeadersRefreshRequest() != null) { - session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefreshRequest()); + if (config.getOnMcpHeadersRefresh() != null) { + session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefresh()); } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 21afe2864c..d4e7b6703e 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -68,7 +68,7 @@ public class ResumeSessionConfig { private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; - private McpHeadersRefreshHandler onMcpHeadersRefreshRequest; + private McpHeadersRefreshHandler onMcpHeadersRefresh; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -874,19 +874,19 @@ public ResumeSessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) * @return the handler, or {@code null} if not set */ @JsonIgnore - public McpHeadersRefreshHandler getOnMcpHeadersRefreshRequest() { - return onMcpHeadersRefreshRequest; + public McpHeadersRefreshHandler getOnMcpHeadersRefresh() { + return onMcpHeadersRefresh; } /** * Sets the managed MCP dynamic-header refresh handler. * - * @param onMcpHeadersRefreshRequest + * @param onMcpHeadersRefresh * the handler * @return this config instance for method chaining */ - public ResumeSessionConfig setOnMcpHeadersRefreshRequest(McpHeadersRefreshHandler onMcpHeadersRefreshRequest) { - this.onMcpHeadersRefreshRequest = onMcpHeadersRefreshRequest; + public ResumeSessionConfig setOnMcpHeadersRefresh(McpHeadersRefreshHandler onMcpHeadersRefresh) { + this.onMcpHeadersRefresh = onMcpHeadersRefresh; return this; } @@ -1427,6 +1427,7 @@ public ResumeSessionConfig setMcpServers(Map mcpServers * * @return the managed MCP servers map */ + @CopilotExperimental public Map getManagedMcpServers() { return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); } @@ -1438,6 +1439,7 @@ public Map getManagedMcpServers() { * non-secret server configurations keyed by stable managed identity * @return this config instance for method chaining */ + @CopilotExperimental public ResumeSessionConfig setManagedMcpServers(Map managedMcpServers) { this.managedMcpServers = managedMcpServers; return this; @@ -2215,7 +2217,7 @@ public ResumeSessionConfig clone() { copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; copy.onMcpAuthRequest = this.onMcpAuthRequest; - copy.onMcpHeadersRefreshRequest = this.onMcpHeadersRefreshRequest; + copy.onMcpHeadersRefresh = this.onMcpHeadersRefresh; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index ca2e428d49..04567c04ef 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -68,7 +68,7 @@ public class SessionConfig { private Boolean manageScheduleEnabled; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; - private McpHeadersRefreshHandler onMcpHeadersRefreshRequest; + private McpHeadersRefreshHandler onMcpHeadersRefresh; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -918,19 +918,19 @@ public SessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) { * @return the handler, or {@code null} if not set */ @JsonIgnore - public McpHeadersRefreshHandler getOnMcpHeadersRefreshRequest() { - return onMcpHeadersRefreshRequest; + public McpHeadersRefreshHandler getOnMcpHeadersRefresh() { + return onMcpHeadersRefresh; } /** * Sets the managed MCP dynamic-header refresh handler. * - * @param onMcpHeadersRefreshRequest + * @param onMcpHeadersRefresh * the handler * @return this config instance for method chaining */ - public SessionConfig setOnMcpHeadersRefreshRequest(McpHeadersRefreshHandler onMcpHeadersRefreshRequest) { - this.onMcpHeadersRefreshRequest = onMcpHeadersRefreshRequest; + public SessionConfig setOnMcpHeadersRefresh(McpHeadersRefreshHandler onMcpHeadersRefresh) { + this.onMcpHeadersRefresh = onMcpHeadersRefresh; return this; } @@ -1080,6 +1080,7 @@ public SessionConfig setMcpServers(Map mcpServers) { * * @return the managed MCP servers map */ + @CopilotExperimental public Map getManagedMcpServers() { return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); } @@ -1091,6 +1092,7 @@ public Map getManagedMcpServers() { * non-secret server configurations keyed by stable managed identity * @return this config instance for method chaining */ + @CopilotExperimental public SessionConfig setManagedMcpServers(Map managedMcpServers) { this.managedMcpServers = managedMcpServers; return this; @@ -2355,7 +2357,7 @@ public SessionConfig clone() { copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; copy.onMcpAuthRequest = this.onMcpAuthRequest; - copy.onMcpHeadersRefreshRequest = this.onMcpHeadersRefreshRequest; + copy.onMcpHeadersRefresh = this.onMcpHeadersRefresh; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java index b7005c5d52..2722913cc9 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java @@ -12,6 +12,11 @@ import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import javax.tools.ToolProvider; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.ElementFilter; import java.net.URI; import java.net.URL; import java.nio.file.Path; @@ -19,8 +24,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -148,6 +157,49 @@ void passes_whenOptInFlagIsProvided() { assertFalse(hasError, "Expected no errors with opt-in flag, got: " + diagnostics.getDiagnostics()); } + @Test + void managedMcpServerAccessorsAreExperimentalForCreateAndResume() { + var diagnostics = new DiagnosticCollector(); + var checkedMethods = new AtomicInteger(); + var task = ToolProvider.getSystemJavaCompiler().getTask(null, null, diagnostics, + List.of("-classpath", resolveClasspath(), "-proc:only"), null, + List.of(inMemorySource("consumer.Consumer", "package consumer; public class Consumer {}"))); + task.setProcessors(List.of(new AbstractProcessor() { + @Override + public Set getSupportedAnnotationTypes() { + return Set.of("*"); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (!roundEnv.processingOver()) { + for (String configName : List.of("SessionConfig", "ResumeSessionConfig")) { + var config = processingEnv.getElementUtils() + .getTypeElement("com.github.copilot.rpc." + configName); + assertNotNull(config); + for (var method : ElementFilter.methodsIn(config.getEnclosedElements())) { + if (Set.of("getManagedMcpServers", "setManagedMcpServers") + .contains(method.getSimpleName().toString())) { + assertNotNull(method.getAnnotation(CopilotExperimental.class), + configName + "." + method.getSimpleName()); + checkedMethods.incrementAndGet(); + } + } + } + } + return false; + } + })); + + assertTrue(task.call(), () -> "Compiler diagnostics: " + diagnostics.getDiagnostics()); + assertEquals(4, checkedMethods.get()); + } + private DiagnosticCollector compile(List sources, List extraOptions) { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); diff --git a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java index d5b731ae84..4c4cf95b63 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java @@ -181,10 +181,10 @@ void resumeSessionRegistersMcpAuthInterestOnlyWhenHandlerConfigured() throws Exc void createAndResumeRegisterManagedMcpHeadersRefreshInterest() throws Exception { try (var server = new RecordingRuntime(); var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { - try (var session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setOnMcpHeadersRefreshRequest((request, invocation) -> java.util.concurrent.CompletableFuture - .completedFuture(McpHeadersRefreshResult.none()))) + try (var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnMcpHeadersRefresh((request, invocation) -> java.util.concurrent.CompletableFuture + .completedFuture(McpHeadersRefreshResult.none()))) .get()) { assertNotNull(session); } @@ -201,9 +201,8 @@ void createAndResumeRegisterManagedMcpHeadersRefreshInterest() throws Exception try (var session = client.resumeSession("managed-session", new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setOnMcpHeadersRefreshRequest( - (request, invocation) -> java.util.concurrent.CompletableFuture - .completedFuture(McpHeadersRefreshResult.none()))) + .setOnMcpHeadersRefresh((request, invocation) -> java.util.concurrent.CompletableFuture + .completedFuture(McpHeadersRefreshResult.none()))) .get()) { assertNotNull(session); } diff --git a/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java b/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java index d9a97a550f..a99779fd06 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -32,6 +33,38 @@ class McpHeadersRefreshHandlerTest { private static final String METHOD = "session.mcp.headers.handlePendingHeadersRefreshRequest"; + @Test + void createConfigPreservesRefreshHandlerWithoutSerializingIt() { + var config = new SessionConfig(); + assertNull(config.getOnMcpHeadersRefresh()); + McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture + .completedFuture(McpHeadersRefreshResult.none()); + + assertSame(config, config.setOnMcpHeadersRefresh(handler)); + assertSame(handler, config.getOnMcpHeadersRefresh()); + assertSame(handler, config.clone().getOnMcpHeadersRefresh()); + assertFalse(JsonRpcClient.getObjectMapper().valueToTree(config).has("onMcpHeadersRefresh")); + + assertSame(config, config.setOnMcpHeadersRefresh(null)); + assertNull(config.getOnMcpHeadersRefresh()); + } + + @Test + void resumeConfigPreservesRefreshHandlerWithoutSerializingIt() { + var config = new ResumeSessionConfig(); + assertNull(config.getOnMcpHeadersRefresh()); + McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture + .completedFuture(McpHeadersRefreshResult.none()); + + assertSame(config, config.setOnMcpHeadersRefresh(handler)); + assertSame(handler, config.getOnMcpHeadersRefresh()); + assertSame(handler, config.clone().getOnMcpHeadersRefresh()); + assertFalse(JsonRpcClient.getObjectMapper().valueToTree(config).has("onMcpHeadersRefresh")); + + assertSame(config, config.setOnMcpHeadersRefresh(null)); + assertNull(config.getOnMcpHeadersRefresh()); + } + @Test void createHandlerDispatchesHeadersWithoutTtl() { McpHeadersRefreshHandler handler = (request, invocation) -> { @@ -43,7 +76,7 @@ void createHandlerDispatchesHeadersWithoutTtl() { .completedFuture(McpHeadersRefreshResult.withHeaders(Map.of("Authorization", "Bearer dynamic"))); }; - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); assertEquals("headers", params.path("result").path("kind").asText()); assertEquals("Bearer dynamic", params.path("result").path("headers").path("Authorization").asText()); @@ -55,7 +88,7 @@ void resumeHandlerDispatchesHeadersWithTtl() { McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture .completedFuture(McpHeadersRefreshResult.withHeaders(Map.of("X-Dynamic", "value"), 60_000L)); - JsonNode params = dispatch(new ResumeSessionConfig().setOnMcpHeadersRefreshRequest(handler)); + JsonNode params = dispatch(new ResumeSessionConfig().setOnMcpHeadersRefresh(handler)); assertEquals("headers", params.path("result").path("kind").asText()); assertEquals("value", params.path("result").path("headers").path("X-Dynamic").asText()); @@ -67,7 +100,7 @@ void handlerCanReturnExplicitNone() { McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture .completedFuture(McpHeadersRefreshResult.none()); - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); assertEquals("none", params.path("result").path("kind").asText()); assertEquals(1, params.path("result").size()); @@ -78,7 +111,7 @@ void handlerFailureReturnsExplicitErrorMessage() { McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture .failedFuture(new IllegalStateException("header provider unavailable")); - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); assertEquals("error", params.path("result").path("kind").asText()); assertEquals("header provider unavailable", params.path("result").path("message").asText()); @@ -90,7 +123,7 @@ void handlerExceptionReturnsExplicitErrorMessage() { throw new IllegalArgumentException("invalid header configuration"); }; - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefreshRequest(handler)); + JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); assertEquals("error", params.path("result").path("kind").asText()); assertEquals("invalid header configuration", params.path("result").path("message").asText()); From eff610461a49941756318a3644de520cad3e760f Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Tue, 15 Sep 2026 14:09:03 +0100 Subject: [PATCH 09/16] Preserve HookContext source compatibility with request-aware dispatch Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 9 ++++ rust/src/hooks.rs | 22 ++++++++-- rust/tests/session_test.rs | 87 +++++++++++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/rust/README.md b/rust/README.md index cdc8e07c05..010160665a 100644 --- a/rust/README.md +++ b/rust/README.md @@ -455,6 +455,15 @@ let session = client **Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. Return `HookOutput::None` for events you don't handle. +For request correlation, override `on_hook_with_request_id(event, request_id)` and +delegate to `self.on_hook(event).await` to preserve your existing hook dispatch. +The default implementation delegates automatically, so existing `on_hook` and +per-hook implementations need no changes; `HookContext` still contains only +`session_id`. After successfully writing a response, the SDK calls +`on_hook_response_sent(HookResponseSent)` with the same session and JSON-RPC +request IDs and the runtime hook type. This notification confirms the write, +not CLI processing, and does not fire if the write fails. + ### System Message Transforms Transforms customize system message sections during session creation. The SDK injects `action: "transform"` entries for each section ID your transform handles. diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs index 05481f1aea..9d713a1efc 100644 --- a/rust/src/hooks.rs +++ b/rust/src/hooks.rs @@ -19,8 +19,6 @@ use crate::types::SessionId; pub struct HookContext { /// The session this hook was triggered in. pub session_id: SessionId, - /// JSON-RPC request ID for this hook invocation. - pub request_id: u64, } /// Hook response that was successfully written back to the CLI. @@ -525,6 +523,18 @@ impl HookOutput { /// sets automatically). #[async_trait] pub trait SessionHooks: Send + Sync + 'static { + /// Request-aware dispatch for correlating a hook invocation with + /// [`on_hook_response_sent`](Self::on_hook_response_sent). + /// + /// `request_id` is the JSON-RPC request ID also reported in + /// [`HookResponseSent::request_id`]. The default delegates to + /// [`on_hook`](Self::on_hook), preserving existing dispatch and per-hook + /// implementations. Override this method when correlation is needed, + /// and call `self.on_hook(event).await` to retain that dispatch. + async fn on_hook_with_request_id(&self, event: HookEvent, _request_id: u64) -> HookOutput { + self.on_hook(event).await + } + /// Top-level dispatch. The default implementation fans out to the /// per-hook methods below; override this only if you want a single /// matching point across all hook types. @@ -584,6 +594,11 @@ pub trait SessionHooks: Send + Sync + 'static { } /// Called after a hook response is successfully written back to the CLI. + /// + /// This confirms transport delivery, not CLI processing. It is not called + /// when writing the response fails. Correlate it with + /// [`on_hook_with_request_id`](Self::on_hook_with_request_id) using the + /// response's session and request IDs. async fn on_hook_response_sent(&self, _response: HookResponseSent) {} /// Called before a tool executes. Return `Some(output)` to approve/deny @@ -715,7 +730,6 @@ pub(crate) async fn dispatch_hook_for_request( ) -> Result { let ctx = HookContext { session_id: session_id.clone(), - request_id, }; let event = match hook_type { @@ -770,7 +784,7 @@ pub(crate) async fn dispatch_hook_for_request( }; let dispatch_start = Instant::now(); - let output = hooks.on_hook(event).await; + let output = hooks.on_hook_with_request_id(event, request_id).await; tracing::debug!( elapsed_ms = dispatch_start.elapsed().as_millis(), session_id = %session_id, diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 8d39447f43..684994fa17 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -5771,14 +5771,23 @@ async fn hooks_invoke_dispatches_to_session_hooks() { }; struct PolicyHooks { + invoked: tokio::sync::mpsc::UnboundedSender<(SessionId, u64)>, response_sent: tokio::sync::mpsc::UnboundedSender, } #[async_trait] impl SessionHooks for PolicyHooks { + async fn on_hook_with_request_id(&self, event: HookEvent, request_id: u64) -> HookOutput { + if let HookEvent::PreToolUse { ctx, .. } = &event { + self.invoked + .send((ctx.session_id.clone(), request_id)) + .unwrap(); + } + self.on_hook(event).await + } + async fn on_hook(&self, event: HookEvent) -> HookOutput { match event { - HookEvent::PreToolUse { input, ctx } => { - assert_eq!(ctx.request_id, 300); + HookEvent::PreToolUse { input, .. } => { if input.tool_name == "rm" { HookOutput::PreToolUse(PreToolUseOutput { permission_decision: Some("deny".to_string()), @@ -5800,7 +5809,9 @@ async fn hooks_invoke_dispatches_to_session_hooks() { let (response_sent_tx, mut response_sent_rx) = tokio::sync::mpsc::unbounded_channel::(); + let (invoked_tx, mut invoked_rx) = tokio::sync::mpsc::unbounded_channel(); let (_session, mut server) = create_session_pair_with_hooks(Arc::new(PolicyHooks { + invoked: invoked_tx, response_sent: response_sent_tx, })) .await; @@ -5837,6 +5848,78 @@ async fn hooks_invoke_dispatches_to_session_hooks() { .unwrap(); assert_eq!(response_sent.request_id, 300); assert_eq!(response_sent.hook_type, "preToolUse"); + assert_eq!(response_sent.session_id.as_str(), server.session_id); + let invoked = timeout(TIMEOUT, invoked_rx.recv()).await.unwrap().unwrap(); + assert_eq!( + invoked, + (response_sent.session_id, response_sent.request_id) + ); +} + +#[test] +fn hooks_context_supports_legacy_literal_and_exhaustive_pattern() { + use github_copilot_sdk::hooks::HookContext; + + let context = HookContext { + session_id: SessionId::from("legacy-session"), + }; + let HookContext { session_id } = context; + assert_eq!(session_id.as_str(), "legacy-session"); +} + +#[tokio::test] +async fn hooks_response_sent_is_not_called_when_write_fails() { + use github_copilot_sdk::hooks::{HookEvent, HookOutput, HookResponseSent, SessionHooks}; + + struct GatedHooks { + entered: Notify, + release: Notify, + response_sent: tokio::sync::mpsc::UnboundedSender, + } + + #[async_trait] + impl SessionHooks for GatedHooks { + async fn on_hook(&self, _event: HookEvent) -> HookOutput { + self.entered.notify_one(); + self.release.notified().await; + HookOutput::None + } + + async fn on_hook_response_sent(&self, response: HookResponseSent) { + self.response_sent.send(response).unwrap(); + } + } + + let (response_sent_tx, mut response_sent_rx) = tokio::sync::mpsc::unbounded_channel(); + let hooks = Arc::new(GatedHooks { + entered: Notify::new(), + release: Notify::new(), + response_sent: response_sent_tx, + }); + let (_session, mut server) = create_session_pair_with_hooks(hooks.clone()).await; + server + .send_request( + 302, + "hooks.invoke", + serde_json::json!({ + "sessionId": server.session_id, + "hookType": "sessionEnd", + "input": { + "sessionId": server.session_id, + "timestamp": 1234567890, + "cwd": ".", + "reason": "complete" + } + }), + ) + .await; + timeout(TIMEOUT, hooks.entered.notified()).await.unwrap(); + assert!(response_sent_rx.try_recv().is_err()); + + // Keep the inbound stream open, but force the response write to fail. + drop(server.read); + hooks.release.notify_one(); + assert!(timeout(TIMEOUT, response_sent_rx.recv()).await.is_err()); } #[tokio::test] From cd6a8f11692bbff088a8a0bcf0f80b8560afe241 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Wed, 16 Sep 2026 11:31:47 +0100 Subject: [PATCH 10/16] Expose Connector MCP session bridge across SDKs Keep runtime managed-MCP details behind Connector-facing configuration, validate authorization against the real runtime, and remove the obsolete schema overlay. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/mcp.md | 140 +++++++--- dotnet/README.md | 81 ++++++ dotnet/src/Client.cs | 36 ++- dotnet/src/Session.cs | 2 +- dotnet/src/Types.cs | 65 +++-- .../test/Unit/ClientSessionLifetimeTests.cs | 61 ++++- dotnet/test/Unit/SerializationTests.cs | 15 +- go/README.md | 37 +++ go/client.go | 4 +- go/client_test.go | 57 ++-- go/session.go | 10 +- go/session_event_serialization_test.go | 73 ++++++ go/session_test.go | 102 +++++++- go/types.go | 101 ++++++-- java/README.md | 37 +++ java/scripts/codegen/java.ts | 17 +- .../com/github/copilot/CopilotSession.java | 12 +- .../github/copilot/SessionRequestBuilder.java | 4 +- .../copilot/rpc/ConnectorMcpServerConfig.java | 175 +++++++++++++ .../copilot/rpc/CreateSessionRequest.java | 25 +- .../copilot/rpc/McpHeadersRefreshHandler.java | 12 +- .../rpc/McpHeadersRefreshInvocation.java | 2 +- .../copilot/rpc/McpHeadersRefreshRequest.java | 11 +- .../copilot/rpc/McpHeadersRefreshResult.java | 16 +- .../copilot/rpc/ResumeSessionConfig.java | 43 ++-- .../copilot/rpc/ResumeSessionRequest.java | 25 +- .../com/github/copilot/rpc/SessionConfig.java | 43 ++-- .../CopilotExperimentalProcessorTest.java | 71 ++++- .../McpAuthInterestRegistrationTest.java | 4 +- .../copilot/McpHeadersRefreshHandlerTest.java | 4 +- .../copilot/SessionRequestBuilderTest.java | 44 ++-- nodejs/README.md | 13 + nodejs/src/client.ts | 26 +- nodejs/src/index.ts | 2 +- nodejs/src/session.ts | 6 +- nodejs/src/types.ts | 37 ++- nodejs/test/client.test.ts | 32 ++- nodejs/test/e2e/mcp_oauth.e2e.test.ts | 52 +++- nodejs/test/shared-codegen.test.ts | 191 +------------- python/README.md | 15 ++ python/copilot/__init__.py | 4 +- python/copilot/client.py | 57 ++-- python/copilot/session.py | 44 +++- python/test_client.py | 26 +- rust/README.md | 94 ++++++- rust/cli-version-in-process.txt | 18 +- rust/src/handler.rs | 30 ++- rust/src/session.rs | 4 +- rust/src/types.rs | 145 +++++++---- rust/tests/session_test.rs | 213 +++++++++++++-- .../codegen/managed-mcp-schema-overlay.json | 243 ------------------ scripts/codegen/managedMcpSchemaOverlay.ts | 204 --------------- scripts/codegen/rust.ts | 12 +- scripts/codegen/utils.ts | 6 +- 54 files changed, 1743 insertions(+), 1060 deletions(-) create mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java delete mode 100644 scripts/codegen/managed-mcp-schema-overlay.json delete mode 100644 scripts/codegen/managedMcpSchemaOverlay.ts diff --git a/docs/features/mcp.md b/docs/features/mcp.md index b4d9ea0cc8..512ecb0cf6 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -24,39 +24,75 @@ The SDK supports two MCP transport families: | **Local/Stdio** | Runs as a subprocess, communicates via stdin/stdout | Local tools, file access, custom scripts | | **HTTP/SSE** | Remote server accessed via HTTP | Shared services, cloud-hosted tools | -Transport and configuration origin are separate concepts. A managed MCP server -uses the HTTP transport, but its configuration comes from a trusted host catalog -instead of user or workspace configuration. Session status and loaded-server -events report this distinction with `source: "managed"` and include the catalog -display name. +Transport and configuration origin are separate concepts. A Connector MCP +endpoint uses the HTTP transport, but its configuration comes from the +Copilot Connectors service rather than user or workspace settings. -## Managed MCP servers +## Integrating Copilot Connectors -Managed MCP lets a trusted SDK host inject a catalog of non-secret hosted -servers for one session. The runtime keeps this catalog separate from -`mcpServers`, so existing local stdio, HTTP, SSE, and OAuth behavior remains -unchanged. +People use Copilot Connectors by choosing a Connector in the host application, +completing consent when required, and then using its tools. They do not enter an +MCP endpoint, configure authorization, or manage the underlying MCP server. -The host supplies each server under a stable managed identity: +An SDK-based host implements that experience: + +1. Retrieve the supported Connector catalog for the selected account. +1. Present Connector metadata and connection status, and perform Connect, + Reconnect, or Disconnect through the Connector service. +1. Consume each service-advertised MCP URL without constructing or rewriting it. +1. Supply connected Connector endpoints to the session through + `connectorMcpServers`. +1. Authorize calls to those endpoints for the same selected GitHub account + through `onMcpHeadersRefresh`. + +The SDK API described here is the session bridge for connected Connectors. It +does not fetch the catalog, render Connector UI, perform consent, or manage +service connections. Existing local stdio, HTTP, SSE, and OAuth MCP behavior +remains unchanged. + +The integration boundary is: + +| Responsibility | Owner | +| --- | --- | +| List available Connectors and connection state | Connector service and host application | +| Connect, Reconnect, Disconnect, browser consent, and polling | Connector service and host application | +| Select and pin the active account | Host application | +| Load connected Connector endpoints and supply service authorization | Copilot SDK | +| List server state and tools, enable or disable per session, and receive status events | Copilot SDK | +| Apply MCP policy, cache authorization, and retry once after a 401 | Connected Copilot runtime | + +> [!NOTE] +> Connector session integration is experimental and only takes effect when +> enabled by the connected runtime. The host must re-supply the effective +> connected set on cold resume. + +The host supplies each connected endpoint under a stable Connector server key: ```typescript const session = await client.createSession({ - managedMcpServers: { - "github-enterprise": { - displayName: "GitHub Enterprise", - url: "https://mcp.example.com/", - tools: ["issues", "pull_requests"], + connectorMcpServers: { + [connector.serverKey]: { + displayName: connector.displayName, + url: connector.advertisedMcpUrl, + tools: connector.tools, timeout: 30_000, - headersRefreshTtlMs: 60_000, + authorizationCacheTtlMs: 60_000, }, }, - onMcpHeadersRefresh: async ({ serverName, serverUrl, reason }) => { - const credential = await broker.getCredential({ serverName, serverUrl, reason }); + onMcpHeadersRefresh: async ({ serverKey, serverUrl, reason }) => { + const authorization = await getConnectorAuthorization({ + account: selectedAccount, + serverKey, + serverUrl, + reason, + }); return { - headers: { Authorization: credential.authorizationHeader }, - ttlMs: credential.expiresInMs, + headers: { + Authorization: ["Bearer", authorization.accessToken].join(" "), + }, + ttlMs: authorization.expiresInMs, }; }, }); @@ -64,36 +100,58 @@ const session = await client.createSession({ The corresponding configuration and callback names are: -| SDK | Managed servers | Header refresh callback | +| SDK | Connector MCP servers | Header refresh callback | | --- | --- | --- | -| Node.js | `managedMcpServers` | `onMcpHeadersRefresh` | -| Python | `managed_mcp_servers` | `on_mcp_headers_refresh` | -| Go | `ManagedMCPServers` | `OnMCPHeadersRefresh` | -| .NET | `ManagedMcpServers` | `OnMcpHeadersRefresh` | -| Java | `setManagedMcpServers(...)` | `setOnMcpHeadersRefresh(...)` | -| Rust | `with_managed_mcp_servers(...)` | `with_mcp_headers_handler(...)` | +| Node.js | `connectorMcpServers` | `onMcpHeadersRefresh` | +| Python | `connector_mcp_servers` | `on_mcp_headers_refresh` | +| Go | `ConnectorMCPServers` | `OnMCPHeadersRefresh` | +| .NET | `ConnectorMcpServers` | `OnMcpHeadersRefresh` | +| Java | `setConnectorMcpServers(...)` | `setOnMcpHeadersRefresh(...)` | +| Rust | `with_connector_mcp_servers(...)` | `with_mcp_headers_handler(...)` | + +The typed session MCP API supplies UI hooks for already loaded Connectors: list +server status and tools, enable or disable a server for the current session, and +subscribe to MCP loaded and status-change events. Connect, Reconnect, +Disconnect, browser consent, and catalog polling remain operations against the +Connector service. + +> [!WARNING] +> The current public runtime contract cannot replace the authoritative +> Connector set on a running session. After a Connect, Reconnect, or Disconnect, +> create or cold-resume a session with the updated `connectorMcpServers` map. +> Do not treat the generic MCP `stopServer` operation as Connector Disconnect: +> an MCP reload can restore a server from the session's original configuration. ### Host responsibilities -Managed MCP hosts must enforce these boundaries: +SDK hosts must enforce these boundaries: * **Trusted catalog injection**: Only inject server identities, display metadata, endpoints, and tool policy from a trusted catalog. Do not treat model output or untrusted content as catalog configuration. -* **Memory-only credentials**: Keep access tokens and derived authorization - headers in memory. Do not place credentials in `managedMcpServers`, session - history, workspace state, or persistent MCP OAuth storage. -* **Expiry and revocation**: Set `ttlMs` to the remaining credential lifetime. - The runtime clamps it to `headersRefreshTtlMs`. Throw from the callback when - the broker denies access, fails, or reports revocation; the SDK forwards an - explicit broker error without converting it to a successful empty response. -* **Cold resume**: Re-supply both the managed catalog and the header refresh - callback when cold-resuming a session. Managed configuration and credentials - are not recovered from persisted session state. +* **Connector service authorization**: Supply the selected account's + short-lived GitHub authorization only to the exact endpoint advertised by the + Connector service. The Connector service, not the SDK host, owns downstream + provider credentials such as Microsoft or Slack tokens. +* **Memory-only authorization**: Keep access tokens and derived authorization + headers in memory. Do not place them in `connectorMcpServers`, session history, + workspace state, or persistent MCP OAuth storage. +* **Account binding**: Fetch the catalog and authorization for the same + authenticated account. Invalidate cached authorization before switching + accounts so one account's access cannot be reused by another. +* **Expiry and revocation**: Set `ttlMs` to the remaining authorization lifetime. + The runtime clamps it to `authorizationCacheTtlMs`. Throw from the callback + when Connector authorization is denied, fails, or is revoked; the SDK + forwards an explicit error without converting it to a successful empty + response. +* **Cold resume**: Re-supply both the connected Connector endpoints and the + header refresh callback when cold-resuming a session. Connector configuration + and authorization are not recovered from persisted session state. Returning no result from the callback reports that no dynamic headers are -available. Existing static `headers`, arbitrary HTTP servers, and MCP OAuth -handlers continue to use their current behavior. +available for the Connector endpoint. Existing static `headers`, arbitrary HTTP +servers configured through `mcpServers`, and MCP OAuth handlers continue to use +their current behavior. ## Configuration diff --git a/dotnet/README.md b/dotnet/README.md index c518a40326..5069ef2ec8 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -146,6 +146,8 @@ Create a new conversation session. - `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory. - `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled. - `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`. +- `ConnectorMcpServers` - Connected Copilot Connector MCP endpoints supplied from the service catalog, keyed by stable server key. Configuration is non-secret. +- `OnMcpHeadersRefresh` - Supplies short-lived client-to-Copilot Connectors service authorization headers, normally the selected account's GitHub bearer. `McpHeadersRefreshContext.ServerKey` matches the `ConnectorMcpServers` key. - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`. @@ -160,6 +162,8 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. - `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. +- `ConnectorMcpServers` - Re-supplies the connected Connector MCP endpoint set on cold resume. +- `OnMcpHeadersRefresh` - Re-registers the callback that supplies Copilot Connectors service authorization headers. ```csharp await using var session = await client.CreateSessionAsync(new SessionConfig @@ -299,6 +303,83 @@ Abort the currently processing message in this session. Get all events/messages from this session. +##### Connector MCP configuration and runtime APIs + +Supply connected Copilot Connector MCP endpoints when creating a session and +re-supply the effective connected set after a cold resume. + +The endpoint configuration is non-secret. `OnMcpHeadersRefresh` supplies +short-lived authorization from the SDK client to the exact service-advertised +Copilot Connectors MCP endpoint, normally using the selected account's GitHub +bearer. It does not supply Outlook, Slack, or other downstream provider tokens; +the Copilot Connectors service owns those. Hosts should match +`McpHeadersRefreshContext.ServerKey` and `ServerUrl` to the service-catalog entry +before returning headers. `ConnectorMcpServerConfig.AuthorizationCacheTtlMs` +caps how long the runtime may reuse service authorization, while +`McpHeadersRefreshResult.TtlMs` reports the remaining lifetime of the +authorization returned by a specific refresh. + +```csharp +var connectorServers = new Dictionary +{ + ["github"] = new() + { + DisplayName = "GitHub", + Url = "https://example.com/github/mcp", + AuthorizationCacheTtlMs = 60_000, + }, +}; + +async Task RefreshConnectorHeadersAsync( + McpHeadersRefreshContext context) => new() +{ + Headers = await GetCopilotConnectorsAuthorizationHeadersAsync( + context.ServerKey, + context.ServerUrl), + TtlMs = 60_000, +}; + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + ConnectorMcpServers = connectorServers, + OnMcpHeadersRefresh = RefreshConnectorHeadersAsync, +}); + +// Re-supply the connected set and callback after a cold resume. +await using var resumed = await client.ResumeSessionAsync("session-id", new ResumeSessionConfig +{ + ConnectorMcpServers = connectorServers, + OnMcpHeadersRefresh = RefreshConnectorHeadersAsync, +}); +``` + +The current public runtime contract does not support authoritative same-session +Connect or Disconnect reconciliation for `ConnectorMcpServers`. Generic MCP +start and stop operations are transient, and a runtime reload can restore the +configuration supplied when the session was created. Until the runtime promotes +its internal replacement and removal operations, create a new session or +cold-resume with the updated connected set. + +The generated session MCP API provides the public runtime operations needed for +Connector status and lifecycle UI: + +```csharp +var servers = await session.Rpc.Mcp.ListAsync(); +var running = await session.Rpc.Mcp.IsServerRunningAsync("github"); +var tools = await session.Rpc.Mcp.ListToolsAsync("github"); +await session.Rpc.Mcp.DisableAsync("github"); +await session.Rpc.Mcp.EnableAsync("github"); + +using var statusChanges = session.On(evt => + Console.WriteLine($"{evt.Data.ServerName}: {evt.Data.Status}")); +``` + +`SessionMcpServersLoadedEvent`, `SessionMcpServerRemovedEvent`, and +`SessionMcpServerNeedsReconnectEvent` provide the related lifecycle hooks. The +application host owns Connector service-catalog access, consent, polling, and +UI. These generic runtime APIs expose status and operational controls; they do +not reconcile Connector service-catalog membership. + ##### `DisposeAsync(): ValueTask` Close the session and release in-memory resources. Session data on disk is preserved — the conversation can be resumed later via `ResumeSessionAsync()`. To permanently delete session data, use `client.DeleteSessionAsync()`. diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f252599b7f..fc0b668ed9 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1251,7 +1251,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, - config.ManagedMcpServers, + ToManagedMcpServers(config.ConnectorMcpServers), config.McpOAuthTokenStorage, config.AuthClientIdMetadataUrl, "direct", @@ -1509,7 +1509,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, - config.ManagedMcpServers, + ToManagedMcpServers(config.ConnectorMcpServers), config.McpOAuthTokenStorage, config.AuthClientIdMetadataUrl, "direct", @@ -3016,7 +3016,39 @@ public static string Escape(string arg) } } + private static Dictionary? ToManagedMcpServers( + IDictionary? connectorMcpServers) + { + if (connectorMcpServers is null) + { + return null; + } + + var managedMcpServers = connectorMcpServers is Dictionary dictionary + ? new Dictionary(dictionary.Comparer) + : new Dictionary(); + + foreach (var (name, config) in connectorMcpServers) + { + managedMcpServers.Add(name, new ManagedMcpServerConfig( + config.DisplayName, + config.Url, + config.Tools, + config.Timeout, + config.AuthorizationCacheTtlMs)); + } + + return managedMcpServers; + } + // Request/Response types for RPC + internal sealed record ManagedMcpServerConfig( + string DisplayName, + string Url, + IList? Tools, + long? Timeout, + long? HeadersRefreshTtlMs); + internal record CreateSessionRequest( string? Model, string? SessionId, diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 1cfcb55352..39dbdb6119 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -752,7 +752,7 @@ await ExecuteMcpHeadersRefreshAndRespondAsync( new McpHeadersRefreshContext { SessionId = SessionId, - ServerName = data.ServerName, + ServerKey = data.ServerName, ServerUrl = data.ServerUrl, Reason = data.Reason }, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 347a997f35..c17553123a 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1317,45 +1317,56 @@ public sealed class McpAuthResult public static McpAuthResult Cancel() => new() { Cancelled = true }; } -/// Context for a managed MCP dynamic-headers refresh callback. +/// +/// Context for refreshing client-to-Copilot Connectors service authorization +/// headers for a connected MCP endpoint. +/// [Experimental(Diagnostics.Experimental)] public sealed class McpHeadersRefreshContext { /// Identifier of the session that requested refreshed headers. public string SessionId { get; set; } = string.Empty; - /// Human-readable managed catalog display name. - public string ServerName { get; set; } = string.Empty; + /// + /// Stable server key matching the key in . + /// + public string ServerKey { get; set; } = string.Empty; - /// Hosted MCP streamable HTTP endpoint. + /// Exact Copilot Connectors service MCP endpoint advertised for this server key. public string ServerUrl { get; set; } = string.Empty; /// Why the runtime invalidated or requested dynamic headers. public McpHeadersRefreshRequiredReason Reason { get; set; } } -/// Dynamic headers and their optional credential-bounded cache lifetime. +/// Copilot Connectors service authorization headers and their optional cache lifetime. [Experimental(Diagnostics.Experimental)] public sealed class McpHeadersRefreshResult { - /// HTTP headers to overlay on requests to the managed MCP server. + /// + /// Short-lived HTTP headers authorizing this client to call the exact Copilot + /// Connectors service MCP endpoint. These are not downstream provider tokens. + /// public required IDictionary Headers { get; set; } - /// Optional lifetime in milliseconds for the returned credentials. + /// Optional remaining lifetime in milliseconds for the returned service authorization. public long? TtlMs { get; set; } } /// -/// Non-secret hosted MCP server configuration supplied by a trusted managed catalog. -/// Credentials are provided only through . +/// Non-secret MCP endpoint configuration for a connected Copilot Connector +/// supplied from the service catalog. +/// Client authorization to call the endpoint is supplied only through +/// . The Copilot Connectors +/// service owns downstream provider tokens such as Outlook or Slack tokens. /// [Experimental(Diagnostics.Experimental)] -public sealed class ManagedMcpServerConfig +public sealed class ConnectorMcpServerConfig { - /// Human-readable catalog display name. + /// Human-readable Connector display name, distinct from the stable server identity. public required string DisplayName { get; set; } - /// Hosted MCP streamable HTTP endpoint. + /// Copilot Connector remote MCP streamable HTTP endpoint. public required string Url { get; set; } /// Tools to include. The runtime includes all tools when omitted. @@ -1364,8 +1375,11 @@ public sealed class ManagedMcpServerConfig /// Timeout in milliseconds for tool discovery and tool calls. public long? Timeout { get; set; } - /// Maximum dynamic-header cache lifetime in milliseconds. - public long? HeadersRefreshTtlMs { get; set; } + /// + /// Maximum time in milliseconds that the runtime may reuse the + /// client-to-Copilot Connectors service authorization. + /// + public long? AuthorizationCacheTtlMs { get; set; } } // ============================================================================ @@ -3377,8 +3391,10 @@ protected SessionConfigBase(SessionConfigBase? other) ? new Dictionary(dict, dict.Comparer) : new Dictionary(other.McpServers)) : null; - ManagedMcpServers = other.ManagedMcpServers is not null - ? new Dictionary(other.ManagedMcpServers) + ConnectorMcpServers = other.ConnectorMcpServers is not null + ? (other.ConnectorMcpServers is Dictionary connectorDict + ? new Dictionary(connectorDict, connectorDict.Comparer) + : new Dictionary(other.ConnectorMcpServers)) : null; McpOAuthTokenStorage = other.McpOAuthTokenStorage; AuthClientIdMetadataUrl = other.AuthClientIdMetadataUrl; @@ -3772,11 +3788,12 @@ protected SessionConfigBase(SessionConfigBase? other) public IDictionary? McpServers { get; set; } /// - /// Non-secret hosted MCP servers injected from a trusted managed catalog. - /// Keys are stable managed identities. Re-supply this map on cold resume. + /// Connected Copilot Connector MCP endpoints supplied from the service catalog. + /// Keys are reported through when headers need refreshing. + /// Re-supply the connected set on cold resume. /// [Experimental(Diagnostics.Experimental)] - public IDictionary? ManagedMcpServers { get; set; } + public IDictionary? ConnectorMcpServers { get; set; } /// /// Controls how MCP OAuth tokens are stored for this session. @@ -4023,9 +4040,13 @@ protected SessionConfigBase(SessionConfigBase? other) public Func>? OnMcpAuthRequest { get; set; } /// - /// Supplies short-lived HTTP headers for managed MCP servers. Returning - /// reports no headers; exceptions are sent to the - /// runtime as explicit credential-broker errors. + /// Supplies short-lived headers authorizing this client to call the exact + /// Copilot Connectors service MCP endpoint configured through + /// . This is normally the selected account's + /// GitHub bearer authorization. The SDK host does not supply downstream provider + /// tokens for Outlook, Slack, or other connected services; the Copilot Connectors + /// service owns those tokens. Returning reports no headers; + /// exceptions are sent to the runtime as explicit refresh errors. /// [Experimental(Diagnostics.Experimental)] [JsonIgnore] diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index 8c1a40ff95..49684aaf74 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -1217,6 +1217,49 @@ public async Task McpAuth_Handler_Exception_Cancels_Pending_Request() Assert.Equal("cancelled", request.Params.GetProperty("result").GetProperty("kind").GetString()); } + [Fact] + public async Task ConnectorMcpServers_Map_To_Managed_Wire_Field_On_Create_And_Resume() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var connectorMcpServers = new Dictionary + { + ["github"] = new() + { + DisplayName = "GitHub", + Url = "https://example.com/mcp", + Tools = ["issues"], + Timeout = 30_000, + AuthorizationCacheTtlMs = 60_000 + } + }; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + ConnectorMcpServers = connectorMcpServers + }); + AssertConnectorMcpServers(Assert.Single(server.Requests, request => request.Method == "session.create")); + + server.ClearRequests(); + await using var resumed = await client.ResumeSessionAsync("connector-resume", new ResumeSessionConfig + { + ConnectorMcpServers = connectorMcpServers + }); + AssertConnectorMcpServers(Assert.Single(server.Requests, request => request.Method == "session.resume")); + + static void AssertConnectorMcpServers(RpcRequestRecord request) + { + var root = request.Params; + var serverConfig = root.GetProperty("managedMcpServers").GetProperty("github"); + Assert.Equal("GitHub", serverConfig.GetProperty("displayName").GetString()); + Assert.Equal("https://example.com/mcp", serverConfig.GetProperty("url").GetString()); + Assert.Equal("issues", serverConfig.GetProperty("tools")[0].GetString()); + Assert.Equal(30_000, serverConfig.GetProperty("timeout").GetInt64()); + Assert.Equal(60_000, serverConfig.GetProperty("headersRefreshTtlMs").GetInt64()); + Assert.False(root.TryGetProperty("connectorMcpServers", out _)); + } + } + [Theory] [InlineData(null)] [InlineData(5000L)] @@ -1227,18 +1270,18 @@ public async Task McpHeadersRefresh_Handler_Sends_Headers_With_Optional_Ttl(long await using var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, - ManagedMcpServers = new Dictionary + ConnectorMcpServers = new Dictionary { ["github"] = new() { DisplayName = "GitHub", Url = "https://example.com/mcp", - HeadersRefreshTtlMs = 60_000 + AuthorizationCacheTtlMs = 60_000 } }, OnMcpHeadersRefresh = context => { - Assert.Equal("managed-session-name", context.ServerName); + Assert.Equal("github", context.ServerKey); Assert.Equal("https://example.com/mcp", context.ServerUrl); Assert.Equal(McpHeadersRefreshRequiredReason.Startup, context.Reason); return Task.FromResult(new() @@ -1254,7 +1297,7 @@ public async Task McpHeadersRefresh_Handler_Sends_Headers_With_Optional_Ttl(long Data = new McpHeadersRefreshRequiredData { RequestId = "mcp-refresh-1", - ServerName = "managed-session-name", + ServerName = "github", ServerUrl = "https://example.com/mcp", Reason = McpHeadersRefreshRequiredReason.Startup } @@ -1277,7 +1320,7 @@ public async Task McpHeadersRefresh_Handler_Sends_Headers_With_Optional_Ttl(long } [Fact] - public async Task McpHeadersRefresh_Handler_Exception_Sends_Explicit_Broker_Error() + public async Task McpHeadersRefresh_Handler_Exception_Sends_Explicit_Refresh_Error() { await using var server = await FakeCopilotServer.StartAsync(); await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); @@ -1292,20 +1335,21 @@ public async Task McpHeadersRefresh_Handler_Exception_Sends_Explicit_Broker_Erro Data = new McpHeadersRefreshRequiredData { RequestId = "mcp-refresh-error", - ServerName = "GitHub", + ServerName = "github", ServerUrl = "https://example.com/mcp", Reason = McpHeadersRefreshRequiredReason.AuthFailed } }); var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); + Assert.Equal("mcp-refresh-error", request.Params.GetProperty("requestId").GetString()); var result = request.Params.GetProperty("result"); Assert.Equal("error", result.GetProperty("kind").GetString()); Assert.Equal("credential revoked", result.GetProperty("message").GetString()); } [Fact] - public async Task McpHeadersRefresh_Handler_Cancellation_Sends_Explicit_Broker_Error() + public async Task McpHeadersRefresh_Handler_Cancellation_Sends_Explicit_Refresh_Error() { await using var server = await FakeCopilotServer.StartAsync(); await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); @@ -1320,13 +1364,14 @@ public async Task McpHeadersRefresh_Handler_Cancellation_Sends_Explicit_Broker_E Data = new McpHeadersRefreshRequiredData { RequestId = "mcp-refresh-cancelled", - ServerName = "GitHub", + ServerName = "github", ServerUrl = "https://example.com/mcp", Reason = McpHeadersRefreshRequiredReason.AuthFailed } }); var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); + Assert.Equal("mcp-refresh-cancelled", request.Params.GetProperty("requestId").GetString()); var result = request.Params.GetProperty("result"); Assert.Equal("error", result.GetProperty("kind").GetString()); Assert.False(string.IsNullOrWhiteSpace(result.GetProperty("message").GetString())); diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 1a1a37d95f..613201bf6c 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -97,10 +97,10 @@ public void SandboxConfig_RoundtripsAllowBypass_AndOmitsWhenAbsent() } [Fact] - public void SessionRequests_Serialize_ManagedMcpServers_On_Create_And_Resume() + public void SessionRequests_Serialize_ConnectorMcpServers_Using_Managed_Wire_Field() { var options = GetSerializerOptions(); - var managed = new Dictionary + var connectorServers = new Dictionary { ["github"] = new() { @@ -108,9 +108,15 @@ public void SessionRequests_Serialize_ManagedMcpServers_On_Create_And_Resume() Url = "https://example.com/mcp", Tools = ["issues"], Timeout = 30_000, - HeadersRefreshTtlMs = 60_000 + AuthorizationCacheTtlMs = 60_000 } }; + var mapper = typeof(CopilotClient).GetMethod( + "ToManagedMcpServers", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + Assert.NotNull(mapper); + var managed = mapper!.Invoke(null, [connectorServers]); + Assert.NotNull(managed); foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" }) { @@ -127,7 +133,10 @@ public void SessionRequests_Serialize_ManagedMcpServers_On_Create_And_Resume() Assert.Equal("GitHub", server.GetProperty("displayName").GetString()); Assert.Equal("https://example.com/mcp", server.GetProperty("url").GetString()); + Assert.Equal("issues", server.GetProperty("tools")[0].GetString()); + Assert.Equal(30_000, server.GetProperty("timeout").GetInt64()); Assert.Equal(60_000, server.GetProperty("headersRefreshTtlMs").GetInt64()); + Assert.False(document.RootElement.TryGetProperty("connectorMcpServers", out _)); } } diff --git a/go/README.md b/go/README.md index 2eb2c720fc..72e8b3263e 100644 --- a/go/README.md +++ b/go/README.md @@ -278,6 +278,43 @@ Initial acquisition runs during session creation or resume. Cancellation, provid - `UI() *SessionUI` - Interactive UI API for elicitation dialogs - `Capabilities() SessionCapabilities` - Host capabilities (e.g. elicitation support) +#### Connector MCP session controls (Experimental) + +Supply connected Copilot Connector MCP endpoints from the service catalog +through `SessionConfig.ConnectorMCPServers` when creating a session, or through +`ResumeSessionConfig.ConnectorMCPServers` when cold-resuming one. + +`OnMCPHeadersRefresh` supplies short-lived authorization from the SDK client to +the Copilot Connectors service for the exact service-advertised MCP endpoint, +normally using the selected account's GitHub bearer token. It does not supply +Outlook, Slack, or other downstream service tokens; the Connector service owns +and applies those tokens. + +`ConnectorMCPServerConfig.AuthorizationCacheTTLMS` sets the maximum time the +runtime may reuse Connector service authorization. `MCPHeadersRefreshResult.TTLMS` +reports the remaining authorization lifetime for a particular refresh. + +Correct same-session Connect and Disconnect reconciliation is not available +through the current public runtime contract. Until the runtime promotes its +authoritative replacement/removal operations, create a session or cold-resume +one with the new `ConnectorMCPServers` set. + +The public typed `session.RPC.MCP` API can inspect and control the current +runtime state, but these operations do not replace the authoritative Connector +set: + +| Capability | Go API | +| --- | --- | +| List servers and connection status | `session.RPC.MCP.List(ctx)` | +| Enable or disable a server | `session.RPC.MCP.Enable(ctx, &rpc.MCPEnableRequest{ServerName: key})` / `Disable` | +| List connected-server tools | `session.RPC.MCP.ListTools(ctx, &rpc.MCPListToolsRequest{ServerName: key})` | +| Observe status changes | `session.On(...)` with `SessionMCPServersLoadedData`, `SessionMCPServerStatusChangedData`, `SessionMCPServerNeedsReconnectData`, and `SessionMCPServerRemovedData` | + +The generated request fields named `ServerName` carry the stable Connector +server key, not the display name or Connector service ID. The host application +remains responsible for Connector service catalog and consent HTTP calls and +for all browser or UI flows. + #### Message source Set `MessageOptions.Source` to `copilot.MessageSourceAgent(id)` for messages from diff --git a/go/client.go b/go/client.go index e27ffa6213..5cbb7c2413 100644 --- a/go/client.go +++ b/go/client.go @@ -902,7 +902,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.WorkingDirectory = config.WorkingDirectory req.AdditionalDirectories = config.AdditionalDirectories req.MCPServers = config.MCPServers - req.ManagedMCPServers = config.ManagedMCPServers + req.ManagedMCPServers = connectorMCPServersToWire(config.ConnectorMCPServers) req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL req.EnvValueMode = "direct" @@ -1335,7 +1335,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } req.ContinuePendingWork = config.ContinuePendingWork req.MCPServers = config.MCPServers - req.ManagedMCPServers = config.ManagedMCPServers + req.ManagedMCPServers = connectorMCPServersToWire(config.ConnectorMCPServers) req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL req.EnvValueMode = "direct" diff --git a/go/client_test.go b/go/client_test.go index ca0a292fde..663802fd68 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -2711,17 +2711,19 @@ func TestClient_MCPAuthInterestRegistration(t *testing.T) { }) } -func TestClient_ManagedMCPCreateAndResume(t *testing.T) { +func TestClient_ConnectorMCPServersMapToInternalWireOnCreateAndResume(t *testing.T) { client, requests, cleanup := newInMemoryClient(t) defer cleanup() ttl := int64(60_000) - managed := map[string]ManagedMCPServerConfig{ + timeout := int64(30_000) + connectors := map[string]ConnectorMCPServerConfig{ "github": { - DisplayName: "GitHub", - URL: "https://example.com/mcp", - Tools: []string{"issues"}, - HeadersRefreshTtlMs: &ttl, + DisplayName: "GitHub", + URL: "https://example.com/mcp", + Tools: []string{"issues"}, + Timeout: &timeout, + AuthorizationCacheTTLMS: &ttl, }, } handler := func(MCPHeadersRefreshRequest, MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { @@ -2729,7 +2731,7 @@ func TestClient_ManagedMCPCreateAndResume(t *testing.T) { } session, err := client.CreateSession(t.Context(), &SessionConfig{ - ManagedMCPServers: managed, + ConnectorMCPServers: connectors, OnMCPHeadersRefresh: handler, }) if err != nil { @@ -2741,21 +2743,28 @@ func TestClient_ManagedMCPCreateAndResume(t *testing.T) { if !ok { t.Fatal("missing session.create request") } - managedWire, ok := create.Params["managedMcpServers"].(map[string]any) - if !ok { - t.Fatalf("unexpected managedMcpServers: %#v", create.Params["managedMcpServers"]) + expectedWire := map[string]any{ + "github": map[string]any{ + "displayName": "GitHub", + "headersRefreshTtlMs": float64(60_000), + "timeout": float64(30_000), + "tools": []any{"issues"}, + "url": "https://example.com/mcp", + }, } - github, ok := managedWire["github"].(map[string]any) - if !ok || github["displayName"] != "GitHub" || github["headersRefreshTtlMs"] != float64(60_000) { - t.Fatalf("unexpected managed server payload: %#v", managedWire) + if got := create.Params["managedMcpServers"]; !reflect.DeepEqual(got, expectedWire) { + t.Fatalf("unexpected managedMcpServers wire payload: %#v", got) + } + if _, ok := create.Params["catalogMcpServers"]; ok { + t.Fatalf("catalogMcpServers must not appear on the wire: %#v", create.Params) } if !hasEventInterest(requests.snapshot(), "mcp.headers_refresh_required") { - t.Fatal("missing managed MCP headers refresh interest on create") + t.Fatal("missing MCP headers refresh interest on create") } requests.clear() resumed, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{ - ManagedMCPServers: managed, + ConnectorMCPServers: connectors, OnMCPHeadersRefresh: handler, }) if err != nil { @@ -2764,11 +2773,17 @@ func TestClient_ManagedMCPCreateAndResume(t *testing.T) { defer resumed.Disconnect() resume, ok := findRequest(requests.snapshot(), "session.resume") - if !ok || resume.Params["managedMcpServers"] == nil { - t.Fatalf("managed MCP servers missing from resume: %#v", resume.Params) + if !ok { + t.Fatal("missing session.resume request") + } + if got := resume.Params["managedMcpServers"]; !reflect.DeepEqual(got, expectedWire) { + t.Fatalf("unexpected managedMcpServers wire payload on resume: %#v", got) + } + if _, ok := resume.Params["catalogMcpServers"]; ok { + t.Fatalf("catalogMcpServers must not appear on the wire: %#v", resume.Params) } if !hasEventInterest(requests.snapshot(), "mcp.headers_refresh_required") { - t.Fatal("missing managed MCP headers refresh interest on resume") + t.Fatal("missing MCP headers refresh interest on resume") } } @@ -3046,6 +3061,12 @@ func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeW result = map[string]any{"sessionId": sessionID, "workspacePath": nil} case "session.eventLog.registerInterest": result = map[string]any{"id": "interest-1"} + case "session.mcp.list": + result = map[string]any{"servers": []any{}} + case "session.mcp.enable", "session.mcp.disable": + result = map[string]any{} + case "session.mcp.listTools": + result = map[string]any{"tools": []any{}} case "session.options.update": result = map[string]any{"success": true} case "session.skills.reload": diff --git a/go/session.go b/go/session.go index 27f6ad142f..43d20e9c89 100644 --- a/go/session.go +++ b/go/session.go @@ -1549,9 +1549,9 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { s.executeMCPHeadersRefreshAndRespond( d.RequestID, MCPHeadersRefreshRequest{ - ServerName: d.ServerName, - ServerURL: d.ServerURL, - Reason: d.Reason, + ServerKey: d.ServerName, + ServerURL: d.ServerURL, + Reason: d.Reason, }, handler, ) @@ -1822,9 +1822,9 @@ func (s *Session) executeMCPHeadersRefreshAndRespond( switch { case err != nil: log.Printf( - "MCP headers refresh failed: session_id=%s server_name=%s error=%v", + "MCP headers refresh failed: session_id=%s server_key=%s error=%v", s.SessionID, - request.ServerName, + request.ServerKey, err, ) wireResult = rpc.MCPHeadersHandlePendingHeadersRefreshRequestError{Message: err.Error()} diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go index c64b6ce598..2ab437922f 100644 --- a/go/session_event_serialization_test.go +++ b/go/session_event_serialization_test.go @@ -14,6 +14,79 @@ var _ SessionEventData = (*rpc.UserMessageData)(nil) var _ rpc.EmbeddedTextResourceContents = EmbeddedTextResourceContents{} var _ EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents{} +func TestConnectorMCPStatusEventsUsePublicSessionEventTypes(t *testing.T) { + tests := []struct { + eventType string + data map[string]any + assert func(*testing.T, SessionEventData) + }{ + { + eventType: "session.mcp_servers_loaded", + data: map[string]any{ + "servers": []any{ + map[string]any{"name": "calendar", "status": "connected"}, + }, + }, + assert: func(t *testing.T, data SessionEventData) { + loaded, ok := data.(*SessionMCPServersLoadedData) + if !ok || len(loaded.Servers) != 1 || loaded.Servers[0].Name != "calendar" { + t.Fatalf("unexpected loaded event data: %#v", data) + } + }, + }, + { + eventType: "session.mcp_server_status_changed", + data: map[string]any{"serverName": "calendar", "status": "connected"}, + assert: func(t *testing.T, data SessionEventData) { + status, ok := data.(*SessionMCPServerStatusChangedData) + if !ok || status.ServerName != "calendar" || status.Status != MCPServerStatusConnected { + t.Fatalf("unexpected status event data: %#v", data) + } + }, + }, + { + eventType: "session.mcp_server_needs_reconnect", + data: map[string]any{"serverName": "calendar"}, + assert: func(t *testing.T, data SessionEventData) { + reconnect, ok := data.(*SessionMCPServerNeedsReconnectData) + if !ok || reconnect.ServerName != "calendar" { + t.Fatalf("unexpected reconnect event data: %#v", data) + } + }, + }, + { + eventType: "session.mcp_server_removed", + data: map[string]any{"serverName": "calendar"}, + assert: func(t *testing.T, data SessionEventData) { + removed, ok := data.(*SessionMCPServerRemovedData) + if !ok || removed.ServerName != "calendar" { + t.Fatalf("unexpected removed event data: %#v", data) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.eventType, func(t *testing.T) { + wire, err := json.Marshal(map[string]any{ + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-09-16T00:00:00Z", + "parentId": nil, + "type": tt.eventType, + "data": tt.data, + }) + if err != nil { + t.Fatal(err) + } + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + tt.assert(t, event.Data) + }) + } +} + func TestSessionEventAutoTier(t *testing.T) { for _, eventType := range []string{"session.start", "session.resume"} { for _, tier := range []AutoTier{"", AutoTierEfficiency, AutoTierBalance, AutoTierIntelligence} { diff --git a/go/session_test.go b/go/session_test.go index 416927c59f..2973b9a8c6 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -328,7 +328,7 @@ func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { } } -func TestSession_MCPHeadersRefreshResponses(t *testing.T) { +func TestSession_MCPHeadersRefreshHandlerResponses(t *testing.T) { ttl := int64(5_000) tests := []struct { name string @@ -361,7 +361,7 @@ func TestSession_MCPHeadersRefreshResponses(t *testing.T) { expected: map[string]any{"kind": "none"}, }, { - name: "broker error", + name: "handler error", err: errors.New("credential revoked"), expected: map[string]any{"kind": "error", "message": "credential revoked"}, }, @@ -424,7 +424,9 @@ func TestSession_MCPHeadersRefreshResponses(t *testing.T) { } session.registerMCPHeadersRefreshHandler( func(request MCPHeadersRefreshRequest, invocation MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { - if request.ServerName != "GitHub" || request.Reason != MCPHeadersRefreshRequiredReasonStartup { + if request.ServerKey != "connector-key" || + request.ServerURL != "https://example.com/mcp" || + request.Reason != MCPHeadersRefreshRequiredReasonStartup { t.Fatalf("unexpected request: %#v", request) } if invocation.SessionID != "session-1" { @@ -436,7 +438,7 @@ func TestSession_MCPHeadersRefreshResponses(t *testing.T) { session.handleBroadcastEvent(SessionEvent{ Data: &MCPHeadersRefreshRequiredData{ RequestID: "refresh-1", - ServerName: "GitHub", + ServerName: "connector-key", ServerURL: "https://example.com/mcp", Reason: MCPHeadersRefreshRequiredReasonStartup, }, @@ -459,7 +461,7 @@ func TestSession_MCPHeadersRefreshResponses(t *testing.T) { } } -func TestSession_MCPHeadersRefreshPanicDoesNotStopDispatch(t *testing.T) { +func TestSession_MCPHeadersRefreshHandlerPanicDoesNotStopDispatch(t *testing.T) { stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() defer stdinR.Close() @@ -519,20 +521,20 @@ func TestSession_MCPHeadersRefreshPanicDoesNotStopDispatch(t *testing.T) { session.registerMCPHeadersRefreshHandler( func(MCPHeadersRefreshRequest, MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { if calls.Add(1) == 1 { - panic("credential broker crashed") + panic("header provider crashed") } return &MCPHeadersRefreshResult{Headers: map[string]string{"Authorization": "refreshed"}}, nil }, ) for i, expected := range []map[string]any{ - {"kind": "error", "message": "MCP headers refresh handler panic: credential broker crashed"}, + {"kind": "error", "message": "MCP headers refresh handler panic: header provider crashed"}, {"kind": "headers", "headers": map[string]any{"Authorization": "refreshed"}}, } { requestID := fmt.Sprintf("refresh-%d", i) session.dispatchEvent(SessionEvent{Data: &MCPHeadersRefreshRequiredData{ RequestID: requestID, - ServerName: "GitHub", + ServerName: "github", ServerURL: "https://example.com/mcp", Reason: MCPHeadersRefreshRequiredReasonStartup, }}) @@ -556,6 +558,90 @@ func TestSession_MCPHeadersRefreshPanicDoesNotStopDispatch(t *testing.T) { } } +func TestSession_PublicMCPRPCIsAvailable(t *testing.T) { + client, requests, cleanup := newInMemoryClient(t) + defer cleanup() + session, err := client.CreateSession(t.Context(), &SessionConfig{}) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + if session.RPC == nil || session.RPC.MCP == nil { + t.Fatal("created session does not expose the typed MCP RPC API") + } + requests.clear() + + list, err := session.RPC.MCP.List(t.Context()) + if err != nil { + t.Fatalf("MCP.List failed: %v", err) + } + if len(list.Servers) != 0 { + t.Fatalf("unexpected MCP list: %#v", list) + } + if _, err := session.RPC.MCP.Enable( + t.Context(), + &rpc.MCPEnableRequest{ServerName: "calendar"}, + ); err != nil { + t.Fatalf("MCP.Enable failed: %v", err) + } + if _, err := session.RPC.MCP.Disable( + t.Context(), + &rpc.MCPDisableRequest{ServerName: "calendar"}, + ); err != nil { + t.Fatalf("MCP.Disable failed: %v", err) + } + tools, err := session.RPC.MCP.ListTools( + t.Context(), + &rpc.MCPListToolsRequest{ServerName: "calendar"}, + ) + if err != nil { + t.Fatalf("MCP.ListTools failed: %v", err) + } + if len(tools.Tools) != 0 { + t.Fatalf("unexpected MCP tools: %#v", tools) + } + + snapshot := requests.snapshot() + expected := []struct { + method string + params map[string]any + }{ + { + method: "session.mcp.list", + params: map[string]any{"sessionId": session.SessionID}, + }, + { + method: "session.mcp.enable", + params: map[string]any{ + "sessionId": session.SessionID, + "serverName": "calendar", + }, + }, + { + method: "session.mcp.disable", + params: map[string]any{ + "sessionId": session.SessionID, + "serverName": "calendar", + }, + }, + { + method: "session.mcp.listTools", + params: map[string]any{ + "sessionId": session.SessionID, + "serverName": "calendar", + }, + }, + } + if len(snapshot) != len(expected) { + t.Fatalf("unexpected requests: %#v", snapshot) + } + for i, want := range expected { + if snapshot[i].Method != want.method || !mapsEqual(snapshot[i].Params, want.params) { + t.Fatalf("request %d = %#v, want %s %#v", i, snapshot[i], want.method, want.params) + } + } +} + func mapsEqual(actual any, expected map[string]any) bool { actualJSON, err := json.Marshal(actual) if err != nil { diff --git a/go/types.go b/go/types.go index 5b24f70315..fdab010037 100644 --- a/go/types.go +++ b/go/types.go @@ -555,35 +555,78 @@ type MCPAuthInvocation struct { // MCPAuthHandler handles MCP OAuth requests from the runtime. type MCPAuthHandler func(request MCPAuthRequest, invocation MCPAuthInvocation) (*MCPAuthResult, error) -// ManagedMCPServerConfig is a non-secret hosted MCP server from a trusted -// managed catalog. Credentials are supplied only through [MCPHeadersRefreshHandler]. +// ConnectorMCPServerConfig describes a connected Copilot Connector MCP +// endpoint supplied from the service catalog. The containing map key is the +// stable Connector server key. Short-lived client authorization for the exact +// service-advertised endpoint is supplied only through +// [MCPHeadersRefreshHandler]; this configuration never contains Outlook, +// Slack, or other downstream service tokens. // -// Experimental: ManagedMCPServerConfig is part of an experimental API and may change. -type ManagedMCPServerConfig = rpc.ManagedMCPServerConfig +// Experimental: ConnectorMCPServerConfig is part of an experimental API and may change. +type ConnectorMCPServerConfig struct { + // DisplayName is the human-readable Connector display name. + DisplayName string `json:"displayName"` + // AuthorizationCacheTTLMS is the maximum time, in milliseconds, that the + // runtime may reuse Connector service authorization. + AuthorizationCacheTTLMS *int64 `json:"authorizationCacheTtlMs,omitempty"` + // Timeout is the timeout in milliseconds for tool discovery and tool calls. + Timeout *int64 `json:"timeout,omitempty"` + // Tools lists the tools to include. When omitted, all tools are included. + Tools []string `json:"tools,omitzero"` + // URL is the hosted MCP streamable HTTP endpoint. + URL string `json:"url"` +} + +func connectorMCPServersToWire(servers map[string]ConnectorMCPServerConfig) map[string]rpc.ManagedMCPServerConfig { + if servers == nil { + return nil + } + wire := make(map[string]rpc.ManagedMCPServerConfig, len(servers)) + for identity, server := range servers { + wire[identity] = rpc.ManagedMCPServerConfig{ + DisplayName: server.DisplayName, + HeadersRefreshTtlMs: server.AuthorizationCacheTTLMS, + Timeout: server.Timeout, + Tools: server.Tools, + URL: server.URL, + } + } + return wire +} -// MCPHeadersRefreshRequest identifies a managed MCP server whose short-lived -// HTTP headers need refreshing. +// MCPHeadersRefreshRequest identifies the exact connected Copilot Connector MCP +// endpoint whose short-lived client authorization headers need refreshing. type MCPHeadersRefreshRequest struct { - ServerName string - ServerURL string - Reason MCPHeadersRefreshRequiredReason + // ServerKey is the stable server identity used as the ConnectorMCPServers map key. + ServerKey string + // ServerURL is the hosted MCP streamable HTTP endpoint. + ServerURL string + // Reason describes why the runtime requested refreshed service authorization. + Reason MCPHeadersRefreshRequiredReason } -// MCPHeadersRefreshResult contains dynamic headers and their optional -// credential-bounded cache lifetime. A nil result means no headers are available. +// MCPHeadersRefreshResult contains dynamic service-authorization headers and +// their optional cache lifetime. A nil result means no headers are available. type MCPHeadersRefreshResult struct { + // Headers contains client-to-Copilot-Connectors service authorization headers. Headers map[string]string - TTLMS *int64 + // TTLMS is the remaining authorization lifetime in milliseconds. + TTLMS *int64 } -// MCPHeadersRefreshInvocation provides context for a managed MCP headers refresh. +// MCPHeadersRefreshInvocation provides context for a Copilot Connector service +// authorization refresh. type MCPHeadersRefreshInvocation struct { SessionID string } -// MCPHeadersRefreshHandler supplies short-lived HTTP headers for managed MCP -// servers. Returning an error sends an explicit broker error to the runtime. -// Handler panics are recovered and also sent as explicit broker errors. +// MCPHeadersRefreshHandler supplies short-lived authorization from the SDK +// client to the Copilot Connectors service for the exact endpoint in the +// request, normally using the selected account's GitHub bearer token. It does +// not supply Outlook, Slack, or other downstream service tokens; the Connector +// service owns and applies those tokens. Returning an error sends an explicit +// refresh error to the runtime. Handler panics are recovered and also sent as +// explicit refresh errors. type MCPHeadersRefreshHandler func(request MCPHeadersRefreshRequest, invocation MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) // UserInputRequest represents a request for user input from the agent @@ -1433,7 +1476,9 @@ type SessionConfig struct { // When provided, the SDK can satisfy MCP server OAuth requests with host-provided // token data or cancellation. OnMCPAuthRequest MCPAuthHandler - // OnMCPHeadersRefresh supplies short-lived HTTP headers for ManagedMCPServers. + // OnMCPHeadersRefresh supplies short-lived client-to-Copilot-Connectors + // service authorization when an endpoint in ConnectorMCPServers requests + // dynamic-header refresh. It does not supply downstream service tokens. OnMCPHeadersRefresh MCPHeadersRefreshHandler // GitHubTokenProvider acquires session-scoped GitHub tokens on demand. It // cannot be combined with GitHubToken. @@ -1523,9 +1568,10 @@ type SessionConfig struct { ModelCapabilities *rpc.ModelCapabilitiesOverride // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig - // ManagedMCPServers contains non-secret hosted MCP servers keyed by stable - // managed identity. Re-supply this map when cold-resuming a session. - ManagedMCPServers map[string]ManagedMCPServerConfig + // ConnectorMCPServers contains connected Copilot Connector MCP endpoints + // supplied from the service catalog. Keys are stable Connector server keys. + // Re-supply this map when cold-resuming a session. + ConnectorMCPServers map[string]ConnectorMCPServerConfig // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -2028,7 +2074,9 @@ type ResumeSessionConfig struct { // OnMCPAuthRequest is an optional handler for MCP OAuth requests from MCP servers. // See SessionConfig.OnMCPAuthRequest. OnMCPAuthRequest MCPAuthHandler - // OnMCPHeadersRefresh supplies short-lived HTTP headers for ManagedMCPServers. + // OnMCPHeadersRefresh handles client-to-Copilot-Connectors service + // authorization refresh requests, not downstream service authentication. + // See [SessionConfig.OnMCPHeadersRefresh]. OnMCPHeadersRefresh MCPHeadersRefreshHandler // OnUserInputRequest handles legacy question-and-answer requests from the agent // and enables the legacy ask_user tool. @@ -2091,9 +2139,10 @@ type ResumeSessionConfig struct { IncludeSubAgentStreamingEvents *bool // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig - // ManagedMCPServers contains non-secret hosted MCP servers keyed by stable - // managed identity. Re-supply this map when cold-resuming a session. - ManagedMCPServers map[string]ManagedMCPServerConfig + // ConnectorMCPServers re-supplies connected Copilot Connector MCP endpoints + // from the service catalog after an app restart. Keys are stable Connector + // server keys. + ConnectorMCPServers map[string]ConnectorMCPServerConfig // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -2691,7 +2740,7 @@ type createSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` + ManagedMCPServers map[string]rpc.ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` @@ -2804,7 +2853,7 @@ type resumeSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - ManagedMCPServers map[string]ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` + ManagedMCPServers map[string]rpc.ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` diff --git a/java/README.md b/java/README.md index af13c3ec16..04ac8b9b04 100644 --- a/java/README.md +++ b/java/README.md @@ -176,6 +176,43 @@ and `setExcludedTools(...)`, prefer the source-qualified filter form `DefaultAgentConfig.setExcludedTools(...)`, use `-` directly. +For experimental Copilot Connector integration, supply connected Connector MCP +endpoints from the service catalog through +`SessionConfig.setConnectorMcpServers(...)`. Re-supply them through +`ResumeSessionConfig.setConnectorMcpServers(...)` when cold-resuming. + +The MCP headers refresh handler supplies short-lived authorization from the SDK +client to the exact Copilot Connectors service endpoint advertised by the +service catalog. This is normally the selected account's GitHub bearer token. +It is not an Outlook, Slack, or other downstream provider credential; the +Connector service owns those provider tokens. + +`ConnectorMcpServerConfig.setAuthorizationCacheTtlMs(...)` sets the maximum +time the runtime may reuse that Connector service authorization. A refresh +handler result's `ttlMs` is the remaining authorization lifetime; it may further +shorten that cache period. + +Correct same-session reconciliation after Connector service Connect or +Disconnect operations is not available through the current public runtime +contract. Transient server lifecycle operations do not replace the +authoritative Connector configuration and may be undone by a reload. To apply a +changed connected set, create a new session or cold-resume with the updated +map passed to `setConnectorMcpServers(...)` until the runtime exposes supported +public replacement and removal operations. + +For servers already configured on a session, the public generated session RPC +API supports `list()` for status, `enable(...)`, `disable(...)`, +and `listTools(...)`. Access it through `session.getRpc().mcp`. These operations +do not add or remove Connector configurations. Generated RPC parameter and +event models call the stable Connector MCP server map key `serverName`; it is +not the display name or Connector service ID. + +Hosts can subscribe with `session.on(...)` to +`SessionMcpServersLoadedEvent`, `SessionMcpServerStatusChangedEvent`, +`SessionMcpServerRemovedEvent`, and `SessionMcpServerNeedsReconnectEvent`. +The SDK does not fetch the Connector service catalog, request consent, or +provide UI; application hosts own those responsibilities. + `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. `SessionConfig.setAskUserVariant(AskUserVariant.ELICITATION)` selects the diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 3d52d993f3..97fdfe3250 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -12,8 +12,6 @@ import type { JSONSchema7 } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; -import { applyManagedMcpSchemaOverlay } from "../../../scripts/codegen/managedMcpSchemaOverlay.js"; - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -863,12 +861,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { async function generateSessionEvents(schemaPath: string): Promise { console.log("\n📋 Generating session event classes..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing( - applyManagedMcpSchemaOverlay( - JSON.parse(schemaContent) as JSONSchema7, - path.basename(schemaPath) - ) - ); + const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent) as JSONSchema7); // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; @@ -1485,9 +1478,7 @@ function generateRpcClass( async function generateRpcTypes(schemaPath: string): Promise { console.log("\n🔌 Generating RPC types..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing( - applyManagedMcpSchemaOverlay(JSON.parse(schemaContent), path.basename(schemaPath)) - ) as Record & { + const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent)) as Record & { server?: Record; session?: Record; clientSession?: Record; @@ -2351,9 +2342,7 @@ async function generateRpcWrappers(schemaPath: string): Promise { console.log("\n🔧 Generating RPC wrapper classes..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing( - applyManagedMcpSchemaOverlay(JSON.parse(schemaContent), path.basename(schemaPath)) - ) as { + const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent)) as { server?: Record; session?: Record; clientSession?: Record; diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index 37059b8e0a..8a054aac7d 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -402,6 +402,16 @@ public SessionUiApi getUi() { *
{@code
      * var agents = session.getRpc().agent.list().get();
      * }
+ *

+ * For MCP servers already configured on the session, the public {@code mcp} + * namespace supports listing server status and tools, enabling or disabling + * servers. The generated parameter field named {@code serverName} takes the + * stable configuration map key. Status changes are available through + * {@link #on(Class, Consumer)} with + * {@link com.github.copilot.generated.SessionMcpServersLoadedEvent}, + * {@link com.github.copilot.generated.SessionMcpServerStatusChangedEvent}, + * {@link com.github.copilot.generated.SessionMcpServerRemovedEvent}, and + * {@link com.github.copilot.generated.SessionMcpServerNeedsReconnectEvent}. * * @return the session-scoped typed RPC client (never {@code null}) * @throws IllegalStateException @@ -2649,7 +2659,7 @@ public void close() { } } - // ===== Internal response types for agent API ===== + // ===== Internal wire types ===== @JsonIgnoreProperties(ignoreUnknown = true) record SessionDetachResponse(@JsonProperty("success") boolean success, @JsonProperty("error") String error) { diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index 9d1506d533..0e2683ccee 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -148,7 +148,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); - request.setManagedMcpServers(config.getManagedMcpServers()); + request.setConnectorMcpServers(config.getConnectorMcpServers()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl()); request.setCustomAgents(config.getCustomAgents()); @@ -305,7 +305,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); - request.setManagedMcpServers(config.getManagedMcpServers()); + request.setConnectorMcpServers(config.getConnectorMcpServers()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl()); request.setCustomAgents(config.getCustomAgents()); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java new file mode 100644 index 0000000000..55f57f39e3 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.github.copilot.generated.rpc.ManagedMcpServerConfig; + +/** + * Configuration for a connected Copilot Connector MCP endpoint supplied from + * the service catalog. + *

+ * Short-lived client-to-Copilot-Connectors service authorization must be + * supplied dynamically through {@link McpHeadersRefreshHandler} and must not be + * stored in this configuration. This is normally the selected account's GitHub + * bearer token for the exact service-advertised endpoint. It is not a + * downstream provider credential; the Connector service owns provider tokens + * for services such as Outlook or Slack. + *

+ * The containing map key is the stable Connector server key reported by + * {@link McpHeadersRefreshRequest#serverKey()}; {@link #getDisplayName()} is + * only the human-readable Connector label. + * + * @see SessionConfig#setConnectorMcpServers(Map) + * @see ResumeSessionConfig#setConnectorMcpServers(Map) + * @since 1.0.0 + */ +public final class ConnectorMcpServerConfig { + + private String displayName; + private String url; + private List tools; + private Long timeout; + private Long authorizationCacheTtlMs; + + /** + * Gets the human-readable Connector display name. + * + * @return the display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets the human-readable Connector display name. + * + * @param displayName + * the display name + * @return this config instance for method chaining + */ + public ConnectorMcpServerConfig setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Gets the Copilot Connector MCP streamable HTTP endpoint. + * + * @return the server URL + */ + public String getUrl() { + return url; + } + + /** + * Sets the Copilot Connector MCP streamable HTTP endpoint. + * + * @param url + * the server URL + * @return this config instance for method chaining + */ + public ConnectorMcpServerConfig setUrl(String url) { + this.url = url; + return this; + } + + /** + * Gets the tools to include from the server. + * + * @return the included tools, or {@code null} to include all tools + */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** + * Sets the tools to include from the server. + * + * @param tools + * the included tools, or {@code null} to include all tools + * @return this config instance for method chaining + */ + public ConnectorMcpServerConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the timeout for tool discovery and tool calls. + * + * @return the timeout in milliseconds, or {@code null} for the default + */ + public Long getTimeout() { + return timeout; + } + + /** + * Sets the timeout for tool discovery and tool calls. + * + * @param timeout + * the timeout in milliseconds, or {@code null} for the default + * @return this config instance for method chaining + */ + public ConnectorMcpServerConfig setTimeout(Long timeout) { + this.timeout = timeout; + return this; + } + + /** + * Gets the maximum time the runtime may reuse Connector service authorization. + * + * @return the maximum authorization cache lifetime in milliseconds, or + * {@code null} for the runtime default + */ + public Long getAuthorizationCacheTtlMs() { + return authorizationCacheTtlMs; + } + + /** + * Sets the maximum time the runtime may reuse Connector service authorization. + * + * @param authorizationCacheTtlMs + * the maximum authorization cache lifetime in milliseconds, or + * {@code null} for the runtime default + * @return this config instance for method chaining + */ + public ConnectorMcpServerConfig setAuthorizationCacheTtlMs(Long authorizationCacheTtlMs) { + this.authorizationCacheTtlMs = authorizationCacheTtlMs; + return this; + } + + ManagedMcpServerConfig toWire() { + return new ManagedMcpServerConfig(displayName, url, tools, timeout, authorizationCacheTtlMs); + } + + static ConnectorMcpServerConfig fromWire(ManagedMcpServerConfig wire) { + return new ConnectorMcpServerConfig().setDisplayName(wire.displayName()).setUrl(wire.url()) + .setTools(wire.tools()).setTimeout(wire.timeout()) + .setAuthorizationCacheTtlMs(wire.headersRefreshTtlMs()); + } + + static Map toWire(Map servers) { + if (servers == null) { + return null; + } + var wire = new LinkedHashMap(); + servers.forEach((name, server) -> wire.put(name, server == null ? null : server.toWire())); + return wire; + } + + static Map fromWire(Map servers) { + if (servers == null) { + return null; + } + var configs = new LinkedHashMap(); + servers.forEach((name, server) -> configs.put(name, server == null ? null : fromWire(server))); + return configs; + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index bae6a83722..f7c3e14072 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -598,16 +599,28 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } - /** Gets managed MCP servers. @return the managed servers map */ - public Map getManagedMcpServers() { - return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + /** + * Gets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog. + * + * @return the Connector MCP servers map + */ + @JsonIgnore + public Map getConnectorMcpServers() { + var servers = ConnectorMcpServerConfig.fromWire(managedMcpServers); + return servers == null ? null : Collections.unmodifiableMap(servers); } /** - * Sets managed MCP servers. @param managedMcpServers the managed servers map + * Sets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog. + * + * @param connectorMcpServers + * the Connector MCP servers map */ - public void setManagedMcpServers(Map managedMcpServers) { - this.managedMcpServers = managedMcpServers; + @JsonIgnore + public void setConnectorMcpServers(Map connectorMcpServers) { + this.managedMcpServers = ConnectorMcpServerConfig.toWire(connectorMcpServers); } /** Gets MCP OAuth token storage mode. @return the storage mode */ diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java index cf357b6bd0..9ac63400b4 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java @@ -7,17 +7,23 @@ import java.util.concurrent.CompletableFuture; /** - * Supplies dynamic headers for a host-managed MCP server. + * Supplies short-lived client-to-Copilot-Connectors service authorization + * headers for a connected Connector MCP endpoint. + *

+ * The authorization is normally the selected account's GitHub bearer token for + * the exact service-advertised endpoint. This handler does not supply + * downstream provider credentials; the Connector service owns those provider + * tokens. * * @since 1.0.0 */ @FunctionalInterface public interface McpHeadersRefreshHandler { /** - * Handles a dynamic-header refresh request. + * Handles a service-authorization header refresh request. * * @param request - * the managed MCP server details + * the connected Copilot Connector MCP endpoint details * @param invocation * the invocation context with session information * @return a future resolving to headers or an explicit no-result response diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java index f69b78aee2..e7c53c86fd 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java @@ -5,7 +5,7 @@ package com.github.copilot.rpc; /** - * Context for a managed MCP dynamic-header refresh invocation. + * Context for a Copilot Connector MCP dynamic-header refresh invocation. * * @since 1.0.0 */ diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java index 254a04376d..5c5c8a7f3a 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java @@ -7,15 +7,16 @@ import com.github.copilot.generated.McpHeadersRefreshRequiredReason; /** - * Request to refresh dynamic headers for a host-managed MCP server. + * Request to refresh short-lived client-to-Copilot-Connectors service + * authorization headers for a connected Connector MCP endpoint. * - * @param serverName - * display name of the managed MCP server + * @param serverKey + * stable server key used in the Connector MCP server map * @param serverUrl - * URL of the managed MCP server + * exact service-advertised Connector MCP endpoint URL * @param reason * reason the headers must be refreshed * @since 1.0.0 */ -public record McpHeadersRefreshRequest(String serverName, String serverUrl, McpHeadersRefreshRequiredReason reason) { +public record McpHeadersRefreshRequest(String serverKey, String serverUrl, McpHeadersRefreshRequiredReason reason) { } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java index a86737b76e..fed2f83a06 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java @@ -7,12 +7,14 @@ import java.util.Map; /** - * Dynamic headers returned for a host-managed MCP server. + * Short-lived client-to-Copilot-Connectors service authorization headers + * returned for a connected Connector MCP endpoint. * * @param headers - * headers to use, or {@code null} for no result + * headers that authorize the client-to-service request, or + * {@code null} for no result * @param ttlMs - * optional cache lifetime in milliseconds + * optional remaining authorization lifetime in milliseconds * @since 1.0.0 */ public record McpHeadersRefreshResult(Map headers, Long ttlMs) { @@ -28,7 +30,7 @@ public record McpHeadersRefreshResult(Map headers, Long ttlMs) { * Creates a result with headers and no handler-specific cache lifetime. * * @param headers - * headers to use + * headers that authorize the client-to-service request * @return a headers result */ public static McpHeadersRefreshResult withHeaders(Map headers) { @@ -36,12 +38,12 @@ public static McpHeadersRefreshResult withHeaders(Map headers) { } /** - * Creates a result with headers and a cache lifetime. + * Creates a result with headers and the remaining authorization lifetime. * * @param headers - * headers to use + * headers that authorize the client-to-service request * @param ttlMs - * cache lifetime in milliseconds + * remaining authorization lifetime in milliseconds * @return a headers result */ public static McpHeadersRefreshResult withHeaders(Map headers, long ttlMs) { diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index d4e7b6703e..01cd3c5f41 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -16,7 +16,6 @@ import com.github.copilot.CopilotExperimental; import com.github.copilot.generated.SessionEvent; -import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -87,7 +86,7 @@ public class ResumeSessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; - private Map managedMcpServers; + private Map connectorMcpServers; private String mcpOAuthTokenStorage; private String authClientIdMetadataUrl; private List customAgents; @@ -869,7 +868,12 @@ public ResumeSessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) } /** - * Gets the managed MCP dynamic-header refresh handler. + * Gets the dynamic-header refresh handler for connected Copilot Connector MCP + * endpoints. + *

+ * The handler supplies short-lived client-to-Copilot-Connectors service + * authorization, not downstream provider credentials. The Connector service + * owns downstream provider tokens. * * @return the handler, or {@code null} if not set */ @@ -879,7 +883,12 @@ public McpHeadersRefreshHandler getOnMcpHeadersRefresh() { } /** - * Sets the managed MCP dynamic-header refresh handler. + * Sets the dynamic-header refresh handler for connected Copilot Connector MCP + * endpoints. + *

+ * The handler supplies short-lived client-to-Copilot-Connectors service + * authorization, not downstream provider credentials. The Connector service + * owns downstream provider tokens. * * @param onMcpHeadersRefresh * the handler @@ -1423,25 +1432,29 @@ public ResumeSessionConfig setMcpServers(Map mcpServers } /** - * Gets host-managed HTTP MCP server configurations. + * Gets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog. * - * @return the managed MCP servers map + * @return the Connector MCP servers map */ @CopilotExperimental - public Map getManagedMcpServers() { - return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + public Map getConnectorMcpServers() { + return connectorMcpServers == null ? null : Collections.unmodifiableMap(connectorMcpServers); } /** - * Sets host-managed HTTP MCP server configurations for a cold resume. + * Sets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog for a cold resume. * - * @param managedMcpServers - * non-secret server configurations keyed by stable managed identity + * @param connectorMcpServers + * non-secret Connector endpoint configurations keyed by stable + * server key; each key is reported as + * {@link McpHeadersRefreshRequest#serverKey()} * @return this config instance for method chaining */ @CopilotExperimental - public ResumeSessionConfig setManagedMcpServers(Map managedMcpServers) { - this.managedMcpServers = managedMcpServers; + public ResumeSessionConfig setConnectorMcpServers(Map connectorMcpServers) { + this.connectorMcpServers = connectorMcpServers; return this; } @@ -2191,8 +2204,8 @@ public ResumeSessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; - copy.managedMcpServers = this.managedMcpServers != null - ? new java.util.HashMap<>(this.managedMcpServers) + copy.connectorMcpServers = this.connectorMcpServers != null + ? new java.util.HashMap<>(this.connectorMcpServers) : null; copy.mcpOAuthTokenStorage = this.mcpOAuthTokenStorage; copy.authClientIdMetadataUrl = this.authClientIdMetadataUrl; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 5da38b1eb6..af0b9bae70 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -824,16 +825,28 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } - /** Gets managed MCP servers. @return the managed servers map */ - public Map getManagedMcpServers() { - return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + /** + * Gets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog. + * + * @return the Connector MCP servers map + */ + @JsonIgnore + public Map getConnectorMcpServers() { + var servers = ConnectorMcpServerConfig.fromWire(managedMcpServers); + return servers == null ? null : Collections.unmodifiableMap(servers); } /** - * Sets managed MCP servers. @param managedMcpServers the managed servers map + * Sets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog. + * + * @param connectorMcpServers + * the Connector MCP servers map */ - public void setManagedMcpServers(Map managedMcpServers) { - this.managedMcpServers = managedMcpServers; + @JsonIgnore + public void setConnectorMcpServers(Map connectorMcpServers) { + this.managedMcpServers = ConnectorMcpServerConfig.toWire(connectorMcpServers); } /** Gets MCP OAuth token storage mode. @return the storage mode */ diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index 04567c04ef..b3bf863516 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -16,7 +16,6 @@ import com.github.copilot.CopilotExperimental; import com.github.copilot.generated.SessionEvent; -import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -76,7 +75,7 @@ public class SessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; - private Map managedMcpServers; + private Map connectorMcpServers; private String mcpOAuthTokenStorage; private String authClientIdMetadataUrl; private List customAgents; @@ -913,7 +912,12 @@ public SessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) { } /** - * Gets the managed MCP dynamic-header refresh handler. + * Gets the dynamic-header refresh handler for connected Copilot Connector MCP + * endpoints. + *

+ * The handler supplies short-lived client-to-Copilot-Connectors service + * authorization, not downstream provider credentials. The Connector service + * owns downstream provider tokens. * * @return the handler, or {@code null} if not set */ @@ -923,7 +927,12 @@ public McpHeadersRefreshHandler getOnMcpHeadersRefresh() { } /** - * Sets the managed MCP dynamic-header refresh handler. + * Sets the dynamic-header refresh handler for connected Copilot Connector MCP + * endpoints. + *

+ * The handler supplies short-lived client-to-Copilot-Connectors service + * authorization, not downstream provider credentials. The Connector service + * owns downstream provider tokens. * * @param onMcpHeadersRefresh * the handler @@ -1076,25 +1085,29 @@ public SessionConfig setMcpServers(Map mcpServers) { } /** - * Gets host-managed HTTP MCP server configurations. + * Gets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog. * - * @return the managed MCP servers map + * @return the Connector MCP servers map */ @CopilotExperimental - public Map getManagedMcpServers() { - return managedMcpServers == null ? null : Collections.unmodifiableMap(managedMcpServers); + public Map getConnectorMcpServers() { + return connectorMcpServers == null ? null : Collections.unmodifiableMap(connectorMcpServers); } /** - * Sets host-managed HTTP MCP server configurations. + * Sets connected Copilot Connector MCP endpoint configurations supplied from + * the service catalog. * - * @param managedMcpServers - * non-secret server configurations keyed by stable managed identity + * @param connectorMcpServers + * non-secret Connector endpoint configurations keyed by stable + * server key; each key is reported as + * {@link McpHeadersRefreshRequest#serverKey()} * @return this config instance for method chaining */ @CopilotExperimental - public SessionConfig setManagedMcpServers(Map managedMcpServers) { - this.managedMcpServers = managedMcpServers; + public SessionConfig setConnectorMcpServers(Map connectorMcpServers) { + this.connectorMcpServers = connectorMcpServers; return this; } @@ -2320,8 +2333,8 @@ public SessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; - copy.managedMcpServers = this.managedMcpServers != null - ? new java.util.HashMap<>(this.managedMcpServers) + copy.connectorMcpServers = this.connectorMcpServers != null + ? new java.util.HashMap<>(this.connectorMcpServers) : null; copy.mcpOAuthTokenStorage = this.mcpOAuthTokenStorage; copy.authClientIdMetadataUrl = this.authClientIdMetadataUrl; diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java index 2722913cc9..802fc392b4 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java @@ -15,6 +15,7 @@ import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.util.ElementFilter; import java.net.URI; @@ -97,6 +98,42 @@ public ExperimentalType getIt(ExperimentalType value) { } """; + private static final String CONNECTOR_MCP_PUBLIC_PRIMITIVES_CONSUMER = """ + package consumer; + + import java.util.Map; + + import com.github.copilot.CopilotSession; + import com.github.copilot.generated.SessionMcpServerNeedsReconnectEvent; + import com.github.copilot.generated.SessionMcpServerRemovedEvent; + import com.github.copilot.generated.SessionMcpServerStatusChangedEvent; + import com.github.copilot.generated.SessionMcpServersLoadedEvent; + import com.github.copilot.generated.rpc.SessionMcpDisableParams; + import com.github.copilot.generated.rpc.SessionMcpEnableParams; + import com.github.copilot.generated.rpc.SessionMcpListToolsParams; + import com.github.copilot.rpc.ConnectorMcpServerConfig; + import com.github.copilot.rpc.ResumeSessionConfig; + import com.github.copilot.rpc.SessionConfig; + + public class Consumer { + public void compose(CopilotSession session, ConnectorMcpServerConfig server) { + String serverKey = "calendar"; + server.setAuthorizationCacheTtlMs(60_000L); + var servers = Map.of(serverKey, server); + new SessionConfig().setConnectorMcpServers(servers); + new ResumeSessionConfig().setConnectorMcpServers(servers); + session.getRpc().mcp.list(); + session.getRpc().mcp.enable(new SessionMcpEnableParams(null, serverKey)); + session.getRpc().mcp.disable(new SessionMcpDisableParams(null, serverKey)); + session.getRpc().mcp.listTools(new SessionMcpListToolsParams(null, serverKey)); + session.on(SessionMcpServersLoadedEvent.class, event -> {}); + session.on(SessionMcpServerStatusChangedEvent.class, event -> {}); + session.on(SessionMcpServerRemovedEvent.class, event -> {}); + session.on(SessionMcpServerNeedsReconnectEvent.class, event -> {}); + } + } + """; + @Test void failsByDefault_whenFieldOrSignatureUsesExperimentalType() { DiagnosticCollector diagnostics = compile( @@ -158,7 +195,7 @@ void passes_whenOptInFlagIsProvided() { } @Test - void managedMcpServerAccessorsAreExperimentalForCreateAndResume() { + void connectorMcpServerApisArePublicAndExperimentalForCreateAndResume() { var diagnostics = new DiagnosticCollector(); var checkedMethods = new AtomicInteger(); var task = ToolProvider.getSystemJavaCompiler().getTask(null, null, diagnostics, @@ -178,12 +215,31 @@ public SourceVersion getSupportedSourceVersion() { @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { + var connectorConfig = processingEnv.getElementUtils() + .getTypeElement("com.github.copilot.rpc.ConnectorMcpServerConfig"); + assertNotNull(connectorConfig); + assertTrue(connectorConfig.getModifiers().contains(Modifier.PUBLIC)); + var connectorMethodNames = ElementFilter.methodsIn(connectorConfig.getEnclosedElements()).stream() + .map(method -> method.getSimpleName().toString()) + .collect(java.util.stream.Collectors.toSet()); + assertTrue(connectorMethodNames.contains("getAuthorizationCacheTtlMs")); + assertTrue(connectorMethodNames.contains("setAuthorizationCacheTtlMs")); + assertFalse(connectorMethodNames.contains("getHeadersRefreshTtlMs")); + assertFalse(connectorMethodNames.contains("setHeadersRefreshTtlMs")); + for (String configName : List.of("SessionConfig", "ResumeSessionConfig")) { var config = processingEnv.getElementUtils() .getTypeElement("com.github.copilot.rpc." + configName); assertNotNull(config); + var methodNames = ElementFilter.methodsIn(config.getEnclosedElements()).stream() + .map(method -> method.getSimpleName().toString()) + .collect(java.util.stream.Collectors.toSet()); + assertFalse(methodNames.contains("getManagedMcpServers")); + assertFalse(methodNames.contains("setManagedMcpServers")); + assertFalse(methodNames.contains("getCatalogMcpServers")); + assertFalse(methodNames.contains("setCatalogMcpServers")); for (var method : ElementFilter.methodsIn(config.getEnclosedElements())) { - if (Set.of("getManagedMcpServers", "setManagedMcpServers") + if (Set.of("getConnectorMcpServers", "setConnectorMcpServers") .contains(method.getSimpleName().toString())) { assertNotNull(method.getAnnotation(CopilotExperimental.class), configName + "." + method.getSimpleName()); @@ -191,6 +247,7 @@ public boolean process(Set annotations, RoundEnvironment } } } + } return false; } @@ -200,6 +257,16 @@ public boolean process(Set annotations, RoundEnvironment assertEquals(4, checkedMethods.get()); } + @Test + void connectorMcpConfigurationComposesWithPublicSessionRpcAndStatusEvents() { + var diagnostics = compile( + List.of(inMemorySource("consumer.Consumer", CONNECTOR_MCP_PUBLIC_PRIMITIVES_CONSUMER)), + List.of("-Acopilot.experimental.allowed=true")); + + boolean hasError = diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR); + assertFalse(hasError, "Expected public MCP primitives to compile, got: " + diagnostics.getDiagnostics()); + } + private DiagnosticCollector compile(List sources, List extraOptions) { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); diff --git a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java index 4c4cf95b63..1dabffb8f7 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java @@ -178,7 +178,7 @@ void resumeSessionRegistersMcpAuthInterestOnlyWhenHandlerConfigured() throws Exc } @Test - void createAndResumeRegisterManagedMcpHeadersRefreshInterest() throws Exception { + void createAndResumeRegisterMcpHeadersRefreshInterest() throws Exception { try (var server = new RecordingRuntime(); var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { try (var session = client @@ -199,7 +199,7 @@ void createAndResumeRegisterManagedMcpHeadersRefreshInterest() throws Exception server.clearRequests(); - try (var session = client.resumeSession("managed-session", + try (var session = client.resumeSession("connector-session", new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setOnMcpHeadersRefresh((request, invocation) -> java.util.concurrent.CompletableFuture .completedFuture(McpHeadersRefreshResult.none()))) diff --git a/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java b/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java index a99779fd06..b10df77914 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java @@ -68,7 +68,7 @@ void resumeConfigPreservesRefreshHandlerWithoutSerializingIt() { @Test void createHandlerDispatchesHeadersWithoutTtl() { McpHeadersRefreshHandler handler = (request, invocation) -> { - assertEquals("managed-server", request.serverName()); + assertEquals("connector-server-key", request.serverKey()); assertEquals("https://mcp.example.com", request.serverUrl()); assertEquals(McpHeadersRefreshRequiredReason.STARTUP, request.reason()); assertEquals("session-1", invocation.getSessionId()); @@ -156,7 +156,7 @@ private JsonRpcClient rpc() { private JsonNode dispatchAndCapture(CopilotSession session, JsonRpcClient rpc) { var event = new McpHeadersRefreshRequiredEvent(); event.setData(new McpHeadersRefreshRequiredEvent.McpHeadersRefreshRequiredEventData("headers-request", - "managed-server", "https://mcp.example.com", McpHeadersRefreshRequiredReason.STARTUP)); + "connector-server-key", "https://mcp.example.com", McpHeadersRefreshRequiredReason.STARTUP)); session.dispatchEvent(event); diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index a7e61cffb1..1e1461ea8b 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -12,10 +12,10 @@ import org.junit.jupiter.api.Test; -import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; import com.github.copilot.rpc.AskUserVariant; import com.github.copilot.rpc.AutoModeSwitchResponse; +import com.github.copilot.rpc.ConnectorMcpServerConfig; import com.github.copilot.rpc.CloudSessionOptions; import com.github.copilot.rpc.CloudSessionRepository; import com.github.copilot.rpc.CopilotClientMode; @@ -1156,27 +1156,41 @@ void githubMcpToolConfigIsMappedAndSerializedForCreateAndResume() throws Excepti } @Test - void managedMcpServersAreMappedAndSerializedForCreateAndColdResume() throws Exception { - var managedServer = new ManagedMcpServerConfig("Managed catalog server", "https://mcp.example.com", - List.of("search"), 15_000L, 300_000L); - var managedServers = Map.of("catalog-server", managedServer); + void connectorMcpServersUsePublicFacadeAndExactInternalWireName() throws Exception { + var connectorServer = new ConnectorMcpServerConfig().setDisplayName("Connector server") + .setUrl("https://mcp.example.com").setTools(List.of("search")).setTimeout(15_000L) + .setAuthorizationCacheTtlMs(300_000L); + var connectorServers = Map.of("connector-server", connectorServer); var createRequest = SessionRequestBuilder - .buildCreateRequest(new SessionConfig().setManagedMcpServers(managedServers), "session-1"); + .buildCreateRequest(new SessionConfig().setConnectorMcpServers(connectorServers), "session-1"); var resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", - new ResumeSessionConfig().setManagedMcpServers(managedServers)); + new ResumeSessionConfig().setConnectorMcpServers(connectorServers)); - assertEquals(managedServers, createRequest.getManagedMcpServers()); - assertEquals(managedServers, resumeRequest.getManagedMcpServers()); + assertEquals("Connector server", + createRequest.getConnectorMcpServers().get("connector-server").getDisplayName()); + assertEquals(300_000L, + createRequest.getConnectorMcpServers().get("connector-server").getAuthorizationCacheTtlMs()); + assertEquals("Connector server", + resumeRequest.getConnectorMcpServers().get("connector-server").getDisplayName()); var mapper = JsonRpcClient.getObjectMapper(); var createJson = mapper.readTree(mapper.writeValueAsBytes(createRequest)); var resumeJson = mapper.readTree(mapper.writeValueAsBytes(resumeRequest)); - assertEquals("Managed catalog server", - createJson.path("managedMcpServers").path("catalog-server").path("displayName").asText()); - assertEquals(300_000L, - createJson.path("managedMcpServers").path("catalog-server").path("headersRefreshTtlMs").asLong()); - assertEquals("https://mcp.example.com", - resumeJson.path("managedMcpServers").path("catalog-server").path("url").asText()); + var expectedWire = mapper.readTree(""" + { + "connector-server": { + "displayName": "Connector server", + "url": "https://mcp.example.com", + "tools": ["search"], + "timeout": 15000, + "headersRefreshTtlMs": 300000 + } + } + """); + assertEquals(expectedWire, createJson.path("managedMcpServers")); + assertEquals(expectedWire, resumeJson.path("managedMcpServers")); + assertFalse(createJson.has("connectorMcpServers")); + assertFalse(resumeJson.has("connectorMcpServers")); } } diff --git a/nodejs/README.md b/nodejs/README.md index 7effb81e95..ba3ad03c73 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -156,6 +156,8 @@ Create a new conversation session. - `enableSessionStore?: boolean` - Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled. - `gitHubTokenProvider?: GitHubTokenProvider` - Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `gitHubToken`. - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. +- `connectorMcpServers?: Record` - Connected Copilot Connector MCP endpoints advertised by the Connector service. Re-supply on cold resume. +- `onMcpHeadersRefresh?: McpHeadersRefreshHandler` - Supplies short-lived headers for Connector MCP endpoints. Register this even when the initial connected set is empty if the host supports connecting a Connector during the session. - `onPermissionRequest?: PermissionHandler` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `approveAll` approves requests when managed settings are disabled and throws when `enableManagedSettings` is true. Custom handlers can inspect `managedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `onUserInputRequest?: UserInputHandler` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `askUserVariant?: "legacy" | "elicitation"` - Selects the model-facing `ask_user` tool shape when creating or cold-resuming a session. Defaults to `"legacy"`; use `"elicitation"` with `onElicitationRequest`. @@ -369,6 +371,17 @@ Abort the currently processing message in this session. Get all events/messages from this session. +Use `session.rpc.mcp.list()`, `enable()`, `disable()`, and `listTools()` with the +same stable server keys to build Connector status and management UI. Subscribe +to the MCP loaded, removed, reconnect, and status-change session events for live +updates. + +The current public runtime contract cannot replace the authoritative Connector +set on a running session. After a service Connect, Reconnect, or Disconnect, +create or cold-resume a session with the updated `connectorMcpServers` map. +Generic MCP `stopServer()` is temporary and an MCP reload can restore the +originally configured server. + ##### `disconnect(): Promise` Disconnect the session and free resources. Session data on disk is preserved for later resumption. diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 88fc4ceb67..90cec6391f 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -36,6 +36,7 @@ import type { GitHubTelemetryNotification, GitHubTokenAcquireRequest, GitHubTokenAcquireResult, + ManagedMcpServerConfig as WireManagedMcpServerConfig, OpenCanvasInstance, SessionUpdateOptionsParams, TaskKind, @@ -56,6 +57,7 @@ import type { CopilotClientInfo, CopilotClientMode, CopilotClientOptions, + ConnectorMcpServerConfig, CustomAgentConfig, ExitPlanModeRequest, ExitPlanModeResult, @@ -248,6 +250,26 @@ function toWireMcpServers( ); } +function toWireConnectorMcpServers( + servers: Record | undefined +): Record | undefined { + if (servers === undefined) { + return undefined; + } + return Object.fromEntries( + Object.entries(servers).map(([serverKey, server]) => [ + serverKey, + { + displayName: server.displayName, + url: server.url, + tools: server.tools, + timeout: server.timeout, + headersRefreshTtlMs: server.authorizationCacheTtlMs, + }, + ]) + ); +} + /** * Convert custom agent configs, transforming nested mcpServers from * public API format (workingDirectory) to wire format (cwd). @@ -1706,7 +1728,7 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), - managedMcpServers: config.managedMcpServers, + managedMcpServers: toWireConnectorMcpServers(config.connectorMcpServers), mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, authClientIdMetadataUrl: config.authClientIdMetadataUrl, envValueMode: "direct", @@ -1996,7 +2018,7 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), - managedMcpServers: config.managedMcpServers, + managedMcpServers: toWireConnectorMcpServers(config.connectorMcpServers), mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, authClientIdMetadataUrl: config.authClientIdMetadataUrl, envValueMode: "direct", diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 6470891f8f..9113debb8d 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -113,7 +113,7 @@ export type { MCPStdioServerConfig, MCPHTTPServerConfig, MCPServerConfig, - ManagedMcpServerConfig, + ConnectorMcpServerConfig, McpHeadersRefreshHandler, McpHeadersRefreshRequest, McpHeadersRefreshResult, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 9a1063b914..42a67c8916 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -1089,7 +1089,7 @@ export class CopilotSession { return; } void this._executeMcpHeadersRefreshAndRespond(requestId, { - serverName, + serverKey: serverName, serverUrl, reason, }); @@ -1349,7 +1349,7 @@ export class CopilotSession { } /** - * Executes a managed MCP headers refresh handler and sends the result via RPC. + * Executes an MCP headers refresh handler and sends the result via RPC. * @internal */ private async _executeMcpHeadersRefreshAndRespond( @@ -1374,7 +1374,7 @@ export class CopilotSession { } } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn(`MCP headers refresh failed for '${request.serverName}': ${message}`); + console.warn(`MCP headers refresh failed for '${request.serverKey}': ${message}`); result = { kind: "error", message }; } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index bf89e3adcc..89d2ff1e77 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -26,7 +26,6 @@ import type { GitHubTokenAcquireRequest, GitHubTokenAcquireResult, GitHubTelemetryNotification, - ManagedMcpServerConfig, ModelBillingTokenPrices, OpenCanvasInstance, RemoteSessionMode, @@ -35,7 +34,19 @@ import type { import type { ToolSet } from "./toolSet.js"; export type { RemoteSessionMode } from "./generated/rpc.js"; export type { CurrentToolMetadata } from "./generated/rpc.js"; -export type { ManagedMcpServerConfig } from "./generated/rpc.js"; +/** Non-secret hosted MCP endpoint advertised for a connected Copilot Connector. */ +export interface ConnectorMcpServerConfig { + /** Human-readable Connector name. */ + displayName: string; + /** Service-advertised MCP endpoint. */ + url: string; + /** Tools to expose. Defaults to all tools when omitted. */ + tools?: string[]; + /** Timeout in milliseconds for discovery and tool calls. */ + timeout?: number; + /** Maximum time the runtime may reuse Connector service authorization. */ + authorizationCacheTtlMs?: number; +} export type { GitHubTokenAcquireReason, GitHubTokenAcquireResult, @@ -2083,28 +2094,28 @@ export type McpAuthHandler = ( | undefined | Promise; -/** MCP server whose short-lived HTTP headers must be refreshed by the host. */ +/** Connector MCP server whose short-lived HTTP headers must be refreshed. */ export interface McpHeadersRefreshRequest { - /** Human-readable managed catalog display name. */ - serverName: string; + /** Stable server identity used as the connectorMcpServers map key. */ + serverKey: string; /** Hosted MCP streamable HTTP endpoint. */ serverUrl: string; /** Why the runtime invalidated or requested dynamic headers. */ reason: McpHeadersRefreshRequiredReason; } -/** Dynamic headers and their optional credential-bounded cache lifetime. */ +/** Connector service authorization headers and their optional cache lifetime. */ export interface McpHeadersRefreshResult { headers: Record; ttlMs?: number; } /** - * Callback invoked when a managed MCP server needs fresh HTTP headers. + * Callback invoked when a Connector MCP server needs fresh HTTP headers. * * Return a bare header map or a structured result with `ttlMs`. Returning * `undefined` reports that no headers are available. Throwing reports an - * explicit credential-broker error to the runtime. + * explicit Connector authorization error to the runtime. */ export type McpHeadersRefreshHandler = ( request: McpHeadersRefreshRequest, @@ -2657,7 +2668,7 @@ export interface SessionConfigBase { onMcpAuthRequest?: McpAuthHandler; /** - * Supplies short-lived HTTP headers for managed MCP servers. + * Supplies short-lived HTTP headers for Connector MCP servers. * Re-register this handler when cold-resuming a session. */ onMcpHeadersRefresh?: McpHeadersRefreshHandler; @@ -2794,13 +2805,13 @@ export interface SessionConfigBase { mcpServers?: Record; /** - * Non-secret hosted MCP servers injected from a trusted managed catalog. - * Keys are stable managed identities. Credentials must be supplied through - * {@link onMcpHeadersRefresh}, never stored in this configuration. + * Non-secret hosted MCP endpoints advertised for connected Copilot Connectors. + * Keys are stable Connector server identities. Service authorization must be + * supplied through {@link onMcpHeadersRefresh}, never stored in this configuration. * * @experimental */ - managedMcpServers?: Record; + connectorMcpServers?: Record; /** * Custom agent configurations for the session. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index c293f9c6be..140c2d94f7 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -375,7 +375,7 @@ describe("CopilotClient", () => { ttlMs: 5_000, }, }, - ])("responds to managed MCP header refresh with $name", async ({ response, expected }) => { + ])("responds to remote MCP header refresh with $name", async ({ response, expected }) => { const sendRequest = vi.fn(async () => ({ success: true })); const session = new CopilotSession( "session-1", @@ -386,7 +386,7 @@ describe("CopilotClient", () => { ); await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { - serverName: "Managed GitHub", + serverKey: "github", serverUrl: "https://example.com/mcp", reason: "startup", }); @@ -401,7 +401,7 @@ describe("CopilotClient", () => { ); }); - it("reports no managed MCP headers when the handler returns undefined", async () => { + it("reports no dynamic MCP headers when the handler returns undefined", async () => { const sendRequest = vi.fn(async () => ({ success: true })); const session = new CopilotSession( "session-1", @@ -412,7 +412,7 @@ describe("CopilotClient", () => { ); await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { - serverName: "Managed GitHub", + serverKey: "github", serverUrl: "https://example.com/mcp", reason: "ttl-expired", }); @@ -427,7 +427,7 @@ describe("CopilotClient", () => { ); }); - it("propagates managed MCP credential broker failures explicitly", async () => { + it("propagates MCP credential broker failures explicitly", async () => { const sendRequest = vi.fn(async () => ({ success: true })); const warning = vi.spyOn(console, "warn").mockImplementation(() => {}); const session = new CopilotSession( @@ -443,7 +443,7 @@ describe("CopilotClient", () => { ); await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { - serverName: "Managed GitHub", + serverKey: "github", serverUrl: "https://example.com/mcp", reason: "auth-failed", }); @@ -459,7 +459,7 @@ describe("CopilotClient", () => { warning.mockRestore(); }); - it("forwards managed MCP servers and refresh interest on create and resume", async () => { + it("maps Connector MCP servers to the internal wire contract on create and resume", async () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => stopClient(client)); @@ -475,29 +475,37 @@ describe("CopilotClient", () => { } throw new Error(`Unexpected method: ${method}`); }); - const managedMcpServers = { + const connectorMcpServers = { github: { displayName: "GitHub", url: "https://api.example.com/mcp", tools: ["issues"], timeout: 30_000, - headersRefreshTtlMs: 60_000, + authorizationCacheTtlMs: 60_000, }, }; const onMcpHeadersRefresh = () => ({ Authorization: "Bearer short-lived" }); const session = await client.createSession({ - managedMcpServers, + connectorMcpServers, onMcpHeadersRefresh, }); await client.resumeSession(session.sessionId, { - managedMcpServers, + connectorMcpServers, onMcpHeadersRefresh, }); for (const method of ["session.create", "session.resume"]) { expect(spy.mock.calls.find(([called]) => called === method)![1]).toMatchObject({ - managedMcpServers, + managedMcpServers: { + github: { + displayName: "GitHub", + url: "https://api.example.com/mcp", + tools: ["issues"], + timeout: 30_000, + headersRefreshTtlMs: 60_000, + }, + }, }); } expect(spy).toHaveBeenCalledTimes(4); diff --git a/nodejs/test/e2e/mcp_oauth.e2e.test.ts b/nodejs/test/e2e/mcp_oauth.e2e.test.ts index ab68afc2ef..590fb480c4 100644 --- a/nodejs/test/e2e/mcp_oauth.e2e.test.ts +++ b/nodejs/test/e2e/mcp_oauth.e2e.test.ts @@ -7,7 +7,12 @@ import { dirname, resolve } from "node:path"; import { createInterface } from "node:readline"; import { fileURLToPath } from "node:url"; import { describe, expect, it, onTestFinished } from "vitest"; -import type { CopilotSession, MCPServerConfig, McpAuthRequest } from "../../src/index.js"; +import type { + CopilotSession, + MCPServerConfig, + McpAuthRequest, + McpHeadersRefreshRequest, +} from "../../src/index.js"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { stopChildProcess, waitForCondition } from "./harness/sdkTestHelper.js"; @@ -27,6 +32,8 @@ describe("MCP OAuth host auth", async () => { env: { COPILOT_MCP_APPS: "true", MCP_APPS: "true", + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "MANAGED_MCP_SERVERS", + COPILOT_EXP_COPILOT_CLI_MANAGED_MCP_SERVERS: "true", }, }, }); @@ -123,6 +130,49 @@ describe("MCP OAuth host auth", async () => { ).toBe(true); }); + it( + "should project a connected Connector endpoint at startup", + { timeout: 180_000 }, + async () => { + const oauthServer = await startOAuthMcpServer(); + const serverKey = "connector-calendar"; + const connectorServer = { + displayName: "Calendar", + url: `${oauthServer.url}/mcp`, + authorizationCacheTtlMs: 0, + }; + const refreshRequests: McpHeadersRefreshRequest[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + connectorMcpServers: { [serverKey]: connectorServer }, + onMcpHeadersRefresh: (request) => { + refreshRequests.push(request); + return { Authorization: ["Bearer", EXPECTED_TOKEN].join(" ") }; + }, + }); + onTestFinished(() => disconnectSession(session)); + + await waitForMcpServerStatus(session, serverKey); + expect((await session.rpc.mcp.listTools({ serverName: serverKey })).tools).toEqual( + expect.arrayContaining([expect.objectContaining({ name: "whoami" })]) + ); + expect(refreshRequests[0]).toEqual({ + serverKey, + serverUrl: `${oauthServer.url}/mcp`, + reason: "startup", + }); + + expect(refreshRequests.length).toBeGreaterThanOrEqual(1); + expect( + refreshRequests.every( + (request) => + request.serverKey === serverKey && + request.serverUrl === `${oauthServer.url}/mcp` + ) + ).toBe(true); + } + ); + it( "should resolve pending MCP OAuth request with direct RPC", { timeout: 120_000 }, diff --git a/nodejs/test/shared-codegen.test.ts b/nodejs/test/shared-codegen.test.ts index 015358e431..a34d910dda 100644 --- a/nodejs/test/shared-codegen.test.ts +++ b/nodejs/test/shared-codegen.test.ts @@ -14,7 +14,6 @@ import { isIntegerSchemaBoundedToInt32, rewriteSharedDefinitionReferences, } from "../../scripts/codegen/utils.ts"; -import { applyManagedMcpSchemaOverlay } from "../../scripts/codegen/managedMcpSchemaOverlay.ts"; describe("shared schema definition codegen utilities", () => { it("detects integer schemas bounded to the 32-bit signed range", () => { @@ -64,185 +63,19 @@ describe("shared schema definition codegen utilities", () => { ).toBe(false); }); - describe("managed MCP schema overlay", () => { - async function loadPinnedApiSchema(): Promise<{ - definitions: Record; - }> { - return JSON.parse(await readFile(await getApiSchemaPath(), "utf8")) as { - definitions: Record; - }; - } - - it("accepts the pinned API schema", async () => { - const schema = await loadPinnedApiSchema(); - - applyManagedMcpSchemaOverlay(schema, "api.schema.json"); - - expect(schema.definitions).toHaveProperty("ManagedMcpServerConfig"); - expect( - (schema.definitions.SessionOpenOptions as JSONSchema7).properties - ).toHaveProperty("managedMcpServers"); - expect((schema.definitions.McpServer as JSONSchema7).properties).toHaveProperty( - "displayName" - ); - expect((schema.definitions.McpServerSource as JSONSchema7).enum).toContain("managed"); - expect( - (schema.definitions.McpHeadersHandlePendingHeadersRefreshRequest as JSONSchema7) - .anyOf - ).toHaveLength(3); - }); - - it("accepts the pinned session-event schema", async () => { - const schema = JSON.parse( - await readFile(await getSessionEventsSchemaPath(), "utf8") - ) as { definitions: Record }; - - applyManagedMcpSchemaOverlay(schema, "session-events.schema.json"); - - expect(schema.definitions.McpServerSource.enum).toContain("managed"); - expect(schema.definitions.McpServersLoadedServer.properties).toHaveProperty( - "displayName" - ); - }); - - it("rejects an unknown upstream shape instead of masking it", async () => { - const schema = await loadPinnedApiSchema(); - const refresh = schema.definitions - .McpHeadersHandlePendingHeadersRefreshRequest as JSONSchema7; - const headersVariant = refresh.anyOf?.[0] as JSONSchema7; - headersVariant.required = [...(headersVariant.required ?? []), "futureField"]; - const before = structuredClone(schema); - - expect(() => applyManagedMcpSchemaOverlay(schema, "api.schema.json")).toThrow( - "unknown upstream shape" - ); - expect(schema).toEqual(before); - }); - - it("rejects a missing legacy node instead of recreating it", async () => { - const schema = await loadPinnedApiSchema(); - delete schema.definitions.McpServerSource; - - expect(() => applyManagedMcpSchemaOverlay(schema, "api.schema.json")).toThrow( - "expected an upstream node" - ); - }); - - const contracts = [ - { - fileName: "api.schema.json", - nodes: [ - { definition: "ManagedMcpServerConfig", overlayKey: "ManagedMcpServerConfig" }, - { - definition: "McpHeadersHandlePendingHeadersRefreshRequest", - overlayKey: "McpHeadersHandlePendingHeadersRefreshRequest", - }, - { definition: "McpServerSource", overlayKey: "McpServerSource" }, - { - definition: "SessionOpenOptions", - property: "managedMcpServers", - overlayKey: "managedMcpServers", - }, - { - definition: "McpServer", - property: "displayName", - overlayKey: "mcpServerDisplayName", - }, - ], - }, - { - fileName: "session-events.schema.json", - nodes: [ - { definition: "McpServerSource", overlayKey: "McpServerSource" }, - { - definition: "McpServersLoadedServer", - property: "displayName", - overlayKey: "mcpServersLoadedDisplayName", - }, - ], - }, - ]; - - for (const { fileName, nodes } of contracts) { - async function fixture(targetMask: number) { - const overlay = JSON.parse( - await readFile( - new URL( - "../../scripts/codegen/managed-mcp-schema-overlay.json", - import.meta.url - ), - "utf8" - ) - ) as Record> & { - legacy: Record>; - }; - const schema: { definitions: Record } = { - definitions: { Unrelated: { type: "string" } }, - }; - for (const [index, { definition, property, overlayKey }] of nodes.entries()) { - const parent = property - ? (schema.definitions[definition] = { properties: {} }).properties! - : schema.definitions; - const value = - targetMask & (1 << index) - ? overlay[fileName][overlayKey] - : overlay.legacy[fileName][overlayKey]; - if (value !== undefined) { - parent[property ?? definition] = value; - } - } - return schema; - } - - const targetMask = (1 << nodes.length) - 1; - - it(`upgrades the complete legacy ${fileName} contract and is idempotent`, async () => { - const schema = await fixture(0); - const target = await fixture(targetMask); - - expect(applyManagedMcpSchemaOverlay(schema, fileName)).toEqual(target); - expect(applyManagedMcpSchemaOverlay(schema, fileName)).toEqual(target); - }); - - it(`accepts the complete upstream ${fileName} target without mutation`, async () => { - const schema = await fixture(targetMask); - const before = structuredClone(schema); - - expect(applyManagedMcpSchemaOverlay(schema, fileName)).toBe(schema); - expect(schema).toEqual(before); - }); - - it.each(Array.from({ length: targetMask - 1 }, (_, index) => index + 1))( - `rejects partial ${fileName} contract %i without mutation`, - async (mask) => { - const schema = await fixture(mask); - const before = structuredClone(schema); - - expect(() => applyManagedMcpSchemaOverlay(schema, fileName)).toThrow( - "unknown upstream shape or partial contract" - ); - expect(schema).toEqual(before); - } - ); - - it.each(nodes)( - `rejects missing ${fileName} target node $overlayKey without mutation`, - async ({ definition, property }) => { - const schema = await fixture(targetMask); - if (property) { - delete schema.definitions[definition].properties![property]; - } else { - delete schema.definitions[definition]; - } - const before = structuredClone(schema); + it("pins schemas containing the catalog MCP wire contract", async () => { + const api = JSON.parse(await readFile(await getApiSchemaPath(), "utf8")) as { + definitions: Record; + }; + const events = JSON.parse(await readFile(await getSessionEventsSchemaPath(), "utf8")) as { + definitions: Record; + }; - expect(() => applyManagedMcpSchemaOverlay(schema, fileName)).toThrow( - "Managed MCP schema overlay" - ); - expect(schema).toEqual(before); - } - ); - } + expect(api.definitions).toHaveProperty("ManagedMcpServerConfig"); + expect(api.definitions.SessionOpenOptions.properties).toHaveProperty("managedMcpServers"); + expect(api.definitions.McpHeadersHandlePendingHeadersRefreshRequest.anyOf).toHaveLength(3); + expect(events.definitions.McpServerSource.enum).toContain("managed"); + expect(events.definitions.McpServersLoadedServer.properties).toHaveProperty("displayName"); }); it("extracts non-empty enum value descriptions from schema extensions", () => { diff --git a/python/README.md b/python/README.md index 3b7d937c44..feb50bd22a 100644 --- a/python/README.md +++ b/python/README.md @@ -313,6 +313,8 @@ These are passed as keyword arguments to `create_session()`: - `system_message` (SystemMessageConfig): System message configuration - `streaming` (bool): Enable streaming delta events - `provider` (ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. +- `connector_mcp_servers` (dict[str, ConnectorMCPServerConfig]): Connected Copilot Connector MCP endpoints advertised by the Connector service. Re-supply on cold resume. +- `on_mcp_headers_refresh` (callable): Supplies short-lived headers for Connector MCP endpoints. Register this even when the initial connected set is empty if the host supports connecting a Connector during the session. - `infinite_sessions` (InfiniteSessionConfig): Automatic context compaction configuration - `working_directory` (str | None): Working directory for the session (default: runtime process working directory). - `enable_session_store` (bool): Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled. @@ -338,6 +340,19 @@ Initial acquisition runs during session creation or resume. Cancellation, provid - `available_tools` / `excluded_tools` / `default_agent.excluded_tools` / custom-agent `tools`: MCP tools registered from `mcp_servers` are exposed to the runtime as `-`. For `available_tools` and `excluded_tools`, prefer `ToolSet().add_mcp("-")` or the raw `mcp:-` form. For custom-agent `tools` and `default_agent.excluded_tools`, use `-` directly. +**Connector session integration (experimental):** + +Use `session.rpc.mcp.list()`, `enable()`, `disable()`, and `list_tools()` with +the same stable server keys to build Connector status and management UI. +Subscribe to the MCP loaded, removed, reconnect, and status-change session +events for live updates. + +The current public runtime contract cannot replace the authoritative Connector +set on a running session. After a service Connect, Reconnect, or Disconnect, +create or cold-resume a session with the updated `connector_mcp_servers` map. +Generic MCP `stop_server()` is temporary and an MCP reload can restore the +originally configured server. + **Session Lifecycle Methods:** ```python diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index b6184a0ef8..d41ce6e929 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -127,6 +127,7 @@ BearerTokenProvider, CommandContext, CommandDefinition, + ConnectorMCPServerConfig, CopilotSession, CreateSessionFsHandler, ElicitationContext, @@ -143,7 +144,6 @@ InfiniteSessionConfig, InputOptions, LargeToolOutputConfig, - ManagedMCPServerConfig, McpAuthContext, McpAuthHandler, McpAuthRequest, @@ -328,7 +328,7 @@ "McpHeadersRefreshHandler", "McpHeadersRefreshRequest", "McpHeadersRefreshResult", - "ManagedMCPServerConfig", + "ConnectorMCPServerConfig", "ManagedSettings", "ManagedSettingsPermissions", "MessageSource", diff --git a/python/copilot/client.py b/python/copilot/client.py index e3f3ae9a85..cc3546b22b 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -95,6 +95,7 @@ AutoTier, BearerTokenProvider, CommandDefinition, + ConnectorMCPServerConfig, ContextTier, CopilotSession, CreateSessionFsHandler, @@ -105,7 +106,6 @@ GitHubMcpToolConfig, InfiniteSessionConfig, LargeToolOutputConfig, - ManagedMCPServerConfig, McpAuthHandler, McpHeadersRefreshHandler, MCPServerConfig, @@ -124,6 +124,7 @@ ToolSearchConfig, UserInputHandler, _capabilities_to_dict, + _connector_mcp_servers_to_wire, _PermissionHandlerFn, ) from .session_fs_provider import SessionFsProvider, create_session_fs_adapter @@ -440,26 +441,6 @@ def _mcp_servers_to_wire( return wire -def _managed_mcp_servers_to_wire( - servers: dict[str, ManagedMCPServerConfig], -) -> dict[str, Any]: - """Convert managed MCP configuration keys to the JSON-RPC wire casing.""" - wire: dict[str, Any] = {} - for name, config in servers.items(): - server: dict[str, Any] = { - "displayName": config["display_name"], - "url": config["url"], - } - if "tools" in config: - server["tools"] = config["tools"] - if "timeout" in config: - server["timeout"] = config["timeout"] - if "headers_refresh_ttl_ms" in config: - server["headersRefreshTtlMs"] = config["headers_refresh_ttl_ms"] - wire[name] = server - return wire - - def _large_output_to_wire(config: Mapping[str, Any]) -> dict[str, Any]: """Convert a ``LargeToolOutputConfig`` mapping to wire format.""" wire: dict[str, Any] = {} @@ -2315,7 +2296,7 @@ async def create_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, - managed_mcp_servers: dict[str, ManagedMCPServerConfig] | None = None, + connector_mcp_servers: dict[str, ConnectorMCPServerConfig] | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, auth_client_id_metadata_url: str | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, @@ -2449,12 +2430,13 @@ async def create_session( ``agentId`` set). When False, only non-streaming sub-agent events and ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. - managed_mcp_servers: Non-secret hosted MCP servers from a trusted - managed catalog, keyed by stable managed identity. Re-supply on - cold resume. - on_mcp_headers_refresh: Supplies short-lived HTTP headers for managed - MCP servers. Exceptions are reported to the runtime as explicit - credential-broker errors. + connector_mcp_servers: Non-secret hosted MCP endpoints advertised + for connected Copilot Connectors, keyed by stable server identity. + Re-supply on cold resume. + on_mcp_headers_refresh: Supplies short-lived HTTP headers for + Connector MCP endpoints. These authorize the selected GitHub + account to the Connector service; the service owns downstream + provider credentials. Exceptions are explicit runtime errors. mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored. ``"persistent"`` uses the OS keychain (shared across sessions). ``"in-memory"`` stores tokens in memory (discarded on session end). @@ -2765,8 +2747,8 @@ async def create_session( # Add MCP servers configuration if provided if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) - if managed_mcp_servers is not None: - payload["managedMcpServers"] = _managed_mcp_servers_to_wire(managed_mcp_servers) + if connector_mcp_servers is not None: + payload["managedMcpServers"] = _connector_mcp_servers_to_wire(connector_mcp_servers) # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: @@ -3119,7 +3101,7 @@ async def resume_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, - managed_mcp_servers: dict[str, ManagedMCPServerConfig] | None = None, + connector_mcp_servers: dict[str, ConnectorMCPServerConfig] | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, auth_client_id_metadata_url: str | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, @@ -3256,10 +3238,11 @@ async def resume_session( ``agentId`` set). When False, only non-streaming sub-agent events and ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. - managed_mcp_servers: Non-secret hosted MCP servers from a trusted - managed catalog. Re-supply them on cold resume. - on_mcp_headers_refresh: Supplies short-lived HTTP headers for managed - MCP servers. + connector_mcp_servers: Non-secret hosted MCP endpoints advertised + for connected Copilot Connectors. Re-supply them on cold resume. + on_mcp_headers_refresh: Supplies short-lived HTTP headers for + Connector MCP endpoints. The Connector service owns downstream + provider credentials. mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored. ``"persistent"`` uses the OS keychain (shared across sessions). ``"in-memory"`` stores tokens in memory (discarded on session end). @@ -3565,8 +3548,8 @@ async def resume_session( # TODO: disable_resume is not a keyword arg yet; keeping for future use if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) - if managed_mcp_servers is not None: - payload["managedMcpServers"] = _managed_mcp_servers_to_wire(managed_mcp_servers) + if connector_mcp_servers is not None: + payload["managedMcpServers"] = _connector_mcp_servers_to_wire(connector_mcp_servers) # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: diff --git a/python/copilot/session.py b/python/copilot/session.py index 628e5a69a3..bc2c23b174 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -555,22 +555,22 @@ class McpAuthContext(TypedDict): class McpHeadersRefreshRequest(TypedDict): - """Managed MCP server whose short-lived HTTP headers need refreshing.""" + """Connector MCP server whose short-lived HTTP headers need refreshing.""" - server_name: str + server_key: str # Stable key from connector_mcp_servers. server_url: str reason: Literal["startup", "ttl-expired", "auth-failed"] class McpHeadersRefreshResult(TypedDict): - """Dynamic headers and their optional credential-bounded cache lifetime.""" + """Connector service authorization headers and their optional cache lifetime.""" headers: dict[str, str] ttl_ms: NotRequired[int] class McpHeadersRefreshContext(TypedDict): - """Context for a managed MCP headers refresh handler invocation.""" + """Context for a catalog MCP headers refresh handler invocation.""" session_id: str @@ -1255,14 +1255,34 @@ class MCPHTTPServerConfig(TypedDict, total=False): MCPServerConfig = MCPStdioServerConfig | MCPHTTPServerConfig -class ManagedMCPServerConfig(TypedDict, total=False): - """Non-secret hosted MCP server from a trusted managed catalog.""" +class ConnectorMCPServerConfig(TypedDict, total=False): + """Non-secret hosted MCP endpoint advertised for a connected Connector.""" display_name: Required[str] url: Required[str] tools: list[str] timeout: int - headers_refresh_ttl_ms: int + authorization_cache_ttl_ms: int + + +def _connector_mcp_servers_to_wire( + servers: dict[str, ConnectorMCPServerConfig], +) -> dict[str, Any]: + """Convert Connector MCP configuration keys to the JSON-RPC wire casing.""" + wire: dict[str, Any] = {} + for name, config in servers.items(): + server: dict[str, Any] = { + "displayName": config["display_name"], + "url": config["url"], + } + if "tools" in config: + server["tools"] = config["tools"] + if "timeout" in config: + server["timeout"] = config["timeout"] + if "authorization_cache_ttl_ms" in config: + server["headersRefreshTtlMs"] = config["authorization_cache_ttl_ms"] + wire[name] = server + return wire class GitHubMcpToolConfig(TypedDict, total=False): @@ -2185,7 +2205,7 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: self._execute_mcp_headers_refresh_and_respond( data.request_id, { - "server_name": data.server_name, + "server_key": data.server_name, "server_url": data.server_url, "reason": data.reason.value, }, @@ -2506,7 +2526,7 @@ async def _execute_mcp_headers_refresh_and_respond( request: McpHeadersRefreshRequest, handler: McpHeadersRefreshHandler, ) -> None: - """Execute a managed MCP headers refresh handler and respond via RPC.""" + """Execute an MCP headers refresh handler and respond via RPC.""" try: maybe_result = handler(request, {"session_id": self.session_id}) if inspect.isawaitable(maybe_result): @@ -2534,7 +2554,7 @@ async def _execute_mcp_headers_refresh_and_respond( message = str(exc) or "MCP headers refresh cancelled" logger.warning( "MCP headers refresh cancelled for %r: %s", - request["server_name"], + request["server_key"], message, ) rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( @@ -2544,7 +2564,7 @@ async def _execute_mcp_headers_refresh_and_respond( except Exception as exc: logger.warning( "MCP headers refresh failed for %r: %s", - request["server_name"], + request["server_key"], exc, ) rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( @@ -2754,7 +2774,7 @@ def _register_mcp_auth_handler(self, handler: McpAuthHandler | None) -> None: def _register_mcp_headers_refresh_handler( self, handler: McpHeadersRefreshHandler | None ) -> None: - """Register the managed MCP dynamic-headers handler for this session.""" + """Register the MCP dynamic-headers handler for this session.""" with self._mcp_headers_refresh_handler_lock: self._mcp_headers_refresh_handler = handler diff --git a/python/test_client.py b/python/test_client.py index 44af374a26..fd64839dc9 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -17,10 +17,10 @@ from copilot import ( CanvasProviderIdentity, CapiSessionOptions, + ConnectorMCPServerConfig, CopilotClient, DisableBypassPermissionsModes, ExtensionInfo, - ManagedMCPServerConfig, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, ModelSwitchAutoTierStatus, @@ -696,7 +696,7 @@ def handle_mcp_auth_request(request, invocation): await client.force_stop() @pytest.mark.asyncio - async def test_managed_mcp_headers_refresh_dispatches_ttl_and_broker_errors(self): + async def test_mcp_headers_refresh_dispatches_ttl_and_broker_errors(self): transport = Mock() transport.request = AsyncMock(return_value={"success": True}) session = CopilotSession("managed-session", transport) @@ -711,7 +711,7 @@ async def test_managed_mcp_headers_refresh_dispatches_ttl_and_broker_errors(self async def handle_refresh(request, context): assert request == { - "server_name": "GitHub", + "server_key": "github", "server_url": "https://example.com/mcp", "reason": "startup", } @@ -722,7 +722,7 @@ async def handle_refresh(request, context): event = SessionEvent( data=McpHeadersRefreshRequiredData( request_id="refresh-1", - server_name="GitHub", + server_name="github", server_url="https://example.com/mcp", reason=McpHeadersRefreshRequiredReason.STARTUP, ), @@ -741,7 +741,7 @@ async def handle_refresh(request, context): await session._execute_mcp_headers_refresh_and_respond( "refresh-2", { - "server_name": "GitHub", + "server_key": "github", "server_url": "https://example.com/mcp", "reason": "startup", }, @@ -750,7 +750,7 @@ async def handle_refresh(request, context): await session._execute_mcp_headers_refresh_and_respond( "refresh-3", { - "server_name": "GitHub", + "server_key": "github", "server_url": "https://example.com/mcp", "reason": "startup", }, @@ -763,7 +763,7 @@ def fail_refresh(_request, _context): await session._execute_mcp_headers_refresh_and_respond( "refresh-4", { - "server_name": "GitHub", + "server_key": "github", "server_url": "https://example.com/mcp", "reason": "auth-failed", }, @@ -772,7 +772,7 @@ def fail_refresh(_request, _context): await session._execute_mcp_headers_refresh_and_respond( "refresh-5", { - "server_name": "GitHub", + "server_key": "github", "server_url": "https://example.com/mcp", "reason": "auth-failed", }, @@ -796,7 +796,7 @@ def fail_refresh(_request, _context): ] @pytest.mark.asyncio - async def test_create_and_resume_forward_managed_mcp_servers(self): + async def test_create_and_resume_map_connector_mcp_servers_to_wire(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -815,23 +815,23 @@ async def mock_request(method, params, **kwargs): return {} client._client.request = mock_request - managed: dict[str, ManagedMCPServerConfig] = { + connectors: dict[str, ConnectorMCPServerConfig] = { "github": { "display_name": "GitHub", "url": "https://example.com/mcp", "tools": ["issues"], "timeout": 30_000, - "headers_refresh_ttl_ms": 60_000, + "authorization_cache_ttl_ms": 60_000, } } session = await client.create_session( - managed_mcp_servers=managed, + connector_mcp_servers=connectors, on_mcp_headers_refresh=lambda _request, _context: None, ) await client.resume_session( session.session_id, - managed_mcp_servers=managed, + connector_mcp_servers=connectors, on_mcp_headers_refresh=lambda _request, _context: None, ) diff --git a/rust/README.md b/rust/README.md index 010160665a..8f520ece5b 100644 --- a/rust/README.md +++ b/rust/README.md @@ -259,15 +259,17 @@ maintenance. ### Handler Traits -The SDK exposes five focused handler traits, one per CLI callback type. Implement only the traits you need and install each with the matching `SessionConfig` setter. Each trait has a single `async fn handle(...)` method: - -| Trait | Setter | Purpose | -| ----------------------- | --------------------------------- | --------------------------------------------- | -| `PermissionHandler` | `with_permission_handler(...)` | Approve/deny tool-use permission requests | -| `ElicitationHandler` | `with_elicitation_handler(...)` | Respond to structured elicitation prompts | -| `UserInputHandler` | `with_user_input_handler(...)` | Answer free-form / choice user-input prompts | -| `ExitPlanModeHandler` | `with_exit_plan_mode_handler(...)`| Respond when the agent exits plan mode | -| `AutoModeSwitchHandler` | `with_auto_mode_switch_handler(...)`| Respond to automatic mode-switch proposals | +The SDK exposes seven focused handler traits, one per CLI callback type. Implement only the traits you need and install each with the matching `SessionConfig` setter. Each trait has a single `async fn handle(...)` method: + +| Trait | Setter | Purpose | +| -------------------------- | ------------------------------------- | -------------------------------------------- | +| `PermissionHandler` | `with_permission_handler(...)` | Approve/deny tool-use permission requests | +| `ElicitationHandler` | `with_elicitation_handler(...)` | Respond to structured elicitation prompts | +| `McpAuthHandler` | `with_mcp_auth_handler(...)` | Supply host-provided MCP OAuth tokens | +| `McpHeadersRefreshHandler` | `with_mcp_headers_handler(...)` | Supply short-lived Copilot Connectors service authorization | +| `UserInputHandler` | `with_user_input_handler(...)` | Answer free-form / choice user-input prompts | +| `ExitPlanModeHandler` | `with_exit_plan_mode_handler(...)` | Respond when the agent exits plan mode | +| `AutoModeSwitchHandler` | `with_auto_mode_switch_handler(...)` | Respond to automatic mode-switch proposals | The CLI's `requestPermission` / `requestElicitation` / `requestUserInput` / etc. wire flags are derived automatically from which traits you've installed — clients that don't install a handler are silently skipped, letting another connected client handle the request. @@ -334,6 +336,80 @@ Use `with_ask_user_variant(AskUserVariant::Elicitation)` together with tool. The default remains `AskUserVariant::Legacy`. Re-supply the option and handler through `ResumeSessionConfig` on a cold resume. +#### Connector MCP servers + +Use `ConnectorMcpServerConfig` for connected Copilot Connector MCP endpoints +supplied from the service catalog. Map keys are stable Connector server keys; +short-lived client-to-Copilot-Connectors service authorization comes from +`McpHeadersRefreshHandler` rather than the configuration. This is normally the +selected account's GitHub bearer authorization. The host does not supply +Outlook, Slack, or other downstream provider tokens; the Connector service owns +those provider tokens. The callback's `server_key` is the stable map key (for +example, `"github-enterprise"`), not the human-readable `display_name` or +Connector service ID. + +```rust,ignore +use std::collections::HashMap; +use std::sync::Arc; + +use github_copilot_sdk::{ConnectorMcpServerConfig, SessionConfig}; + +let connectors = HashMap::from([( + "github-enterprise".to_string(), + ConnectorMcpServerConfig { + display_name: "GitHub Enterprise".to_string(), + url: "https://mcp.example.com/".to_string(), + tools: Some(vec!["issues".to_string(), "pull_requests".to_string()]), + timeout: Some(30_000), + authorization_cache_ttl_ms: Some(60_000), + }, +)]); + +let config = SessionConfig::default() + .with_connector_mcp_servers(connectors) + .with_mcp_headers_handler(Arc::new(MyMcpHeadersHandler)); +``` + +The generic `McpHeaders*` API names describe the remote-header protocol, but +the handler is for connected Copilot Connector MCP endpoints; ordinary +`mcp_servers` entries do not opt into dynamic refresh. +`authorization_cache_ttl_ms` caps how long the runtime may reuse Connector +service authorization; the handler result's `ttl_ms` reports that +authorization's remaining lifetime. + +> **Same-session Connector reconciliation is not available through the current +> public runtime contract.** Generic MCP start, restart, and stop operations +> only change transient running state; a reload can restore an endpoint from +> the session's original configuration. Do not map Connector service Connect +> or Disconnect to those operations. To change the connected set, create or +> cold-resume a session with the new `connector_mcp_servers` map until the +> runtime exposes public authoritative replace/remove operations. + +The public live-session MCP observation and availability surface remains: + +| Capability | Rust API | +| --- | --- | +| List servers and current status | `session.rpc().mcp().list()` | +| Enable a server | `session.rpc().mcp().enable(rpc::McpEnableRequest { ... })` | +| Disable a server | `session.rpc().mcp().disable(rpc::McpDisableRequest { ... })` | +| List connected-server tools | `session.rpc().mcp().list_tools(rpc::McpListToolsRequest { ... })` | +| Observe status changes | `session.subscribe()` plus `SessionEvent::parsed_type()` / `typed_data()` | + +The generated MCP request and status-event fields named `server_name`, plus +`rpc::McpServer::name`, carry the stable Connector server key. The separate +`display_name` is human-readable. Relevant typed event payloads are +`SessionMcpServersLoadedData`, `SessionMcpServerStatusChangedData`, +`SessionMcpServerNeedsReconnectData`, and `SessionMcpServerRemovedData` from +`github_copilot_sdk::session_events`. + +The host owns service-catalog discovery, Connector consent, external service +HTTP calls, and all browser or UI flows. The SDK does not add polling for these +operations. + +On cold resume, re-supply both the connected endpoints with +`ResumeSessionConfig::with_connector_mcp_servers` and the handler with +`with_mcp_headers_handler`. + For rotating per-session GitHub credentials, install a `GitHubTokenProvider` instead of setting `github_token`: diff --git a/rust/cli-version-in-process.txt b/rust/cli-version-in-process.txt index 9b1c2c0535..f8eb65c922 100644 --- a/rust/cli-version-in-process.txt +++ b/rust/cli-version-in-process.txt @@ -1,11 +1,11 @@ # Auto-generated by rust/scripts/snapshot-bundled-in-process-version.sh # Do not edit. Regenerated by the publish workflow on every release. -version=1.0.84-5 -copilot-darwin-arm64=bf7237c06cf52c42d9845670a25b4faabf65513cc3febb7de4f1431389ec2a24 -copilot-darwin-x64=4464c70800d1f416d0811325fad83247d2d1703876e96594514b7381ba4f7bf0 -copilot-linux-arm64=711e24faf6ae135cbfe650f6e163af0c11dc87449e2967a17cfe1410a19ab4cb -copilot-linux-x64=b847ea0c82014b7475620023288828a01430b5fd14588c7600555e9aca7a2623 -copilot-linuxmusl-arm64=5059e6256a889c09b050d41737fdf49d2fcf04beb39f04fe7b26589dc9ce058d -copilot-linuxmusl-x64=bea4ee146c87d42470b5c8fc37fa1c27d17a6339faab3dbb817e0bf72d032831 -copilot-win32-arm64=ffdbceb0cb891419a5926a2da73ffc1adf81865a5f5b35620c241200d2e3c680 -copilot-win32-x64=de0ed6f6a945312e9446156a46142b5f34a0c3dfc67be740c36c6d1f6f7f5636 +version=1.0.85 +copilot-darwin-arm64=a4445dc278f8f114bfd04699c65f8cbf1bcce4b7ecec3659993c606cdadaa2c1 +copilot-darwin-x64=ff87dadccfba1106631e15ff391b363f31cda2a4a80dbc35fda033e422d71112 +copilot-linux-arm64=e3b5f190051c38267ce58c3d7ce3decba9d0b14c0ef98ac8fef687520aab31db +copilot-linux-x64=0ce01152e3943a3c8119861838b61649ad0b237830411cc7a8bbab47fcf62bb2 +copilot-linuxmusl-arm64=2f4741238349860bea8163afa99aac705b82427fca8de83bd2552af22da144af +copilot-linuxmusl-x64=50652722c0d5fe82f256f9180e03dd55ba00f7ad605e272faffe67767de9995d +copilot-win32-arm64=48209db38a6620bd5931d8431af85308d748a076c4658ff71f27f65585f62e42 +copilot-win32-x64=b9b39908222e24e50af6f32be732d2d2935efbe469a41fc7dc3b000857a6ce97 diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 572cada23e..1905cbfab3 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -2,7 +2,7 @@ //! //! Each callback the CLI may dispatch (permission requests, elicitation //! prompts, user-input questions, exit-plan-mode prompts, -//! auto-mode-switch prompts, MCP credential requests) has its own focused trait with a single +//! auto-mode-switch prompts, MCP authorization requests) has its own focused trait with a single //! `handle` method. //! //! Handlers are **optional**: install only the ones the application cares @@ -240,7 +240,7 @@ pub trait ElicitationHandler: Send + Sync + 'static { pub struct McpAuthRequest { /// Identifier for the pending MCP OAuth request. pub request_id: RequestId, - /// Display name of the MCP server that requires OAuth. + /// Stable server identity used as the configuration map key. pub server_name: String, /// URL of the MCP server that requires OAuth. pub server_url: String, @@ -304,11 +304,15 @@ pub trait McpAuthHandler: Send + Sync + 'static { ) -> McpAuthResult; } -/// Dynamic-header request for a host-managed MCP server. +/// Dynamic-header request for a connected Copilot Connector MCP endpoint. +/// +/// These headers authorize the client-to-Copilot-Connectors service request. +/// The Connector service owns downstream provider tokens. #[derive(Debug, Clone)] pub struct McpHeadersRefreshRequest { - /// Display name of the MCP server that requires headers. - pub server_name: String, + /// Stable Connector server key used in the configuration map, not the + /// human-readable display name or Connector service ID. + pub server_key: String, /// URL of the MCP server that requires headers. pub server_url: String, /// Why the runtime is requesting fresh headers. @@ -318,14 +322,14 @@ pub struct McpHeadersRefreshRequest { /// Result returned by an [`McpHeadersRefreshHandler`]. #[derive(Debug, Clone)] pub enum McpHeadersRefreshResult { - /// Supplies dynamic HTTP headers for the managed MCP server. + /// Supplies short-lived Copilot Connectors service authorization headers. Headers { - /// Headers to overlay onto MCP requests. + /// Headers to overlay onto the client-to-Copilot-Connectors request. headers: HashMap, - /// Optional lifetime in milliseconds for the returned headers. + /// Remaining Connector service authorization lifetime in milliseconds. ttl_ms: Option, }, - /// Indicates that no dynamic headers are available. + /// Indicates that no service authorization headers are available. None, } @@ -361,10 +365,14 @@ pub(crate) fn mcp_headers_error_result( ) } -/// Handler for dynamic headers requested by host-managed MCP servers. +/// Handler for short-lived Copilot Connectors service authorization. +/// +/// Hosts normally return the selected account's GitHub bearer authorization. +/// They do not supply Outlook, Slack, or other downstream provider tokens; +/// those remain owned by the Connector service. #[async_trait] pub trait McpHeadersRefreshHandler: Send + Sync + 'static { - /// Resolve a managed MCP headers refresh request. + /// Resolve a Copilot Connectors service authorization refresh request. async fn handle( &self, session_id: SessionId, diff --git a/rust/src/session.rs b/rust/src/session.rs index f36a459760..b9007ad804 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -458,7 +458,7 @@ impl Session { /// The hand-authored helpers on [`Session`] delegate to this namespace /// and remain the recommended entry point for everyday use; reach for /// `rpc()` when you want a method without a hand-written wrapper. - pub fn rpc(&self) -> crate::generated::rpc::SessionRpc<'_> { + pub fn rpc(&self) -> crate::rpc::SessionRpc<'_> { crate::generated::rpc::SessionRpc { session: self } } @@ -2954,7 +2954,7 @@ async fn handle_notification( }; let request_id = data.request_id; let request = McpHeadersRefreshRequest { - server_name: data.server_name, + server_key: data.server_name, server_url: data.server_url, reason: data.reason, }; diff --git a/rust/src/types.rs b/rust/src/types.rs index 175af7bee9..838e1f6df4 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -20,8 +20,9 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage, CopilotWebSocketResponse, WebSocketTransform, forward_http, }; -pub use crate::generated::api_types::ManagedMcpServerConfig; -use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; +use crate::generated::api_types::{ + CurrentToolMetadata, ManagedMcpServerConfig as WireManagedMcpServerConfig, OpenCanvasInstance, +}; /// Acknowledgement and Auto preference snapshot returned by an Auto tier switch. pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus}; /// Routing tier for the `auto` model with Auto mode V2. @@ -1216,6 +1217,57 @@ pub struct McpHttpServerConfig { pub headers: HashMap, } +/// Connected Copilot Connector MCP endpoint supplied from the service catalog. +/// +/// The containing map key is the stable Connector server key. Short-lived +/// client-to-Copilot-Connectors service authorization is supplied through +/// [`McpHeadersRefreshHandler`], never stored in this configuration. The +/// Connector service owns downstream provider tokens. +/// +/// **Experimental.** This type is part of an experimental Connector MCP +/// surface and may change or be removed in a future release. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorMcpServerConfig { + /// Human-readable Connector display name. + pub display_name: String, + /// Maximum time, in milliseconds, that the runtime may reuse Connector + /// service authorization. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub authorization_cache_ttl_ms: Option, + /// Timeout in milliseconds for tool discovery and tool calls. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, + /// Tools to include. Defaults to all tools when omitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tools: Option>, + /// Hosted MCP streamable HTTP endpoint. + pub url: String, +} + +impl ConnectorMcpServerConfig { + fn into_wire(self) -> WireManagedMcpServerConfig { + WireManagedMcpServerConfig { + display_name: self.display_name, + headers_refresh_ttl_ms: self.authorization_cache_ttl_ms, + timeout: self.timeout, + tools: self.tools, + url: self.url, + } + } +} + +fn connector_mcp_servers_into_wire( + servers: Option>, +) -> Option> { + servers.map(|servers| { + servers + .into_iter() + .map(|(identity, server)| (identity, server.into_wire())) + .collect() + }) +} + /// Configures a custom inference provider (BYOK — Bring Your Own Key). /// /// Routes session requests through an alternative model provider @@ -2003,11 +2055,12 @@ pub struct SessionConfig { pub included_builtin_skills: Option>, /// MCP server configurations passed through to the CLI. pub mcp_servers: Option>, - /// Non-secret host-managed HTTP MCP servers keyed by stable managed identity. + /// Connected Copilot Connector MCP endpoints supplied from the service catalog. /// - /// Credentials are supplied dynamically through - /// [`McpHeadersRefreshHandler`]. - pub managed_mcp_servers: Option>, + /// Keys are stable Connector server keys. Short-lived Copilot Connectors + /// service authorization is supplied through [`McpHeadersRefreshHandler`]; + /// downstream provider tokens remain owned by the Connector service. + pub connector_mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// /// - `"persistent"` — tokens are stored in the OS keychain (shared across sessions). @@ -2240,7 +2293,7 @@ pub struct SessionConfig { /// Optional MCP OAuth request handler. When set, the SDK can satisfy MCP /// server OAuth requests with host-acquired token data or cancellation. pub mcp_auth_handler: Option>, - /// Optional dynamic-header handler for host-managed MCP servers. + /// Optional service-authorization handler for Copilot Connector MCP endpoints. pub mcp_headers_handler: Option>, /// Optional handler for the legacy question-and-answer `ask_user` variant. /// When `None`, `requestUserInput: false` goes on the wire, so this client @@ -2333,7 +2386,7 @@ impl std::fmt::Debug for SessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) - .field("managed_mcp_servers", &self.managed_mcp_servers) + .field("connector_mcp_servers", &self.connector_mcp_servers) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field( "auth_client_id_metadata_url", @@ -2482,7 +2535,7 @@ impl Default for SessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, - managed_mcp_servers: None, + connector_mcp_servers: None, mcp_oauth_token_storage: None, auth_client_id_metadata_url: None, enable_config_discovery: None, @@ -2655,7 +2708,7 @@ impl SessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, - managed_mcp_servers: self.managed_mcp_servers, + managed_mcp_servers: connector_mcp_servers_into_wire(self.connector_mcp_servers), mcp_oauth_token_storage: self.mcp_oauth_token_storage, auth_client_id_metadata_url: self.auth_client_id_metadata_url, embedding_cache_storage: self.embedding_cache_storage, @@ -2758,7 +2811,8 @@ impl SessionConfig { self } - /// Install an [`McpHeadersRefreshHandler`] for host-managed MCP dynamic headers. + /// Install an [`McpHeadersRefreshHandler`] for short-lived Copilot + /// Connectors service authorization. pub fn with_mcp_headers_handler(mut self, handler: Arc) -> Self { self.mcp_headers_handler = Some(handler); self @@ -2994,12 +3048,12 @@ impl SessionConfig { self } - /// Set host-managed HTTP MCP server configurations. - pub fn with_managed_mcp_servers( + /// Set connected Copilot Connector MCP endpoints keyed by stable server key. + pub fn with_connector_mcp_servers( mut self, - servers: HashMap, + servers: HashMap, ) -> Self { - self.managed_mcp_servers = Some(servers); + self.connector_mcp_servers = Some(servers); self } @@ -3494,8 +3548,8 @@ pub struct ResumeSessionConfig { pub included_builtin_skills: Option>, /// Re-supply MCP servers so they remain available after app restart. pub mcp_servers: Option>, - /// Re-supply host-managed HTTP MCP servers after app restart. - pub managed_mcp_servers: Option>, + /// Re-supply connected Copilot Connector MCP endpoints after app restart. + pub connector_mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// See [`SessionConfig::mcp_oauth_token_storage`] for details. pub mcp_oauth_token_storage: Option, @@ -3670,7 +3724,7 @@ pub struct ResumeSessionConfig { pub elicitation_handler: Option>, /// Optional MCP OAuth handler. See [`SessionConfig::mcp_auth_handler`]. pub mcp_auth_handler: Option>, - /// Optional managed MCP dynamic-header handler. + /// Optional Copilot Connectors service-authorization handler. pub mcp_headers_handler: Option>, /// Optional user-input handler. See /// [`SessionConfig::user_input_handler`]. @@ -3733,7 +3787,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) - .field("managed_mcp_servers", &self.managed_mcp_servers) + .field("connector_mcp_servers", &self.connector_mcp_servers) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field( "auth_client_id_metadata_url", @@ -3924,7 +3978,7 @@ impl ResumeSessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, - managed_mcp_servers: self.managed_mcp_servers, + managed_mcp_servers: connector_mcp_servers_into_wire(self.connector_mcp_servers), mcp_oauth_token_storage: self.mcp_oauth_token_storage, auth_client_id_metadata_url: self.auth_client_id_metadata_url, embedding_cache_storage: self.embedding_cache_storage, @@ -4036,7 +4090,7 @@ impl ResumeSessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, - managed_mcp_servers: None, + connector_mcp_servers: None, mcp_oauth_token_storage: None, auth_client_id_metadata_url: None, enable_config_discovery: None, @@ -4124,7 +4178,8 @@ impl ResumeSessionConfig { self } - /// Install an [`McpHeadersRefreshHandler`] for host-managed MCP dynamic headers. + /// Install an [`McpHeadersRefreshHandler`] for short-lived Copilot + /// Connectors service authorization. pub fn with_mcp_headers_handler(mut self, handler: Arc) -> Self { self.mcp_headers_handler = Some(handler); self @@ -4352,12 +4407,12 @@ impl ResumeSessionConfig { self } - /// Re-supply host-managed HTTP MCP server configurations on resume. - pub fn with_managed_mcp_servers( + /// Re-supply connected Copilot Connector MCP endpoints on resume. + pub fn with_connector_mcp_servers( mut self, - servers: HashMap, + servers: HashMap, ) -> Self { - self.managed_mcp_servers = Some(servers); + self.connector_mcp_servers = Some(servers); self } @@ -5751,7 +5806,7 @@ impl SessionEvent { /// /// Returns `SessionEventType::Unknown` for unrecognized event types, /// ensuring forward compatibility with newer CLI versions. - pub fn parsed_type(&self) -> crate::generated::SessionEventType { + pub fn parsed_type(&self) -> crate::session_events::SessionEventType { use serde::de::IntoDeserializer; let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> = self.event_type.as_str().into_deserializer(); @@ -6295,9 +6350,9 @@ mod tests { use super::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, - ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, - ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, - InfiniteSessionConfig, LargeToolOutputConfig, ManagedMcpServerConfig, McpServerConfig, + ConnectionState, ConnectorMcpServerConfig, CopilotExpAssignmentResponse, CustomAgentConfig, + DeliveryMode, ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, + GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool, @@ -6591,27 +6646,31 @@ mod tests { } #[test] - fn managed_mcp_servers_serialize_on_create_and_resume() { + fn connector_mcp_servers_map_to_internal_wire_on_create_and_resume() { let servers = HashMap::from([( - "catalog-id".to_string(), - ManagedMcpServerConfig { - display_name: "Managed server".to_string(), - headers_refresh_ttl_ms: Some(30_000), + "connector-key".to_string(), + ConnectorMcpServerConfig { + display_name: "Connector server".to_string(), + authorization_cache_ttl_ms: Some(30_000), timeout: Some(15_000), tools: Some(vec!["search".to_string()]), url: "https://mcp.example.test".to_string(), }, )]); + let public_json = serde_json::to_value(servers.get("connector-key").unwrap()).unwrap(); + assert_eq!(public_json["authorizationCacheTtlMs"], 30_000); + assert!(public_json.get("headersRefreshTtlMs").is_none()); + let (create_wire, _) = SessionConfig::default() - .with_managed_mcp_servers(servers.clone()) - .into_wire(Some(SessionId::from("managed-create"))) + .with_connector_mcp_servers(servers.clone()) + .into_wire(Some(SessionId::from("connector-create"))) .expect("create config has no duplicate handlers"); let create_json = serde_json::to_value(create_wire).unwrap(); assert_eq!( - create_json["managedMcpServers"]["catalog-id"], + create_json["managedMcpServers"]["connector-key"], json!({ - "displayName": "Managed server", + "displayName": "Connector server", "headersRefreshTtlMs": 30_000, "timeout": 15_000, "tools": ["search"], @@ -6619,14 +6678,14 @@ mod tests { }) ); - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("managed-resume")) - .with_managed_mcp_servers(servers) + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("connector-resume")) + .with_connector_mcp_servers(servers) .into_wire() .expect("resume config has no duplicate handlers"); let resume_json = serde_json::to_value(resume_wire).unwrap(); assert_eq!( - resume_json["managedMcpServers"]["catalog-id"]["displayName"], - "Managed server" + resume_json["managedMcpServers"]["connector-key"]["displayName"], + "Connector server" ); } diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 684994fa17..cba003c431 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -254,7 +254,7 @@ impl McpHeadersRefreshHandler for TestMcpHeadersHandler { self.calls .send((session_id, request_id, request.clone())) .unwrap(); - match request.server_name.as_str() { + match request.server_key.as_str() { "ttl-absent" => Ok(McpHeadersRefreshResult::Headers { headers: HashMap::from([( "Authorization".to_string(), @@ -1272,14 +1272,14 @@ async fn resume_session_registers_mcp_auth_interest_only_with_handler() { } #[tokio::test] -async fn managed_mcp_headers_handler_dispatches_all_results() { +async fn mcp_headers_handler_dispatches_all_results() { let (calls_tx, mut calls_rx) = tokio::sync::mpsc::unbounded_channel(); let handler = Arc::new(TestMcpHeadersHandler { calls: calls_tx }); let (client, server_read, server_write) = make_client(); let mut server = FakeServer { read: server_read, write: server_write, - session_id: "managed-mcp-session".to_string(), + session_id: "mcp-headers-session".to_string(), }; let create_handle = tokio::spawn({ let client = client.clone(); @@ -1287,7 +1287,7 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { client .create_session( SessionConfig::default() - .with_session_id("managed-mcp-session") + .with_session_id("mcp-headers-session") .with_mcp_headers_handler(handler), ) .await @@ -1315,7 +1315,7 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { ); assert_eq!( response_request["params"]["sessionId"], - "managed-mcp-session" + "mcp-headers-session" ); assert_eq!( response_request["params"]["requestId"], @@ -1329,9 +1329,9 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { ); let (received_session_id, received_request_id, received_request) = timeout(TIMEOUT, calls_rx.recv()).await.unwrap().unwrap(); - assert_eq!(received_session_id.as_str(), "managed-mcp-session"); + assert_eq!(received_session_id.as_str(), "mcp-headers-session"); assert_eq!(received_request_id, "headers-before-create"); - assert_eq!(received_request.server_name, "ttl-absent"); + assert_eq!(received_request.server_key, "ttl-absent"); assert_eq!( received_request.server_url, "https://ttl-absent.example.test/mcp" @@ -1348,7 +1348,7 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { .respond( &create_request, serde_json::json!({ - "sessionId": "managed-mcp-session", + "sessionId": "mcp-headers-session", "workspacePath": "/workspace" }), ) @@ -1404,17 +1404,17 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { ), ]; - for (index, (server_name, reason, expected_reason, expected_result)) in + for (index, (server_identity, reason, expected_reason, expected_result)) in cases.into_iter().enumerate() { let request_id = format!("headers-{index}"); - let server_url = format!("https://{server_name}.example.test/mcp"); + let server_url = format!("https://{server_identity}.example.test/mcp"); server .send_event( "mcp.headers_refresh_required", serde_json::json!({ "requestId": request_id, - "serverName": server_name, + "serverName": server_identity, "serverUrl": server_url, "reason": reason }), @@ -1431,9 +1431,9 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { let (received_session_id, received_request_id, received_request) = timeout(TIMEOUT, calls_rx.recv()).await.unwrap().unwrap(); - assert_eq!(received_session_id.as_str(), "managed-mcp-session"); + assert_eq!(received_session_id.as_str(), "mcp-headers-session"); assert_eq!(received_request_id, request_id); - assert_eq!(received_request.server_name, server_name); + assert_eq!(received_request.server_key, server_identity); assert_eq!(received_request.server_url, server_url); assert_eq!(received_request.reason, expected_reason); @@ -1444,14 +1444,14 @@ async fn managed_mcp_headers_handler_dispatches_all_results() { } #[tokio::test] -async fn managed_mcp_interest_failure_cleans_up_session_dispatch() { +async fn mcp_headers_interest_failure_cleans_up_session_dispatch() { let (calls_tx, mut calls_rx) = tokio::sync::mpsc::unbounded_channel(); let handler = Arc::new(TestMcpHeadersHandler { calls: calls_tx }); let (client, server_read, server_write) = make_client(); let mut server = FakeServer { read: server_read, write: server_write, - session_id: "managed-mcp-interest-failure".to_string(), + session_id: "mcp-headers-interest-failure".to_string(), }; let create_handle = tokio::spawn({ let client = client.clone(); @@ -1459,7 +1459,7 @@ async fn managed_mcp_interest_failure_cleans_up_session_dispatch() { client .create_session( SessionConfig::default() - .with_session_id("managed-mcp-interest-failure") + .with_session_id("mcp-headers-interest-failure") .with_mcp_headers_handler(handler), ) .await @@ -1471,7 +1471,7 @@ async fn managed_mcp_interest_failure_cleans_up_session_dispatch() { .respond( &create_request, serde_json::json!({ - "sessionId": "managed-mcp-interest-failure", + "sessionId": "mcp-headers-interest-failure", "workspacePath": "/workspace" }), ) @@ -1509,6 +1509,185 @@ async fn managed_mcp_interest_failure_cleans_up_session_dispatch() { assert!(timeout(TIMEOUT, calls_rx.recv()).await.unwrap().is_none()); } +#[tokio::test] +async fn public_mcp_list_toggle_tools_and_status_events_are_reachable() { + use github_copilot_sdk::rpc::{McpDisableRequest, McpEnableRequest, McpListToolsRequest}; + use github_copilot_sdk::session_events::{ + McpServerSource, McpServerStatus, SessionEventType, SessionMcpServerNeedsReconnectData, + SessionMcpServerRemovedData, SessionMcpServerStatusChangedData, + SessionMcpServersLoadedData, + }; + + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + let _: github_copilot_sdk::rpc::SessionRpc<'_> = session.rpc(); + + let list = tokio::spawn({ + let session = session.clone(); + async move { session.rpc().mcp().list().await.unwrap() } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.mcp.list"); + assert_eq!(request["params"]["sessionId"], server.session_id); + server + .respond( + &request, + serde_json::json!({ + "servers": [{ + "displayName": "Calendar", + "name": "calendar", + "source": "managed", + "status": "connected" + }] + }), + ) + .await; + let list = timeout(TIMEOUT, list).await.unwrap().unwrap(); + assert_eq!(list.servers.len(), 1); + assert_eq!(list.servers[0].name, "calendar"); + assert_eq!(list.servers[0].display_name.as_deref(), Some("Calendar")); + assert_eq!(list.servers[0].source, Some(McpServerSource::Managed)); + assert_eq!(list.servers[0].status, McpServerStatus::Connected); + + let enable = tokio::spawn({ + let session = session.clone(); + async move { + session + .rpc() + .mcp() + .enable(McpEnableRequest { + server_name: "calendar".to_string(), + }) + .await + .unwrap(); + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.mcp.enable"); + assert_eq!(request["params"]["serverName"], "calendar"); + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, enable).await.unwrap().unwrap(); + + let disable = tokio::spawn({ + let session = session.clone(); + async move { + session + .rpc() + .mcp() + .disable(McpDisableRequest { + server_name: "calendar".to_string(), + }) + .await + .unwrap(); + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.mcp.disable"); + assert_eq!(request["params"]["serverName"], "calendar"); + server.respond(&request, serde_json::json!({})).await; + timeout(TIMEOUT, disable).await.unwrap().unwrap(); + + let list_tools = tokio::spawn({ + let session = session.clone(); + async move { + session + .rpc() + .mcp() + .list_tools(McpListToolsRequest { + server_name: "calendar".to_string(), + }) + .await + .unwrap() + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.mcp.listTools"); + assert_eq!(request["params"]["serverName"], "calendar"); + server + .respond( + &request, + serde_json::json!({ + "tools": [{ + "name": "events", + "description": "List calendar events" + }] + }), + ) + .await; + let tools = timeout(TIMEOUT, list_tools).await.unwrap().unwrap(); + assert_eq!(tools.tools.len(), 1); + assert_eq!(tools.tools[0].name, "events"); + + let mut events = session.subscribe(); + server + .send_event( + "session.mcp_servers_loaded", + serde_json::json!({ + "servers": [{ + "displayName": "Calendar", + "name": "calendar", + "source": "managed", + "status": "connected" + }] + }), + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!( + event.parsed_type(), + SessionEventType::SessionMcpServersLoaded + ); + let loaded: SessionMcpServersLoadedData = event.typed_data().unwrap(); + assert_eq!(loaded.servers[0].name, "calendar"); + assert_eq!(loaded.servers[0].status, McpServerStatus::Connected); + + server + .send_event( + "session.mcp_server_status_changed", + serde_json::json!({ + "serverName": "calendar", + "status": "needs-auth" + }), + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!( + event.parsed_type(), + SessionEventType::SessionMcpServerStatusChanged + ); + let changed: SessionMcpServerStatusChangedData = event.typed_data().unwrap(); + assert_eq!(changed.server_name, "calendar"); + assert_eq!(changed.status, McpServerStatus::NeedsAuth); + + server + .send_event( + "session.mcp_server_needs_reconnect", + serde_json::json!({ "serverName": "calendar" }), + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!( + event.parsed_type(), + SessionEventType::SessionMcpServerNeedsReconnect + ); + let reconnect: SessionMcpServerNeedsReconnectData = event.typed_data().unwrap(); + assert_eq!(reconnect.server_name, "calendar"); + + server + .send_event( + "session.mcp_server_removed", + serde_json::json!({ "serverName": "calendar" }), + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!( + event.parsed_type(), + SessionEventType::SessionMcpServerRemoved + ); + let removed: SessionMcpServerRemovedData = event.typed_data().unwrap(); + assert_eq!(removed.server_name, "calendar"); +} + async fn server_respond_create( writer: &mut (impl AsyncWrite + Unpin), request: &Value, diff --git a/scripts/codegen/managed-mcp-schema-overlay.json b/scripts/codegen/managed-mcp-schema-overlay.json deleted file mode 100644 index db413e58ef..0000000000 --- a/scripts/codegen/managed-mcp-schema-overlay.json +++ /dev/null @@ -1,243 +0,0 @@ -{ - "api.schema.json": { - "ManagedMcpServerConfig": { - "type": "object", - "properties": { - "displayName": { - "type": "string", - "description": "Human-readable catalog display name." - }, - "url": { - "type": "string", - "format": "uri", - "description": "Hosted MCP streamable HTTP endpoint." - }, - "tools": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Tools to include. Defaults to all tools when omitted." - }, - "timeout": { - "type": "integer", - "exclusiveMinimum": 0, - "format": "duration", - "description": "Timeout in milliseconds for tool discovery and tool calls." - }, - "headersRefreshTtlMs": { - "type": "integer", - "minimum": 0, - "description": "Maximum dynamic-header cache lifetime in milliseconds." - } - }, - "required": [ - "displayName", - "url" - ], - "additionalProperties": false, - "description": "Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host.", - "title": "ManagedMcpServerConfig" - }, - "McpHeadersHandlePendingHeadersRefreshRequest": { - "anyOf": [ - { - "type": "object", - "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers." - }, - "ttlMs": { - "type": "integer", - "minimum": 0, - "description": "Optional lifetime in milliseconds for these returned headers. The runtime clamps its configured cache lifetime to this value." - }, - "kind": { - "type": "string", - "const": "headers", - "description": "Headers-refresh response variant discriminator." - } - }, - "required": [ - "kind", - "headers" - ] - }, - { - "type": "object", - "properties": { - "kind": { - "type": "string", - "const": "none", - "description": "Headers-refresh response variant discriminator." - } - }, - "required": [ - "kind" - ] - }, - { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Host credential broker failure, denial, or revocation reason." - }, - "kind": { - "type": "string", - "const": "error", - "description": "Headers-refresh response variant discriminator." - } - }, - "required": [ - "kind", - "message" - ] - } - ], - "description": "Host response: supply dynamic headers or decline this refresh.", - "title": "McpHeadersHandlePendingHeadersRefreshRequest" - }, - "managedMcpServers": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ManagedMcpServerConfig", - "description": "Non-secret host-managed HTTP MCP server configuration. The containing map key is the stable managed identity; credentials are supplied dynamically by the host." - }, - "description": "Non-secret host-managed HTTP MCP servers keyed by stable managed identity. Managed provenance is runtime-established from this separate field and credentials are supplied through dynamic-header refresh.", - "stability": "experimental" - }, - "McpServerSource": { - "type": "string", - "enum": [ - "user", - "workspace", - "plugin", - "builtin", - "managed" - ], - "description": "Configuration source: user, workspace, plugin, builtin, or managed", - "title": "McpServerSource", - "x-enumDescriptions": { - "user": "Server configured in the user's global MCP configuration.", - "workspace": "Server configured by the current workspace.", - "plugin": "Server contributed by an installed plugin.", - "builtin": "Server bundled with the runtime.", - "managed": "Server supplied by a trusted host-managed catalog." - } - }, - "mcpServerDisplayName": { - "type": "string", - "description": "Human-readable display name supplied by a managed server catalog." - } - }, - "session-events.schema.json": { - "McpServerSource": { - "type": "string", - "enum": [ - "user", - "workspace", - "plugin", - "builtin", - "managed" - ], - "description": "Configuration source: user, workspace, plugin, builtin, or managed", - "title": "McpServerSource", - "x-enumDescriptions": { - "user": "Server configured in the user's global MCP configuration.", - "workspace": "Server configured by the current workspace.", - "plugin": "Server contributed by an installed plugin.", - "builtin": "Server bundled with the runtime.", - "managed": "Server supplied by a trusted host-managed catalog." - } - }, - "mcpServersLoadedDisplayName": { - "type": "string", - "description": "Human-readable display name supplied by a managed server catalog." - } - }, - "legacy": { - "api.schema.json": { - "McpHeadersHandlePendingHeadersRefreshRequest": { - "anyOf": [ - { - "type": "object", - "properties": { - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers." - }, - "kind": { - "type": "string", - "const": "headers", - "description": "Headers-refresh response variant discriminator." - } - }, - "required": [ - "kind", - "headers" - ] - }, - { - "type": "object", - "properties": { - "kind": { - "type": "string", - "const": "none", - "description": "Headers-refresh response variant discriminator." - } - }, - "required": [ - "kind" - ] - } - ], - "description": "Host response: supply dynamic headers or decline this refresh.", - "title": "McpHeadersHandlePendingHeadersRefreshRequest" - }, - "McpServerSource": { - "type": "string", - "enum": [ - "user", - "workspace", - "plugin", - "builtin" - ], - "description": "Configuration source: user, workspace, plugin, or builtin", - "title": "McpServerSource", - "x-enumDescriptions": { - "user": "Server configured in the user's global MCP configuration.", - "workspace": "Server configured by the current workspace.", - "plugin": "Server contributed by an installed plugin.", - "builtin": "Server bundled with the runtime." - } - } - }, - "session-events.schema.json": { - "McpServerSource": { - "type": "string", - "enum": [ - "user", - "workspace", - "plugin", - "builtin" - ], - "description": "Configuration source: user, workspace, plugin, or builtin", - "title": "McpServerSource", - "x-enumDescriptions": { - "user": "Server configured in the user's global MCP configuration.", - "workspace": "Server configured by the current workspace.", - "plugin": "Server contributed by an installed plugin.", - "builtin": "Server bundled with the runtime." - } - } - } - } -} diff --git a/scripts/codegen/managedMcpSchemaOverlay.ts b/scripts/codegen/managedMcpSchemaOverlay.ts deleted file mode 100644 index cae15e33c8..0000000000 --- a/scripts/codegen/managedMcpSchemaOverlay.ts +++ /dev/null @@ -1,204 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -import fs from "fs"; - -type SchemaNode = Record; - -interface ManagedMcpApiOverlay { - ManagedMcpServerConfig: SchemaNode; - McpHeadersHandlePendingHeadersRefreshRequest: SchemaNode; - managedMcpServers: SchemaNode; - McpServerSource: SchemaNode; - mcpServerDisplayName: SchemaNode; -} - -interface ManagedMcpEventOverlay { - McpServerSource: SchemaNode; - mcpServersLoadedDisplayName: SchemaNode; -} - -interface ManagedMcpSchemaOverlay { - "api.schema.json": ManagedMcpApiOverlay; - "session-events.schema.json": ManagedMcpEventOverlay; - legacy: { - "api.schema.json": { - McpHeadersHandlePendingHeadersRefreshRequest: SchemaNode; - McpServerSource: SchemaNode; - }; - "session-events.schema.json": { - McpServerSource: SchemaNode; - }; - }; -} - -const overlay = JSON.parse( - fs.readFileSync( - new URL("./managed-mcp-schema-overlay.json", import.meta.url), - "utf8", - ), -) as ManagedMcpSchemaOverlay; - -function stableStringify(value: unknown): string { - if (Array.isArray(value)) { - return `[${value.map(stableStringify).join(",")}]`; - } - if (value !== null && typeof value === "object") { - return `{${Object.entries(value as SchemaNode) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`) - .join(",")}}`; - } - return JSON.stringify(value); -} - -function clone(value: T): T { - return JSON.parse(JSON.stringify(value)) as T; -} - -interface ContractNode { - parent: SchemaNode; - key: string; - desired: SchemaNode; - legacy?: SchemaNode; -} - -function installContract(nodes: ContractNode[]): void { - if ( - nodes.every( - ({ parent, key, desired }) => - stableStringify(parent[key]) === stableStringify(desired), - ) - ) { - return; - } - if ( - !nodes.every( - ({ parent, key, legacy }) => - stableStringify(parent[key]) === stableStringify(legacy), - ) - ) { - const missing = nodes.find( - ({ parent, key, legacy }) => - parent[key] === undefined && legacy !== undefined, - ); - throw new Error( - (missing - ? `Managed MCP schema overlay expected an upstream node at ${missing.key}. ` - : "Managed MCP schema overlay conflicts with an unknown upstream shape or partial contract. ") + - "Update or remove scripts/codegen/managed-mcp-schema-overlay.json.", - ); - } - for (const { parent, key, desired } of nodes) { - parent[key] = clone(desired); - } -} - -function requireDefinitions(schema: SchemaNode): SchemaNode { - const definitions = schema.definitions; - if ( - definitions === null || - typeof definitions !== "object" || - Array.isArray(definitions) - ) { - throw new Error( - "Managed MCP schema overlay requires a definitions object.", - ); - } - return definitions as SchemaNode; -} - -function requireProperties( - definitions: SchemaNode, - definitionName: string, -): SchemaNode { - const definition = definitions[definitionName] as SchemaNode | undefined; - const properties = definition?.properties; - if ( - properties === null || - typeof properties !== "object" || - Array.isArray(properties) - ) { - throw new Error( - `Managed MCP schema overlay requires ${definitionName}.properties to be an object.`, - ); - } - return properties as SchemaNode; -} - -function applyApiOverlay(schema: SchemaNode): void { - const definitions = requireDefinitions(schema); - const api = overlay["api.schema.json"]; - const legacy = overlay.legacy["api.schema.json"]; - installContract([ - { - parent: definitions, - key: "ManagedMcpServerConfig", - desired: api.ManagedMcpServerConfig, - }, - { - parent: definitions, - key: "McpHeadersHandlePendingHeadersRefreshRequest", - desired: api.McpHeadersHandlePendingHeadersRefreshRequest, - legacy: legacy.McpHeadersHandlePendingHeadersRefreshRequest, - }, - { - parent: definitions, - key: "McpServerSource", - desired: api.McpServerSource, - legacy: legacy.McpServerSource, - }, - { - parent: requireProperties(definitions, "SessionOpenOptions"), - key: "managedMcpServers", - desired: api.managedMcpServers, - }, - { - parent: requireProperties(definitions, "McpServer"), - key: "displayName", - desired: api.mcpServerDisplayName, - }, - ]); -} - -function applySessionEventsOverlay(schema: SchemaNode): void { - const definitions = requireDefinitions(schema); - const events = overlay["session-events.schema.json"]; - const legacy = overlay.legacy["session-events.schema.json"]; - installContract([ - { - parent: definitions, - key: "McpServerSource", - desired: events.McpServerSource, - legacy: legacy.McpServerSource, - }, - { - parent: requireProperties(definitions, "McpServersLoadedServer"), - key: "displayName", - desired: events.mcpServersLoadedDisplayName, - }, - ]); -} - -/** - * Applies the additive managed MCP contract from copilot-agent-runtime#17210. - * - * Remove this overlay after the pinned @github/copilot package publishes the - * same schema. Only complete legacy or target contracts are accepted; validation - * happens before mutation so unknown or partially published shapes fail closed. - */ -export function applyManagedMcpSchemaOverlay( - schema: T, - fileName: string, -): T { - if (schema === null || typeof schema !== "object" || Array.isArray(schema)) { - throw new Error(`Managed MCP schema overlay cannot process ${fileName}.`); - } - if (fileName === "api.schema.json") { - applyApiOverlay(schema as SchemaNode); - } else if (fileName === "session-events.schema.json") { - applySessionEventsOverlay(schema as SchemaNode); - } - return schema; -} diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 28f188b23e..714988cb05 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -16,8 +16,6 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; - -import { applyManagedMcpSchemaOverlay } from "./managedMcpSchemaOverlay.js"; import { addManagedApprovalRequiredToPermissionRequests, type ApiSchema, @@ -2239,16 +2237,10 @@ async function generate(): Promise { const apiSchemaPath = await getApiSchemaPath(schemaArgs.apiSchemaPath); const sessionEventsRaw = normalizeSchemaBrandCasing( - applyManagedMcpSchemaOverlay( - JSON.parse(await fs.readFile(sessionEventsSchemaPath, "utf-8")), - path.basename(sessionEventsSchemaPath), - ), + JSON.parse(await fs.readFile(sessionEventsSchemaPath, "utf-8")), ); const apiRaw = normalizeSchemaBrandCasing( - applyManagedMcpSchemaOverlay( - JSON.parse(await fs.readFile(apiSchemaPath, "utf-8")) as ApiSchema, - path.basename(apiSchemaPath), - ), + JSON.parse(await fs.readFile(apiSchemaPath, "utf-8")) as ApiSchema, ); const sessionEventsSchema = propagateInternalVisibility( diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 4e0e1e3375..bad31a7675 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -15,8 +15,6 @@ import { promisify } from "util"; import { COPILOT_CLI_VERSION } from "../../nodejs/src/cliVersion.js"; import { ensureCopilotPackage } from "../../nodejs/scripts/releaseArtifacts.js"; -import { applyManagedMcpSchemaOverlay } from "./managedMcpSchemaOverlay.js"; - export const execFileAsync = promisify(execFile); const __filename = fileURLToPath(import.meta.url); @@ -153,9 +151,7 @@ function renameBrandDefinitionKeys(defs: Record): void { /** Load a JSON schema file and normalize GitHub brand casing in titles, refs, and definition keys. */ export async function loadSchemaJson(filePath: string): Promise { const parsed = JSON.parse(await fs.readFile(filePath, "utf-8")) as T; - return normalizeSchemaBrandCasing( - applyManagedMcpSchemaOverlay(parsed, path.basename(filePath)) - ); + return normalizeSchemaBrandCasing(parsed); } // ── Schema processing ─────────────────────────────────────────────────────── From 9cb96f66989fbfe0e2b0c86a00095de29d479258 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Wed, 16 Sep 2026 11:56:11 +0100 Subject: [PATCH 11/16] Remove unrelated Rust compatibility changes Keep the pull request focused on Connector session configuration and authorization. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/.gitignore | 1 + rust/README.md | 9 --- rust/cli-version-in-process.txt | 11 ---- rust/src/handler.rs | 2 +- rust/src/hooks.rs | 46 +------------- rust/src/resolve.rs | 1 - rust/src/session.rs | 18 +----- rust/tests/session_test.rs | 109 +------------------------------- 8 files changed, 9 insertions(+), 188 deletions(-) delete mode 100644 rust/cli-version-in-process.txt diff --git a/rust/.gitignore b/rust/.gitignore index c4095ffc0f..c149fa3946 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,3 +1,4 @@ /target Cargo.lock.bak cli-version.txt +cli-version-in-process.txt diff --git a/rust/README.md b/rust/README.md index a81541e754..49e6db6609 100644 --- a/rust/README.md +++ b/rust/README.md @@ -534,15 +534,6 @@ let session = client **Hook events:** `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmitted`, `UserPromptTransformed`, `SessionStart`, `SessionEnd`, `ErrorOccurred`. Each carries typed input/output structs. `PostToolUse` only fires on success; override `on_post_tool_use_failure` to observe failed tool calls. Return `HookOutput::None` for events you don't handle. -For request correlation, override `on_hook_with_request_id(event, request_id)` and -delegate to `self.on_hook(event).await` to preserve your existing hook dispatch. -The default implementation delegates automatically, so existing `on_hook` and -per-hook implementations need no changes; `HookContext` still contains only -`session_id`. After successfully writing a response, the SDK calls -`on_hook_response_sent(HookResponseSent)` with the same session and JSON-RPC -request IDs and the runtime hook type. This notification confirms the write, -not CLI processing, and does not fire if the write fails. - ### System Message Transforms Transforms customize system message sections during session creation. The SDK injects `action: "transform"` entries for each section ID your transform handles. diff --git a/rust/cli-version-in-process.txt b/rust/cli-version-in-process.txt deleted file mode 100644 index f8eb65c922..0000000000 --- a/rust/cli-version-in-process.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Auto-generated by rust/scripts/snapshot-bundled-in-process-version.sh -# Do not edit. Regenerated by the publish workflow on every release. -version=1.0.85 -copilot-darwin-arm64=a4445dc278f8f114bfd04699c65f8cbf1bcce4b7ecec3659993c606cdadaa2c1 -copilot-darwin-x64=ff87dadccfba1106631e15ff391b363f31cda2a4a80dbc35fda033e422d71112 -copilot-linux-arm64=e3b5f190051c38267ce58c3d7ce3decba9d0b14c0ef98ac8fef687520aab31db -copilot-linux-x64=0ce01152e3943a3c8119861838b61649ad0b237830411cc7a8bbab47fcf62bb2 -copilot-linuxmusl-arm64=2f4741238349860bea8163afa99aac705b82427fca8de83bd2552af22da144af -copilot-linuxmusl-x64=50652722c0d5fe82f256f9180e03dd55ba00f7ad605e272faffe67767de9995d -copilot-win32-arm64=48209db38a6620bd5931d8431af85308d748a076c4658ff71f27f65585f62e42 -copilot-win32-x64=b9b39908222e24e50af6f32be732d2d2935efbe469a41fc7dc3b000857a6ce97 diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 1905cbfab3..cdb46b3f2b 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -240,7 +240,7 @@ pub trait ElicitationHandler: Send + Sync + 'static { pub struct McpAuthRequest { /// Identifier for the pending MCP OAuth request. pub request_id: RequestId, - /// Stable server identity used as the configuration map key. + /// Display name of the MCP server that requires OAuth. pub server_name: String, /// URL of the MCP server that requires OAuth. pub server_url: String, diff --git a/rust/src/hooks.rs b/rust/src/hooks.rs index 9d713a1efc..4986d6cb18 100644 --- a/rust/src/hooks.rs +++ b/rust/src/hooks.rs @@ -21,17 +21,6 @@ pub struct HookContext { pub session_id: SessionId, } -/// Hook response that was successfully written back to the CLI. -#[derive(Debug, Clone)] -pub struct HookResponseSent { - /// The session this hook was triggered in. - pub session_id: SessionId, - /// JSON-RPC request ID for this hook invocation. - pub request_id: u64, - /// Runtime hook type, such as `userPromptSubmitted` or `preToolUse`. - pub hook_type: String, -} - /// Input for the `preToolUse` hook — received before a tool executes. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -523,18 +512,6 @@ impl HookOutput { /// sets automatically). #[async_trait] pub trait SessionHooks: Send + Sync + 'static { - /// Request-aware dispatch for correlating a hook invocation with - /// [`on_hook_response_sent`](Self::on_hook_response_sent). - /// - /// `request_id` is the JSON-RPC request ID also reported in - /// [`HookResponseSent::request_id`]. The default delegates to - /// [`on_hook`](Self::on_hook), preserving existing dispatch and per-hook - /// implementations. Override this method when correlation is needed, - /// and call `self.on_hook(event).await` to retain that dispatch. - async fn on_hook_with_request_id(&self, event: HookEvent, _request_id: u64) -> HookOutput { - self.on_hook(event).await - } - /// Top-level dispatch. The default implementation fans out to the /// per-hook methods below; override this only if you want a single /// matching point across all hook types. @@ -593,14 +570,6 @@ pub trait SessionHooks: Send + Sync + 'static { } } - /// Called after a hook response is successfully written back to the CLI. - /// - /// This confirms transport delivery, not CLI processing. It is not called - /// when writing the response fails. Correlate it with - /// [`on_hook_with_request_id`](Self::on_hook_with_request_id) using the - /// response's session and request IDs. - async fn on_hook_response_sent(&self, _response: HookResponseSent) {} - /// Called before a tool executes. Return `Some(output)` to approve/deny /// or modify the call, or `None` (default) to pass through unchanged. async fn on_pre_tool_use( @@ -711,20 +680,9 @@ pub trait SessionHooks: Send + Sync + 'static { /// Returns `Ok(Value)` shaped like `{ "output": ... }` on success. /// If no hook is registered ([`HookOutput::None`]), the output is an empty /// object: `{ "output": {} }`. -#[cfg(test)] -async fn dispatch_hook( - hooks: &dyn SessionHooks, - session_id: &SessionId, - hook_type: &str, - raw_input: Value, -) -> Result { - dispatch_hook_for_request(hooks, session_id, 0, hook_type, raw_input).await -} - -pub(crate) async fn dispatch_hook_for_request( +pub(crate) async fn dispatch_hook( hooks: &dyn SessionHooks, session_id: &SessionId, - request_id: u64, hook_type: &str, raw_input: Value, ) -> Result { @@ -784,7 +742,7 @@ pub(crate) async fn dispatch_hook_for_request( }; let dispatch_start = Instant::now(); - let output = hooks.on_hook_with_request_id(event, request_id).await; + let output = hooks.on_hook(event).await; tracing::debug!( elapsed_ms = dispatch_start.elapsed().as_millis(), session_id = %session_id, diff --git a/rust/src/resolve.rs b/rust/src/resolve.rs index 1fbeb7c9f7..c72610b3ec 100644 --- a/rust/src/resolve.rs +++ b/rust/src/resolve.rs @@ -140,7 +140,6 @@ fn extracted_program(_use_runtime_wrapper: bool) -> Option { None } -#[cfg(any(feature = "bundled-cli", has_extracted_cli, test))] fn validate_runtime_pair(wrapper: &Path) -> Result<(), Error> { let wrapper_valid = wrapper .metadata() diff --git a/rust/src/session.rs b/rust/src/session.rs index b9007ad804..f7e6d5380e 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -3144,11 +3144,7 @@ async fn handle_request( .unwrap_or(Value::Object(Default::default())); let rpc_result = if let Some(hooks) = hooks { - match crate::hooks::dispatch_hook_for_request( - hooks, &sid, request.id, hook_type, input, - ) - .await - { + match crate::hooks::dispatch_hook(hooks, &sid, hook_type, input).await { Ok(output) => output, Err(e) => { warn!(error = %e, hook_type = hook_type, "hook dispatch failed"); @@ -3165,17 +3161,7 @@ async fn handle_request( result: Some(rpc_result), error: None, }; - if client.send_response(&rpc_response).await.is_ok() - && let Some(hooks) = hooks - { - hooks - .on_hook_response_sent(crate::hooks::HookResponseSent { - session_id: sid, - request_id: request.id, - hook_type: hook_type.to_string(), - }) - .await; - } + let _ = client.send_response(&rpc_response).await; } "userInput.request" => { diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 863ba49b5c..9675401810 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -6022,25 +6022,11 @@ async fn create_session_pair_with_hooks( #[tokio::test] async fn hooks_invoke_dispatches_to_session_hooks() { - use github_copilot_sdk::hooks::{ - HookEvent, HookOutput, HookResponseSent, PreToolUseOutput, SessionHooks, - }; + use github_copilot_sdk::hooks::{HookEvent, HookOutput, PreToolUseOutput, SessionHooks}; - struct PolicyHooks { - invoked: tokio::sync::mpsc::UnboundedSender<(SessionId, u64)>, - response_sent: tokio::sync::mpsc::UnboundedSender, - } + struct PolicyHooks; #[async_trait] impl SessionHooks for PolicyHooks { - async fn on_hook_with_request_id(&self, event: HookEvent, request_id: u64) -> HookOutput { - if let HookEvent::PreToolUse { ctx, .. } = &event { - self.invoked - .send((ctx.session_id.clone(), request_id)) - .unwrap(); - } - self.on_hook(event).await - } - async fn on_hook(&self, event: HookEvent) -> HookOutput { match event { HookEvent::PreToolUse { input, .. } => { @@ -6057,20 +6043,9 @@ async fn hooks_invoke_dispatches_to_session_hooks() { _ => HookOutput::None, } } - - async fn on_hook_response_sent(&self, response: HookResponseSent) { - self.response_sent.send(response).unwrap(); - } } - let (response_sent_tx, mut response_sent_rx) = - tokio::sync::mpsc::unbounded_channel::(); - let (invoked_tx, mut invoked_rx) = tokio::sync::mpsc::unbounded_channel(); - let (_session, mut server) = create_session_pair_with_hooks(Arc::new(PolicyHooks { - invoked: invoked_tx, - response_sent: response_sent_tx, - })) - .await; + let (_session, mut server) = create_session_pair_with_hooks(Arc::new(PolicyHooks)).await; // Send a hooks.invoke request for a denied tool server @@ -6098,84 +6073,6 @@ async fn hooks_invoke_dispatches_to_session_hooks() { response["result"]["output"]["permissionDecisionReason"], "destructive" ); - let response_sent = timeout(TIMEOUT, response_sent_rx.recv()) - .await - .unwrap() - .unwrap(); - assert_eq!(response_sent.request_id, 300); - assert_eq!(response_sent.hook_type, "preToolUse"); - assert_eq!(response_sent.session_id.as_str(), server.session_id); - let invoked = timeout(TIMEOUT, invoked_rx.recv()).await.unwrap().unwrap(); - assert_eq!( - invoked, - (response_sent.session_id, response_sent.request_id) - ); -} - -#[test] -fn hooks_context_supports_legacy_literal_and_exhaustive_pattern() { - use github_copilot_sdk::hooks::HookContext; - - let context = HookContext { - session_id: SessionId::from("legacy-session"), - }; - let HookContext { session_id } = context; - assert_eq!(session_id.as_str(), "legacy-session"); -} - -#[tokio::test] -async fn hooks_response_sent_is_not_called_when_write_fails() { - use github_copilot_sdk::hooks::{HookEvent, HookOutput, HookResponseSent, SessionHooks}; - - struct GatedHooks { - entered: Notify, - release: Notify, - response_sent: tokio::sync::mpsc::UnboundedSender, - } - - #[async_trait] - impl SessionHooks for GatedHooks { - async fn on_hook(&self, _event: HookEvent) -> HookOutput { - self.entered.notify_one(); - self.release.notified().await; - HookOutput::None - } - - async fn on_hook_response_sent(&self, response: HookResponseSent) { - self.response_sent.send(response).unwrap(); - } - } - - let (response_sent_tx, mut response_sent_rx) = tokio::sync::mpsc::unbounded_channel(); - let hooks = Arc::new(GatedHooks { - entered: Notify::new(), - release: Notify::new(), - response_sent: response_sent_tx, - }); - let (_session, mut server) = create_session_pair_with_hooks(hooks.clone()).await; - server - .send_request( - 302, - "hooks.invoke", - serde_json::json!({ - "sessionId": server.session_id, - "hookType": "sessionEnd", - "input": { - "sessionId": server.session_id, - "timestamp": 1234567890, - "cwd": ".", - "reason": "complete" - } - }), - ) - .await; - timeout(TIMEOUT, hooks.entered.notified()).await.unwrap(); - assert!(response_sent_rx.try_recv().is_err()); - - // Keep the inbound stream open, but force the response write to fail. - drop(server.read); - hooks.release.notify_one(); - assert!(timeout(TIMEOUT, response_sent_rx.recv()).await.is_err()); } #[tokio::test] From 95e67ba32545a988cae637b0888637f063a44478 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Fri, 18 Sep 2026 16:13:15 +0100 Subject: [PATCH 12/16] Expose experimental Connector session APIs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/mcp.md | 178 ++--- dotnet/README.md | 119 +-- dotnet/src/Client.cs | 45 -- dotnet/src/Generated/Rpc.cs | 657 ++++++++++++++++ dotnet/src/Session.cs | 76 -- dotnet/src/Types.cs | 92 --- .../test/Unit/ClientSessionLifetimeTests.cs | 169 +--- dotnet/test/Unit/RpcConnectorsTests.cs | 297 +++++++ dotnet/test/Unit/SerializationTests.cs | 44 -- go/README.md | 48 +- go/client.go | 24 - go/client_test.go | 91 --- go/connectors_rpc_test.go | 309 ++++++++ go/rpc/zrpc.go | 489 ++++++++++++ go/rpc/zrpc_encoding.go | 80 ++ go/session.go | 77 -- go/session_event_serialization_test.go | 73 -- go/session_test.go | 327 -------- go/types.go | 92 --- java/README.md | 92 ++- java/scripts/codegen/java.ts | 10 +- .../generated/rpc/ConnectorAvailability.java | 37 + .../generated/rpc/ConnectorCatalogEntry.java | 36 + .../generated/rpc/ConnectorCatalogResult.java | 32 + .../generated/rpc/ConnectorCatalogStatus.java | 41 + .../generated/rpc/ConnectorConnectResult.java | 36 + .../rpc/ConnectorConnectResultConnected.java | 37 + ...ConnectorConnectResultConsentRequired.java | 44 ++ .../rpc/ConnectorConnectResultPending.java | 37 + .../generated/rpc/ConnectorMcpStatus.java | 45 ++ .../generated/rpc/ConnectorRuntimeStatus.java | 31 + .../generated/rpc/ConnectorStatus.java | 38 + .../generated/rpc/SessionConnectorsApi.java | 167 ++++ .../rpc/SessionConnectorsConnectParams.java | 34 + ...ionConnectorsContinueConnectionParams.java | 38 + .../SessionConnectorsDisconnectParams.java | 34 + .../SessionConnectorsDisconnectResult.java | 32 + ...essionConnectorsGetCapabilitiesParams.java | 30 + ...essionConnectorsGetCapabilitiesResult.java | 42 + .../rpc/SessionConnectorsGetStatusParams.java | 30 + .../rpc/SessionConnectorsGetStatusResult.java | 41 + .../rpc/SessionConnectorsListParams.java | 32 + .../rpc/SessionConnectorsListResult.java | 35 + .../rpc/SessionConnectorsReconcileParams.java | 34 + .../rpc/SessionConnectorsReconcileResult.java | 41 + .../rpc/SessionConnectorsReconnectParams.java | 34 + .../rpc/SessionConnectorsRefreshParams.java | 32 + .../rpc/SessionConnectorsRefreshResult.java | 35 + .../copilot/generated/rpc/SessionRpc.java | 3 + .../com/github/copilot/CopilotClient.java | 73 +- .../com/github/copilot/CopilotSession.java | 99 +-- .../github/copilot/SessionRequestBuilder.java | 8 - .../copilot/rpc/ConnectorMcpServerConfig.java | 175 ----- .../copilot/rpc/CreateSessionRequest.java | 29 - .../copilot/rpc/McpHeadersRefreshHandler.java | 33 - .../rpc/McpHeadersRefreshInvocation.java | 36 - .../copilot/rpc/McpHeadersRefreshRequest.java | 22 - .../copilot/rpc/McpHeadersRefreshResult.java | 61 -- .../copilot/rpc/ResumeSessionConfig.java | 65 -- .../copilot/rpc/ResumeSessionRequest.java | 29 - .../com/github/copilot/rpc/SessionConfig.java | 65 -- .../CopilotExperimentalProcessorTest.java | 119 --- .../McpAuthInterestRegistrationTest.java | 38 - .../copilot/McpHeadersRefreshHandlerTest.java | 172 ---- .../com/github/copilot/RpcWrappersTest.java | 82 +- .../copilot/SessionRequestBuilderTest.java | 40 - .../rpc/ConnectorSerializationTest.java | 92 +++ nodejs/README.md | 27 +- nodejs/src/client.ts | 38 - nodejs/src/generated/rpc.ts | 385 +++++++++ nodejs/src/index.ts | 18 +- nodejs/src/session.ts | 70 -- nodejs/src/types.ts | 75 +- nodejs/test/client.test.ts | 164 ---- nodejs/test/connectors.test.ts | 147 ++++ nodejs/test/e2e/mcp_oauth.e2e.test.ts | 52 +- nodejs/test/shared-codegen.test.ts | 63 +- python/README.md | 23 +- python/copilot/__init__.py | 10 - python/copilot/client.py | 41 - python/copilot/generated/rpc.py | 546 ++++++++++++- python/copilot/session.py | 157 ---- python/test_client.py | 170 ---- python/test_connectors.py | 101 +++ rust/README.md | 139 ++-- rust/src/generated/api_types.rs | 574 ++++++++++++++ rust/src/generated/rpc.rs | 305 ++++++++ rust/src/handler.rs | 99 +-- rust/src/session.rs | 250 ++---- rust/src/types.rs | 181 +---- rust/src/wire.rs | 6 +- rust/tests/api_types_test.rs | 73 +- rust/tests/session_test.rs | 698 ++++++----------- .../connector-session-api-overlay.json | 737 ++++++++++++++++++ scripts/codegen/connectorSessionApiOverlay.ts | 100 +++ scripts/codegen/rust.ts | 10 +- scripts/codegen/utils.ts | 6 +- 97 files changed, 6758 insertions(+), 4242 deletions(-) create mode 100644 dotnet/test/Unit/RpcConnectorsTests.cs create mode 100644 go/connectors_rpc_test.go create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsConnectParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileResult.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconnectParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshParams.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshResult.java delete mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java delete mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java delete mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java delete mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java delete mode 100644 java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java delete mode 100644 java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java create mode 100644 java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java create mode 100644 nodejs/test/connectors.test.ts create mode 100644 python/test_connectors.py create mode 100644 scripts/codegen/connector-session-api-overlay.json create mode 100644 scripts/codegen/connectorSessionApiOverlay.ts diff --git a/docs/features/mcp.md b/docs/features/mcp.md index 512ecb0cf6..a111040cf9 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -15,143 +15,81 @@ The Copilot SDK can integrate with **MCP servers** (Model Context Protocol) to e * Call external APIs * And much more -## Server transports +## Server types -The SDK supports two MCP transport families: +The SDK supports two types of MCP servers: | Type | Description | Use Case | |------|-------------|----------| | **Local/Stdio** | Runs as a subprocess, communicates via stdin/stdout | Local tools, file access, custom scripts | | **HTTP/SSE** | Remote server accessed via HTTP | Shared services, cloud-hosted tools | -Transport and configuration origin are separate concepts. A Connector MCP -endpoint uses the HTTP transport, but its configuration comes from the -Copilot Connectors service rather than user or workspace settings. +## Copilot Connectors -## Integrating Copilot Connectors +The experimental Connector session API lets SDK hosts offer the same non-UI +Connector flow as Copilot CLI. The runtime owns catalog validation, GitHub +authorization, bounded connection polling, MCP projection, and authoritative +same-session reconciliation. The SDK host owns account selection, browser +opening, confirmation, and presentation. -People use Copilot Connectors by choosing a Connector in the host application, -completing consent when required, and then using its tools. They do not enter an -MCP endpoint, configure authorization, or manage the underlying MCP server. +Use `mcpServers` for MCP servers that your application configures directly. +Use the Connector API for service-backed catalog entries whose connection and +session lifecycle the runtime manages. -An SDK-based host implements that experience: - -1. Retrieve the supported Connector catalog for the selected account. -1. Present Connector metadata and connection status, and perform Connect, - Reconnect, or Disconnect through the Connector service. -1. Consume each service-advertised MCP URL without constructing or rewriting it. -1. Supply connected Connector endpoints to the session through - `connectorMcpServers`. -1. Authorize calls to those endpoints for the same selected GitHub account - through `onMcpHeadersRefresh`. - -The SDK API described here is the session bridge for connected Connectors. It -does not fetch the catalog, render Connector UI, perform consent, or manage -service connections. Existing local stdio, HTTP, SSE, and OAuth MCP behavior -remains unchanged. - -The integration boundary is: - -| Responsibility | Owner | -| --- | --- | -| List available Connectors and connection state | Connector service and host application | -| Connect, Reconnect, Disconnect, browser consent, and polling | Connector service and host application | -| Select and pin the active account | Host application | -| Load connected Connector endpoints and supply service authorization | Copilot SDK | -| List server state and tools, enable or disable per session, and receive status events | Copilot SDK | -| Apply MCP policy, cache authorization, and retry once after a 401 | Connected Copilot runtime | - -> [!NOTE] -> Connector session integration is experimental and only takes effect when -> enabled by the connected runtime. The host must re-supply the effective -> connected set on cold resume. - -The host supplies each connected endpoint under a stable Connector server key: +Start with capability detection. When availability is `enabled`, obtain the +opaque account ID from your host's account-selection flow, list the catalog, +reconcile already-connected entries, and drive the typed connection result: ```typescript -const session = await client.createSession({ - connectorMcpServers: { - [connector.serverKey]: { - displayName: connector.displayName, - url: connector.advertisedMcpUrl, - tools: connector.tools, - timeout: 30_000, - authorizationCacheTtlMs: 60_000, - }, - }, - onMcpHeadersRefresh: async ({ serverKey, serverUrl, reason }) => { - const authorization = await getConnectorAuthorization({ - account: selectedAccount, - serverKey, - serverUrl, - reason, +async function connectConnector(session, accountId, openConsentUrl) { + const capabilities = await session.rpc.connectors.getCapabilities(); + if (capabilities.availability !== "enabled") return; + + const catalog = await session.rpc.connectors.list({ accountId }); + await session.rpc.connectors.reconcile({ accountId }); + const connectorName = await chooseConnector(catalog.connectors); + + const result = await session.rpc.connectors.connect({ + accountId, + connectorName, + }); + if (result.kind === "consent_required") { + await openConsentUrl(result.consentUrl); + } + if (result.kind !== "connected") { + await session.rpc.connectors.continueConnection({ + continuationId: result.continuationId, + maxAttempts: Math.min(30, capabilities.maxPollAttempts), + pollIntervalMs: Math.min(2_000, capabilities.maxPollIntervalMs), + deadlineMs: Math.min(60_000, capabilities.maxDeadlineMs), }); - return { - headers: { - Authorization: ["Bearer", authorization.accessToken].join(" "), - }, - ttlMs: authorization.expiresInMs, - }; - }, -}); + } +} ``` -The corresponding configuration and callback names are: - -| SDK | Connector MCP servers | Header refresh callback | -| --- | --- | --- | -| Node.js | `connectorMcpServers` | `onMcpHeadersRefresh` | -| Python | `connector_mcp_servers` | `on_mcp_headers_refresh` | -| Go | `ConnectorMCPServers` | `OnMCPHeadersRefresh` | -| .NET | `ConnectorMcpServers` | `OnMcpHeadersRefresh` | -| Java | `setConnectorMcpServers(...)` | `setOnMcpHeadersRefresh(...)` | -| Rust | `with_connector_mcp_servers(...)` | `with_mcp_headers_handler(...)` | - -The typed session MCP API supplies UI hooks for already loaded Connectors: list -server status and tools, enable or disable a server for the current session, and -subscribe to MCP loaded and status-change events. Connect, Reconnect, -Disconnect, browser consent, and catalog polling remain operations against the -Connector service. - -> [!WARNING] -> The current public runtime contract cannot replace the authoritative -> Connector set on a running session. After a Connect, Reconnect, or Disconnect, -> create or cold-resume a session with the updated `connectorMcpServers` map. -> Do not treat the generic MCP `stopServer` operation as Connector Disconnect: -> an MCP reload can restore a server from the session's original configuration. - -### Host responsibilities - -SDK hosts must enforce these boundaries: - -* **Trusted catalog injection**: Only inject server identities, display metadata, - endpoints, and tool policy from a trusted catalog. Do not treat model output - or untrusted content as catalog configuration. -* **Connector service authorization**: Supply the selected account's - short-lived GitHub authorization only to the exact endpoint advertised by the - Connector service. The Connector service, not the SDK host, owns downstream - provider credentials such as Microsoft or Slack tokens. -* **Memory-only authorization**: Keep access tokens and derived authorization - headers in memory. Do not place them in `connectorMcpServers`, session history, - workspace state, or persistent MCP OAuth storage. -* **Account binding**: Fetch the catalog and authorization for the same - authenticated account. Invalidate cached authorization before switching - accounts so one account's access cannot be reused by another. -* **Expiry and revocation**: Set `ttlMs` to the remaining authorization lifetime. - The runtime clamps it to `authorizationCacheTtlMs`. Throw from the callback - when Connector authorization is denied, fails, or is revoked; the SDK - forwards an explicit error without converting it to a successful empty - response. -* **Cold resume**: Re-supply both the connected Connector endpoints and the - header refresh callback when cold-resuming a session. Connector configuration - and authorization are not recovered from persisted session state. - -Returning no result from the callback reports that no dynamic headers are -available for the Connector endpoint. Existing static `headers`, arbitrary HTTP -servers configured through `mcpServers`, and MCP OAuth handlers continue to use -their current behavior. +The generated API also exposes `refresh`, `reconnect`, `disconnect`, +`getStatus`, and `reconcile`. Connect and continuation results are discriminated +as `connected`, `consent_required`, or `pending`. Disconnect and reconcile +return authoritative session state, including live Connector-owned MCP status. +Account IDs and continuation IDs are opaque. The host obtains the account ID +through its existing account-selection flow and passes only that identifier to +Connector methods; credentials and provider tokens never appear in Connector +DTOs. + +| SDK | Connector API | +| --- | --- | +| Node.js | `session.rpc.connectors` | +| Python | `session.rpc.connectors` | +| Go | `session.RPC.Connectors` | +| .NET | `session.Rpc.Connectors` | +| Java | `session.getRpc().connectors` | +| Rust | `session.rpc().connectors()` | + +> [!NOTE] +> Connector support is experimental and may be unavailable in some runtime +> versions. Always call the capability method before using the API. ## Configuration diff --git a/dotnet/README.md b/dotnet/README.md index 01cb0cb08d..caabcba068 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -77,6 +77,44 @@ tool name is `-`. For `AvailableTools` and `mcp:-`. For `CustomAgents[].Tools` and `DefaultAgent.ExcludedTools`, use `-` directly. +## Experimental Connector API + +`session.Rpc.Connectors` exposes the runtime-owned Connector catalog and +lifecycle. The API is experimental, so call `GetCapabilitiesAsync()` first and +use the remaining methods only when availability is enabled. + +```csharp +using GitHub.Copilot.Rpc; + +const string accountId = "opaque-account-selection-id"; +var connectors = session.Rpc.Connectors; +var capabilities = await connectors.GetCapabilitiesAsync(); + +if (capabilities.Availability == ConnectorAvailability.Enabled) +{ + var catalog = await connectors.ListAsync(accountId); + await connectors.ReconcileAsync(accountId); + var result = await connectors.ConnectAsync(accountId, catalog.Connectors[0].Name); + + if (result is ConnectorConnectResultConsentRequired consent) + { + OpenBrowser(consent.ConsentUrl); + result = await connectors.ContinueConnectionAsync( + consent.ContinuationId, + maxAttempts: capabilities.MaxPollAttempts, + pollIntervalMs: capabilities.MaxPollIntervalMs, + deadlineMs: capabilities.MaxDeadlineMs); + } +} +``` + +The nine experimental methods are `GetCapabilitiesAsync`, `GetStatusAsync`, +`ListAsync`, `RefreshAsync`, `ConnectAsync`, `ReconnectAsync`, +`ContinueConnectionAsync`, `DisconnectAsync`, and `ReconcileAsync`. The host +selects the opaque GitHub account and handles consent UI. Connector methods +accept only that account ID, never credentials or provider tokens; credentials +and MCP projection remain runtime-owned. + ## API Reference ### CopilotClient @@ -146,8 +184,6 @@ Create a new conversation session. - `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory. - `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled. - `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`. -- `ConnectorMcpServers` - Connected Copilot Connector MCP endpoints supplied from the service catalog, keyed by stable server key. Configuration is non-secret. -- `OnMcpHeadersRefresh` - Supplies short-lived client-to-Copilot Connectors service authorization headers, normally the selected account's GitHub bearer. `McpHeadersRefreshContext.ServerKey` matches the `ConnectorMcpServers` key. - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`. @@ -162,8 +198,6 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i - `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`. - `AskUserVariant` - Re-supplies the model-facing `ask_user` tool shape on cold resume. -- `ConnectorMcpServers` - Re-supplies the connected Connector MCP endpoint set on cold resume. -- `OnMcpHeadersRefresh` - Re-registers the callback that supplies Copilot Connectors service authorization headers. ```csharp await using var session = await client.CreateSessionAsync(new SessionConfig @@ -303,83 +337,6 @@ Abort the currently processing message in this session. Get all events/messages from this session. -##### Connector MCP configuration and runtime APIs - -Supply connected Copilot Connector MCP endpoints when creating a session and -re-supply the effective connected set after a cold resume. - -The endpoint configuration is non-secret. `OnMcpHeadersRefresh` supplies -short-lived authorization from the SDK client to the exact service-advertised -Copilot Connectors MCP endpoint, normally using the selected account's GitHub -bearer. It does not supply Outlook, Slack, or other downstream provider tokens; -the Copilot Connectors service owns those. Hosts should match -`McpHeadersRefreshContext.ServerKey` and `ServerUrl` to the service-catalog entry -before returning headers. `ConnectorMcpServerConfig.AuthorizationCacheTtlMs` -caps how long the runtime may reuse service authorization, while -`McpHeadersRefreshResult.TtlMs` reports the remaining lifetime of the -authorization returned by a specific refresh. - -```csharp -var connectorServers = new Dictionary -{ - ["github"] = new() - { - DisplayName = "GitHub", - Url = "https://example.com/github/mcp", - AuthorizationCacheTtlMs = 60_000, - }, -}; - -async Task RefreshConnectorHeadersAsync( - McpHeadersRefreshContext context) => new() -{ - Headers = await GetCopilotConnectorsAuthorizationHeadersAsync( - context.ServerKey, - context.ServerUrl), - TtlMs = 60_000, -}; - -await using var session = await client.CreateSessionAsync(new SessionConfig -{ - ConnectorMcpServers = connectorServers, - OnMcpHeadersRefresh = RefreshConnectorHeadersAsync, -}); - -// Re-supply the connected set and callback after a cold resume. -await using var resumed = await client.ResumeSessionAsync("session-id", new ResumeSessionConfig -{ - ConnectorMcpServers = connectorServers, - OnMcpHeadersRefresh = RefreshConnectorHeadersAsync, -}); -``` - -The current public runtime contract does not support authoritative same-session -Connect or Disconnect reconciliation for `ConnectorMcpServers`. Generic MCP -start and stop operations are transient, and a runtime reload can restore the -configuration supplied when the session was created. Until the runtime promotes -its internal replacement and removal operations, create a new session or -cold-resume with the updated connected set. - -The generated session MCP API provides the public runtime operations needed for -Connector status and lifecycle UI: - -```csharp -var servers = await session.Rpc.Mcp.ListAsync(); -var running = await session.Rpc.Mcp.IsServerRunningAsync("github"); -var tools = await session.Rpc.Mcp.ListToolsAsync("github"); -await session.Rpc.Mcp.DisableAsync("github"); -await session.Rpc.Mcp.EnableAsync("github"); - -using var statusChanges = session.On(evt => - Console.WriteLine($"{evt.Data.ServerName}: {evt.Data.Status}")); -``` - -`SessionMcpServersLoadedEvent`, `SessionMcpServerRemovedEvent`, and -`SessionMcpServerNeedsReconnectEvent` provide the related lifecycle hooks. The -application host owns Connector service-catalog access, consent, polling, and -UI. These generic runtime APIs expose status and operational controls; they do -not reconcile Connector service-catalog membership. - ##### `DisposeAsync(): ValueTask` Close the session and release in-memory resources. Session data on disk is preserved — the conversation can be resumed later via `ResumeSessionAsync()`. To permanently delete session data, use `client.DeleteSessionAsync()`. diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index fc0b668ed9..dff4681a80 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -868,7 +868,6 @@ private CopilotSession InitializeSession( config.OnPermissionRequest, config.EnableManagedSettings is true || config.ManagedSettings is not null); session.RegisterMcpAuthHandler(config.OnMcpAuthRequest); - session.RegisterMcpHeadersRefreshHandler(config.OnMcpHeadersRefresh); session.RegisterCommands(config.Commands); session.RegisterElicitationHandler(config.OnElicitationRequest); session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest); @@ -1251,7 +1250,6 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, - ToManagedMcpServers(config.ConnectorMcpServers), config.McpOAuthTokenStorage, config.AuthClientIdMetadataUrl, "direct", @@ -1355,10 +1353,6 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken); } - if (config.OnMcpHeadersRefresh is not null) - { - await session.Rpc.EventLog.RegisterInterestAsync("mcp.headers_refresh_required", cancellationToken); - } session.WorkspacePath = response.WorkspacePath; session.SetCapabilities(response.Capabilities); @@ -1509,7 +1503,6 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, - ToManagedMcpServers(config.ConnectorMcpServers), config.McpOAuthTokenStorage, config.AuthClientIdMetadataUrl, "direct", @@ -1570,10 +1563,6 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes { await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken); } - if (config.OnMcpHeadersRefresh is not null) - { - await session.Rpc.EventLog.RegisterInterestAsync("mcp.headers_refresh_required", cancellationToken); - } await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false); if (registrationId is not null) @@ -3016,39 +3005,7 @@ public static string Escape(string arg) } } - private static Dictionary? ToManagedMcpServers( - IDictionary? connectorMcpServers) - { - if (connectorMcpServers is null) - { - return null; - } - - var managedMcpServers = connectorMcpServers is Dictionary dictionary - ? new Dictionary(dictionary.Comparer) - : new Dictionary(); - - foreach (var (name, config) in connectorMcpServers) - { - managedMcpServers.Add(name, new ManagedMcpServerConfig( - config.DisplayName, - config.Url, - config.Tools, - config.Timeout, - config.AuthorizationCacheTtlMs)); - } - - return managedMcpServers; - } - // Request/Response types for RPC - internal sealed record ManagedMcpServerConfig( - string DisplayName, - string Url, - IList? Tools, - long? Timeout, - long? HeadersRefreshTtlMs); - internal record CreateSessionRequest( string? Model, string? SessionId, @@ -3077,7 +3034,6 @@ internal record CreateSessionRequest( bool? Streaming, bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, - [property: JsonPropertyName("managedMcpServers")] IDictionary? ManagedMcpServers, McpOAuthTokenStorageMode? McpOAuthTokenStorage, string? AuthClientIdMetadataUrl, string? EnvValueMode, @@ -3208,7 +3164,6 @@ internal record ResumeSessionRequest( bool? Streaming, bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, - [property: JsonPropertyName("managedMcpServers")] IDictionary? ManagedMcpServers, McpOAuthTokenStorageMode? McpOAuthTokenStorage, string? AuthClientIdMetadataUrl, string? EnvValueMode, diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 81cd4e6da8..eb39d84b94 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -19773,6 +19773,293 @@ internal sealed class ScheduleStopRequest public string SessionId { get; set; } = string.Empty; } +///

Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +[Experimental(Diagnostics.Experimental)] +public sealed class ConnectorCapabilities +{ + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } + + /// Current session availability. Disabled availability is reported without making a Connector request. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { get; set; } + + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + [JsonPropertyName("consentContinuation")] + public bool ConsentContinuation { get; set; } + + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + [JsonPropertyName("maxDeadlineMs")] + public long MaxDeadlineMs { get; set; } + + /// Maximum accepted polling attempts for one continuation call. + [JsonPropertyName("maxPollAttempts")] + public long MaxPollAttempts { get; set; } + + /// Maximum accepted delay in milliseconds between polling attempts. + [JsonPropertyName("maxPollIntervalMs")] + public long MaxPollIntervalMs { get; set; } + + /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + [JsonPropertyName("opaqueAccountSelection")] + public bool OpaqueAccountSelection { get; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionConnectorsGetCapabilitiesRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Credential-free Connector catalog entry. +[Experimental(Diagnostics.Experimental)] +public sealed class ConnectorCatalogEntry +{ + /// Untrusted service description, when present. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Untrusted display label from the service. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Canonical Connector name used by lifecycle methods. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Opaque stable runtime IDs currently projected into the session for this Connector. + [JsonPropertyName("runtimeServerIds")] + public IList RuntimeServerIds { get => field ??= []; set; } + + /// Current authoritative service connection state. + [JsonPropertyName("status")] + public ConnectorCatalogStatus Status { get; set; } +} + +/// Validated Connector catalog snapshot cached by the session. +[Experimental(Diagnostics.Experimental)] +public sealed class ConnectorCatalogResult +{ + /// Validated catalog entries in service order. + [JsonPropertyName("connectors")] + public IList Connectors { get => field ??= []; set; } + + /// Unix epoch milliseconds when this snapshot was accepted. + [JsonPropertyName("refreshedAtMs")] + public long RefreshedAtMs { get; set; } + + /// Monotonically increasing session-local catalog revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } +} + +/// Live status of one session-owned MCP projection. +[Experimental(Diagnostics.Experimental)] +public sealed class ConnectorRuntimeStatus +{ + /// Canonical Connector name that owns this server. + [JsonPropertyName("connectorName")] + public string ConnectorName { get; set; } = string.Empty; + + /// Opaque runtime server ID. + [JsonPropertyName("runtimeServerId")] + public string RuntimeServerId { get; set; } = string.Empty; + + /// Current live MCP host status. + [JsonPropertyName("status")] + public ConnectorMcpStatus Status { get; set; } +} + +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +[Experimental(Diagnostics.Experimental)] +public sealed class ConnectorStatus +{ + /// Opaque account selection pinned to this session, when one has been selected. + [JsonPropertyName("accountId")] + public string? AccountId { get; set; } + + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { get; set; } + + /// Current feature and session availability. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { get; set; } + + /// Latest validated catalog snapshot, when available. + [JsonPropertyName("catalog")] + public ConnectorCatalogResult? Catalog { get; set; } + + /// Number of active opaque connection continuations. + [JsonPropertyName("pendingConnections")] + public long PendingConnections { get; set; } + + /// Live MCP status for every Connector-owned runtime server. + [JsonPropertyName("runtimeServers")] + public IList RuntimeServers { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionConnectorsGetStatusRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. +[Experimental(Diagnostics.Experimental)] +internal sealed class ConnectorAccountRequest +{ + /// Opaque account selection ID previously returned by an account discovery API. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Typed result of initiating or continuing a Connector connection. +/// Polymorphic base type discriminated by kind. +[Experimental(Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ConnectorConnectResultConnected), "connected")] +[JsonDerivedType(typeof(ConnectorConnectResultConsentRequired), "consent_required")] +[JsonDerivedType(typeof(ConnectorConnectResultPending), "pending")] +public partial class ConnectorConnectResult +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + +/// The service is connected and the session MCP graph was reconciled. +/// The connected variant of . +[Experimental(Diagnostics.Experimental)] +public partial class ConnectorConnectResultConnected : ConnectorConnectResult +{ + /// + [JsonIgnore] + public override string Kind => "connected"; + + /// Fresh authoritative Connector state after MCP reconciliation. + [JsonPropertyName("status")] + public required ConnectorStatus Status { get; set; } +} + +/// Host-owned consent is required before bounded continuation can complete. +/// The consent_required variant of . +[Experimental(Diagnostics.Experimental)] +public partial class ConnectorConnectResultConsentRequired : ConnectorConnectResult +{ + /// + [JsonIgnore] + public override string Kind => "consent_required"; + + /// Validated HTTPS consent URL. The runtime does not open it. + [JsonPropertyName("consentUrl")] + public required string ConsentUrl { get; set; } + + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { get; set; } +} + +/// The service is still completing the connection without a consent URL. +/// The pending variant of . +[Experimental(Diagnostics.Experimental)] +public partial class ConnectorConnectResultPending : ConnectorConnectResult +{ + /// + [JsonIgnore] + public override string Kind => "pending"; + + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { get; set; } +} + +/// Selects one Connector and the pinned host-owned account used for its service and MCP authorization. +[Experimental(Diagnostics.Experimental)] +internal sealed class ConnectorConnectRequest +{ + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// Canonical Connector name from the current catalog. + [JsonPropertyName("connectorName")] + public string ConnectorName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Explicitly bounded continuation of a pending Connector connection. +[Experimental(Diagnostics.Experimental)] +internal sealed class ConnectorContinueRequest +{ + /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + [JsonPropertyName("continuationId")] + public string ContinuationId { get; set; } = string.Empty; + + /// Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. + [JsonPropertyName("deadlineMs")] + public long DeadlineMs { get; set; } + + /// Maximum catalog requests made by this call. Must be between one and the capability limit. + [JsonPropertyName("maxAttempts")] + public long MaxAttempts { get; set; } + + /// Delay in milliseconds between attempts. Must not exceed the capability limit. + [JsonPropertyName("pollIntervalMs")] + public long PollIntervalMs { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Authoritative result after disconnect and MCP reconciliation. +[Experimental(Diagnostics.Experimental)] +public sealed class ConnectorDisconnectResult +{ + /// Whether the service accepted the idempotent disconnect. + [JsonPropertyName("disconnected")] + public bool Disconnected { get; set; } + + /// Fresh authoritative session state after removing Connector-owned MCP servers. + [JsonPropertyName("status")] + public ConnectorStatus Status { get => field ??= new(); set; } +} + +/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +[Experimental(Diagnostics.Experimental)] +internal sealed class ConnectorReconcileRequest +{ + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; + + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + [JsonPropertyName("refreshCatalog")] + public bool? RefreshCatalog { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer <token>` on the outbound request and does no caching; the SDK consumer owns token caching and refresh. [Experimental(Diagnostics.Experimental)] public sealed class ProviderTokenAcquireResult @@ -32486,6 +32773,222 @@ public override void Write(Utf8JsonWriter writer, SessionVisibilityStatus value, } +/// Availability of the EXPERIMENTAL session connector API. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ConnectorAvailability : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ConnectorAvailability(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The resolved Connector feature is enabled and Connector requests are permitted. + public static ConnectorAvailability Enabled { get; } = new("enabled"); + + /// The resolved Connector feature is off. No Connector service request is made while disabled. + public static ConnectorAvailability Disabled { get; } = new("disabled"); + + /// The session has no eligible host-owned GitHub account or does not support local Connector projection. + public static ConnectorAvailability Unavailable { get; } = new("unavailable"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorAvailability left, ConnectorAvailability right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); + + /// + public bool Equals(ConnectorAvailability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ConnectorAvailability Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ConnectorAvailability value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); + } + } +} + + +/// Authoritative service connection state for one Connector. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ConnectorCatalogStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ConnectorCatalogStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The Connector is available but not connected. + public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); + + /// The Connector service is still completing connection or consent. + public static ConnectorCatalogStatus Pending { get; } = new("pending"); + + /// The Connector is connected and may contribute MCP servers. + public static ConnectorCatalogStatus Connected { get; } = new("connected"); + + /// The Connector service reports an unusable connection. + public static ConnectorCatalogStatus Error { get; } = new("error"); + + /// The service returned a future or unrecognized state. + public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorCatalogStatus left, ConnectorCatalogStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); + + /// + public bool Equals(ConnectorCatalogStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ConnectorCatalogStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ConnectorCatalogStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); + } + } +} + + +/// Live MCP status of one Connector-owned runtime server. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ConnectorMcpStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ConnectorMcpStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The server is connected and its tools are available. + public static ConnectorMcpStatus Connected { get; } = new("connected"); + + /// The server connection is still being established. + public static ConnectorMcpStatus Pending { get; } = new("pending"); + + /// The server requires refreshed GitHub authorization. + public static ConnectorMcpStatus NeedsAuth { get; } = new("needs_auth"); + + /// The server failed to connect or initialize. + public static ConnectorMcpStatus Failed { get; } = new("failed"); + + /// The server is intentionally stopped, including when managed policy blocks it. + public static ConnectorMcpStatus Stopped { get; } = new("stopped"); + + /// The server is configured but explicitly disabled. + public static ConnectorMcpStatus Disabled { get; } = new("disabled"); + + /// The Connector currently has no live server configuration. + public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ConnectorMcpStatus left, ConnectorMcpStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ConnectorMcpStatus left, ConnectorMcpStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ConnectorMcpStatus other && Equals(other); + + /// + public bool Equals(ConnectorMcpStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ConnectorMcpStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ConnectorMcpStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); + } + } +} + + /// Why the runtime requests client-task cancellation. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -34673,6 +35176,12 @@ internal SessionRpc(CopilotSession session) Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + /// Connectors APIs. + public ConnectorsApi Connectors => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Suspends the session while preserving persisted state for later resume. /// The to monitor for cancellation requests. The default is . [Experimental(Diagnostics.Experimental)] @@ -38939,6 +39448,141 @@ public async Task StopAsync(long id, CancellationToken cance } } +/// Provides session-scoped Connectors APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class ConnectorsApi +{ + private readonly CopilotSession _session; + + internal ConnectorsApi(CopilotSession session) + { + _session = session; + } + + /// Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. + /// The to monitor for cancellation requests. The default is . + /// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. + public async Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionConnectorsGetCapabilitiesRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.getCapabilities", [request], cancellationToken); + } + + /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. + /// The to monitor for cancellation requests. The default is . + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + public async Task GetStatusAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionConnectorsGetStatusRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.getStatus", [request], cancellationToken); + } + + /// Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. + /// Opaque account selection ID previously returned by an account discovery API. + /// The to monitor for cancellation requests. The default is . + /// Validated Connector catalog snapshot cached by the session. + public async Task ListAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + _session.ThrowIfDisposed(); + + var request = new ConnectorAccountRequest { SessionId = _session.SessionId, AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.list", [request], cancellationToken); + } + + /// Refreshes and validates the Connector catalog for the pinned opaque account selection. + /// Opaque account selection ID previously returned by an account discovery API. + /// The to monitor for cancellation requests. The default is . + /// Validated Connector catalog snapshot cached by the session. + public async Task RefreshAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + _session.ThrowIfDisposed(); + + var request = new ConnectorAccountRequest { SessionId = _session.SessionId, AccountId = accountId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.refresh", [request], cancellationToken); + } + + /// Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + /// Canonical Connector name from the current catalog. + /// The to monitor for cancellation requests. The default is . + /// Typed result of initiating or continuing a Connector connection. + public async Task ConnectAsync(string accountId, string connectorName, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(connectorName); + _session.ThrowIfDisposed(); + + var request = new ConnectorConnectRequest { SessionId = _session.SessionId, AccountId = accountId, ConnectorName = connectorName }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.connect", [request], cancellationToken); + } + + /// Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + /// Canonical Connector name from the current catalog. + /// The to monitor for cancellation requests. The default is . + /// Typed result of initiating or continuing a Connector connection. + public async Task ReconnectAsync(string accountId, string connectorName, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(connectorName); + _session.ThrowIfDisposed(); + + var request = new ConnectorConnectRequest { SessionId = _session.SessionId, AccountId = accountId, ConnectorName = connectorName }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.reconnect", [request], cancellationToken); + } + + /// Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. + /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + /// Maximum catalog requests made by this call. Must be between one and the capability limit. + /// Delay in milliseconds between attempts. Must not exceed the capability limit. + /// Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. + /// The to monitor for cancellation requests. The default is . + /// Typed result of initiating or continuing a Connector connection. + public async Task ContinueConnectionAsync(string continuationId, long maxAttempts, long pollIntervalMs, long deadlineMs, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(continuationId); + _session.ThrowIfDisposed(); + + var request = new ConnectorContinueRequest { SessionId = _session.SessionId, ContinuationId = continuationId, MaxAttempts = maxAttempts, PollIntervalMs = pollIntervalMs, DeadlineMs = deadlineMs }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.continueConnection", [request], cancellationToken); + } + + /// Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + /// Canonical Connector name from the current catalog. + /// The to monitor for cancellation requests. The default is . + /// Authoritative result after disconnect and MCP reconciliation. + public async Task DisconnectAsync(string accountId, string connectorName, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(connectorName); + _session.ThrowIfDisposed(); + + var request = new ConnectorConnectRequest { SessionId = _session.SessionId, AccountId = accountId, ConnectorName = connectorName }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.disconnect", [request], cancellationToken); + } + + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + /// The to monitor for cancellation requests. The default is . + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + public async Task ReconcileAsync(string accountId, bool? refreshCatalog = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + _session.ThrowIfDisposed(); + + var request = new ConnectorReconcileRequest { SessionId = _session.SessionId, AccountId = accountId, RefreshCatalog = refreshCatalog }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.connectors.reconcile", [request], cancellationToken); + } +} + /// Handles `providerToken` client session API methods. [Experimental(Diagnostics.Experimental)] public interface IProviderTokenHandler @@ -39845,6 +40489,17 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ConnectResult))] [JsonSerializable(typeof(ConnectedRemoteSessionMetadata))] [JsonSerializable(typeof(ConnectedRemoteSessionMetadataRepository))] +[JsonSerializable(typeof(ConnectorAccountRequest))] +[JsonSerializable(typeof(ConnectorCapabilities))] +[JsonSerializable(typeof(ConnectorCatalogEntry))] +[JsonSerializable(typeof(ConnectorCatalogResult))] +[JsonSerializable(typeof(ConnectorConnectRequest))] +[JsonSerializable(typeof(ConnectorConnectResult))] +[JsonSerializable(typeof(ConnectorContinueRequest))] +[JsonSerializable(typeof(ConnectorDisconnectResult))] +[JsonSerializable(typeof(ConnectorReconcileRequest))] +[JsonSerializable(typeof(ConnectorRuntimeStatus))] +[JsonSerializable(typeof(ConnectorStatus))] [JsonSerializable(typeof(ContentExclusionCheckPathsRequest))] [JsonSerializable(typeof(ContentExclusionCheckPathsResult))] [JsonSerializable(typeof(ContentExclusionPathCheck))] @@ -40362,6 +41017,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionCommandsListRequestWithSession))] [JsonSerializable(typeof(SessionCompletionItem))] [JsonSerializable(typeof(SessionCompletionsGetTriggerCharactersRequest))] +[JsonSerializable(typeof(SessionConnectorsGetCapabilitiesRequest))] +[JsonSerializable(typeof(SessionConnectorsGetStatusRequest))] [JsonSerializable(typeof(SessionContext))] [JsonSerializable(typeof(SessionEnrichMetadataResult))] [JsonSerializable(typeof(SessionEventLogTailRequest))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 39dbdb6119..404c0054b7 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -69,7 +69,6 @@ public sealed partial class CopilotSession : IAsyncDisposable private volatile Func>? _permissionHandler; private bool _managedSettingsEnabled; private volatile Func>? _mcpAuthHandler; - private volatile Func>? _mcpHeadersRefreshHandler; private volatile Func>? _userInputHandler; private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; @@ -622,11 +621,6 @@ internal void RegisterMcpAuthHandler(Func>? _mcpAuthHandler = handler; } - internal void RegisterMcpHeadersRefreshHandler(Func>? handler) - { - _mcpHeadersRefreshHandler = handler; - } - /// /// Handles a permission request from the Copilot CLI. /// @@ -740,26 +734,6 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) break; } - case McpHeadersRefreshRequiredEvent refreshEvent: - { - var data = refreshEvent.Data; - var handler = _mcpHeadersRefreshHandler; - if (string.IsNullOrEmpty(data.RequestId) || handler is null) - return; - - await ExecuteMcpHeadersRefreshAndRespondAsync( - data.RequestId, - new McpHeadersRefreshContext - { - SessionId = SessionId, - ServerKey = data.ServerName, - ServerUrl = data.ServerUrl, - Reason = data.Reason - }, - handler); - break; - } - case CommandExecuteEvent cmdEvent: { var data = cmdEvent.Data; @@ -887,56 +861,6 @@ private async Task ExecuteMcpAuthAndRespondAsync( } } - private async Task ExecuteMcpHeadersRefreshAndRespondAsync( - string requestId, - McpHeadersRefreshContext context, - Func> handler) - { - McpHeadersHandlePendingHeadersRefreshRequest response; - try - { - var result = await handler(context); - response = result is null - ? new McpHeadersHandlePendingHeadersRefreshRequestNone() - : new McpHeadersHandlePendingHeadersRefreshRequestHeaders - { - Headers = result.Headers, - TtlMs = result.TtlMs - }; - } - catch (OperationCanceledException ex) - { - response = new McpHeadersHandlePendingHeadersRefreshRequestError - { - Message = ex.Message - }; - } - catch (Exception ex) when (IsRecoverableMcpAuthFailure(ex)) - { - response = new McpHeadersHandlePendingHeadersRefreshRequestError - { - Message = ex.Message - }; - } - - try - { - await Rpc.Mcp.Headers.HandlePendingHeadersRefreshRequestAsync(requestId, response); - } - 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. - } - } - private static bool IsRecoverableMcpAuthFailure(Exception exception) => exception is not OperationCanceledException and not OutOfMemoryException diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index c17553123a..1cc4919093 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1317,71 +1317,6 @@ public sealed class McpAuthResult public static McpAuthResult Cancel() => new() { Cancelled = true }; } -/// -/// Context for refreshing client-to-Copilot Connectors service authorization -/// headers for a connected MCP endpoint. -/// -[Experimental(Diagnostics.Experimental)] -public sealed class McpHeadersRefreshContext -{ - /// Identifier of the session that requested refreshed headers. - public string SessionId { get; set; } = string.Empty; - - /// - /// Stable server key matching the key in . - /// - public string ServerKey { get; set; } = string.Empty; - - /// Exact Copilot Connectors service MCP endpoint advertised for this server key. - public string ServerUrl { get; set; } = string.Empty; - - /// Why the runtime invalidated or requested dynamic headers. - public McpHeadersRefreshRequiredReason Reason { get; set; } -} - -/// Copilot Connectors service authorization headers and their optional cache lifetime. -[Experimental(Diagnostics.Experimental)] -public sealed class McpHeadersRefreshResult -{ - /// - /// Short-lived HTTP headers authorizing this client to call the exact Copilot - /// Connectors service MCP endpoint. These are not downstream provider tokens. - /// - public required IDictionary Headers { get; set; } - - /// Optional remaining lifetime in milliseconds for the returned service authorization. - public long? TtlMs { get; set; } -} - -/// -/// Non-secret MCP endpoint configuration for a connected Copilot Connector -/// supplied from the service catalog. -/// Client authorization to call the endpoint is supplied only through -/// . The Copilot Connectors -/// service owns downstream provider tokens such as Outlook or Slack tokens. -/// -[Experimental(Diagnostics.Experimental)] -public sealed class ConnectorMcpServerConfig -{ - /// Human-readable Connector display name, distinct from the stable server identity. - public required string DisplayName { get; set; } - - /// Copilot Connector remote MCP streamable HTTP endpoint. - public required string Url { get; set; } - - /// Tools to include. The runtime includes all tools when omitted. - public IList? Tools { get; set; } - - /// Timeout in milliseconds for tool discovery and tool calls. - public long? Timeout { get; set; } - - /// - /// Maximum time in milliseconds that the runtime may reuse the - /// client-to-Copilot Connectors service authorization. - /// - public long? AuthorizationCacheTtlMs { get; set; } -} - // ============================================================================ // Session Capabilities // ============================================================================ @@ -3391,11 +3326,6 @@ protected SessionConfigBase(SessionConfigBase? other) ? new Dictionary(dict, dict.Comparer) : new Dictionary(other.McpServers)) : null; - ConnectorMcpServers = other.ConnectorMcpServers is not null - ? (other.ConnectorMcpServers is Dictionary connectorDict - ? new Dictionary(connectorDict, connectorDict.Comparer) - : new Dictionary(other.ConnectorMcpServers)) - : null; McpOAuthTokenStorage = other.McpOAuthTokenStorage; AuthClientIdMetadataUrl = other.AuthClientIdMetadataUrl; Model = other.Model; @@ -3405,7 +3335,6 @@ protected SessionConfigBase(SessionConfigBase? other) OnEvent = other.OnEvent; OnExitPlanModeRequest = other.OnExitPlanModeRequest; OnMcpAuthRequest = other.OnMcpAuthRequest; - OnMcpHeadersRefresh = other.OnMcpHeadersRefresh; OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; @@ -3787,14 +3716,6 @@ protected SessionConfigBase(SessionConfigBase? other) ///
public IDictionary? McpServers { get; set; } - /// - /// Connected Copilot Connector MCP endpoints supplied from the service catalog. - /// Keys are reported through when headers need refreshing. - /// Re-supply the connected set on cold resume. - /// - [Experimental(Diagnostics.Experimental)] - public IDictionary? ConnectorMcpServers { get; set; } - /// /// Controls how MCP OAuth tokens are stored for this session. /// Default: for safe multitenant behavior. @@ -4038,19 +3959,6 @@ protected SessionConfigBase(SessionConfigBase? other) [Experimental(Diagnostics.Experimental)] [JsonIgnore] public Func>? OnMcpAuthRequest { get; set; } - - /// - /// Supplies short-lived headers authorizing this client to call the exact - /// Copilot Connectors service MCP endpoint configured through - /// . This is normally the selected account's - /// GitHub bearer authorization. The SDK host does not supply downstream provider - /// tokens for Outlook, Slack, or other connected services; the Copilot Connectors - /// service owns those tokens. Returning reports no headers; - /// exceptions are sent to the runtime as explicit refresh errors. - /// - [Experimental(Diagnostics.Experimental)] - [JsonIgnore] - public Func>? OnMcpHeadersRefresh { get; set; } } /// diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index 16582f1514..f0356fc23d 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -18,7 +18,7 @@ namespace GitHub.Copilot.Test.Unit; -public sealed class ClientSessionLifetimeTests +public sealed partial class ClientSessionLifetimeTests { private sealed record RpcRequestRecord(string Method, JsonElement Params); @@ -1220,166 +1220,6 @@ public async Task McpAuth_Handler_Exception_Cancels_Pending_Request() Assert.Equal("cancelled", request.Params.GetProperty("result").GetProperty("kind").GetString()); } - [Fact] - public async Task ConnectorMcpServers_Map_To_Managed_Wire_Field_On_Create_And_Resume() - { - await using var server = await FakeCopilotServer.StartAsync(); - await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); - var connectorMcpServers = new Dictionary - { - ["github"] = new() - { - DisplayName = "GitHub", - Url = "https://example.com/mcp", - Tools = ["issues"], - Timeout = 30_000, - AuthorizationCacheTtlMs = 60_000 - } - }; - - await using var created = await client.CreateSessionAsync(new SessionConfig - { - ConnectorMcpServers = connectorMcpServers - }); - AssertConnectorMcpServers(Assert.Single(server.Requests, request => request.Method == "session.create")); - - server.ClearRequests(); - await using var resumed = await client.ResumeSessionAsync("connector-resume", new ResumeSessionConfig - { - ConnectorMcpServers = connectorMcpServers - }); - AssertConnectorMcpServers(Assert.Single(server.Requests, request => request.Method == "session.resume")); - - static void AssertConnectorMcpServers(RpcRequestRecord request) - { - var root = request.Params; - var serverConfig = root.GetProperty("managedMcpServers").GetProperty("github"); - Assert.Equal("GitHub", serverConfig.GetProperty("displayName").GetString()); - Assert.Equal("https://example.com/mcp", serverConfig.GetProperty("url").GetString()); - Assert.Equal("issues", serverConfig.GetProperty("tools")[0].GetString()); - Assert.Equal(30_000, serverConfig.GetProperty("timeout").GetInt64()); - Assert.Equal(60_000, serverConfig.GetProperty("headersRefreshTtlMs").GetInt64()); - Assert.False(root.TryGetProperty("connectorMcpServers", out _)); - } - } - - [Theory] - [InlineData(null)] - [InlineData(5000L)] - public async Task McpHeadersRefresh_Handler_Sends_Headers_With_Optional_Ttl(long? ttlMs) - { - await using var server = await FakeCopilotServer.StartAsync(); - await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); - await using var session = await client.CreateSessionAsync(new SessionConfig - { - OnPermissionRequest = PermissionHandler.ApproveAll, - ConnectorMcpServers = new Dictionary - { - ["github"] = new() - { - DisplayName = "GitHub", - Url = "https://example.com/mcp", - AuthorizationCacheTtlMs = 60_000 - } - }, - OnMcpHeadersRefresh = context => - { - Assert.Equal("github", context.ServerKey); - Assert.Equal("https://example.com/mcp", context.ServerUrl); - Assert.Equal(McpHeadersRefreshRequiredReason.Startup, context.Reason); - return Task.FromResult(new() - { - Headers = new Dictionary { ["Authorization"] = "Bearer short-lived" }, - TtlMs = ttlMs - }); - } - }); - - DispatchEvent(session, new McpHeadersRefreshRequiredEvent - { - Data = new McpHeadersRefreshRequiredData - { - RequestId = "mcp-refresh-1", - ServerName = "github", - ServerUrl = "https://example.com/mcp", - Reason = McpHeadersRefreshRequiredReason.Startup - } - }); - - var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); - var result = request.Params.GetProperty("result"); - Assert.Equal("headers", result.GetProperty("kind").GetString()); - Assert.Equal("Bearer short-lived", result.GetProperty("headers").GetProperty("Authorization").GetString()); - Assert.Equal(ttlMs.HasValue, result.TryGetProperty("ttlMs", out var ttl)); - if (ttlMs.HasValue) - Assert.Equal(ttlMs.Value, ttl.GetInt64()); - - var create = server.Requests.First(request => request.Method == "session.create"); - Assert.Equal("GitHub", create.Params.GetProperty("managedMcpServers").GetProperty("github").GetProperty("displayName").GetString()); - Assert.Contains( - server.Requests, - request => request.Method == "session.eventLog.registerInterest" - && request.Params.GetProperty("eventType").GetString() == "mcp.headers_refresh_required"); - } - - [Fact] - public async Task McpHeadersRefresh_Handler_Exception_Sends_Explicit_Refresh_Error() - { - await using var server = await FakeCopilotServer.StartAsync(); - await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); - await using var session = await client.CreateSessionAsync(new SessionConfig - { - OnPermissionRequest = PermissionHandler.ApproveAll, - OnMcpHeadersRefresh = _ => throw new ApplicationException("credential revoked") - }); - - DispatchEvent(session, new McpHeadersRefreshRequiredEvent - { - Data = new McpHeadersRefreshRequiredData - { - RequestId = "mcp-refresh-error", - ServerName = "github", - ServerUrl = "https://example.com/mcp", - Reason = McpHeadersRefreshRequiredReason.AuthFailed - } - }); - - var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); - Assert.Equal("mcp-refresh-error", request.Params.GetProperty("requestId").GetString()); - var result = request.Params.GetProperty("result"); - Assert.Equal("error", result.GetProperty("kind").GetString()); - Assert.Equal("credential revoked", result.GetProperty("message").GetString()); - } - - [Fact] - public async Task McpHeadersRefresh_Handler_Cancellation_Sends_Explicit_Refresh_Error() - { - await using var server = await FakeCopilotServer.StartAsync(); - await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); - await using var session = await client.CreateSessionAsync(new SessionConfig - { - OnPermissionRequest = PermissionHandler.ApproveAll, - OnMcpHeadersRefresh = _ => Task.FromCanceled(new CancellationToken(true)) - }); - - DispatchEvent(session, new McpHeadersRefreshRequiredEvent - { - Data = new McpHeadersRefreshRequiredData - { - RequestId = "mcp-refresh-cancelled", - ServerName = "github", - ServerUrl = "https://example.com/mcp", - Reason = McpHeadersRefreshRequiredReason.AuthFailed - } - }); - - var request = await WaitForRequestAsync(server, "session.mcp.headers.handlePendingHeadersRefreshRequest"); - Assert.Equal("mcp-refresh-cancelled", request.Params.GetProperty("requestId").GetString()); - var result = request.Params.GetProperty("result"); - Assert.Equal("error", result.GetProperty("kind").GetString()); - Assert.False(string.IsNullOrWhiteSpace(result.GetProperty("message").GetString())); - } - [Fact] public async Task ExternalToolCompleted_Cancels_Blocked_Tool_When_Cancellation_Callback_Throws() { @@ -2567,6 +2407,8 @@ public static Task StartAsync() public Func? AfterResponseAsync { get; set; } + public Func? ResponseFactory { get; set; } + public IReadOnlyList Requests { get @@ -2833,10 +2675,6 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { ["success"] = true }, - "session.mcp.headers.handlePendingHeadersRefreshRequest" => new Dictionary - { - ["success"] = true - }, "session.permissions.handlePendingPermissionRequest" => new Dictionary { ["success"] = true @@ -2860,6 +2698,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel }, "session.detach" => await DetachSessionAsync(cancellationToken), "runtime.shutdown" => HandleRuntimeShutdown(), + _ when ResponseFactory is { } responseFactory => responseFactory(requestRecord), _ => throw new InvalidOperationException($"Unexpected RPC method '{method}'.") }; diff --git a/dotnet/test/Unit/RpcConnectorsTests.cs b/dotnet/test/Unit/RpcConnectorsTests.cs new file mode 100644 index 0000000000..f7304700bd --- /dev/null +++ b/dotnet/test/Unit/RpcConnectorsTests.cs @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#if NET8_0_OR_GREATER +using System.Text.Json; +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public sealed partial class ClientSessionLifetimeTests +{ + [Fact] + public async Task Session_Rpc_Connectors_Reports_Capabilities_Status_And_Catalogs() + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = CreateConnectorRpcResponse; + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + + var connectors = session.Rpc.Connectors; + Assert.Same(connectors, session.Rpc.Connectors); + + var capabilities = await connectors.GetCapabilitiesAsync(); + Assert.Equal(1, capabilities.ApiVersion); + Assert.Equal(ConnectorAvailability.Enabled, capabilities.Availability); + Assert.True(capabilities.ConsentContinuation); + Assert.Equal(5, capabilities.MaxPollAttempts); + Assert.Equal(2_000, capabilities.MaxPollIntervalMs); + Assert.Equal(30_000, capabilities.MaxDeadlineMs); + Assert.True(capabilities.OpaqueAccountSelection); + + var status = await connectors.GetStatusAsync(); + Assert.Equal("account-1", status.AccountId); + Assert.Equal(2, status.PendingConnections); + Assert.Equal(7, status.Catalog?.Revision); + var runtimeServer = Assert.Single(status.RuntimeServers); + Assert.Equal("slack", runtimeServer.ConnectorName); + Assert.Equal("connector-slack", runtimeServer.RuntimeServerId); + Assert.Equal(ConnectorMcpStatus.Connected, runtimeServer.Status); + + var listed = await connectors.ListAsync("account-1"); + AssertConnectorCatalog(listed, 3, ConnectorCatalogStatus.NotConnected); + + var refreshed = await connectors.RefreshAsync("account-1"); + AssertConnectorCatalog(refreshed, 4, ConnectorCatalogStatus.Connected); + + Assert.Collection( + server.Requests, + request => AssertConnectorRequest( + request, + "session.connectors.getCapabilities", + session.SessionId), + request => AssertConnectorRequest( + request, + "session.connectors.getStatus", + session.SessionId), + request => AssertConnectorRequest( + request, + "session.connectors.list", + session.SessionId, + ("accountId", "account-1")), + request => AssertConnectorRequest( + request, + "session.connectors.refresh", + session.SessionId, + ("accountId", "account-1"))); + } + + [Fact] + public async Task Session_Rpc_Connectors_Maps_Connection_Wire_And_Result_Variants() + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = CreateConnectorRpcResponse; + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + + var connected = Assert.IsType( + await session.Rpc.Connectors.ConnectAsync("account-1", "slack")); + Assert.Equal("connected", connected.Kind); + Assert.Equal(8, connected.Status.Catalog?.Revision); + + var consent = Assert.IsType( + await session.Rpc.Connectors.ReconnectAsync("account-1", "slack")); + Assert.Equal("consent_required", consent.Kind); + Assert.Equal("https://example.com/consent", consent.ConsentUrl); + Assert.Equal("continuation-reconnect", consent.ContinuationId); + + var pending = Assert.IsType( + await session.Rpc.Connectors.ContinueConnectionAsync( + "continuation-reconnect", + maxAttempts: 3, + pollIntervalMs: 1_000, + deadlineMs: 10_000)); + Assert.Equal("pending", pending.Kind); + Assert.Equal("continuation-next", pending.ContinuationId); + + Assert.Collection( + server.Requests, + request => AssertConnectorRequest( + request, + "session.connectors.connect", + session.SessionId, + ("accountId", "account-1"), + ("connectorName", "slack")), + request => AssertConnectorRequest( + request, + "session.connectors.reconnect", + session.SessionId, + ("accountId", "account-1"), + ("connectorName", "slack")), + request => AssertConnectorRequest( + request, + "session.connectors.continueConnection", + session.SessionId, + ("continuationId", "continuation-reconnect"), + ("maxAttempts", 3L), + ("pollIntervalMs", 1_000L), + ("deadlineMs", 10_000L))); + } + + [Fact] + public async Task Session_Rpc_Connectors_Maps_Disconnect_And_Reconcile() + { + await using var server = await FakeCopilotServer.StartAsync(); + server.ResponseFactory = CreateConnectorRpcResponse; + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url) + }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + server.ClearRequests(); + + var disconnected = await session.Rpc.Connectors.DisconnectAsync("account-1", "slack"); + Assert.True(disconnected.Disconnected); + Assert.Equal(9, disconnected.Status.Catalog?.Revision); + + var reconciled = await session.Rpc.Connectors.ReconcileAsync("account-1", refreshCatalog: true); + Assert.Equal(10, reconciled.Catalog?.Revision); + Assert.Empty(reconciled.RuntimeServers); + + Assert.Collection( + server.Requests, + request => AssertConnectorRequest( + request, + "session.connectors.disconnect", + session.SessionId, + ("accountId", "account-1"), + ("connectorName", "slack")), + request => AssertConnectorRequest( + request, + "session.connectors.reconcile", + session.SessionId, + ("accountId", "account-1"), + ("refreshCatalog", true))); + } + + private static Dictionary CreateConnectorRpcResponse(RpcRequestRecord request) => + request.Method switch + { + "session.connectors.getCapabilities" => new Dictionary + { + ["apiVersion"] = 1L, + ["availability"] = "enabled", + ["consentContinuation"] = true, + ["maxDeadlineMs"] = 30_000L, + ["maxPollAttempts"] = 5L, + ["maxPollIntervalMs"] = 2_000L, + ["opaqueAccountSelection"] = true + }, + "session.connectors.getStatus" => CreateConnectorStatusResponse(7, pendingConnections: 2), + "session.connectors.list" => CreateConnectorCatalogResponse(3, "not_connected"), + "session.connectors.refresh" => CreateConnectorCatalogResponse(4, "connected"), + "session.connectors.connect" => new Dictionary + { + ["kind"] = "connected", + ["status"] = CreateConnectorStatusResponse(8) + }, + "session.connectors.reconnect" => new Dictionary + { + ["kind"] = "consent_required", + ["consentUrl"] = "https://example.com/consent", + ["continuationId"] = "continuation-reconnect" + }, + "session.connectors.continueConnection" => new Dictionary + { + ["kind"] = "pending", + ["continuationId"] = "continuation-next" + }, + "session.connectors.disconnect" => new Dictionary + { + ["disconnected"] = true, + ["status"] = CreateConnectorStatusResponse(9) + }, + "session.connectors.reconcile" => CreateConnectorStatusResponse( + 10, + includeRuntimeServer: false), + _ => throw new InvalidOperationException($"Unexpected Connector RPC method '{request.Method}'.") + }; + + private static Dictionary CreateConnectorStatusResponse( + long revision, + long pendingConnections = 0, + bool includeRuntimeServer = true) => + new() + { + ["accountId"] = "account-1", + ["apiVersion"] = 1L, + ["availability"] = "enabled", + ["catalog"] = CreateConnectorCatalogResponse(revision, "connected"), + ["pendingConnections"] = pendingConnections, + ["runtimeServers"] = includeRuntimeServer + ? new object?[] + { + new Dictionary + { + ["connectorName"] = "slack", + ["runtimeServerId"] = "connector-slack", + ["status"] = "connected" + } + } + : Array.Empty() + }; + + private static Dictionary CreateConnectorCatalogResponse(long revision, string status) => + new() + { + ["connectors"] = new object?[] + { + new Dictionary + { + ["description"] = "Slack workspace search", + ["displayName"] = "Slack", + ["name"] = "slack", + ["runtimeServerIds"] = new object?[] { "connector-slack" }, + ["status"] = status + } + }, + ["refreshedAtMs"] = 1_750_000_000_000L, + ["revision"] = revision + }; + + private static void AssertConnectorCatalog( + ConnectorCatalogResult catalog, + long revision, + ConnectorCatalogStatus status) + { + Assert.Equal(revision, catalog.Revision); + Assert.Equal(1_750_000_000_000L, catalog.RefreshedAtMs); + var connector = Assert.Single(catalog.Connectors); + Assert.Equal("Slack", connector.DisplayName); + Assert.Equal("slack", connector.Name); + Assert.Equal(status, connector.Status); + Assert.Equal(["connector-slack"], connector.RuntimeServerIds); + } + + private static void AssertConnectorRequest( + RpcRequestRecord request, + string method, + string sessionId, + params (string Name, object Value)[] expectedProperties) + { + Assert.Equal(method, request.Method); + Assert.Equal(expectedProperties.Length + 1, request.Params.EnumerateObject().Count()); + Assert.Equal(sessionId, request.Params.GetProperty("sessionId").GetString()); + + foreach (var (name, expectedValue) in expectedProperties) + { + var actualValue = request.Params.GetProperty(name); + switch (expectedValue) + { + case string stringValue: + Assert.Equal(stringValue, actualValue.GetString()); + break; + case long longValue: + Assert.Equal(longValue, actualValue.GetInt64()); + break; + case bool boolValue: + Assert.Equal(boolValue, actualValue.GetBoolean()); + break; + default: + throw new InvalidOperationException( + $"Unsupported expected JSON value type '{expectedValue.GetType().Name}'."); + } + } + } +} +#endif diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 613201bf6c..843c3b7e01 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -96,50 +96,6 @@ public void SandboxConfig_RoundtripsAllowBypass_AndOmitsWhenAbsent() Assert.False(omittedDocument.RootElement.TryGetProperty("allowBypass", out _)); } - [Fact] - public void SessionRequests_Serialize_ConnectorMcpServers_Using_Managed_Wire_Field() - { - var options = GetSerializerOptions(); - var connectorServers = new Dictionary - { - ["github"] = new() - { - DisplayName = "GitHub", - Url = "https://example.com/mcp", - Tools = ["issues"], - Timeout = 30_000, - AuthorizationCacheTtlMs = 60_000 - } - }; - var mapper = typeof(CopilotClient).GetMethod( - "ToManagedMcpServers", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - Assert.NotNull(mapper); - var managed = mapper!.Invoke(null, [connectorServers]); - Assert.NotNull(managed); - - foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" }) - { - var requestType = GetNestedType(typeof(CopilotClient), requestName); - var request = CreateInternalRequest( - requestType, - ("SessionId", "session-id"), - ("ManagedMcpServers", managed)); - var json = JsonSerializer.Serialize(request, requestType, options); - using var document = JsonDocument.Parse(json); - var server = document.RootElement - .GetProperty("managedMcpServers") - .GetProperty("github"); - - Assert.Equal("GitHub", server.GetProperty("displayName").GetString()); - Assert.Equal("https://example.com/mcp", server.GetProperty("url").GetString()); - Assert.Equal("issues", server.GetProperty("tools")[0].GetString()); - Assert.Equal(30_000, server.GetProperty("timeout").GetInt64()); - Assert.Equal(60_000, server.GetProperty("headersRefreshTtlMs").GetInt64()); - Assert.False(document.RootElement.TryGetProperty("connectorMcpServers", out _)); - } - } - [Fact] public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() { diff --git a/go/README.md b/go/README.md index e9c430023c..815d8571fa 100644 --- a/go/README.md +++ b/go/README.md @@ -278,42 +278,18 @@ Initial acquisition runs during session creation or resume. Cancellation, provid - `UI() *SessionUI` - Interactive UI API for elicitation dialogs - `Capabilities() SessionCapabilities` - Host capabilities (e.g. elicitation support) -#### Connector MCP session controls (Experimental) - -Supply connected Copilot Connector MCP endpoints from the service catalog -through `SessionConfig.ConnectorMCPServers` when creating a session, or through -`ResumeSessionConfig.ConnectorMCPServers` when cold-resuming one. - -`OnMCPHeadersRefresh` supplies short-lived authorization from the SDK client to -the Copilot Connectors service for the exact service-advertised MCP endpoint, -normally using the selected account's GitHub bearer token. It does not supply -Outlook, Slack, or other downstream service tokens; the Connector service owns -and applies those tokens. - -`ConnectorMCPServerConfig.AuthorizationCacheTTLMS` sets the maximum time the -runtime may reuse Connector service authorization. `MCPHeadersRefreshResult.TTLMS` -reports the remaining authorization lifetime for a particular refresh. - -Correct same-session Connect and Disconnect reconciliation is not available -through the current public runtime contract. Until the runtime promotes its -authoritative replacement/removal operations, create a session or cold-resume -one with the new `ConnectorMCPServers` set. - -The public typed `session.RPC.MCP` API can inspect and control the current -runtime state, but these operations do not replace the authoritative Connector -set: - -| Capability | Go API | -| --- | --- | -| List servers and connection status | `session.RPC.MCP.List(ctx)` | -| Enable or disable a server | `session.RPC.MCP.Enable(ctx, &rpc.MCPEnableRequest{ServerName: key})` / `Disable` | -| List connected-server tools | `session.RPC.MCP.ListTools(ctx, &rpc.MCPListToolsRequest{ServerName: key})` | -| Observe status changes | `session.On(...)` with `SessionMCPServersLoadedData`, `SessionMCPServerStatusChangedData`, `SessionMCPServerNeedsReconnectData`, and `SessionMCPServerRemovedData` | - -The generated request fields named `ServerName` carry the stable Connector -server key, not the display name or Connector service ID. The host application -remains responsible for Connector service catalog and consent HTTP calls and -for all browser or UI flows. +#### Connector session API (Experimental) + +`session.RPC.Connectors` exposes the runtime-owned Connector catalog, lifecycle, +bounded consent continuation, and MCP reconciliation APIs: +`GetCapabilities`, `GetStatus`, `List`, `Refresh`, `Connect`, `Reconnect`, +`ContinueConnection`, `Disconnect`, and `Reconcile`. The host account-selection +flow supplies an opaque `selectionId`, passed as `AccountID` (`accountId` on the +wire), and owns consent/browser UX. Connector APIs neither require nor return +tokens; Connector credentials and MCP projection remain runtime-owned. + +`GetCapabilities` reports API availability and bounded continuation limits +without changing Connector state. Check it before using the remaining methods. #### Message source diff --git a/go/client.go b/go/client.go index 5cbb7c2413..e450c595f2 100644 --- a/go/client.go +++ b/go/client.go @@ -902,7 +902,6 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.WorkingDirectory = config.WorkingDirectory req.AdditionalDirectories = config.AdditionalDirectories req.MCPServers = config.MCPServers - req.ManagedMCPServers = connectorMCPServersToWire(config.ConnectorMCPServers) req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL req.EnvValueMode = "direct" @@ -1041,7 +1040,6 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses s.registerTools(config.Tools) s.registerPermissionHandler(config.OnPermissionRequest) s.registerMCPAuthHandler(config.OnMCPAuthRequest) - s.registerMCPHeadersRefreshHandler(config.OnMCPHeadersRefresh) if config.OnUserInputRequest != nil { s.registerUserInputHandler(config.OnUserInputRequest) } @@ -1172,17 +1170,6 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return nil, err } } - if config.OnMCPHeadersRefresh != nil { - if _, err := c.client.Request(ctx, "session.eventLog.registerInterest", map[string]any{ - "sessionId": session.SessionID, - "eventType": "mcp.headers_refresh_required", - }); err != nil { - c.sessionsMux.Lock() - delete(c.sessions, registeredSessionID) - c.sessionsMux.Unlock() - return nil, err - } - } session.workspacePath = response.WorkspacePath session.setCapabilities(response.Capabilities) @@ -1335,7 +1322,6 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } req.ContinuePendingWork = config.ContinuePendingWork req.MCPServers = config.MCPServers - req.ManagedMCPServers = connectorMCPServersToWire(config.ConnectorMCPServers) req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL req.EnvValueMode = "direct" @@ -1411,7 +1397,6 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, session.registerTools(config.Tools) session.registerPermissionHandler(config.OnPermissionRequest) session.registerMCPAuthHandler(config.OnMCPAuthRequest) - session.registerMCPHeadersRefreshHandler(config.OnMCPHeadersRefresh) if config.OnUserInputRequest != nil { session.registerUserInputHandler(config.OnUserInputRequest) } @@ -1502,15 +1487,6 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, return nil, err } } - if config.OnMCPHeadersRefresh != nil { - if _, err := c.client.Request(ctx, "session.eventLog.registerInterest", map[string]any{ - "sessionId": sessionID, - "eventType": "mcp.headers_refresh_required", - }); err != nil { - restoreReplacedSession() - return nil, err - } - } session.workspacePath = response.WorkspacePath session.setCapabilities(response.Capabilities) diff --git a/go/client_test.go b/go/client_test.go index 4f145a627c..1613c3b394 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -2713,91 +2713,6 @@ func TestClient_MCPAuthInterestRegistration(t *testing.T) { }) } -func TestClient_ConnectorMCPServersMapToInternalWireOnCreateAndResume(t *testing.T) { - client, requests, cleanup := newInMemoryClient(t) - defer cleanup() - - ttl := int64(60_000) - timeout := int64(30_000) - connectors := map[string]ConnectorMCPServerConfig{ - "github": { - DisplayName: "GitHub", - URL: "https://example.com/mcp", - Tools: []string{"issues"}, - Timeout: &timeout, - AuthorizationCacheTTLMS: &ttl, - }, - } - handler := func(MCPHeadersRefreshRequest, MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { - return nil, nil - } - - session, err := client.CreateSession(t.Context(), &SessionConfig{ - ConnectorMCPServers: connectors, - OnMCPHeadersRefresh: handler, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - defer session.Disconnect() - - create, ok := findRequest(requests.snapshot(), "session.create") - if !ok { - t.Fatal("missing session.create request") - } - expectedWire := map[string]any{ - "github": map[string]any{ - "displayName": "GitHub", - "headersRefreshTtlMs": float64(60_000), - "timeout": float64(30_000), - "tools": []any{"issues"}, - "url": "https://example.com/mcp", - }, - } - if got := create.Params["managedMcpServers"]; !reflect.DeepEqual(got, expectedWire) { - t.Fatalf("unexpected managedMcpServers wire payload: %#v", got) - } - if _, ok := create.Params["catalogMcpServers"]; ok { - t.Fatalf("catalogMcpServers must not appear on the wire: %#v", create.Params) - } - if !hasEventInterest(requests.snapshot(), "mcp.headers_refresh_required") { - t.Fatal("missing MCP headers refresh interest on create") - } - - requests.clear() - resumed, err := client.ResumeSession(t.Context(), session.SessionID, &ResumeSessionConfig{ - ConnectorMCPServers: connectors, - OnMCPHeadersRefresh: handler, - }) - if err != nil { - t.Fatalf("ResumeSession failed: %v", err) - } - defer resumed.Disconnect() - - resume, ok := findRequest(requests.snapshot(), "session.resume") - if !ok { - t.Fatal("missing session.resume request") - } - if got := resume.Params["managedMcpServers"]; !reflect.DeepEqual(got, expectedWire) { - t.Fatalf("unexpected managedMcpServers wire payload on resume: %#v", got) - } - if _, ok := resume.Params["catalogMcpServers"]; ok { - t.Fatalf("catalogMcpServers must not appear on the wire: %#v", resume.Params) - } - if !hasEventInterest(requests.snapshot(), "mcp.headers_refresh_required") { - t.Fatal("missing MCP headers refresh interest on resume") - } -} - -func hasEventInterest(requests []recordedRequest, eventType string) bool { - for _, request := range requests { - if request.Method == "session.eventLog.registerInterest" && request.Params["eventType"] == eventType { - return true - } - } - return false -} - func findRequest(requests []recordedRequest, method string) (recordedRequest, bool) { for _, r := range requests { if r.Method == method { @@ -3063,12 +2978,6 @@ func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeW result = map[string]any{"sessionId": sessionID, "workspacePath": nil} case "session.eventLog.registerInterest": result = map[string]any{"id": "interest-1"} - case "session.mcp.list": - result = map[string]any{"servers": []any{}} - case "session.mcp.enable", "session.mcp.disable": - result = map[string]any{} - case "session.mcp.listTools": - result = map[string]any{"tools": []any{}} case "session.options.update": result = map[string]any{"success": true} case "session.skills.reload": diff --git a/go/connectors_rpc_test.go b/go/connectors_rpc_test.go new file mode 100644 index 0000000000..ff92a1b29a --- /dev/null +++ b/go/connectors_rpc_test.go @@ -0,0 +1,309 @@ +package copilot + +import ( + "encoding/json" + "fmt" + "net" + "reflect" + "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" +) + +const ( + connectorAccountID = "opaque-account-selection" + connectorStatusResult = `{ + "accountId":"` + connectorAccountID + `", + "apiVersion":1, + "availability":"enabled", + "catalog":{ + "connectors":[{ + "description":"Calendar and mail", + "displayName":"Outlook", + "name":"outlook", + "runtimeServerIds":["connector-outlook"], + "status":"connected" + }], + "refreshedAtMs":1700000000000, + "revision":7 + }, + "pendingConnections":2, + "runtimeServers":[{ + "connectorName":"outlook", + "runtimeServerId":"connector-outlook", + "status":"connected" + }] +}` +) + +func TestSessionRPCConnectors(t *testing.T) { + clientConn, serverConn := net.Pipe() + client := jsonrpc2.NewClient(clientConn, clientConn) + server := jsonrpc2.NewClient(serverConn, serverConn) + client.Start() + server.Start() + t.Cleanup(func() { + client.Stop() + server.Stop() + _ = clientConn.Close() + _ = serverConn.Close() + }) + + session := newSession("session-1", client, "", false) + t.Cleanup(session.stopEventProcessing) + if session.RPC == nil || session.RPC.Connectors == nil { + t.Fatal("session.RPC.Connectors is not initialized") + } + + t.Run("get capabilities", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getCapabilities", map[string]any{ + "sessionId": "session-1", + }, `{ + "apiVersion":1, + "availability":"enabled", + "consentContinuation":true, + "maxDeadlineMs":60000, + "maxPollAttempts":10, + "maxPollIntervalMs":5000, + "opaqueAccountSelection":true + }`) + + result, err := session.RPC.Connectors.GetCapabilities(t.Context()) + if err != nil { + t.Fatalf("GetCapabilities: %v", err) + } + if result.APIVersion != 1 || + result.Availability != rpc.ConnectorAvailabilityEnabled || + !result.ConsentContinuation || + result.MaxDeadlineMs != 60000 || + result.MaxPollAttempts != 10 || + result.MaxPollIntervalMs != 5000 || + !result.OpaqueAccountSelection { + t.Fatalf("GetCapabilities result = %#v", result) + } + }) + + t.Run("get status", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.getStatus", map[string]any{ + "sessionId": "session-1", + }, connectorStatusResult) + + result, err := session.RPC.Connectors.GetStatus(t.Context()) + if err != nil { + t.Fatalf("GetStatus: %v", err) + } + assertConnectorStatus(t, result) + }) + + t.Run("list", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.list", map[string]any{ + "accountId": connectorAccountID, + "sessionId": "session-1", + }, `{ + "connectors":[{ + "description":"Calendar and mail", + "displayName":"Outlook", + "name":"outlook", + "runtimeServerIds":["connector-outlook"], + "status":"connected" + }], + "refreshedAtMs":1700000000000, + "revision":7 + }`) + + result, err := session.RPC.Connectors.List(t.Context(), &rpc.ConnectorAccountRequest{ + AccountID: connectorAccountID, + }) + if err != nil { + t.Fatalf("List: %v", err) + } + assertConnectorCatalog(t, result, 7, rpc.ConnectorCatalogStatusConnected) + }) + + t.Run("refresh", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.refresh", map[string]any{ + "accountId": connectorAccountID, + "sessionId": "session-1", + }, `{ + "connectors":[{ + "displayName":"Outlook", + "name":"outlook", + "runtimeServerIds":[], + "status":"not_connected" + }], + "refreshedAtMs":1700000000100, + "revision":8 + }`) + + result, err := session.RPC.Connectors.Refresh(t.Context(), &rpc.ConnectorAccountRequest{ + AccountID: connectorAccountID, + }) + if err != nil { + t.Fatalf("Refresh: %v", err) + } + assertConnectorCatalog(t, result, 8, rpc.ConnectorCatalogStatusNotConnected) + }) + + t.Run("connect", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.connect", map[string]any{ + "accountId": connectorAccountID, + "connectorName": "outlook", + "sessionId": "session-1", + }, `{ + "kind":"consent_required", + "consentUrl":"https://example.com/consent", + "continuationId":"continuation-1" + }`) + + result, err := session.RPC.Connectors.Connect(t.Context(), &rpc.ConnectorConnectRequest{ + AccountID: connectorAccountID, + ConnectorName: "outlook", + }) + if err != nil { + t.Fatalf("Connect: %v", err) + } + consent, ok := result.(*rpc.ConnectorConnectResultConsentRequired) + if !ok || consent.ConsentURL != "https://example.com/consent" || consent.ContinuationID != "continuation-1" { + t.Fatalf("Connect result = %#v", result) + } + }) + + t.Run("reconnect", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.reconnect", map[string]any{ + "accountId": connectorAccountID, + "connectorName": "outlook", + "sessionId": "session-1", + }, `{"kind":"pending","continuationId":"continuation-2"}`) + + result, err := session.RPC.Connectors.Reconnect(t.Context(), &rpc.ConnectorConnectRequest{ + AccountID: connectorAccountID, + ConnectorName: "outlook", + }) + if err != nil { + t.Fatalf("Reconnect: %v", err) + } + pending, ok := result.(*rpc.ConnectorConnectResultPending) + if !ok || pending.ContinuationID != "continuation-2" { + t.Fatalf("Reconnect result = %#v", result) + } + }) + + t.Run("continue connection", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.continueConnection", map[string]any{ + "continuationId": "continuation-2", + "deadlineMs": float64(30000), + "maxAttempts": float64(4), + "pollIntervalMs": float64(500), + "sessionId": "session-1", + }, `{"kind":"connected","status":`+connectorStatusResult+`}`) + + result, err := session.RPC.Connectors.ContinueConnection(t.Context(), &rpc.ConnectorContinueRequest{ + ContinuationID: "continuation-2", + DeadlineMs: 30000, + MaxAttempts: 4, + PollIntervalMs: 500, + }) + if err != nil { + t.Fatalf("ContinueConnection: %v", err) + } + connected, ok := result.(*rpc.ConnectorConnectResultConnected) + if !ok { + t.Fatalf("ContinueConnection result = %T", result) + } + assertConnectorStatus(t, &connected.Status) + }) + + t.Run("disconnect", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.disconnect", map[string]any{ + "accountId": connectorAccountID, + "connectorName": "outlook", + "sessionId": "session-1", + }, `{"disconnected":true,"status":`+connectorStatusResult+`}`) + + result, err := session.RPC.Connectors.Disconnect(t.Context(), &rpc.ConnectorConnectRequest{ + AccountID: connectorAccountID, + ConnectorName: "outlook", + }) + if err != nil { + t.Fatalf("Disconnect: %v", err) + } + if !result.Disconnected { + t.Fatalf("Disconnect result = %#v", result) + } + assertConnectorStatus(t, &result.Status) + }) + + t.Run("reconcile", func(t *testing.T) { + expectConnectorRPC(t, server, "session.connectors.reconcile", map[string]any{ + "accountId": connectorAccountID, + "refreshCatalog": true, + "sessionId": "session-1", + }, connectorStatusResult) + + result, err := session.RPC.Connectors.Reconcile(t.Context(), &rpc.ConnectorReconcileRequest{ + AccountID: connectorAccountID, + RefreshCatalog: Bool(true), + }) + if err != nil { + t.Fatalf("Reconcile: %v", err) + } + assertConnectorStatus(t, result) + }) +} + +func expectConnectorRPC(t *testing.T, server *jsonrpc2.Client, method string, wantParams map[string]any, result string) { + t.Helper() + wantJSON, err := json.Marshal(wantParams) + if err != nil { + t.Fatalf("marshal expected %s params: %v", method, err) + } + var want any + if err := json.Unmarshal(wantJSON, &want); err != nil { + t.Fatalf("normalize expected %s params: %v", method, err) + } + + server.SetRequestHandler(method, func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var got any + if err := json.Unmarshal(params, &got); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + if !reflect.DeepEqual(got, want) { + return nil, &jsonrpc2.Error{ + Code: -32602, + Message: fmt.Sprintf("%s params = %s, want %s", method, params, wantJSON), + } + } + return json.RawMessage(result), nil + }) +} + +func assertConnectorCatalog(t *testing.T, catalog *rpc.ConnectorCatalogResult, revision int64, status rpc.ConnectorCatalogStatus) { + t.Helper() + if catalog.Revision != revision || len(catalog.Connectors) != 1 { + t.Fatalf("catalog = %#v", catalog) + } + connector := catalog.Connectors[0] + if connector.Name != "outlook" || connector.DisplayName != "Outlook" || connector.Status != status { + t.Fatalf("connector = %#v", connector) + } +} + +func assertConnectorStatus(t *testing.T, status *rpc.ConnectorStatus) { + t.Helper() + if status.AccountID == nil || + *status.AccountID != connectorAccountID || + status.APIVersion != 1 || + status.Availability != rpc.ConnectorAvailabilityEnabled || + status.PendingConnections != 2 || + len(status.RuntimeServers) != 1 { + t.Fatalf("status = %#v", status) + } + assertConnectorCatalog(t, status.Catalog, 7, rpc.ConnectorCatalogStatusConnected) + runtimeServer := status.RuntimeServers[0] + if runtimeServer.ConnectorName != "outlook" || + runtimeServer.RuntimeServerID != "connector-outlook" || + runtimeServer.Status != rpc.ConnectorMCPStatusConnected { + t.Fatalf("runtime server = %#v", runtimeServer) + } +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index e974c9ed62..ab3bd54603 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -2308,6 +2308,198 @@ type ConnectedRemoteSessionMetadataRepository struct { Owner string `json:"owner"` } +// Pins a Connector operation to one host-owned GitHub account through its opaque selection +// ID. Provider tokens are never accepted. +// Experimental: ConnectorAccountRequest is part of an experimental API and may change or be +// removed. +type ConnectorAccountRequest struct { + // Opaque account selection ID previously returned by an account discovery API. + AccountID string `json:"accountId"` +} + +// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +// Experimental: ConnectorCapabilities is part of an experimental API and may change or be +// removed. +type ConnectorCapabilities struct { + // Connector API contract version. + APIVersion int64 `json:"apiVersion"` + // Current session availability. Disabled availability is reported without making a + // Connector request. + Availability ConnectorAvailability `json:"availability"` + // Whether connect and reconnect can return an opaque continuation for bounded consent + // polling. + ConsentContinuation bool `json:"consentContinuation"` + // Maximum accepted wall-clock deadline in milliseconds for one continuation call. + MaxDeadlineMs int64 `json:"maxDeadlineMs"` + // Maximum accepted polling attempts for one continuation call. + MaxPollAttempts int64 `json:"maxPollAttempts"` + // Maximum accepted delay in milliseconds between polling attempts. + MaxPollIntervalMs int64 `json:"maxPollIntervalMs"` + // Whether callers select a host-owned GitHub account through an opaque selection ID rather + // than supplying a provider token. + OpaqueAccountSelection bool `json:"opaqueAccountSelection"` +} + +// Credential-free Connector catalog entry. +// Experimental: ConnectorCatalogEntry is part of an experimental API and may change or be +// removed. +type ConnectorCatalogEntry struct { + // Untrusted service description, when present. + Description *string `json:"description,omitempty"` + // Untrusted display label from the service. + DisplayName string `json:"displayName"` + // Canonical Connector name used by lifecycle methods. + Name string `json:"name"` + // Opaque stable runtime IDs currently projected into the session for this Connector. + RuntimeServerIDs []string `json:"runtimeServerIds"` + // Current authoritative service connection state. + Status ConnectorCatalogStatus `json:"status"` +} + +// Validated Connector catalog snapshot cached by the session. +// Experimental: ConnectorCatalogResult is part of an experimental API and may change or be +// removed. +type ConnectorCatalogResult struct { + // Validated catalog entries in service order. + Connectors []ConnectorCatalogEntry `json:"connectors"` + // Unix epoch milliseconds when this snapshot was accepted. + RefreshedAtMs int64 `json:"refreshedAtMs"` + // Monotonically increasing session-local catalog revision. + Revision int64 `json:"revision"` +} + +// Selects one Connector and the pinned host-owned account used for its service and MCP +// authorization. +// Experimental: ConnectorConnectRequest is part of an experimental API and may change or be +// removed. +type ConnectorConnectRequest struct { + // Opaque account selection ID. It must match the account already pinned to the session, if + // any. + AccountID string `json:"accountId"` + // Canonical Connector name from the current catalog. + ConnectorName string `json:"connectorName"` +} + +// Typed result of initiating or continuing a Connector connection. +// Experimental: ConnectorConnectResult is part of an experimental API and may change or be +// removed. +type ConnectorConnectResult interface { + connectorConnectResult() + Kind() ConnectorConnectResultKind +} + +type RawConnectorConnectResultData struct { + Discriminator ConnectorConnectResultKind + Raw json.RawMessage +} + +func (RawConnectorConnectResultData) connectorConnectResult() {} +func (r RawConnectorConnectResultData) Kind() ConnectorConnectResultKind { + return r.Discriminator +} + +// The service is connected and the session MCP graph was reconciled. +type ConnectorConnectResultConnected struct { + // Fresh authoritative Connector state after MCP reconciliation. + Status ConnectorStatus `json:"status"` +} + +func (ConnectorConnectResultConnected) connectorConnectResult() {} +func (ConnectorConnectResultConnected) Kind() ConnectorConnectResultKind { + return ConnectorConnectResultKindConnected +} + +// Host-owned consent is required before bounded continuation can complete. +type ConnectorConnectResultConsentRequired struct { + // Validated HTTPS consent URL. The runtime does not open it. + ConsentURL string `json:"consentUrl"` + // Opaque ID accepted by continueConnection. + ContinuationID string `json:"continuationId"` +} + +func (ConnectorConnectResultConsentRequired) connectorConnectResult() {} +func (ConnectorConnectResultConsentRequired) Kind() ConnectorConnectResultKind { + return ConnectorConnectResultKindConsentRequired +} + +// The service is still completing the connection without a consent URL. +type ConnectorConnectResultPending struct { + // Opaque ID accepted by continueConnection. + ContinuationID string `json:"continuationId"` +} + +func (ConnectorConnectResultPending) connectorConnectResult() {} +func (ConnectorConnectResultPending) Kind() ConnectorConnectResultKind { + return ConnectorConnectResultKindPending +} + +// Explicitly bounded continuation of a pending Connector connection. +// Experimental: ConnectorContinueRequest is part of an experimental API and may change or +// be removed. +type ConnectorContinueRequest struct { + // Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + ContinuationID string `json:"continuationId"` + // Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability + // limit. + DeadlineMs int64 `json:"deadlineMs"` + // Maximum catalog requests made by this call. Must be between one and the capability limit. + MaxAttempts int64 `json:"maxAttempts"` + // Delay in milliseconds between attempts. Must not exceed the capability limit. + PollIntervalMs int64 `json:"pollIntervalMs"` +} + +// Authoritative result after disconnect and MCP reconciliation. +// Experimental: ConnectorDisconnectResult is part of an experimental API and may change or +// be removed. +type ConnectorDisconnectResult struct { + // Whether the service accepted the idempotent disconnect. + Disconnected bool `json:"disconnected"` + // Fresh authoritative session state after removing Connector-owned MCP servers. + Status ConnectorStatus `json:"status"` +} + +// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +// Experimental: ConnectorReconcileRequest is part of an experimental API and may change or +// be removed. +type ConnectorReconcileRequest struct { + // Opaque account selection ID. It must match the account already pinned to the session, if + // any. + AccountID string `json:"accountId"` + // When true, refresh the catalog before reconciling. A disabled Connector API performs no + // service request. + RefreshCatalog *bool `json:"refreshCatalog,omitempty"` +} + +// Live status of one session-owned MCP projection. +// Experimental: ConnectorRuntimeStatus is part of an experimental API and may change or be +// removed. +type ConnectorRuntimeStatus struct { + // Canonical Connector name that owns this server. + ConnectorName string `json:"connectorName"` + // Opaque runtime server ID. + RuntimeServerID string `json:"runtimeServerId"` + // Current live MCP host status. + Status ConnectorMCPStatus `json:"status"` +} + +// Authoritative session connector state. Account IDs are opaque routing identifiers and +// credentials are never included. +// Experimental: ConnectorStatus is part of an experimental API and may change or be removed. +type ConnectorStatus struct { + // Opaque account selection pinned to this session, when one has been selected. + AccountID *string `json:"accountId,omitempty"` + // Connector API contract version. + APIVersion int64 `json:"apiVersion"` + // Current feature and session availability. + Availability ConnectorAvailability `json:"availability"` + // Latest validated catalog snapshot, when available. + Catalog *ConnectorCatalogResult `json:"catalog,omitempty"` + // Number of active opaque connection continuations. + PendingConnections int64 `json:"pendingConnections"` + // Live MCP status for every Connector-owned runtime server. + RuntimeServers []ConnectorRuntimeStatus `json:"runtimeServers"` +} + // Remote session connection parameters. // Experimental: ConnectRemoteSessionParams is part of an experimental API and may change or // be removed. @@ -17815,6 +18007,73 @@ const ( ConnectedRemoteSessionMetadataKindRemoteSession ConnectedRemoteSessionMetadataKind = "remote-session" ) +// Availability of the EXPERIMENTAL session connector API. +// Experimental: ConnectorAvailability is part of an experimental API and may change or be +// removed. +type ConnectorAvailability string + +const ( + // The resolved Connector feature is off. No Connector service request is made while + // disabled. + ConnectorAvailabilityDisabled ConnectorAvailability = "disabled" + // The resolved Connector feature is enabled and Connector requests are permitted. + ConnectorAvailabilityEnabled ConnectorAvailability = "enabled" + // The session has no eligible host-owned GitHub account or does not support local Connector + // projection. + ConnectorAvailabilityUnavailable ConnectorAvailability = "unavailable" +) + +// Authoritative service connection state for one Connector. +// Experimental: ConnectorCatalogStatus is part of an experimental API and may change or be +// removed. +type ConnectorCatalogStatus string + +const ( + // The Connector is connected and may contribute MCP servers. + ConnectorCatalogStatusConnected ConnectorCatalogStatus = "connected" + // The Connector service reports an unusable connection. + ConnectorCatalogStatusError ConnectorCatalogStatus = "error" + // The Connector is available but not connected. + ConnectorCatalogStatusNotConnected ConnectorCatalogStatus = "not_connected" + // The Connector service is still completing connection or consent. + ConnectorCatalogStatusPending ConnectorCatalogStatus = "pending" + // The service returned a future or unrecognized state. + ConnectorCatalogStatusUnknown ConnectorCatalogStatus = "unknown" +) + +// Kind discriminator for ConnectorConnectResult. +// Experimental: ConnectorConnectResultKind is part of an experimental API and may change or +// be removed. +type ConnectorConnectResultKind string + +const ( + ConnectorConnectResultKindConnected ConnectorConnectResultKind = "connected" + ConnectorConnectResultKindConsentRequired ConnectorConnectResultKind = "consent_required" + ConnectorConnectResultKindPending ConnectorConnectResultKind = "pending" +) + +// Live MCP status of one Connector-owned runtime server. +// Experimental: ConnectorMCPStatus is part of an experimental API and may change or be +// removed. +type ConnectorMCPStatus string + +const ( + // The server is connected and its tools are available. + ConnectorMCPStatusConnected ConnectorMCPStatus = "connected" + // The server is configured but explicitly disabled. + ConnectorMCPStatusDisabled ConnectorMCPStatus = "disabled" + // The server failed to connect or initialize. + ConnectorMCPStatusFailed ConnectorMCPStatus = "failed" + // The server requires refreshed GitHub authorization. + ConnectorMCPStatusNeedsAuth ConnectorMCPStatus = "needs_auth" + // The Connector currently has no live server configuration. + ConnectorMCPStatusNotConfigured ConnectorMCPStatus = "not_configured" + // The server connection is still being established. + ConnectorMCPStatusPending ConnectorMCPStatus = "pending" + // The server is intentionally stopped, including when managed policy blocks it. + ConnectorMCPStatusStopped ConnectorMCPStatus = "stopped" +) + // Controls how MCP tool result content is filtered: none leaves content unchanged, markdown // sanitizes HTML while preserving Markdown-friendly output, and hidden_characters removes // characters that can hide directives. @@ -23449,6 +23708,234 @@ func (a *CompletionsAPI) Request(ctx context.Context, params *CompletionsRequest return &result, nil } +// Experimental: ConnectorsAPI contains experimental APIs that may change or be removed. +type ConnectorsAPI sessionAPI + +// Connect initiates an idempotent Connector connection request without opening a browser. +// Returns connected when the service is immediately authoritative, consent_required with a +// validated URL, or pending with an opaque continuation ID. +// +// RPC method: session.connectors.connect. +// +// Parameters: Selects one Connector and the pinned host-owned account used for its service +// and MCP authorization. +// +// Returns: Typed result of initiating or continuing a Connector connection. +func (a *ConnectorsAPI) Connect(ctx context.Context, params *ConnectorConnectRequest) (ConnectorConnectResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["accountId"] = params.AccountID + req["connectorName"] = params.ConnectorName + } + raw, err := a.client.Request(ctx, "session.connectors.connect", req) + if err != nil { + return nil, err + } + result, err := unmarshalConnectorConnectResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// ContinueConnection continues a pending Connector connection with caller-supplied attempt, +// interval, and deadline bounds. The runtime never opens the returned consent URL. +// +// RPC method: session.connectors.continueConnection. +// +// Parameters: Explicitly bounded continuation of a pending Connector connection. +// +// Returns: Typed result of initiating or continuing a Connector connection. +func (a *ConnectorsAPI) ContinueConnection(ctx context.Context, params *ConnectorContinueRequest) (ConnectorConnectResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["continuationId"] = params.ContinuationID + req["deadlineMs"] = params.DeadlineMs + req["maxAttempts"] = params.MaxAttempts + req["pollIntervalMs"] = params.PollIntervalMs + } + raw, err := a.client.Request(ctx, "session.connectors.continueConnection", req) + if err != nil { + return nil, err + } + result, err := unmarshalConnectorConnectResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Disconnects one Connector for the pinned opaque account selection, refreshes the +// authoritative catalog, and removes its session-owned MCP projection. +// +// RPC method: session.connectors.disconnect. +// +// Parameters: Selects one Connector and the pinned host-owned account used for its service +// and MCP authorization. +// +// Returns: Authoritative result after disconnect and MCP reconciliation. +func (a *ConnectorsAPI) Disconnect(ctx context.Context, params *ConnectorConnectRequest) (*ConnectorDisconnectResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["accountId"] = params.AccountID + req["connectorName"] = params.ConnectorName + } + raw, err := a.client.Request(ctx, "session.connectors.disconnect", req) + if err != nil { + return nil, err + } + var result ConnectorDisconnectResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetCapabilities returns feature availability and bounded polling limits for the +// EXPERIMENTAL session connector API. This method never performs a Connector service +// request. +// +// RPC method: session.connectors.getCapabilities. +// +// Returns: Feature detection and hard polling limits for the EXPERIMENTAL session connector +// API. +func (a *ConnectorsAPI) GetCapabilities(ctx context.Context) (*ConnectorCapabilities, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.connectors.getCapabilities", req) + if err != nil { + return nil, err + } + var result ConnectorCapabilities + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetStatus returns authoritative session Connector state from current availability, pinned +// account selection, cached catalog, and live MCP projection without performing a Connector +// service request. +// +// RPC method: session.connectors.getStatus. +// +// Returns: Authoritative session connector state. Account IDs are opaque routing +// identifiers and credentials are never included. +func (a *ConnectorsAPI) GetStatus(ctx context.Context) (*ConnectorStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.connectors.getStatus", req) + if err != nil { + return nil, err + } + var result ConnectorStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// List returns the cached Connector catalog for the pinned opaque account selection, +// fetching it only when this session has no cached catalog. +// +// RPC method: session.connectors.list. +// +// Parameters: Pins a Connector operation to one host-owned GitHub account through its +// opaque selection ID. Provider tokens are never accepted. +// +// Returns: Validated Connector catalog snapshot cached by the session. +func (a *ConnectorsAPI) List(ctx context.Context, params *ConnectorAccountRequest) (*ConnectorCatalogResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["accountId"] = params.AccountID + } + raw, err := a.client.Request(ctx, "session.connectors.list", req) + if err != nil { + return nil, err + } + var result ConnectorCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Reconciles the authoritative cached or freshly requested Connector catalog into the +// session Connector MCP projection and returns live status. +// +// RPC method: session.connectors.reconcile. +// +// Parameters: Requests authoritative Connector-to-MCP reconciliation for the pinned account. +// +// Returns: Authoritative session connector state. Account IDs are opaque routing +// identifiers and credentials are never included. +func (a *ConnectorsAPI) Reconcile(ctx context.Context, params *ConnectorReconcileRequest) (*ConnectorStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["accountId"] = params.AccountID + if params.RefreshCatalog != nil { + req["refreshCatalog"] = *params.RefreshCatalog + } + } + raw, err := a.client.Request(ctx, "session.connectors.reconcile", req) + if err != nil { + return nil, err + } + var result ConnectorStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Reconnect re-initiates an idempotent Connector connection request without browser or UI +// effects, with the same typed outcomes as connect. +// +// RPC method: session.connectors.reconnect. +// +// Parameters: Selects one Connector and the pinned host-owned account used for its service +// and MCP authorization. +// +// Returns: Typed result of initiating or continuing a Connector connection. +func (a *ConnectorsAPI) Reconnect(ctx context.Context, params *ConnectorConnectRequest) (ConnectorConnectResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["accountId"] = params.AccountID + req["connectorName"] = params.ConnectorName + } + raw, err := a.client.Request(ctx, "session.connectors.reconnect", req) + if err != nil { + return nil, err + } + result, err := unmarshalConnectorConnectResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +// Refreshes and validates the Connector catalog for the pinned opaque account selection. +// +// RPC method: session.connectors.refresh. +// +// Parameters: Pins a Connector operation to one host-owned GitHub account through its +// opaque selection ID. Provider tokens are never accepted. +// +// Returns: Validated Connector catalog snapshot cached by the session. +func (a *ConnectorsAPI) Refresh(ctx context.Context, params *ConnectorAccountRequest) (*ConnectorCatalogResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["accountId"] = params.AccountID + } + raw, err := a.client.Request(ctx, "session.connectors.refresh", req) + if err != nil { + return nil, err + } + var result ConnectorCatalogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ContentExclusionAPI contains experimental APIs that may change or be // removed. type ContentExclusionAPI sessionAPI @@ -28939,6 +29426,7 @@ type SessionRPC struct { Canvas *CanvasAPI Commands *CommandsAPI Completions *CompletionsAPI + Connectors *ConnectorsAPI ContentExclusion *ContentExclusionAPI Debug *DebugAPI EventLog *EventLogAPI @@ -29271,6 +29759,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r.Canvas = (*CanvasAPI)(&r.common) r.Commands = (*CommandsAPI)(&r.common) r.Completions = (*CompletionsAPI)(&r.common) + r.Connectors = (*ConnectorsAPI)(&r.common) r.ContentExclusion = (*ContentExclusionAPI)(&r.common) r.Debug = (*DebugAPI)(&r.common) r.EventLog = (*EventLogAPI)(&r.common) diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index d18b2c09e7..97d6353411 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -1528,6 +1528,86 @@ func (r *CommandsRespondToQueuedCommandRequest) UnmarshalJSON(data []byte) error return nil } +func unmarshalConnectorConnectResult(data []byte) (ConnectorConnectResult, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind ConnectorConnectResultKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case ConnectorConnectResultKindConnected: + var d ConnectorConnectResultConnected + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case ConnectorConnectResultKindConsentRequired: + var d ConnectorConnectResultConsentRequired + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case ConnectorConnectResultKindPending: + var d ConnectorConnectResultPending + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawConnectorConnectResultData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawConnectorConnectResultData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind ConnectorConnectResultKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r ConnectorConnectResultConnected) MarshalJSON() ([]byte, error) { + type alias ConnectorConnectResultConnected + return json.Marshal(struct { + Kind ConnectorConnectResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r ConnectorConnectResultConsentRequired) MarshalJSON() ([]byte, error) { + type alias ConnectorConnectResultConsentRequired + return json.Marshal(struct { + Kind ConnectorConnectResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r ConnectorConnectResultPending) MarshalJSON() ([]byte, error) { + type alias ConnectorConnectResultPending + return json.Marshal(struct { + Kind ConnectorConnectResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func unmarshalDebugCollectLogsDestination(data []byte) (DebugCollectLogsDestination, error) { if string(data) == "null" { return nil, nil diff --git a/go/session.go b/go/session.go index 43d20e9c89..2dd2a8a9a1 100644 --- a/go/session.go +++ b/go/session.go @@ -74,8 +74,6 @@ type Session struct { managedSettings bool mcpAuthHandler MCPAuthHandler mcpAuthMu sync.RWMutex - mcpHeadersRefreshHandler MCPHeadersRefreshHandler - mcpHeadersRefreshMu sync.RWMutex userInputHandler UserInputHandler userInputMux sync.RWMutex exitPlanModeHandler ExitPlanModeRequestHandler @@ -994,18 +992,6 @@ func (s *Session) getMCPAuthHandler() MCPAuthHandler { return s.mcpAuthHandler } -func (s *Session) registerMCPHeadersRefreshHandler(handler MCPHeadersRefreshHandler) { - s.mcpHeadersRefreshMu.Lock() - defer s.mcpHeadersRefreshMu.Unlock() - s.mcpHeadersRefreshHandler = handler -} - -func (s *Session) getMCPHeadersRefreshHandler() MCPHeadersRefreshHandler { - s.mcpHeadersRefreshMu.RLock() - defer s.mcpHeadersRefreshMu.RUnlock() - return s.mcpHeadersRefreshHandler -} - func (s *Session) handleMCPAuthRequest(request MCPAuthRequest) { handler := s.getMCPAuthHandler() if handler == nil { @@ -1541,21 +1527,6 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } s.handleMCPAuthRequest(request) - case *MCPHeadersRefreshRequiredData: - handler := s.getMCPHeadersRefreshHandler() - if d.RequestID == "" || handler == nil { - return - } - s.executeMCPHeadersRefreshAndRespond( - d.RequestID, - MCPHeadersRefreshRequest{ - ServerKey: d.ServerName, - ServerURL: d.ServerURL, - Reason: d.Reason, - }, - handler, - ) - case *CommandExecuteData: s.executeCommandAndRespond(d.RequestID, d.CommandName, d.Command, d.Args) @@ -1805,54 +1776,6 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques }) } -func (s *Session) executeMCPHeadersRefreshAndRespond( - requestID string, - request MCPHeadersRefreshRequest, - handler MCPHeadersRefreshHandler, -) { - var wireResult rpc.MCPHeadersHandlePendingHeadersRefreshRequest - result, err := func() (result *MCPHeadersRefreshResult, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("MCP headers refresh handler panic: %v", r) - } - }() - return handler(request, MCPHeadersRefreshInvocation{SessionID: s.SessionID}) - }() - switch { - case err != nil: - log.Printf( - "MCP headers refresh failed: session_id=%s server_key=%s error=%v", - s.SessionID, - request.ServerKey, - err, - ) - wireResult = rpc.MCPHeadersHandlePendingHeadersRefreshRequestError{Message: err.Error()} - case result == nil: - wireResult = rpc.MCPHeadersHandlePendingHeadersRefreshRequestNone{} - default: - wireResult = rpc.MCPHeadersHandlePendingHeadersRefreshRequestHeaders{ - Headers: result.Headers, - TtlMs: result.TTLMS, - } - } - - if _, rpcErr := s.RPC.MCP.Headers().HandlePendingHeadersRefreshRequest( - context.Background(), - &rpc.MCPHeadersHandlePendingHeadersRefreshRequestRequest{ - RequestID: requestID, - Result: wireResult, - }, - ); rpcErr != nil { - log.Printf( - "failed to send MCP headers refresh response: session_id=%s request_id=%s error=%v", - s.SessionID, - requestID, - rpcErr, - ) - } -} - // GetEvents retrieves all events from this session's history. // // This returns the complete conversation history including user messages, diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go index c223f0a1aa..4941abc811 100644 --- a/go/session_event_serialization_test.go +++ b/go/session_event_serialization_test.go @@ -14,79 +14,6 @@ var _ SessionEventData = (*rpc.UserMessageData)(nil) var _ rpc.EmbeddedTextResourceContents = EmbeddedTextResourceContents{} var _ EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents{} -func TestConnectorMCPStatusEventsUsePublicSessionEventTypes(t *testing.T) { - tests := []struct { - eventType string - data map[string]any - assert func(*testing.T, SessionEventData) - }{ - { - eventType: "session.mcp_servers_loaded", - data: map[string]any{ - "servers": []any{ - map[string]any{"name": "calendar", "status": "connected"}, - }, - }, - assert: func(t *testing.T, data SessionEventData) { - loaded, ok := data.(*SessionMCPServersLoadedData) - if !ok || len(loaded.Servers) != 1 || loaded.Servers[0].Name != "calendar" { - t.Fatalf("unexpected loaded event data: %#v", data) - } - }, - }, - { - eventType: "session.mcp_server_status_changed", - data: map[string]any{"serverName": "calendar", "status": "connected"}, - assert: func(t *testing.T, data SessionEventData) { - status, ok := data.(*SessionMCPServerStatusChangedData) - if !ok || status.ServerName != "calendar" || status.Status != MCPServerStatusConnected { - t.Fatalf("unexpected status event data: %#v", data) - } - }, - }, - { - eventType: "session.mcp_server_needs_reconnect", - data: map[string]any{"serverName": "calendar"}, - assert: func(t *testing.T, data SessionEventData) { - reconnect, ok := data.(*SessionMCPServerNeedsReconnectData) - if !ok || reconnect.ServerName != "calendar" { - t.Fatalf("unexpected reconnect event data: %#v", data) - } - }, - }, - { - eventType: "session.mcp_server_removed", - data: map[string]any{"serverName": "calendar"}, - assert: func(t *testing.T, data SessionEventData) { - removed, ok := data.(*SessionMCPServerRemovedData) - if !ok || removed.ServerName != "calendar" { - t.Fatalf("unexpected removed event data: %#v", data) - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.eventType, func(t *testing.T) { - wire, err := json.Marshal(map[string]any{ - "id": "00000000-0000-0000-0000-000000000001", - "timestamp": "2026-09-16T00:00:00Z", - "parentId": nil, - "type": tt.eventType, - "data": tt.data, - }) - if err != nil { - t.Fatal(err) - } - var event SessionEvent - if err := json.Unmarshal(wire, &event); err != nil { - t.Fatal(err) - } - tt.assert(t, event.Data) - }) - } -} - func TestSessionEventAutoTier(t *testing.T) { for _, eventType := range []string{"session.start", "session.resume"} { for _, tier := range []AutoTier{"", AutoTierEfficiency, AutoTierBalance, AutoTierIntelligence, AutoTierFast} { diff --git a/go/session_test.go b/go/session_test.go index 6bfedf70b4..c3ab144afe 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "encoding/json" - "errors" "fmt" "io" "strconv" @@ -328,332 +327,6 @@ func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { } } -func TestSession_MCPHeadersRefreshHandlerResponses(t *testing.T) { - ttl := int64(5_000) - tests := []struct { - name string - result *MCPHeadersRefreshResult - err error - expected map[string]any - }{ - { - name: "headers without ttl", - result: &MCPHeadersRefreshResult{Headers: map[string]string{"Authorization": "Bearer first"}}, - expected: map[string]any{ - "kind": "headers", - "headers": map[string]any{"Authorization": "Bearer first"}, - }, - }, - { - name: "headers with ttl", - result: &MCPHeadersRefreshResult{ - Headers: map[string]string{"Authorization": "Bearer refreshed"}, - TTLMS: &ttl, - }, - expected: map[string]any{ - "kind": "headers", - "headers": map[string]any{"Authorization": "Bearer refreshed"}, - "ttlMs": float64(5_000), - }, - }, - { - name: "no headers", - expected: map[string]any{"kind": "none"}, - }, - { - name: "handler error", - err: errors.New("credential revoked"), - expected: map[string]any{"kind": "error", "message": "credential revoked"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - stdinR, stdinW := io.Pipe() - stdoutR, stdoutW := io.Pipe() - defer stdinR.Close() - defer stdinW.Close() - defer stdoutR.Close() - defer stdoutW.Close() - - client := jsonrpc2.NewClient(stdinW, stdoutR) - client.Start() - defer client.Stop() - - paramsCh := make(chan map[string]any, 1) - errCh := make(chan error, 1) - go func() { - frame, err := readTestJSONRPCFrame(stdinR) - if err != nil { - errCh <- err - return - } - var request struct { - ID json.RawMessage `json:"id"` - Method string `json:"method"` - Params map[string]any `json:"params"` - } - if err := json.Unmarshal(frame, &request); err != nil { - errCh <- err - return - } - if request.Method != "session.mcp.headers.handlePendingHeadersRefreshRequest" { - errCh <- fmt.Errorf("unexpected method %s", request.Method) - return - } - paramsCh <- request.Params - response, err := json.Marshal(map[string]any{ - "jsonrpc": "2.0", - "id": json.RawMessage(request.ID), - "result": map[string]any{"success": true}, - }) - if err != nil { - errCh <- err - return - } - _, err = fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(response), response) - if err != nil { - errCh <- err - } - }() - - session := &Session{ - SessionID: "session-1", - client: client, - RPC: rpc.NewSessionRPC(client, "session-1"), - } - session.registerMCPHeadersRefreshHandler( - func(request MCPHeadersRefreshRequest, invocation MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { - if request.ServerKey != "connector-key" || - request.ServerURL != "https://example.com/mcp" || - request.Reason != MCPHeadersRefreshRequiredReasonStartup { - t.Fatalf("unexpected request: %#v", request) - } - if invocation.SessionID != "session-1" { - t.Fatalf("unexpected invocation: %#v", invocation) - } - return tt.result, tt.err - }, - ) - session.handleBroadcastEvent(SessionEvent{ - Data: &MCPHeadersRefreshRequiredData{ - RequestID: "refresh-1", - ServerName: "connector-key", - ServerURL: "https://example.com/mcp", - Reason: MCPHeadersRefreshRequiredReasonStartup, - }, - }) - - select { - case params := <-paramsCh: - if params["requestId"] != "refresh-1" { - t.Fatalf("unexpected requestId: %v", params["requestId"]) - } - if !mapsEqual(params["result"], tt.expected) { - t.Fatalf("unexpected result: %#v", params["result"]) - } - case err := <-errCh: - t.Fatal(err) - case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for MCP headers refresh response") - } - }) - } -} - -func TestSession_MCPHeadersRefreshHandlerPanicDoesNotStopDispatch(t *testing.T) { - stdinR, stdinW := io.Pipe() - stdoutR, stdoutW := io.Pipe() - defer stdinR.Close() - defer stdinW.Close() - defer stdoutR.Close() - defer stdoutW.Close() - - client := jsonrpc2.NewClient(stdinW, stdoutR) - client.Start() - defer client.Stop() - - paramsCh := make(chan map[string]any, 2) - errCh := make(chan error, 1) - go func() { - for range 2 { - frame, err := readTestJSONRPCFrame(stdinR) - if err != nil { - errCh <- err - return - } - var request struct { - ID json.RawMessage `json:"id"` - Method string `json:"method"` - Params map[string]any `json:"params"` - } - if err := json.Unmarshal(frame, &request); err != nil { - errCh <- err - return - } - if request.Method != "session.mcp.headers.handlePendingHeadersRefreshRequest" { - errCh <- fmt.Errorf("unexpected method %s", request.Method) - return - } - response, err := json.Marshal(map[string]any{ - "jsonrpc": "2.0", - "id": request.ID, - "result": map[string]any{"success": true}, - }) - if err != nil { - errCh <- err - return - } - if _, err := fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(response), response); err != nil { - errCh <- err - return - } - paramsCh <- request.Params - } - }() - - session, cleanup := newTestSession() - defer cleanup() - session.SessionID = "session-panic" - session.client = client - session.RPC = rpc.NewSessionRPC(client, session.SessionID) - var calls atomic.Int32 - session.registerMCPHeadersRefreshHandler( - func(MCPHeadersRefreshRequest, MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) { - if calls.Add(1) == 1 { - panic("header provider crashed") - } - return &MCPHeadersRefreshResult{Headers: map[string]string{"Authorization": "refreshed"}}, nil - }, - ) - - for i, expected := range []map[string]any{ - {"kind": "error", "message": "MCP headers refresh handler panic: header provider crashed"}, - {"kind": "headers", "headers": map[string]any{"Authorization": "refreshed"}}, - } { - requestID := fmt.Sprintf("refresh-%d", i) - session.dispatchEvent(SessionEvent{Data: &MCPHeadersRefreshRequiredData{ - RequestID: requestID, - ServerName: "github", - ServerURL: "https://example.com/mcp", - Reason: MCPHeadersRefreshRequiredReasonStartup, - }}) - - select { - case params := <-paramsCh: - if params["sessionId"] != session.SessionID || params["requestId"] != requestID { - t.Fatalf("unexpected response correlation: %#v", params) - } - if !mapsEqual(params["result"], expected) { - t.Fatalf("unexpected result: %#v", params["result"]) - } - case err := <-errCh: - t.Fatal(err) - case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for MCP headers refresh response") - } - } - if got := calls.Load(); got != 2 { - t.Fatalf("expected two callback invocations, got %d", got) - } -} - -func TestSession_PublicMCPRPCIsAvailable(t *testing.T) { - client, requests, cleanup := newInMemoryClient(t) - defer cleanup() - session, err := client.CreateSession(t.Context(), &SessionConfig{}) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - defer session.Disconnect() - if session.RPC == nil || session.RPC.MCP == nil { - t.Fatal("created session does not expose the typed MCP RPC API") - } - requests.clear() - - list, err := session.RPC.MCP.List(t.Context()) - if err != nil { - t.Fatalf("MCP.List failed: %v", err) - } - if len(list.Servers) != 0 { - t.Fatalf("unexpected MCP list: %#v", list) - } - if _, err := session.RPC.MCP.Enable( - t.Context(), - &rpc.MCPEnableRequest{ServerName: "calendar"}, - ); err != nil { - t.Fatalf("MCP.Enable failed: %v", err) - } - if _, err := session.RPC.MCP.Disable( - t.Context(), - &rpc.MCPDisableRequest{ServerName: "calendar"}, - ); err != nil { - t.Fatalf("MCP.Disable failed: %v", err) - } - tools, err := session.RPC.MCP.ListTools( - t.Context(), - &rpc.MCPListToolsRequest{ServerName: "calendar"}, - ) - if err != nil { - t.Fatalf("MCP.ListTools failed: %v", err) - } - if len(tools.Tools) != 0 { - t.Fatalf("unexpected MCP tools: %#v", tools) - } - - snapshot := requests.snapshot() - expected := []struct { - method string - params map[string]any - }{ - { - method: "session.mcp.list", - params: map[string]any{"sessionId": session.SessionID}, - }, - { - method: "session.mcp.enable", - params: map[string]any{ - "sessionId": session.SessionID, - "serverName": "calendar", - }, - }, - { - method: "session.mcp.disable", - params: map[string]any{ - "sessionId": session.SessionID, - "serverName": "calendar", - }, - }, - { - method: "session.mcp.listTools", - params: map[string]any{ - "sessionId": session.SessionID, - "serverName": "calendar", - }, - }, - } - if len(snapshot) != len(expected) { - t.Fatalf("unexpected requests: %#v", snapshot) - } - for i, want := range expected { - if snapshot[i].Method != want.method || !mapsEqual(snapshot[i].Params, want.params) { - t.Fatalf("request %d = %#v, want %s %#v", i, snapshot[i], want.method, want.params) - } - } -} - -func mapsEqual(actual any, expected map[string]any) bool { - actualJSON, err := json.Marshal(actual) - if err != nil { - return false - } - expectedJSON, err := json.Marshal(expected) - if err != nil { - return false - } - return string(actualJSON) == string(expectedJSON) -} - func TestMCPAuthRequestAllowsMissingOptionalMetadata(t *testing.T) { request := MCPAuthRequest{RequestID: "oauth-request"} if request.ResourceMetadata != nil { diff --git a/go/types.go b/go/types.go index fdab010037..7bc5bfb9af 100644 --- a/go/types.go +++ b/go/types.go @@ -555,80 +555,6 @@ type MCPAuthInvocation struct { // MCPAuthHandler handles MCP OAuth requests from the runtime. type MCPAuthHandler func(request MCPAuthRequest, invocation MCPAuthInvocation) (*MCPAuthResult, error) -// ConnectorMCPServerConfig describes a connected Copilot Connector MCP -// endpoint supplied from the service catalog. The containing map key is the -// stable Connector server key. Short-lived client authorization for the exact -// service-advertised endpoint is supplied only through -// [MCPHeadersRefreshHandler]; this configuration never contains Outlook, -// Slack, or other downstream service tokens. -// -// Experimental: ConnectorMCPServerConfig is part of an experimental API and may change. -type ConnectorMCPServerConfig struct { - // DisplayName is the human-readable Connector display name. - DisplayName string `json:"displayName"` - // AuthorizationCacheTTLMS is the maximum time, in milliseconds, that the - // runtime may reuse Connector service authorization. - AuthorizationCacheTTLMS *int64 `json:"authorizationCacheTtlMs,omitempty"` - // Timeout is the timeout in milliseconds for tool discovery and tool calls. - Timeout *int64 `json:"timeout,omitempty"` - // Tools lists the tools to include. When omitted, all tools are included. - Tools []string `json:"tools,omitzero"` - // URL is the hosted MCP streamable HTTP endpoint. - URL string `json:"url"` -} - -func connectorMCPServersToWire(servers map[string]ConnectorMCPServerConfig) map[string]rpc.ManagedMCPServerConfig { - if servers == nil { - return nil - } - wire := make(map[string]rpc.ManagedMCPServerConfig, len(servers)) - for identity, server := range servers { - wire[identity] = rpc.ManagedMCPServerConfig{ - DisplayName: server.DisplayName, - HeadersRefreshTtlMs: server.AuthorizationCacheTTLMS, - Timeout: server.Timeout, - Tools: server.Tools, - URL: server.URL, - } - } - return wire -} - -// MCPHeadersRefreshRequest identifies the exact connected Copilot Connector MCP -// endpoint whose short-lived client authorization headers need refreshing. -type MCPHeadersRefreshRequest struct { - // ServerKey is the stable server identity used as the ConnectorMCPServers map key. - ServerKey string - // ServerURL is the hosted MCP streamable HTTP endpoint. - ServerURL string - // Reason describes why the runtime requested refreshed service authorization. - Reason MCPHeadersRefreshRequiredReason -} - -// MCPHeadersRefreshResult contains dynamic service-authorization headers and -// their optional cache lifetime. A nil result means no headers are available. -type MCPHeadersRefreshResult struct { - // Headers contains client-to-Copilot-Connectors service authorization headers. - Headers map[string]string - // TTLMS is the remaining authorization lifetime in milliseconds. - TTLMS *int64 -} - -// MCPHeadersRefreshInvocation provides context for a Copilot Connector service -// authorization refresh. -type MCPHeadersRefreshInvocation struct { - SessionID string -} - -// MCPHeadersRefreshHandler supplies short-lived authorization from the SDK -// client to the Copilot Connectors service for the exact endpoint in the -// request, normally using the selected account's GitHub bearer token. It does -// not supply Outlook, Slack, or other downstream service tokens; the Connector -// service owns and applies those tokens. Returning an error sends an explicit -// refresh error to the runtime. Handler panics are recovered and also sent as -// explicit refresh errors. -type MCPHeadersRefreshHandler func(request MCPHeadersRefreshRequest, invocation MCPHeadersRefreshInvocation) (*MCPHeadersRefreshResult, error) - // UserInputRequest represents a request for user input from the agent type UserInputRequest struct { Question string @@ -1476,10 +1402,6 @@ type SessionConfig struct { // When provided, the SDK can satisfy MCP server OAuth requests with host-provided // token data or cancellation. OnMCPAuthRequest MCPAuthHandler - // OnMCPHeadersRefresh supplies short-lived client-to-Copilot-Connectors - // service authorization when an endpoint in ConnectorMCPServers requests - // dynamic-header refresh. It does not supply downstream service tokens. - OnMCPHeadersRefresh MCPHeadersRefreshHandler // GitHubTokenProvider acquires session-scoped GitHub tokens on demand. It // cannot be combined with GitHubToken. GitHubTokenProvider GitHubTokenProvider @@ -1568,10 +1490,6 @@ type SessionConfig struct { ModelCapabilities *rpc.ModelCapabilitiesOverride // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig - // ConnectorMCPServers contains connected Copilot Connector MCP endpoints - // supplied from the service catalog. Keys are stable Connector server keys. - // Re-supply this map when cold-resuming a session. - ConnectorMCPServers map[string]ConnectorMCPServerConfig // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -2074,10 +1992,6 @@ type ResumeSessionConfig struct { // OnMCPAuthRequest is an optional handler for MCP OAuth requests from MCP servers. // See SessionConfig.OnMCPAuthRequest. OnMCPAuthRequest MCPAuthHandler - // OnMCPHeadersRefresh handles client-to-Copilot-Connectors service - // authorization refresh requests, not downstream service authentication. - // See [SessionConfig.OnMCPHeadersRefresh]. - OnMCPHeadersRefresh MCPHeadersRefreshHandler // OnUserInputRequest handles legacy question-and-answer requests from the agent // and enables the legacy ask_user tool. OnUserInputRequest UserInputHandler @@ -2139,10 +2053,6 @@ type ResumeSessionConfig struct { IncludeSubAgentStreamingEvents *bool // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig - // ConnectorMCPServers re-supplies connected Copilot Connector MCP endpoints - // from the service catalog after an app restart. Keys are stable Connector - // server keys. - ConnectorMCPServers map[string]ConnectorMCPServerConfig // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -2740,7 +2650,6 @@ type createSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - ManagedMCPServers map[string]rpc.ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` @@ -2853,7 +2762,6 @@ type resumeSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - ManagedMCPServers map[string]rpc.ManagedMCPServerConfig `json:"managedMcpServers,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` diff --git a/java/README.md b/java/README.md index 23d1289eed..e70b40c6a4 100644 --- a/java/README.md +++ b/java/README.md @@ -176,42 +176,62 @@ and `setExcludedTools(...)`, prefer the source-qualified filter form `DefaultAgentConfig.setExcludedTools(...)`, use `-` directly. -For experimental Copilot Connector integration, supply connected Connector MCP -endpoints from the service catalog through -`SessionConfig.setConnectorMcpServers(...)`. Re-supply them through -`ResumeSessionConfig.setConnectorMcpServers(...)` when cold-resuming. - -The MCP headers refresh handler supplies short-lived authorization from the SDK -client to the exact Copilot Connectors service endpoint advertised by the -service catalog. This is normally the selected account's GitHub bearer token. -It is not an Outlook, Slack, or other downstream provider credential; the -Connector service owns those provider tokens. - -`ConnectorMcpServerConfig.setAuthorizationCacheTtlMs(...)` sets the maximum -time the runtime may reuse that Connector service authorization. A refresh -handler result's `ttlMs` is the remaining authorization lifetime; it may further -shorten that cache period. - -Correct same-session reconciliation after Connector service Connect or -Disconnect operations is not available through the current public runtime -contract. Transient server lifecycle operations do not replace the -authoritative Connector configuration and may be undone by a reload. To apply a -changed connected set, create a new session or cold-resume with the updated -map passed to `setConnectorMcpServers(...)` until the runtime exposes supported -public replacement and removal operations. - -For servers already configured on a session, the public generated session RPC -API supports `list()` for status, `enable(...)`, `disable(...)`, -and `listTools(...)`. Access it through `session.getRpc().mcp`. These operations -do not add or remove Connector configurations. Generated RPC parameter and -event models call the stable Connector MCP server map key `serverName`; it is -not the display name or Connector service ID. - -Hosts can subscribe with `session.on(...)` to -`SessionMcpServersLoadedEvent`, `SessionMcpServerStatusChangedEvent`, -`SessionMcpServerRemovedEvent`, and `SessionMcpServerNeedsReconnectEvent`. -The SDK does not fetch the Connector service catalog, request consent, or -provide UI; application hosts own those responsibilities. +## Connector session API (experimental) + +The Connector API is experimental and may be unavailable in some runtime +versions. Check `getCapabilities()` before using the remaining methods. + +Opt in to experimental APIs, then use the generated DTOs through +`session.getRpc().connectors`: + +```java +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.CopilotSession; +import com.github.copilot.generated.rpc.ConnectorAvailability; +import com.github.copilot.generated.rpc.ConnectorConnectResultConsentRequired; +import com.github.copilot.generated.rpc.SessionConnectorsConnectParams; +import com.github.copilot.generated.rpc.SessionConnectorsContinueConnectionParams; +import com.github.copilot.generated.rpc.SessionConnectorsListParams; +import com.github.copilot.generated.rpc.SessionConnectorsReconcileParams; + +@AllowCopilotExperimental +static void connectConnector(CopilotSession session, String accountId) + throws Exception { + var connectors = session.getRpc().connectors; + var capabilities = connectors.getCapabilities().get(); + if (capabilities.availability() != ConnectorAvailability.ENABLED) { + return; + } + + var catalog = connectors + .list(new SessionConnectorsListParams(null, accountId)).get(); + connectors + .reconcile(new SessionConnectorsReconcileParams( + null, accountId, false)).get(); + var connectorName = chooseConnector(catalog.connectors()); + var connection = connectors + .connect(new SessionConnectorsConnectParams( + null, accountId, connectorName)).get(); + if (connection instanceof ConnectorConnectResultConsentRequired consent) { + openConsentUrl(consent.getConsentUrl()); + connection = connectors + .continueConnection(new SessionConnectorsContinueConnectionParams( + null, + consent.getContinuationId(), + capabilities.maxPollAttempts(), + capabilities.maxPollIntervalMs(), + capabilities.maxDeadlineMs())) + .get(); + } +} +``` + +Pass `null` for each DTO's `sessionId`; the session-scoped wrapper injects the +active session ID. Bound continuation values using `getCapabilities()`. The +host supplies only its opaque account selection ID and owns browser and consent +UI; Connector methods never accept or return credentials or provider tokens. +The namespace also exposes `getStatus`, `refresh`, `reconnect`, `disconnect`, +and `reconcile`. `CopilotClientOptions.setCwd(...)` sets the runtime process working directory, which otherwise inherits the current process working directory. `SessionConfig.setWorkingDirectory(...)` sets the session working directory, which otherwise defaults to the runtime process working directory. diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 97fdfe3250..234d24beaa 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -12,6 +12,8 @@ import type { JSONSchema7 } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; +import { applyConnectorSessionApiOverlay } from "../../../scripts/codegen/connectorSessionApiOverlay.js"; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -1478,7 +1480,9 @@ function generateRpcClass( async function generateRpcTypes(schemaPath: string): Promise { console.log("\n🔌 Generating RPC types..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent)) as Record & { + const schema = normalizeSchemaBrandCasing( + applyConnectorSessionApiOverlay(JSON.parse(schemaContent), path.basename(schemaPath)) + ) as Record & { server?: Record; session?: Record; clientSession?: Record; @@ -2342,7 +2346,9 @@ async function generateRpcWrappers(schemaPath: string): Promise { console.log("\n🔧 Generating RPC wrapper classes..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); - const schema = normalizeSchemaBrandCasing(JSON.parse(schemaContent)) as { + const schema = normalizeSchemaBrandCasing( + applyConnectorSessionApiOverlay(JSON.parse(schemaContent), path.basename(schemaPath)) + ) as { server?: Record; session?: Record; clientSession?: Record; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java new file mode 100644 index 0000000000..2b2b19dc53 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Availability of the EXPERIMENTAL session connector API. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ConnectorAvailability { + /** The {@code enabled} variant. */ + ENABLED("enabled"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code unavailable} variant. */ + UNAVAILABLE("unavailable"); + + private final String value; + ConnectorAvailability(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ConnectorAvailability fromValue(String value) { + for (ConnectorAvailability v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ConnectorAvailability value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java new file mode 100644 index 0000000000..612dd11f8b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Credential-free Connector catalog entry. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorCatalogEntry( + /** Canonical Connector name used by lifecycle methods. */ + @JsonProperty("name") String name, + /** Untrusted display label from the service. */ + @JsonProperty("displayName") String displayName, + /** Untrusted service description, when present. */ + @JsonProperty("description") String description, + /** Current authoritative service connection state. */ + @JsonProperty("status") ConnectorCatalogStatus status, + /** Opaque stable runtime IDs currently projected into the session for this Connector. */ + @JsonProperty("runtimeServerIds") List runtimeServerIds +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java new file mode 100644 index 0000000000..2e4102daf9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Validated Connector catalog snapshot cached by the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorCatalogResult( + /** Monotonically increasing session-local catalog revision. */ + @JsonProperty("revision") Long revision, + /** Unix epoch milliseconds when this snapshot was accepted. */ + @JsonProperty("refreshedAtMs") Long refreshedAtMs, + /** Validated catalog entries in service order. */ + @JsonProperty("connectors") List connectors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java new file mode 100644 index 0000000000..50f4ede020 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Authoritative service connection state for one Connector. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ConnectorCatalogStatus { + /** The {@code not_connected} variant. */ + NOT_CONNECTED("not_connected"), + /** The {@code pending} variant. */ + PENDING("pending"), + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code error} variant. */ + ERROR("error"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"); + + private final String value; + ConnectorCatalogStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ConnectorCatalogStatus fromValue(String value) { + for (ConnectorCatalogStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ConnectorCatalogStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java new file mode 100644 index 0000000000..53a6cd0884 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Typed result of initiating or continuing a Connector connection. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = ConnectorConnectResultConnected.class, name = "connected"), + @JsonSubTypes.Type(value = ConnectorConnectResultConsentRequired.class, name = "consent_required"), + @JsonSubTypes.Type(value = ConnectorConnectResultPending.class, name = "pending") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class ConnectorConnectResult { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java new file mode 100644 index 0000000000..4eb5f6f63f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The service is connected and the session MCP graph was reconciled. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ConnectorConnectResultConnected extends ConnectorConnectResult { + + @JsonProperty("kind") + private final String kind = "connected"; + + @Override + public String getKind() { return kind; } + + /** Fresh authoritative Connector state after MCP reconciliation. */ + @JsonProperty("status") + private ConnectorStatus status; + + public ConnectorStatus getStatus() { return status; } + public void setStatus(ConnectorStatus status) { this.status = status; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java new file mode 100644 index 0000000000..c501753aaf --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Host-owned consent is required before bounded continuation can complete. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ConnectorConnectResultConsentRequired extends ConnectorConnectResult { + + @JsonProperty("kind") + private final String kind = "consent_required"; + + @Override + public String getKind() { return kind; } + + /** Validated HTTPS consent URL. The runtime does not open it. */ + @JsonProperty("consentUrl") + private String consentUrl; + + /** Opaque ID accepted by continueConnection. */ + @JsonProperty("continuationId") + private String continuationId; + + public String getConsentUrl() { return consentUrl; } + public void setConsentUrl(String consentUrl) { this.consentUrl = consentUrl; } + + public String getContinuationId() { return continuationId; } + public void setContinuationId(String continuationId) { this.continuationId = continuationId; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java new file mode 100644 index 0000000000..8ef1f4f608 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The service is still completing the connection without a consent URL. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ConnectorConnectResultPending extends ConnectorConnectResult { + + @JsonProperty("kind") + private final String kind = "pending"; + + @Override + public String getKind() { return kind; } + + /** Opaque ID accepted by continueConnection. */ + @JsonProperty("continuationId") + private String continuationId; + + public String getContinuationId() { return continuationId; } + public void setContinuationId(String continuationId) { this.continuationId = continuationId; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java new file mode 100644 index 0000000000..97fbf5a9bf --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Live MCP status of one Connector-owned runtime server. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ConnectorMcpStatus { + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code pending} variant. */ + PENDING("pending"), + /** The {@code needs_auth} variant. */ + NEEDS_AUTH("needs_auth"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code stopped} variant. */ + STOPPED("stopped"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code not_configured} variant. */ + NOT_CONFIGURED("not_configured"); + + private final String value; + ConnectorMcpStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ConnectorMcpStatus fromValue(String value) { + for (ConnectorMcpStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ConnectorMcpStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java new file mode 100644 index 0000000000..61f01b82fa --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Live status of one session-owned MCP projection. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorRuntimeStatus( + /** Opaque runtime server ID. */ + @JsonProperty("runtimeServerId") String runtimeServerId, + /** Canonical Connector name that owns this server. */ + @JsonProperty("connectorName") String connectorName, + /** Current live MCP host status. */ + @JsonProperty("status") ConnectorMcpStatus status +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java new file mode 100644 index 0000000000..00bbfbef9d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorStatus( + /** Connector API contract version. */ + @JsonProperty("apiVersion") Long apiVersion, + /** Current feature and session availability. */ + @JsonProperty("availability") ConnectorAvailability availability, + /** Opaque account selection pinned to this session, when one has been selected. */ + @JsonProperty("accountId") String accountId, + /** Latest validated catalog snapshot, when available. */ + @JsonProperty("catalog") ConnectorCatalogResult catalog, + /** Live MCP status for every Connector-owned runtime server. */ + @JsonProperty("runtimeServers") List runtimeServers, + /** Number of active opaque connection continuations. */ + @JsonProperty("pendingConnections") Long pendingConnections +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java new file mode 100644 index 0000000000..84537bed58 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code connectors} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionConnectorsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionConnectorsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getCapabilities() { + return caller.invoke("session.connectors.getCapabilities", java.util.Map.of("sessionId", this.sessionId), SessionConnectorsGetCapabilitiesResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getStatus() { + return caller.invoke("session.connectors.getStatus", java.util.Map.of("sessionId", this.sessionId), SessionConnectorsGetStatusResult.class); + } + + /** + * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list(SessionConnectorsListParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.list", _p, SessionConnectorsListResult.class); + } + + /** + * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture refresh(SessionConnectorsRefreshParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.refresh", _p, SessionConnectorsRefreshResult.class); + } + + /** + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture connect(SessionConnectorsConnectParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.connect", _p, ConnectorConnectResult.class); + } + + /** + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture reconnect(SessionConnectorsReconnectParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.reconnect", _p, ConnectorConnectResult.class); + } + + /** + * Explicitly bounded continuation of a pending Connector connection. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture continueConnection(SessionConnectorsContinueConnectionParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.continueConnection", _p, ConnectorConnectResult.class); + } + + /** + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture disconnect(SessionConnectorsDisconnectParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.disconnect", _p, SessionConnectorsDisconnectResult.class); + } + + /** + * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture reconcile(SessionConnectorsReconcileParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.reconcile", _p, SessionConnectorsReconcileResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsConnectParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsConnectParams.java new file mode 100644 index 0000000000..6a3af85a4e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsConnectParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsConnectParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque account selection ID. It must match the account already pinned to the session, if any. */ + @JsonProperty("accountId") String accountId, + /** Canonical Connector name from the current catalog. */ + @JsonProperty("connectorName") String connectorName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java new file mode 100644 index 0000000000..96660faeea --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Explicitly bounded continuation of a pending Connector connection. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsContinueConnectionParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque continuation ID returned by connect, reconnect, or an earlier continuation. */ + @JsonProperty("continuationId") String continuationId, + /** Maximum catalog requests made by this call. Must be between one and the capability limit. */ + @JsonProperty("maxAttempts") Long maxAttempts, + /** Delay in milliseconds between attempts. Must not exceed the capability limit. */ + @JsonProperty("pollIntervalMs") Long pollIntervalMs, + /** Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. */ + @JsonProperty("deadlineMs") Long deadlineMs +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectParams.java new file mode 100644 index 0000000000..93ea746ed6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsDisconnectParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque account selection ID. It must match the account already pinned to the session, if any. */ + @JsonProperty("accountId") String accountId, + /** Canonical Connector name from the current catalog. */ + @JsonProperty("connectorName") String connectorName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectResult.java new file mode 100644 index 0000000000..2e13cfbfb6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsDisconnectResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Authoritative result after disconnect and MCP reconciliation. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsDisconnectResult( + /** Whether the service accepted the idempotent disconnect. */ + @JsonProperty("disconnected") Boolean disconnected, + /** Fresh authoritative session state after removing Connector-owned MCP servers. */ + @JsonProperty("status") ConnectorStatus status +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesParams.java new file mode 100644 index 0000000000..8eb2b1f917 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsGetCapabilitiesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java new file mode 100644 index 0000000000..65f41baece --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Feature detection and hard polling limits for the EXPERIMENTAL session connector API. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsGetCapabilitiesResult( + /** Connector API contract version. */ + @JsonProperty("apiVersion") Long apiVersion, + /** Current session availability. Disabled availability is reported without making a Connector request. */ + @JsonProperty("availability") ConnectorAvailability availability, + /** Whether connect and reconnect can return an opaque continuation for bounded consent polling. */ + @JsonProperty("consentContinuation") Boolean consentContinuation, + /** Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. */ + @JsonProperty("opaqueAccountSelection") Boolean opaqueAccountSelection, + /** Maximum accepted polling attempts for one continuation call. */ + @JsonProperty("maxPollAttempts") Long maxPollAttempts, + /** Maximum accepted delay in milliseconds between polling attempts. */ + @JsonProperty("maxPollIntervalMs") Long maxPollIntervalMs, + /** Maximum accepted wall-clock deadline in milliseconds for one continuation call. */ + @JsonProperty("maxDeadlineMs") Long maxDeadlineMs +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusParams.java new file mode 100644 index 0000000000..a658675fda --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsGetStatusParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusResult.java new file mode 100644 index 0000000000..69fbd028e9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetStatusResult.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsGetStatusResult( + /** Connector API contract version. */ + @JsonProperty("apiVersion") Long apiVersion, + /** Current feature and session availability. */ + @JsonProperty("availability") ConnectorAvailability availability, + /** Opaque account selection pinned to this session, when one has been selected. */ + @JsonProperty("accountId") String accountId, + /** Latest validated catalog snapshot, when available. */ + @JsonProperty("catalog") ConnectorCatalogResult catalog, + /** Live MCP status for every Connector-owned runtime server. */ + @JsonProperty("runtimeServers") List runtimeServers, + /** Number of active opaque connection continuations. */ + @JsonProperty("pendingConnections") Long pendingConnections +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListParams.java new file mode 100644 index 0000000000..192edb85cb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque account selection ID previously returned by an account discovery API. */ + @JsonProperty("accountId") String accountId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListResult.java new file mode 100644 index 0000000000..b074632671 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsListResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Validated Connector catalog snapshot cached by the session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsListResult( + /** Monotonically increasing session-local catalog revision. */ + @JsonProperty("revision") Long revision, + /** Unix epoch milliseconds when this snapshot was accepted. */ + @JsonProperty("refreshedAtMs") Long refreshedAtMs, + /** Validated catalog entries in service order. */ + @JsonProperty("connectors") List connectors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileParams.java new file mode 100644 index 0000000000..1a048d9ce0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsReconcileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque account selection ID. It must match the account already pinned to the session, if any. */ + @JsonProperty("accountId") String accountId, + /** When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. */ + @JsonProperty("refreshCatalog") Boolean refreshCatalog +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileResult.java new file mode 100644 index 0000000000..e854207f2a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileResult.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsReconcileResult( + /** Connector API contract version. */ + @JsonProperty("apiVersion") Long apiVersion, + /** Current feature and session availability. */ + @JsonProperty("availability") ConnectorAvailability availability, + /** Opaque account selection pinned to this session, when one has been selected. */ + @JsonProperty("accountId") String accountId, + /** Latest validated catalog snapshot, when available. */ + @JsonProperty("catalog") ConnectorCatalogResult catalog, + /** Live MCP status for every Connector-owned runtime server. */ + @JsonProperty("runtimeServers") List runtimeServers, + /** Number of active opaque connection continuations. */ + @JsonProperty("pendingConnections") Long pendingConnections +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconnectParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconnectParams.java new file mode 100644 index 0000000000..eebe0ea5ae --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconnectParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsReconnectParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque account selection ID. It must match the account already pinned to the session, if any. */ + @JsonProperty("accountId") String accountId, + /** Canonical Connector name from the current catalog. */ + @JsonProperty("connectorName") String connectorName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshParams.java new file mode 100644 index 0000000000..71be63308d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsRefreshParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque account selection ID previously returned by an account discovery API. */ + @JsonProperty("accountId") String accountId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshResult.java new file mode 100644 index 0000000000..840b861c4c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsRefreshResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Validated Connector catalog snapshot cached by the session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionConnectorsRefreshResult( + /** Monotonically increasing session-local catalog revision. */ + @JsonProperty("revision") Long revision, + /** Unix epoch milliseconds when this snapshot was accepted. */ + @JsonProperty("refreshedAtMs") Long refreshedAtMs, + /** Validated catalog entries in service order. */ + @JsonProperty("connectors") List connectors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index cf89d37208..6c029e5de4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -109,6 +109,8 @@ public final class SessionRpc { public final SessionVisibilityApi visibility; /** API methods for the {@code schedule} namespace. */ public final SessionScheduleApi schedule; + /** API methods for the {@code connectors} namespace. */ + public final SessionConnectorsApi connectors; /** * Creates a new session RPC client. @@ -159,6 +161,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.remote = new SessionRemoteApi(caller, sessionId); this.visibility = new SessionVisibilityApi(caller, sessionId); this.schedule = new SessionScheduleApi(caller, sessionId); + this.connectors = new SessionConnectorsApi(caller, sessionId); } /** diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index dfdac9eed9..9f7d8ebcf8 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -1026,29 +1026,32 @@ public CompletableFuture createSession(SessionConfig config) { session.setGitHubTokenProviderRegistration(tokenRegistration); } registeredIdHolder[0] = returnedId; - CompletableFuture interests = registerMcpEventInterests(session, returnedId, - config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefresh() != null); + CompletableFuture interest = config.getOnMcpAuthRequest() != null + ? session.getRpc().eventLog.registerInterest( + new SessionEventLogRegisterInterestParams(returnedId, "mcp.oauth_required")) + : CompletableFuture.completedFuture(null); session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); session.setOpenCanvases(response.openCanvases()); - return interests.thenCompose(v -> updateSessionOptionsForMode(session, - config.getSkipCustomInstructions().orElse(null), - config.getCustomAgentsLocalOnly().orElse(null), - config.getCoauthorEnabled().orElse(null), - config.getManageScheduleEnabled().orElse(null), config.getIncludedBuiltinSkills())) - .thenApply(v -> { - if (tokenRegistration != null) { - tokenRegistration.claim(session.getSessionId()); - } else { - gitHubTokenProviders.retire(session.getSessionId()); - } - LoggingHelpers.logTiming(LOG, Level.FINE, - "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" - + session.getSessionId(), - totalNanos); - return session; - }); + return interest.thenCompose(interestResult -> { + logMcpAuthInterestRegistration(interestResult); + return updateSessionOptionsForMode(session, config.getSkipCustomInstructions().orElse(null), + config.getCustomAgentsLocalOnly().orElse(null), + config.getCoauthorEnabled().orElse(null), + config.getManageScheduleEnabled().orElse(null), config.getIncludedBuiltinSkills()); + }).thenApply(v -> { + if (tokenRegistration != null) { + tokenRegistration.claim(session.getSessionId()); + } else { + gitHubTokenProviders.retire(session.getSessionId()); + } + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" + + session.getSessionId(), + totalNanos); + return session; + }); }).exceptionally(ex -> { if (preRegisteredSessionHolder[0] != null) { preRegisteredSessionHolder[0].cancelPendingExternalTools(); @@ -1068,26 +1071,9 @@ public CompletableFuture createSession(SessionConfig config) { }); } - private static CompletableFuture registerMcpEventInterests(CopilotSession session, String sessionId, - boolean auth, boolean headersRefresh) { - CompletableFuture interests = CompletableFuture.completedFuture(null); - if (auth) { - interests = interests.thenCompose(v -> session.getRpc().eventLog - .registerInterest(new SessionEventLogRegisterInterestParams(sessionId, "mcp.oauth_required")) - .thenAccept(result -> logMcpInterestRegistration(result, "OAuth"))); - } - if (headersRefresh) { - interests = interests.thenCompose(v -> session.getRpc().eventLog - .registerInterest( - new SessionEventLogRegisterInterestParams(sessionId, "mcp.headers_refresh_required")) - .thenAccept(result -> logMcpInterestRegistration(result, "headers refresh"))); - } - return interests; - } - - private static void logMcpInterestRegistration(Object interestResult, String eventName) { + private static void logMcpAuthInterestRegistration(Object interestResult) { if (interestResult != null && LOG.isLoggable(Level.FINEST)) { - LOG.finest("MCP " + eventName + " event interest registered"); + LOG.finest("MCP OAuth event interest registered"); } } @@ -1213,9 +1199,14 @@ public CompletableFuture resumeSession(String sessionId, ResumeS rpcNanos); String returnedId = response.sessionId(); String interestSessionId = returnedId != null ? returnedId : sessionId; - return registerMcpEventInterests(session, interestSessionId, - config.getOnMcpAuthRequest() != null, config.getOnMcpHeadersRefresh() != null) - .thenApply(v -> response); + CompletableFuture interest = config.getOnMcpAuthRequest() != null + ? session.getRpc().eventLog.registerInterest(new SessionEventLogRegisterInterestParams( + interestSessionId, "mcp.oauth_required")) + : CompletableFuture.completedFuture(null); + return interest.thenApply(interestResult -> { + logMcpAuthInterestRegistration(interestResult); + return response; + }); }).thenCompose(response -> { session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index 8a054aac7d..e0aab22e1f 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -14,7 +14,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; -import java.util.concurrent.CompletionException; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -38,7 +37,6 @@ import com.github.copilot.generated.rpc.SessionLogParams; import com.github.copilot.generated.rpc.SessionLogLevel; import com.github.copilot.generated.rpc.SessionMcpOauthHandlePendingRequestParams; -import com.github.copilot.generated.rpc.SessionMcpHeadersHandlePendingHeadersRefreshRequestParams; import com.github.copilot.generated.rpc.ModelCapabilitiesOverride; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideLimits; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideSupports; @@ -57,7 +55,6 @@ import com.github.copilot.generated.CommandExecuteEvent; import com.github.copilot.generated.ElicitationRequestedEvent; import com.github.copilot.generated.ExternalToolRequestedEvent; -import com.github.copilot.generated.McpHeadersRefreshRequiredEvent; import com.github.copilot.generated.McpOauthRequiredEvent; import com.github.copilot.generated.PermissionRequestedEvent; import com.github.copilot.generated.SessionCanvasClosedEvent; @@ -95,10 +92,6 @@ import com.github.copilot.rpc.McpAuthInvocation; import com.github.copilot.rpc.McpAuthRequest; import com.github.copilot.rpc.McpAuthResult; -import com.github.copilot.rpc.McpHeadersRefreshHandler; -import com.github.copilot.rpc.McpHeadersRefreshInvocation; -import com.github.copilot.rpc.McpHeadersRefreshRequest; -import com.github.copilot.rpc.McpHeadersRefreshResult; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.PermissionInvocation; import com.github.copilot.rpc.PermissionRequest; @@ -203,7 +196,6 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference permissionHandler = new AtomicReference<>(); private volatile boolean managedSettingsEnabled; private final AtomicReference mcpAuthHandler = new AtomicReference<>(); - private final AtomicReference mcpHeadersRefreshHandler = new AtomicReference<>(); private final AtomicReference userInputHandler = new AtomicReference<>(); private final AtomicReference elicitationHandler = new AtomicReference<>(); private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); @@ -402,16 +394,6 @@ public SessionUiApi getUi() { *

{@code
      * var agents = session.getRpc().agent.list().get();
      * }
- *

- * For MCP servers already configured on the session, the public {@code mcp} - * namespace supports listing server status and tools, enabling or disabling - * servers. The generated parameter field named {@code serverName} takes the - * stable configuration map key. Status changes are available through - * {@link #on(Class, Consumer)} with - * {@link com.github.copilot.generated.SessionMcpServersLoadedEvent}, - * {@link com.github.copilot.generated.SessionMcpServerStatusChangedEvent}, - * {@link com.github.copilot.generated.SessionMcpServerRemovedEvent}, and - * {@link com.github.copilot.generated.SessionMcpServerNeedsReconnectEvent}. * * @return the session-scoped typed RPC client (never {@code null}) * @throws IllegalStateException @@ -959,17 +941,6 @@ private void handleBroadcastEventAsync(SessionEvent event) { executeMcpAuthAndRespondAsync(new McpAuthRequest(data.requestId(), data.serverName(), data.serverUrl(), data.reason(), data.wwwAuthenticateParams(), data.resourceMetadata(), data.staticClientConfig()), handler); - } else if (event instanceof McpHeadersRefreshRequiredEvent headersEvent) { - var data = headersEvent.getData(); - if (data == null || data.requestId() == null) { - return; - } - McpHeadersRefreshHandler handler = mcpHeadersRefreshHandler.get(); - if (handler == null) { - return; - } - executeMcpHeadersRefreshAndRespondAsync(data.requestId(), - new McpHeadersRefreshRequest(data.serverName(), data.serverUrl(), data.reason()), handler); } else if (event instanceof CommandExecuteEvent cmdEvent) { var data = cmdEvent.getData(); if (data == null || data.requestId() == null || data.commandName() == null) { @@ -1265,70 +1236,6 @@ private void sendMcpAuthResponse(String requestId, McpAuthResult result) { } } - private void executeMcpHeadersRefreshAndRespondAsync(String requestId, McpHeadersRefreshRequest request, - McpHeadersRefreshHandler handler) { - Runnable task = () -> { - try { - var invocation = new McpHeadersRefreshInvocation().setSessionId(sessionId); - handler.handle(request, invocation) - .thenAccept(result -> sendMcpHeadersRefreshResponse(requestId, result)).exceptionally(ex -> { - sendMcpHeadersRefreshError(requestId, ex); - return null; - }); - } catch (Exception e) { - sendMcpHeadersRefreshError(requestId, e); - } - }; - try { - if (executor != null) { - CompletableFuture.runAsync(task, executor); - } else { - CompletableFuture.runAsync(task); - } - } catch (RejectedExecutionException e) { - LOG.log(Level.WARNING, - "Executor rejected MCP headers refresh task for requestId=" + requestId + "; running inline", e); - task.run(); - } - } - - private void sendMcpHeadersRefreshResponse(String requestId, McpHeadersRefreshResult result) { - Map response; - if (result == null || result.headers() == null) { - response = Map.of("kind", "none"); - } else { - var headersResponse = new java.util.HashMap(); - headersResponse.put("kind", "headers"); - headersResponse.put("headers", result.headers()); - if (result.ttlMs() != null) { - headersResponse.put("ttlMs", result.ttlMs()); - } - response = headersResponse; - } - sendMcpHeadersRefreshResponse(requestId, response); - } - - private void sendMcpHeadersRefreshError(String requestId, Throwable error) { - sendMcpHeadersRefreshResponse(requestId, Map.of("kind", "error", "message", errorMessage(error))); - } - - private void sendMcpHeadersRefreshResponse(String requestId, Map response) { - try { - getRpc().mcp.headers.handlePendingHeadersRefreshRequest( - new SessionMcpHeadersHandlePendingHeadersRefreshRequestParams(sessionId, requestId, response)); - } catch (Exception e) { - LOG.log(Level.WARNING, "Error sending MCP headers refresh response for requestId=" + requestId, e); - } - } - - private static String errorMessage(Throwable error) { - Throwable current = error; - while (current instanceof CompletionException && current.getCause() != null) { - current = current.getCause(); - } - return current.getMessage() != null ? current.getMessage() : current.toString(); - } - /** * Registers custom tool handlers for this session. *

@@ -1601,10 +1508,6 @@ void registerMcpAuthHandler(McpAuthHandler handler) { mcpAuthHandler.set(handler); } - void registerMcpHeadersRefreshHandler(McpHeadersRefreshHandler handler) { - mcpHeadersRefreshHandler.set(handler); - } - /** * Handles a permission request from the Copilot CLI. *

@@ -2659,7 +2562,7 @@ public void close() { } } - // ===== Internal wire types ===== + // ===== Internal response types for agent API ===== @JsonIgnoreProperties(ignoreUnknown = true) record SessionDetachResponse(@JsonProperty("success") boolean success, @JsonProperty("error") String error) { diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index 0e2683ccee..b336e70472 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -148,7 +148,6 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); - request.setConnectorMcpServers(config.getConnectorMcpServers()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl()); request.setCustomAgents(config.getCustomAgents()); @@ -305,7 +304,6 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); - request.setConnectorMcpServers(config.getConnectorMcpServers()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl()); request.setCustomAgents(config.getCustomAgents()); @@ -391,9 +389,6 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getOnMcpAuthRequest() != null) { session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); } - if (config.getOnMcpHeadersRefresh() != null) { - session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefresh()); - } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } @@ -446,9 +441,6 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getOnMcpAuthRequest() != null) { session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); } - if (config.getOnMcpHeadersRefresh() != null) { - session.registerMcpHeadersRefreshHandler(config.getOnMcpHeadersRefresh()); - } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java deleted file mode 100644 index 55f57f39e3..0000000000 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ConnectorMcpServerConfig.java +++ /dev/null @@ -1,175 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.rpc; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import com.github.copilot.generated.rpc.ManagedMcpServerConfig; - -/** - * Configuration for a connected Copilot Connector MCP endpoint supplied from - * the service catalog. - *

- * Short-lived client-to-Copilot-Connectors service authorization must be - * supplied dynamically through {@link McpHeadersRefreshHandler} and must not be - * stored in this configuration. This is normally the selected account's GitHub - * bearer token for the exact service-advertised endpoint. It is not a - * downstream provider credential; the Connector service owns provider tokens - * for services such as Outlook or Slack. - *

- * The containing map key is the stable Connector server key reported by - * {@link McpHeadersRefreshRequest#serverKey()}; {@link #getDisplayName()} is - * only the human-readable Connector label. - * - * @see SessionConfig#setConnectorMcpServers(Map) - * @see ResumeSessionConfig#setConnectorMcpServers(Map) - * @since 1.0.0 - */ -public final class ConnectorMcpServerConfig { - - private String displayName; - private String url; - private List tools; - private Long timeout; - private Long authorizationCacheTtlMs; - - /** - * Gets the human-readable Connector display name. - * - * @return the display name - */ - public String getDisplayName() { - return displayName; - } - - /** - * Sets the human-readable Connector display name. - * - * @param displayName - * the display name - * @return this config instance for method chaining - */ - public ConnectorMcpServerConfig setDisplayName(String displayName) { - this.displayName = displayName; - return this; - } - - /** - * Gets the Copilot Connector MCP streamable HTTP endpoint. - * - * @return the server URL - */ - public String getUrl() { - return url; - } - - /** - * Sets the Copilot Connector MCP streamable HTTP endpoint. - * - * @param url - * the server URL - * @return this config instance for method chaining - */ - public ConnectorMcpServerConfig setUrl(String url) { - this.url = url; - return this; - } - - /** - * Gets the tools to include from the server. - * - * @return the included tools, or {@code null} to include all tools - */ - public List getTools() { - return tools == null ? null : Collections.unmodifiableList(tools); - } - - /** - * Sets the tools to include from the server. - * - * @param tools - * the included tools, or {@code null} to include all tools - * @return this config instance for method chaining - */ - public ConnectorMcpServerConfig setTools(List tools) { - this.tools = tools; - return this; - } - - /** - * Gets the timeout for tool discovery and tool calls. - * - * @return the timeout in milliseconds, or {@code null} for the default - */ - public Long getTimeout() { - return timeout; - } - - /** - * Sets the timeout for tool discovery and tool calls. - * - * @param timeout - * the timeout in milliseconds, or {@code null} for the default - * @return this config instance for method chaining - */ - public ConnectorMcpServerConfig setTimeout(Long timeout) { - this.timeout = timeout; - return this; - } - - /** - * Gets the maximum time the runtime may reuse Connector service authorization. - * - * @return the maximum authorization cache lifetime in milliseconds, or - * {@code null} for the runtime default - */ - public Long getAuthorizationCacheTtlMs() { - return authorizationCacheTtlMs; - } - - /** - * Sets the maximum time the runtime may reuse Connector service authorization. - * - * @param authorizationCacheTtlMs - * the maximum authorization cache lifetime in milliseconds, or - * {@code null} for the runtime default - * @return this config instance for method chaining - */ - public ConnectorMcpServerConfig setAuthorizationCacheTtlMs(Long authorizationCacheTtlMs) { - this.authorizationCacheTtlMs = authorizationCacheTtlMs; - return this; - } - - ManagedMcpServerConfig toWire() { - return new ManagedMcpServerConfig(displayName, url, tools, timeout, authorizationCacheTtlMs); - } - - static ConnectorMcpServerConfig fromWire(ManagedMcpServerConfig wire) { - return new ConnectorMcpServerConfig().setDisplayName(wire.displayName()).setUrl(wire.url()) - .setTools(wire.tools()).setTimeout(wire.timeout()) - .setAuthorizationCacheTtlMs(wire.headersRefreshTtlMs()); - } - - static Map toWire(Map servers) { - if (servers == null) { - return null; - } - var wire = new LinkedHashMap(); - servers.forEach((name, server) -> wire.put(name, server == null ? null : server.toWire())); - return wire; - } - - static Map fromWire(Map servers) { - if (servers == null) { - return null; - } - var configs = new LinkedHashMap(); - servers.forEach((name, server) -> configs.put(name, server == null ? null : fromWire(server))); - return configs; - } -} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index f7c3e14072..b7fc219258 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -8,12 +8,10 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; -import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -118,9 +116,6 @@ public final class CreateSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; - @JsonProperty("managedMcpServers") - private Map managedMcpServers; - @JsonProperty("mcpOAuthTokenStorage") private String mcpOAuthTokenStorage; @@ -599,30 +594,6 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } - /** - * Gets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog. - * - * @return the Connector MCP servers map - */ - @JsonIgnore - public Map getConnectorMcpServers() { - var servers = ConnectorMcpServerConfig.fromWire(managedMcpServers); - return servers == null ? null : Collections.unmodifiableMap(servers); - } - - /** - * Sets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog. - * - * @param connectorMcpServers - * the Connector MCP servers map - */ - @JsonIgnore - public void setConnectorMcpServers(Map connectorMcpServers) { - this.managedMcpServers = ConnectorMcpServerConfig.toWire(connectorMcpServers); - } - /** Gets MCP OAuth token storage mode. @return the storage mode */ public String getMcpOAuthTokenStorage() { return mcpOAuthTokenStorage; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java deleted file mode 100644 index 9ac63400b4..0000000000 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.rpc; - -import java.util.concurrent.CompletableFuture; - -/** - * Supplies short-lived client-to-Copilot-Connectors service authorization - * headers for a connected Connector MCP endpoint. - *

- * The authorization is normally the selected account's GitHub bearer token for - * the exact service-advertised endpoint. This handler does not supply - * downstream provider credentials; the Connector service owns those provider - * tokens. - * - * @since 1.0.0 - */ -@FunctionalInterface -public interface McpHeadersRefreshHandler { - /** - * Handles a service-authorization header refresh request. - * - * @param request - * the connected Copilot Connector MCP endpoint details - * @param invocation - * the invocation context with session information - * @return a future resolving to headers or an explicit no-result response - */ - CompletableFuture handle(McpHeadersRefreshRequest request, - McpHeadersRefreshInvocation invocation); -} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java deleted file mode 100644 index e7c53c86fd..0000000000 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshInvocation.java +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.rpc; - -/** - * Context for a Copilot Connector MCP dynamic-header refresh invocation. - * - * @since 1.0.0 - */ -public class McpHeadersRefreshInvocation { - - private String sessionId; - - /** - * Gets the session ID. - * - * @return the session ID - */ - public String getSessionId() { - return sessionId; - } - - /** - * Sets the session ID. - * - * @param sessionId - * the session ID - * @return this instance for method chaining - */ - public McpHeadersRefreshInvocation setSessionId(String sessionId) { - this.sessionId = sessionId; - return this; - } -} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java deleted file mode 100644 index 5c5c8a7f3a..0000000000 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.rpc; - -import com.github.copilot.generated.McpHeadersRefreshRequiredReason; - -/** - * Request to refresh short-lived client-to-Copilot-Connectors service - * authorization headers for a connected Connector MCP endpoint. - * - * @param serverKey - * stable server key used in the Connector MCP server map - * @param serverUrl - * exact service-advertised Connector MCP endpoint URL - * @param reason - * reason the headers must be refreshed - * @since 1.0.0 - */ -public record McpHeadersRefreshRequest(String serverKey, String serverUrl, McpHeadersRefreshRequiredReason reason) { -} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java b/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java deleted file mode 100644 index fed2f83a06..0000000000 --- a/java/sdk/src/main/java/com/github/copilot/rpc/McpHeadersRefreshResult.java +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.rpc; - -import java.util.Map; - -/** - * Short-lived client-to-Copilot-Connectors service authorization headers - * returned for a connected Connector MCP endpoint. - * - * @param headers - * headers that authorize the client-to-service request, or - * {@code null} for no result - * @param ttlMs - * optional remaining authorization lifetime in milliseconds - * @since 1.0.0 - */ -public record McpHeadersRefreshResult(Map headers, Long ttlMs) { - - /** - * Creates a defensive copy of the supplied headers. - */ - public McpHeadersRefreshResult { - headers = headers == null ? null : Map.copyOf(headers); - } - - /** - * Creates a result with headers and no handler-specific cache lifetime. - * - * @param headers - * headers that authorize the client-to-service request - * @return a headers result - */ - public static McpHeadersRefreshResult withHeaders(Map headers) { - return new McpHeadersRefreshResult(headers, null); - } - - /** - * Creates a result with headers and the remaining authorization lifetime. - * - * @param headers - * headers that authorize the client-to-service request - * @param ttlMs - * remaining authorization lifetime in milliseconds - * @return a headers result - */ - public static McpHeadersRefreshResult withHeaders(Map headers, long ttlMs) { - return new McpHeadersRefreshResult(headers, ttlMs); - } - - /** - * Creates an explicit no-result response. - * - * @return a no-result response - */ - public static McpHeadersRefreshResult none() { - return new McpHeadersRefreshResult(null, null); - } -} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 01cd3c5f41..ec31562323 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -67,7 +67,6 @@ public class ResumeSessionConfig { private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; - private McpHeadersRefreshHandler onMcpHeadersRefresh; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -86,7 +85,6 @@ public class ResumeSessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; - private Map connectorMcpServers; private String mcpOAuthTokenStorage; private String authClientIdMetadataUrl; private List customAgents; @@ -867,38 +865,6 @@ public ResumeSessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) return this; } - /** - * Gets the dynamic-header refresh handler for connected Copilot Connector MCP - * endpoints. - *

- * The handler supplies short-lived client-to-Copilot-Connectors service - * authorization, not downstream provider credentials. The Connector service - * owns downstream provider tokens. - * - * @return the handler, or {@code null} if not set - */ - @JsonIgnore - public McpHeadersRefreshHandler getOnMcpHeadersRefresh() { - return onMcpHeadersRefresh; - } - - /** - * Sets the dynamic-header refresh handler for connected Copilot Connector MCP - * endpoints. - *

- * The handler supplies short-lived client-to-Copilot-Connectors service - * authorization, not downstream provider credentials. The Connector service - * owns downstream provider tokens. - * - * @param onMcpHeadersRefresh - * the handler - * @return this config instance for method chaining - */ - public ResumeSessionConfig setOnMcpHeadersRefresh(McpHeadersRefreshHandler onMcpHeadersRefresh) { - this.onMcpHeadersRefresh = onMcpHeadersRefresh; - return this; - } - /** * Gets the user input request handler. * @@ -1431,33 +1397,6 @@ public ResumeSessionConfig setMcpServers(Map mcpServers return this; } - /** - * Gets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog. - * - * @return the Connector MCP servers map - */ - @CopilotExperimental - public Map getConnectorMcpServers() { - return connectorMcpServers == null ? null : Collections.unmodifiableMap(connectorMcpServers); - } - - /** - * Sets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog for a cold resume. - * - * @param connectorMcpServers - * non-secret Connector endpoint configurations keyed by stable - * server key; each key is reported as - * {@link McpHeadersRefreshRequest#serverKey()} - * @return this config instance for method chaining - */ - @CopilotExperimental - public ResumeSessionConfig setConnectorMcpServers(Map connectorMcpServers) { - this.connectorMcpServers = connectorMcpServers; - return this; - } - /** * Gets the MCP OAuth token storage mode. * @@ -2204,9 +2143,6 @@ public ResumeSessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; - copy.connectorMcpServers = this.connectorMcpServers != null - ? new java.util.HashMap<>(this.connectorMcpServers) - : null; copy.mcpOAuthTokenStorage = this.mcpOAuthTokenStorage; copy.authClientIdMetadataUrl = this.authClientIdMetadataUrl; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; @@ -2230,7 +2166,6 @@ public ResumeSessionConfig clone() { copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; copy.onMcpAuthRequest = this.onMcpAuthRequest; - copy.onMcpHeadersRefresh = this.onMcpHeadersRefresh; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index af0b9bae70..42d0ee536d 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -8,12 +8,10 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; -import com.github.copilot.generated.rpc.ManagedMcpServerConfig; import com.github.copilot.generated.rpc.SessionLimitsConfig; /** @@ -161,9 +159,6 @@ public final class ResumeSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; - @JsonProperty("managedMcpServers") - private Map managedMcpServers; - @JsonProperty("mcpOAuthTokenStorage") private String mcpOAuthTokenStorage; @@ -825,30 +820,6 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } - /** - * Gets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog. - * - * @return the Connector MCP servers map - */ - @JsonIgnore - public Map getConnectorMcpServers() { - var servers = ConnectorMcpServerConfig.fromWire(managedMcpServers); - return servers == null ? null : Collections.unmodifiableMap(servers); - } - - /** - * Sets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog. - * - * @param connectorMcpServers - * the Connector MCP servers map - */ - @JsonIgnore - public void setConnectorMcpServers(Map connectorMcpServers) { - this.managedMcpServers = ConnectorMcpServerConfig.toWire(connectorMcpServers); - } - /** Gets MCP OAuth token storage mode. @return the storage mode */ public String getMcpOAuthTokenStorage() { return mcpOAuthTokenStorage; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index b3bf863516..d77642c103 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -67,7 +67,6 @@ public class SessionConfig { private Boolean manageScheduleEnabled; private PermissionHandler onPermissionRequest; private McpAuthHandler onMcpAuthRequest; - private McpHeadersRefreshHandler onMcpHeadersRefresh; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -75,7 +74,6 @@ public class SessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; - private Map connectorMcpServers; private String mcpOAuthTokenStorage; private String authClientIdMetadataUrl; private List customAgents; @@ -911,38 +909,6 @@ public SessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) { return this; } - /** - * Gets the dynamic-header refresh handler for connected Copilot Connector MCP - * endpoints. - *

- * The handler supplies short-lived client-to-Copilot-Connectors service - * authorization, not downstream provider credentials. The Connector service - * owns downstream provider tokens. - * - * @return the handler, or {@code null} if not set - */ - @JsonIgnore - public McpHeadersRefreshHandler getOnMcpHeadersRefresh() { - return onMcpHeadersRefresh; - } - - /** - * Sets the dynamic-header refresh handler for connected Copilot Connector MCP - * endpoints. - *

- * The handler supplies short-lived client-to-Copilot-Connectors service - * authorization, not downstream provider credentials. The Connector service - * owns downstream provider tokens. - * - * @param onMcpHeadersRefresh - * the handler - * @return this config instance for method chaining - */ - public SessionConfig setOnMcpHeadersRefresh(McpHeadersRefreshHandler onMcpHeadersRefresh) { - this.onMcpHeadersRefresh = onMcpHeadersRefresh; - return this; - } - /** * Gets the user input request handler. * @@ -1084,33 +1050,6 @@ public SessionConfig setMcpServers(Map mcpServers) { return this; } - /** - * Gets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog. - * - * @return the Connector MCP servers map - */ - @CopilotExperimental - public Map getConnectorMcpServers() { - return connectorMcpServers == null ? null : Collections.unmodifiableMap(connectorMcpServers); - } - - /** - * Sets connected Copilot Connector MCP endpoint configurations supplied from - * the service catalog. - * - * @param connectorMcpServers - * non-secret Connector endpoint configurations keyed by stable - * server key; each key is reported as - * {@link McpHeadersRefreshRequest#serverKey()} - * @return this config instance for method chaining - */ - @CopilotExperimental - public SessionConfig setConnectorMcpServers(Map connectorMcpServers) { - this.connectorMcpServers = connectorMcpServers; - return this; - } - /** * Gets the MCP OAuth token storage mode. * @@ -2333,9 +2272,6 @@ public SessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; - copy.connectorMcpServers = this.connectorMcpServers != null - ? new java.util.HashMap<>(this.connectorMcpServers) - : null; copy.mcpOAuthTokenStorage = this.mcpOAuthTokenStorage; copy.authClientIdMetadataUrl = this.authClientIdMetadataUrl; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; @@ -2370,7 +2306,6 @@ public SessionConfig clone() { copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; copy.onMcpAuthRequest = this.onMcpAuthRequest; - copy.onMcpHeadersRefresh = this.onMcpHeadersRefresh; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java index 802fc392b4..b7005c5d52 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java @@ -12,12 +12,6 @@ import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import javax.tools.ToolProvider; -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; -import javax.lang.model.util.ElementFilter; import java.net.URI; import java.net.URL; import java.nio.file.Path; @@ -25,12 +19,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -98,42 +88,6 @@ public ExperimentalType getIt(ExperimentalType value) { } """; - private static final String CONNECTOR_MCP_PUBLIC_PRIMITIVES_CONSUMER = """ - package consumer; - - import java.util.Map; - - import com.github.copilot.CopilotSession; - import com.github.copilot.generated.SessionMcpServerNeedsReconnectEvent; - import com.github.copilot.generated.SessionMcpServerRemovedEvent; - import com.github.copilot.generated.SessionMcpServerStatusChangedEvent; - import com.github.copilot.generated.SessionMcpServersLoadedEvent; - import com.github.copilot.generated.rpc.SessionMcpDisableParams; - import com.github.copilot.generated.rpc.SessionMcpEnableParams; - import com.github.copilot.generated.rpc.SessionMcpListToolsParams; - import com.github.copilot.rpc.ConnectorMcpServerConfig; - import com.github.copilot.rpc.ResumeSessionConfig; - import com.github.copilot.rpc.SessionConfig; - - public class Consumer { - public void compose(CopilotSession session, ConnectorMcpServerConfig server) { - String serverKey = "calendar"; - server.setAuthorizationCacheTtlMs(60_000L); - var servers = Map.of(serverKey, server); - new SessionConfig().setConnectorMcpServers(servers); - new ResumeSessionConfig().setConnectorMcpServers(servers); - session.getRpc().mcp.list(); - session.getRpc().mcp.enable(new SessionMcpEnableParams(null, serverKey)); - session.getRpc().mcp.disable(new SessionMcpDisableParams(null, serverKey)); - session.getRpc().mcp.listTools(new SessionMcpListToolsParams(null, serverKey)); - session.on(SessionMcpServersLoadedEvent.class, event -> {}); - session.on(SessionMcpServerStatusChangedEvent.class, event -> {}); - session.on(SessionMcpServerRemovedEvent.class, event -> {}); - session.on(SessionMcpServerNeedsReconnectEvent.class, event -> {}); - } - } - """; - @Test void failsByDefault_whenFieldOrSignatureUsesExperimentalType() { DiagnosticCollector diagnostics = compile( @@ -194,79 +148,6 @@ void passes_whenOptInFlagIsProvided() { assertFalse(hasError, "Expected no errors with opt-in flag, got: " + diagnostics.getDiagnostics()); } - @Test - void connectorMcpServerApisArePublicAndExperimentalForCreateAndResume() { - var diagnostics = new DiagnosticCollector(); - var checkedMethods = new AtomicInteger(); - var task = ToolProvider.getSystemJavaCompiler().getTask(null, null, diagnostics, - List.of("-classpath", resolveClasspath(), "-proc:only"), null, - List.of(inMemorySource("consumer.Consumer", "package consumer; public class Consumer {}"))); - task.setProcessors(List.of(new AbstractProcessor() { - @Override - public Set getSupportedAnnotationTypes() { - return Set.of("*"); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latestSupported(); - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - if (!roundEnv.processingOver()) { - var connectorConfig = processingEnv.getElementUtils() - .getTypeElement("com.github.copilot.rpc.ConnectorMcpServerConfig"); - assertNotNull(connectorConfig); - assertTrue(connectorConfig.getModifiers().contains(Modifier.PUBLIC)); - var connectorMethodNames = ElementFilter.methodsIn(connectorConfig.getEnclosedElements()).stream() - .map(method -> method.getSimpleName().toString()) - .collect(java.util.stream.Collectors.toSet()); - assertTrue(connectorMethodNames.contains("getAuthorizationCacheTtlMs")); - assertTrue(connectorMethodNames.contains("setAuthorizationCacheTtlMs")); - assertFalse(connectorMethodNames.contains("getHeadersRefreshTtlMs")); - assertFalse(connectorMethodNames.contains("setHeadersRefreshTtlMs")); - - for (String configName : List.of("SessionConfig", "ResumeSessionConfig")) { - var config = processingEnv.getElementUtils() - .getTypeElement("com.github.copilot.rpc." + configName); - assertNotNull(config); - var methodNames = ElementFilter.methodsIn(config.getEnclosedElements()).stream() - .map(method -> method.getSimpleName().toString()) - .collect(java.util.stream.Collectors.toSet()); - assertFalse(methodNames.contains("getManagedMcpServers")); - assertFalse(methodNames.contains("setManagedMcpServers")); - assertFalse(methodNames.contains("getCatalogMcpServers")); - assertFalse(methodNames.contains("setCatalogMcpServers")); - for (var method : ElementFilter.methodsIn(config.getEnclosedElements())) { - if (Set.of("getConnectorMcpServers", "setConnectorMcpServers") - .contains(method.getSimpleName().toString())) { - assertNotNull(method.getAnnotation(CopilotExperimental.class), - configName + "." + method.getSimpleName()); - checkedMethods.incrementAndGet(); - } - } - } - - } - return false; - } - })); - - assertTrue(task.call(), () -> "Compiler diagnostics: " + diagnostics.getDiagnostics()); - assertEquals(4, checkedMethods.get()); - } - - @Test - void connectorMcpConfigurationComposesWithPublicSessionRpcAndStatusEvents() { - var diagnostics = compile( - List.of(inMemorySource("consumer.Consumer", CONNECTOR_MCP_PUBLIC_PRIMITIVES_CONSUMER)), - List.of("-Acopilot.experimental.allowed=true")); - - boolean hasError = diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR); - assertFalse(hasError, "Expected public MCP primitives to compile, got: " + diagnostics.getDiagnostics()); - } - private DiagnosticCollector compile(List sources, List extraOptions) { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); diff --git a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java index 1dabffb8f7..b83cedc70a 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java @@ -23,7 +23,6 @@ import com.github.copilot.rpc.CloudSessionRepository; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.McpAuthResult; -import com.github.copilot.rpc.McpHeadersRefreshResult; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.SessionConfig; @@ -177,43 +176,6 @@ void resumeSessionRegistersMcpAuthInterestOnlyWhenHandlerConfigured() throws Exc } } - @Test - void createAndResumeRegisterMcpHeadersRefreshInterest() throws Exception { - try (var server = new RecordingRuntime(); - var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setOnMcpHeadersRefresh((request, invocation) -> java.util.concurrent.CompletableFuture - .completedFuture(McpHeadersRefreshResult.none()))) - .get()) { - assertNotNull(session); - } - - List createRequests = server.requests().stream() - .filter(request -> "session.create".equals(request.method()) - || "session.eventLog.registerInterest".equals(request.method())) - .toList(); - assertEquals("session.create", createRequests.get(0).method()); - assertEquals("session.eventLog.registerInterest", createRequests.get(1).method()); - assertEquals("mcp.headers_refresh_required", createRequests.get(1).params().path("eventType").asText()); - - server.clearRequests(); - - try (var session = client.resumeSession("connector-session", - new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setOnMcpHeadersRefresh((request, invocation) -> java.util.concurrent.CompletableFuture - .completedFuture(McpHeadersRefreshResult.none()))) - .get()) { - assertNotNull(session); - } - - List resumeRequests = server.requests(); - assertEquals("session.resume", resumeRequests.get(0).method()); - assertEquals("session.eventLog.registerInterest", resumeRequests.get(1).method()); - assertEquals("mcp.headers_refresh_required", resumeRequests.get(1).params().path("eventType").asText()); - } - } - private static void assertNoMcpAuthInterest(List requests) { assertFalse(requests.stream().anyMatch(request -> "session.eventLog.registerInterest".equals(request.method()) && "mcp.oauth_required".equals(request.params().path("eventType").asText()))); diff --git a/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java b/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java deleted file mode 100644 index b10df77914..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/McpHeadersRefreshHandlerTest.java +++ /dev/null @@ -1,172 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import com.fasterxml.jackson.databind.JsonNode; -import com.github.copilot.generated.McpHeadersRefreshRequiredEvent; -import com.github.copilot.generated.McpHeadersRefreshRequiredReason; -import com.github.copilot.generated.rpc.SessionMcpHeadersHandlePendingHeadersRefreshRequestResult; -import com.github.copilot.rpc.McpHeadersRefreshHandler; -import com.github.copilot.rpc.McpHeadersRefreshResult; -import com.github.copilot.rpc.ResumeSessionConfig; -import com.github.copilot.rpc.SessionConfig; - -class McpHeadersRefreshHandlerTest { - - private static final String METHOD = "session.mcp.headers.handlePendingHeadersRefreshRequest"; - - @Test - void createConfigPreservesRefreshHandlerWithoutSerializingIt() { - var config = new SessionConfig(); - assertNull(config.getOnMcpHeadersRefresh()); - McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture - .completedFuture(McpHeadersRefreshResult.none()); - - assertSame(config, config.setOnMcpHeadersRefresh(handler)); - assertSame(handler, config.getOnMcpHeadersRefresh()); - assertSame(handler, config.clone().getOnMcpHeadersRefresh()); - assertFalse(JsonRpcClient.getObjectMapper().valueToTree(config).has("onMcpHeadersRefresh")); - - assertSame(config, config.setOnMcpHeadersRefresh(null)); - assertNull(config.getOnMcpHeadersRefresh()); - } - - @Test - void resumeConfigPreservesRefreshHandlerWithoutSerializingIt() { - var config = new ResumeSessionConfig(); - assertNull(config.getOnMcpHeadersRefresh()); - McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture - .completedFuture(McpHeadersRefreshResult.none()); - - assertSame(config, config.setOnMcpHeadersRefresh(handler)); - assertSame(handler, config.getOnMcpHeadersRefresh()); - assertSame(handler, config.clone().getOnMcpHeadersRefresh()); - assertFalse(JsonRpcClient.getObjectMapper().valueToTree(config).has("onMcpHeadersRefresh")); - - assertSame(config, config.setOnMcpHeadersRefresh(null)); - assertNull(config.getOnMcpHeadersRefresh()); - } - - @Test - void createHandlerDispatchesHeadersWithoutTtl() { - McpHeadersRefreshHandler handler = (request, invocation) -> { - assertEquals("connector-server-key", request.serverKey()); - assertEquals("https://mcp.example.com", request.serverUrl()); - assertEquals(McpHeadersRefreshRequiredReason.STARTUP, request.reason()); - assertEquals("session-1", invocation.getSessionId()); - return CompletableFuture - .completedFuture(McpHeadersRefreshResult.withHeaders(Map.of("Authorization", "Bearer dynamic"))); - }; - - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); - - assertEquals("headers", params.path("result").path("kind").asText()); - assertEquals("Bearer dynamic", params.path("result").path("headers").path("Authorization").asText()); - assertFalse(params.path("result").has("ttlMs")); - } - - @Test - void resumeHandlerDispatchesHeadersWithTtl() { - McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture - .completedFuture(McpHeadersRefreshResult.withHeaders(Map.of("X-Dynamic", "value"), 60_000L)); - - JsonNode params = dispatch(new ResumeSessionConfig().setOnMcpHeadersRefresh(handler)); - - assertEquals("headers", params.path("result").path("kind").asText()); - assertEquals("value", params.path("result").path("headers").path("X-Dynamic").asText()); - assertEquals(60_000L, params.path("result").path("ttlMs").asLong()); - } - - @Test - void handlerCanReturnExplicitNone() { - McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture - .completedFuture(McpHeadersRefreshResult.none()); - - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); - - assertEquals("none", params.path("result").path("kind").asText()); - assertEquals(1, params.path("result").size()); - } - - @Test - void handlerFailureReturnsExplicitErrorMessage() { - McpHeadersRefreshHandler handler = (request, invocation) -> CompletableFuture - .failedFuture(new IllegalStateException("header provider unavailable")); - - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); - - assertEquals("error", params.path("result").path("kind").asText()); - assertEquals("header provider unavailable", params.path("result").path("message").asText()); - } - - @Test - void handlerExceptionReturnsExplicitErrorMessage() { - McpHeadersRefreshHandler handler = (request, invocation) -> { - throw new IllegalArgumentException("invalid header configuration"); - }; - - JsonNode params = dispatch(new SessionConfig().setOnMcpHeadersRefresh(handler)); - - assertEquals("error", params.path("result").path("kind").asText()); - assertEquals("invalid header configuration", params.path("result").path("message").asText()); - } - - private JsonNode dispatch(SessionConfig config) { - var rpc = rpc(); - var session = new CopilotSession("session-1", rpc); - session.setExecutor(Runnable::run); - SessionRequestBuilder.configureSession(session, config); - return dispatchAndCapture(session, rpc); - } - - private JsonNode dispatch(ResumeSessionConfig config) { - var rpc = rpc(); - var session = new CopilotSession("session-1", rpc); - session.setExecutor(Runnable::run); - SessionRequestBuilder.configureSession(session, config); - return dispatchAndCapture(session, rpc); - } - - private JsonRpcClient rpc() { - var rpc = mock(JsonRpcClient.class); - when(rpc.invoke(eq(METHOD), any(), eq(SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class))) - .thenReturn(CompletableFuture - .completedFuture(new SessionMcpHeadersHandlePendingHeadersRefreshRequestResult(true))); - return rpc; - } - - private JsonNode dispatchAndCapture(CopilotSession session, JsonRpcClient rpc) { - var event = new McpHeadersRefreshRequiredEvent(); - event.setData(new McpHeadersRefreshRequiredEvent.McpHeadersRefreshRequiredEventData("headers-request", - "connector-server-key", "https://mcp.example.com", McpHeadersRefreshRequiredReason.STARTUP)); - - session.dispatchEvent(event); - - var paramsCaptor = ArgumentCaptor.forClass(Object.class); - verify(rpc).invoke(eq(METHOD), paramsCaptor.capture(), - eq(SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class)); - JsonNode params = JsonRpcClient.getObjectMapper().valueToTree(paramsCaptor.getValue()); - assertEquals("session-1", params.path("sessionId").asText()); - assertEquals("headers-request", params.path("requestId").asText()); - assertNull(params.get("unexpected")); - return params; - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java index f19cb77962..c8fefcec05 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java @@ -20,6 +20,19 @@ import com.github.copilot.generated.rpc.RpcCaller; import com.github.copilot.generated.rpc.ServerRpc; import com.github.copilot.generated.rpc.SessionAgentSelectParams; +import com.github.copilot.generated.rpc.SessionConnectorsConnectParams; +import com.github.copilot.generated.rpc.SessionConnectorsContinueConnectionParams; +import com.github.copilot.generated.rpc.SessionConnectorsDisconnectParams; +import com.github.copilot.generated.rpc.SessionConnectorsDisconnectResult; +import com.github.copilot.generated.rpc.SessionConnectorsGetCapabilitiesResult; +import com.github.copilot.generated.rpc.SessionConnectorsGetStatusResult; +import com.github.copilot.generated.rpc.SessionConnectorsListParams; +import com.github.copilot.generated.rpc.SessionConnectorsListResult; +import com.github.copilot.generated.rpc.SessionConnectorsReconcileParams; +import com.github.copilot.generated.rpc.SessionConnectorsReconcileResult; +import com.github.copilot.generated.rpc.SessionConnectorsReconnectParams; +import com.github.copilot.generated.rpc.SessionConnectorsRefreshParams; +import com.github.copilot.generated.rpc.SessionConnectorsRefreshResult; import com.github.copilot.generated.rpc.SessionModelSwitchToParams; import com.github.copilot.generated.rpc.SessionRpc; @@ -43,7 +56,7 @@ class RpcWrappersTest { */ private static final class StubCaller implements RpcCaller { - static record Call(String method, Object params) { + static record Call(String method, Object params, Class resultType) { } final List calls = new ArrayList<>(); @@ -52,7 +65,7 @@ static record Call(String method, Object params) { @Override @SuppressWarnings("unchecked") public CompletableFuture invoke(String method, Object params, Class resultType) { - calls.add(new Call(method, params)); + calls.add(new Call(method, params, resultType)); return CompletableFuture.completedFuture((T) nextResult); } } @@ -181,6 +194,61 @@ void sessionRpc_instantiates_with_all_namespace_fields() { assertNotNull(session.shell); assertNotNull(session.history); assertNotNull(session.usage); + assertNotNull(session.connectors); + } + + @Test + @AllowCopilotExperimental + void sessionRpc_connectors_exposes_all_generated_methods() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-connectors"); + + session.connectors.getCapabilities(); + session.connectors.getStatus(); + session.connectors.list(new SessionConnectorsListParams("ignored-session", "account-1")); + session.connectors.refresh(new SessionConnectorsRefreshParams("ignored-session", "account-1")); + session.connectors.connect(new SessionConnectorsConnectParams("ignored-session", "account-1", "outlook")); + session.connectors.reconnect(new SessionConnectorsReconnectParams("ignored-session", "account-1", "outlook")); + session.connectors.continueConnection(new SessionConnectorsContinueConnectionParams("ignored-session", + "continuation-1", 3L, 1_000L, 30_000L)); + session.connectors.disconnect(new SessionConnectorsDisconnectParams("ignored-session", "account-1", "outlook")); + session.connectors.reconcile(new SessionConnectorsReconcileParams("ignored-session", "account-1", true)); + + assertEquals(9, stub.calls.size()); + assertConnectorCall(stub.calls.get(0), "session.connectors.getCapabilities", + SessionConnectorsGetCapabilitiesResult.class); + assertConnectorCall(stub.calls.get(1), "session.connectors.getStatus", SessionConnectorsGetStatusResult.class); + + var listParams = assertConnectorCall(stub.calls.get(2), "session.connectors.list", + SessionConnectorsListResult.class); + assertEquals("account-1", listParams.get("accountId").asText()); + + var refreshParams = assertConnectorCall(stub.calls.get(3), "session.connectors.refresh", + SessionConnectorsRefreshResult.class); + assertEquals("account-1", refreshParams.get("accountId").asText()); + + var connectParams = assertConnectorCall(stub.calls.get(4), "session.connectors.connect", + com.github.copilot.generated.rpc.ConnectorConnectResult.class); + assertEquals("outlook", connectParams.get("connectorName").asText()); + + var reconnectParams = assertConnectorCall(stub.calls.get(5), "session.connectors.reconnect", + com.github.copilot.generated.rpc.ConnectorConnectResult.class); + assertEquals("outlook", reconnectParams.get("connectorName").asText()); + + var continueParams = assertConnectorCall(stub.calls.get(6), "session.connectors.continueConnection", + com.github.copilot.generated.rpc.ConnectorConnectResult.class); + assertEquals("continuation-1", continueParams.get("continuationId").asText()); + assertEquals(3L, continueParams.get("maxAttempts").asLong()); + assertEquals(1_000L, continueParams.get("pollIntervalMs").asLong()); + assertEquals(30_000L, continueParams.get("deadlineMs").asLong()); + + var disconnectParams = assertConnectorCall(stub.calls.get(7), "session.connectors.disconnect", + SessionConnectorsDisconnectResult.class); + assertEquals("outlook", disconnectParams.get("connectorName").asText()); + + var reconcileParams = assertConnectorCall(stub.calls.get(8), "session.connectors.reconcile", + SessionConnectorsReconcileResult.class); + assertTrue(reconcileParams.get("refreshCatalog").asBoolean()); } @Test @@ -362,6 +430,7 @@ void copilotSession_getRpc_all_namespace_fields_present() throws Exception { assertNotNull(sessionRpc.permissions); assertNotNull(sessionRpc.commands); assertNotNull(sessionRpc.ui); + assertNotNull(sessionRpc.connectors); } } @@ -462,6 +531,15 @@ void sessionRpc_mcp_apps_callTool_returns_jsonNode_payload() throws Exception { assertEquals(false, result.get("isError").asBoolean()); } + private static com.fasterxml.jackson.databind.JsonNode assertConnectorCall(StubCaller.Call call, + String expectedMethod, Class expectedResultType) { + assertEquals(expectedMethod, call.method()); + assertEquals(expectedResultType, call.resultType()); + var params = new ObjectMapper().valueToTree(call.params()); + assertEquals("sess-connectors", params.get("sessionId").asText()); + return params; + } + /** * Helper that creates a loopback socket pair. The client side is used by * {@link JsonRpcClient}; the server side can be read to inspect outbound diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 1e1461ea8b..edc40175d0 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -15,7 +15,6 @@ import com.github.copilot.generated.rpc.SessionLimitsConfig; import com.github.copilot.rpc.AskUserVariant; import com.github.copilot.rpc.AutoModeSwitchResponse; -import com.github.copilot.rpc.ConnectorMcpServerConfig; import com.github.copilot.rpc.CloudSessionOptions; import com.github.copilot.rpc.CloudSessionRepository; import com.github.copilot.rpc.CopilotClientMode; @@ -1154,43 +1153,4 @@ void githubMcpToolConfigIsMappedAndSerializedForCreateAndResume() throws Excepti mapper.writeValueAsString(SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "session-2")) .contains("\"githubMcpToolConfig\"")); } - - @Test - void connectorMcpServersUsePublicFacadeAndExactInternalWireName() throws Exception { - var connectorServer = new ConnectorMcpServerConfig().setDisplayName("Connector server") - .setUrl("https://mcp.example.com").setTools(List.of("search")).setTimeout(15_000L) - .setAuthorizationCacheTtlMs(300_000L); - var connectorServers = Map.of("connector-server", connectorServer); - - var createRequest = SessionRequestBuilder - .buildCreateRequest(new SessionConfig().setConnectorMcpServers(connectorServers), "session-1"); - var resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", - new ResumeSessionConfig().setConnectorMcpServers(connectorServers)); - - assertEquals("Connector server", - createRequest.getConnectorMcpServers().get("connector-server").getDisplayName()); - assertEquals(300_000L, - createRequest.getConnectorMcpServers().get("connector-server").getAuthorizationCacheTtlMs()); - assertEquals("Connector server", - resumeRequest.getConnectorMcpServers().get("connector-server").getDisplayName()); - - var mapper = JsonRpcClient.getObjectMapper(); - var createJson = mapper.readTree(mapper.writeValueAsBytes(createRequest)); - var resumeJson = mapper.readTree(mapper.writeValueAsBytes(resumeRequest)); - var expectedWire = mapper.readTree(""" - { - "connector-server": { - "displayName": "Connector server", - "url": "https://mcp.example.com", - "tools": ["search"], - "timeout": 15000, - "headersRefreshTtlMs": 300000 - } - } - """); - assertEquals(expectedWire, createJson.path("managedMcpServers")); - assertEquals(expectedWire, resumeJson.path("managedMcpServers")); - assertFalse(createJson.has("connectorMcpServers")); - assertFalse(resumeJson.has("connectorMcpServers")); - } } diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java new file mode 100644 index 0000000000..3360bc04d3 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +class ConnectorSerializationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void connectorConnectResult_roundTrips_all_variants() throws Exception { + var connected = roundTrip(""" + { + "kind": "connected", + "status": { + "apiVersion": 1, + "availability": "enabled", + "accountId": "account-1", + "catalog": { + "revision": 7, + "refreshedAtMs": 1000, + "connectors": [{ + "name": "outlook", + "displayName": "Outlook", + "description": "Mail and calendar", + "status": "connected", + "runtimeServerIds": ["connector-outlook"] + }] + }, + "runtimeServers": [{ + "runtimeServerId": "connector-outlook", + "connectorName": "outlook", + "status": "connected" + }], + "pendingConnections": 0 + } + } + """, ConnectorConnectResultConnected.class); + assertEquals(ConnectorAvailability.ENABLED, connected.getStatus().availability()); + assertEquals(ConnectorCatalogStatus.CONNECTED, connected.getStatus().catalog().connectors().get(0).status()); + assertEquals(ConnectorMcpStatus.CONNECTED, connected.getStatus().runtimeServers().get(0).status()); + + var consentRequired = roundTrip(""" + { + "kind": "consent_required", + "consentUrl": "https://example.com/consent", + "continuationId": "continuation-1" + } + """, ConnectorConnectResultConsentRequired.class); + assertEquals("https://example.com/consent", consentRequired.getConsentUrl()); + assertEquals("continuation-1", consentRequired.getContinuationId()); + + var pending = roundTrip(""" + { + "kind": "pending", + "continuationId": "continuation-2" + } + """, ConnectorConnectResultPending.class); + assertEquals("continuation-2", pending.getContinuationId()); + } + + @Test + void connectorGeneratedRecords_preserve_wire_names_and_null_omission() throws Exception { + var params = new SessionConnectorsContinueConnectionParams(null, "continuation-1", 4L, 500L, 10_000L); + + var json = MAPPER.readTree(MAPPER.writeValueAsString(params)); + + assertFalse(json.has("sessionId")); + assertEquals("continuation-1", json.get("continuationId").asText()); + assertEquals(4L, json.get("maxAttempts").asLong()); + assertEquals(500L, json.get("pollIntervalMs").asLong()); + assertEquals(10_000L, json.get("deadlineMs").asLong()); + assertEquals(params, MAPPER.treeToValue(json, SessionConnectorsContinueConnectionParams.class)); + } + + private static T roundTrip(String json, Class expectedType) throws Exception { + var result = MAPPER.readValue(json, ConnectorConnectResult.class); + var typedResult = assertInstanceOf(expectedType, result); + assertEquals(typedResult.getKind(), + MAPPER.readTree(MAPPER.writeValueAsString(typedResult)).get("kind").asText()); + + var serialized = MAPPER.writeValueAsString(typedResult); + return assertInstanceOf(expectedType, MAPPER.readValue(serialized, ConnectorConnectResult.class)); + } +} diff --git a/nodejs/README.md b/nodejs/README.md index fcada81efb..0fe5081bdf 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -160,8 +160,6 @@ Create a new conversation session. - `enableSessionStore?: boolean` - Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled. - `gitHubTokenProvider?: GitHubTokenProvider` - Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `gitHubToken`. - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. -- `connectorMcpServers?: Record` - Connected Copilot Connector MCP endpoints advertised by the Connector service. Re-supply on cold resume. -- `onMcpHeadersRefresh?: McpHeadersRefreshHandler` - Supplies short-lived headers for Connector MCP endpoints. Register this even when the initial connected set is empty if the host supports connecting a Connector during the session. - `onPermissionRequest?: PermissionHandler` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `approveAll` approves requests when managed settings are disabled and throws when `enableManagedSettings` is true. Custom handlers can inspect `managedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section. - `onUserInputRequest?: UserInputHandler` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `askUserVariant?: "legacy" | "elicitation"` - Selects the model-facing `ask_user` tool shape when creating or cold-resuming a session. Defaults to `"legacy"`; use `"elicitation"` with `onElicitationRequest`. @@ -375,17 +373,6 @@ Abort the currently processing message in this session. Get all events/messages from this session. -Use `session.rpc.mcp.list()`, `enable()`, `disable()`, and `listTools()` with the -same stable server keys to build Connector status and management UI. Subscribe -to the MCP loaded, removed, reconnect, and status-change session events for live -updates. - -The current public runtime contract cannot replace the authoritative Connector -set on a running session. After a service Connect, Reconnect, or Disconnect, -create or cold-resume a session with the updated `connectorMcpServers` map. -Generic MCP `stopServer()` is temporary and an MCP reload can restore the -originally configured server. - ##### `disconnect(): Promise` Disconnect the session and free resources. Session data on disk is preserved for later resumption. @@ -406,6 +393,20 @@ Capabilities may update during the session. For example, when another client joi Interactive UI methods for showing dialogs to the user. Only available when the CLI host supports elicitation (`session.capabilities.ui?.elicitation === true`). See [UI Elicitation](#ui-elicitation) for full details. +##### `rpc.connectors` _(experimental)_ + +The generated Connector namespace exposes `getCapabilities`, `getStatus`, +`list`, `refresh`, `connect`, `reconnect`, `continueConnection`, `disconnect`, +and `reconcile`. The runtime owns Connector service calls, bounded polling, and +session MCP reconciliation. Your application chooses the opaque account ID, +opens returned consent URLs, confirms destructive actions, and renders UI. +Pass only the host-selected account ID; Connector methods do not accept or +return credentials or provider tokens. + +Check `getCapabilities()` before use. Connector support remains default-off +unless enabled by the connected runtime. See +[Using MCP servers](../docs/features/mcp.md#copilot-connectors). + ##### `destroy(): Promise` _(deprecated)_ Deprecated — use `disconnect()` instead. diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 90cec6391f..6e4b4fb5b4 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -36,7 +36,6 @@ import type { GitHubTelemetryNotification, GitHubTokenAcquireRequest, GitHubTokenAcquireResult, - ManagedMcpServerConfig as WireManagedMcpServerConfig, OpenCanvasInstance, SessionUpdateOptionsParams, TaskKind, @@ -57,7 +56,6 @@ import type { CopilotClientInfo, CopilotClientMode, CopilotClientOptions, - ConnectorMcpServerConfig, CustomAgentConfig, ExitPlanModeRequest, ExitPlanModeResult, @@ -250,26 +248,6 @@ function toWireMcpServers( ); } -function toWireConnectorMcpServers( - servers: Record | undefined -): Record | undefined { - if (servers === undefined) { - return undefined; - } - return Object.fromEntries( - Object.entries(servers).map(([serverKey, server]) => [ - serverKey, - { - displayName: server.displayName, - url: server.url, - tools: server.tools, - timeout: server.timeout, - headersRefreshTtlMs: server.authorizationCacheTtlMs, - }, - ]) - ); -} - /** * Convert custom agent configs, transforming nested mcpServers from * public API format (workingDirectory) to wire format (cwd). @@ -1598,7 +1576,6 @@ export class CopilotClient { this.onGetTraceContext, { mcpAuthHandler: config.onMcpAuthRequest, - mcpHeadersRefreshHandler: config.onMcpHeadersRefresh, managedSettingsEnabled: config.enableManagedSettings === true || config.managedSettings !== undefined, @@ -1728,7 +1705,6 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), - managedMcpServers: toWireConnectorMcpServers(config.connectorMcpServers), mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, authClientIdMetadataUrl: config.authClientIdMetadataUrl, envValueMode: "direct", @@ -1793,12 +1769,6 @@ export class CopilotClient { eventType: "mcp.oauth_required", }); } - if (config.onMcpHeadersRefresh) { - await this.connection!.sendRequest("session.eventLog.registerInterest", { - sessionId: returnedSessionId, - eventType: "mcp.headers_refresh_required", - }); - } session["_workspacePath"] = workspacePath; session.setCapabilities(capabilities); @@ -1878,7 +1848,6 @@ export class CopilotClient { this.onGetTraceContext, { mcpAuthHandler: config.onMcpAuthRequest, - mcpHeadersRefreshHandler: config.onMcpHeadersRefresh, managedSettingsEnabled: config.enableManagedSettings === true || config.managedSettings !== undefined, } @@ -2018,7 +1987,6 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), - managedMcpServers: toWireConnectorMcpServers(config.connectorMcpServers), mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, authClientIdMetadataUrl: config.authClientIdMetadataUrl, envValueMode: "direct", @@ -2083,12 +2051,6 @@ export class CopilotClient { eventType: "mcp.oauth_required", }); } - if (config.onMcpHeadersRefresh) { - await this.connection!.sendRequest("session.eventLog.registerInterest", { - sessionId, - eventType: "mcp.headers_refresh_required", - }); - } await this.updateSessionOptionsForMode(session, config); this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId); diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index b0bbbb56e4..47d841f16a 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -4296,6 +4296,99 @@ export type WorkspacesWorkspaceDetailsHostType = | "github" /** Workspace repository is hosted on Azure DevOps. */ | "ado"; +/** + * Availability of the EXPERIMENTAL session connector API. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorAvailability". + */ +/** @experimental */ +export type ConnectorAvailability = + /** The resolved Connector feature is enabled and Connector requests are permitted. */ + | "enabled" + /** The resolved Connector feature is off. No Connector service request is made while disabled. */ + | "disabled" + /** The session has no eligible host-owned GitHub account or does not support local Connector projection. */ + | "unavailable"; +/** + * Authoritative service connection state for one Connector. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorCatalogStatus". + */ +/** @experimental */ +export type ConnectorCatalogStatus = + /** The Connector is available but not connected. */ + | "not_connected" + /** The Connector service is still completing connection or consent. */ + | "pending" + /** The Connector is connected and may contribute MCP servers. */ + | "connected" + /** The Connector service reports an unusable connection. */ + | "error" + /** The service returned a future or unrecognized state. */ + | "unknown"; +/** + * Typed result of initiating or continuing a Connector connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorConnectResult". + */ +/** @experimental */ +export type ConnectorConnectResult = + | { + /** + * The service is connected and the session MCP graph was reconciled. + */ + kind: "connected"; + status: ConnectorStatus; + } + | { + /** + * Host-owned consent is required before bounded continuation can complete. + */ + kind: "consent_required"; + /** + * Validated HTTPS consent URL. The runtime does not open it. + */ + consentUrl: string; + /** + * Opaque ID accepted by continueConnection. + */ + continuationId: string; + } + | { + /** + * The service is still completing the connection without a consent URL. + */ + kind: "pending"; + /** + * Opaque ID accepted by continueConnection. + */ + continuationId: string; + }; +/** + * Live MCP status of one Connector-owned runtime server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorMcpStatus". + */ +/** @experimental */ +export type ConnectorMcpStatus = + /** The server is connected and its tools are available. */ + | "connected" + /** The server connection is still being established. */ + | "pending" + /** The server requires refreshed GitHub authorization. */ + | "needs_auth" + /** The server failed to connect or initialize. */ + | "failed" + /** The server is intentionally stopped, including when managed policy blocks it. */ + | "stopped" + /** The server is configured but explicitly disabled. */ + | "disabled" + /** The Connector currently has no live server configuration. */ + | "not_configured"; /** * List of all authenticated users * @@ -24484,6 +24577,218 @@ export interface WorkspacesWriteAutopilotObjectiveResult { */ operation: string; } +/** + * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorAccountRequest". + */ +/** @experimental */ +export interface ConnectorAccountRequest { + /** + * Opaque account selection ID previously returned by an account discovery API. + */ + accountId: string; +} +/** + * Feature detection and hard polling limits for the EXPERIMENTAL session connector API. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorCapabilities". + */ +/** @experimental */ +export interface ConnectorCapabilities { + /** + * Connector API contract version. + */ + apiVersion: number; + availability: ConnectorAvailability; + /** + * Whether connect and reconnect can return an opaque continuation for bounded consent polling. + */ + consentContinuation: boolean; + /** + * Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + */ + opaqueAccountSelection: boolean; + /** + * Maximum accepted polling attempts for one continuation call. + */ + maxPollAttempts: number; + /** + * Maximum accepted delay in milliseconds between polling attempts. + */ + maxPollIntervalMs: number; + /** + * Maximum accepted wall-clock deadline in milliseconds for one continuation call. + */ + maxDeadlineMs: number; +} +/** + * Credential-free Connector catalog entry. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorCatalogEntry". + */ +/** @experimental */ +export interface ConnectorCatalogEntry { + /** + * Canonical Connector name used by lifecycle methods. + */ + name: string; + /** + * Untrusted display label from the service. + */ + displayName: string; + /** + * Untrusted service description, when present. + */ + description?: string; + status: ConnectorCatalogStatus; + /** + * Opaque stable runtime IDs currently projected into the session for this Connector. + */ + runtimeServerIds: string[]; +} +/** + * Validated Connector catalog snapshot cached by the session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorCatalogResult". + */ +/** @experimental */ +export interface ConnectorCatalogResult { + /** + * Monotonically increasing session-local catalog revision. + */ + revision: number; + /** + * Unix epoch milliseconds when this snapshot was accepted. + */ + refreshedAtMs: number; + /** + * Validated catalog entries in service order. + */ + connectors: ConnectorCatalogEntry[]; +} +/** + * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorConnectRequest". + */ +/** @experimental */ +export interface ConnectorConnectRequest { + /** + * Opaque account selection ID. It must match the account already pinned to the session, if any. + */ + accountId: string; + /** + * Canonical Connector name from the current catalog. + */ + connectorName: string; +} +/** + * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorStatus". + */ +/** @experimental */ +export interface ConnectorStatus { + /** + * Connector API contract version. + */ + apiVersion: number; + availability: ConnectorAvailability; + /** + * Opaque account selection pinned to this session, when one has been selected. + */ + accountId?: string; + catalog?: ConnectorCatalogResult; + /** + * Live MCP status for every Connector-owned runtime server. + */ + runtimeServers: ConnectorRuntimeStatus[]; + /** + * Number of active opaque connection continuations. + */ + pendingConnections: number; +} +/** + * Live status of one session-owned MCP projection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorRuntimeStatus". + */ +/** @experimental */ +export interface ConnectorRuntimeStatus { + /** + * Opaque runtime server ID. + */ + runtimeServerId: string; + /** + * Canonical Connector name that owns this server. + */ + connectorName: string; + status: ConnectorMcpStatus; +} +/** + * Explicitly bounded continuation of a pending Connector connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorContinueRequest". + */ +/** @experimental */ +export interface ConnectorContinueRequest { + /** + * Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + */ + continuationId: string; + /** + * Maximum catalog requests made by this call. Must be between one and the capability limit. + */ + maxAttempts: number; + /** + * Delay in milliseconds between attempts. Must not exceed the capability limit. + */ + pollIntervalMs: number; + /** + * Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. + */ + deadlineMs: number; +} +/** + * Authoritative result after disconnect and MCP reconciliation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorDisconnectResult". + */ +/** @experimental */ +export interface ConnectorDisconnectResult { + /** + * Whether the service accepted the idempotent disconnect. + */ + disconnected: boolean; + status: ConnectorStatus; +} +/** + * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorReconcileRequest". + */ +/** @experimental */ +export interface ConnectorReconcileRequest { + /** + * Opaque account selection ID. It must match the account already pinned to the session, if any. + */ + accountId: string; + /** + * When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + */ + refreshCatalog?: boolean; +} /** @experimental */ export interface SessionFactoryPauseAtCheckpointResult { @@ -27510,6 +27815,86 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin stop: async (params: ScheduleStopRequest): Promise => connection.sendRequest("session.schedule.stop", { sessionId, ...params }), }, + /** @experimental */ + connectors: { + /** + * Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. + * + * @returns Feature detection and hard polling limits for the EXPERIMENTAL session connector API. + */ + getCapabilities: async (): Promise => + connection.sendRequest("session.connectors.getCapabilities", { sessionId }), + /** + * Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. + * + * @returns Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + */ + getStatus: async (): Promise => + connection.sendRequest("session.connectors.getStatus", { sessionId }), + /** + * Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. + * + * @param params Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * + * @returns Validated Connector catalog snapshot cached by the session. + */ + list: async (params: ConnectorAccountRequest): Promise => + connection.sendRequest("session.connectors.list", { sessionId, ...params }), + /** + * Refreshes and validates the Connector catalog for the pinned opaque account selection. + * + * @param params Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * + * @returns Validated Connector catalog snapshot cached by the session. + */ + refresh: async (params: ConnectorAccountRequest): Promise => + connection.sendRequest("session.connectors.refresh", { sessionId, ...params }), + /** + * Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. + * + * @param params Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * + * @returns Typed result of initiating or continuing a Connector connection. + */ + connect: async (params: ConnectorConnectRequest): Promise => + connection.sendRequest("session.connectors.connect", { sessionId, ...params }), + /** + * Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. + * + * @param params Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * + * @returns Typed result of initiating or continuing a Connector connection. + */ + reconnect: async (params: ConnectorConnectRequest): Promise => + connection.sendRequest("session.connectors.reconnect", { sessionId, ...params }), + /** + * Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. + * + * @param params Explicitly bounded continuation of a pending Connector connection. + * + * @returns Typed result of initiating or continuing a Connector connection. + */ + continueConnection: async (params: ConnectorContinueRequest): Promise => + connection.sendRequest("session.connectors.continueConnection", { sessionId, ...params }), + /** + * Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. + * + * @param params Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * + * @returns Authoritative result after disconnect and MCP reconciliation. + */ + disconnect: async (params: ConnectorConnectRequest): Promise => + connection.sendRequest("session.connectors.disconnect", { sessionId, ...params }), + /** + * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. + * + * @param params Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * + * @returns Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + */ + reconcile: async (params: ConnectorReconcileRequest): Promise => + connection.sendRequest("session.connectors.reconcile", { sessionId, ...params }), + }, }; } diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 9113debb8d..3a27826569 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -60,6 +60,20 @@ export type { CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, + ConnectorAccountRequest, + ConnectorAvailability, + ConnectorCapabilities, + ConnectorCatalogEntry, + ConnectorCatalogResult, + ConnectorCatalogStatus, + ConnectorConnectRequest, + ConnectorConnectResult, + ConnectorContinueRequest, + ConnectorDisconnectResult, + ConnectorMcpStatus, + ConnectorReconcileRequest, + ConnectorRuntimeStatus, + ConnectorStatus, AutoModeSwitchHandler, AutoModeSwitchRequest, AutoModeSwitchResponse, @@ -113,10 +127,6 @@ export type { MCPStdioServerConfig, MCPHTTPServerConfig, MCPServerConfig, - ConnectorMcpServerConfig, - McpHeadersRefreshHandler, - McpHeadersRefreshRequest, - McpHeadersRefreshResult, DefaultAgentConfig, BearerTokenProvider, MessageOptions, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 42a67c8916..896eded0bd 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -15,7 +15,6 @@ import type { ClientSessionApiHandlers, CanvasActionInvokeResult, CurrentToolMetadata, - McpHeadersHandlePendingHeadersRefreshRequest, McpOauthPendingRequestResponse, FactoryLogLine, FactoryRunResult as WireFactoryRunResult, @@ -42,9 +41,6 @@ import type { MessageOptions, McpAuthHandler, McpAuthRequest, - McpHeadersRefreshHandler, - McpHeadersRefreshRequest, - McpHeadersRefreshResult, PermissionHandler, PermissionRequest, PermissionRequestResult, @@ -177,17 +173,6 @@ function isOpenCanvasInstance(value: unknown): value is OpenCanvasInstance { ); } -function isMcpHeadersRefreshResult( - value: McpHeadersRefreshResult | Record -): value is McpHeadersRefreshResult { - const headers = (value as { headers?: unknown }).headers; - return ( - typeof headers === "object" && - headers !== null && - Object.values(headers).every((header) => typeof header === "string") - ); -} - const FACTORY_LOG_FLUSH_DELAY_MS = 10; const MAX_FACTORY_FANOUT_ITEMS = 4096; @@ -460,7 +445,6 @@ export class CopilotSession { private factoryAbortControllers = new Map>(); private permissionHandler?: PermissionHandler; private mcpAuthHandler?: McpAuthHandler; - private mcpHeadersRefreshHandler?: McpHeadersRefreshHandler; private userInputHandler?: UserInputHandler; private elicitationHandler?: ElicitationHandler; private exitPlanModeHandler?: ExitPlanModeHandler; @@ -670,14 +654,12 @@ export class CopilotSession { traceContextProvider?: TraceContextProvider, options?: { mcpAuthHandler?: McpAuthHandler; - mcpHeadersRefreshHandler?: McpHeadersRefreshHandler; managedSettingsEnabled?: boolean; onDisconnected?: () => void; } ) { this.traceContextProvider = traceContextProvider; this.mcpAuthHandler = options?.mcpAuthHandler; - this.mcpHeadersRefreshHandler = options?.mcpHeadersRefreshHandler; this.managedSettingsEnabled = options?.managedSettingsEnabled === true; this.onDisconnected = options?.onDisconnected; } @@ -1083,16 +1065,6 @@ export class CopilotSession { return; } void this._executeMcpAuthAndRespond(data); - } else if (event.type === "mcp.headers_refresh_required") { - const { requestId, serverName, serverUrl, reason } = event.data; - if (!requestId || !this.mcpHeadersRefreshHandler) { - return; - } - void this._executeMcpHeadersRefreshAndRespond(requestId, { - serverKey: serverName, - serverUrl, - reason, - }); } else if (event.type === "command.execute") { const { requestId, commandName, command, args } = event.data as { requestId: string; @@ -1348,48 +1320,6 @@ export class CopilotSession { } } - /** - * Executes an MCP headers refresh handler and sends the result via RPC. - * @internal - */ - private async _executeMcpHeadersRefreshAndRespond( - requestId: string, - request: McpHeadersRefreshRequest - ): Promise { - let result: McpHeadersHandlePendingHeadersRefreshRequest; - try { - const response = await this.mcpHeadersRefreshHandler!(request, { - sessionId: this.sessionId, - }); - if (response === undefined) { - result = { kind: "none" }; - } else if (isMcpHeadersRefreshResult(response)) { - result = { - kind: "headers", - headers: response.headers, - ...(response.ttlMs === undefined ? {} : { ttlMs: response.ttlMs }), - }; - } else { - result = { kind: "headers", headers: response }; - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn(`MCP headers refresh failed for '${request.serverKey}': ${message}`); - result = { kind: "error", message }; - } - - try { - await this.rpc.mcp.headers.handlePendingHeadersRefreshRequest({ - requestId, - result, - }); - } catch (rpcError) { - if (!(rpcError instanceof ConnectionError || rpcError instanceof ResponseError)) { - throw rpcError; - } - } - } - /** * Executes a command handler and sends the result back via RPC. * @internal diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 89d2ff1e77..859dc70eca 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -15,7 +15,6 @@ import type { PermissionRequest as GeneratedPermissionRequest, PermissionRequestedData as GeneratedPermissionRequestedData, PermissionRequestedEvent as GeneratedPermissionRequestedEvent, - McpHeadersRefreshRequiredReason, ReasoningSummary, SessionLimitsConfig, SessionEvent as GeneratedSessionEvent, @@ -34,20 +33,21 @@ import type { import type { ToolSet } from "./toolSet.js"; export type { RemoteSessionMode } from "./generated/rpc.js"; export type { CurrentToolMetadata } from "./generated/rpc.js"; -/** Non-secret hosted MCP endpoint advertised for a connected Copilot Connector. */ -export interface ConnectorMcpServerConfig { - /** Human-readable Connector name. */ - displayName: string; - /** Service-advertised MCP endpoint. */ - url: string; - /** Tools to expose. Defaults to all tools when omitted. */ - tools?: string[]; - /** Timeout in milliseconds for discovery and tool calls. */ - timeout?: number; - /** Maximum time the runtime may reuse Connector service authorization. */ - authorizationCacheTtlMs?: number; -} export type { + ConnectorAccountRequest, + ConnectorAvailability, + ConnectorCapabilities, + ConnectorCatalogEntry, + ConnectorCatalogResult, + ConnectorCatalogStatus, + ConnectorConnectRequest, + ConnectorConnectResult, + ConnectorContinueRequest, + ConnectorDisconnectResult, + ConnectorMcpStatus, + ConnectorReconcileRequest, + ConnectorRuntimeStatus, + ConnectorStatus, GitHubTokenAcquireReason, GitHubTokenAcquireResult, GitHubTelemetryNotification, @@ -2094,38 +2094,6 @@ export type McpAuthHandler = ( | undefined | Promise; -/** Connector MCP server whose short-lived HTTP headers must be refreshed. */ -export interface McpHeadersRefreshRequest { - /** Stable server identity used as the connectorMcpServers map key. */ - serverKey: string; - /** Hosted MCP streamable HTTP endpoint. */ - serverUrl: string; - /** Why the runtime invalidated or requested dynamic headers. */ - reason: McpHeadersRefreshRequiredReason; -} - -/** Connector service authorization headers and their optional cache lifetime. */ -export interface McpHeadersRefreshResult { - headers: Record; - ttlMs?: number; -} - -/** - * Callback invoked when a Connector MCP server needs fresh HTTP headers. - * - * Return a bare header map or a structured result with `ttlMs`. Returning - * `undefined` reports that no headers are available. Throwing reports an - * explicit Connector authorization error to the runtime. - */ -export type McpHeadersRefreshHandler = ( - request: McpHeadersRefreshRequest, - context: { sessionId: string } -) => - | McpHeadersRefreshResult - | Record - | undefined - | Promise | undefined>; - /** * Stable extension identity for session participants that provide canvases. */ @@ -2667,12 +2635,6 @@ export interface SessionConfigBase { */ onMcpAuthRequest?: McpAuthHandler; - /** - * Supplies short-lived HTTP headers for Connector MCP servers. - * Re-register this handler when cold-resuming a session. - */ - onMcpHeadersRefresh?: McpHeadersRefreshHandler; - /** * Handler for user input requests from the agent. * When provided with the default `legacy` {@link AskUserVariant}, enables the @@ -2804,15 +2766,6 @@ export interface SessionConfigBase { */ mcpServers?: Record; - /** - * Non-secret hosted MCP endpoints advertised for connected Copilot Connectors. - * Keys are stable Connector server identities. Service authorization must be - * supplied through {@link onMcpHeadersRefresh}, never stored in this configuration. - * - * @experimental - */ - connectorMcpServers?: Record; - /** * Custom agent configurations for the session. */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index cf8ab4f700..f8f3848b11 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -354,170 +354,6 @@ describe("CopilotClient", () => { }); }); - it.each([ - { - name: "bare headers without ttlMs", - response: { Authorization: "Bearer short-lived" }, - expected: { - kind: "headers", - headers: { Authorization: "Bearer short-lived" }, - }, - }, - { - name: "structured headers with ttlMs", - response: { - headers: { Authorization: "Bearer short-lived" }, - ttlMs: 5_000, - }, - expected: { - kind: "headers", - headers: { Authorization: "Bearer short-lived" }, - ttlMs: 5_000, - }, - }, - ])("responds to remote MCP header refresh with $name", async ({ response, expected }) => { - const sendRequest = vi.fn(async () => ({ success: true })); - const session = new CopilotSession( - "session-1", - { sendRequest } as any, - undefined, - undefined, - { mcpHeadersRefreshHandler: async () => response } - ); - - await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { - serverKey: "github", - serverUrl: "https://example.com/mcp", - reason: "startup", - }); - - expect(sendRequest).toHaveBeenCalledWith( - "session.mcp.headers.handlePendingHeadersRefreshRequest", - { - sessionId: "session-1", - requestId: "refresh-request", - result: expected, - } - ); - }); - - it("reports no dynamic MCP headers when the handler returns undefined", async () => { - const sendRequest = vi.fn(async () => ({ success: true })); - const session = new CopilotSession( - "session-1", - { sendRequest } as any, - undefined, - undefined, - { mcpHeadersRefreshHandler: () => undefined } - ); - - await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { - serverKey: "github", - serverUrl: "https://example.com/mcp", - reason: "ttl-expired", - }); - - expect(sendRequest).toHaveBeenCalledWith( - "session.mcp.headers.handlePendingHeadersRefreshRequest", - { - sessionId: "session-1", - requestId: "refresh-request", - result: { kind: "none" }, - } - ); - }); - - it("propagates MCP credential broker failures explicitly", async () => { - const sendRequest = vi.fn(async () => ({ success: true })); - const warning = vi.spyOn(console, "warn").mockImplementation(() => {}); - const session = new CopilotSession( - "session-1", - { sendRequest } as any, - undefined, - undefined, - { - mcpHeadersRefreshHandler: () => { - throw new Error("credential revoked"); - }, - } - ); - - await (session as any)._executeMcpHeadersRefreshAndRespond("refresh-request", { - serverKey: "github", - serverUrl: "https://example.com/mcp", - reason: "auth-failed", - }); - - expect(sendRequest).toHaveBeenCalledWith( - "session.mcp.headers.handlePendingHeadersRefreshRequest", - { - sessionId: "session-1", - requestId: "refresh-request", - result: { kind: "error", message: "credential revoked" }, - } - ); - warning.mockRestore(); - }); - - it("maps Connector MCP servers to the internal wire contract on create and resume", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => stopClient(client)); - - const spy = vi - .spyOn((client as any).connection!, "sendRequest") - .mockImplementation(async (method: string, params: any) => { - if (method === "session.create" || method === "session.resume") { - return { sessionId: params.sessionId }; - } - if (method === "session.eventLog.registerInterest") { - return { id: "interest-1" }; - } - throw new Error(`Unexpected method: ${method}`); - }); - const connectorMcpServers = { - github: { - displayName: "GitHub", - url: "https://api.example.com/mcp", - tools: ["issues"], - timeout: 30_000, - authorizationCacheTtlMs: 60_000, - }, - }; - const onMcpHeadersRefresh = () => ({ Authorization: "Bearer short-lived" }); - - const session = await client.createSession({ - connectorMcpServers, - onMcpHeadersRefresh, - }); - await client.resumeSession(session.sessionId, { - connectorMcpServers, - onMcpHeadersRefresh, - }); - - for (const method of ["session.create", "session.resume"]) { - expect(spy.mock.calls.find(([called]) => called === method)![1]).toMatchObject({ - managedMcpServers: { - github: { - displayName: "GitHub", - url: "https://api.example.com/mcp", - tools: ["issues"], - timeout: 30_000, - headersRefreshTtlMs: 60_000, - }, - }, - }); - } - expect(spy).toHaveBeenCalledTimes(4); - expect( - spy.mock.calls.filter( - ([method, params]) => - method === "session.eventLog.registerInterest" && - params.eventType === "mcp.headers_refresh_required" - ) - ).toHaveLength(2); - }); - it("forwards GitHub MCP tool config on create and resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/nodejs/test/connectors.test.ts b/nodejs/test/connectors.test.ts new file mode 100644 index 0000000000..fbda1b1c8c --- /dev/null +++ b/nodejs/test/connectors.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it, vi } from "vitest"; +import type { MessageConnection } from "vscode-jsonrpc"; + +import { CopilotSession } from "../src/session.js"; + +describe("session Connector RPC", () => { + it("exposes the complete experimental Connector lifecycle", async () => { + const sendRequest = vi.fn(async (method: string) => { + if (method === "session.connectors.getCapabilities") { + return { + apiVersion: 1, + availability: "enabled", + consentContinuation: true, + opaqueAccountSelection: true, + maxPollAttempts: 30, + maxPollIntervalMs: 2_000, + maxDeadlineMs: 60_000, + }; + } + if (method === "session.connectors.list" || method === "session.connectors.refresh") { + return { revision: 1, refreshedAtMs: 1, connectors: [] }; + } + if (method === "session.connectors.connect") { + return { + kind: "consent_required", + consentUrl: "https://example.com/consent", + continuationId: "continuation-1", + }; + } + if ( + method === "session.connectors.reconnect" || + method === "session.connectors.continueConnection" + ) { + return { kind: "pending", continuationId: "continuation-1" }; + } + if (method === "session.connectors.disconnect") { + return { disconnected: true, status: connectorStatus() }; + } + return connectorStatus(); + }); + const session = new CopilotSession("session-1", { + sendRequest, + } as unknown as MessageConnection); + + expect(await session.rpc.connectors.getCapabilities()).toMatchObject({ + apiVersion: 1, + availability: "enabled", + }); + expect(await session.rpc.connectors.getStatus()).toEqual(connectorStatus()); + expect(await session.rpc.connectors.list({ accountId: "account-1" })).toMatchObject({ + revision: 1, + }); + expect(await session.rpc.connectors.refresh({ accountId: "account-1" })).toMatchObject({ + revision: 1, + }); + expect( + await session.rpc.connectors.connect({ + accountId: "account-1", + connectorName: "calendar", + }) + ).toMatchObject({ kind: "consent_required" }); + expect( + await session.rpc.connectors.reconnect({ + accountId: "account-1", + connectorName: "calendar", + }) + ).toMatchObject({ kind: "pending" }); + expect( + await session.rpc.connectors.continueConnection({ + continuationId: "continuation-1", + maxAttempts: 3, + pollIntervalMs: 100, + deadlineMs: 1_000, + }) + ).toMatchObject({ kind: "pending" }); + expect( + await session.rpc.connectors.disconnect({ + accountId: "account-1", + connectorName: "calendar", + }) + ).toMatchObject({ disconnected: true }); + expect( + await session.rpc.connectors.reconcile({ + accountId: "account-1", + refreshCatalog: true, + }) + ).toEqual(connectorStatus()); + + expect(sendRequest.mock.calls).toEqual([ + ["session.connectors.getCapabilities", { sessionId: "session-1" }], + ["session.connectors.getStatus", { sessionId: "session-1" }], + ["session.connectors.list", { sessionId: "session-1", accountId: "account-1" }], + ["session.connectors.refresh", { sessionId: "session-1", accountId: "account-1" }], + [ + "session.connectors.connect", + { + sessionId: "session-1", + accountId: "account-1", + connectorName: "calendar", + }, + ], + [ + "session.connectors.reconnect", + { + sessionId: "session-1", + accountId: "account-1", + connectorName: "calendar", + }, + ], + [ + "session.connectors.continueConnection", + { + sessionId: "session-1", + continuationId: "continuation-1", + maxAttempts: 3, + pollIntervalMs: 100, + deadlineMs: 1_000, + }, + ], + [ + "session.connectors.disconnect", + { + sessionId: "session-1", + accountId: "account-1", + connectorName: "calendar", + }, + ], + [ + "session.connectors.reconcile", + { + sessionId: "session-1", + accountId: "account-1", + refreshCatalog: true, + }, + ], + ]); + }); +}); + +function connectorStatus() { + return { + apiVersion: 1, + availability: "enabled" as const, + runtimeServers: [], + pendingConnections: 0, + }; +} diff --git a/nodejs/test/e2e/mcp_oauth.e2e.test.ts b/nodejs/test/e2e/mcp_oauth.e2e.test.ts index 590fb480c4..ab68afc2ef 100644 --- a/nodejs/test/e2e/mcp_oauth.e2e.test.ts +++ b/nodejs/test/e2e/mcp_oauth.e2e.test.ts @@ -7,12 +7,7 @@ import { dirname, resolve } from "node:path"; import { createInterface } from "node:readline"; import { fileURLToPath } from "node:url"; import { describe, expect, it, onTestFinished } from "vitest"; -import type { - CopilotSession, - MCPServerConfig, - McpAuthRequest, - McpHeadersRefreshRequest, -} from "../../src/index.js"; +import type { CopilotSession, MCPServerConfig, McpAuthRequest } from "../../src/index.js"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { stopChildProcess, waitForCondition } from "./harness/sdkTestHelper.js"; @@ -32,8 +27,6 @@ describe("MCP OAuth host auth", async () => { env: { COPILOT_MCP_APPS: "true", MCP_APPS: "true", - COPILOT_CLI_ENABLED_FEATURE_FLAGS: "MANAGED_MCP_SERVERS", - COPILOT_EXP_COPILOT_CLI_MANAGED_MCP_SERVERS: "true", }, }, }); @@ -130,49 +123,6 @@ describe("MCP OAuth host auth", async () => { ).toBe(true); }); - it( - "should project a connected Connector endpoint at startup", - { timeout: 180_000 }, - async () => { - const oauthServer = await startOAuthMcpServer(); - const serverKey = "connector-calendar"; - const connectorServer = { - displayName: "Calendar", - url: `${oauthServer.url}/mcp`, - authorizationCacheTtlMs: 0, - }; - const refreshRequests: McpHeadersRefreshRequest[] = []; - const session = await client.createSession({ - onPermissionRequest: approveAll, - connectorMcpServers: { [serverKey]: connectorServer }, - onMcpHeadersRefresh: (request) => { - refreshRequests.push(request); - return { Authorization: ["Bearer", EXPECTED_TOKEN].join(" ") }; - }, - }); - onTestFinished(() => disconnectSession(session)); - - await waitForMcpServerStatus(session, serverKey); - expect((await session.rpc.mcp.listTools({ serverName: serverKey })).tools).toEqual( - expect.arrayContaining([expect.objectContaining({ name: "whoami" })]) - ); - expect(refreshRequests[0]).toEqual({ - serverKey, - serverUrl: `${oauthServer.url}/mcp`, - reason: "startup", - }); - - expect(refreshRequests.length).toBeGreaterThanOrEqual(1); - expect( - refreshRequests.every( - (request) => - request.serverKey === serverKey && - request.serverUrl === `${oauthServer.url}/mcp` - ) - ).toBe(true); - } - ); - it( "should resolve pending MCP OAuth request with direct RPC", { timeout: 120_000 }, diff --git a/nodejs/test/shared-codegen.test.ts b/nodejs/test/shared-codegen.test.ts index a34d910dda..2bf97f7df4 100644 --- a/nodejs/test/shared-codegen.test.ts +++ b/nodejs/test/shared-codegen.test.ts @@ -2,14 +2,13 @@ import { readFile } from "node:fs/promises"; import type { JSONSchema7 } from "json-schema"; import { describe, expect, it } from "vitest"; +import { applyConnectorSessionApiOverlay } from "../../scripts/codegen/connectorSessionApiOverlay.ts"; import { collectDefinitionCollections, collectExperimentalOnlyRpcReferencedDefinitionNames, collectReachableDefinitionNames, findSharedSchemaDefinitions, - getApiSchemaPath, getEnumValueDescriptions, - getSessionEventsSchemaPath, inlineExternalSchemaDefinitions, isIntegerSchemaBoundedToInt32, rewriteSharedDefinitionReferences, @@ -63,19 +62,55 @@ describe("shared schema definition codegen utilities", () => { ).toBe(false); }); - it("pins schemas containing the catalog MCP wire contract", async () => { - const api = JSON.parse(await readFile(await getApiSchemaPath(), "utf8")) as { - definitions: Record; - }; - const events = JSON.parse(await readFile(await getSessionEventsSchemaPath(), "utf8")) as { - definitions: Record; - }; + describe("Connector session API overlay", () => { + async function loadOverlay(): Promise<{ + sessionConnectors: Record; + definitions: Record>; + }> { + return JSON.parse( + await readFile( + new URL( + "../../scripts/codegen/connector-session-api-overlay.json", + import.meta.url + ), + "utf8" + ) + ); + } + + it("installs the complete draft contract and is idempotent", async () => { + const schema = { session: {}, definitions: {} }; + const target = await loadOverlay(); + + expect(applyConnectorSessionApiOverlay(schema, "api.schema.json")).toBe(schema); + expect(schema.session).toEqual({ connectors: target.sessionConnectors }); + expect(schema.definitions).toEqual(target.definitions); + + const before = structuredClone(schema); + expect(applyConnectorSessionApiOverlay(schema, "api.schema.json")).toBe(schema); + expect(schema).toEqual(before); + }); + + it("rejects partial or changed upstream contracts without mutation", async () => { + const target = await loadOverlay(); + const schema = { + session: { connectors: target.sessionConnectors }, + definitions: {}, + }; + const before = structuredClone(schema); + + expect(() => applyConnectorSessionApiOverlay(schema, "api.schema.json")).toThrow( + "partial or unknown upstream contract" + ); + expect(schema).toEqual(before); + }); - expect(api.definitions).toHaveProperty("ManagedMcpServerConfig"); - expect(api.definitions.SessionOpenOptions.properties).toHaveProperty("managedMcpServers"); - expect(api.definitions.McpHeadersHandlePendingHeadersRefreshRequest.anyOf).toHaveLength(3); - expect(events.definitions.McpServerSource.enum).toContain("managed"); - expect(events.definitions.McpServersLoadedServer.properties).toHaveProperty("displayName"); + it("ignores non-API schemas", () => { + const schema = { definitions: {} }; + expect(applyConnectorSessionApiOverlay(schema, "session-events.schema.json")).toBe( + schema + ); + }); }); it("extracts non-empty enum value descriptions from schema extensions", () => { diff --git a/python/README.md b/python/README.md index 8c62f564dd..b29d670d18 100644 --- a/python/README.md +++ b/python/README.md @@ -313,8 +313,6 @@ These are passed as keyword arguments to `create_session()`: - `system_message` (SystemMessageConfig): System message configuration - `streaming` (bool): Enable streaming delta events - `provider` (ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. -- `connector_mcp_servers` (dict[str, ConnectorMCPServerConfig]): Connected Copilot Connector MCP endpoints advertised by the Connector service. Re-supply on cold resume. -- `on_mcp_headers_refresh` (callable): Supplies short-lived headers for Connector MCP endpoints. Register this even when the initial connected set is empty if the host supports connecting a Connector during the session. - `infinite_sessions` (InfiniteSessionConfig): Automatic context compaction configuration - `working_directory` (str | None): Working directory for the session (default: runtime process working directory). - `enable_session_store` (bool): Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled. @@ -340,18 +338,19 @@ Initial acquisition runs during session creation or resume. Cancellation, provid - `available_tools` / `excluded_tools` / `default_agent.excluded_tools` / custom-agent `tools`: MCP tools registered from `mcp_servers` are exposed to the runtime as `-`. For `available_tools` and `excluded_tools`, prefer `ToolSet().add_mcp("-")` or the raw `mcp:-` form. For custom-agent `tools` and `default_agent.excluded_tools`, use `-` directly. -**Connector session integration (experimental):** +**Copilot Connectors (experimental):** -Use `session.rpc.mcp.list()`, `enable()`, `disable()`, and `list_tools()` with -the same stable server keys to build Connector status and management UI. -Subscribe to the MCP loaded, removed, reconnect, and status-change session -events for live updates. +`session.rpc.connectors` exposes `get_capabilities`, `get_status`, `list`, +`refresh`, `connect`, `reconnect`, `continue_connection`, `disconnect`, and +`reconcile`. The runtime owns Connector service calls, bounded polling, and MCP +reconciliation. Your application chooses the opaque account ID, opens returned +consent URLs, confirms destructive actions, and renders UI. +Pass only the host-selected account ID; Connector methods do not accept or +return credentials or provider tokens. -The current public runtime contract cannot replace the authoritative Connector -set on a running session. After a service Connect, Reconnect, or Disconnect, -create or cold-resume a session with the updated `connector_mcp_servers` map. -Generic MCP `stop_server()` is temporary and an MCP reload can restore the -originally configured server. +Check `get_capabilities()` before use. Connector support remains default-off +unless enabled by the connected runtime. See +[Using MCP servers](../docs/features/mcp.md#copilot-connectors). **Session Lifecycle Methods:** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index d41ce6e929..e2326e1135 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -127,7 +127,6 @@ BearerTokenProvider, CommandContext, CommandDefinition, - ConnectorMCPServerConfig, CopilotSession, CreateSessionFsHandler, ElicitationContext, @@ -151,10 +150,6 @@ McpAuthStaticClientConfig, McpAuthToken, McpAuthWwwAuthenticateParams, - McpHeadersRefreshContext, - McpHeadersRefreshHandler, - McpHeadersRefreshRequest, - McpHeadersRefreshResult, MCPHTTPServerConfig, MCPServerConfig, MCPStdioServerConfig, @@ -324,11 +319,6 @@ "McpAuthStaticClientConfig", "McpAuthToken", "McpAuthWwwAuthenticateParams", - "McpHeadersRefreshContext", - "McpHeadersRefreshHandler", - "McpHeadersRefreshRequest", - "McpHeadersRefreshResult", - "ConnectorMCPServerConfig", "ManagedSettings", "ManagedSettingsPermissions", "MessageSource", diff --git a/python/copilot/client.py b/python/copilot/client.py index 7d250fafae..4d9d4dfb76 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -95,7 +95,6 @@ AutoTier, BearerTokenProvider, CommandDefinition, - ConnectorMCPServerConfig, ContextTier, CopilotSession, CreateSessionFsHandler, @@ -107,7 +106,6 @@ InfiniteSessionConfig, LargeToolOutputConfig, McpAuthHandler, - McpHeadersRefreshHandler, MCPServerConfig, MemoryConfiguration, ModelCapabilitiesOverride, @@ -124,7 +122,6 @@ ToolSearchConfig, UserInputHandler, _capabilities_to_dict, - _connector_mcp_servers_to_wire, _PermissionHandlerFn, ) from .session_fs_provider import SessionFsProvider, create_session_fs_adapter @@ -2296,7 +2293,6 @@ async def create_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, - connector_mcp_servers: dict[str, ConnectorMCPServerConfig] | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, auth_client_id_metadata_url: str | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, @@ -2325,7 +2321,6 @@ async def create_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, on_mcp_auth_request: McpAuthHandler | None = None, - on_mcp_headers_refresh: McpHeadersRefreshHandler | None = None, enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, @@ -2432,13 +2427,6 @@ async def create_session( ``agentId`` set). When False, only non-streaming sub-agent events and ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. - connector_mcp_servers: Non-secret hosted MCP endpoints advertised - for connected Copilot Connectors, keyed by stable server identity. - Re-supply on cold resume. - on_mcp_headers_refresh: Supplies short-lived HTTP headers for - Connector MCP endpoints. These authorize the selected GitHub - account to the Connector service; the service owns downstream - provider credentials. Exceptions are explicit runtime errors. mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored. ``"persistent"`` uses the OS keychain (shared across sessions). ``"in-memory"`` stores tokens in memory (discarded on session end). @@ -2749,8 +2737,6 @@ async def create_session( # Add MCP servers configuration if provided if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) - if connector_mcp_servers is not None: - payload["managedMcpServers"] = _connector_mcp_servers_to_wire(connector_mcp_servers) # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: @@ -2923,7 +2909,6 @@ def _initialize_session(sid: str) -> CopilotSession: s._register_commands(commands) s._register_permission_handler(on_permission_request) s._register_mcp_auth_handler(on_mcp_auth_request) - s._register_mcp_headers_refresh_handler(on_mcp_headers_refresh) if on_user_input_request: s._register_user_input_handler(on_user_input_request) if on_elicitation_request: @@ -3016,14 +3001,6 @@ def _register_inline(raw_response: Any) -> None: "session.eventLog.registerInterest", {"sessionId": session.session_id, "eventType": "mcp.oauth_required"}, ) - if on_mcp_headers_refresh is not None: - await self._client.request( - "session.eventLog.registerInterest", - { - "sessionId": session.session_id, - "eventType": "mcp.headers_refresh_required", - }, - ) session._workspace_path = response.get("workspacePath") capabilities = response.get("capabilities") session._set_capabilities(capabilities) @@ -3103,7 +3080,6 @@ async def resume_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, - connector_mcp_servers: dict[str, ConnectorMCPServerConfig] | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, auth_client_id_metadata_url: str | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, @@ -3132,7 +3108,6 @@ async def resume_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, on_mcp_auth_request: McpAuthHandler | None = None, - on_mcp_headers_refresh: McpHeadersRefreshHandler | None = None, enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, @@ -3240,11 +3215,6 @@ async def resume_session( ``agentId`` set). When False, only non-streaming sub-agent events and ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. - connector_mcp_servers: Non-secret hosted MCP endpoints advertised - for connected Copilot Connectors. Re-supply them on cold resume. - on_mcp_headers_refresh: Supplies short-lived HTTP headers for - Connector MCP endpoints. The Connector service owns downstream - provider credentials. mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored. ``"persistent"`` uses the OS keychain (shared across sessions). ``"in-memory"`` stores tokens in memory (discarded on session end). @@ -3550,8 +3520,6 @@ async def resume_session( # TODO: disable_resume is not a keyword arg yet; keeping for future use if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) - if connector_mcp_servers is not None: - payload["managedMcpServers"] = _connector_mcp_servers_to_wire(connector_mcp_servers) # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: @@ -3661,7 +3629,6 @@ async def resume_session( session._register_commands(commands) session._register_permission_handler(on_permission_request) session._register_mcp_auth_handler(on_mcp_auth_request) - session._register_mcp_headers_refresh_handler(on_mcp_headers_refresh) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if on_elicitation_request: @@ -3727,14 +3694,6 @@ async def resume_session( "session.eventLog.registerInterest", {"sessionId": session.session_id, "eventType": "mcp.oauth_required"}, ) - if on_mcp_headers_refresh is not None: - await self._client.request( - "session.eventLog.registerInterest", - { - "sessionId": session.session_id, - "eventType": "mcp.headers_refresh_required", - }, - ) except BaseException as exc: with self._sessions_lock: self._sessions.pop(session_id, None) diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index bcf53a1495..82fcf3cb54 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -1997,6 +1997,159 @@ def to_dict(self) -> dict: result["owner"] = from_str(self.owner) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorAccountRequest: + """Pins a Connector operation to one host-owned GitHub account through its opaque selection + ID. Provider tokens are never accepted. + """ + account_id: str + """Opaque account selection ID previously returned by an account discovery API.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorAccountRequest': + assert isinstance(obj, dict) + account_id = from_str(obj.get("accountId")) + return ConnectorAccountRequest(account_id) + + def to_dict(self) -> dict: + result: dict = {} + result["accountId"] = from_str(self.account_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ConnectorAvailability(Enum): + """Availability of the EXPERIMENTAL session connector API. + + Current session availability. Disabled availability is reported without making a + Connector request. + + Current feature and session availability. + """ + DISABLED = "disabled" + ENABLED = "enabled" + UNAVAILABLE = "unavailable" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ConnectorCatalogStatus(Enum): + """Current authoritative service connection state. + + Authoritative service connection state for one Connector. + """ + CONNECTED = "connected" + ERROR = "error" + NOT_CONNECTED = "not_connected" + PENDING = "pending" + UNKNOWN = "unknown" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorConnectRequest: + """Selects one Connector and the pinned host-owned account used for its service and MCP + authorization. + """ + account_id: str + """Opaque account selection ID. It must match the account already pinned to the session, if + any. + """ + connector_name: str + """Canonical Connector name from the current catalog.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorConnectRequest': + assert isinstance(obj, dict) + account_id = from_str(obj.get("accountId")) + connector_name = from_str(obj.get("connectorName")) + return ConnectorConnectRequest(account_id, connector_name) + + def to_dict(self) -> dict: + result: dict = {} + result["accountId"] = from_str(self.account_id) + result["connectorName"] = from_str(self.connector_name) + return result + +class ConnectorConnectResultKind(Enum): + CONNECTED = "connected" + CONSENT_REQUIRED = "consent_required" + PENDING = "pending" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ConnectorMCPStatus(Enum): + """Current live MCP host status. + + Live MCP status of one Connector-owned runtime server. + """ + CONNECTED = "connected" + DISABLED = "disabled" + FAILED = "failed" + NEEDS_AUTH = "needs_auth" + NOT_CONFIGURED = "not_configured" + PENDING = "pending" + STOPPED = "stopped" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorContinueRequest: + """Explicitly bounded continuation of a pending Connector connection.""" + + continuation_id: str + """Opaque continuation ID returned by connect, reconnect, or an earlier continuation.""" + + deadline_ms: int + """Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability + limit. + """ + max_attempts: int + """Maximum catalog requests made by this call. Must be between one and the capability limit.""" + + poll_interval_ms: int + """Delay in milliseconds between attempts. Must not exceed the capability limit.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorContinueRequest': + assert isinstance(obj, dict) + continuation_id = from_str(obj.get("continuationId")) + deadline_ms = from_int(obj.get("deadlineMs")) + max_attempts = from_int(obj.get("maxAttempts")) + poll_interval_ms = from_int(obj.get("pollIntervalMs")) + return ConnectorContinueRequest(continuation_id, deadline_ms, max_attempts, poll_interval_ms) + + def to_dict(self) -> dict: + result: dict = {} + result["continuationId"] = from_str(self.continuation_id) + result["deadlineMs"] = from_int(self.deadline_ms) + result["maxAttempts"] = from_int(self.max_attempts) + result["pollIntervalMs"] = from_int(self.poll_interval_ms) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorReconcileRequest: + """Requests authoritative Connector-to-MCP reconciliation for the pinned account.""" + + account_id: str + """Opaque account selection ID. It must match the account already pinned to the session, if + any. + """ + refresh_catalog: bool | None = None + """When true, refresh the catalog before reconciling. A disabled Connector API performs no + service request. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorReconcileRequest': + assert isinstance(obj, dict) + account_id = from_str(obj.get("accountId")) + refresh_catalog = from_union([from_bool, from_none], obj.get("refreshCatalog")) + return ConnectorReconcileRequest(account_id, refresh_catalog) + + def to_dict(self) -> dict: + result: dict = {} + result["accountId"] = from_str(self.account_id) + if self.refresh_catalog is not None: + result["refreshCatalog"] = from_union([from_bool, from_none], self.refresh_catalog) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ContentExclusionCheckPathsRequest: @@ -16945,6 +17098,128 @@ def to_dict(self) -> dict: result["summary"] = from_union([from_str, from_none], self.summary) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorCapabilities: + """Feature detection and hard polling limits for the EXPERIMENTAL session connector API.""" + + api_version: int + """Connector API contract version.""" + + availability: ConnectorAvailability + """Current session availability. Disabled availability is reported without making a + Connector request. + """ + consent_continuation: bool + """Whether connect and reconnect can return an opaque continuation for bounded consent + polling. + """ + max_deadline_ms: int + """Maximum accepted wall-clock deadline in milliseconds for one continuation call.""" + + max_poll_attempts: int + """Maximum accepted polling attempts for one continuation call.""" + + max_poll_interval_ms: int + """Maximum accepted delay in milliseconds between polling attempts.""" + + opaque_account_selection: bool + """Whether callers select a host-owned GitHub account through an opaque selection ID rather + than supplying a provider token. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorCapabilities': + assert isinstance(obj, dict) + api_version = from_int(obj.get("apiVersion")) + availability = ConnectorAvailability(obj.get("availability")) + consent_continuation = from_bool(obj.get("consentContinuation")) + max_deadline_ms = from_int(obj.get("maxDeadlineMs")) + max_poll_attempts = from_int(obj.get("maxPollAttempts")) + max_poll_interval_ms = from_int(obj.get("maxPollIntervalMs")) + opaque_account_selection = from_bool(obj.get("opaqueAccountSelection")) + return ConnectorCapabilities(api_version, availability, consent_continuation, max_deadline_ms, max_poll_attempts, max_poll_interval_ms, opaque_account_selection) + + def to_dict(self) -> dict: + result: dict = {} + result["apiVersion"] = from_int(self.api_version) + result["availability"] = to_enum(ConnectorAvailability, self.availability) + result["consentContinuation"] = from_bool(self.consent_continuation) + result["maxDeadlineMs"] = from_int(self.max_deadline_ms) + result["maxPollAttempts"] = from_int(self.max_poll_attempts) + result["maxPollIntervalMs"] = from_int(self.max_poll_interval_ms) + result["opaqueAccountSelection"] = from_bool(self.opaque_account_selection) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorCatalogEntry: + """Credential-free Connector catalog entry.""" + + display_name: str + """Untrusted display label from the service.""" + + name: str + """Canonical Connector name used by lifecycle methods.""" + + runtime_server_ids: list[str] + """Opaque stable runtime IDs currently projected into the session for this Connector.""" + + status: ConnectorCatalogStatus + """Current authoritative service connection state.""" + + description: str | None = None + """Untrusted service description, when present.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorCatalogEntry': + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + name = from_str(obj.get("name")) + runtime_server_ids = from_list(from_str, obj.get("runtimeServerIds")) + status = ConnectorCatalogStatus(obj.get("status")) + description = from_union([from_str, from_none], obj.get("description")) + return ConnectorCatalogEntry(display_name, name, runtime_server_ids, status, description) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["name"] = from_str(self.name) + result["runtimeServerIds"] = from_list(from_str, self.runtime_server_ids) + result["status"] = to_enum(ConnectorCatalogStatus, self.status) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorRuntimeStatus: + """Live status of one session-owned MCP projection.""" + + connector_name: str + """Canonical Connector name that owns this server.""" + + runtime_server_id: str + """Opaque runtime server ID.""" + + status: ConnectorMCPStatus + """Current live MCP host status.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorRuntimeStatus': + assert isinstance(obj, dict) + connector_name = from_str(obj.get("connectorName")) + runtime_server_id = from_str(obj.get("runtimeServerId")) + status = ConnectorMCPStatus(obj.get("status")) + return ConnectorRuntimeStatus(connector_name, runtime_server_id, status) + + def to_dict(self) -> dict: + result: dict = {} + result["connectorName"] = from_str(self.connector_name) + result["runtimeServerId"] = from_str(self.runtime_server_id) + result["status"] = to_enum(ConnectorMCPStatus, self.status) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ContentExclusionCheckPathsResult: @@ -27198,6 +27473,37 @@ def to_dict(self) -> dict: result["sessionId"] = from_str(self.session_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorCatalogResult: + """Validated Connector catalog snapshot cached by the session. + + Latest validated catalog snapshot, when available. + """ + connectors: list[ConnectorCatalogEntry] + """Validated catalog entries in service order.""" + + refreshed_at_ms: int + """Unix epoch milliseconds when this snapshot was accepted.""" + + revision: int + """Monotonically increasing session-local catalog revision.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorCatalogResult': + assert isinstance(obj, dict) + connectors = from_list(ConnectorCatalogEntry.from_dict, obj.get("connectors")) + refreshed_at_ms = from_int(obj.get("refreshedAtMs")) + revision = from_int(obj.get("revision")) + return ConnectorCatalogResult(connectors, refreshed_at_ms, revision) + + def to_dict(self) -> dict: + result: dict = {} + result["connectors"] = from_list(lambda x: to_class(ConnectorCatalogEntry, x), self.connectors) + result["refreshedAtMs"] = from_int(self.refreshed_at_ms) + result["revision"] = from_int(self.revision) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CanvasHostContext: @@ -32022,6 +32328,57 @@ def to_dict(self) -> dict: result["commands"] = from_list(lambda x: to_class(SlashCommandInfo, x), self.commands) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorStatus: + """Fresh authoritative Connector state after MCP reconciliation. + + Authoritative session connector state. Account IDs are opaque routing identifiers and + credentials are never included. + + Fresh authoritative session state after removing Connector-owned MCP servers. + """ + api_version: int + """Connector API contract version.""" + + availability: ConnectorAvailability + """Current feature and session availability.""" + + pending_connections: int + """Number of active opaque connection continuations.""" + + runtime_servers: list[ConnectorRuntimeStatus] + """Live MCP status for every Connector-owned runtime server.""" + + account_id: str | None = None + """Opaque account selection pinned to this session, when one has been selected.""" + + catalog: ConnectorCatalogResult | None = None + """Latest validated catalog snapshot, when available.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorStatus': + assert isinstance(obj, dict) + api_version = from_int(obj.get("apiVersion")) + availability = ConnectorAvailability(obj.get("availability")) + pending_connections = from_int(obj.get("pendingConnections")) + runtime_servers = from_list(ConnectorRuntimeStatus.from_dict, obj.get("runtimeServers")) + account_id = from_union([from_str, from_none], obj.get("accountId")) + catalog = from_union([ConnectorCatalogResult.from_dict, from_none], obj.get("catalog")) + return ConnectorStatus(api_version, availability, pending_connections, runtime_servers, account_id, catalog) + + def to_dict(self) -> dict: + result: dict = {} + result["apiVersion"] = from_int(self.api_version) + result["availability"] = to_enum(ConnectorAvailability, self.availability) + result["pendingConnections"] = from_int(self.pending_connections) + result["runtimeServers"] = from_list(lambda x: to_class(ConnectorRuntimeStatus, x), self.runtime_servers) + if self.account_id is not None: + result["accountId"] = from_union([from_str, from_none], self.account_id) + if self.catalog is not None: + result["catalog"] = from_union([lambda x: to_class(ConnectorCatalogResult, x), from_none], self.catalog) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CanvasProviderCloseRequest: @@ -33588,6 +33945,77 @@ def to_dict(self) -> dict: result["plan"] = to_class(MCPInstallPlan, self.plan) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorConnectResult: + """Typed result of initiating or continuing a Connector connection. + + The service is connected and the session MCP graph was reconciled. + + Host-owned consent is required before bounded continuation can complete. + + The service is still completing the connection without a consent URL. + """ + kind: ConnectorConnectResultKind + """The service is connected and the session MCP graph was reconciled. + + Host-owned consent is required before bounded continuation can complete. + + The service is still completing the connection without a consent URL. + """ + status: ConnectorStatus | None = None + """Fresh authoritative Connector state after MCP reconciliation.""" + + consent_url: str | None = None + """Validated HTTPS consent URL. The runtime does not open it.""" + + continuation_id: str | None = None + """Opaque ID accepted by continueConnection.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorConnectResult': + assert isinstance(obj, dict) + kind = ConnectorConnectResultKind(obj.get("kind")) + status = from_union([ConnectorStatus.from_dict, from_none], obj.get("status")) + consent_url = from_union([from_str, from_none], obj.get("consentUrl")) + continuation_id = from_union([from_str, from_none], obj.get("continuationId")) + return ConnectorConnectResult(kind, status, consent_url, continuation_id) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(ConnectorConnectResultKind, self.kind) + if self.status is not None: + result["status"] = from_union([lambda x: to_class(ConnectorStatus, x), from_none], self.status) + if self.consent_url is not None: + result["consentUrl"] = from_union([from_str, from_none], self.consent_url) + if self.continuation_id is not None: + result["continuationId"] = from_union([from_str, from_none], self.continuation_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorDisconnectResult: + """Authoritative result after disconnect and MCP reconciliation.""" + + disconnected: bool + """Whether the service accepted the idempotent disconnect.""" + + status: ConnectorStatus + """Fresh authoritative session state after removing Connector-owned MCP servers.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorDisconnectResult': + assert isinstance(obj, dict) + disconnected = from_bool(obj.get("disconnected")) + status = ConnectorStatus.from_dict(obj.get("status")) + return ConnectorDisconnectResult(disconnected, status) + + def to_dict(self) -> dict: + result: dict = {} + result["disconnected"] = from_bool(self.disconnected) + result["status"] = to_class(ConnectorStatus, self.status) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class FactoryListRunsResult: @@ -38291,6 +38719,20 @@ class RPC: connected_remote_session_metadata: ConnectedRemoteSessionMetadata connected_remote_session_metadata_kind: ConnectedRemoteSessionMetadataKind connected_remote_session_metadata_repository: ConnectedRemoteSessionMetadataRepository + connector_account_request: ConnectorAccountRequest + connector_availability: ConnectorAvailability + connector_capabilities: ConnectorCapabilities + connector_catalog_entry: ConnectorCatalogEntry + connector_catalog_result: ConnectorCatalogResult + connector_catalog_status: ConnectorCatalogStatus + connector_connect_request: ConnectorConnectRequest + connector_connect_result: ConnectorConnectResult + connector_continue_request: ConnectorContinueRequest + connector_disconnect_result: ConnectorDisconnectResult + connector_mcp_status: ConnectorMCPStatus + connector_reconcile_request: ConnectorReconcileRequest + connector_runtime_status: ConnectorRuntimeStatus + connector_status: ConnectorStatus connect_remote_session_params: ConnectRemoteSessionParams connect_request: _ConnectRequest connect_result: _ConnectResult @@ -39566,6 +40008,20 @@ def from_dict(obj: Any) -> 'RPC': connected_remote_session_metadata = ConnectedRemoteSessionMetadata.from_dict(obj.get("ConnectedRemoteSessionMetadata")) connected_remote_session_metadata_kind = ConnectedRemoteSessionMetadataKind(obj.get("ConnectedRemoteSessionMetadataKind")) connected_remote_session_metadata_repository = ConnectedRemoteSessionMetadataRepository.from_dict(obj.get("ConnectedRemoteSessionMetadataRepository")) + connector_account_request = ConnectorAccountRequest.from_dict(obj.get("ConnectorAccountRequest")) + connector_availability = ConnectorAvailability(obj.get("ConnectorAvailability")) + connector_capabilities = ConnectorCapabilities.from_dict(obj.get("ConnectorCapabilities")) + connector_catalog_entry = ConnectorCatalogEntry.from_dict(obj.get("ConnectorCatalogEntry")) + connector_catalog_result = ConnectorCatalogResult.from_dict(obj.get("ConnectorCatalogResult")) + connector_catalog_status = ConnectorCatalogStatus(obj.get("ConnectorCatalogStatus")) + connector_connect_request = ConnectorConnectRequest.from_dict(obj.get("ConnectorConnectRequest")) + connector_connect_result = ConnectorConnectResult.from_dict(obj.get("ConnectorConnectResult")) + connector_continue_request = ConnectorContinueRequest.from_dict(obj.get("ConnectorContinueRequest")) + connector_disconnect_result = ConnectorDisconnectResult.from_dict(obj.get("ConnectorDisconnectResult")) + connector_mcp_status = ConnectorMCPStatus(obj.get("ConnectorMcpStatus")) + connector_reconcile_request = ConnectorReconcileRequest.from_dict(obj.get("ConnectorReconcileRequest")) + connector_runtime_status = ConnectorRuntimeStatus.from_dict(obj.get("ConnectorRuntimeStatus")) + connector_status = ConnectorStatus.from_dict(obj.get("ConnectorStatus")) connect_remote_session_params = ConnectRemoteSessionParams.from_dict(obj.get("ConnectRemoteSessionParams")) connect_request = _ConnectRequest.from_dict(obj.get("ConnectRequest")) connect_result = _ConnectResult.from_dict(obj.get("ConnectResult")) @@ -40667,7 +41123,7 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_pause_checkpoint_action, factory_pause_checkpoint_request, factory_pause_checkpoint_result, factory_pause_info, factory_pause_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_factory_pause_at_checkpoint_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connector_account_request, connector_availability, connector_capabilities, connector_catalog_entry, connector_catalog_result, connector_catalog_status, connector_connect_request, connector_connect_result, connector_continue_request, connector_disconnect_result, connector_mcp_status, connector_reconcile_request, connector_runtime_status, connector_status, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_pause_checkpoint_action, factory_pause_checkpoint_request, factory_pause_checkpoint_result, factory_pause_info, factory_pause_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_factory_pause_at_checkpoint_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -40841,6 +41297,20 @@ def to_dict(self) -> dict: result["ConnectedRemoteSessionMetadata"] = to_class(ConnectedRemoteSessionMetadata, self.connected_remote_session_metadata) result["ConnectedRemoteSessionMetadataKind"] = to_enum(ConnectedRemoteSessionMetadataKind, self.connected_remote_session_metadata_kind) result["ConnectedRemoteSessionMetadataRepository"] = to_class(ConnectedRemoteSessionMetadataRepository, self.connected_remote_session_metadata_repository) + result["ConnectorAccountRequest"] = to_class(ConnectorAccountRequest, self.connector_account_request) + result["ConnectorAvailability"] = to_enum(ConnectorAvailability, self.connector_availability) + result["ConnectorCapabilities"] = to_class(ConnectorCapabilities, self.connector_capabilities) + result["ConnectorCatalogEntry"] = to_class(ConnectorCatalogEntry, self.connector_catalog_entry) + result["ConnectorCatalogResult"] = to_class(ConnectorCatalogResult, self.connector_catalog_result) + result["ConnectorCatalogStatus"] = to_enum(ConnectorCatalogStatus, self.connector_catalog_status) + result["ConnectorConnectRequest"] = to_class(ConnectorConnectRequest, self.connector_connect_request) + result["ConnectorConnectResult"] = to_class(ConnectorConnectResult, self.connector_connect_result) + result["ConnectorContinueRequest"] = to_class(ConnectorContinueRequest, self.connector_continue_request) + result["ConnectorDisconnectResult"] = to_class(ConnectorDisconnectResult, self.connector_disconnect_result) + result["ConnectorMcpStatus"] = to_enum(ConnectorMCPStatus, self.connector_mcp_status) + result["ConnectorReconcileRequest"] = to_class(ConnectorReconcileRequest, self.connector_reconcile_request) + result["ConnectorRuntimeStatus"] = to_class(ConnectorRuntimeStatus, self.connector_runtime_status) + result["ConnectorStatus"] = to_class(ConnectorStatus, self.connector_status) result["ConnectRemoteSessionParams"] = to_class(ConnectRemoteSessionParams, self.connect_remote_session_params) result["ConnectRequest"] = to_class(_ConnectRequest, self.connect_request) result["ConnectResult"] = to_class(_ConnectResult, self.connect_result) @@ -44613,6 +45083,63 @@ async def stop(self, params: ScheduleStopRequest, *, timeout: float | None = Non return ScheduleStopResult.from_dict(await self._client.request("session.schedule.stop", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class ConnectorsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def get_capabilities(self, *, timeout: float | None = None) -> ConnectorCapabilities: + "Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request.\n\nReturns:\n Feature detection and hard polling limits for the EXPERIMENTAL session connector API." + return ConnectorCapabilities.from_dict(await self._client.request("session.connectors.getCapabilities", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def get_status(self, *, timeout: float | None = None) -> ConnectorStatus: + "Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request.\n\nReturns:\n Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included." + return ConnectorStatus.from_dict(await self._client.request("session.connectors.getStatus", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def list(self, params: ConnectorAccountRequest, *, timeout: float | None = None) -> ConnectorCatalogResult: + "Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog.\n\nArgs:\n params: Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted.\n\nReturns:\n Validated Connector catalog snapshot cached by the session." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ConnectorCatalogResult.from_dict(await self._client.request("session.connectors.list", params_dict, **_timeout_kwargs(timeout))) + + async def refresh(self, params: ConnectorAccountRequest, *, timeout: float | None = None) -> ConnectorCatalogResult: + "Refreshes and validates the Connector catalog for the pinned opaque account selection.\n\nArgs:\n params: Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted.\n\nReturns:\n Validated Connector catalog snapshot cached by the session." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ConnectorCatalogResult.from_dict(await self._client.request("session.connectors.refresh", params_dict, **_timeout_kwargs(timeout))) + + async def connect(self, params: ConnectorConnectRequest, *, timeout: float | None = None) -> ConnectorConnectResult: + "Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID.\n\nArgs:\n params: Selects one Connector and the pinned host-owned account used for its service and MCP authorization.\n\nReturns:\n Typed result of initiating or continuing a Connector connection." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ConnectorConnectResult.from_dict(await self._client.request("session.connectors.connect", params_dict, **_timeout_kwargs(timeout))) + + async def reconnect(self, params: ConnectorConnectRequest, *, timeout: float | None = None) -> ConnectorConnectResult: + "Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect.\n\nArgs:\n params: Selects one Connector and the pinned host-owned account used for its service and MCP authorization.\n\nReturns:\n Typed result of initiating or continuing a Connector connection." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ConnectorConnectResult.from_dict(await self._client.request("session.connectors.reconnect", params_dict, **_timeout_kwargs(timeout))) + + async def continue_connection(self, params: ConnectorContinueRequest, *, timeout: float | None = None) -> ConnectorConnectResult: + "Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL.\n\nArgs:\n params: Explicitly bounded continuation of a pending Connector connection.\n\nReturns:\n Typed result of initiating or continuing a Connector connection." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ConnectorConnectResult.from_dict(await self._client.request("session.connectors.continueConnection", params_dict, **_timeout_kwargs(timeout))) + + async def disconnect(self, params: ConnectorConnectRequest, *, timeout: float | None = None) -> ConnectorDisconnectResult: + "Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection.\n\nArgs:\n params: Selects one Connector and the pinned host-owned account used for its service and MCP authorization.\n\nReturns:\n Authoritative result after disconnect and MCP reconciliation." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ConnectorDisconnectResult.from_dict(await self._client.request("session.connectors.disconnect", params_dict, **_timeout_kwargs(timeout))) + + async def reconcile(self, params: ConnectorReconcileRequest, *, timeout: float | None = None) -> ConnectorStatus: + "Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status.\n\nArgs:\n params: Requests authoritative Connector-to-MCP reconciliation for the pinned account.\n\nReturns:\n Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ConnectorStatus.from_dict(await self._client.request("session.connectors.reconcile", params_dict, **_timeout_kwargs(timeout))) + + class SessionRpc: """Typed session-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient", session_id: str): @@ -44657,6 +45184,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.remote = RemoteApi(client, session_id) self.visibility = VisibilityApi(client, session_id) self.schedule = ScheduleApi(client, session_id) + self.connectors = ConnectorsApi(client, session_id) async def suspend(self, *, timeout: float | None = None) -> None: "Suspends the session while preserving persisted state for later resume.\n\n.. warning:: This API is experimental and may change or be removed in future versions." @@ -45526,6 +46054,22 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ConnectedRemoteSessionMetadata", "ConnectedRemoteSessionMetadataKind", "ConnectedRemoteSessionMetadataRepository", + "ConnectorAccountRequest", + "ConnectorAvailability", + "ConnectorCapabilities", + "ConnectorCatalogEntry", + "ConnectorCatalogResult", + "ConnectorCatalogStatus", + "ConnectorConnectRequest", + "ConnectorConnectResult", + "ConnectorConnectResultKind", + "ConnectorContinueRequest", + "ConnectorDisconnectResult", + "ConnectorMCPStatus", + "ConnectorReconcileRequest", + "ConnectorRuntimeStatus", + "ConnectorStatus", + "ConnectorsApi", "ContentExclusionApi", "ContentExclusionCheckPathsRequest", "ContentExclusionCheckPathsResult", diff --git a/python/copilot/session.py b/python/copilot/session.py index 4bc8de8944..4149aa5893 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -41,9 +41,6 @@ GitHubTokenAcquireResultKind, HandlePendingToolCallRequest, LogRequest, - MCPHeadersHandlePendingHeadersRefreshRequest, - MCPHeadersHandlePendingHeadersRefreshRequestKind, - MCPHeadersHandlePendingHeadersRefreshRequestRequest, MCPOauthHandlePendingRequest, MCPOauthPendingRequestResponse, ModelSwitchAutoTierResult, @@ -79,7 +76,6 @@ ElicitationRequestedData, ExternalToolCompletedData, ExternalToolRequestedData, - McpHeadersRefreshRequiredData, McpOauthRequiredData, PermissionRequest, PermissionRequestedData, @@ -554,41 +550,6 @@ class McpAuthContext(TypedDict): ] -class McpHeadersRefreshRequest(TypedDict): - """Connector MCP server whose short-lived HTTP headers need refreshing.""" - - server_key: str # Stable key from connector_mcp_servers. - server_url: str - reason: Literal["startup", "ttl-expired", "auth-failed"] - - -class McpHeadersRefreshResult(TypedDict): - """Connector service authorization headers and their optional cache lifetime.""" - - headers: dict[str, str] - ttl_ms: NotRequired[int] - - -class McpHeadersRefreshContext(TypedDict): - """Context for a catalog MCP headers refresh handler invocation.""" - - session_id: str - - -McpHeadersRefreshHandlerResult = McpHeadersRefreshResult | dict[str, str] | None -McpHeadersRefreshHandler = Callable[ - [McpHeadersRefreshRequest, McpHeadersRefreshContext], - McpHeadersRefreshHandlerResult | Awaitable[McpHeadersRefreshHandlerResult], -] - - -def _is_mcp_headers_refresh_result( - value: McpHeadersRefreshResult | dict[str, str], -) -> bool: - headers = value.get("headers") - return isinstance(headers, dict) and all(isinstance(header, str) for header in headers.values()) - - # ============================================================================ # User Input Request Types # ============================================================================ @@ -1255,36 +1216,6 @@ class MCPHTTPServerConfig(TypedDict, total=False): MCPServerConfig = MCPStdioServerConfig | MCPHTTPServerConfig -class ConnectorMCPServerConfig(TypedDict, total=False): - """Non-secret hosted MCP endpoint advertised for a connected Connector.""" - - display_name: Required[str] - url: Required[str] - tools: list[str] - timeout: int - authorization_cache_ttl_ms: int - - -def _connector_mcp_servers_to_wire( - servers: dict[str, ConnectorMCPServerConfig], -) -> dict[str, Any]: - """Convert Connector MCP configuration keys to the JSON-RPC wire casing.""" - wire: dict[str, Any] = {} - for name, config in servers.items(): - server: dict[str, Any] = { - "displayName": config["display_name"], - "url": config["url"], - } - if "tools" in config: - server["tools"] = config["tools"] - if "timeout" in config: - server["timeout"] = config["timeout"] - if "authorization_cache_ttl_ms" in config: - server["headersRefreshTtlMs"] = config["authorization_cache_ttl_ms"] - wire[name] = server - return wire - - class GitHubMcpToolConfig(TypedDict, total=False): """Configuration for the built-in GitHub MCP server. @@ -1700,8 +1631,6 @@ def __init__( self._permission_handler_lock = threading.Lock() self._mcp_auth_handler: McpAuthHandler | None = None self._mcp_auth_handler_lock = threading.Lock() - self._mcp_headers_refresh_handler: McpHeadersRefreshHandler | None = None - self._mcp_headers_refresh_handler_lock = threading.Lock() self._user_input_handler: UserInputHandler | None = None self._user_input_handler_lock = threading.Lock() self._exit_plan_mode_handler: ExitPlanModeHandler | None = None @@ -2196,23 +2125,6 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: request["staticClientConfig"] = static_client_config asyncio.ensure_future(self._execute_mcp_auth_and_respond(request, handler)) - case McpHeadersRefreshRequiredData() as data: - with self._mcp_headers_refresh_handler_lock: - handler = self._mcp_headers_refresh_handler - if not data.request_id or not handler: - return - asyncio.ensure_future( - self._execute_mcp_headers_refresh_and_respond( - data.request_id, - { - "server_key": data.server_name, - "server_url": data.server_url, - "reason": data.reason.value, - }, - handler, - ) - ) - case CommandExecuteData() as data: request_id = data.request_id command_name = data.command_name @@ -2520,68 +2432,6 @@ async def _execute_mcp_auth_and_respond( except (JsonRpcError, ProcessExitedError, OSError): pass # Connection lost or RPC error — nothing we can do - async def _execute_mcp_headers_refresh_and_respond( - self, - request_id: str, - request: McpHeadersRefreshRequest, - handler: McpHeadersRefreshHandler, - ) -> None: - """Execute an MCP headers refresh handler and respond via RPC.""" - try: - maybe_result = handler(request, {"session_id": self.session_id}) - if inspect.isawaitable(maybe_result): - result = cast(McpHeadersRefreshHandlerResult, await maybe_result) - else: - result = maybe_result - - if result is None: - rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( - kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.NONE - ) - elif _is_mcp_headers_refresh_result(result): - structured = cast(McpHeadersRefreshResult, result) - rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( - kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.HEADERS, - headers=structured["headers"], - ttl_ms=structured.get("ttl_ms"), - ) - else: - rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( - kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.HEADERS, - headers=cast(dict[str, str], result), - ) - except asyncio.CancelledError as exc: - message = str(exc) or "MCP headers refresh cancelled" - logger.warning( - "MCP headers refresh cancelled for %r: %s", - request["server_key"], - message, - ) - rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( - kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.ERROR, - message=message, - ) - except Exception as exc: - logger.warning( - "MCP headers refresh failed for %r: %s", - request["server_key"], - exc, - ) - rpc_result = MCPHeadersHandlePendingHeadersRefreshRequest( - kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.ERROR, - message=str(exc), - ) - - try: - await self.rpc.mcp.headers.handle_pending_headers_refresh_request( - MCPHeadersHandlePendingHeadersRefreshRequestRequest( - request_id=request_id, - result=rpc_result, - ) - ) - except (JsonRpcError, ProcessExitedError, OSError): - pass # The runtime connection is gone, so the pending request cannot be answered. - async def _execute_command_and_respond( self, request_id: str, @@ -2771,13 +2621,6 @@ def _register_mcp_auth_handler(self, handler: McpAuthHandler | None) -> None: with self._mcp_auth_handler_lock: self._mcp_auth_handler = handler - def _register_mcp_headers_refresh_handler( - self, handler: McpHeadersRefreshHandler | None - ) -> None: - """Register the MCP dynamic-headers handler for this session.""" - with self._mcp_headers_refresh_handler_lock: - self._mcp_headers_refresh_handler = handler - def _register_exit_plan_mode_handler(self, handler: ExitPlanModeHandler | None) -> None: """Register the exit-plan-mode handler for this session.""" with self._exit_plan_mode_handler_lock: diff --git a/python/test_client.py b/python/test_client.py index 26ea845923..775d54a5db 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -17,7 +17,6 @@ from copilot import ( CanvasProviderIdentity, CapiSessionOptions, - ConnectorMCPServerConfig, CopilotClient, DisableBypassPermissionsModes, ExtensionInfo, @@ -44,8 +43,6 @@ from copilot.generated.rpc import AutoTier as AutoTierEnum from copilot.session import CopilotSession, PermissionHandler from copilot.session_events import ( - McpHeadersRefreshRequiredData, - McpHeadersRefreshRequiredReason, McpOauthRequestReason, McpOauthRequiredData, McpOauthRequiredStaticClientConfig, @@ -695,173 +692,6 @@ def handle_mcp_auth_request(request, invocation): finally: await client.force_stop() - @pytest.mark.asyncio - async def test_mcp_headers_refresh_dispatches_ttl_and_broker_errors(self): - transport = Mock() - transport.request = AsyncMock(return_value={"success": True}) - session = CopilotSession("managed-session", transport) - responses = [ - {"Authorization": "Bearer short-lived"}, - { - "headers": {"Authorization": "Bearer refreshed"}, - "ttl_ms": 5_000, - }, - None, - ] - - async def handle_refresh(request, context): - assert request == { - "server_key": "github", - "server_url": "https://example.com/mcp", - "reason": "startup", - } - assert context == {"session_id": "managed-session"} - return responses.pop(0) - - session._register_mcp_headers_refresh_handler(handle_refresh) - event = SessionEvent( - data=McpHeadersRefreshRequiredData( - request_id="refresh-1", - server_name="github", - server_url="https://example.com/mcp", - reason=McpHeadersRefreshRequiredReason.STARTUP, - ), - id="evt-refresh", - timestamp="2026-01-01T00:00:00Z", - type=SessionEventType.MCP_HEADERS_REFRESH_REQUIRED, - ephemeral=True, - parent_id=None, - ) - session._dispatch_event(event) - for _ in range(200): - if transport.request.await_count == 1: - break - await asyncio.sleep(0.005) - - await session._execute_mcp_headers_refresh_and_respond( - "refresh-2", - { - "server_key": "github", - "server_url": "https://example.com/mcp", - "reason": "startup", - }, - handle_refresh, - ) - await session._execute_mcp_headers_refresh_and_respond( - "refresh-3", - { - "server_key": "github", - "server_url": "https://example.com/mcp", - "reason": "startup", - }, - handle_refresh, - ) - - def fail_refresh(_request, _context): - raise RuntimeError("credential revoked") - - await session._execute_mcp_headers_refresh_and_respond( - "refresh-4", - { - "server_key": "github", - "server_url": "https://example.com/mcp", - "reason": "auth-failed", - }, - fail_refresh, - ) - await session._execute_mcp_headers_refresh_and_respond( - "refresh-5", - { - "server_key": "github", - "server_url": "https://example.com/mcp", - "reason": "auth-failed", - }, - lambda _request, _context: (_ for _ in ()).throw(asyncio.CancelledError()), - ) - - results = [call.args[1]["result"] for call in transport.request.await_args_list] - assert results == [ - { - "kind": "headers", - "headers": {"Authorization": "Bearer short-lived"}, - }, - { - "kind": "headers", - "headers": {"Authorization": "Bearer refreshed"}, - "ttlMs": 5_000, - }, - {"kind": "none"}, - {"kind": "error", "message": "credential revoked"}, - {"kind": "error", "message": "MCP headers refresh cancelled"}, - ] - - @pytest.mark.asyncio - async def test_create_and_resume_map_connector_mcp_servers_to_wire(self): - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - await client.start() - try: - captured: list[tuple[str, dict]] = [] - - async def mock_request(method, params, **kwargs): - captured.append((method, params)) - if method in ("session.create", "session.resume"): - result = {"sessionId": params["sessionId"], "workspacePath": None} - callback = kwargs.get("on_response_inline") - if callback is not None: - callback(result) - return result - if method == "session.eventLog.registerInterest": - return {"id": "interest-1"} - return {} - - client._client.request = mock_request - connectors: dict[str, ConnectorMCPServerConfig] = { - "github": { - "display_name": "GitHub", - "url": "https://example.com/mcp", - "tools": ["issues"], - "timeout": 30_000, - "authorization_cache_ttl_ms": 60_000, - } - } - - session = await client.create_session( - connector_mcp_servers=connectors, - on_mcp_headers_refresh=lambda _request, _context: None, - ) - await client.resume_session( - session.session_id, - connector_mcp_servers=connectors, - on_mcp_headers_refresh=lambda _request, _context: None, - ) - - requests = { - method: params - for method, params in captured - if method in ("session.create", "session.resume") - } - expected = { - "github": { - "displayName": "GitHub", - "url": "https://example.com/mcp", - "tools": ["issues"], - "timeout": 30_000, - "headersRefreshTtlMs": 60_000, - } - } - assert requests["session.create"]["managedMcpServers"] == expected - assert requests["session.resume"]["managedMcpServers"] == expected - assert ( - sum( - method == "session.eventLog.registerInterest" - and params["eventType"] == "mcp.headers_refresh_required" - for method, params in captured - ) - == 2 - ) - finally: - await client.force_stop() - @pytest.mark.asyncio async def test_create_session_forwards_cloud_options(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/python/test_connectors.py b/python/test_connectors.py new file mode 100644 index 0000000000..a7e0d8906e --- /dev/null +++ b/python/test_connectors.py @@ -0,0 +1,101 @@ +from unittest.mock import AsyncMock, Mock + +import pytest + +from copilot.generated.rpc import ( + ConnectorAccountRequest, + ConnectorAvailability, + ConnectorConnectRequest, + ConnectorConnectResultKind, + ConnectorContinueRequest, + ConnectorReconcileRequest, +) +from copilot.session import CopilotSession + + +@pytest.mark.asyncio +async def test_session_connectors_exposes_complete_runtime_api(): + transport = Mock() + transport.request = AsyncMock(side_effect=connector_response) + session = CopilotSession("session-1", transport) + + capabilities = await session.rpc.connectors.get_capabilities() + assert capabilities.availability is ConnectorAvailability.ENABLED + assert (await session.rpc.connectors.get_status()).api_version == 1 + assert ( + await session.rpc.connectors.list(ConnectorAccountRequest(account_id="account-1")) + ).revision == 1 + assert ( + await session.rpc.connectors.refresh(ConnectorAccountRequest(account_id="account-1")) + ).revision == 1 + + request = ConnectorConnectRequest(account_id="account-1", connector_name="calendar") + assert ( + await session.rpc.connectors.connect(request) + ).kind is ConnectorConnectResultKind.CONSENT_REQUIRED + assert ( + await session.rpc.connectors.reconnect(request) + ).kind is ConnectorConnectResultKind.PENDING + assert ( + await session.rpc.connectors.continue_connection( + ConnectorContinueRequest( + continuation_id="continuation-1", + max_attempts=3, + poll_interval_ms=100, + deadline_ms=1_000, + ) + ) + ).kind is ConnectorConnectResultKind.PENDING + assert (await session.rpc.connectors.disconnect(request)).disconnected + assert ( + await session.rpc.connectors.reconcile( + ConnectorReconcileRequest(account_id="account-1", refresh_catalog=True) + ) + ).api_version == 1 + + assert [call.args[0] for call in transport.request.await_args_list] == [ + "session.connectors.getCapabilities", + "session.connectors.getStatus", + "session.connectors.list", + "session.connectors.refresh", + "session.connectors.connect", + "session.connectors.reconnect", + "session.connectors.continueConnection", + "session.connectors.disconnect", + "session.connectors.reconcile", + ] + + +def connector_response(method, _params, **_kwargs): + if method == "session.connectors.getCapabilities": + return { + "apiVersion": 1, + "availability": "enabled", + "consentContinuation": True, + "opaqueAccountSelection": True, + "maxPollAttempts": 30, + "maxPollIntervalMs": 2_000, + "maxDeadlineMs": 60_000, + } + if method in ("session.connectors.list", "session.connectors.refresh"): + return {"revision": 1, "refreshedAtMs": 1, "connectors": []} + if method == "session.connectors.connect": + return { + "kind": "consent_required", + "consentUrl": "https://example.com/consent", + "continuationId": "continuation-1", + } + if method in ("session.connectors.reconnect", "session.connectors.continueConnection"): + return {"kind": "pending", "continuationId": "continuation-1"} + if method == "session.connectors.disconnect": + return {"disconnected": True, "status": connector_status()} + return connector_status() + + +def connector_status(): + return { + "apiVersion": 1, + "availability": "enabled", + "runtimeServers": [], + "pendingConnections": 0, + } diff --git a/rust/README.md b/rust/README.md index 49e6db6609..e78154fc73 100644 --- a/rust/README.md +++ b/rust/README.md @@ -257,19 +257,64 @@ New RPCs land in the namespace immediately as the schema regenerates; helpers are added on top only when an ergonomic story is worth the maintenance. +#### Connectors (experimental) + +When the runtime's Connector experiment is enabled, use the generated +session-scoped namespace to discover and manage Connectors: + +```rust,ignore +use github_copilot_sdk::rpc::{ + ConnectorAccountRequest, ConnectorAvailability, ConnectorConnectRequest, + ConnectorReconcileRequest, +}; + +let capabilities = session.rpc().connectors().get_capabilities().await?; +if capabilities.availability == ConnectorAvailability::Enabled { + let account_id = "selected-account".to_string(); + let catalog = session + .rpc() + .connectors() + .list(ConnectorAccountRequest { + account_id: account_id.clone(), + }) + .await?; + session + .rpc() + .connectors() + .reconcile(ConnectorReconcileRequest { + account_id: account_id.clone(), + refresh_catalog: None, + }) + .await?; + let connection = session + .rpc() + .connectors() + .connect(ConnectorConnectRequest { + account_id, + connector_name: catalog.connectors[0].name.clone(), + }) + .await?; +} +``` + +The namespace also provides `get_status`, `refresh`, `reconnect`, +`continue_connection`, `disconnect`, and `reconcile`. Hosts select an opaque +GitHub account ID and own consent and browser UI. Connector methods accept only +that account ID, never credentials or provider tokens; the runtime returns +validated consent URLs and owns MCP reconciliation. This API is experimental, +so check its reported availability before use. + ### Handler Traits -The SDK exposes seven focused handler traits, one per CLI callback type. Implement only the traits you need and install each with the matching `SessionConfig` setter. Each trait has a single `async fn handle(...)` method: +The SDK exposes five focused handler traits, one per CLI callback type. Implement only the traits you need and install each with the matching `SessionConfig` setter. Each trait has a single `async fn handle(...)` method: -| Trait | Setter | Purpose | -| -------------------------- | ------------------------------------- | -------------------------------------------- | -| `PermissionHandler` | `with_permission_handler(...)` | Approve/deny tool-use permission requests | -| `ElicitationHandler` | `with_elicitation_handler(...)` | Respond to structured elicitation prompts | -| `McpAuthHandler` | `with_mcp_auth_handler(...)` | Supply host-provided MCP OAuth tokens | -| `McpHeadersRefreshHandler` | `with_mcp_headers_handler(...)` | Supply short-lived Copilot Connectors service authorization | -| `UserInputHandler` | `with_user_input_handler(...)` | Answer free-form / choice user-input prompts | -| `ExitPlanModeHandler` | `with_exit_plan_mode_handler(...)` | Respond when the agent exits plan mode | -| `AutoModeSwitchHandler` | `with_auto_mode_switch_handler(...)` | Respond to automatic mode-switch proposals | +| Trait | Setter | Purpose | +| ----------------------- | --------------------------------- | --------------------------------------------- | +| `PermissionHandler` | `with_permission_handler(...)` | Approve/deny tool-use permission requests | +| `ElicitationHandler` | `with_elicitation_handler(...)` | Respond to structured elicitation prompts | +| `UserInputHandler` | `with_user_input_handler(...)` | Answer free-form / choice user-input prompts | +| `ExitPlanModeHandler` | `with_exit_plan_mode_handler(...)`| Respond when the agent exits plan mode | +| `AutoModeSwitchHandler` | `with_auto_mode_switch_handler(...)`| Respond to automatic mode-switch proposals | The CLI's `requestPermission` / `requestElicitation` / `requestUserInput` / etc. wire flags are derived automatically from which traits you've installed — clients that don't install a handler are silently skipped, letting another connected client handle the request. @@ -336,80 +381,6 @@ Use `with_ask_user_variant(AskUserVariant::Elicitation)` together with tool. The default remains `AskUserVariant::Legacy`. Re-supply the option and handler through `ResumeSessionConfig` on a cold resume. -#### Connector MCP servers - -Use `ConnectorMcpServerConfig` for connected Copilot Connector MCP endpoints -supplied from the service catalog. Map keys are stable Connector server keys; -short-lived client-to-Copilot-Connectors service authorization comes from -`McpHeadersRefreshHandler` rather than the configuration. This is normally the -selected account's GitHub bearer authorization. The host does not supply -Outlook, Slack, or other downstream provider tokens; the Connector service owns -those provider tokens. The callback's `server_key` is the stable map key (for -example, `"github-enterprise"`), not the human-readable `display_name` or -Connector service ID. - -```rust,ignore -use std::collections::HashMap; -use std::sync::Arc; - -use github_copilot_sdk::{ConnectorMcpServerConfig, SessionConfig}; - -let connectors = HashMap::from([( - "github-enterprise".to_string(), - ConnectorMcpServerConfig { - display_name: "GitHub Enterprise".to_string(), - url: "https://mcp.example.com/".to_string(), - tools: Some(vec!["issues".to_string(), "pull_requests".to_string()]), - timeout: Some(30_000), - authorization_cache_ttl_ms: Some(60_000), - }, -)]); - -let config = SessionConfig::default() - .with_connector_mcp_servers(connectors) - .with_mcp_headers_handler(Arc::new(MyMcpHeadersHandler)); -``` - -The generic `McpHeaders*` API names describe the remote-header protocol, but -the handler is for connected Copilot Connector MCP endpoints; ordinary -`mcp_servers` entries do not opt into dynamic refresh. -`authorization_cache_ttl_ms` caps how long the runtime may reuse Connector -service authorization; the handler result's `ttl_ms` reports that -authorization's remaining lifetime. - -> **Same-session Connector reconciliation is not available through the current -> public runtime contract.** Generic MCP start, restart, and stop operations -> only change transient running state; a reload can restore an endpoint from -> the session's original configuration. Do not map Connector service Connect -> or Disconnect to those operations. To change the connected set, create or -> cold-resume a session with the new `connector_mcp_servers` map until the -> runtime exposes public authoritative replace/remove operations. - -The public live-session MCP observation and availability surface remains: - -| Capability | Rust API | -| --- | --- | -| List servers and current status | `session.rpc().mcp().list()` | -| Enable a server | `session.rpc().mcp().enable(rpc::McpEnableRequest { ... })` | -| Disable a server | `session.rpc().mcp().disable(rpc::McpDisableRequest { ... })` | -| List connected-server tools | `session.rpc().mcp().list_tools(rpc::McpListToolsRequest { ... })` | -| Observe status changes | `session.subscribe()` plus `SessionEvent::parsed_type()` / `typed_data()` | - -The generated MCP request and status-event fields named `server_name`, plus -`rpc::McpServer::name`, carry the stable Connector server key. The separate -`display_name` is human-readable. Relevant typed event payloads are -`SessionMcpServersLoadedData`, `SessionMcpServerStatusChangedData`, -`SessionMcpServerNeedsReconnectData`, and `SessionMcpServerRemovedData` from -`github_copilot_sdk::session_events`. - -The host owns service-catalog discovery, Connector consent, external service -HTTP calls, and all browser or UI flows. The SDK does not add polling for these -operations. - -On cold resume, re-supply both the connected endpoints with -`ResumeSessionConfig::with_connector_mcp_servers` and the handler with -`with_mcp_headers_handler`. - For rotating per-session GitHub credentials, install a `GitHubTokenProvider` instead of setting `github_token`: diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 3e8b45a72b..734e60cb12 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -771,6 +771,24 @@ pub mod rpc_methods { pub const SESSION_SCHEDULE_REARMSELFPACED: &str = "session.schedule.rearmSelfPaced"; /// `session.schedule.stop` pub const SESSION_SCHEDULE_STOP: &str = "session.schedule.stop"; + /// `session.connectors.getCapabilities` + pub const SESSION_CONNECTORS_GETCAPABILITIES: &str = "session.connectors.getCapabilities"; + /// `session.connectors.getStatus` + pub const SESSION_CONNECTORS_GETSTATUS: &str = "session.connectors.getStatus"; + /// `session.connectors.list` + pub const SESSION_CONNECTORS_LIST: &str = "session.connectors.list"; + /// `session.connectors.refresh` + pub const SESSION_CONNECTORS_REFRESH: &str = "session.connectors.refresh"; + /// `session.connectors.connect` + pub const SESSION_CONNECTORS_CONNECT: &str = "session.connectors.connect"; + /// `session.connectors.reconnect` + pub const SESSION_CONNECTORS_RECONNECT: &str = "session.connectors.reconnect"; + /// `session.connectors.continueConnection` + pub const SESSION_CONNECTORS_CONTINUECONNECTION: &str = "session.connectors.continueConnection"; + /// `session.connectors.disconnect` + pub const SESSION_CONNECTORS_DISCONNECT: &str = "session.connectors.disconnect"; + /// `session.connectors.reconcile` + pub const SESSION_CONNECTORS_RECONCILE: &str = "session.connectors.reconcile"; /// `skillProvider.list` pub const SKILLPROVIDER_LIST: &str = "skillProvider.list"; /// `skillProvider.read` @@ -22823,6 +22841,263 @@ pub struct WorkspacesWriteAutopilotObjectiveResult { pub operation: String, } +/// Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. +/// +///

+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorAccountRequest { + /// Opaque account selection ID previously returned by an account discovery API. + pub account_id: String, +} + +/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorCapabilities { + /// Connector API contract version. + pub api_version: i64, + /// Current session availability. Disabled availability is reported without making a Connector request. + pub availability: ConnectorAvailability, + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + pub consent_continuation: bool, + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + pub max_deadline_ms: i64, + /// Maximum accepted polling attempts for one continuation call. + pub max_poll_attempts: i64, + /// Maximum accepted delay in milliseconds between polling attempts. + pub max_poll_interval_ms: i64, + /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + pub opaque_account_selection: bool, +} + +/// Credential-free Connector catalog entry. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorCatalogEntry { + /// Untrusted service description, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Untrusted display label from the service. + pub display_name: String, + /// Canonical Connector name used by lifecycle methods. + pub name: String, + /// Opaque stable runtime IDs currently projected into the session for this Connector. + pub runtime_server_ids: Vec, + /// Current authoritative service connection state. + pub status: ConnectorCatalogStatus, +} + +/// Validated Connector catalog snapshot cached by the session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorCatalogResult { + /// Validated catalog entries in service order. + pub connectors: Vec, + /// Unix epoch milliseconds when this snapshot was accepted. + pub refreshed_at_ms: i64, + /// Monotonically increasing session-local catalog revision. + pub revision: i64, +} + +/// Selects one Connector and the pinned host-owned account used for its service and MCP authorization. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorConnectRequest { + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + pub account_id: String, + /// Canonical Connector name from the current catalog. + pub connector_name: String, +} + +/// Live status of one session-owned MCP projection. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorRuntimeStatus { + /// Canonical Connector name that owns this server. + pub connector_name: String, + /// Opaque runtime server ID. + pub runtime_server_id: String, + /// Current live MCP host status. + pub status: ConnectorMcpStatus, +} + +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorStatus { + /// Opaque account selection pinned to this session, when one has been selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + /// Connector API contract version. + pub api_version: i64, + /// Current feature and session availability. + pub availability: ConnectorAvailability, + /// Latest validated catalog snapshot, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub catalog: Option, + /// Number of active opaque connection continuations. + pub pending_connections: i64, + /// Live MCP status for every Connector-owned runtime server. + pub runtime_servers: Vec, +} + +/// The service is connected and the session MCP graph was reconciled. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorConnectResultConnected { + /// The service is connected and the session MCP graph was reconciled. + pub kind: ConnectorConnectResultConnectedKind, + /// Fresh authoritative Connector state after MCP reconciliation. + pub status: ConnectorStatus, +} + +/// Host-owned consent is required before bounded continuation can complete. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorConnectResultConsentRequired { + /// Validated HTTPS consent URL. The runtime does not open it. + pub consent_url: String, + /// Opaque ID accepted by continueConnection. + pub continuation_id: String, + /// Host-owned consent is required before bounded continuation can complete. + pub kind: ConnectorConnectResultConsentRequiredKind, +} + +/// The service is still completing the connection without a consent URL. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorConnectResultPending { + /// Opaque ID accepted by continueConnection. + pub continuation_id: String, + /// The service is still completing the connection without a consent URL. + pub kind: ConnectorConnectResultPendingKind, +} + +/// Explicitly bounded continuation of a pending Connector connection. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorContinueRequest { + /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + pub continuation_id: String, + /// Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. + pub deadline_ms: i64, + /// Maximum catalog requests made by this call. Must be between one and the capability limit. + pub max_attempts: i64, + /// Delay in milliseconds between attempts. Must not exceed the capability limit. + pub poll_interval_ms: i64, +} + +/// Authoritative result after disconnect and MCP reconciliation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorDisconnectResult { + /// Whether the service accepted the idempotent disconnect. + pub disconnected: bool, + /// Fresh authoritative session state after removing Connector-owned MCP servers. + pub status: ConnectorStatus, +} + +/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectorReconcileRequest { + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + pub account_id: String, + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_catalog: Option, +} + /// List of Copilot models available to the resolved user, including capabilities and billing metadata. /// ///
@@ -28737,6 +29012,172 @@ pub struct SessionScheduleStopResult { pub entry: Option, } +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsGetCapabilitiesParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsGetCapabilitiesResult { + /// Connector API contract version. + pub api_version: i64, + /// Current session availability. Disabled availability is reported without making a Connector request. + pub availability: ConnectorAvailability, + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + pub consent_continuation: bool, + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + pub max_deadline_ms: i64, + /// Maximum accepted polling attempts for one continuation call. + pub max_poll_attempts: i64, + /// Maximum accepted delay in milliseconds between polling attempts. + pub max_poll_interval_ms: i64, + /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + pub opaque_account_selection: bool, +} + +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsGetStatusParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsGetStatusResult { + /// Opaque account selection pinned to this session, when one has been selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + /// Connector API contract version. + pub api_version: i64, + /// Current feature and session availability. + pub availability: ConnectorAvailability, + /// Latest validated catalog snapshot, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub catalog: Option, + /// Number of active opaque connection continuations. + pub pending_connections: i64, + /// Live MCP status for every Connector-owned runtime server. + pub runtime_servers: Vec, +} + +/// Validated Connector catalog snapshot cached by the session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsListResult { + /// Validated catalog entries in service order. + pub connectors: Vec, + /// Unix epoch milliseconds when this snapshot was accepted. + pub refreshed_at_ms: i64, + /// Monotonically increasing session-local catalog revision. + pub revision: i64, +} + +/// Validated Connector catalog snapshot cached by the session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsRefreshResult { + /// Validated catalog entries in service order. + pub connectors: Vec, + /// Unix epoch milliseconds when this snapshot was accepted. + pub refreshed_at_ms: i64, + /// Monotonically increasing session-local catalog revision. + pub revision: i64, +} + +/// Authoritative result after disconnect and MCP reconciliation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsDisconnectResult { + /// Whether the service accepted the idempotent disconnect. + pub disconnected: bool, + /// Fresh authoritative session state after removing Connector-owned MCP servers. + pub status: ConnectorStatus, +} + +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConnectorsReconcileResult { + /// Opaque account selection pinned to this session, when one has been selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + /// Connector API contract version. + pub api_version: i64, + /// Current feature and session availability. + pub availability: ConnectorAvailability, + /// Latest validated catalog snapshot, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub catalog: Option, + /// Number of active opaque connection continuations. + pub pending_connections: i64, + /// Live MCP status for every Connector-owned runtime server. + pub runtime_servers: Vec, +} + /// Identifies the target session. /// ///
@@ -36128,3 +36569,136 @@ pub enum WorkspacesWorkspaceDetailsHostType { #[serde(other)] Unknown, } + +/// Availability of the EXPERIMENTAL session connector API. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorAvailability { + /// The resolved Connector feature is enabled and Connector requests are permitted. + #[serde(rename = "enabled")] + Enabled, + /// The resolved Connector feature is off. No Connector service request is made while disabled. + #[serde(rename = "disabled")] + Disabled, + /// The session has no eligible host-owned GitHub account or does not support local Connector projection. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Authoritative service connection state for one Connector. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorCatalogStatus { + /// The Connector is available but not connected. + #[serde(rename = "not_connected")] + NotConnected, + /// The Connector service is still completing connection or consent. + #[serde(rename = "pending")] + Pending, + /// The Connector is connected and may contribute MCP servers. + #[serde(rename = "connected")] + Connected, + /// The Connector service reports an unusable connection. + #[serde(rename = "error")] + Error, + /// The service returned a future or unrecognized state. + #[serde(rename = "unknown")] + UnknownValue, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// The service is connected and the session MCP graph was reconciled. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorConnectResultConnectedKind { + #[serde(rename = "connected")] + #[default] + Connected, +} + +/// Live MCP status of one Connector-owned runtime server. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorMcpStatus { + /// The server is connected and its tools are available. + #[serde(rename = "connected")] + Connected, + /// The server connection is still being established. + #[serde(rename = "pending")] + Pending, + /// The server requires refreshed GitHub authorization. + #[serde(rename = "needs_auth")] + NeedsAuth, + /// The server failed to connect or initialize. + #[serde(rename = "failed")] + Failed, + /// The server is intentionally stopped, including when managed policy blocks it. + #[serde(rename = "stopped")] + Stopped, + /// The server is configured but explicitly disabled. + #[serde(rename = "disabled")] + Disabled, + /// The Connector currently has no live server configuration. + #[serde(rename = "not_configured")] + NotConfigured, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Host-owned consent is required before bounded continuation can complete. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorConnectResultConsentRequiredKind { + #[serde(rename = "consent_required")] + #[default] + ConsentRequired, +} + +/// The service is still completing the connection without a consent URL. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorConnectResultPendingKind { + #[serde(rename = "pending")] + #[default] + Pending, +} + +/// Typed result of initiating or continuing a Connector connection. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ConnectorConnectResult { + Connected(ConnectorConnectResultConnected), + ConsentRequired(ConnectorConnectResultConsentRequired), + Pending(ConnectorConnectResultPending), +} diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index bbde719b6a..098049dfe0 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -3125,6 +3125,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.connectors.*` sub-namespace. + pub fn connectors(&self) -> SessionRpcConnectors<'a> { + SessionRpcConnectors { + session: self.session, + } + } + /// `session.contentExclusion.*` sub-namespace. pub fn content_exclusion(&self) -> SessionRpcContentExclusion<'a> { SessionRpcContentExclusion { @@ -4447,6 +4454,304 @@ impl<'a> SessionRpcCompletions<'a> { } } +/// `session.connectors.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcConnectors<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcConnectors<'a> { + /// Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. + /// + /// Wire method: `session.connectors.getCapabilities`. + /// + /// # Returns + /// + /// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_capabilities(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_CONNECTORS_GETCAPABILITIES, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. + /// + /// Wire method: `session.connectors.getStatus`. + /// + /// # Returns + /// + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_status(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CONNECTORS_GETSTATUS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. + /// + /// Wire method: `session.connectors.list`. + /// + /// # Parameters + /// + /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + /// + /// # Returns + /// + /// Validated Connector catalog snapshot cached by the session. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn list( + &self, + params: ConnectorAccountRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CONNECTORS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Refreshes and validates the Connector catalog for the pinned opaque account selection. + /// + /// Wire method: `session.connectors.refresh`. + /// + /// # Parameters + /// + /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + /// + /// # Returns + /// + /// Validated Connector catalog snapshot cached by the session. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn refresh( + &self, + params: ConnectorAccountRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CONNECTORS_REFRESH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. + /// + /// Wire method: `session.connectors.connect`. + /// + /// # Parameters + /// + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + /// + /// # Returns + /// + /// Typed result of initiating or continuing a Connector connection. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn connect( + &self, + params: ConnectorConnectRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CONNECTORS_CONNECT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. + /// + /// Wire method: `session.connectors.reconnect`. + /// + /// # Parameters + /// + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + /// + /// # Returns + /// + /// Typed result of initiating or continuing a Connector connection. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn reconnect( + &self, + params: ConnectorConnectRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CONNECTORS_RECONNECT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. + /// + /// Wire method: `session.connectors.continueConnection`. + /// + /// # Parameters + /// + /// * `params` - Explicitly bounded continuation of a pending Connector connection. + /// + /// # Returns + /// + /// Typed result of initiating or continuing a Connector connection. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn continue_connection( + &self, + params: ConnectorContinueRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_CONNECTORS_CONTINUECONNECTION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. + /// + /// Wire method: `session.connectors.disconnect`. + /// + /// # Parameters + /// + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + /// + /// # Returns + /// + /// Authoritative result after disconnect and MCP reconciliation. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn disconnect( + &self, + params: ConnectorConnectRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_CONNECTORS_DISCONNECT, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. + /// + /// Wire method: `session.connectors.reconcile`. + /// + /// # Parameters + /// + /// * `params` - Requests authoritative Connector-to-MCP reconciliation for the pinned account. + /// + /// # Returns + /// + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn reconcile( + &self, + params: ConnectorReconcileRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_CONNECTORS_RECONCILE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.contentExclusion.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcContentExclusion<'a> { diff --git a/rust/src/handler.rs b/rust/src/handler.rs index cdb46b3f2b..61d9b192cb 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -2,7 +2,7 @@ //! //! Each callback the CLI may dispatch (permission requests, elicitation //! prompts, user-input questions, exit-plan-mode prompts, -//! auto-mode-switch prompts, MCP authorization requests) has its own focused trait with a single +//! auto-mode-switch prompts) has its own focused trait with a single //! `handle` method. //! //! Handlers are **optional**: install only the ones the application cares @@ -15,27 +15,17 @@ //! [`Tool::with_handler`](crate::types::Tool::with_handler) on entries passed to //! [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools). -use std::collections::HashMap; - use async_trait::async_trait; use serde::{Deserialize, Serialize}; use crate::generated::api_types::{ - McpHeadersHandlePendingHeadersRefreshRequest, - McpHeadersHandlePendingHeadersRefreshRequestError, - McpHeadersHandlePendingHeadersRefreshRequestErrorKind, - McpHeadersHandlePendingHeadersRefreshRequestHeaders, - McpHeadersHandlePendingHeadersRefreshRequestHeadersKind, - McpHeadersHandlePendingHeadersRefreshRequestNone, - McpHeadersHandlePendingHeadersRefreshRequestNoneKind, McpOauthPendingRequestResponse, - McpOauthPendingRequestResponseCancelled, McpOauthPendingRequestResponseCancelledKind, - McpOauthPendingRequestResponseToken, McpOauthPendingRequestResponseTokenKind, - PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionContext, - PermissionDecisionReject, PermissionDecisionUserNotAvailable, + McpOauthPendingRequestResponse, McpOauthPendingRequestResponseCancelled, + McpOauthPendingRequestResponseCancelledKind, McpOauthPendingRequestResponseToken, + McpOauthPendingRequestResponseTokenKind, PermissionDecision, PermissionDecisionApproveOnce, + PermissionDecisionContext, PermissionDecisionReject, PermissionDecisionUserNotAvailable, }; use crate::session_events::{ - McpHeadersRefreshRequiredReason, McpOauthRequestReason, McpOauthRequiredStaticClientConfig, - McpOauthWWWAuthenticateParams, + McpOauthRequestReason, McpOauthRequiredStaticClientConfig, McpOauthWWWAuthenticateParams, }; use crate::types::{ ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId, @@ -304,83 +294,6 @@ pub trait McpAuthHandler: Send + Sync + 'static { ) -> McpAuthResult; } -/// Dynamic-header request for a connected Copilot Connector MCP endpoint. -/// -/// These headers authorize the client-to-Copilot-Connectors service request. -/// The Connector service owns downstream provider tokens. -#[derive(Debug, Clone)] -pub struct McpHeadersRefreshRequest { - /// Stable Connector server key used in the configuration map, not the - /// human-readable display name or Connector service ID. - pub server_key: String, - /// URL of the MCP server that requires headers. - pub server_url: String, - /// Why the runtime is requesting fresh headers. - pub reason: McpHeadersRefreshRequiredReason, -} - -/// Result returned by an [`McpHeadersRefreshHandler`]. -#[derive(Debug, Clone)] -pub enum McpHeadersRefreshResult { - /// Supplies short-lived Copilot Connectors service authorization headers. - Headers { - /// Headers to overlay onto the client-to-Copilot-Connectors request. - headers: HashMap, - /// Remaining Connector service authorization lifetime in milliseconds. - ttl_ms: Option, - }, - /// Indicates that no service authorization headers are available. - None, -} - -impl McpHeadersRefreshResult { - pub(crate) fn into_wire(self) -> McpHeadersHandlePendingHeadersRefreshRequest { - match self { - Self::Headers { headers, ttl_ms } => { - McpHeadersHandlePendingHeadersRefreshRequest::Headers( - McpHeadersHandlePendingHeadersRefreshRequestHeaders { - headers, - kind: McpHeadersHandlePendingHeadersRefreshRequestHeadersKind::Headers, - ttl_ms, - }, - ) - } - Self::None => McpHeadersHandlePendingHeadersRefreshRequest::None( - McpHeadersHandlePendingHeadersRefreshRequestNone { - kind: McpHeadersHandlePendingHeadersRefreshRequestNoneKind::None, - }, - ), - } - } -} - -pub(crate) fn mcp_headers_error_result( - message: impl Into, -) -> McpHeadersHandlePendingHeadersRefreshRequest { - McpHeadersHandlePendingHeadersRefreshRequest::Error( - McpHeadersHandlePendingHeadersRefreshRequestError { - kind: McpHeadersHandlePendingHeadersRefreshRequestErrorKind::Error, - message: message.into(), - }, - ) -} - -/// Handler for short-lived Copilot Connectors service authorization. -/// -/// Hosts normally return the selected account's GitHub bearer authorization. -/// They do not supply Outlook, Slack, or other downstream provider tokens; -/// those remain owned by the Connector service. -#[async_trait] -pub trait McpHeadersRefreshHandler: Send + Sync + 'static { - /// Resolve a Copilot Connectors service authorization refresh request. - async fn handle( - &self, - session_id: SessionId, - request_id: RequestId, - request: McpHeadersRefreshRequest, - ) -> Result; -} - /// Handler for `user_input.requested` events from the legacy question-and-answer /// `ask_user` variant. /// diff --git a/rust/src/session.rs b/rust/src/session.rs index f7e6d5380e..82e97b6606 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -14,20 +14,18 @@ use tracing::{Instrument, error, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - LogRequest, McpHeadersHandlePendingHeadersRefreshRequestRequest, ModelSwitchAutoTierRequest, - ModelSwitchAutoTierResult, ModelSwitchToRequest, OpenCanvasInstance, PermissionDecisionRequest, - RegisterEventInterestParams, ToolsGetCurrentMetadataResult, rpc_methods, + LogRequest, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchToRequest, + OpenCanvasInstance, PermissionDecisionRequest, RegisterEventInterestParams, + ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ - CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, - McpHeadersRefreshRequiredData, McpOauthRequiredData, SessionCanvasClosedData, SessionErrorData, - SessionEventType, SessionIdleData, SessionMode, + CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, + SessionCanvasClosedData, SessionErrorData, SessionEventType, SessionIdleData, SessionMode, }; use crate::handler::{ AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler, - McpAuthHandler, McpAuthRequest, McpAuthResult, McpHeadersRefreshHandler, - McpHeadersRefreshRequest, PermissionHandler, PermissionResult, UserInputHandler, - UserInputResponse, mcp_headers_error_result, + McpAuthHandler, McpAuthRequest, McpAuthResult, PermissionHandler, PermissionResult, + UserInputHandler, UserInputResponse, }; use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; @@ -90,7 +88,6 @@ pub(crate) struct SessionHandlers { pub managed_settings_enabled: bool, pub elicitation: Option>, pub mcp_auth: Option>, - pub mcp_headers: Option>, pub user_input: Option>, pub exit_plan_mode: Option>, pub auto_mode_switch: Option>, @@ -191,6 +188,7 @@ enum PendingSessionId { /// callback. `None` in the stash means nothing was ever registered. Deferred(Arc>>), } + impl PendingSessionRegistration { fn new( client: Client, @@ -458,7 +456,7 @@ impl Session { /// The hand-authored helpers on [`Session`] delegate to this namespace /// and remain the recommended entry point for everyday use; reach for /// `rpc()` when you want a method without a hand-written wrapper. - pub fn rpc(&self) -> crate::rpc::SessionRpc<'_> { + pub fn rpc(&self) -> crate::generated::rpc::SessionRpc<'_> { crate::generated::rpc::SessionRpc { session: self } } @@ -1268,7 +1266,6 @@ impl Client { ), elicitation: runtime.elicitation_handler.take(), mcp_auth: runtime.mcp_auth_handler.take(), - mcp_headers: runtime.mcp_headers_handler.take(), user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), @@ -1291,7 +1288,6 @@ impl Client { .as_ref() .map(|registration| registration.id().to_string()); let has_mcp_auth_handler = handlers.mcp_auth.is_some(); - let has_mcp_headers_handler = handlers.mcp_headers.is_some(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into()); } @@ -1324,31 +1320,12 @@ impl Client { let inline_stash: Arc< ParkingLotMutex>, > = Arc::new(ParkingLotMutex::new(None)); - let mut local_startup = None; let inline_callback: Option = if let Some(ref sid) = local_session_id { - let registration = self.register_session(sid); - let event_loop = spawn_event_loop( - sid.clone(), - self.clone(), - handlers.clone(), - hooks.clone(), - transforms.clone(), - command_handlers.clone(), - canvas_handler.clone(), - session_fs_provider.clone(), - bearer_token_providers.clone(), - registration.channels, - idle_waiter.clone(), - capabilities.clone(), - open_canvases.clone(), - event_tx.clone(), - shutdown.clone(), - external_tools_shutdown.clone(), - ); - local_startup = Some((registration.token, event_loop)); + let channels = self.register_session(sid); + *inline_stash.lock() = Some((sid.clone(), channels)); None } else { let client = self.clone(); @@ -1388,12 +1365,14 @@ impl Client { // token and unregisters whatever was registered on the router. For // the cloud path the ID is only known once the inline callback has // run, so the guard reads the stash at cleanup time. - let mut pending_registration = match local_session_id.as_ref() { - Some(sid) => { - let token = local_startup + let mut pending_registration = match local_session_id { + Some(ref sid) => { + let token = inline_stash + .lock() .as_ref() - .expect("local session startup must exist") - .0; + .expect("session registration must exist") + .1 + .token; PendingSessionRegistration::new( self.clone(), sid.clone(), @@ -1430,42 +1409,31 @@ impl Client { .into()); } - let (session_id, registration_token, event_loop) = - if let Some((registration_token, event_loop)) = local_startup.take() { - ( - local_session_id - .clone() - .expect("local session ID must exist for local startup"), - registration_token, - event_loop, - ) - } else { - let (session_id, registration) = inline_stash - .lock() - .take() - .expect("server-assigned session registration must exist"); - let registration_token = registration.token; - pending_registration.resolve_to(session_id.clone(), registration_token); - let event_loop = spawn_event_loop( - session_id.clone(), - self.clone(), - handlers, - hooks, - transforms, - command_handlers, - canvas_handler, - session_fs_provider, - bearer_token_providers, - registration.channels, - idle_waiter.clone(), - capabilities.clone(), - open_canvases.clone(), - event_tx.clone(), - shutdown.clone(), - external_tools_shutdown.clone(), - ); - (session_id, registration_token, event_loop) - }; + let (session_id, registration) = inline_stash + .lock() + .take() + .expect("session registration must have populated stash on success"); + let channels = registration.channels; + let registration_token = registration.token; + pending_registration.resolve_to(session_id.clone(), registration_token); + let event_loop = spawn_event_loop( + session_id.clone(), + self.clone(), + handlers, + hooks, + transforms, + command_handlers, + canvas_handler, + session_fs_provider, + bearer_token_providers, + channels, + idle_waiter.clone(), + capabilities.clone(), + open_canvases.clone(), + event_tx.clone(), + shutdown.clone(), + external_tools_shutdown.clone(), + ); tracing::debug!( elapsed_ms = setup_start.elapsed().as_millis(), session_id = %session_id, @@ -1475,13 +1443,8 @@ impl Client { "Client::create_session local setup complete" ); *capabilities.write() = create_result.capabilities.unwrap_or_default(); - if let Err(error) = register_mcp_handler_interests( - self, - &session_id, - has_mcp_auth_handler, - has_mcp_headers_handler, - ) - .await + if has_mcp_auth_handler + && let Err(error) = register_mcp_auth_interest(self, &session_id).await { pending_registration.cleanup(event_loop).await; return Err(error); @@ -1622,7 +1585,6 @@ impl Client { ), elicitation: runtime.elicitation_handler.take(), mcp_auth: runtime.mcp_auth_handler.take(), - mcp_headers: runtime.mcp_headers_handler.take(), user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), @@ -1645,7 +1607,6 @@ impl Client { .as_ref() .map(|registration| registration.id().to_string()); let has_mcp_auth_handler = handlers.mcp_auth.is_some(); - let has_mcp_headers_handler = handlers.mcp_headers.is_some(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into()); } @@ -1739,13 +1700,8 @@ impl Client { }) .into()); } - if let Err(error) = register_mcp_handler_interests( - self, - &session_id, - has_mcp_auth_handler, - has_mcp_headers_handler, - ) - .await + if has_mcp_auth_handler + && let Err(error) = register_mcp_auth_interest(self, &session_id).await { registration.cleanup(event_loop).await; return Err(error); @@ -2265,13 +2221,9 @@ fn permission_response_params( Some(params) } -async fn register_mcp_event_interest( - client: &Client, - session_id: &SessionId, - event_type: &str, -) -> Result<(), Error> { +async fn register_mcp_auth_interest(client: &Client, session_id: &SessionId) -> Result<(), Error> { let mut params = serde_json::to_value(RegisterEventInterestParams { - event_type: event_type.to_string(), + event_type: "mcp.oauth_required".to_string(), })?; params["sessionId"] = Value::String(session_id.to_string()); client @@ -2280,21 +2232,6 @@ async fn register_mcp_event_interest( Ok(()) } -async fn register_mcp_handler_interests( - client: &Client, - session_id: &SessionId, - has_auth_handler: bool, - has_headers_handler: bool, -) -> Result<(), Error> { - if has_auth_handler { - register_mcp_event_interest(client, session_id, "mcp.oauth_required").await?; - } - if has_headers_handler { - register_mcp_event_interest(client, session_id, "mcp.headers_refresh_required").await?; - } - Ok(()) -} - fn tool_failure_result(message: impl Into) -> ToolResult { let message = message.into(); ToolResult::Expanded(ToolResultExpanded { @@ -2936,95 +2873,6 @@ async fn handle_notification( .instrument(span), ); } - SessionEventType::McpHeadersRefreshRequired => { - let Some(mcp_headers_handler) = handlers.mcp_headers.clone() else { - warn!( - session_id = %session_id, - "received MCP headers refresh request without a registered handler" - ); - return; - }; - let data: McpHeadersRefreshRequiredData = - match serde_json::from_value(notification.event.data.clone()) { - Ok(data) => data, - Err(error) => { - warn!(error = %error, "failed to deserialize MCP headers refresh request"); - return; - } - }; - let request_id = data.request_id; - let request = McpHeadersRefreshRequest { - server_key: data.server_name, - server_url: data.server_url, - reason: data.reason, - }; - let client = client.clone(); - let sid = session_id.clone(); - let span = tracing::error_span!( - "mcp_headers_request_handler", - session_id = %sid, - request_id = %request_id - ); - tokio::spawn( - async move { - let handler_task = tokio::spawn({ - let sid = sid.clone(); - let request_id = request_id.clone(); - let span = tracing::error_span!( - "mcp_headers_callback", - session_id = %sid, - request_id = %request_id - ); - async move { - let handler_start = Instant::now(); - let result = mcp_headers_handler - .handle(sid.clone(), request_id.clone(), request) - .await; - tracing::debug!( - elapsed_ms = handler_start.elapsed().as_millis(), - session_id = %sid, - request_id = %request_id, - "McpHeadersRefreshHandler::handle dispatch" - ); - result - } - .instrument(span) - }); - let result = match handler_task.await { - Ok(Ok(result)) => result.into_wire(), - Ok(Err(error)) => mcp_headers_error_result(error.to_string()), - Err(error) => mcp_headers_error_result(error.to_string()), - }; - let params = McpHeadersHandlePendingHeadersRefreshRequestRequest { - request_id: request_id.clone(), - result, - }; - let mut wire_params = match serde_json::to_value(params) { - Ok(params) => params, - Err(error) => { - warn!(error = %error, "failed to serialize MCP headers response"); - return; - } - }; - wire_params["sessionId"] = Value::String(sid.to_string()); - if let Err(error) = client - .call( - rpc_methods::SESSION_MCP_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST, - Some(wire_params), - ) - .await - { - warn!( - error = %error, - session_id = %sid, - request_id = %request_id, - "failed to send MCP headers response" - ); - } - } - .instrument(span), - ); - } SessionEventType::CommandExecute => { let data: CommandExecuteData = match serde_json::from_value(notification.event.data.clone()) { diff --git a/rust/src/types.rs b/rust/src/types.rs index e0e473b885..6e3af9273f 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -20,9 +20,7 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage, CopilotWebSocketResponse, WebSocketTransform, forward_http, }; -use crate::generated::api_types::{ - CurrentToolMetadata, ManagedMcpServerConfig as WireManagedMcpServerConfig, OpenCanvasInstance, -}; +use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; /// Acknowledgement and Auto preference snapshot returned by an Auto tier switch. pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus}; /// Routing tier for the `auto` model with Auto mode V2. @@ -36,7 +34,7 @@ pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig}; use crate::github_token::GitHubTokenProvider; use crate::handler::{ AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler, - McpHeadersRefreshHandler, PermissionHandler, UserInputHandler, + PermissionHandler, UserInputHandler, }; use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; @@ -1220,57 +1218,6 @@ pub struct McpHttpServerConfig { pub headers: HashMap, } -/// Connected Copilot Connector MCP endpoint supplied from the service catalog. -/// -/// The containing map key is the stable Connector server key. Short-lived -/// client-to-Copilot-Connectors service authorization is supplied through -/// [`McpHeadersRefreshHandler`], never stored in this configuration. The -/// Connector service owns downstream provider tokens. -/// -/// **Experimental.** This type is part of an experimental Connector MCP -/// surface and may change or be removed in a future release. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ConnectorMcpServerConfig { - /// Human-readable Connector display name. - pub display_name: String, - /// Maximum time, in milliseconds, that the runtime may reuse Connector - /// service authorization. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub authorization_cache_ttl_ms: Option, - /// Timeout in milliseconds for tool discovery and tool calls. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub timeout: Option, - /// Tools to include. Defaults to all tools when omitted. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tools: Option>, - /// Hosted MCP streamable HTTP endpoint. - pub url: String, -} - -impl ConnectorMcpServerConfig { - fn into_wire(self) -> WireManagedMcpServerConfig { - WireManagedMcpServerConfig { - display_name: self.display_name, - headers_refresh_ttl_ms: self.authorization_cache_ttl_ms, - timeout: self.timeout, - tools: self.tools, - url: self.url, - } - } -} - -fn connector_mcp_servers_into_wire( - servers: Option>, -) -> Option> { - servers.map(|servers| { - servers - .into_iter() - .map(|(identity, server)| (identity, server.into_wire())) - .collect() - }) -} - /// Configures a custom inference provider (BYOK — Bring Your Own Key). /// /// Routes session requests through an alternative model provider @@ -2063,12 +2010,6 @@ pub struct SessionConfig { pub included_builtin_skills: Option>, /// MCP server configurations passed through to the CLI. pub mcp_servers: Option>, - /// Connected Copilot Connector MCP endpoints supplied from the service catalog. - /// - /// Keys are stable Connector server keys. Short-lived Copilot Connectors - /// service authorization is supplied through [`McpHeadersRefreshHandler`]; - /// downstream provider tokens remain owned by the Connector service. - pub connector_mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// /// - `"persistent"` — tokens are stored in the OS keychain (shared across sessions). @@ -2301,8 +2242,6 @@ pub struct SessionConfig { /// Optional MCP OAuth request handler. When set, the SDK can satisfy MCP /// server OAuth requests with host-acquired token data or cancellation. pub mcp_auth_handler: Option>, - /// Optional service-authorization handler for Copilot Connector MCP endpoints. - pub mcp_headers_handler: Option>, /// Optional handler for the legacy question-and-answer `ask_user` variant. /// When `None`, `requestUserInput: false` goes on the wire, so this client /// cannot handle legacy user-input requests. @@ -2395,7 +2334,6 @@ impl std::fmt::Debug for SessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) - .field("connector_mcp_servers", &self.connector_mcp_servers) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field( "auth_client_id_metadata_url", @@ -2485,10 +2423,6 @@ impl std::fmt::Debug for SessionConfig { "mcp_auth_handler", &self.mcp_auth_handler.as_ref().map(|_| ""), ) - .field( - "mcp_headers_handler", - &self.mcp_headers_handler.as_ref().map(|_| ""), - ) .field( "user_input_handler", &self.user_input_handler.as_ref().map(|_| ""), @@ -2545,7 +2479,6 @@ impl Default for SessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, - connector_mcp_servers: None, mcp_oauth_token_storage: None, auth_client_id_metadata_url: None, enable_config_discovery: None, @@ -2598,7 +2531,6 @@ impl Default for SessionConfig { permission_handler: None, elicitation_handler: None, mcp_auth_handler: None, - mcp_headers_handler: None, user_input_handler: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, @@ -2625,7 +2557,6 @@ pub(crate) struct SessionConfigRuntime { pub permission_policy: Option, pub elicitation_handler: Option>, pub mcp_auth_handler: Option>, - pub mcp_headers_handler: Option>, pub user_input_handler: Option>, pub exit_plan_mode_handler: Option>, pub auto_mode_switch_handler: Option>, @@ -2719,7 +2650,6 @@ impl SessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, - managed_mcp_servers: connector_mcp_servers_into_wire(self.connector_mcp_servers), mcp_oauth_token_storage: self.mcp_oauth_token_storage, auth_client_id_metadata_url: self.auth_client_id_metadata_url, embedding_cache_storage: self.embedding_cache_storage, @@ -2784,7 +2714,6 @@ impl SessionConfig { permission_policy: self.permission_policy, elicitation_handler: self.elicitation_handler, mcp_auth_handler: self.mcp_auth_handler, - mcp_headers_handler: self.mcp_headers_handler, user_input_handler: self.user_input_handler, exit_plan_mode_handler: self.exit_plan_mode_handler, auto_mode_switch_handler: self.auto_mode_switch_handler, @@ -2822,13 +2751,6 @@ impl SessionConfig { self } - /// Install an [`McpHeadersRefreshHandler`] for short-lived Copilot - /// Connectors service authorization. - pub fn with_mcp_headers_handler(mut self, handler: Arc) -> Self { - self.mcp_headers_handler = Some(handler); - self - } - /// Install a [`UserInputHandler`] for the legacy question-and-answer /// `ask_user` variant. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { @@ -3072,15 +2994,6 @@ impl SessionConfig { self } - /// Set connected Copilot Connector MCP endpoints keyed by stable server key. - pub fn with_connector_mcp_servers( - mut self, - servers: HashMap, - ) -> Self { - self.connector_mcp_servers = Some(servers); - self - } - /// Set MCP OAuth token storage mode. /// /// - `"persistent"` — tokens stored in the OS keychain. @@ -3577,8 +3490,6 @@ pub struct ResumeSessionConfig { pub included_builtin_skills: Option>, /// Re-supply MCP servers so they remain available after app restart. pub mcp_servers: Option>, - /// Re-supply connected Copilot Connector MCP endpoints after app restart. - pub connector_mcp_servers: Option>, /// Controls how MCP OAuth tokens are stored for this session. /// See [`SessionConfig::mcp_oauth_token_storage`] for details. pub mcp_oauth_token_storage: Option, @@ -3753,8 +3664,6 @@ pub struct ResumeSessionConfig { pub elicitation_handler: Option>, /// Optional MCP OAuth handler. See [`SessionConfig::mcp_auth_handler`]. pub mcp_auth_handler: Option>, - /// Optional Copilot Connectors service-authorization handler. - pub mcp_headers_handler: Option>, /// Optional user-input handler. See /// [`SessionConfig::user_input_handler`]. pub user_input_handler: Option>, @@ -3817,7 +3726,6 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) - .field("connector_mcp_servers", &self.connector_mcp_servers) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field( "auth_client_id_metadata_url", @@ -3902,10 +3810,6 @@ impl std::fmt::Debug for ResumeSessionConfig { "elicitation_handler", &self.elicitation_handler.as_ref().map(|_| ""), ) - .field( - "mcp_headers_handler", - &self.mcp_headers_handler.as_ref().map(|_| ""), - ) .field( "user_input_handler", &self.user_input_handler.as_ref().map(|_| ""), @@ -4009,7 +3913,6 @@ impl ResumeSessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, - managed_mcp_servers: connector_mcp_servers_into_wire(self.connector_mcp_servers), mcp_oauth_token_storage: self.mcp_oauth_token_storage, auth_client_id_metadata_url: self.auth_client_id_metadata_url, embedding_cache_storage: self.embedding_cache_storage, @@ -4075,7 +3978,6 @@ impl ResumeSessionConfig { permission_policy: self.permission_policy, elicitation_handler: self.elicitation_handler, mcp_auth_handler: self.mcp_auth_handler, - mcp_headers_handler: self.mcp_headers_handler, user_input_handler: self.user_input_handler, exit_plan_mode_handler: self.exit_plan_mode_handler, auto_mode_switch_handler: self.auto_mode_switch_handler, @@ -4122,7 +4024,6 @@ impl ResumeSessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, - connector_mcp_servers: None, mcp_oauth_token_storage: None, auth_client_id_metadata_url: None, enable_config_discovery: None, @@ -4176,7 +4077,6 @@ impl ResumeSessionConfig { permission_handler: None, elicitation_handler: None, mcp_auth_handler: None, - mcp_headers_handler: None, user_input_handler: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, @@ -4210,13 +4110,6 @@ impl ResumeSessionConfig { self } - /// Install an [`McpHeadersRefreshHandler`] for short-lived Copilot - /// Connectors service authorization. - pub fn with_mcp_headers_handler(mut self, handler: Arc) -> Self { - self.mcp_headers_handler = Some(handler); - self - } - /// Install a [`UserInputHandler`] for the resumed session. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { self.user_input_handler = Some(handler); @@ -4452,15 +4345,6 @@ impl ResumeSessionConfig { self } - /// Re-supply connected Copilot Connector MCP endpoints on resume. - pub fn with_connector_mcp_servers( - mut self, - servers: HashMap, - ) -> Self { - self.connector_mcp_servers = Some(servers); - self - } - /// Set MCP OAuth token storage mode on resume. /// See [`SessionConfig::with_mcp_oauth_token_storage`] for details. pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into) -> Self { @@ -5851,7 +5735,7 @@ impl SessionEvent { /// /// Returns `SessionEventType::Unknown` for unrecognized event types, /// ensuring forward compatibility with newer CLI versions. - pub fn parsed_type(&self) -> crate::session_events::SessionEventType { + pub fn parsed_type(&self) -> crate::generated::SessionEventType { use serde::de::IntoDeserializer; let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> = self.event_type.as_str().into_deserializer(); @@ -6395,14 +6279,13 @@ mod tests { use super::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, - ConnectionState, ConnectorMcpServerConfig, CopilotExpAssignmentResponse, CustomAgentConfig, - DeliveryMode, ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, - GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, - McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, - PermissionResponseCapability, ProviderConfig, ProviderModelConfig, ReasoningSummary, - ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool, - ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse, - ensure_attachment_display_names, + ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, + ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, + InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, + MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, + ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, + SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, + ToolResultResponse, ensure_attachment_display_names, }; use crate::generated::session_events::TypedSessionEvent; @@ -6730,50 +6613,6 @@ mod tests { ); } - #[test] - fn connector_mcp_servers_map_to_internal_wire_on_create_and_resume() { - let servers = HashMap::from([( - "connector-key".to_string(), - ConnectorMcpServerConfig { - display_name: "Connector server".to_string(), - authorization_cache_ttl_ms: Some(30_000), - timeout: Some(15_000), - tools: Some(vec!["search".to_string()]), - url: "https://mcp.example.test".to_string(), - }, - )]); - - let public_json = serde_json::to_value(servers.get("connector-key").unwrap()).unwrap(); - assert_eq!(public_json["authorizationCacheTtlMs"], 30_000); - assert!(public_json.get("headersRefreshTtlMs").is_none()); - - let (create_wire, _) = SessionConfig::default() - .with_connector_mcp_servers(servers.clone()) - .into_wire(Some(SessionId::from("connector-create"))) - .expect("create config has no duplicate handlers"); - let create_json = serde_json::to_value(create_wire).unwrap(); - assert_eq!( - create_json["managedMcpServers"]["connector-key"], - json!({ - "displayName": "Connector server", - "headersRefreshTtlMs": 30_000, - "timeout": 15_000, - "tools": ["search"], - "url": "https://mcp.example.test" - }) - ); - - let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("connector-resume")) - .with_connector_mcp_servers(servers) - .into_wire() - .expect("resume config has no duplicate handlers"); - let resume_json = serde_json::to_value(resume_wire).unwrap(); - assert_eq!( - resume_json["managedMcpServers"]["connector-key"]["displayName"], - "Connector server" - ); - } - #[test] fn custom_agents_local_only_serializes_on_create_and_resume() { let (create_wire, _) = SessionConfig::default() diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 2b9385d5c4..1bd368ff18 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -21,7 +21,7 @@ use serde::Serialize; use crate::canvas::CanvasDeclaration; use crate::generated::api_types::{ - ManagedMcpServerConfig, ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, + ModelCapabilitiesOverride, OpenCanvasInstance, RemoteSessionMode, }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ @@ -94,8 +94,6 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub managed_mcp_servers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth_client_id_metadata_url: Option, @@ -259,8 +257,6 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub managed_mcp_servers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth_client_id_metadata_url: Option, diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 94f233f19a..767837e96f 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -4,11 +4,12 @@ #![allow(clippy::unwrap_used)] use github_copilot_sdk::rpc::{ - Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, - ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, ModelSetAllowedModelsRequest, - ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, - ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, SandboxConfig, - SendAgentMode, TasksStartAgentRequest, + ConnectorAccountRequest, ConnectorCatalogStatus, ConnectorConnectRequest, + ConnectorContinueRequest, ConnectorReconcileRequest, Extension, ExtensionList, ExtensionSource, + ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, + FleetStartResult, ModelSetAllowedModelsRequest, ModelSetAllowedModelsResult, + ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus, + QueuePendingItems, QueuePendingItemsKind, SandboxConfig, SendAgentMode, TasksStartAgentRequest, }; use github_copilot_sdk::session_events::{ PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent, @@ -130,6 +131,68 @@ fn tasks_start_agent_request_fields_are_accessible() { assert_eq!(request.description.as_deref(), Some("SDK task agent")); } +#[test] +fn connector_request_dtos_use_public_camel_case_wire_fields() { + let account = ConnectorAccountRequest { + account_id: "account-1".to_string(), + }; + assert_eq!( + serde_json::to_value(account).unwrap(), + serde_json::json!({ "accountId": "account-1" }) + ); + + let connector = ConnectorConnectRequest { + account_id: "account-1".to_string(), + connector_name: "github".to_string(), + }; + assert_eq!( + serde_json::to_value(connector).unwrap(), + serde_json::json!({ + "accountId": "account-1", + "connectorName": "github", + }) + ); + + let continuation = ConnectorContinueRequest { + continuation_id: "continuation-1".to_string(), + deadline_ms: 30_000, + max_attempts: 5, + poll_interval_ms: 1_000, + }; + assert_eq!( + serde_json::to_value(continuation).unwrap(), + serde_json::json!({ + "continuationId": "continuation-1", + "deadlineMs": 30_000, + "maxAttempts": 5, + "pollIntervalMs": 1_000, + }) + ); + + let reconcile = ConnectorReconcileRequest { + account_id: "account-1".to_string(), + refresh_catalog: Some(true), + }; + assert_eq!( + serde_json::to_value(reconcile).unwrap(), + serde_json::json!({ + "accountId": "account-1", + "refreshCatalog": true, + }) + ); +} + +#[test] +fn connector_catalog_unknown_wire_value_has_a_distinct_variant() { + let service_unknown: ConnectorCatalogStatus = + serde_json::from_value(serde_json::json!("unknown")).unwrap(); + let future_status: ConnectorCatalogStatus = + serde_json::from_value(serde_json::json!("future_status")).unwrap(); + + assert_eq!(service_unknown, ConnectorCatalogStatus::UnknownValue); + assert_eq!(future_status, ConnectorCatalogStatus::Unknown); +} + #[test] fn model_allowed_models_request_and_result_preserve_contract_fields() { let replace = ModelSetAllowedModelsRequest { diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 9675401810..f49a23fa72 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -15,16 +15,19 @@ use github_copilot_sdk::github_token::{ use github_copilot_sdk::handler::{ ApproveAllHandler, AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler, ExitPlanModeResult, McpAuthHandler, McpAuthRequest, McpAuthResult, - McpHeadersRefreshHandler, McpHeadersRefreshRequest, McpHeadersRefreshResult, PermissionHandler, - PermissionResult, UserInputHandler, UserInputResponse, + PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, }; use github_copilot_sdk::rpc::{ CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - ModelSetAllowedModelsRequest, OpenCanvasInstance, SendAgentMode, SendMode, SendRequest, + ConnectorAccountRequest, ConnectorAvailability, ConnectorCapabilities, ConnectorCatalogResult, + ConnectorCatalogStatus, ConnectorConnectRequest, ConnectorConnectResult, + ConnectorContinueRequest, ConnectorDisconnectResult, ConnectorMcpStatus, + ConnectorReconcileRequest, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance, + SendAgentMode, SendMode, SendRequest, SessionRpcConnectors, }; use github_copilot_sdk::session_events::{ - ManagedSettingsResolvedSource, McpHeadersRefreshRequiredReason, McpOauthRequiredData, - ReasoningSummary, SessionLimitsConfig, SessionManagedSettingsResolvedData, + ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, + SessionManagedSettingsResolvedData, }; use github_copilot_sdk::types::{ AskUserVariant, CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, @@ -189,10 +192,6 @@ struct TestCanvasHandler; struct CancelMcpAuthHandler; -struct TestMcpHeadersHandler { - calls: tokio::sync::mpsc::UnboundedSender<(SessionId, RequestId, McpHeadersRefreshRequest)>, -} - struct ContextualApproveHandler; struct GatedApproveHandler { @@ -243,41 +242,6 @@ impl McpAuthHandler for CancelMcpAuthHandler { } } -#[async_trait] -impl McpHeadersRefreshHandler for TestMcpHeadersHandler { - async fn handle( - &self, - session_id: SessionId, - request_id: RequestId, - request: McpHeadersRefreshRequest, - ) -> Result { - self.calls - .send((session_id, request_id, request.clone())) - .unwrap(); - match request.server_key.as_str() { - "ttl-absent" => Ok(McpHeadersRefreshResult::Headers { - headers: HashMap::from([( - "Authorization".to_string(), - "Bearer absent".to_string(), - )]), - ttl_ms: None, - }), - "ttl-present" => Ok(McpHeadersRefreshResult::Headers { - headers: HashMap::from([( - "Authorization".to_string(), - "Bearer present".to_string(), - )]), - ttl_ms: Some(12_345), - }), - "none" => Ok(McpHeadersRefreshResult::None), - _ => Err(github_copilot_sdk::Error::with_message( - ErrorKind::InvalidConfig, - "credential broker unavailable", - )), - } - } -} - #[async_trait] impl CanvasHandler for TestCanvasHandler { async fn on_open( @@ -1271,423 +1235,6 @@ async fn resume_session_registers_mcp_auth_interest_only_with_handler() { let _session = timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); } -#[tokio::test] -async fn mcp_headers_handler_dispatches_all_results() { - let (calls_tx, mut calls_rx) = tokio::sync::mpsc::unbounded_channel(); - let handler = Arc::new(TestMcpHeadersHandler { calls: calls_tx }); - let (client, server_read, server_write) = make_client(); - let mut server = FakeServer { - read: server_read, - write: server_write, - session_id: "mcp-headers-session".to_string(), - }; - let create_handle = tokio::spawn({ - let client = client.clone(); - async move { - client - .create_session( - SessionConfig::default() - .with_session_id("mcp-headers-session") - .with_mcp_headers_handler(handler), - ) - .await - .unwrap() - } - }); - - let create_request = server.read_request().await; - assert_eq!(create_request["method"], "session.create"); - server - .send_event( - "mcp.headers_refresh_required", - serde_json::json!({ - "requestId": "headers-before-create", - "serverName": "ttl-absent", - "serverUrl": "https://ttl-absent.example.test/mcp", - "reason": "startup" - }), - ) - .await; - let response_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); - assert_eq!( - response_request["method"], - "session.mcp.headers.handlePendingHeadersRefreshRequest" - ); - assert_eq!( - response_request["params"]["sessionId"], - "mcp-headers-session" - ); - assert_eq!( - response_request["params"]["requestId"], - "headers-before-create" - ); - assert_eq!(response_request["params"]["result"]["kind"], "headers"); - assert!( - response_request["params"]["result"]["headers"]["Authorization"] - .as_str() - .is_some() - ); - let (received_session_id, received_request_id, received_request) = - timeout(TIMEOUT, calls_rx.recv()).await.unwrap().unwrap(); - assert_eq!(received_session_id.as_str(), "mcp-headers-session"); - assert_eq!(received_request_id, "headers-before-create"); - assert_eq!(received_request.server_key, "ttl-absent"); - assert_eq!( - received_request.server_url, - "https://ttl-absent.example.test/mcp" - ); - assert_eq!( - received_request.reason, - McpHeadersRefreshRequiredReason::Startup - ); - server - .respond(&response_request, serde_json::json!({ "success": true })) - .await; - - server - .respond( - &create_request, - serde_json::json!({ - "sessionId": "mcp-headers-session", - "workspacePath": "/workspace" - }), - ) - .await; - let interest_request = server.read_request().await; - assert_eq!( - interest_request["method"], - "session.eventLog.registerInterest" - ); - assert_eq!( - interest_request["params"]["eventType"], - "mcp.headers_refresh_required" - ); - server - .respond(&interest_request, serde_json::json!({ "id": "interest-1" })) - .await; - let _session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); - - let cases = [ - ( - "ttl-absent", - "startup", - McpHeadersRefreshRequiredReason::Startup, - serde_json::json!({ - "kind": "headers", - "headers": { "Authorization": "Bearer absent" } - }), - ), - ( - "ttl-present", - "ttl-expired", - McpHeadersRefreshRequiredReason::TtlExpired, - serde_json::json!({ - "kind": "headers", - "headers": { "Authorization": "Bearer present" }, - "ttlMs": 12_345 - }), - ), - ( - "none", - "auth-failed", - McpHeadersRefreshRequiredReason::AuthFailed, - serde_json::json!({ "kind": "none" }), - ), - ( - "error", - "startup", - McpHeadersRefreshRequiredReason::Startup, - serde_json::json!({ - "kind": "error", - "message": "credential broker unavailable" - }), - ), - ]; - - for (index, (server_identity, reason, expected_reason, expected_result)) in - cases.into_iter().enumerate() - { - let request_id = format!("headers-{index}"); - let server_url = format!("https://{server_identity}.example.test/mcp"); - server - .send_event( - "mcp.headers_refresh_required", - serde_json::json!({ - "requestId": request_id, - "serverName": server_identity, - "serverUrl": server_url, - "reason": reason - }), - ) - .await; - - let response_request = timeout(TIMEOUT, server.read_request()).await.unwrap(); - assert_eq!( - response_request["method"], - "session.mcp.headers.handlePendingHeadersRefreshRequest" - ); - assert_eq!(response_request["params"]["requestId"], request_id); - assert_eq!(response_request["params"]["result"], expected_result); - - let (received_session_id, received_request_id, received_request) = - timeout(TIMEOUT, calls_rx.recv()).await.unwrap().unwrap(); - assert_eq!(received_session_id.as_str(), "mcp-headers-session"); - assert_eq!(received_request_id, request_id); - assert_eq!(received_request.server_key, server_identity); - assert_eq!(received_request.server_url, server_url); - assert_eq!(received_request.reason, expected_reason); - - server - .respond(&response_request, serde_json::json!({ "success": true })) - .await; - } -} - -#[tokio::test] -async fn mcp_headers_interest_failure_cleans_up_session_dispatch() { - let (calls_tx, mut calls_rx) = tokio::sync::mpsc::unbounded_channel(); - let handler = Arc::new(TestMcpHeadersHandler { calls: calls_tx }); - let (client, server_read, server_write) = make_client(); - let mut server = FakeServer { - read: server_read, - write: server_write, - session_id: "mcp-headers-interest-failure".to_string(), - }; - let create_handle = tokio::spawn({ - let client = client.clone(); - async move { - client - .create_session( - SessionConfig::default() - .with_session_id("mcp-headers-interest-failure") - .with_mcp_headers_handler(handler), - ) - .await - } - }); - - let create_request = server.read_request().await; - server - .respond( - &create_request, - serde_json::json!({ - "sessionId": "mcp-headers-interest-failure", - "workspacePath": "/workspace" - }), - ) - .await; - let interest_request = server.read_request().await; - assert_eq!( - interest_request["method"], - "session.eventLog.registerInterest" - ); - assert_eq!( - interest_request["params"]["eventType"], - "mcp.headers_refresh_required" - ); - let response = serde_json::json!({ - "jsonrpc": "2.0", - "id": interest_request["id"], - "error": { "code": -32603, "message": "registration failed" } - }); - write_framed(&mut server.write, &serde_json::to_vec(&response).unwrap()).await; - - let result = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); - assert!(result.is_err()); - - server - .send_event( - "mcp.headers_refresh_required", - serde_json::json!({ - "requestId": "stale-request", - "serverName": "ttl-absent", - "serverUrl": "https://example.test/mcp", - "reason": "startup" - }), - ) - .await; - assert!(timeout(TIMEOUT, calls_rx.recv()).await.unwrap().is_none()); -} - -#[tokio::test] -async fn public_mcp_list_toggle_tools_and_status_events_are_reachable() { - use github_copilot_sdk::rpc::{McpDisableRequest, McpEnableRequest, McpListToolsRequest}; - use github_copilot_sdk::session_events::{ - McpServerSource, McpServerStatus, SessionEventType, SessionMcpServerNeedsReconnectData, - SessionMcpServerRemovedData, SessionMcpServerStatusChangedData, - SessionMcpServersLoadedData, - }; - - let (session, mut server) = create_session_pair().await; - let session = Arc::new(session); - let _: github_copilot_sdk::rpc::SessionRpc<'_> = session.rpc(); - - let list = tokio::spawn({ - let session = session.clone(); - async move { session.rpc().mcp().list().await.unwrap() } - }); - let request = server.read_request().await; - assert_eq!(request["method"], "session.mcp.list"); - assert_eq!(request["params"]["sessionId"], server.session_id); - server - .respond( - &request, - serde_json::json!({ - "servers": [{ - "displayName": "Calendar", - "name": "calendar", - "source": "managed", - "status": "connected" - }] - }), - ) - .await; - let list = timeout(TIMEOUT, list).await.unwrap().unwrap(); - assert_eq!(list.servers.len(), 1); - assert_eq!(list.servers[0].name, "calendar"); - assert_eq!(list.servers[0].display_name.as_deref(), Some("Calendar")); - assert_eq!(list.servers[0].source, Some(McpServerSource::Managed)); - assert_eq!(list.servers[0].status, McpServerStatus::Connected); - - let enable = tokio::spawn({ - let session = session.clone(); - async move { - session - .rpc() - .mcp() - .enable(McpEnableRequest { - server_name: "calendar".to_string(), - }) - .await - .unwrap(); - } - }); - let request = server.read_request().await; - assert_eq!(request["method"], "session.mcp.enable"); - assert_eq!(request["params"]["serverName"], "calendar"); - server.respond(&request, serde_json::json!({})).await; - timeout(TIMEOUT, enable).await.unwrap().unwrap(); - - let disable = tokio::spawn({ - let session = session.clone(); - async move { - session - .rpc() - .mcp() - .disable(McpDisableRequest { - server_name: "calendar".to_string(), - }) - .await - .unwrap(); - } - }); - let request = server.read_request().await; - assert_eq!(request["method"], "session.mcp.disable"); - assert_eq!(request["params"]["serverName"], "calendar"); - server.respond(&request, serde_json::json!({})).await; - timeout(TIMEOUT, disable).await.unwrap().unwrap(); - - let list_tools = tokio::spawn({ - let session = session.clone(); - async move { - session - .rpc() - .mcp() - .list_tools(McpListToolsRequest { - server_name: "calendar".to_string(), - }) - .await - .unwrap() - } - }); - let request = server.read_request().await; - assert_eq!(request["method"], "session.mcp.listTools"); - assert_eq!(request["params"]["serverName"], "calendar"); - server - .respond( - &request, - serde_json::json!({ - "tools": [{ - "name": "events", - "description": "List calendar events" - }] - }), - ) - .await; - let tools = timeout(TIMEOUT, list_tools).await.unwrap().unwrap(); - assert_eq!(tools.tools.len(), 1); - assert_eq!(tools.tools[0].name, "events"); - - let mut events = session.subscribe(); - server - .send_event( - "session.mcp_servers_loaded", - serde_json::json!({ - "servers": [{ - "displayName": "Calendar", - "name": "calendar", - "source": "managed", - "status": "connected" - }] - }), - ) - .await; - let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); - assert_eq!( - event.parsed_type(), - SessionEventType::SessionMcpServersLoaded - ); - let loaded: SessionMcpServersLoadedData = event.typed_data().unwrap(); - assert_eq!(loaded.servers[0].name, "calendar"); - assert_eq!(loaded.servers[0].status, McpServerStatus::Connected); - - server - .send_event( - "session.mcp_server_status_changed", - serde_json::json!({ - "serverName": "calendar", - "status": "needs-auth" - }), - ) - .await; - let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); - assert_eq!( - event.parsed_type(), - SessionEventType::SessionMcpServerStatusChanged - ); - let changed: SessionMcpServerStatusChangedData = event.typed_data().unwrap(); - assert_eq!(changed.server_name, "calendar"); - assert_eq!(changed.status, McpServerStatus::NeedsAuth); - - server - .send_event( - "session.mcp_server_needs_reconnect", - serde_json::json!({ "serverName": "calendar" }), - ) - .await; - let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); - assert_eq!( - event.parsed_type(), - SessionEventType::SessionMcpServerNeedsReconnect - ); - let reconnect: SessionMcpServerNeedsReconnectData = event.typed_data().unwrap(); - assert_eq!(reconnect.server_name, "calendar"); - - server - .send_event( - "session.mcp_server_removed", - serde_json::json!({ "serverName": "calendar" }), - ) - .await; - let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); - assert_eq!( - event.parsed_type(), - SessionEventType::SessionMcpServerRemoved - ); - let removed: SessionMcpServerRemovedData = event.typed_data().unwrap(); - assert_eq!(removed.server_name, "calendar"); -} - async fn server_respond_create( writer: &mut (impl AsyncWrite + Unpin), request: &Value, @@ -6270,6 +5817,235 @@ async fn rpc_namespace_session_tasks_list_dispatches_correctly() { assert!(result.tasks.is_empty()); } +#[tokio::test] +async fn rpc_namespace_session_connectors_dispatches_all_methods() { + let (session, mut server) = create_session_pair().await; + let session_id = server.session_id.clone(); + let catalog = serde_json::json!({ + "connectors": [{ + "description": "Source control", + "displayName": "GitHub", + "name": "github", + "runtimeServerIds": ["connector-github"], + "status": "connected" + }], + "refreshedAtMs": 1_726_668_000_000_i64, + "revision": 4 + }); + let status = serde_json::json!({ + "accountId": "account-1", + "apiVersion": 1, + "availability": "enabled", + "catalog": catalog.clone(), + "pendingConnections": 0, + "runtimeServers": [{ + "connectorName": "github", + "runtimeServerId": "connector-github", + "status": "connected" + }] + }); + let capabilities = serde_json::json!({ + "apiVersion": 1, + "availability": "enabled", + "consentContinuation": true, + "maxDeadlineMs": 60_000, + "maxPollAttempts": 10, + "maxPollIntervalMs": 5_000, + "opaqueAccountSelection": true + }); + + let server_handle = tokio::spawn(async move { + let expectations = [ + ( + "session.connectors.getCapabilities", + serde_json::json!({ "sessionId": session_id }), + capabilities, + ), + ( + "session.connectors.getStatus", + serde_json::json!({ "sessionId": session_id }), + status.clone(), + ), + ( + "session.connectors.list", + serde_json::json!({ + "accountId": "account-1", + "sessionId": session_id + }), + catalog.clone(), + ), + ( + "session.connectors.refresh", + serde_json::json!({ + "accountId": "account-1", + "sessionId": session_id + }), + catalog, + ), + ( + "session.connectors.connect", + serde_json::json!({ + "accountId": "account-1", + "connectorName": "github", + "sessionId": session_id + }), + serde_json::json!({ + "kind": "connected", + "status": status.clone() + }), + ), + ( + "session.connectors.reconnect", + serde_json::json!({ + "accountId": "account-1", + "connectorName": "github", + "sessionId": session_id + }), + serde_json::json!({ + "consentUrl": "https://example.test/consent", + "continuationId": "continuation-1", + "kind": "consent_required" + }), + ), + ( + "session.connectors.continueConnection", + serde_json::json!({ + "continuationId": "continuation-1", + "deadlineMs": 60_000, + "maxAttempts": 10, + "pollIntervalMs": 1_000, + "sessionId": session_id + }), + serde_json::json!({ + "continuationId": "continuation-2", + "kind": "pending" + }), + ), + ( + "session.connectors.disconnect", + serde_json::json!({ + "accountId": "account-1", + "connectorName": "github", + "sessionId": session_id + }), + serde_json::json!({ + "disconnected": true, + "status": status.clone() + }), + ), + ( + "session.connectors.reconcile", + serde_json::json!({ + "accountId": "account-1", + "refreshCatalog": true, + "sessionId": session_id + }), + status, + ), + ]; + + for (method, params, result) in expectations { + let request = server.read_request().await; + assert_eq!(request["method"], method); + assert_eq!(request["params"], params); + server.respond(&request, result).await; + } + }); + + let connectors: SessionRpcConnectors<'_> = session.rpc().connectors(); + + let capabilities: ConnectorCapabilities = connectors.get_capabilities().await.unwrap(); + assert_eq!(capabilities.availability, ConnectorAvailability::Enabled); + assert_eq!(capabilities.max_poll_attempts, 10); + + let status: ConnectorStatus = connectors.get_status().await.unwrap(); + assert_eq!(status.account_id.as_deref(), Some("account-1")); + assert_eq!( + status.runtime_servers[0].status, + ConnectorMcpStatus::Connected + ); + + let catalog: ConnectorCatalogResult = connectors + .list(ConnectorAccountRequest { + account_id: "account-1".to_string(), + }) + .await + .unwrap(); + assert_eq!( + catalog.connectors[0].status, + ConnectorCatalogStatus::Connected + ); + + let refreshed: ConnectorCatalogResult = connectors + .refresh(ConnectorAccountRequest { + account_id: "account-1".to_string(), + }) + .await + .unwrap(); + assert_eq!(refreshed.revision, 4); + + let connected: ConnectorConnectResult = connectors + .connect(ConnectorConnectRequest { + account_id: "account-1".to_string(), + connector_name: "github".to_string(), + }) + .await + .unwrap(); + let ConnectorConnectResult::Connected(connected) = connected else { + panic!("expected connected result"); + }; + assert_eq!( + connected.status.availability, + ConnectorAvailability::Enabled + ); + + let reconnect: ConnectorConnectResult = connectors + .reconnect(ConnectorConnectRequest { + account_id: "account-1".to_string(), + connector_name: "github".to_string(), + }) + .await + .unwrap(); + let ConnectorConnectResult::ConsentRequired(consent) = reconnect else { + panic!("expected consent-required result"); + }; + assert_eq!(consent.continuation_id, "continuation-1"); + + let continuation: ConnectorConnectResult = connectors + .continue_connection(ConnectorContinueRequest { + continuation_id: "continuation-1".to_string(), + deadline_ms: 60_000, + max_attempts: 10, + poll_interval_ms: 1_000, + }) + .await + .unwrap(); + let ConnectorConnectResult::Pending(pending) = continuation else { + panic!("expected pending result"); + }; + assert_eq!(pending.continuation_id, "continuation-2"); + + let disconnected: ConnectorDisconnectResult = connectors + .disconnect(ConnectorConnectRequest { + account_id: "account-1".to_string(), + connector_name: "github".to_string(), + }) + .await + .unwrap(); + assert!(disconnected.disconnected); + + let reconciled: ConnectorStatus = connectors + .reconcile(ConnectorReconcileRequest { + account_id: "account-1".to_string(), + refresh_catalog: Some(true), + }) + .await + .unwrap(); + assert_eq!(reconciled.catalog.unwrap().revision, 4); + + timeout(TIMEOUT, server_handle).await.unwrap().unwrap(); +} + #[tokio::test] async fn rpc_namespace_client_models_list_dispatches_correctly() { let (session, mut server) = create_session_pair().await; diff --git a/scripts/codegen/connector-session-api-overlay.json b/scripts/codegen/connector-session-api-overlay.json new file mode 100644 index 0000000000..42e6fbf355 --- /dev/null +++ b/scripts/codegen/connector-session-api-overlay.json @@ -0,0 +1,737 @@ +{ + "sessionConnectors": { + "getCapabilities": { + "rpcMethod": "session.connectors.getCapabilities", + "description": "Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + } + }, + "required": [ + "sessionId" + ], + "additionalProperties": false, + "description": "Identifies the target session." + }, + "result": { + "$ref": "#/definitions/ConnectorCapabilities", + "description": "Feature detection and hard polling limits for the EXPERIMENTAL session connector API." + }, + "stability": "experimental" + }, + "getStatus": { + "rpcMethod": "session.connectors.getStatus", + "description": "Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + } + }, + "required": [ + "sessionId" + ], + "additionalProperties": false, + "description": "Identifies the target session." + }, + "result": { + "$ref": "#/definitions/ConnectorStatus", + "description": "Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included." + }, + "stability": "experimental" + }, + "list": { + "rpcMethod": "session.connectors.list", + "description": "Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "accountId": { + "type": "string", + "description": "Opaque account selection ID previously returned by an account discovery API." + } + }, + "required": [ + "sessionId", + "accountId" + ], + "additionalProperties": false, + "description": "Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted.", + "title": "ConnectorAccountRequest", + "stability": "experimental" + }, + "result": { + "$ref": "#/definitions/ConnectorCatalogResult", + "description": "Validated Connector catalog snapshot cached by the session." + }, + "stability": "experimental" + }, + "refresh": { + "rpcMethod": "session.connectors.refresh", + "description": "Refreshes and validates the Connector catalog for the pinned opaque account selection.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "accountId": { + "type": "string", + "description": "Opaque account selection ID previously returned by an account discovery API." + } + }, + "required": [ + "sessionId", + "accountId" + ], + "additionalProperties": false, + "description": "Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted.", + "title": "ConnectorAccountRequest", + "stability": "experimental" + }, + "result": { + "$ref": "#/definitions/ConnectorCatalogResult", + "description": "Validated Connector catalog snapshot cached by the session." + }, + "stability": "experimental" + }, + "connect": { + "rpcMethod": "session.connectors.connect", + "description": "Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "accountId": { + "type": "string", + "description": "Opaque account selection ID. It must match the account already pinned to the session, if any." + }, + "connectorName": { + "type": "string", + "description": "Canonical Connector name from the current catalog." + } + }, + "required": [ + "sessionId", + "accountId", + "connectorName" + ], + "additionalProperties": false, + "description": "Selects one Connector and the pinned host-owned account used for its service and MCP authorization.", + "title": "ConnectorConnectRequest", + "stability": "experimental" + }, + "result": { + "$ref": "#/definitions/ConnectorConnectResult", + "description": "Typed result of initiating or continuing a Connector connection." + }, + "stability": "experimental" + }, + "reconnect": { + "rpcMethod": "session.connectors.reconnect", + "description": "Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "accountId": { + "type": "string", + "description": "Opaque account selection ID. It must match the account already pinned to the session, if any." + }, + "connectorName": { + "type": "string", + "description": "Canonical Connector name from the current catalog." + } + }, + "required": [ + "sessionId", + "accountId", + "connectorName" + ], + "additionalProperties": false, + "description": "Selects one Connector and the pinned host-owned account used for its service and MCP authorization.", + "title": "ConnectorConnectRequest", + "stability": "experimental" + }, + "result": { + "$ref": "#/definitions/ConnectorConnectResult", + "description": "Typed result of initiating or continuing a Connector connection." + }, + "stability": "experimental" + }, + "continueConnection": { + "rpcMethod": "session.connectors.continueConnection", + "description": "Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "continuationId": { + "type": "string", + "description": "Opaque continuation ID returned by connect, reconnect, or an earlier continuation." + }, + "maxAttempts": { + "type": "integer", + "minimum": 0, + "description": "Maximum catalog requests made by this call. Must be between one and the capability limit." + }, + "pollIntervalMs": { + "type": "integer", + "minimum": 0, + "description": "Delay in milliseconds between attempts. Must not exceed the capability limit." + }, + "deadlineMs": { + "type": "integer", + "minimum": 0, + "description": "Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit." + } + }, + "required": [ + "sessionId", + "continuationId", + "maxAttempts", + "pollIntervalMs", + "deadlineMs" + ], + "additionalProperties": false, + "description": "Explicitly bounded continuation of a pending Connector connection.", + "title": "ConnectorContinueRequest", + "stability": "experimental" + }, + "result": { + "$ref": "#/definitions/ConnectorConnectResult", + "description": "Typed result of initiating or continuing a Connector connection." + }, + "stability": "experimental" + }, + "disconnect": { + "rpcMethod": "session.connectors.disconnect", + "description": "Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "accountId": { + "type": "string", + "description": "Opaque account selection ID. It must match the account already pinned to the session, if any." + }, + "connectorName": { + "type": "string", + "description": "Canonical Connector name from the current catalog." + } + }, + "required": [ + "sessionId", + "accountId", + "connectorName" + ], + "additionalProperties": false, + "description": "Selects one Connector and the pinned host-owned account used for its service and MCP authorization.", + "title": "ConnectorConnectRequest", + "stability": "experimental" + }, + "result": { + "$ref": "#/definitions/ConnectorDisconnectResult", + "description": "Authoritative result after disconnect and MCP reconciliation." + }, + "stability": "experimental" + }, + "reconcile": { + "rpcMethod": "session.connectors.reconcile", + "description": "Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "accountId": { + "type": "string", + "description": "Opaque account selection ID. It must match the account already pinned to the session, if any." + }, + "refreshCatalog": { + "type": "boolean", + "description": "When true, refresh the catalog before reconciling. A disabled Connector API performs no service request." + } + }, + "required": [ + "sessionId", + "accountId" + ], + "additionalProperties": false, + "description": "Requests authoritative Connector-to-MCP reconciliation for the pinned account.", + "title": "ConnectorReconcileRequest", + "stability": "experimental" + }, + "result": { + "$ref": "#/definitions/ConnectorStatus", + "description": "Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included." + }, + "stability": "experimental" + } + }, + "definitions": { + "ConnectorAccountRequest": { + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Opaque account selection ID previously returned by an account discovery API." + } + }, + "required": [ + "accountId" + ], + "additionalProperties": false, + "description": "Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted.", + "title": "ConnectorAccountRequest", + "stability": "experimental" + }, + "ConnectorAvailability": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "unavailable" + ], + "description": "Availability of the EXPERIMENTAL session connector API.", + "title": "ConnectorAvailability", + "x-enumDescriptions": { + "enabled": "The resolved Connector feature is enabled and Connector requests are permitted.", + "disabled": "The resolved Connector feature is off. No Connector service request is made while disabled.", + "unavailable": "The session has no eligible host-owned GitHub account or does not support local Connector projection." + }, + "stability": "experimental" + }, + "ConnectorCapabilities": { + "type": "object", + "properties": { + "apiVersion": { + "type": "integer", + "minimum": 0, + "description": "Connector API contract version." + }, + "availability": { + "$ref": "#/definitions/ConnectorAvailability", + "description": "Current session availability. Disabled availability is reported without making a Connector request." + }, + "consentContinuation": { + "type": "boolean", + "description": "Whether connect and reconnect can return an opaque continuation for bounded consent polling." + }, + "opaqueAccountSelection": { + "type": "boolean", + "description": "Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token." + }, + "maxPollAttempts": { + "type": "integer", + "minimum": 0, + "description": "Maximum accepted polling attempts for one continuation call." + }, + "maxPollIntervalMs": { + "type": "integer", + "minimum": 0, + "description": "Maximum accepted delay in milliseconds between polling attempts." + }, + "maxDeadlineMs": { + "type": "integer", + "minimum": 0, + "description": "Maximum accepted wall-clock deadline in milliseconds for one continuation call." + } + }, + "required": [ + "apiVersion", + "availability", + "consentContinuation", + "opaqueAccountSelection", + "maxPollAttempts", + "maxPollIntervalMs", + "maxDeadlineMs" + ], + "additionalProperties": false, + "description": "Feature detection and hard polling limits for the EXPERIMENTAL session connector API.", + "title": "ConnectorCapabilities", + "stability": "experimental" + }, + "ConnectorCatalogEntry": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Canonical Connector name used by lifecycle methods." + }, + "displayName": { + "type": "string", + "description": "Untrusted display label from the service." + }, + "description": { + "type": "string", + "description": "Untrusted service description, when present." + }, + "status": { + "$ref": "#/definitions/ConnectorCatalogStatus", + "description": "Current authoritative service connection state." + }, + "runtimeServerIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Opaque stable runtime IDs currently projected into the session for this Connector." + } + }, + "required": [ + "name", + "displayName", + "status", + "runtimeServerIds" + ], + "additionalProperties": false, + "description": "Credential-free Connector catalog entry.", + "title": "ConnectorCatalogEntry", + "stability": "experimental" + }, + "ConnectorCatalogResult": { + "type": "object", + "properties": { + "revision": { + "type": "integer", + "minimum": 0, + "description": "Monotonically increasing session-local catalog revision." + }, + "refreshedAtMs": { + "type": "integer", + "minimum": 0, + "description": "Unix epoch milliseconds when this snapshot was accepted." + }, + "connectors": { + "type": "array", + "items": { + "$ref": "#/definitions/ConnectorCatalogEntry", + "description": "Credential-free Connector catalog entry." + }, + "description": "Validated catalog entries in service order." + } + }, + "required": [ + "revision", + "refreshedAtMs", + "connectors" + ], + "additionalProperties": false, + "description": "Validated Connector catalog snapshot cached by the session.", + "title": "ConnectorCatalogResult", + "stability": "experimental" + }, + "ConnectorCatalogStatus": { + "type": "string", + "enum": [ + "not_connected", + "pending", + "connected", + "error", + "unknown" + ], + "description": "Authoritative service connection state for one Connector.", + "title": "ConnectorCatalogStatus", + "x-enumDescriptions": { + "not_connected": "The Connector is available but not connected.", + "pending": "The Connector service is still completing connection or consent.", + "connected": "The Connector is connected and may contribute MCP servers.", + "error": "The Connector service reports an unusable connection.", + "unknown": "The service returned a future or unrecognized state." + }, + "stability": "experimental" + }, + "ConnectorConnectRequest": { + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Opaque account selection ID. It must match the account already pinned to the session, if any." + }, + "connectorName": { + "type": "string", + "description": "Canonical Connector name from the current catalog." + } + }, + "required": [ + "accountId", + "connectorName" + ], + "additionalProperties": false, + "description": "Selects one Connector and the pinned host-owned account used for its service and MCP authorization.", + "title": "ConnectorConnectRequest", + "stability": "experimental" + }, + "ConnectorConnectResult": { + "anyOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "connected", + "description": "The service is connected and the session MCP graph was reconciled." + }, + "status": { + "$ref": "#/definitions/ConnectorStatus", + "description": "Fresh authoritative Connector state after MCP reconciliation." + } + }, + "required": [ + "kind", + "status" + ], + "additionalProperties": false, + "description": "The service is connected and the session MCP graph was reconciled.", + "stability": "experimental" + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "consent_required", + "description": "Host-owned consent is required before bounded continuation can complete." + }, + "consentUrl": { + "type": "string", + "description": "Validated HTTPS consent URL. The runtime does not open it." + }, + "continuationId": { + "type": "string", + "description": "Opaque ID accepted by continueConnection." + } + }, + "required": [ + "kind", + "consentUrl", + "continuationId" + ], + "additionalProperties": false, + "description": "Host-owned consent is required before bounded continuation can complete.", + "stability": "experimental" + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "pending", + "description": "The service is still completing the connection without a consent URL." + }, + "continuationId": { + "type": "string", + "description": "Opaque ID accepted by continueConnection." + } + }, + "required": [ + "kind", + "continuationId" + ], + "additionalProperties": false, + "description": "The service is still completing the connection without a consent URL.", + "stability": "experimental" + } + ], + "description": "Typed result of initiating or continuing a Connector connection.", + "title": "ConnectorConnectResult", + "stability": "experimental" + }, + "ConnectorContinueRequest": { + "type": "object", + "properties": { + "continuationId": { + "type": "string", + "description": "Opaque continuation ID returned by connect, reconnect, or an earlier continuation." + }, + "maxAttempts": { + "type": "integer", + "minimum": 0, + "description": "Maximum catalog requests made by this call. Must be between one and the capability limit." + }, + "pollIntervalMs": { + "type": "integer", + "minimum": 0, + "description": "Delay in milliseconds between attempts. Must not exceed the capability limit." + }, + "deadlineMs": { + "type": "integer", + "minimum": 0, + "description": "Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit." + } + }, + "required": [ + "continuationId", + "maxAttempts", + "pollIntervalMs", + "deadlineMs" + ], + "additionalProperties": false, + "description": "Explicitly bounded continuation of a pending Connector connection.", + "title": "ConnectorContinueRequest", + "stability": "experimental" + }, + "ConnectorDisconnectResult": { + "type": "object", + "properties": { + "disconnected": { + "type": "boolean", + "description": "Whether the service accepted the idempotent disconnect." + }, + "status": { + "$ref": "#/definitions/ConnectorStatus", + "description": "Fresh authoritative session state after removing Connector-owned MCP servers." + } + }, + "required": [ + "disconnected", + "status" + ], + "additionalProperties": false, + "description": "Authoritative result after disconnect and MCP reconciliation.", + "title": "ConnectorDisconnectResult", + "stability": "experimental" + }, + "ConnectorMcpStatus": { + "type": "string", + "enum": [ + "connected", + "pending", + "needs_auth", + "failed", + "stopped", + "disabled", + "not_configured" + ], + "description": "Live MCP status of one Connector-owned runtime server.", + "title": "ConnectorMcpStatus", + "x-enumDescriptions": { + "connected": "The server is connected and its tools are available.", + "pending": "The server connection is still being established.", + "needs_auth": "The server requires refreshed GitHub authorization.", + "failed": "The server failed to connect or initialize.", + "stopped": "The server is intentionally stopped, including when managed policy blocks it.", + "disabled": "The server is configured but explicitly disabled.", + "not_configured": "The Connector currently has no live server configuration." + }, + "stability": "experimental" + }, + "ConnectorReconcileRequest": { + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Opaque account selection ID. It must match the account already pinned to the session, if any." + }, + "refreshCatalog": { + "type": "boolean", + "description": "When true, refresh the catalog before reconciling. A disabled Connector API performs no service request." + } + }, + "required": [ + "accountId" + ], + "additionalProperties": false, + "description": "Requests authoritative Connector-to-MCP reconciliation for the pinned account.", + "title": "ConnectorReconcileRequest", + "stability": "experimental" + }, + "ConnectorRuntimeStatus": { + "type": "object", + "properties": { + "runtimeServerId": { + "type": "string", + "description": "Opaque runtime server ID." + }, + "connectorName": { + "type": "string", + "description": "Canonical Connector name that owns this server." + }, + "status": { + "$ref": "#/definitions/ConnectorMcpStatus", + "description": "Current live MCP host status." + } + }, + "required": [ + "runtimeServerId", + "connectorName", + "status" + ], + "additionalProperties": false, + "description": "Live status of one session-owned MCP projection.", + "title": "ConnectorRuntimeStatus", + "stability": "experimental" + }, + "ConnectorStatus": { + "type": "object", + "properties": { + "apiVersion": { + "type": "integer", + "minimum": 0, + "description": "Connector API contract version." + }, + "availability": { + "$ref": "#/definitions/ConnectorAvailability", + "description": "Current feature and session availability." + }, + "accountId": { + "type": "string", + "description": "Opaque account selection pinned to this session, when one has been selected." + }, + "catalog": { + "$ref": "#/definitions/ConnectorCatalogResult", + "description": "Latest validated catalog snapshot, when available." + }, + "runtimeServers": { + "type": "array", + "items": { + "$ref": "#/definitions/ConnectorRuntimeStatus", + "description": "Live status of one session-owned MCP projection." + }, + "description": "Live MCP status for every Connector-owned runtime server." + }, + "pendingConnections": { + "type": "integer", + "minimum": 0, + "description": "Number of active opaque connection continuations." + } + }, + "required": [ + "apiVersion", + "availability", + "runtimeServers", + "pendingConnections" + ], + "additionalProperties": false, + "description": "Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included.", + "title": "ConnectorStatus", + "stability": "experimental" + } + } +} diff --git a/scripts/codegen/connectorSessionApiOverlay.ts b/scripts/codegen/connectorSessionApiOverlay.ts new file mode 100644 index 0000000000..f90334d392 --- /dev/null +++ b/scripts/codegen/connectorSessionApiOverlay.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import fs from "fs"; + +type SchemaNode = Record; + +interface ConnectorSessionApiOverlay { + sessionConnectors: SchemaNode; + definitions: Record; +} + +const overlay = JSON.parse( + fs.readFileSync( + new URL("./connector-session-api-overlay.json", import.meta.url), + "utf8", + ), +) as ConnectorSessionApiOverlay; + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (value !== null && typeof value === "object") { + return `{${Object.entries(value as SchemaNode) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function requireObject(parent: SchemaNode, key: string): SchemaNode { + const value = parent[key]; + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error( + `Connector session API overlay requires ${key} to be an object.`, + ); + } + return value as SchemaNode; +} + +/** + * Applies the public Connector session contract pending schema publication. + * + * Remove this overlay after the pinned runtime release publishes the contract. + * Only the complete pre-contract or exact target shape is accepted. + */ +export function applyConnectorSessionApiOverlay( + schema: T, + fileName: string, +): T { + if (fileName !== "api.schema.json") { + return schema; + } + if (schema === null || typeof schema !== "object" || Array.isArray(schema)) { + throw new Error( + `Connector session API overlay cannot process ${fileName}.`, + ); + } + + const root = schema as SchemaNode; + const session = requireObject(root, "session"); + const definitions = requireObject(root, "definitions"); + const nodes = [ + { + parent: session, + key: "connectors", + desired: overlay.sessionConnectors, + }, + ...Object.entries(overlay.definitions).map(([key, desired]) => ({ + parent: definitions, + key, + desired, + })), + ]; + + const isLegacy = nodes.every(({ parent, key }) => parent[key] === undefined); + const isTarget = nodes.every( + ({ parent, key, desired }) => + stableStringify(parent[key]) === stableStringify(desired), + ); + if (!isLegacy && !isTarget) { + throw new Error( + "Connector session API overlay found a partial or unknown upstream contract. " + + "Update or remove scripts/codegen/connector-session-api-overlay.json.", + ); + } + if (isLegacy) { + for (const { parent, key, desired } of nodes) { + parent[key] = clone(desired); + } + } + return schema; +} diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 714988cb05..3e12cd554d 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -16,6 +16,8 @@ import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; + +import { applyConnectorSessionApiOverlay } from "./connectorSessionApiOverlay.js"; import { addManagedApprovalRequiredToPermissionRequests, type ApiSchema, @@ -89,6 +91,9 @@ const STRING_ENUM_VARIANT_OVERRIDES: Record> = { CatalogTrustEligibility: { unknown: "UnknownValue", }, + ConnectorCatalogStatus: { + unknown: "UnknownValue", + }, }; // ── Naming helpers ────────────────────────────────────────────────────────── @@ -2240,7 +2245,10 @@ async function generate(): Promise { JSON.parse(await fs.readFile(sessionEventsSchemaPath, "utf-8")), ); const apiRaw = normalizeSchemaBrandCasing( - JSON.parse(await fs.readFile(apiSchemaPath, "utf-8")) as ApiSchema, + applyConnectorSessionApiOverlay( + JSON.parse(await fs.readFile(apiSchemaPath, "utf-8")) as ApiSchema, + path.basename(apiSchemaPath), + ), ); const sessionEventsSchema = propagateInternalVisibility( diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index bad31a7675..207729b750 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -15,6 +15,8 @@ import { promisify } from "util"; import { COPILOT_CLI_VERSION } from "../../nodejs/src/cliVersion.js"; import { ensureCopilotPackage } from "../../nodejs/scripts/releaseArtifacts.js"; +import { applyConnectorSessionApiOverlay } from "./connectorSessionApiOverlay.js"; + export const execFileAsync = promisify(execFile); const __filename = fileURLToPath(import.meta.url); @@ -151,7 +153,9 @@ function renameBrandDefinitionKeys(defs: Record): void { /** Load a JSON schema file and normalize GitHub brand casing in titles, refs, and definition keys. */ export async function loadSchemaJson(filePath: string): Promise { const parsed = JSON.parse(await fs.readFile(filePath, "utf-8")) as T; - return normalizeSchemaBrandCasing(parsed); + return normalizeSchemaBrandCasing( + applyConnectorSessionApiOverlay(parsed, path.basename(filePath)) + ); } // ── Schema processing ─────────────────────────────────────────────────────── From 8b0e46ad39d7e5a46d4e1a989a96634418a6c181 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Fri, 18 Sep 2026 16:15:44 +0100 Subject: [PATCH 13/16] Clarify Rust Connector availability Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/README.md b/rust/README.md index e78154fc73..dd3488e7f6 100644 --- a/rust/README.md +++ b/rust/README.md @@ -259,7 +259,7 @@ maintenance. #### Connectors (experimental) -When the runtime's Connector experiment is enabled, use the generated +When the experimental runtime Connector API is available, use the generated session-scoped namespace to discover and manage Connectors: ```rust,ignore From 743b56cfd7bf8d69da5871f2dde174d240985778 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Fri, 18 Sep 2026 16:41:27 +0100 Subject: [PATCH 14/16] Address Connector API review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- java/scripts/codegen/java.ts | 58 +++++++++-- .../AssistantMessageReasoningBlocks.java | 4 + .../AssistantMessageServerTools.java | 4 + .../CanvasRegistryChangedCanvas.java | 4 + .../CanvasRegistryChangedCanvasAction.java | 4 + .../copilot/generated/CitableSource.java | 4 + .../copilot/generated/CitationProvider.java | 4 + .../copilot/generated/CitationReference.java | 4 + .../copilot/generated/CitationSource.java | 4 + .../copilot/generated/CitationSpan.java | 4 + .../github/copilot/generated/Citations.java | 4 + .../copilot/generated/FusionAttribution.java | 4 + .../generated/FusionConversationScope.java | 4 + .../generated/FusionFollowUpAction.java | 4 + .../FusionFollowUpRecommendation.java | 4 + .../copilot/generated/FusionPattern.java | 4 + .../generated/FusionPhaseActivityKind.java | 4 + .../copilot/generated/FusionPhaseKind.java | 4 + .../generated/FusionPhasePlanStep.java | 4 + .../copilot/generated/FusionPhaseStatus.java | 4 + .../copilot/generated/FusionPhaseUsage.java | 4 + .../generated/FusionProjectionMode.java | 4 + .../copilot/generated/FusionScores.java | 4 + .../generated/FusionStagedTerminal.java | 4 + .../copilot/generated/FusionTurnKind.java | 4 + ...ermissionMessageAuthorizationPolarity.java | 4 + .../copilot/generated/PermissionMode.java | 4 + .../ToolExecutionCompleteShellExecution.java | 4 + .../generated/UIEphemeralQueryPhase.java | 4 + .../rpc/AutopilotObjectiveCreditLimit.java | 4 + .../rpc/AutopilotObjectiveState.java | 4 + .../rpc/AutopilotObjectiveStatus.java | 4 + .../generated/rpc/BuiltinToolDescriptor.java | 4 + .../generated/rpc/BuiltinToolFormat.java | 4 + .../generated/rpc/BuiltinToolFormatType.java | 4 + .../generated/rpc/BuiltinToolInputSchema.java | 4 + .../rpc/BuiltinToolInputSchemaType.java | 4 + .../copilot/generated/rpc/CanvasAction.java | 4 + .../generated/rpc/CanvasHostContext.java | 4 + .../rpc/CanvasHostContextCapabilities.java | 4 + .../generated/rpc/CanvasSessionContext.java | 4 + .../rpc/CommandsInvocationEffectOutcome.java | 4 + .../generated/rpc/ConnectorAvailability.java | 4 + .../generated/rpc/ConnectorCatalogEntry.java | 4 + .../generated/rpc/ConnectorCatalogResult.java | 4 + .../generated/rpc/ConnectorCatalogStatus.java | 4 + .../generated/rpc/ConnectorConnectResult.java | 4 + .../rpc/ConnectorConnectResultConnected.java | 4 + ...ConnectorConnectResultConsentRequired.java | 4 + .../rpc/ConnectorConnectResultPending.java | 4 + .../generated/rpc/ConnectorMcpStatus.java | 4 + .../generated/rpc/ConnectorRuntimeStatus.java | 4 + .../generated/rpc/ConnectorStatus.java | 4 + .../rpc/ContentExclusionPathCheck.java | 4 + .../generated/rpc/DiscoveredCanvas.java | 4 + .../generated/rpc/ExtensionLaunchProfile.java | 4 + ...ExtensionLaunchProviderResolveRequest.java | 4 + .../generated/rpc/FactoryAgentOptions.java | 4 + .../generated/rpc/FactoryAgentSummary.java | 4 + .../generated/rpc/FactoryCurrentPhase.java | 4 + .../generated/rpc/FactoryDeclaredLimits.java | 4 + .../copilot/generated/rpc/FactoryLogLine.java | 4 + .../rpc/FactoryPhaseObservation.java | 4 + .../generated/rpc/FactoryPhaseStatus.java | 4 + .../generated/rpc/FactoryProgressLine.java | 4 + .../generated/rpc/FactoryProgressPage.java | 4 + .../generated/rpc/FactoryRunConsumed.java | 4 + .../generated/rpc/FactoryRunLimits.java | 4 + .../generated/rpc/FactoryRunSummary.java | 4 + .../generated/rpc/FactoryRunTerminal.java | 4 + .../generated/rpc/FactoryToolRunOptions.java | 4 + .../rpc/GitHubTelemetryClientInfo.java | 4 + .../generated/rpc/GitHubTelemetryEvent.java | 4 + .../rpc/GitHubTelemetryNotification.java | 4 + .../rpc/GitHubTokenAcquireRequest.java | 4 + .../rpc/GitHubTokenAcquireResult.java | 4 + .../rpc/McpExecuteSamplingResult.java | 4 + .../rpc/ModelSwitchAutoTierStatus.java | 4 + .../generated/rpc/OpenCanvasInstance.java | 4 + .../copilot/generated/rpc/PermissionMode.java | 4 + .../generated/rpc/PermissionModeSource.java | 4 + .../rpc/ProtocolExternalToolDefer.java | 4 + .../rpc/ProtocolExternalToolDefinition.java | 4 + .../copilot/generated/rpc/ProviderConfig.java | 4 + .../copilot/generated/rpc/RunOptions.java | 4 + .../copilot/generated/rpc/SandboxConfig.java | 4 + .../rpc/SessionFsSqliteTransactionError.java | 4 + .../SessionFsSqliteTransactionStatement.java | 4 + .../generated/rpc/ShellCredentials.java | 4 + .../generated/rpc/ToolResultExpanded.java | 4 + .../generated/rpc/ToolResultNewMessage.java | 4 + .../copilot/generated/rpc/ToolResultType.java | 4 + .../CopilotExperimentalProcessorTest.java | 21 ++++ .../rpc/ConnectorSerializationTest.java | 2 + python/test_connectors.py | 97 ++++++++++++++----- .../connector-session-api-overlay.json | 4 +- 96 files changed, 513 insertions(+), 33 deletions(-) diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 234d24beaa..93b9289769 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -29,6 +29,19 @@ function isSchemaInternal(schema: JSONSchema7 | null | undefined): boolean { (schema as Record).visibility === "internal"; } +function isSchemaExperimental(schema: JSONSchema7 | null | undefined): boolean { + return typeof schema === "object" && + schema !== null && + (schema as Record).stability === "experimental"; +} + +function appendExperimentalTypeApiNote(lines: string[], schema: JSONSchema7): void { + if (isSchemaExperimental(schema)) { + lines.push(` *`); + lines.push(` * @apiNote This type is experimental and may change in a future version.`); + } +} + const AUTO_GENERATED_HEADER = `// AUTO-GENERATED FILE - DO NOT EDIT`; const GENERATED_FROM_SESSION_EVENTS = `// Generated from: session-events.schema.json`; const GENERATED_FROM_API = `// Generated from: api.schema.json`; @@ -437,6 +450,7 @@ async function generatePolymorphicResultClass( const anyOf = schema.anyOf as JSONSchema7[]; const variants = resolveAnyOfVariants(anyOf); const discriminator = findDiscriminator(variants); + const experimental = isSchemaExperimental(schema); if (!discriminator) { console.warn(`[codegen] Cannot find discriminator for ${className} — skipping polymorphic generation`); @@ -468,11 +482,15 @@ async function generatePolymorphicResultClass( baseLines.push(`import com.fasterxml.jackson.annotation.JsonIgnoreProperties;`); baseLines.push(`import com.fasterxml.jackson.annotation.JsonSubTypes;`); baseLines.push(`import com.fasterxml.jackson.annotation.JsonTypeInfo;`); + if (experimental) { + baseLines.push(`import com.github.copilot.CopilotExperimental;`); + } baseLines.push(`import javax.annotation.processing.Generated;`); baseLines.push(""); - if (schema.description) { + if (schema.description || experimental) { baseLines.push(`/**`); - baseLines.push(` * ${schema.description}`); + baseLines.push(` * ${schema.description ?? `Polymorphic result type {@code ${className}}.`}`); + appendExperimentalTypeApiNote(baseLines, schema); baseLines.push(` *`); baseLines.push(` * @since 1.0.0`); baseLines.push(` */`); @@ -488,6 +506,9 @@ async function generatePolymorphicResultClass( baseLines.push(` @JsonSubTypes.Type(value = ${v.variantClassName}.class, name = "${v.discriminatorValue}")${comma}`); } baseLines.push(`})`); + if (experimental) { + baseLines.push(`@CopilotExperimental`); + } baseLines.push(`@JsonIgnoreProperties(ignoreUnknown = true)`); baseLines.push(GENERATED_ANNOTATION); baseLines.push(`public abstract class ${className} {`); @@ -521,12 +542,16 @@ async function generatePolymorphicVariantClass( packageName: string, packageDir: string ): Promise { + const experimental = isSchemaExperimental(schema); const allImports = new Set([ "com.fasterxml.jackson.annotation.JsonIgnoreProperties", "com.fasterxml.jackson.annotation.JsonInclude", "com.fasterxml.jackson.annotation.JsonProperty", "javax.annotation.processing.Generated", ]); + if (experimental) { + allImports.add("com.github.copilot.CopilotExperimental"); + } const nestedTypes = new Map(); // Collect fields (excluding the discriminator property) @@ -571,16 +596,21 @@ async function generatePolymorphicVariantClass( if (schema.description) { lines.push(`/**`); lines.push(` * ${schema.description}`); + appendExperimentalTypeApiNote(lines, schema); lines.push(` *`); lines.push(` * @since 1.0.0`); lines.push(` */`); } else { lines.push(`/**`); lines.push(` * Variant {@code ${discriminatorValue}} of {@link ${baseClassName}}.`); + appendExperimentalTypeApiNote(lines, schema); lines.push(` *`); lines.push(` * @since 1.0.0`); lines.push(` */`); } + if (experimental) { + lines.push(`@CopilotExperimental`); + } lines.push(`@JsonIgnoreProperties(ignoreUnknown = true)`); lines.push(`@JsonInclude(JsonInclude.Include.NON_NULL)`); lines.push(GENERATED_ANNOTATION); @@ -1283,6 +1313,7 @@ async function generateStandaloneEnum( headerComment: string ): Promise { const values = schema.enum as string[]; + const experimental = isSchemaExperimental(schema); const lines: string[] = []; lines.push(COPYRIGHT); lines.push(""); @@ -1291,15 +1322,22 @@ async function generateStandaloneEnum( lines.push(""); lines.push(`package ${packageName};`); lines.push(""); + if (experimental) { + lines.push(`import com.github.copilot.CopilotExperimental;`); + } lines.push(`import javax.annotation.processing.Generated;`); lines.push(""); - if (schema.description) { + if (schema.description || experimental) { lines.push(`/**`); - lines.push(` * ${schema.description}`); + lines.push(` * ${schema.description ?? `Values for {@code ${name}}.`}`); + appendExperimentalTypeApiNote(lines, schema); lines.push(` *`); lines.push(` * @since 1.0.0`); lines.push(` */`); } + if (experimental) { + lines.push(`@CopilotExperimental`); + } lines.push(GENERATED_ANNOTATION); lines.push(`public enum ${name} {`); for (let i = 0; i < values.length; i++) { @@ -1335,6 +1373,7 @@ async function generateStandaloneRecord( ): Promise { const nestedTypes = new Map(); const { code, imports } = generateRpcClass(name, schema, nestedTypes, packageName); + const experimental = isSchemaExperimental(schema); const lines: string[] = []; lines.push(COPYRIGHT); @@ -1352,19 +1391,26 @@ async function generateStandaloneRecord( "javax.annotation.processing.Generated", ...imports, ]); + if (experimental) { + allImports.add("com.github.copilot.CopilotExperimental"); + } const sortedImports = [...allImports].sort(); for (const imp of sortedImports) { lines.push(`import ${imp};`); } lines.push(""); - if (schema.description) { + if (schema.description || experimental) { lines.push(`/**`); - lines.push(` * ${schema.description}`); + lines.push(` * ${schema.description ?? `Data type {@code ${name}}.`}`); + appendExperimentalTypeApiNote(lines, schema); lines.push(` *`); lines.push(` * @since 1.0.0`); lines.push(` */`); } + if (experimental) { + lines.push(`@CopilotExperimental`); + } lines.push(GENERATED_ANNOTATION); lines.push(code); lines.push(""); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java index ee9c5fd485..3603e09f1e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageServerTools.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageServerTools.java index 6d67f6ecbe..eb75a75121 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageServerTools.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageServerTools.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import java.util.Map; import javax.annotation.processing.Generated; @@ -17,8 +18,11 @@ /** * Neutral provider-tagged server-side tool-use payload (tool search, advisor) for verbatim round-tripping * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java b/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java index 12518491e8..b0f2a8d23e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvas.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * A single canvas declaration in `session.canvas.registry_changed`, including provider IDs, display metadata, input schema, and actions. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvasAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvasAction.java index 99c390efb2..68fe3a6444 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvasAction.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CanvasRegistryChangedCanvasAction.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * A single action within a canvas declaration, with its name, optional description, and optional input schema. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CitableSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/CitableSource.java index c66809fc15..6f11f99bc1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CitableSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CitableSource.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * A source supplied by a tool that should be made available to the model as citable content. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CitationProvider.java b/java/sdk/src/generated/java/com/github/copilot/generated/CitationProvider.java index 46a02b2561..4122059df8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CitationProvider.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CitationProvider.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * The system that produced a citation. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum CitationProvider { /** The {@code anthropic} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CitationReference.java b/java/sdk/src/generated/java/com/github/copilot/generated/CitationReference.java index e9ad222c10..db15713df4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CitationReference.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CitationReference.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * A single citation occurrence linking a span of generated text to a supporting source. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CitationSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/CitationSource.java index 561c5eced7..a1daf7def4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CitationSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CitationSource.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * A source that backs one or more cited spans in the assistant's response. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CitationSpan.java b/java/sdk/src/generated/java/com/github/copilot/generated/CitationSpan.java index aaa8647a64..6aeb1d994c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CitationSpan.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CitationSpan.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * A contiguous span of generated assistant text and the source references that support it. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/Citations.java b/java/sdk/src/generated/java/com/github/copilot/generated/Citations.java index c153c39a7b..c18758e87e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/Citations.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/Citations.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Provider-agnostic citations linking spans of the assistant's response to their supporting sources. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionAttribution.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionAttribution.java index 8e4ae7e937..90de85e586 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionAttribution.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionAttribution.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionConversationScope.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionConversationScope.java index d38381ed48..702c43ea7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionConversationScope.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionConversationScope.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Conversation scope in which a HydraFusion phase executes. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionConversationScope { /** The {@code root} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpAction.java index e54bf4604f..e9ec06aff8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpAction.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpAction.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Server-recommended routing behavior for a later HydraFusion turn. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionFollowUpAction { /** The {@code reuse_primary} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpRecommendation.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpRecommendation.java index 101d497fb2..f2678d9373 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpRecommendation.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionFollowUpRecommendation.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Durable server recommendation for subsequent HydraFusion turns. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPattern.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPattern.java index d37ef4ee83..ed9a2391a6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPattern.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPattern.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Validated HydraFusion execution pattern. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionPattern { /** The {@code single} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java index a95a08cf91..e0b10db828 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Content-safe activity observed while a HydraFusion phase is running. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionPhaseActivityKind { /** The {@code model_output} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseKind.java index ead79b75fc..512ea698ef 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseKind.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseKind.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * HydraFusion phase kind. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionPhaseKind { /** The {@code primary} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java index cfb07a789a..de40eaf0ac 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Presentation-neutral phase planned for a HydraFusion turn. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseStatus.java index f670332b5c..88f8a751aa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseStatus.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Durable outcome status of a HydraFusion phase. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionPhaseStatus { /** The {@code succeeded} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseUsage.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseUsage.java index 4a707bb69e..a21ece2845 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseUsage.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseUsage.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Aggregate concrete-model usage for one HydraFusion phase. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionProjectionMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionProjectionMode.java index 9681efc0ba..ef705b48d2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionProjectionMode.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionProjectionMode.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * How a durable phase checkpoint contributes its exact message to canonical root history. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionProjectionMode { /** The {@code append} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionScores.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionScores.java index eefb8190a1..ae2b219d9d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionScores.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionScores.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Validated HydraFusion routing capability scores. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionStagedTerminal.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionStagedTerminal.java index 262c8c664b..297a4325d4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionStagedTerminal.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionStagedTerminal.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionTurnKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionTurnKind.java index 24e39b976c..ec55bab2bb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/FusionTurnKind.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionTurnKind.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Kind of turn for which HydraFusion routing is running. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FusionTurnKind { /** The {@code user} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationPolarity.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationPolarity.java index 14bc8e2895..e142238e7b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationPolarity.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationPolarity.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Which direction a message-backed authorization claim moves authority in. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum PermissionMessageAuthorizationPolarity { /** The {@code grant} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMode.java index 421ad76139..fec188d66e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMode.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMode.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Permission mode for the session. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum PermissionMode { /** The {@code manual} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteShellExecution.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteShellExecution.java index e08ac623f9..4c9a304c40 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteShellExecution.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteShellExecution.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Experimental shell completion facts retained independently of the full tool result. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UIEphemeralQueryPhase.java b/java/sdk/src/generated/java/com/github/copilot/generated/UIEphemeralQueryPhase.java index 7798c159eb..63a53aa0bb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/UIEphemeralQueryPhase.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UIEphemeralQueryPhase.java @@ -7,13 +7,17 @@ package com.github.copilot.generated; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Lifecycle phase for a Rust-owned ephemeral query stream. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum UIEphemeralQueryPhase { /** The {@code started} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java index d3f6a585b8..776e25273a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Current per-window credit limit and consumption for an autopilot objective. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java index 657884c4f6..3b2c3002fa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Public, persistence-independent projection of an autopilot objective. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java index b50cf2072c..024ca37c43 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Current normalized autopilot objective lifecycle status. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum AutopilotObjectiveStatus { /** The {@code active} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolDescriptor.java index 057498fb98..e36268609f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolDescriptor.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolDescriptor.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Rust-owned metadata and input schema for a built-in tool. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormat.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormat.java index 7af5ef2d45..c03c92afc4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormat.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormat.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Custom grammar input format accepted by a built-in tool. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormatType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormatType.java index 6239493671..fcb277e6c9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormatType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolFormatType.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Custom input-format kind. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum BuiltinToolFormatType { /** The {@code grammar} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchema.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchema.java index 764450b37e..277eba0751 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchema.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchema.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * JSON Schema object accepted by a built-in tool. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchemaType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchemaType.java index 729f3329b5..d935afb4a6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchemaType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/BuiltinToolInputSchemaType.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Root JSON Schema type for a built-in tool input. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum BuiltinToolInputSchemaType { /** The {@code object} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasAction.java index 36554ed767..b31f9d6fdb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasAction.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasAction.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Canvas action that the agent or host can invoke. To discover the input schema for a particular action, call the list_canvas_capabilities tool. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContext.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContext.java index 8dba8156e4..cf09ce5679 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContext.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContext.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Host context supplied by the runtime. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContextCapabilities.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContextCapabilities.java index a8118eeebf..c3133d0099 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContextCapabilities.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasHostContextCapabilities.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Host capabilities * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasSessionContext.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasSessionContext.java index 422d32e542..d3c5c75bde 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasSessionContext.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CanvasSessionContext.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Session context supplied by the runtime. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java index 79f3be8baf..65293f1219 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Whether a pending slash-command invocation effect was applied or cancelled by the host. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum CommandsInvocationEffectOutcome { /** The {@code applied} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java index 2b2b19dc53..00f11aed79 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorAvailability.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Availability of the EXPERIMENTAL session connector API. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum ConnectorAvailability { /** The {@code enabled} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java index 612dd11f8b..53c3750792 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Credential-free Connector catalog entry. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java index 2e4102daf9..2d10c7ad93 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogResult.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Validated Connector catalog snapshot cached by the session. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java index 50f4ede020..7be156a58b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogStatus.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Authoritative service connection state for one Connector. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum ConnectorCatalogStatus { /** The {@code not_connected} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java index 53a6cd0884..3598e407d1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResult.java @@ -10,11 +10,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Typed result of initiating or continuing a Connector connection. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) @@ -23,6 +26,7 @@ @JsonSubTypes.Type(value = ConnectorConnectResultConsentRequired.class, name = "consent_required"), @JsonSubTypes.Type(value = ConnectorConnectResultPending.class, name = "pending") }) +@CopilotExperimental @JsonIgnoreProperties(ignoreUnknown = true) @javax.annotation.processing.Generated("copilot-sdk-codegen") public abstract class ConnectorConnectResult { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java index 4eb5f6f63f..cf5051e62c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConnected.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * The service is connected and the session MCP graph was reconciled. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java index c501753aaf..3552bdc9e3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultConsentRequired.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Host-owned consent is required before bounded continuation can complete. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java index 8ef1f4f608..a31a009382 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorConnectResultPending.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * The service is still completing the connection without a consent URL. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java index 97fbf5a9bf..d517230f4f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorMcpStatus.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Live MCP status of one Connector-owned runtime server. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum ConnectorMcpStatus { /** The {@code connected} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java index 61f01b82fa..af01caaba8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorRuntimeStatus.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Live status of one session-owned MCP projection. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java index 00bbfbef9d..ca0505ea0e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorStatus.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ContentExclusionPathCheck.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ContentExclusionPathCheck.java index ba48326c2e..5dd0809887 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ContentExclusionPathCheck.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ContentExclusionPathCheck.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Content-exclusion decision for one requested path. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java index 0b0c518040..ce8759ee33 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredCanvas.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Canvas available in the current session. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProfile.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProfile.java index e7590c7f9e..a2c5bb7ed2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProfile.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProfile.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import java.util.Map; import javax.annotation.processing.Generated; @@ -17,8 +18,11 @@ /** * Opaque integrator-owned process launch profile for one extension entrypoint. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProviderResolveRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProviderResolveRequest.java index 7b520f9067..d33956d10d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProviderResolveRequest.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ExtensionLaunchProviderResolveRequest.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * A discovered extension entrypoint that the registered integrator may classify and resolve to an opaque launch profile. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentOptions.java index 51b9077ba1..35470e7199 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentOptions.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Options for one factory-scoped subagent call. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentSummary.java index d1bd2329b2..d063fc9c6b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentSummary.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryAgentSummary.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Prompt-safe durable identity and live status for a direct factory agent. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryCurrentPhase.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryCurrentPhase.java index e0a30f51a6..c15631835d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryCurrentPhase.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryCurrentPhase.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Current factory phase identity. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryDeclaredLimits.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryDeclaredLimits.java index 7fa99b005b..7e2e4e6b5b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryDeclaredLimits.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryDeclaredLimits.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Declared or approved factory resource ceilings. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLine.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLine.java index 28a9690450..1f7afdd583 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLine.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryLogLine.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * One ordered factory progress line. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseObservation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseObservation.java index aa96e0cb90..167766885c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseObservation.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseObservation.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Durable lifecycle and timing for one factory phase. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseStatus.java index d9fea0bc36..8432507176 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryPhaseStatus.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Derived lifecycle state of a factory phase. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum FactoryPhaseStatus { /** The {@code pending} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressLine.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressLine.java index 3a26b67d79..c9ed149610 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressLine.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressLine.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * One durable factory progress record. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressPage.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressPage.java index 76278a1585..935140612e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressPage.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryProgressPage.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * A bidirectional page of factory progress. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunConsumed.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunConsumed.java index a492db26f6..5ffc43435d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunConsumed.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunConsumed.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Durable factory resource consumption. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunLimits.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunLimits.java index 79304772a0..300e4977b3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunLimits.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunLimits.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Wire-only per-invocation factory resource ceiling overrides. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunSummary.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunSummary.java index e58b774bcb..56d1f91763 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunSummary.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunSummary.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Durable factory run summary with read-time live overlays. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunTerminal.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunTerminal.java index 0123e19793..0b0db7d9b7 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunTerminal.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunTerminal.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Prompt-safe terminal factory outcome. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryToolRunOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryToolRunOptions.java index cc3e7c8da1..e2444c7053 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryToolRunOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryToolRunOptions.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Options for an internal tool-originated factory invocation. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryClientInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryClientInfo.java index b6bef1bffd..513fa2ec98 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryClientInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryClientInfo.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Client environment metadata describing the process that produced a telemetry event. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryEvent.java index efbf920b4f..58c550af05 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryEvent.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.Map; import javax.annotation.processing.Generated; /** * A single telemetry event in the runtime's native GitHub-shaped telemetry format, forwarded verbatim to opted-in hosts. The `restricted` flag on the enclosing GitHubTelemetryNotification distinguishes standard from restricted events; the payload shape is identical for both. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryNotification.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryNotification.java index 6059f1ff6c..7d1cf5b1a3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryNotification.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTelemetryNotification.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Payload for a `gitHubTelemetry.event` notification: a single GitHub telemetry event the runtime forwards to a host connection that opted into telemetry forwarding during the `server.connect` handshake. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireRequest.java index 59ba9d8ed5..890af9a526 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireRequest.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireRequest.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Asks the SDK client to acquire a GitHub access token from an opaque callback registration. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireResult.java index a20724c337..f0d0b2d3ee 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubTokenAcquireResult.java @@ -10,11 +10,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * SDK host response to a GitHub credential request. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) @@ -22,6 +25,7 @@ @JsonSubTypes.Type(value = GitHubTokenAcquireResultToken.class, name = "token"), @JsonSubTypes.Type(value = GitHubTokenAcquireResultCancelled.class, name = "cancelled") }) +@CopilotExperimental @JsonIgnoreProperties(ignoreUnknown = true) @javax.annotation.processing.Generated("copilot-sdk-codegen") public abstract class GitHubTokenAcquireResult { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpExecuteSamplingResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpExecuteSamplingResult.java index 18a838d30c..07b07ab376 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpExecuteSamplingResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpExecuteSamplingResult.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * MCP CreateMessageResult payload (with optional 'tools' extension), present when action='success'. Treated as opaque at the schema layer; consumers should construct/consume it per the MCP CreateMessageResult shape. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java index e4a1baed5e..f3f607dbfd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Whether the requested preference was already effective or was accepted for later transactional activation. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum ModelSwitchAutoTierStatus { /** The {@code unchanged} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java index f38ba82c4f..d84108e62b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/OpenCanvasInstance.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Open canvas instance snapshot. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionMode.java index 1f4ea9b6bc..ddbd006872 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionMode.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionMode.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Current or requested permission mode. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum PermissionMode { /** The {@code manual} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionModeSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionModeSource.java index 107ac3f6d9..52f3e6cea3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionModeSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PermissionModeSource.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Optional source for permission-mode telemetry. Defaults to `rpc` when omitted for SDK callers. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum PermissionModeSource { /** The {@code cli_flag} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefer.java index 58f1ec24d5..0129cf4091 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefer.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Controls whether the runtime may defer loading an external tool definition. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum ProtocolExternalToolDefer { /** The {@code auto} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefinition.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefinition.java index 5e7095700c..27d0c23a9b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefinition.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProtocolExternalToolDefinition.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.Map; import javax.annotation.processing.Generated; /** * Serializable definition of a caller-implemented tool whose execution is handled over the SDK connection. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java index 25cc365ab0..22c7414996 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.Map; import javax.annotation.processing.Generated; /** * Custom model-provider configuration (BYOK). * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RunOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RunOptions.java index 11ac51827a..ae535af8cf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RunOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RunOptions.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Options controlling factory invocation. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java index dd522f7a34..0b02851782 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Resolved sandbox configuration. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionError.java index e25eaed867..6fb780a3aa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionError.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Classified SQLite transaction failure. busyOrLocked guarantees rollback; postCommitAmbiguous must never be retried. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionStatement.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionStatement.java index f56f268ba1..2d21d0a52c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionStatement.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSqliteTransactionStatement.java @@ -10,14 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.Map; import javax.annotation.processing.Generated; /** * One statement in an atomic SQLite transaction. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ShellCredentials.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ShellCredentials.java index 53567c98db..01251b5a13 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ShellCredentials.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ShellCredentials.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -30,9 +31,12 @@ retained between spawns, replacing the session credential (`session.gitHubAuth.s presents, not about what the agent may touch, and it works on every platform whether or not an OS sandboxing backend is available. `sandboxConfig.auth` remains the sandbox-scoped spelling and is additive with this one. + * + * @apiNote This type is experimental and may change in a future version. * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultExpanded.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultExpanded.java index 47527cfec8..db2160eecb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultExpanded.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultExpanded.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import java.util.Map; import javax.annotation.processing.Generated; @@ -17,8 +18,11 @@ /** * Expanded canonical result returned by a session tool. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultNewMessage.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultNewMessage.java index da203a0e98..59cc14d57a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultNewMessage.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultNewMessage.java @@ -10,13 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * A message injected by a tool result. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultType.java index bc3b5ae2ac..5b045c5f56 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ToolResultType.java @@ -7,13 +7,17 @@ package com.github.copilot.generated.rpc; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Execution outcome classification. * + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ +@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") public enum ToolResultType { /** The {@code success} variant. */ diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java index b7005c5d52..3715235bc1 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java @@ -114,6 +114,27 @@ void failsByDefault_whenExtendingExperimentalType() { "Expected compile error for extending experimental type, got: " + diagnostics.getDiagnostics()); } + @Test + void failsByDefault_whenUsingGeneratedExperimentalConnectorTypes() { + for (String type : List.of("ConnectorAvailability", "ConnectorStatus", "ConnectorConnectResult", + "ConnectorConnectResultConnected")) { + String source = """ + package consumer; + import com.github.copilot.generated.rpc.%s; + public class Consumer { + private %s value; + } + """.formatted(type, type); + DiagnosticCollector diagnostics = compile( + List.of(inMemorySource("consumer.Consumer", source)), Collections.emptyList()); + + boolean hasError = diagnostics.getDiagnostics().stream().anyMatch( + d -> d.getKind() == Diagnostic.Kind.ERROR && d.getMessage(null).contains("experimental API")); + assertTrue(hasError, "Expected compile error for generated experimental type " + type + ", got: " + + diagnostics.getDiagnostics()); + } + } + @Test void passes_whenAllowAnnotationIsOnType() { DiagnosticCollector diagnostics = compile( diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java index 3360bc04d3..281569b8f1 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java @@ -9,7 +9,9 @@ import org.junit.jupiter.api.Test; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.AllowCopilotExperimental; +@AllowCopilotExperimental class ConnectorSerializationTest { private static final ObjectMapper MAPPER = new ObjectMapper(); diff --git a/python/test_connectors.py b/python/test_connectors.py index a7e0d8906e..b57dae426f 100644 --- a/python/test_connectors.py +++ b/python/test_connectors.py @@ -30,22 +30,26 @@ async def test_session_connectors_exposes_complete_runtime_api(): ).revision == 1 request = ConnectorConnectRequest(account_id="account-1", connector_name="calendar") - assert ( - await session.rpc.connectors.connect(request) - ).kind is ConnectorConnectResultKind.CONSENT_REQUIRED - assert ( - await session.rpc.connectors.reconnect(request) - ).kind is ConnectorConnectResultKind.PENDING - assert ( - await session.rpc.connectors.continue_connection( - ConnectorContinueRequest( - continuation_id="continuation-1", - max_attempts=3, - poll_interval_ms=100, - deadline_ms=1_000, - ) + consent = await session.rpc.connectors.connect(request) + assert consent.kind is ConnectorConnectResultKind.CONSENT_REQUIRED + assert consent.consent_url == "https://example.com/consent" + assert consent.continuation_id == "continuation-1" + + reconnect = await session.rpc.connectors.reconnect(request) + assert reconnect.kind is ConnectorConnectResultKind.PENDING + assert reconnect.continuation_id == "continuation-1" + + continued = await session.rpc.connectors.continue_connection( + ConnectorContinueRequest( + continuation_id="continuation-1", + max_attempts=3, + poll_interval_ms=100, + deadline_ms=1_000, ) - ).kind is ConnectorConnectResultKind.PENDING + ) + assert continued.kind is ConnectorConnectResultKind.PENDING + assert continued.continuation_id == "continuation-1" + assert (await session.rpc.connectors.disconnect(request)).disconnected assert ( await session.rpc.connectors.reconcile( @@ -53,16 +57,59 @@ async def test_session_connectors_exposes_complete_runtime_api(): ) ).api_version == 1 - assert [call.args[0] for call in transport.request.await_args_list] == [ - "session.connectors.getCapabilities", - "session.connectors.getStatus", - "session.connectors.list", - "session.connectors.refresh", - "session.connectors.connect", - "session.connectors.reconnect", - "session.connectors.continueConnection", - "session.connectors.disconnect", - "session.connectors.reconcile", + assert [(call.args[0], call.args[1]) for call in transport.request.await_args_list] == [ + ("session.connectors.getCapabilities", {"sessionId": "session-1"}), + ("session.connectors.getStatus", {"sessionId": "session-1"}), + ( + "session.connectors.list", + {"accountId": "account-1", "sessionId": "session-1"}, + ), + ( + "session.connectors.refresh", + {"accountId": "account-1", "sessionId": "session-1"}, + ), + ( + "session.connectors.connect", + { + "accountId": "account-1", + "connectorName": "calendar", + "sessionId": "session-1", + }, + ), + ( + "session.connectors.reconnect", + { + "accountId": "account-1", + "connectorName": "calendar", + "sessionId": "session-1", + }, + ), + ( + "session.connectors.continueConnection", + { + "continuationId": "continuation-1", + "deadlineMs": 1_000, + "maxAttempts": 3, + "pollIntervalMs": 100, + "sessionId": "session-1", + }, + ), + ( + "session.connectors.disconnect", + { + "accountId": "account-1", + "connectorName": "calendar", + "sessionId": "session-1", + }, + ), + ( + "session.connectors.reconcile", + { + "accountId": "account-1", + "refreshCatalog": True, + "sessionId": "session-1", + }, + ), ] diff --git a/scripts/codegen/connector-session-api-overlay.json b/scripts/codegen/connector-session-api-overlay.json index 42e6fbf355..eac817e63a 100644 --- a/scripts/codegen/connector-session-api-overlay.json +++ b/scripts/codegen/connector-session-api-overlay.json @@ -192,7 +192,7 @@ }, "maxAttempts": { "type": "integer", - "minimum": 0, + "minimum": 1, "description": "Maximum catalog requests made by this call. Must be between one and the capability limit." }, "pollIntervalMs": { @@ -572,7 +572,7 @@ }, "maxAttempts": { "type": "integer", - "minimum": 0, + "minimum": 1, "description": "Maximum catalog requests made by this call. Must be between one and the capability limit." }, "pollIntervalMs": { From 7d74addf99bd605098165a813680e1f87025b8c7 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Fri, 18 Sep 2026 19:35:07 +0100 Subject: [PATCH 15/16] Align Connector polling bounds with runtime Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 12 ++++++------ go/rpc/zrpc.go | 10 +++++----- .../SessionConnectorsContinueConnectionParams.java | 2 +- nodejs/src/generated/rpc.ts | 2 +- python/copilot/generated/rpc.py | 4 ++-- rust/src/generated/api_types.rs | 8 ++++---- scripts/codegen/connector-session-api-overlay.json | 14 ++++++++++---- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index eb39d84b94..0af77cdba8 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -20013,17 +20013,17 @@ internal sealed class ConnectorContinueRequest [JsonPropertyName("continuationId")] public string ContinuationId { get; set; } = string.Empty; - /// Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. + /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. [JsonPropertyName("deadlineMs")] - public long DeadlineMs { get; set; } + public int DeadlineMs { get; set; } /// Maximum catalog requests made by this call. Must be between one and the capability limit. [JsonPropertyName("maxAttempts")] - public long MaxAttempts { get; set; } + public int MaxAttempts { get; set; } /// Delay in milliseconds between attempts. Must not exceed the capability limit. [JsonPropertyName("pollIntervalMs")] - public long PollIntervalMs { get; set; } + public int PollIntervalMs { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] @@ -39541,10 +39541,10 @@ public async Task ReconnectAsync(string accountId, strin /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. /// Maximum catalog requests made by this call. Must be between one and the capability limit. /// Delay in milliseconds between attempts. Must not exceed the capability limit. - /// Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. + /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. /// The to monitor for cancellation requests. The default is . /// Typed result of initiating or continuing a Connector connection. - public async Task ContinueConnectionAsync(string continuationId, long maxAttempts, long pollIntervalMs, long deadlineMs, CancellationToken cancellationToken = default) + public async Task ContinueConnectionAsync(string continuationId, int maxAttempts, int pollIntervalMs, int deadlineMs, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(continuationId); _session.ThrowIfDisposed(); diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index ab3bd54603..69d571564e 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -2439,13 +2439,13 @@ func (ConnectorConnectResultPending) Kind() ConnectorConnectResultKind { type ConnectorContinueRequest struct { // Opaque continuation ID returned by connect, reconnect, or an earlier continuation. ContinuationID string `json:"continuationId"` - // Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability - // limit. - DeadlineMs int64 `json:"deadlineMs"` + // Maximum wall-clock duration in milliseconds for this call. Must be between one and the + // capability limit. + DeadlineMs int32 `json:"deadlineMs"` // Maximum catalog requests made by this call. Must be between one and the capability limit. - MaxAttempts int64 `json:"maxAttempts"` + MaxAttempts int32 `json:"maxAttempts"` // Delay in milliseconds between attempts. Must not exceed the capability limit. - PollIntervalMs int64 `json:"pollIntervalMs"` + PollIntervalMs int32 `json:"pollIntervalMs"` } // Authoritative result after disconnect and MCP reconciliation. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java index 96660faeea..26eddc696a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsContinueConnectionParams.java @@ -32,7 +32,7 @@ public record SessionConnectorsContinueConnectionParams( @JsonProperty("maxAttempts") Long maxAttempts, /** Delay in milliseconds between attempts. Must not exceed the capability limit. */ @JsonProperty("pollIntervalMs") Long pollIntervalMs, - /** Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. */ + /** Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. */ @JsonProperty("deadlineMs") Long deadlineMs ) { } diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 47d841f16a..a7d0d2d328 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -24754,7 +24754,7 @@ export interface ConnectorContinueRequest { */ pollIntervalMs: number; /** - * Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. + * Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. */ deadlineMs: number; } diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 82fcf3cb54..e4893c205d 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -2096,8 +2096,8 @@ class ConnectorContinueRequest: """Opaque continuation ID returned by connect, reconnect, or an earlier continuation.""" deadline_ms: int - """Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability - limit. + """Maximum wall-clock duration in milliseconds for this call. Must be between one and the + capability limit. """ max_attempts: int """Maximum catalog requests made by this call. Must be between one and the capability limit.""" diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 734e60cb12..b81e2e23ef 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -23055,12 +23055,12 @@ pub struct ConnectorConnectResultPending { pub struct ConnectorContinueRequest { /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. pub continuation_id: String, - /// Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit. - pub deadline_ms: i64, + /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. + pub deadline_ms: i32, /// Maximum catalog requests made by this call. Must be between one and the capability limit. - pub max_attempts: i64, + pub max_attempts: i32, /// Delay in milliseconds between attempts. Must not exceed the capability limit. - pub poll_interval_ms: i64, + pub poll_interval_ms: i32, } /// Authoritative result after disconnect and MCP reconciliation. diff --git a/scripts/codegen/connector-session-api-overlay.json b/scripts/codegen/connector-session-api-overlay.json index eac817e63a..f2c4efc827 100644 --- a/scripts/codegen/connector-session-api-overlay.json +++ b/scripts/codegen/connector-session-api-overlay.json @@ -193,17 +193,20 @@ "maxAttempts": { "type": "integer", "minimum": 1, + "maximum": 60, "description": "Maximum catalog requests made by this call. Must be between one and the capability limit." }, "pollIntervalMs": { "type": "integer", "minimum": 0, + "maximum": 30000, "description": "Delay in milliseconds between attempts. Must not exceed the capability limit." }, "deadlineMs": { "type": "integer", - "minimum": 0, - "description": "Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit." + "minimum": 1, + "maximum": 300000, + "description": "Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit." } }, "required": [ @@ -573,17 +576,20 @@ "maxAttempts": { "type": "integer", "minimum": 1, + "maximum": 60, "description": "Maximum catalog requests made by this call. Must be between one and the capability limit." }, "pollIntervalMs": { "type": "integer", "minimum": 0, + "maximum": 30000, "description": "Delay in milliseconds between attempts. Must not exceed the capability limit." }, "deadlineMs": { "type": "integer", - "minimum": 0, - "description": "Maximum wall-clock duration in milliseconds for this call. Must not exceed the capability limit." + "minimum": 1, + "maximum": 300000, + "description": "Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit." } }, "required": [ From b324480eebcc39718383223390a2c1902efa162a Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Fri, 18 Sep 2026 19:37:21 +0100 Subject: [PATCH 16/16] Handle undefined schema values in overlay comparison Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/codegen/connectorSessionApiOverlay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/codegen/connectorSessionApiOverlay.ts b/scripts/codegen/connectorSessionApiOverlay.ts index f90334d392..879aba0a0b 100644 --- a/scripts/codegen/connectorSessionApiOverlay.ts +++ b/scripts/codegen/connectorSessionApiOverlay.ts @@ -28,7 +28,7 @@ function stableStringify(value: unknown): string { .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`) .join(",")}}`; } - return JSON.stringify(value); + return JSON.stringify(value) ?? "undefined"; } function clone(value: T): T {