Skip to content

Commit e5aa2f5

Browse files
feat: add mcpOAuthTokenStorage support across all SDKs
Add the mcpOAuthTokenStorage protocol property to session creation and resume flows in all five language SDKs (Node.js, Python, Go, .NET, Rust). When set to "in-memory", the runtime uses an in-memory MCP OAuth token store instead of the OS keychain. The SDK defaults to "in-memory" for safe multitenant behavior. - Node.js: Add to SessionConfig interface and ResumeSessionConfig Pick type - Python: Add to both TypedDicts and client methods with docstrings - Go: Add to config structs, wire request structs, and client wiring - .NET: Add McpOAuthTokenStorageMode enum with JsonStringEnumConverter, update config classes, copy constructors, wire records, and serialization context - Rust: Add field, builder methods, Default/new impls, and Debug impls Tests: - Rust: Assert defaults and builder composition in existing type tests - .NET: Add property to SessionConfig_Clone_CopiesAllProperties test - Go: Add wire serialization tests for both request types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f6c1adf commit e5aa2f5

11 files changed

Lines changed: 189 additions & 0 deletions

File tree

dotnet/src/Client.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
616616
config.Streaming is true ? true : null,
617617
config.IncludeSubAgentStreamingEvents,
618618
config.McpServers,
619+
config.McpOAuthTokenStorage ?? McpOAuthTokenStorageMode.InMemory,
619620
"direct",
620621
config.CustomAgents,
621622
config.DefaultAgent,
@@ -780,6 +781,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
780781
config.Streaming is true ? true : null,
781782
config.IncludeSubAgentStreamingEvents,
782783
config.McpServers,
784+
config.McpOAuthTokenStorage ?? McpOAuthTokenStorageMode.InMemory,
783785
"direct",
784786
config.CustomAgents,
785787
config.DefaultAgent,
@@ -1986,6 +1988,7 @@ internal record CreateSessionRequest(
19861988
bool? Streaming,
19871989
bool? IncludeSubAgentStreamingEvents,
19881990
IDictionary<string, McpServerConfig>? McpServers,
1991+
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
19891992
string? EnvValueMode,
19901993
IList<CustomAgentConfig>? CustomAgents,
19911994
DefaultAgentConfig? DefaultAgent,
@@ -2050,6 +2053,7 @@ internal record ResumeSessionRequest(
20502053
bool? Streaming,
20512054
bool? IncludeSubAgentStreamingEvents,
20522055
IDictionary<string, McpServerConfig>? McpServers,
2056+
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
20532057
string? EnvValueMode,
20542058
IList<CustomAgentConfig>? CustomAgents,
20552059
DefaultAgentConfig? DefaultAgent,
@@ -2139,6 +2143,7 @@ internal record PermissionRequestResponseV2(
21392143
[JsonSerializable(typeof(ListSessionsResponse))]
21402144
[JsonSerializable(typeof(GetSessionMetadataRequest))]
21412145
[JsonSerializable(typeof(GetSessionMetadataResponse))]
2146+
[JsonSerializable(typeof(McpOAuthTokenStorageMode))]
21422147
[JsonSerializable(typeof(ModelCapabilitiesOverride))]
21432148
[JsonSerializable(typeof(PermissionRequestResult))]
21442149
[JsonSerializable(typeof(PermissionRequestResultKind))]

dotnet/src/Types.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,6 +1804,21 @@ public enum McpHttpServerConfigOauthGrantType
18041804
ClientCredentials
18051805
}
18061806

1807+
/// <summary>
1808+
/// Controls how MCP OAuth tokens are stored for a session.
1809+
/// </summary>
1810+
[JsonConverter(typeof(JsonStringEnumConverter<McpOAuthTokenStorageMode>))]
1811+
public enum McpOAuthTokenStorageMode
1812+
{
1813+
/// <summary>Tokens are stored in the OS keychain, shared across sessions.</summary>
1814+
[JsonStringEnumMemberName("persistent")]
1815+
Persistent,
1816+
1817+
/// <summary>Tokens are stored in memory and discarded when the session ends.</summary>
1818+
[JsonStringEnumMemberName("in-memory")]
1819+
InMemory
1820+
}
1821+
18071822
/// <summary>
18081823
/// Abstract base class for MCP server configurations.
18091824
/// </summary>
@@ -2085,6 +2100,7 @@ protected SessionConfig(SessionConfig? other)
20852100
? new Dictionary<string, McpServerConfig>(dict, dict.Comparer)
20862101
: new Dictionary<string, McpServerConfig>(other.McpServers))
20872102
: null;
2103+
McpOAuthTokenStorage = other.McpOAuthTokenStorage;
20882104
Model = other.Model;
20892105
ModelCapabilities = other.ModelCapabilities;
20902106
OnAutoModeSwitch = other.OnAutoModeSwitch;
@@ -2261,6 +2277,12 @@ protected SessionConfig(SessionConfig? other)
22612277
/// </summary>
22622278
public IDictionary<string, McpServerConfig>? McpServers { get; set; }
22632279

2280+
/// <summary>
2281+
/// Controls how MCP OAuth tokens are stored for this session.
2282+
/// Default: <see cref="McpOAuthTokenStorageMode.InMemory"/> for safe multitenant behavior.
2283+
/// </summary>
2284+
public McpOAuthTokenStorageMode? McpOAuthTokenStorage { get; set; }
2285+
22642286
/// <summary>
22652287
/// Custom agent configurations for the session.
22662288
/// </summary>
@@ -2394,6 +2416,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
23942416
? new Dictionary<string, McpServerConfig>(dict, dict.Comparer)
23952417
: new Dictionary<string, McpServerConfig>(other.McpServers))
23962418
: null;
2419+
McpOAuthTokenStorage = other.McpOAuthTokenStorage;
23972420
Model = other.Model;
23982421
ModelCapabilities = other.ModelCapabilities;
23992422
OnAutoModeSwitch = other.OnAutoModeSwitch;
@@ -2587,6 +2610,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
25872610
/// </summary>
25882611
public IDictionary<string, McpServerConfig>? McpServers { get; set; }
25892612

2613+
/// <summary>
2614+
/// Controls how MCP OAuth tokens are stored for this session.
2615+
/// Default: <see cref="McpOAuthTokenStorageMode.InMemory"/> for safe multitenant behavior.
2616+
/// </summary>
2617+
public McpOAuthTokenStorageMode? McpOAuthTokenStorage { get; set; }
2618+
25902619
/// <summary>
25912620
/// Custom agent configurations for the session.
25922621
/// </summary>

dotnet/test/Unit/CloneTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
9494
EnableSessionTelemetry = false,
9595
IncludeSubAgentStreamingEvents = false,
9696
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
97+
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
9798
CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }],
9899
Agent = "agent1",
99100
Cloud = new CloudSessionOptions
@@ -127,6 +128,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
127128
Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry);
128129
Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents);
129130
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
131+
Assert.Equal(original.McpOAuthTokenStorage, clone.McpOAuthTokenStorage);
130132
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
131133
Assert.Equal(original.CustomAgents[0].Model, clone.CustomAgents[0].Model);
132134
Assert.Equal(original.Agent, clone.Agent);

go/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,11 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
636636
req.ModelCapabilities = config.ModelCapabilities
637637
req.WorkingDirectory = config.WorkingDirectory
638638
req.MCPServers = config.MCPServers
639+
if config.McpOAuthTokenStorage != "" {
640+
req.McpOAuthTokenStorage = config.McpOAuthTokenStorage
641+
} else {
642+
req.McpOAuthTokenStorage = "in-memory"
643+
}
639644
req.EnvValueMode = "direct"
640645
req.CustomAgents = config.CustomAgents
641646
req.DefaultAgent = config.DefaultAgent
@@ -841,6 +846,11 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
841846
req.ContinuePendingWork = Bool(true)
842847
}
843848
req.MCPServers = config.MCPServers
849+
if config.McpOAuthTokenStorage != "" {
850+
req.McpOAuthTokenStorage = config.McpOAuthTokenStorage
851+
} else {
852+
req.McpOAuthTokenStorage = "in-memory"
853+
}
844854
req.EnvValueMode = "direct"
845855
req.CustomAgents = config.CustomAgents
846856
req.DefaultAgent = config.DefaultAgent

go/client_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,60 @@ func TestResumeSessionRequest_InstructionDirectories(t *testing.T) {
615615
})
616616
}
617617

618+
func TestCreateSessionRequest_McpOAuthTokenStorage(t *testing.T) {
619+
t.Run("includes mcpOAuthTokenStorage in JSON when set", func(t *testing.T) {
620+
req := createSessionRequest{McpOAuthTokenStorage: "in-memory"}
621+
data, err := json.Marshal(req)
622+
if err != nil {
623+
t.Fatalf("Failed to marshal: %v", err)
624+
}
625+
var m map[string]any
626+
if err := json.Unmarshal(data, &m); err != nil {
627+
t.Fatalf("Failed to unmarshal: %v", err)
628+
}
629+
if m["mcpOAuthTokenStorage"] != "in-memory" {
630+
t.Errorf("Expected mcpOAuthTokenStorage to be 'in-memory', got %v", m["mcpOAuthTokenStorage"])
631+
}
632+
})
633+
634+
t.Run("omits mcpOAuthTokenStorage from JSON when empty", func(t *testing.T) {
635+
req := createSessionRequest{}
636+
data, _ := json.Marshal(req)
637+
var m map[string]any
638+
json.Unmarshal(data, &m)
639+
if _, ok := m["mcpOAuthTokenStorage"]; ok {
640+
t.Error("Expected mcpOAuthTokenStorage to be omitted when empty")
641+
}
642+
})
643+
}
644+
645+
func TestResumeSessionRequest_McpOAuthTokenStorage(t *testing.T) {
646+
t.Run("includes mcpOAuthTokenStorage in JSON when set", func(t *testing.T) {
647+
req := resumeSessionRequest{SessionID: "s1", McpOAuthTokenStorage: "persistent"}
648+
data, err := json.Marshal(req)
649+
if err != nil {
650+
t.Fatalf("Failed to marshal: %v", err)
651+
}
652+
var m map[string]any
653+
if err := json.Unmarshal(data, &m); err != nil {
654+
t.Fatalf("Failed to unmarshal: %v", err)
655+
}
656+
if m["mcpOAuthTokenStorage"] != "persistent" {
657+
t.Errorf("Expected mcpOAuthTokenStorage to be 'persistent', got %v", m["mcpOAuthTokenStorage"])
658+
}
659+
})
660+
661+
t.Run("omits mcpOAuthTokenStorage from JSON when empty", func(t *testing.T) {
662+
req := resumeSessionRequest{SessionID: "s1"}
663+
data, _ := json.Marshal(req)
664+
var m map[string]any
665+
json.Unmarshal(data, &m)
666+
if _, ok := m["mcpOAuthTokenStorage"]; ok {
667+
t.Error("Expected mcpOAuthTokenStorage to be omitted when empty")
668+
}
669+
})
670+
}
671+
618672
func TestOverridesBuiltInTool(t *testing.T) {
619673
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
620674
tool := Tool{

go/types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,11 @@ type SessionConfig struct {
658658
ModelCapabilities *rpc.ModelCapabilitiesOverride
659659
// MCPServers configures MCP servers for the session
660660
MCPServers map[string]MCPServerConfig
661+
// McpOAuthTokenStorage controls how MCP OAuth tokens are stored for this session.
662+
// "persistent" stores tokens in the OS keychain (shared across sessions).
663+
// "in-memory" stores tokens in memory and discards them when the session ends.
664+
// Defaults to "in-memory" for safe multitenant behavior.
665+
McpOAuthTokenStorage string
661666
// CustomAgents configures custom agents for the session
662667
CustomAgents []CustomAgentConfig
663668
// DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected).
@@ -902,6 +907,11 @@ type ResumeSessionConfig struct {
902907
IncludeSubAgentStreamingEvents *bool
903908
// MCPServers configures MCP servers for the session
904909
MCPServers map[string]MCPServerConfig
910+
// McpOAuthTokenStorage controls how MCP OAuth tokens are stored for this session.
911+
// "persistent" stores tokens in the OS keychain (shared across sessions).
912+
// "in-memory" stores tokens in memory and discards them when the session ends.
913+
// Defaults to "in-memory" for safe multitenant behavior.
914+
McpOAuthTokenStorage string
905915
// CustomAgents configures custom agents for the session
906916
CustomAgents []CustomAgentConfig
907917
// DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected).
@@ -1162,6 +1172,7 @@ type createSessionRequest struct {
11621172
Streaming *bool `json:"streaming,omitempty"`
11631173
IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"`
11641174
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
1175+
McpOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"`
11651176
EnvValueMode string `json:"envValueMode,omitempty"`
11661177
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
11671178
DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`
@@ -1220,6 +1231,7 @@ type resumeSessionRequest struct {
12201231
Streaming *bool `json:"streaming,omitempty"`
12211232
IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"`
12221233
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
1234+
McpOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"`
12231235
EnvValueMode string `json:"envValueMode,omitempty"`
12241236
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
12251237
DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`

nodejs/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,7 @@ export class CopilotClient {
824824
streaming: config.streaming,
825825
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
826826
mcpServers: config.mcpServers,
827+
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage ?? "in-memory",
827828
envValueMode: "direct",
828829
customAgents: config.customAgents,
829830
defaultAgent: config.defaultAgent,
@@ -981,6 +982,7 @@ export class CopilotClient {
981982
streaming: config.streaming,
982983
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
983984
mcpServers: config.mcpServers,
985+
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage ?? "in-memory",
984986
envValueMode: "direct",
985987
customAgents: config.customAgents,
986988
defaultAgent: config.defaultAgent,

nodejs/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,15 @@ export interface SessionConfig {
14441444
*/
14451445
includeSubAgentStreamingEvents?: boolean;
14461446

1447+
/**
1448+
* Controls how MCP OAuth tokens are stored for this session.
1449+
* - `"persistent"` — tokens are stored in the OS keychain (shared across sessions)
1450+
* - `"in-memory"` — tokens are stored in memory and discarded when the session ends
1451+
*
1452+
* @default "in-memory"
1453+
*/
1454+
mcpOAuthTokenStorage?: "persistent" | "in-memory";
1455+
14471456
/**
14481457
* MCP server configurations for the session.
14491458
* Keys are server names, values are server configurations.
@@ -1567,6 +1576,7 @@ export type ResumeSessionConfig = Pick<
15671576
| "customAgents"
15681577
| "defaultAgent"
15691578
| "agent"
1579+
| "mcpOAuthTokenStorage"
15701580
| "skillDirectories"
15711581
| "instructionDirectories"
15721582
| "disabledSkills"

python/copilot/client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,7 @@ async def create_session(
13401340
streaming: bool | None = None,
13411341
include_sub_agent_streaming_events: bool | None = None,
13421342
mcp_servers: dict[str, MCPServerConfig] | None = None,
1343+
mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None,
13431344
custom_agents: list[CustomAgentConfig] | None = None,
13441345
default_agent: DefaultAgentConfig | dict[str, Any] | None = None,
13451346
agent: str | None = None,
@@ -1402,6 +1403,10 @@ async def create_session(
14021403
``agentId`` set). When False, only non-streaming sub-agent events and
14031404
``subagent.*`` lifecycle events are forwarded. Defaults to True.
14041405
mcp_servers: MCP server configurations.
1406+
mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored.
1407+
``"persistent"`` uses the OS keychain (shared across sessions).
1408+
``"in-memory"`` stores tokens in memory (discarded on session end).
1409+
Defaults to ``"in-memory"`` for safe multitenant behavior.
14051410
custom_agents: Custom agent configurations.
14061411
default_agent: Configuration for the default agent,
14071412
including tool visibility controls.
@@ -1551,6 +1556,8 @@ async def create_session(
15511556
# Add MCP servers configuration if provided
15521557
if mcp_servers:
15531558
payload["mcpServers"] = mcp_servers
1559+
# Default MCP OAuth token storage to in-memory for safe multitenant behavior
1560+
payload["mcpOAuthTokenStorage"] = mcp_oauth_token_storage or "in-memory"
15541561
payload["envValueMode"] = "direct"
15551562

15561563
# Add custom agents configuration if provided
@@ -1713,6 +1720,7 @@ async def resume_session(
17131720
streaming: bool | None = None,
17141721
include_sub_agent_streaming_events: bool | None = None,
17151722
mcp_servers: dict[str, MCPServerConfig] | None = None,
1723+
mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None,
17161724
custom_agents: list[CustomAgentConfig] | None = None,
17171725
default_agent: DefaultAgentConfig | dict[str, Any] | None = None,
17181726
agent: str | None = None,
@@ -1775,6 +1783,10 @@ async def resume_session(
17751783
``agentId`` set). When False, only non-streaming sub-agent events and
17761784
``subagent.*`` lifecycle events are forwarded. Defaults to True.
17771785
mcp_servers: MCP server configurations.
1786+
mcp_oauth_token_storage: Controls how MCP OAuth tokens are stored.
1787+
``"persistent"`` uses the OS keychain (shared across sessions).
1788+
``"in-memory"`` stores tokens in memory (discarded on session end).
1789+
Defaults to ``"in-memory"`` for safe multitenant behavior.
17781790
custom_agents: Custom agent configurations.
17791791
default_agent: Configuration for the default agent,
17801792
including tool visibility controls.
@@ -1918,6 +1930,8 @@ async def resume_session(
19181930
# TODO: disable_resume is not a keyword arg yet; keeping for future use
19191931
if mcp_servers:
19201932
payload["mcpServers"] = mcp_servers
1933+
# Default MCP OAuth token storage to in-memory for safe multitenant behavior
1934+
payload["mcpOAuthTokenStorage"] = mcp_oauth_token_storage or "in-memory"
19211935
payload["envValueMode"] = "direct"
19221936

19231937
if custom_agents:

python/copilot/session.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,11 @@ class SessionConfig(TypedDict, total=False):
947947
include_sub_agent_streaming_events: bool
948948
# MCP server configurations for the session
949949
mcp_servers: dict[str, MCPServerConfig]
950+
# Controls how MCP OAuth tokens are stored for this session.
951+
# "persistent" stores tokens in the OS keychain (shared across sessions).
952+
# "in-memory" stores tokens in memory, discarded when the session ends.
953+
# Defaults to "in-memory" for safe multitenant behavior.
954+
mcp_oauth_token_storage: Literal["persistent", "in-memory"]
950955
# Custom agent configurations for the session
951956
custom_agents: list[CustomAgentConfig]
952957
# Configuration for the default agent.
@@ -1034,6 +1039,11 @@ class ResumeSessionConfig(TypedDict, total=False):
10341039
include_sub_agent_streaming_events: bool
10351040
# MCP server configurations for the session
10361041
mcp_servers: dict[str, MCPServerConfig]
1042+
# Controls how MCP OAuth tokens are stored for this session.
1043+
# "persistent" stores tokens in the OS keychain (shared across sessions).
1044+
# "in-memory" stores tokens in memory, discarded when the session ends.
1045+
# Defaults to "in-memory" for safe multitenant behavior.
1046+
mcp_oauth_token_storage: Literal["persistent", "in-memory"]
10371047
# Custom agent configurations for the session
10381048
custom_agents: list[CustomAgentConfig]
10391049
# Configuration for the default agent.

0 commit comments

Comments
 (0)