Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e5aa2f5
feat: add mcpOAuthTokenStorage support across all SDKs
MackinnonBuck May 18, 2026
c8c0cd6
fix(go): rename McpOAuthTokenStorage to MCPOAuthTokenStorage
MackinnonBuck May 18, 2026
6b6583c
fix(go): add error handling in MCPOAuthTokenStorage omit subtests
MackinnonBuck May 18, 2026
06c091f
test(nodejs): add mcpOAuthTokenStorage default and forwarding tests
MackinnonBuck May 18, 2026
d277fa8
test(python): add mcp_oauth_token_storage default and forwarding tests
MackinnonBuck May 18, 2026
edf4301
Merge remote-tracking branch 'origin/main' into mackinnonbuck/per-ses…
MackinnonBuck May 26, 2026
799d03e
fix(python): use current CopilotClient constructor in mcpOAuthTokenSt…
MackinnonBuck May 26, 2026
2ab266b
feat(java): add mcpOAuthTokenStorage support
MackinnonBuck May 26, 2026
b286323
Merge remote-tracking branch 'origin/main' into mackinnonbuck/per-ses…
MackinnonBuck May 28, 2026
80f6510
Fix MCP OAuth test regressions
MackinnonBuck May 28, 2026
87eff4c
Move mcpOAuthTokenStorage default to empty (multitenant) mode only
MackinnonBuck May 28, 2026
a2775b1
Merge remote-tracking branch 'origin/main' into mackinnonbuck/per-ses…
MackinnonBuck May 28, 2026
1f0df3e
Fix stale doc comments for mcp_oauth_token_storage in Rust
MackinnonBuck May 28, 2026
2bae386
Fix formatting in Node.js and Python test files
MackinnonBuck May 28, 2026
cf3fb91
Fix Python type annotation for _mcp_oauth_token_storage_default
MackinnonBuck May 28, 2026
6794f58
Add base_directory to Python empty mode tests
MackinnonBuck May 28, 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
6 changes: 6 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
if (_options.Mode == CopilotClientMode.Empty)
{
config.EnableSessionTelemetry ??= false;
config.McpOAuthTokenStorage ??= McpOAuthTokenStorageMode.InMemory;
}
}

Expand Down Expand Up @@ -868,6 +869,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Streaming is true ? true : null,
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.McpOAuthTokenStorage,
"direct",
config.CustomAgents,
config.DefaultAgent,
Expand Down Expand Up @@ -1055,6 +1057,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.Streaming is true ? true : null,
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.McpOAuthTokenStorage,
"direct",
config.CustomAgents,
config.DefaultAgent,
Expand Down Expand Up @@ -2169,6 +2172,7 @@ internal record CreateSessionRequest(
bool? Streaming,
bool? IncludeSubAgentStreamingEvents,
IDictionary<string, McpServerConfig>? McpServers,
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
string? EnvValueMode,
IList<CustomAgentConfig>? CustomAgents,
DefaultAgentConfig? DefaultAgent,
Expand Down Expand Up @@ -2247,6 +2251,7 @@ internal record ResumeSessionRequest(
bool? Streaming,
bool? IncludeSubAgentStreamingEvents,
IDictionary<string, McpServerConfig>? McpServers,
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
string? EnvValueMode,
IList<CustomAgentConfig>? CustomAgents,
DefaultAgentConfig? DefaultAgent,
Expand Down Expand Up @@ -2343,6 +2348,7 @@ internal record HooksInvokeResponse(
[JsonSerializable(typeof(ListSessionsResponse))]
[JsonSerializable(typeof(GetSessionMetadataRequest))]
[JsonSerializable(typeof(GetSessionMetadataResponse))]
[JsonSerializable(typeof(McpOAuthTokenStorageMode))]
[JsonSerializable(typeof(ModelCapabilitiesOverride))]
[JsonSerializable(typeof(ProviderConfig))]
[JsonSerializable(typeof(ResumeSessionRequest))]
Expand Down
22 changes: 22 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,21 @@ public enum McpHttpServerConfigOauthGrantType
ClientCredentials
}

/// <summary>
/// Controls how MCP OAuth tokens are stored for a session.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<McpOAuthTokenStorageMode>))]
public enum McpOAuthTokenStorageMode
{
/// <summary>Tokens are stored in the OS keychain, shared across sessions.</summary>
[JsonStringEnumMemberName("persistent")]
Persistent,

/// <summary>Tokens are stored in memory and discarded when the session ends.</summary>
[JsonStringEnumMemberName("in-memory")]
InMemory
}

/// <summary>
/// Abstract base class for MCP server configurations.
/// </summary>
Expand Down Expand Up @@ -2397,6 +2412,7 @@ protected SessionConfigBase(SessionConfigBase? other)
? new Dictionary<string, McpServerConfig>(dict, dict.Comparer)
: new Dictionary<string, McpServerConfig>(other.McpServers))
: null;
McpOAuthTokenStorage = other.McpOAuthTokenStorage;
Model = other.Model;
ModelCapabilities = other.ModelCapabilities;
OnAutoModeSwitchRequest = other.OnAutoModeSwitchRequest;
Expand Down Expand Up @@ -2616,6 +2632,12 @@ protected SessionConfigBase(SessionConfigBase? other)
/// </summary>
public IDictionary<string, McpServerConfig>? McpServers { get; set; }

/// <summary>
/// Controls how MCP OAuth tokens are stored for this session.
/// Default: <see cref="McpOAuthTokenStorageMode.InMemory"/> for safe multitenant behavior.
/// </summary>
public McpOAuthTokenStorageMode? McpOAuthTokenStorage { get; set; }

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

Expand Down
28 changes: 28 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
EnableSessionTelemetry = false,
IncludeSubAgentStreamingEvents = false,
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }],
Agent = "agent1",
Cloud = new CloudSessionOptions
Expand Down Expand Up @@ -113,6 +114,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry);
Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents);
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
Assert.Equal(original.McpOAuthTokenStorage, clone.McpOAuthTokenStorage);
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
Assert.Equal(original.CustomAgents[0].Model, clone.CustomAgents[0].Model);
Assert.Equal(original.Agent, clone.Agent);
Expand Down Expand Up @@ -420,4 +422,30 @@ public void ResumeSessionConfig_Clone_PreservesEnableSessionTelemetryDefault()

Assert.Null(clone.EnableSessionTelemetry);
}

[Fact]
public void SessionConfig_Clone_CopiesMcpOAuthTokenStorage()
{
var original = new SessionConfig
{
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
};

var clone = original.Clone();

Assert.Equal(McpOAuthTokenStorageMode.Persistent, clone.McpOAuthTokenStorage);
}

[Fact]
public void ResumeSessionConfig_Clone_CopiesMcpOAuthTokenStorage()
{
var original = new ResumeSessionConfig
{
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
};

var clone = original.Clone();

Assert.Equal(McpOAuthTokenStorageMode.Persistent, clone.McpOAuthTokenStorage);
}
}
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.ModelCapabilities = config.ModelCapabilities
req.WorkingDirectory = config.WorkingDirectory
req.MCPServers = config.MCPServers
req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
req.DefaultAgent = config.DefaultAgent
Expand Down Expand Up @@ -958,6 +959,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.ContinuePendingWork = Bool(true)
}
req.MCPServers = config.MCPServers
req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
req.DefaultAgent = config.DefaultAgent
Expand Down
64 changes: 64 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,70 @@ func TestResumeSessionRequest_InstructionDirectories(t *testing.T) {
})
}

func TestCreateSessionRequest_MCPOAuthTokenStorage(t *testing.T) {
t.Run("includes mcpOAuthTokenStorage in JSON when set", func(t *testing.T) {
req := createSessionRequest{MCPOAuthTokenStorage: "in-memory"}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if m["mcpOAuthTokenStorage"] != "in-memory" {
t.Errorf("Expected mcpOAuthTokenStorage to be 'in-memory', got %v", m["mcpOAuthTokenStorage"])
}
})

t.Run("omits mcpOAuthTokenStorage from JSON when empty", func(t *testing.T) {
req := createSessionRequest{}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if _, ok := m["mcpOAuthTokenStorage"]; ok {
t.Error("Expected mcpOAuthTokenStorage to be omitted when empty")
}
Comment thread
MackinnonBuck marked this conversation as resolved.
})
}

func TestResumeSessionRequest_MCPOAuthTokenStorage(t *testing.T) {
t.Run("includes mcpOAuthTokenStorage in JSON when set", func(t *testing.T) {
req := resumeSessionRequest{SessionID: "s1", MCPOAuthTokenStorage: "persistent"}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if m["mcpOAuthTokenStorage"] != "persistent" {
t.Errorf("Expected mcpOAuthTokenStorage to be 'persistent', got %v", m["mcpOAuthTokenStorage"])
}
})

t.Run("omits mcpOAuthTokenStorage from JSON when empty", func(t *testing.T) {
req := resumeSessionRequest{SessionID: "s1"}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if _, ok := m["mcpOAuthTokenStorage"]; ok {
t.Error("Expected mcpOAuthTokenStorage to be omitted when empty")
}
})
}

func TestOverridesBuiltInTool(t *testing.T) {
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
tool := Tool{
Expand Down
6 changes: 6 additions & 0 deletions go/mode_empty.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ func (c *Client) applyConfigDefaultsForMode(config *SessionConfig) {
f := false
config.EnableSessionTelemetry = &f
}
if config.MCPOAuthTokenStorage == "" {
config.MCPOAuthTokenStorage = "in-memory"
}
}

func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
Expand All @@ -136,6 +139,9 @@ func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
f := false
config.EnableSessionTelemetry = &f
}
if config.MCPOAuthTokenStorage == "" {
config.MCPOAuthTokenStorage = "in-memory"
}
}

// updateSessionOptionsForMode applies the per-mode safe-defaults patch via
Expand Down
27 changes: 27 additions & 0 deletions go/toolset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,30 @@ func TestApplyConfigDefaultsForMode_copilotCliLeavesNil(t *testing.T) {
t.Errorf("non-empty mode must not default telemetry")
}
}

func TestApplyConfigDefaultsForMode_emptyDefaultsMCPOAuthTokenStorage(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeEmpty, BaseDirectory: t.TempDir()})
cfg := &SessionConfig{}
c.applyConfigDefaultsForMode(cfg)
if cfg.MCPOAuthTokenStorage != "in-memory" {
t.Errorf("expected MCPOAuthTokenStorage 'in-memory' in empty mode, got %q", cfg.MCPOAuthTokenStorage)
}
}

func TestApplyConfigDefaultsForMode_emptyHonorsCallerMCPOAuthTokenStorage(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeEmpty, BaseDirectory: t.TempDir()})
cfg := &SessionConfig{MCPOAuthTokenStorage: "persistent"}
c.applyConfigDefaultsForMode(cfg)
if cfg.MCPOAuthTokenStorage != "persistent" {
t.Errorf("caller-supplied MCPOAuthTokenStorage must win, got %q", cfg.MCPOAuthTokenStorage)
}
}

func TestApplyConfigDefaultsForMode_copilotCliLeavesMCPOAuthTokenStorageEmpty(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeCopilotCli})
cfg := &SessionConfig{}
c.applyConfigDefaultsForMode(cfg)
if cfg.MCPOAuthTokenStorage != "" {
t.Errorf("non-empty mode must not default MCPOAuthTokenStorage, got %q", cfg.MCPOAuthTokenStorage)
}
}
12 changes: 12 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,11 @@ type SessionConfig struct {
ModelCapabilities *rpc.ModelCapabilitiesOverride
// MCPServers configures MCP servers for the session
MCPServers map[string]MCPServerConfig
// MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session.
// "persistent" stores tokens in the OS keychain (shared across sessions).
// "in-memory" stores tokens in memory and discards them when the session ends.
// Defaults to "in-memory" for safe multitenant behavior.
MCPOAuthTokenStorage 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 @@ -1287,6 +1292,11 @@ type ResumeSessionConfig struct {
IncludeSubAgentStreamingEvents *bool
// MCPServers configures MCP servers for the session
MCPServers map[string]MCPServerConfig
// MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session.
// "persistent" stores tokens in the OS keychain (shared across sessions).
// "in-memory" stores tokens in memory and discards them when the session ends.
// Defaults to "in-memory" for safe multitenant behavior.
MCPOAuthTokenStorage 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 @@ -1602,6 +1612,7 @@ type createSessionRequest struct {
Streaming *bool `json:"streaming,omitempty"`
IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"`
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`
Expand Down Expand Up @@ -1673,6 +1684,7 @@ type resumeSessionRequest struct {
Streaming *bool `json:"streaming,omitempty"`
IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"`
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`
Expand Down
6 changes: 6 additions & 0 deletions java/src/main/java/com/github/copilot/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,9 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
+ "the tools it wants — e.g. setAvailableTools(new ToolSet().addBuiltIn(BuiltInTools.ISOLATED)).");
}
request.setToolFilterPrecedence("excluded");
if (request.getMcpOAuthTokenStorage() == null) {
request.setMcpOAuthTokenStorage("in-memory");
}
}

long rpcNanos = System.nanoTime();
Expand Down Expand Up @@ -626,6 +629,9 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
+ "the tools it wants — e.g. setAvailableTools(new ToolSet().addBuiltIn(BuiltInTools.ISOLATED)).");
}
request.setToolFilterPrecedence("excluded");
if (request.getMcpOAuthTokenStorage() == null) {
request.setMcpOAuthTokenStorage("in-memory");
}
}

long rpcNanos = System.nanoTime();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
}
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
request.setCustomAgents(config.getCustomAgents());
request.setDefaultAgent(config.getDefaultAgent());
request.setAgent(config.getAgent());
Expand Down Expand Up @@ -227,6 +228,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
}
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage());
request.setCustomAgents(config.getCustomAgents());
request.setDefaultAgent(config.getDefaultAgent());
request.setAgent(config.getAgent());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public final class CreateSessionRequest {
@JsonProperty("mcpServers")
private Map<String, McpServerConfig> mcpServers;

@JsonProperty("mcpOAuthTokenStorage")
private String mcpOAuthTokenStorage;

@JsonProperty("envValueMode")
private String envValueMode;

Expand Down Expand Up @@ -369,6 +372,19 @@ public void setMcpServers(Map<String, McpServerConfig> mcpServers) {
this.mcpServers = mcpServers;
}

/** Gets MCP OAuth token storage mode. @return the storage mode */
public String getMcpOAuthTokenStorage() {
return mcpOAuthTokenStorage;
}

/**
* Sets MCP OAuth token storage mode. @param mcpOAuthTokenStorage the storage
* mode
*/
public void setMcpOAuthTokenStorage(String mcpOAuthTokenStorage) {
this.mcpOAuthTokenStorage = mcpOAuthTokenStorage;
}

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