Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1b027da
Forward OAuth client metadata URL
SamMorrowDrums Aug 4, 2026
2003b23
chore: merge SDK main into CIMD forwarding
SamMorrowDrums Aug 13, 2026
ee12bad
Merge origin/main into sammorrowdrums-auth-client-metadata-url
SamMorrowDrums Aug 13, 2026
6450d14
Merge concurrent PR branch update
SamMorrowDrums Aug 13, 2026
a9162f9
Merge origin/main into sammorrowdrums-auth-client-metadata-url
SamMorrowDrums Aug 13, 2026
760304b
test(rust): cover omitted OAuth metadata URL
SamMorrowDrums Aug 13, 2026
90e39d3
Merge remote-tracking branch 'origin/main' into sammorrowdrums-auth-c…
SamMorrowDrums Aug 17, 2026
dcb0d17
Merge remote-tracking branch 'origin/main' into sammorrowdrums-auth-c…
SamMorrowDrums Aug 19, 2026
38ab2b5
docs(rust): clarify OAuth client identity defaults
SamMorrowDrums Aug 19, 2026
cc1dd27
Merge remote-tracking branch 'origin/main' into sammorrowdrums-auth-c…
SamMorrowDrums Aug 27, 2026
fbb7821
feat: forward OAuth client metadata URL in all SDKs
SamMorrowDrums Aug 27, 2026
0998f3e
test(java): use supported session request builders
SamMorrowDrums Aug 27, 2026
143f910
Merge origin/main into sammorrowdrums-auth-client-metadata-url
SamMorrowDrums Sep 3, 2026
cf3bbe0
ci: upgrade consistency workflow compiler
SamMorrowDrums Sep 3, 2026
7e60244
ci: upgrade agentic workflows to v0.85.4
SamMorrowDrums Sep 3, 2026
5de023d
revert: remove unrelated workflow upgrades
SamMorrowDrums Sep 3, 2026
ca2fdeb
test: cover MCP CIMD OAuth behavior
SteveSandersonMS Sep 3, 2026
c082ddb
Merge origin/main into CIMD session forwarding
SamMorrowDrums Sep 10, 2026
5ab2eb6
test: fix CIMD coverage after main merge
SamMorrowDrums Sep 10, 2026
d5d0d2e
test(dotnet): support CIMD E2E on net472
SamMorrowDrums Sep 10, 2026
4bfe242
test(rust): document loopback OAuth transport
SamMorrowDrums Sep 10, 2026
db96886
test(rust): attach loopback suppression to sink
SamMorrowDrums Sep 10, 2026
24efca7
Merge remote-tracking branch 'origin/main' into sammorrowdrums-auth-c…
SamMorrowDrums Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.McpOAuthTokenStorage,
config.AuthClientIdMetadataUrl,
"direct",
config.CustomAgents,
config.DefaultAgent,
Expand Down Expand Up @@ -1503,6 +1504,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.McpOAuthTokenStorage,
config.AuthClientIdMetadataUrl,
"direct",
config.CustomAgents,
config.DefaultAgent,
Expand Down Expand Up @@ -3033,6 +3035,7 @@ internal record CreateSessionRequest(
bool? IncludeSubAgentStreamingEvents,
IDictionary<string, McpServerConfig>? McpServers,
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
string? AuthClientIdMetadataUrl,
string? EnvValueMode,
IList<CustomAgentConfig>? CustomAgents,
DefaultAgentConfig? DefaultAgent,
Expand Down Expand Up @@ -3162,6 +3165,7 @@ internal record ResumeSessionRequest(
bool? IncludeSubAgentStreamingEvents,
IDictionary<string, McpServerConfig>? McpServers,
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
string? AuthClientIdMetadataUrl,
string? EnvValueMode,
IList<CustomAgentConfig>? CustomAgents,
DefaultAgentConfig? DefaultAgent,
Expand Down
7 changes: 7 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3327,6 +3327,7 @@ protected SessionConfigBase(SessionConfigBase? other)
: new Dictionary<string, McpServerConfig>(other.McpServers))
: null;
McpOAuthTokenStorage = other.McpOAuthTokenStorage;
AuthClientIdMetadataUrl = other.AuthClientIdMetadataUrl;
Model = other.Model;
ModelCapabilities = other.ModelCapabilities;
OnAutoModeSwitchRequest = other.OnAutoModeSwitchRequest;
Expand Down Expand Up @@ -3721,6 +3722,12 @@ protected SessionConfigBase(SessionConfigBase? other)
/// </summary>
public McpOAuthTokenStorageMode? McpOAuthTokenStorage { get; set; }

/// <summary>
/// OAuth Client ID Metadata Document URL identifying the host for MCP authorization.
/// When unset, no host identity is supplied.
/// </summary>
public string? AuthClientIdMetadataUrl { get; set; }

/// <summary>Custom agent configurations for the session.</summary>
public IList<CustomAgentConfig>? CustomAgents { get; set; }

Expand Down
33 changes: 30 additions & 3 deletions dotnet/test/E2E/McpOAuthE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using GitHub.Copilot.Test.Harness;
using System.Diagnostics;
using System.Net.Http;
using System.Net;
using System.Text.Json;
using System.Threading.Channels;
using Xunit;
Expand All @@ -19,6 +20,30 @@ public class McpOAuthE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
private const string RefreshToken = ExpectedToken + "-refresh";
private const string UpscopeToken = ExpectedToken + "-upscope";
private const string ReauthToken = ExpectedToken + "-reauth";
private const string CimdUrl = "https://github.com/copilot/cli/client-metadata.json";

[Fact]
public async Task Should_Use_Cimd_Url_Instead_Of_Dynamic_Registration()
{
await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken, cimdSupported: true);
const string serverName = "oauth-cimd-mcp";
await using var session = await CreateSessionAsync(new SessionConfig
{
AuthClientIdMetadataUrl = CimdUrl,
McpServers = new Dictionary<string, McpServerConfig>
{
[serverName] = new McpHttpServerConfig { Url = $"{oauthServer.Url}/mcp", Tools = ["*"] }
}
});
await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.NeedsAuth);
var result = await session.Rpc.Mcp.Oauth.LoginAsync(serverName);
Assert.NotNull(result.AuthorizationUrl);
var clientIdParameter = new Uri(result.AuthorizationUrl!).Query.TrimStart('?')
.Split('&').Single(part => part.StartsWith("client_id=", StringComparison.Ordinal));
var clientId = clientIdParameter.Substring("client_id=".Length);
Assert.Equal(CimdUrl, WebUtility.UrlDecode(clientId));
Assert.DoesNotContain(await oauthServer.GetRequestsAsync(), request => request.Path == "/register");
}

[Fact]
public async Task Should_Satisfy_MCP_OAuth_Using_Host_Provided_Token()
Expand Down Expand Up @@ -282,7 +307,7 @@ private OAuthMcpServer(Process process, string url)

public string Url { get; }

public static async Task<OAuthMcpServer> StartAsync(string expectedToken)
public static async Task<OAuthMcpServer> StartAsync(string expectedToken, bool cimdSupported = false)
{
var repoRoot = FindRepoRoot();
var script = GetRepoRelativePath(repoRoot, "test", "harness", "test-mcp-oauth-server.mjs");
Expand All @@ -295,6 +320,7 @@ public static async Task<OAuthMcpServer> StartAsync(string expectedToken)
UseShellExecute = false
};
startInfo.Environment["EXPECTED_TOKEN"] = expectedToken;
startInfo.Environment["CIMD_SUPPORTED"] = cimdSupported ? "true" : "false";

var process = Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start OAuth MCP server.");
Expand Down Expand Up @@ -326,7 +352,8 @@ public async Task<List<OAuthMcpRequest>> GetRequestsAsync()
element.TryGetProperty("authorization", out var authorization)
&& authorization.ValueKind is JsonValueKind.String
? authorization.GetString()
: null))
: null,
element.GetProperty("path").GetString()!))
.ToList();
}

Expand Down Expand Up @@ -370,5 +397,5 @@ private static string QuoteProcessArgument(string argument)
=> "\"" + argument.Replace("\"", "\\\"") + "\"";
}

private sealed record OAuthMcpRequest(string? Authorization);
private sealed record OAuthMcpRequest(string? Authorization, string Path);
}
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.AdditionalDirectories = config.AdditionalDirectories
req.MCPServers = config.MCPServers
req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage
req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
req.DefaultAgent = config.DefaultAgent
Expand Down Expand Up @@ -1322,6 +1323,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.ContinuePendingWork = config.ContinuePendingWork
req.MCPServers = config.MCPServers
req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage
req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
req.DefaultAgent = config.DefaultAgent
Expand Down
43 changes: 42 additions & 1 deletion go/internal/e2e/mcp_oauth_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"encoding/json"
"net/http"
"net/url"
"os"
"os/exec"
"slices"
Expand All @@ -27,6 +28,41 @@ func TestMCPOAuthE2E(t *testing.T) {
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })

t.Run("uses CIMD URL instead of dynamic registration", func(t *testing.T) {
baseURL := startOAuthMCPServer(t, true)
serverName := "oauth-cimd-mcp"
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
AuthClientIDMetadataURL: "https://github.com/copilot/cli/client-metadata.json",
MCPServers: map[string]copilot.MCPServerConfig{
serverName: copilot.MCPHTTPServerConfig{URL: baseURL + "/mcp", Tools: []string{"*"}},
},
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
t.Cleanup(func() { session.Disconnect() })
waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusNeedsAuth)
result, err := session.RPC.MCP.Oauth().Login(t.Context(), &rpc.MCPOauthLoginRequest{ServerName: serverName})
if err != nil {
t.Fatalf("MCP OAuth login failed: %v", err)
}
if result.AuthorizationURL == nil {
t.Fatal("Expected authorization URL")
}
parsed, err := url.Parse(*result.AuthorizationURL)
if err != nil {
t.Fatalf("Invalid authorization URL: %v", err)
}
if parsed.Query().Get("client_id") != "https://github.com/copilot/cli/client-metadata.json" {
t.Fatalf("Expected CIMD client_id, got %q", parsed.Query().Get("client_id"))
}
for _, request := range fetchOAuthMCPRequests(t, baseURL) {
if request.Path == "/register" {
t.Fatal("Runtime should not dynamically register when CIMD is supported")
}
}
})

t.Run("satisfy MCP OAuth using host-provided token", func(t *testing.T) {
baseURL := startOAuthMCPServer(t)
serverName := "oauth-protected-mcp"
Expand Down Expand Up @@ -335,18 +371,23 @@ func TestMCPOAuthE2E(t *testing.T) {

type oauthMCPRequest struct {
Authorization *string `json:"authorization"`
Path string `json:"path"`
}

func startOAuthMCPServer(t *testing.T) string {
func startOAuthMCPServer(t *testing.T, cimdSupported ...bool) string {
t.Helper()

serverPath := testharness.RepoPath("test", "harness", "test-mcp-oauth-server.mjs")
cmd := exec.Command("node", serverPath)
cmd.Env = append(os.Environ(), "EXPECTED_TOKEN="+expectedMCPOAuthToken)
if len(cimdSupported) > 0 && cimdSupported[0] {
cmd.Env = append(cmd.Env, "CIMD_SUPPORTED=true")
}
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatalf("Failed to pipe OAuth MCP server stdout: %v", err)
}

var stderr syncBuffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
Expand Down
8 changes: 8 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,9 @@ type SessionConfig struct {
// MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session.
// When empty, the runtime default ("in-memory") is used.
MCPOAuthTokenStorage string
// AuthClientIDMetadataURL identifies the host for MCP OAuth authorization.
// When empty, no host identity is supplied.
AuthClientIDMetadataURL string
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
// DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected).
Expand Down Expand Up @@ -2053,6 +2056,9 @@ type ResumeSessionConfig struct {
// MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session.
// When empty, the runtime default ("in-memory") is used.
MCPOAuthTokenStorage string
// AuthClientIDMetadataURL identifies the host for MCP OAuth authorization.
// When empty, no host identity is supplied.
AuthClientIDMetadataURL string
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
// DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected).
Expand Down Expand Up @@ -2645,6 +2651,7 @@ type createSessionRequest struct {
EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"`
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"`
AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`
Expand Down Expand Up @@ -2756,6 +2763,7 @@ type resumeSessionRequest struct {
EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"`
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"`
AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl());
request.setCustomAgents(config.getCustomAgents());
request.setCustomAgentsLocalOnly(
resolveCustomAgentsLocalOnly(config.getCustomAgentsLocalOnly().orElse(null), mode));
Expand Down Expand Up @@ -304,6 +305,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl());
request.setCustomAgents(config.getCustomAgents());
request.setCustomAgentsLocalOnly(
resolveCustomAgentsLocalOnly(config.getCustomAgentsLocalOnly().orElse(null), mode));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ public final class CreateSessionRequest {
@JsonProperty("mcpOAuthTokenStorage")
private String mcpOAuthTokenStorage;

@JsonProperty("authClientIdMetadataUrl")
private String authClientIdMetadataUrl;

@JsonProperty("envValueMode")
private String envValueMode;

Expand Down Expand Up @@ -604,6 +607,16 @@ public void setMcpOAuthTokenStorage(String mcpOAuthTokenStorage) {
this.mcpOAuthTokenStorage = mcpOAuthTokenStorage;
}

/** Gets the host OAuth client metadata URL. @return the metadata URL */
public String getAuthClientIdMetadataUrl() {
return authClientIdMetadataUrl;
}

/** Sets the host OAuth client metadata URL. @param url the metadata URL */
public void setAuthClientIdMetadataUrl(String url) {
this.authClientIdMetadataUrl = url;
}

/** Gets MCP environment variable value mode. @return the mode */
public String getEnvValueMode() {
return envValueMode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public class ResumeSessionConfig {
private Boolean includeSubAgentStreamingEvents;
private Map<String, McpServerConfig> mcpServers;
private String mcpOAuthTokenStorage;
private String authClientIdMetadataUrl;
private List<CustomAgentConfig> customAgents;
private DefaultAgentConfig defaultAgent;
private String agent;
Expand Down Expand Up @@ -1428,6 +1429,29 @@ public ResumeSessionConfig setMcpOAuthTokenStorage(String mcpOAuthTokenStorage)
return this;
}

/**
* Gets the OAuth Client ID Metadata Document URL identifying the host.
*
* @return the metadata URL, or {@code null} if not set
*/
public String getAuthClientIdMetadataUrl() {
return authClientIdMetadataUrl;
}

/**
* Sets the OAuth Client ID Metadata Document URL identifying the host for MCP
* authorization. Re-supply the same host identity used when the session was
* created.
*
* @param authClientIdMetadataUrl
* the metadata URL
* @return this config for method chaining
*/
public ResumeSessionConfig setAuthClientIdMetadataUrl(String authClientIdMetadataUrl) {
this.authClientIdMetadataUrl = authClientIdMetadataUrl;
return this;
}

/**
* Gets the custom agent configurations.
*
Expand Down Expand Up @@ -2119,6 +2143,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.mcpOAuthTokenStorage = this.mcpOAuthTokenStorage;
copy.authClientIdMetadataUrl = this.authClientIdMetadataUrl;
copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null;
copy.defaultAgent = this.defaultAgent;
copy.agent = this.agent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ public final class ResumeSessionRequest {
@JsonProperty("mcpOAuthTokenStorage")
private String mcpOAuthTokenStorage;

@JsonProperty("authClientIdMetadataUrl")
private String authClientIdMetadataUrl;

@JsonProperty("envValueMode")
private String envValueMode;

Expand Down Expand Up @@ -830,6 +833,16 @@ public void setMcpOAuthTokenStorage(String mcpOAuthTokenStorage) {
this.mcpOAuthTokenStorage = mcpOAuthTokenStorage;
}

/** Gets the host OAuth client metadata URL. @return the metadata URL */
public String getAuthClientIdMetadataUrl() {
return authClientIdMetadataUrl;
}

/** Sets the host OAuth client metadata URL. @param url the metadata URL */
public void setAuthClientIdMetadataUrl(String url) {
this.authClientIdMetadataUrl = url;
}

/** Gets MCP environment variable value mode. @return the mode */
public String getEnvValueMode() {
return envValueMode;
Expand Down
Loading
Loading