Skip to content
Open
5 changes: 5 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Streaming is true ? true : null,
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.McpOAuthTokenStorage ?? McpOAuthTokenStorageMode.InMemory,
"direct",
config.CustomAgents,
config.DefaultAgent,
Expand Down Expand Up @@ -772,6 +773,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.Streaming is true ? true : null,
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.McpOAuthTokenStorage ?? McpOAuthTokenStorageMode.InMemory,
"direct",
config.CustomAgents,
config.DefaultAgent,
Expand Down Expand Up @@ -1865,6 +1867,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 @@ -1938,6 +1941,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 @@ -2030,6 +2034,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 @@ -1990,6 +1990,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 @@ -2273,6 +2288,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 @@ -2417,6 +2433,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
2 changes: 2 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,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 @@ -109,6 +110,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
10 changes: 10 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,11 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.ModelCapabilities = config.ModelCapabilities
req.WorkingDirectory = config.WorkingDirectory
req.MCPServers = config.MCPServers
if config.MCPOAuthTokenStorage != "" {
req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage
} else {
req.MCPOAuthTokenStorage = "in-memory"
}
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
req.DefaultAgent = config.DefaultAgent
Expand Down Expand Up @@ -839,6 +844,11 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.ContinuePendingWork = Bool(true)
}
req.MCPServers = config.MCPServers
if config.MCPOAuthTokenStorage != "" {
req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage
} else {
req.MCPOAuthTokenStorage = "in-memory"
}
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 @@ -515,6 +515,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
12 changes: 12 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,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 @@ -1189,6 +1194,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 @@ -1464,6 +1474,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 @@ -1526,6 +1537,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
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
request.setRequestPermission(true);
// Always send envValueMode=direct for MCP servers
request.setEnvValueMode("direct");
// Default to in-memory for safe multitenant behavior
request.setMcpOAuthTokenStorage("in-memory");
request.setSessionId(sessionId);
if (config == null) {
return request;
Expand All @@ -124,6 +126,8 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
}
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
request.setMcpOAuthTokenStorage(
config.getMcpOAuthTokenStorage() != null ? config.getMcpOAuthTokenStorage() : "in-memory");
request.setCustomAgents(config.getCustomAgents());
request.setDefaultAgent(config.getDefaultAgent());
request.setAgent(config.getAgent());
Expand Down Expand Up @@ -189,6 +193,8 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setRequestPermission(true);
// Always send envValueMode=direct for MCP servers
request.setEnvValueMode("direct");
// Default to in-memory for safe multitenant behavior
request.setMcpOAuthTokenStorage("in-memory");

if (config == null) {
return request;
Expand Down Expand Up @@ -220,6 +226,8 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
}
config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents);
request.setMcpServers(config.getMcpServers());
request.setMcpOAuthTokenStorage(
config.getMcpOAuthTokenStorage() != null ? config.getMcpOAuthTokenStorage() : "in-memory");
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 @@ -76,6 +76,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 @@ -329,6 +332,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
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class ResumeSessionConfig {
private boolean streaming;
private Boolean includeSubAgentStreamingEvents;
private Map<String, McpServerConfig> mcpServers;
private String mcpOAuthTokenStorage;
private List<CustomAgentConfig> customAgents;
private DefaultAgentConfig defaultAgent;
private String agent;
Expand Down Expand Up @@ -574,6 +575,37 @@ public ResumeSessionConfig setMcpServers(Map<String, McpServerConfig> mcpServers
return this;
}

/**
* Gets the MCP OAuth token storage mode.
*
* @return the storage mode, or {@code null} if not set
*/
public String getMcpOAuthTokenStorage() {
return mcpOAuthTokenStorage;
}

/**
* Sets the MCP OAuth token storage mode.
* <p>
* Controls how MCP OAuth tokens are stored for this session:
* <ul>
* <li>{@code "persistent"} — tokens are stored in the OS keychain (shared
* across sessions)</li>
* <li>{@code "in-memory"} — tokens are stored in memory and discarded when the
* session ends</li>
* </ul>
* If not set, the SDK defaults to {@code "in-memory"} for safe multitenant
* behavior.
*
* @param mcpOAuthTokenStorage
* the storage mode
* @return this config for method chaining
*/
public ResumeSessionConfig setMcpOAuthTokenStorage(String mcpOAuthTokenStorage) {
this.mcpOAuthTokenStorage = mcpOAuthTokenStorage;
return this;
}

/**
* Gets the custom agent configurations.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ public final class ResumeSessionRequest {
@JsonProperty("mcpServers")
private Map<String, McpServerConfig> mcpServers;

@JsonProperty("mcpOAuthTokenStorage")
private String mcpOAuthTokenStorage;

@JsonProperty("envValueMode")
private String envValueMode;

Expand Down Expand Up @@ -398,6 +401,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