Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
5 changes: 5 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,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 @@ -780,6 +781,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 @@ -1986,6 +1988,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 @@ -2050,6 +2053,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 @@ -2139,6 +2143,7 @@ internal record PermissionRequestResponseV2(
[JsonSerializable(typeof(ListSessionsResponse))]
[JsonSerializable(typeof(GetSessionMetadataRequest))]
[JsonSerializable(typeof(GetSessionMetadataResponse))]
[JsonSerializable(typeof(McpOAuthTokenStorageMode))]
[JsonSerializable(typeof(ModelCapabilitiesOverride))]
[JsonSerializable(typeof(PermissionRequestResult))]
[JsonSerializable(typeof(PermissionRequestResultKind))]
Expand Down
29 changes: 29 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,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 @@ -2085,6 +2100,7 @@ protected SessionConfig(SessionConfig? other)
? new Dictionary<string, McpServerConfig>(dict, dict.Comparer)
: new Dictionary<string, McpServerConfig>(other.McpServers))
: null;
McpOAuthTokenStorage = other.McpOAuthTokenStorage;
Model = other.Model;
ModelCapabilities = other.ModelCapabilities;
OnAutoModeSwitch = other.OnAutoModeSwitch;
Expand Down Expand Up @@ -2261,6 +2277,12 @@ protected SessionConfig(SessionConfig? 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>
Expand Down Expand Up @@ -2394,6 +2416,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
? new Dictionary<string, McpServerConfig>(dict, dict.Comparer)
: new Dictionary<string, McpServerConfig>(other.McpServers))
: null;
McpOAuthTokenStorage = other.McpOAuthTokenStorage;
Model = other.Model;
ModelCapabilities = other.ModelCapabilities;
OnAutoModeSwitch = other.OnAutoModeSwitch;
Expand Down Expand Up @@ -2587,6 +2610,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? 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>
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 @@ -94,6 +94,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 @@ -127,6 +128,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 @@ -636,6 +636,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 @@ -841,6 +846,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
54 changes: 54 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,60 @@ 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, _ := json.Marshal(req)
var m map[string]any
json.Unmarshal(data, &m)
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, _ := json.Marshal(req)
var m map[string]any
json.Unmarshal(data, &m)
Comment thread
MackinnonBuck marked this conversation as resolved.
Outdated
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 @@ -658,6 +658,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 @@ -902,6 +907,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 @@ -1162,6 +1172,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 @@ -1220,6 +1231,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
2 changes: 2 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,7 @@ export class CopilotClient {
streaming: config.streaming,
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
mcpServers: config.mcpServers,
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage ?? "in-memory",
Comment thread
MackinnonBuck marked this conversation as resolved.
envValueMode: "direct",
customAgents: config.customAgents,
defaultAgent: config.defaultAgent,
Expand Down Expand Up @@ -981,6 +982,7 @@ export class CopilotClient {
streaming: config.streaming,
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
mcpServers: config.mcpServers,
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage ?? "in-memory",
envValueMode: "direct",
customAgents: config.customAgents,
defaultAgent: config.defaultAgent,
Expand Down
10 changes: 10 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,15 @@ export interface SessionConfig {
*/
includeSubAgentStreamingEvents?: boolean;

/**
* Controls how MCP OAuth tokens are stored for this session.
* - `"persistent"` — tokens are stored in the OS keychain (shared across sessions)
* - `"in-memory"` — tokens are stored in memory and discarded when the session ends
*
* @default "in-memory"
*/
mcpOAuthTokenStorage?: "persistent" | "in-memory";

/**
* MCP server configurations for the session.
* Keys are server names, values are server configurations.
Expand Down Expand Up @@ -1567,6 +1576,7 @@ export type ResumeSessionConfig = Pick<
| "customAgents"
| "defaultAgent"
| "agent"
| "mcpOAuthTokenStorage"
| "skillDirectories"
| "instructionDirectories"
| "disabledSkills"
Expand Down
14 changes: 14 additions & 0 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,7 @@ async def create_session(
streaming: bool | None = None,
include_sub_agent_streaming_events: bool | None = None,
mcp_servers: dict[str, MCPServerConfig] | None = None,
mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None,
custom_agents: list[CustomAgentConfig] | None = None,
default_agent: DefaultAgentConfig | dict[str, Any] | None = None,
agent: str | None = None,
Expand Down Expand Up @@ -1402,6 +1403,10 @@ 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.
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).
Defaults to ``"in-memory"`` for safe multitenant behavior.
custom_agents: Custom agent configurations.
default_agent: Configuration for the default agent,
including tool visibility controls.
Expand Down Expand Up @@ -1551,6 +1556,8 @@ async def create_session(
# Add MCP servers configuration if provided
if mcp_servers:
payload["mcpServers"] = mcp_servers
# Default MCP OAuth token storage to in-memory for safe multitenant behavior
payload["mcpOAuthTokenStorage"] = mcp_oauth_token_storage or "in-memory"
Comment thread
MackinnonBuck marked this conversation as resolved.
payload["envValueMode"] = "direct"

# Add custom agents configuration if provided
Expand Down Expand Up @@ -1713,6 +1720,7 @@ async def resume_session(
streaming: bool | None = None,
include_sub_agent_streaming_events: bool | None = None,
mcp_servers: dict[str, MCPServerConfig] | None = None,
mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None,
custom_agents: list[CustomAgentConfig] | None = None,
default_agent: DefaultAgentConfig | dict[str, Any] | None = None,
agent: str | None = None,
Expand Down Expand Up @@ -1775,6 +1783,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.
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).
Defaults to ``"in-memory"`` for safe multitenant behavior.
custom_agents: Custom agent configurations.
default_agent: Configuration for the default agent,
including tool visibility controls.
Expand Down Expand Up @@ -1918,6 +1930,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
# Default MCP OAuth token storage to in-memory for safe multitenant behavior
payload["mcpOAuthTokenStorage"] = mcp_oauth_token_storage or "in-memory"
payload["envValueMode"] = "direct"

if custom_agents:
Expand Down
10 changes: 10 additions & 0 deletions python/copilot/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,11 @@ class SessionConfig(TypedDict, total=False):
include_sub_agent_streaming_events: bool
# MCP server configurations for the session
mcp_servers: dict[str, MCPServerConfig]
# 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, discarded when the session ends.
# Defaults to "in-memory" for safe multitenant behavior.
mcp_oauth_token_storage: Literal["persistent", "in-memory"]
# Custom agent configurations for the session
custom_agents: list[CustomAgentConfig]
# Configuration for the default agent.
Expand Down Expand Up @@ -1034,6 +1039,11 @@ class ResumeSessionConfig(TypedDict, total=False):
include_sub_agent_streaming_events: bool
# MCP server configurations for the session
mcp_servers: dict[str, MCPServerConfig]
# 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, discarded when the session ends.
# Defaults to "in-memory" for safe multitenant behavior.
mcp_oauth_token_storage: Literal["persistent", "in-memory"]
# Custom agent configurations for the session
custom_agents: list[CustomAgentConfig]
# Configuration for the default agent.
Expand Down
Loading
Loading