From b17ab41d160e45e9c62a3d852ba8c0636d9a45ff Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 4 Aug 2026 14:35:48 -0700 Subject: [PATCH 1/3] sdk: Expose disabled MCP servers across languages Allow callers to disable exact MCP server names per session on create and resume without mutating global settings. Cover all SDK wire mappings and verify plugin and built-in GitHub MCP servers stay stopped on the first message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/mcp.md | 29 ++ dotnet/src/Client.cs | 4 + dotnet/src/Types.cs | 8 + dotnet/test/Unit/CloneTests.cs | 9 + dotnet/test/Unit/SerializationTests.cs | 4 + go/client.go | 6 + go/client_test.go | 52 +- go/types.go | 10 + .../github/copilot/SessionRequestBuilder.java | 2 + .../copilot/rpc/CreateSessionRequest.java | 13 + .../copilot/rpc/ResumeSessionConfig.java | 24 + .../copilot/rpc/ResumeSessionRequest.java | 13 + .../com/github/copilot/rpc/SessionConfig.java | 24 + .../com/github/copilot/ConfigCloneTest.java | 6 + .../copilot/SessionRequestBuilderTest.java | 16 +- nodejs/src/client.ts | 2 + nodejs/src/types.ts | 7 + nodejs/test/client.test.ts | 5 + .../test/e2e/disabled_mcp_servers.e2e.test.ts | 485 ++++++++++++++++++ python/copilot/client.py | 14 + python/test_client.py | 27 + rust/src/types.rs | 86 ++++ rust/src/wire.rs | 4 + test/harness/replayingCapiProxy.ts | 45 ++ test/harness/test-mcp-server.mjs | 14 +- 25 files changed, 901 insertions(+), 8 deletions(-) create mode 100644 nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts diff --git a/docs/features/mcp.md b/docs/features/mcp.md index 6f715bd2ed..caac633275 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -154,6 +154,35 @@ await using var session = await client.CreateSessionAsync(new SessionConfig }); ``` +## Disabling configured servers per session + +Set `disabledMcpServers` to exact MCP server names that must not run in a session. +The setting is scoped to the individual create or resume request; it does not +modify global MCP settings or the server configuration. + +```typescript +const session = await client.createSession({ + mcpServers: { + filesystem: { type: "local", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] }, + github: { type: "http", url: "https://api.githubcopilot.com/mcp/" }, + }, + disabledMcpServers: ["github"], +}); +``` + +| SDK | Configuration property | +| --- | --- | +| Node.js | `disabledMcpServers` | +| Python | `disabled_mcp_servers` | +| Go | `DisabledMCPServers` | +| .NET | `DisabledMcpServers` | +| Java | `setDisabledMcpServers(...)` | +| Rust | `with_disabled_mcp_servers(...)` | + +On session creation and a **cold** resume, disabled servers are not started and +the runtime does not initiate their authentication. A resident resume cannot +undo a server that the runtime has already spawned. Names are matched exactly. + ## Tool configuration You can control which tools are available to an MCP server using the `tools` field. diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 46ce7ba807..b89c571844 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1186,6 +1186,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance Cloud: config.Cloud, InstructionDirectories: config.InstructionDirectories, PluginDirectories: config.PluginDirectories, + DisabledMcpServers: config.DisabledMcpServers, LargeOutput: config.LargeOutput, ToolSearch: config.ToolSearch, Memory: config.Memory, @@ -1401,6 +1402,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes ContinuePendingWork: config.ContinuePendingWork, InstructionDirectories: config.InstructionDirectories, PluginDirectories: config.PluginDirectories, + DisabledMcpServers: config.DisabledMcpServers, LargeOutput: config.LargeOutput, ToolSearch: config.ToolSearch, Memory: config.Memory, @@ -2755,6 +2757,7 @@ internal record CreateSessionRequest( CloudSessionOptions? Cloud = null, IList? InstructionDirectories = null, IList? PluginDirectories = null, + [property: JsonPropertyName("disabledMcpServers")] IList? DisabledMcpServers = null, LargeToolOutputConfig? LargeOutput = null, ToolSearchConfig? ToolSearch = null, MemoryConfiguration? Memory = null, @@ -2862,6 +2865,7 @@ internal record ResumeSessionRequest( bool? ContinuePendingWork = null, IList? InstructionDirectories = null, IList? PluginDirectories = null, + [property: JsonPropertyName("disabledMcpServers")] IList? DisabledMcpServers = null, LargeToolOutputConfig? LargeOutput = null, ToolSearchConfig? ToolSearch = null, MemoryConfiguration? Memory = null, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 6b511117cd..9bbb52b8e1 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -3016,6 +3016,7 @@ protected SessionConfigBase(SessionConfigBase? other) DefaultAgent = other.DefaultAgent; Agent = other.Agent; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; + DisabledMcpServers = other.DisabledMcpServers is not null ? [.. other.DisabledMcpServers] : null; EnableCitations = other.EnableCitations; EnableConfigDiscovery = other.EnableConfigDiscovery; SkipEmbeddingRetrieval = other.SkipEmbeddingRetrieval; @@ -3429,6 +3430,13 @@ protected SessionConfigBase(SessionConfigBase? other) /// List of skill names to disable. public IList? DisabledSkills { get; set; } + /// + /// Exact MCP server names to disable for this session. Disabled servers are not + /// started or authenticated on create or cold resume; a resident resume cannot + /// stop servers that are already running. + /// + public IList? DisabledMcpServers { get; set; } + /// /// Infinite session configuration for persistent workspaces and automatic compaction. /// When enabled (default), sessions automatically manage context limits and persist state. diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index ec509ab169..89bb0a7fab 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -98,6 +98,7 @@ public void SessionConfig_Clone_CopiesAllProperties() SkillDirectories = ["/skills"], InstructionDirectories = ["/instructions"], DisabledSkills = ["skill1"], + DisabledMcpServers = ["server1"], PluginDirectories = ["/plugins"], LargeOutput = new LargeToolOutputConfig { Enabled = true, MaxSizeBytes = 2048, OutputDirectory = "/tmp/out" }, Memory = new MemoryConfiguration { Enabled = true }, @@ -136,6 +137,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.SkillDirectories, clone.SkillDirectories); Assert.Equal(original.InstructionDirectories, clone.InstructionDirectories); Assert.Equal(original.DisabledSkills, clone.DisabledSkills); + Assert.Equal(original.DisabledMcpServers, clone.DisabledMcpServers); Assert.Equal(original.PluginDirectories, clone.PluginDirectories); Assert.Same(original.LargeOutput, clone.LargeOutput); Assert.Same(original.Memory, clone.Memory); @@ -157,6 +159,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent() SkillDirectories = ["/skills"], InstructionDirectories = ["/instructions"], DisabledSkills = ["skill1"], + DisabledMcpServers = ["server1"], }; var clone = original.Clone(); @@ -170,6 +173,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent() clone.SkillDirectories!.Add("/more"); clone.InstructionDirectories!.Add("/more-instructions"); clone.DisabledSkills!.Add("skill99"); + clone.DisabledMcpServers!.Add("server99"); // Original is unaffected Assert.Single(original.AvailableTools!); @@ -180,6 +184,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent() Assert.Single(original.SkillDirectories!); Assert.Single(original.InstructionDirectories!); Assert.Single(original.DisabledSkills!); + Assert.Single(original.DisabledMcpServers!); } [Fact] @@ -206,6 +211,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent() SkillDirectories = ["/skills"], InstructionDirectories = ["/instructions"], DisabledSkills = ["skill1"], + DisabledMcpServers = ["server1"], }; var clone = original.Clone(); @@ -219,6 +225,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent() clone.SkillDirectories!.Add("/more"); clone.InstructionDirectories!.Add("/more-instructions"); clone.DisabledSkills!.Add("skill99"); + clone.DisabledMcpServers!.Add("server99"); // Original is unaffected Assert.Single(original.AvailableTools!); @@ -229,6 +236,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent() Assert.Single(original.SkillDirectories!); Assert.Single(original.InstructionDirectories!); Assert.Single(original.DisabledSkills!); + Assert.Single(original.DisabledMcpServers!); } [Fact] @@ -289,6 +297,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections() Assert.Null(clone.SkillDirectories); Assert.Null(clone.InstructionDirectories); Assert.Null(clone.DisabledSkills); + Assert.Null(clone.DisabledMcpServers); Assert.Null(clone.Tools); Assert.Null(clone.DefaultAgent); Assert.True(clone.IncludeSubAgentStreamingEvents); diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 9108a81343..d7cf9d88ca 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -411,12 +411,14 @@ public void SessionRequests_CanSerializePluginDirectoriesAndLargeOutput_WithSdkO createRequestType, ("SessionId", "session-id"), ("PluginDirectories", pluginDirs), + ("DisabledMcpServers", new List { "local-files", "remote-github" }), ("LargeOutput", largeOutput)); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); var createRoot = createDocument.RootElement; Assert.Equal("/tmp/plugins/a", createRoot.GetProperty("pluginDirectories")[0].GetString()); + Assert.Equal("local-files", createRoot.GetProperty("disabledMcpServers")[0].GetString()); Assert.Equal("/tmp/plugins/b", createRoot.GetProperty("pluginDirectories")[1].GetString()); var createLargeOutput = createRoot.GetProperty("largeOutput"); Assert.True(createLargeOutput.GetProperty("enabled").GetBoolean()); @@ -428,12 +430,14 @@ public void SessionRequests_CanSerializePluginDirectoriesAndLargeOutput_WithSdkO resumeRequestType, ("SessionId", "session-id"), ("PluginDirectories", pluginDirs), + ("DisabledMcpServers", new List { "local-files", "remote-github" }), ("LargeOutput", largeOutput)); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); var resumeRoot = resumeDocument.RootElement; Assert.Equal("/tmp/plugins/a", resumeRoot.GetProperty("pluginDirectories")[0].GetString()); + Assert.Equal("local-files", resumeRoot.GetProperty("disabledMcpServers")[0].GetString()); var resumeLargeOutput = resumeRoot.GetProperty("largeOutput"); Assert.True(resumeLargeOutput.GetProperty("enabled").GetBoolean()); Assert.Equal(1024, resumeLargeOutput.GetProperty("maxSizeBytes").GetInt64()); diff --git a/go/client.go b/go/client.go index 292a5729e5..95588da36f 100644 --- a/go/client.go +++ b/go/client.go @@ -812,6 +812,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.PluginDirectories = config.PluginDirectories req.InstructionDirectories = config.InstructionDirectories req.DisabledSkills = config.DisabledSkills + if config.DisabledMCPServers != nil { + req.DisabledMCPServers = &config.DisabledMCPServers + } req.InfiniteSessions = config.InfiniteSessions req.LargeOutput = config.LargeOutput req.ToolSearch = config.ToolSearch @@ -1187,6 +1190,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.PluginDirectories = config.PluginDirectories req.InstructionDirectories = config.InstructionDirectories req.DisabledSkills = config.DisabledSkills + if config.DisabledMCPServers != nil { + req.DisabledMCPServers = &config.DisabledMCPServers + } req.InfiniteSessions = config.InfiniteSessions req.LargeOutput = config.LargeOutput req.ToolSearch = config.ToolSearch diff --git a/go/client_test.go b/go/client_test.go index 2301c990d3..7669517b8e 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1049,9 +1049,11 @@ func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { "outputDir": "/tmp/large-output", } expectedPluginDirs := []any{"/tmp/plugins/a", "/tmp/plugins/b"} + expectedDisabledMCPServers := []any{"local-files", "remote-github"} + disabledMCPServers := []string{"local-files", "remote-github"} t.Run("create includes pluginDirectories and largeOutput in JSON when set", func(t *testing.T) { - req := createSessionRequest{PluginDirectories: pluginDirs, LargeOutput: largeOutput} + req := createSessionRequest{PluginDirectories: pluginDirs, DisabledMCPServers: &disabledMCPServers, LargeOutput: largeOutput} data, err := json.Marshal(req) if err != nil { t.Fatalf("Failed to marshal: %v", err) @@ -1063,13 +1065,16 @@ func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { if !reflect.DeepEqual(m["pluginDirectories"], expectedPluginDirs) { t.Errorf("Expected pluginDirectories %v, got %v", expectedPluginDirs, m["pluginDirectories"]) } + if !reflect.DeepEqual(m["disabledMcpServers"], expectedDisabledMCPServers) { + t.Errorf("Expected disabledMcpServers %v, got %v", expectedDisabledMCPServers, m["disabledMcpServers"]) + } if !reflect.DeepEqual(m["largeOutput"], expectedLargeOutput) { t.Errorf("Expected largeOutput %v, got %v", expectedLargeOutput, m["largeOutput"]) } }) t.Run("resume includes pluginDirectories and largeOutput in JSON when set", func(t *testing.T) { - req := resumeSessionRequest{SessionID: "s1", PluginDirectories: pluginDirs, LargeOutput: largeOutput} + req := resumeSessionRequest{SessionID: "s1", PluginDirectories: pluginDirs, DisabledMCPServers: &disabledMCPServers, LargeOutput: largeOutput} data, err := json.Marshal(req) if err != nil { t.Fatalf("Failed to marshal: %v", err) @@ -1081,11 +1086,36 @@ func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { if !reflect.DeepEqual(m["pluginDirectories"], expectedPluginDirs) { t.Errorf("Expected pluginDirectories %v, got %v", expectedPluginDirs, m["pluginDirectories"]) } + if !reflect.DeepEqual(m["disabledMcpServers"], expectedDisabledMCPServers) { + t.Errorf("Expected disabledMcpServers %v, got %v", expectedDisabledMCPServers, m["disabledMcpServers"]) + } if !reflect.DeepEqual(m["largeOutput"], expectedLargeOutput) { t.Errorf("Expected largeOutput %v, got %v", expectedLargeOutput, m["largeOutput"]) } }) + t.Run("create and resume include explicit empty disabledMcpServers", func(t *testing.T) { + emptyDisabledMCPServers := []string{} + requests := []any{ + createSessionRequest{DisabledMCPServers: &emptyDisabledMCPServers}, + resumeSessionRequest{SessionID: "s1", DisabledMCPServers: &emptyDisabledMCPServers}, + } + + for _, request := range requests { + data, err := json.Marshal(request) + 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 value, ok := m["disabledMcpServers"]; !ok || !reflect.DeepEqual(value, []any{}) { + t.Errorf("Expected explicit empty disabledMcpServers, got %v", value) + } + } + }) + t.Run("create omits pluginDirectories and largeOutput when nil", func(t *testing.T) { req := createSessionRequest{} data, err := json.Marshal(req) @@ -1099,10 +1129,28 @@ func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { if _, ok := m["pluginDirectories"]; ok { t.Errorf("Expected pluginDirectories to be omitted") } + if _, ok := m["disabledMcpServers"]; ok { + t.Error("Expected disabledMcpServers to be omitted") + } if _, ok := m["largeOutput"]; ok { t.Errorf("Expected largeOutput to be omitted") } }) + + t.Run("resume omits disabledMcpServers when nil", 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["disabledMcpServers"]; ok { + t.Error("Expected disabledMcpServers to be omitted") + } + }) } func TestSessionRequests_Memory(t *testing.T) { diff --git a/go/types.go b/go/types.go index d625310b1b..4d4f4b7a8c 100644 --- a/go/types.go +++ b/go/types.go @@ -1328,6 +1328,10 @@ type SessionConfig struct { InstructionDirectories []string // DisabledSkills is a list of skill names to disable DisabledSkills []string + // DisabledMCPServers is a list of exact MCP server names to disable for this session. + // Disabled servers are not started or authenticated on create or cold resume. + // A resident resume cannot stop servers that are already running. + DisabledMCPServers []string // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. // When enabled (default), sessions automatically manage context limits and persist state. InfiniteSessions *InfiniteSessionConfig @@ -1800,6 +1804,10 @@ type ResumeSessionConfig struct { InstructionDirectories []string // DisabledSkills is a list of skill names to disable DisabledSkills []string + // DisabledMCPServers is a list of exact MCP server names to disable for this session. + // Disabled servers are not started or authenticated on create or cold resume. + // A resident resume cannot stop servers that are already running. + DisabledMCPServers []string // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. InfiniteSessions *InfiniteSessionConfig // LargeOutput configures handling of large tool outputs. When a tool produces @@ -2333,6 +2341,7 @@ type createSessionRequest struct { PluginDirectories []string `json:"pluginDirectories,omitempty"` InstructionDirectories []string `json:"instructionDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` + DisabledMCPServers *[]string `json:"disabledMcpServers,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"` ToolSearch *ToolSearchConfig `json:"toolSearch,omitempty"` @@ -2426,6 +2435,7 @@ type resumeSessionRequest struct { PluginDirectories []string `json:"pluginDirectories,omitempty"` InstructionDirectories []string `json:"instructionDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` + DisabledMCPServers *[]string `json:"disabledMcpServers,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"` ToolSearch *ToolSearchConfig `json:"toolSearch,omitempty"` diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index 1959a9ef83..c27d73c3c4 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -156,6 +156,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setToolSearch(config.getToolSearch()); request.setMemory(config.getMemory()); request.setDisabledSkills(config.getDisabledSkills()); + request.setDisabledMcpServers(config.getDisabledMcpServers()); request.setConfigDirectory(config.getConfigDirectory()); config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); config.getSkipEmbeddingRetrieval().ifPresent(request::setSkipEmbeddingRetrieval); @@ -302,6 +303,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setToolSearch(config.getToolSearch()); request.setMemory(config.getMemory()); request.setDisabledSkills(config.getDisabledSkills()); + request.setDisabledMcpServers(config.getDisabledMcpServers()); request.setInfiniteSessions(config.getInfiniteSessions()); request.setModelCapabilities(config.getModelCapabilities()); diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 1755fddbfa..2ffe35e647 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -149,6 +149,9 @@ public final class CreateSessionRequest { @JsonProperty("disabledSkills") private List disabledSkills; + @JsonProperty("disabledMcpServers") + private List disabledMcpServers; + @JsonProperty("configDir") private String configDirectory; @@ -675,6 +678,16 @@ public void setDisabledSkills(List disabledSkills) { this.disabledSkills = disabledSkills; } + /** Gets disabled MCP server names. @return the server names */ + public List getDisabledMcpServers() { + return disabledMcpServers == null ? null : Collections.unmodifiableList(disabledMcpServers); + } + + /** Sets disabled MCP server names. @param disabledMcpServers the server names */ + public void setDisabledMcpServers(List disabledMcpServers) { + this.disabledMcpServers = disabledMcpServers; + } + /** Gets config directory. @return the config directory path */ public String getConfigDirectory() { return configDirectory; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 42419e5a36..7cac5e74b9 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -92,6 +92,7 @@ public class ResumeSessionConfig { private ToolSearchConfig toolSearch; private MemoryConfiguration memory; private List disabledSkills; + private List disabledMcpServers; private InfiniteSessionConfig infiniteSessions; private Consumer onEvent; private List commands; @@ -1507,6 +1508,28 @@ public ResumeSessionConfig setDisabledSkills(List disabledSkills) { return this; } + /** + * Gets exact MCP server names disabled for this session. + * + * @return the disabled MCP server names, or {@code null} when none are disabled + */ + public List getDisabledMcpServers() { + return disabledMcpServers == null ? null : Collections.unmodifiableList(disabledMcpServers); + } + + /** + * Sets exact MCP server names to disable for this session. Disabled servers are + * not started or authenticated on create or cold resume; a resident resume + * cannot stop servers already running. + * + * @param disabledMcpServers the server names to disable + * @return this config for method chaining + */ + public ResumeSessionConfig setDisabledMcpServers(List disabledMcpServers) { + this.disabledMcpServers = disabledMcpServers; + return this; + } + /** * Gets the infinite session configuration. * @@ -1880,6 +1903,7 @@ public ResumeSessionConfig clone() { copy.toolSearch = this.toolSearch; copy.memory = this.memory; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.disabledMcpServers = this.disabledMcpServers != null ? new ArrayList<>(this.disabledMcpServers) : null; copy.infiniteSessions = this.infiniteSessions; copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 55bd08b915..4ff7a0dbaa 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -189,6 +189,9 @@ public final class ResumeSessionRequest { @JsonProperty("disabledSkills") private List disabledSkills; + @JsonProperty("disabledMcpServers") + private List disabledMcpServers; + @JsonProperty("infiniteSessions") private InfiniteSessionConfig infiniteSessions; @@ -891,6 +894,16 @@ public void setDisabledSkills(List disabledSkills) { this.disabledSkills = disabledSkills; } + /** Gets disabled MCP server names. @return the server names */ + public List getDisabledMcpServers() { + return disabledMcpServers == null ? null : Collections.unmodifiableList(disabledMcpServers); + } + + /** Sets disabled MCP server names. @param disabledMcpServers the server names */ + public void setDisabledMcpServers(List disabledMcpServers) { + this.disabledMcpServers = disabledMcpServers; + } + /** Gets infinite sessions config. @return the infinite sessions config */ public InfiniteSessionConfig getInfiniteSessions() { return infiniteSessions; diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index 7033ba5572..e0aa1bd7e9 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -82,6 +82,7 @@ public class SessionConfig { private ToolSearchConfig toolSearch; private MemoryConfiguration memory; private List disabledSkills; + private List disabledMcpServers; private String configDirectory; private Boolean enableConfigDiscovery; private Boolean skipEmbeddingRetrieval; @@ -1198,6 +1199,28 @@ public SessionConfig setDisabledSkills(List disabledSkills) { return this; } + /** + * Gets exact MCP server names disabled for this session. + * + * @return the disabled MCP server names, or {@code null} when none are disabled + */ + public List getDisabledMcpServers() { + return disabledMcpServers == null ? null : Collections.unmodifiableList(disabledMcpServers); + } + + /** + * Sets exact MCP server names to disable for this session. Disabled servers are + * not started or authenticated on create or cold resume; a resident resume + * cannot stop servers already running. + * + * @param disabledMcpServers the server names to disable + * @return this config for method chaining + */ + public SessionConfig setDisabledMcpServers(List disabledMcpServers) { + this.disabledMcpServers = disabledMcpServers; + return this; + } + /** * Gets the custom configuration directory. * @@ -2005,6 +2028,7 @@ public SessionConfig clone() { copy.toolSearch = this.toolSearch; copy.memory = this.memory; copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.disabledMcpServers = this.disabledMcpServers != null ? new ArrayList<>(this.disabledMcpServers) : null; copy.configDirectory = this.configDirectory; copy.enableConfigDiscovery = this.enableConfigDiscovery; copy.skipEmbeddingRetrieval = this.skipEmbeddingRetrieval; diff --git a/java/src/test/java/com/github/copilot/ConfigCloneTest.java b/java/src/test/java/com/github/copilot/ConfigCloneTest.java index 6986ef7f0e..111e6433f6 100644 --- a/java/src/test/java/com/github/copilot/ConfigCloneTest.java +++ b/java/src/test/java/com/github/copilot/ConfigCloneTest.java @@ -120,6 +120,7 @@ void sessionConfigCloneBasic() { original.setReasoningSummary("detailed"); original.setContextTier("long_context"); original.setPluginDirectories(List.of("/plugins/a", "/plugins/b")); + original.setDisabledMcpServers(List.of("local-files", "remote-github")); original.setLargeOutput( new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L).setOutputDirectory("/tmp/out")); original.setMemory(new MemoryConfiguration().setEnabled(true)); @@ -133,6 +134,7 @@ void sessionConfigCloneBasic() { assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); assertEquals(original.getContextTier(), cloned.getContextTier()); assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); + assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers()); assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); assertEquals(original.getMemory(), cloned.getMemory()); assertEquals(original.isStreaming(), cloned.isStreaming()); @@ -146,6 +148,7 @@ void sessionConfigListIndependence() { toolList.add("bash"); original.setAvailableTools(toolList); original.setInstructionDirectories(new ArrayList<>(List.of("/path/a", "/path/b"))); + original.setDisabledMcpServers(new ArrayList<>(List.of("local-files"))); SessionConfig cloned = original.clone(); @@ -156,6 +159,7 @@ void sessionConfigListIndependence() { assertEquals(2, cloned.getAvailableTools().size()); assertEquals(3, original.getAvailableTools().size()); assertEquals(List.of("/path/a", "/path/b"), cloned.getInstructionDirectories()); + assertEquals(List.of("local-files"), cloned.getDisabledMcpServers()); } @Test @@ -194,6 +198,7 @@ void resumeSessionConfigCloneBasic() { original.setReasoningSummary("none"); original.setContextTier("long_context"); original.setPluginDirectories(List.of("/plugins/r")); + original.setDisabledMcpServers(List.of("local-files-r")); original.setLargeOutput( new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L).setOutputDirectory("/tmp/resume")); original.setMemory(new MemoryConfiguration().setEnabled(false)); @@ -205,6 +210,7 @@ void resumeSessionConfigCloneBasic() { assertEquals(original.getReasoningSummary(), cloned.getReasoningSummary()); assertEquals(original.getContextTier(), cloned.getContextTier()); assertEquals(original.getPluginDirectories(), cloned.getPluginDirectories()); + assertEquals(original.getDisabledMcpServers(), cloned.getDisabledMcpServers()); assertEquals(original.getLargeOutput(), cloned.getLargeOutput()); assertEquals(original.getMemory(), cloned.getMemory()); assertEquals(original.isStreaming(), cloned.isStreaming()); diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 505f93a5a7..b32dda2ee3 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -142,13 +142,17 @@ void testBuildCreateRequestSetsContextTier() { } @Test - void testBuildCreateRequestSetsPluginDirectoriesAndLargeOutput() { + void testBuildCreateRequestSetsPluginDirectoriesAndLargeOutput() throws Exception { var largeOutput = new LargeToolOutputConfig().setEnabled(true).setMaxSizeBytes(1024L) .setOutputDirectory("/tmp/out"); - var config = new SessionConfig().setPluginDirectories(List.of("/plugins/a")).setLargeOutput(largeOutput); + var config = new SessionConfig().setPluginDirectories(List.of("/plugins/a")) + .setDisabledMcpServers(List.of("local-files", "remote-github")).setLargeOutput(largeOutput); CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); assertEquals(List.of("/plugins/a"), request.getPluginDirectories()); + assertEquals(List.of("local-files", "remote-github"), request.getDisabledMcpServers()); assertEquals(largeOutput, request.getLargeOutput()); + assertTrue(JsonRpcClient.getObjectMapper().writeValueAsString(request) + .contains("\"disabledMcpServers\":[\"local-files\",\"remote-github\"]")); } @Test @@ -405,13 +409,17 @@ void testBuildResumeRequestSetsContextTier() { } @Test - void testBuildResumeRequestSetsPluginDirectoriesAndLargeOutput() { + void testBuildResumeRequestSetsPluginDirectoriesAndLargeOutput() throws Exception { var largeOutput = new LargeToolOutputConfig().setEnabled(false).setMaxSizeBytes(2048L) .setOutputDirectory("/tmp/resume"); - var config = new ResumeSessionConfig().setPluginDirectories(List.of("/plugins/r")).setLargeOutput(largeOutput); + var config = new ResumeSessionConfig().setPluginDirectories(List.of("/plugins/r")) + .setDisabledMcpServers(List.of("local-files-r")).setLargeOutput(largeOutput); ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-16", config); assertEquals(List.of("/plugins/r"), request.getPluginDirectories()); + assertEquals(List.of("local-files-r"), request.getDisabledMcpServers()); assertEquals(largeOutput, request.getLargeOutput()); + assertTrue(JsonRpcClient.getObjectMapper().writeValueAsString(request) + .contains("\"disabledMcpServers\":[\"local-files-r\"]")); } @Test diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 78290bf689..4f734da4f3 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1592,6 +1592,7 @@ export class CopilotClient { pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, + disabledMcpServers: config.disabledMcpServers, infiniteSessions: config.infiniteSessions, memory: config.memory, gitHubToken: config.gitHubToken, @@ -1833,6 +1834,7 @@ export class CopilotClient { pluginDirectories: config.pluginDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, + disabledMcpServers: config.disabledMcpServers, infiniteSessions: config.infiniteSessions, memory: config.memory, disableResume: config.suppressResumeEvent, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index acba826673..5b4eb63f30 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2490,6 +2490,13 @@ export interface SessionConfigBase { */ disabledSkills?: string[]; + /** + * Exact MCP server names to disable for this session. Disabled servers are not + * started or authenticated when creating or cold-resuming a session. Supplying + * this on a resident resume cannot stop servers that are already running. + */ + disabledMcpServers?: string[]; + /** * Infinite session configuration for persistent workspaces and automatic compaction. * When enabled (default), sessions automatically manage context limits and persist state. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 124261527a..99df3cd615 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -897,6 +897,7 @@ describe("CopilotClient", () => { }); const pluginDirs = ["/tmp/plugins/a", "/tmp/plugins/b"]; + const disabledMcpServers = ["local-files", "remote-github"]; const largeOutput = { enabled: true, maxSizeBytes: 1024, @@ -911,11 +912,13 @@ describe("CopilotClient", () => { const session = await client.createSession({ onPermissionRequest: approveAll, pluginDirectories: pluginDirs, + disabledMcpServers, largeOutput, }); await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll, pluginDirectories: pluginDirs, + disabledMcpServers, largeOutput, }); @@ -926,8 +929,10 @@ describe("CopilotClient", () => { ([method]) => method === "session.resume" )![1] as any; expect(createPayload.pluginDirectories).toEqual(pluginDirs); + expect(createPayload.disabledMcpServers).toEqual(disabledMcpServers); expect(createPayload.largeOutput).toEqual(expectedWireLargeOutput); expect(resumePayload.pluginDirectories).toEqual(pluginDirs); + expect(resumePayload.disabledMcpServers).toEqual(disabledMcpServers); expect(resumePayload.largeOutput).toEqual(expectedWireLargeOutput); }); diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts new file mode 100644 index 0000000000..ce1a504e8f --- /dev/null +++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts @@ -0,0 +1,485 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { + approveAll, + CopilotRequestHandler, + RuntimeConnection, + type CopilotSession, +} from "../../src/index.js"; +import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url))); +const TEST_MCP_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-server.mjs"); +const SYNTHETIC_RESPONSE = "PERSISTED_SESSION_READY"; +const MCP_TRIGGER_PROMPT = "Reply with the configured MCP test completion marker."; + +class PersistingRequestHandler extends CopilotRequestHandler { + protected override async sendRequest(request: Request): Promise { + const body = request.body ? await request.text() : ""; + const wantsStream = /"stream"\s*:\s*true/.test(body); + const url = request.url.toLowerCase(); + + if (url.endsWith("/models")) { + return new Response(MODEL_CATALOG_JSON, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + + if (url.includes("/responses")) { + return new Response(wantsStream ? RESPONSE_STREAM : RESPONSE_JSON, { + status: 200, + headers: { + "content-type": wantsStream ? "text/event-stream" : "application/json", + }, + }); + } + + if (url.includes("/chat/completions")) { + return new Response( + wantsStream ? CHAT_COMPLETION_STREAM : CHAT_COMPLETION_RESPONSE_JSON, + { + status: 200, + headers: { + "content-type": wantsStream ? "text/event-stream" : "application/json", + }, + } + ); + } + + return new Response("{}", { + status: 200, + headers: { "content-type": "application/json" }, + }); + } +} + +const RESPONSE_STREAM = [ + { + event: "response.created", + data: { + type: "response.created", + response: { + id: "persisted-session", + object: "response", + status: "in_progress", + output: [], + }, + }, + }, + { + event: "response.output_item.added", + data: { + type: "response.output_item.added", + output_index: 0, + item: { id: "message-1", type: "message", role: "assistant", content: [] }, + }, + }, + { + event: "response.content_part.added", + data: { + type: "response.content_part.added", + output_index: 0, + content_index: 0, + part: { type: "output_text", text: "" }, + }, + }, + { + event: "response.output_text.delta", + data: { + type: "response.output_text.delta", + output_index: 0, + content_index: 0, + delta: SYNTHETIC_RESPONSE, + }, + }, + { + event: "response.output_text.done", + data: { + type: "response.output_text.done", + output_index: 0, + content_index: 0, + text: SYNTHETIC_RESPONSE, + }, + }, + { + event: "response.completed", + data: { + type: "response.completed", + response: { + id: "persisted-session", + object: "response", + status: "completed", + output: [ + { + id: "message-1", + type: "message", + role: "assistant", + content: [{ type: "output_text", text: SYNTHETIC_RESPONSE }], + }, + ], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }, + }, + }, +] + .map(({ event, data }) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""); + +const RESPONSE_JSON = JSON.stringify({ + id: "persisted-session", + object: "response", + status: "completed", + output: [ + { + id: "message-1", + type: "message", + role: "assistant", + content: [{ type: "output_text", text: SYNTHETIC_RESPONSE }], + }, + ], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, +}); + +const CHAT_COMPLETION_STREAM = [ + { + id: "persisted-session", + object: "chat.completion.chunk", + created: 1, + model: "claude-sonnet-4.5", + choices: [ + { + index: 0, + delta: { role: "assistant", content: SYNTHETIC_RESPONSE }, + finish_reason: null, + }, + ], + }, + { + id: "persisted-session", + object: "chat.completion.chunk", + created: 1, + model: "claude-sonnet-4.5", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + }, +] + .map((data) => `data: ${JSON.stringify(data)}\n\n`) + .concat("data: [DONE]\n\n") + .join(""); + +const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({ + id: "persisted-session", + object: "chat.completion", + created: 1, + model: "claude-sonnet-4.5", + choices: [ + { + index: 0, + message: { role: "assistant", content: SYNTHETIC_RESPONSE }, + finish_reason: "stop", + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, +}); + +const MODEL_CATALOG_JSON = JSON.stringify({ + data: [ + { + id: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + object: "model", + vendor: "Anthropic", + version: "1", + preview: false, + model_picker_enabled: true, + capabilities: { + type: "chat", + family: "claude-sonnet-4.5", + tokenizer: "o200k_base", + limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, + supports: { streaming: true, tool_calls: true, parallel_tool_calls: true }, + }, + }, + ], +}); + +describe("disabled MCP servers", async () => { + const { + copilotClient: client, + createClient, + openAiEndpoint, + workDir, + } = await createSdkTestContext({ + copilotClientOptions: { + requestHandler: new PersistingRequestHandler(), + }, + }); + + function createPluginDirectory(prefix: string): { + pluginDirectory: string; + controlMarker: string; + disabledMarker: string; + } { + const pluginDirectory = join(workDir, `${prefix}-${randomUUID()}`); + mkdirSync(pluginDirectory, { recursive: true }); + const controlMarker = join(pluginDirectory, "control-started.log"); + const disabledMarker = join(pluginDirectory, "disabled-started.log"); + + writeFileSync( + join(pluginDirectory, "plugin.json"), + JSON.stringify({ + name: `${prefix}-${randomUUID()}`, + version: "1.0.0", + }) + ); + writeFileSync( + join(pluginDirectory, ".mcp.json"), + JSON.stringify({ + mcpServers: { + control: { + type: "stdio", + command: process.execPath, + args: [ + TEST_MCP_SERVER, + "--startup-marker", + controlMarker, + "--server-name", + "control", + ], + }, + disabled: { + type: "stdio", + command: process.execPath, + args: [ + TEST_MCP_SERVER, + "--startup-marker", + disabledMarker, + "--server-name", + "disabled", + ], + }, + }, + }) + ); + + return { pluginDirectory, controlMarker, disabledMarker }; + } + + function markerCount(markerPath: string): number { + if (!existsSync(markerPath)) { + return 0; + } + return readFileSync(markerPath, "utf8").trim().split("\n").filter(Boolean).length; + } + + async function waitForMarkerCount(markerPath: string, expectedCount: number): Promise { + await waitForCondition(() => markerCount(markerPath) >= expectedCount, { + timeoutMs: 60_000, + intervalMs: 100, + timeoutMessage: `Timed out waiting for ${markerPath} to be written ${expectedCount} time(s).`, + }); + } + + async function waitForMcpStatus( + session: CopilotSession, + serverName: string, + expectedStatus: string + ): Promise { + let lastStatus = ""; + await waitForCondition( + async () => { + const result = await session.rpc.mcp.list(); + const server = result.servers.find((candidate) => candidate.name === serverName); + lastStatus = server?.status ?? ""; + return lastStatus === expectedStatus; + }, + { + timeoutMs: 60_000, + intervalMs: 100, + timeoutMessage: `${serverName} did not reach ${expectedStatus}; last status was ${lastStatus}.`, + } + ); + } + + function expectSyntheticResponse(response: Awaited>) { + expect(response?.data.content).toBe(SYNTHETIC_RESPONSE); + } + + async function drainPostCreateRpc(session: CopilotSession): Promise { + // Drain a non-MCP post-create RPC without initializing MCP before the first model turn. + await session.rpc.metadata.snapshot(); + } + + async function mcpRequestCount(): Promise { + const requests = await openAiEndpoint.getRequests(); + return requests.filter((request) => request.method === "POST" && request.url === "/mcp") + .length; + } + + async function waitForMcpRequestCount(expectedCount: number): Promise { + let lastCount = 0; + await waitForCondition( + async () => { + lastCount = await mcpRequestCount(); + return lastCount >= expectedCount; + }, + { + timeoutMs: 60_000, + intervalMs: 100, + timeoutMessage: `Timed out waiting for ${expectedCount} /mcp request(s); saw ${lastCount}.`, + } + ); + } + + it( + "keeps disabled plugin MCP servers per-session on create", + { timeout: 120_000 }, + async () => { + const { + pluginDirectory: disabledPluginDirectory, + controlMarker: disabledControlMarker, + disabledMarker, + } = createPluginDirectory("disabled-mcp-create"); + + await using disabledSession = await client.createSession({ + onPermissionRequest: approveAll, + pluginDirectories: [disabledPluginDirectory], + disabledMcpServers: ["disabled"], + }); + + await drainPostCreateRpc(disabledSession); + expect(existsSync(disabledControlMarker)).toBe(false); + expect(existsSync(disabledMarker)).toBe(false); + expectSyntheticResponse( + await disabledSession.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }) + ); + await waitForMarkerCount(disabledControlMarker, 1); + expect(existsSync(disabledMarker)).toBe(false); + await waitForMcpStatus(disabledSession, "control", "connected"); + await waitForMcpStatus(disabledSession, "disabled", "disabled"); + + const { + pluginDirectory: enabledPluginDirectory, + controlMarker: enabledControlMarker, + disabledMarker: enabledDisabledMarker, + } = createPluginDirectory("enabled-mcp-create"); + await using enabledSession = await client.createSession({ + onPermissionRequest: approveAll, + pluginDirectories: [enabledPluginDirectory], + }); + await drainPostCreateRpc(enabledSession); + expect(existsSync(enabledControlMarker)).toBe(false); + expect(existsSync(enabledDisabledMarker)).toBe(false); + expectSyntheticResponse( + await enabledSession.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }) + ); + await waitForMarkerCount(enabledControlMarker, 1); + await waitForMarkerCount(enabledDisabledMarker, 1); + await waitForMcpStatus(enabledSession, "control", "connected"); + await waitForMcpStatus(enabledSession, "disabled", "connected"); + } + ); + + it( + "keeps the built-in GitHub MCP server disabled on the first message", + { timeout: 120_000 }, + async () => { + const disabledSession = await client.createSession({ + onPermissionRequest: approveAll, + enableConfigDiscovery: true, + enableMcpApps: true, + githubMcpToolConfig: { enableAllTools: true }, + disabledMcpServers: ["github-mcp-server"], + }); + + let disabledRequestsBeforeFirstMessage: number; + try { + await drainPostCreateRpc(disabledSession); + disabledRequestsBeforeFirstMessage = await mcpRequestCount(); + expect(disabledRequestsBeforeFirstMessage).toBe(0); + expectSyntheticResponse( + await disabledSession.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }) + ); + expect(await mcpRequestCount()).toBe(disabledRequestsBeforeFirstMessage); + await waitForMcpStatus(disabledSession, "github-mcp-server", "disabled"); + expect(await mcpRequestCount()).toBe(disabledRequestsBeforeFirstMessage); + } finally { + await disabledSession.disconnect(); + } + + expect(await mcpRequestCount()).toBe(disabledRequestsBeforeFirstMessage); + + await using enabledSession = await client.createSession({ + onPermissionRequest: approveAll, + enableConfigDiscovery: true, + enableMcpApps: true, + githubMcpToolConfig: { enableAllTools: true }, + }); + await drainPostCreateRpc(enabledSession); + const requestsBeforeFirstMessage = await mcpRequestCount(); + expect(requestsBeforeFirstMessage).toBe(0); + expectSyntheticResponse( + await enabledSession.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }) + ); + await waitForMcpRequestCount(requestsBeforeFirstMessage + 1); + await waitForMcpStatus(enabledSession, "github-mcp-server", "connected"); + } + ); + + it.skipIf(isInProcessTransport)( + "applies disabled plugin MCP servers on cold stdio resume", + async () => { + const { pluginDirectory, controlMarker, disabledMarker } = + createPluginDirectory("disabled-mcp-resume"); + const initialClient = createClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + requestHandler: new PersistingRequestHandler(), + }); + const resumeClient = createClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + + try { + const originalSession = await initialClient.createSession({ + onPermissionRequest: approveAll, + enableSessionStore: true, + }); + const sessionId = originalSession.sessionId; + // A session.log entry alone does not materialize a session that a + // restarted runtime can resume. This self-contained model turn + // persists it without initializing MCP because no plugin directory + // is supplied until the resume request below. + const response = await originalSession.sendAndWait({ + prompt: "Return the configured persistence marker.", + }); + expectSyntheticResponse(response); + + expect(existsSync(controlMarker)).toBe(false); + expect(existsSync(disabledMarker)).toBe(false); + await initialClient.stop(); + + await using resumedSession = await resumeClient.resumeSession(sessionId, { + onPermissionRequest: approveAll, + enableSessionStore: true, + pluginDirectories: [pluginDirectory], + disabledMcpServers: ["disabled"], + }); + await waitForMcpStatus(resumedSession, "control", "connected"); + await waitForMcpStatus(resumedSession, "disabled", "disabled"); + await waitForMarkerCount(controlMarker, 1); + expect(existsSync(disabledMarker)).toBe(false); + } finally { + await initialClient.stop().catch(() => {}); + await resumeClient.stop().catch(() => {}); + } + } + ); +}); diff --git a/python/copilot/client.py b/python/copilot/client.py index f7f0a4eb26..8bfd99d940 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2062,6 +2062,7 @@ async def create_session( plugin_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, + disabled_mcp_servers: list[str] | None = None, infinite_sessions: InfiniteSessionConfig | None = None, large_output: LargeToolOutputConfig | None = None, memory: MemoryConfiguration | None = None, @@ -2187,6 +2188,10 @@ async def create_session( instruction_directories: Additional directories to search for custom instruction files. disabled_skills: Skills to disable. + disabled_mcp_servers: Exact MCP server names to disable only for this + session. Disabled servers are not started or authenticated on + create or cold resume; a resident resume cannot stop servers + already running. This does not change global MCP settings. infinite_sessions: Infinite session configuration. memory: Session memory configuration. cloud: Creates a remote session in the cloud instead of a local @@ -2485,6 +2490,8 @@ async def create_session( # Add disabled skills configuration if provided if disabled_skills: payload["disabledSkills"] = disabled_skills + if disabled_mcp_servers is not None: + payload["disabledMcpServers"] = disabled_mcp_servers # Add infinite sessions configuration if provided if infinite_sessions: @@ -2751,6 +2758,7 @@ async def resume_session( plugin_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, + disabled_mcp_servers: list[str] | None = None, infinite_sessions: InfiniteSessionConfig | None = None, large_output: LargeToolOutputConfig | None = None, memory: MemoryConfiguration | None = None, @@ -2877,6 +2885,10 @@ async def resume_session( instruction_directories: Additional directories to search for custom instruction files. disabled_skills: Skills to disable. + disabled_mcp_servers: Exact MCP server names to disable only for this + session. Disabled servers are not started or authenticated on + create or cold resume; a resident resume cannot stop servers + already running. This does not change global MCP settings. infinite_sessions: Infinite session configuration. memory: Session memory configuration. on_event: Callback for session events. @@ -3144,6 +3156,8 @@ async def resume_session( payload["instructionDirectories"] = instruction_directories if disabled_skills: payload["disabledSkills"] = disabled_skills + if disabled_mcp_servers is not None: + payload["disabledMcpServers"] = disabled_mcp_servers if infinite_sessions: wire_config: dict[str, Any] = {} diff --git a/python/test_client.py b/python/test_client.py index ba353bde38..b9465376d7 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -794,6 +794,7 @@ async def mock_request(method, params, **kwargs): client._client.request = mock_request plugin_dirs = ["/tmp/plugins/a", "/tmp/plugins/b"] + disabled_mcp_servers = ["local-files", "remote-github"] large_output = { "enabled": True, "max_size_bytes": 1024, @@ -808,19 +809,45 @@ async def mock_request(method, params, **kwargs): session = await client.create_session( on_permission_request=PermissionHandler.approve_all, plugin_directories=plugin_dirs, + disabled_mcp_servers=disabled_mcp_servers, large_output=large_output, ) await client.resume_session( session.session_id, on_permission_request=PermissionHandler.approve_all, plugin_directories=plugin_dirs, + disabled_mcp_servers=disabled_mcp_servers, large_output=large_output, ) assert captured["session.create"]["pluginDirectories"] == plugin_dirs + assert captured["session.create"]["disabledMcpServers"] == disabled_mcp_servers assert captured["session.create"]["largeOutput"] == expected_large_output_wire assert captured["session.resume"]["pluginDirectories"] == plugin_dirs + assert captured["session.resume"]["disabledMcpServers"] == disabled_mcp_servers assert captured["session.resume"]["largeOutput"] == expected_large_output_wire + + empty_session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + disabled_mcp_servers=[], + ) + await client.resume_session( + empty_session.session_id, + on_permission_request=PermissionHandler.approve_all, + disabled_mcp_servers=[], + ) + assert captured["session.create"]["disabledMcpServers"] == [] + assert captured["session.resume"]["disabledMcpServers"] == [] + + omitted_session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + ) + await client.resume_session( + omitted_session.session_id, + on_permission_request=PermissionHandler.approve_all, + ) + assert "disabledMcpServers" not in captured["session.create"] + assert "disabledMcpServers" not in captured["session.resume"] finally: await client.force_stop() diff --git a/rust/src/types.rs b/rust/src/types.rs index b5c180e4ab..0a48514acf 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1928,6 +1928,10 @@ pub struct SessionConfig { /// Skill names to disable. Skills in this set will not be available /// even if found in skill directories. pub disabled_skills: Option>, + /// Exact MCP server names to disable for this session. Disabled servers are + /// not started or authenticated on create or cold resume; a resident resume + /// cannot stop servers that are already running. + pub disabled_mcp_servers: Option>, /// Enable session hooks. When `true`, the CLI sends `hooks.invoke` /// RPC requests at key lifecycle points (pre/post tool use, prompt /// submission, session start/end, errors). @@ -2139,6 +2143,7 @@ impl std::fmt::Debug for SessionConfig { .field("large_output", &self.large_output) .field("tool_search", &self.tool_search) .field("disabled_skills", &self.disabled_skills) + .field("disabled_mcp_servers", &self.disabled_mcp_servers) .field("hooks", &self.hooks) .field("custom_agents", &self.custom_agents) .field("default_agent", &self.default_agent) @@ -2252,6 +2257,7 @@ impl Default for SessionConfig { large_output: None, tool_search: None, disabled_skills: None, + disabled_mcp_servers: None, hooks: None, custom_agents: None, default_agent: None, @@ -2412,6 +2418,7 @@ impl SessionConfig { large_output: self.large_output, tool_search: self.tool_search, disabled_skills: self.disabled_skills, + disabled_mcp_servers: self.disabled_mcp_servers, custom_agents: self.custom_agents, custom_agents_local_only: self.custom_agents_local_only, default_agent: self.default_agent, @@ -2847,6 +2854,16 @@ impl SessionConfig { self } + /// Set exact MCP server names to disable for this session. + pub fn with_disabled_mcp_servers(mut self, names: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect()); + self + } + /// Set the custom agents (sub-agents) configured for this session. pub fn with_custom_agents>( mut self, @@ -3145,6 +3162,9 @@ pub struct ResumeSessionConfig { pub tool_search: Option, /// Skill names to disable on resume. pub disabled_skills: Option>, + /// Exact MCP server names to disable on resume. This prevents startup and + /// authentication during a cold resume, but cannot stop resident servers. + pub disabled_mcp_servers: Option>, /// Enable session hooks on resume. pub hooks: Option, /// Custom agents to re-supply on resume. @@ -3324,6 +3344,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("large_output", &self.large_output) .field("tool_search", &self.tool_search) .field("disabled_skills", &self.disabled_skills) + .field("disabled_mcp_servers", &self.disabled_mcp_servers) .field("hooks", &self.hooks) .field("custom_agents", &self.custom_agents) .field("default_agent", &self.default_agent) @@ -3481,6 +3502,7 @@ impl ResumeSessionConfig { large_output: self.large_output, tool_search: self.tool_search, disabled_skills: self.disabled_skills, + disabled_mcp_servers: self.disabled_mcp_servers, custom_agents: self.custom_agents, custom_agents_local_only: self.custom_agents_local_only, default_agent: self.default_agent, @@ -3573,6 +3595,7 @@ impl ResumeSessionConfig { large_output: None, tool_search: None, disabled_skills: None, + disabled_mcp_servers: None, hooks: None, custom_agents: None, default_agent: None, @@ -3987,6 +4010,16 @@ impl ResumeSessionConfig { self } + /// Set exact MCP server names to disable for this session. + pub fn with_disabled_mcp_servers(mut self, names: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect()); + self + } + /// Re-supply custom agents on resume. pub fn with_custom_agents>( mut self, @@ -6281,6 +6314,10 @@ mod tests { let cfg = SessionConfig { plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]), + disabled_mcp_servers: Some(vec![ + "local-files".to_string(), + "remote-github".to_string(), + ]), large_output: Some( LargeToolOutputConfig::new() .with_enabled(true) @@ -6295,6 +6332,10 @@ mod tests { .expect("no duplicate handlers"); let wire_json = serde_json::to_value(&wire).unwrap(); assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files", "remote-github"]) + ); assert_eq!(wire_json["largeOutput"]["enabled"], true); assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024); assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output"); @@ -6304,6 +6345,7 @@ mod tests { .expect("default has no duplicate handlers"); let empty_json = serde_json::to_value(&empty_wire).unwrap(); assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); assert!(empty_json.get("largeOutput").is_none()); } @@ -6353,6 +6395,7 @@ mod tests { let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1")); cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]); + cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]); cfg.large_output = Some( LargeToolOutputConfig::new() .with_enabled(false) @@ -6363,6 +6406,10 @@ mod tests { let (wire, _) = cfg.into_wire().expect("no duplicate handlers"); let wire_json = serde_json::to_value(&wire).unwrap(); assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r"); + assert_eq!( + wire_json["disabledMcpServers"], + serde_json::json!(["local-files-r"]) + ); assert_eq!(wire_json["largeOutput"]["enabled"], false); assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048); assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r"); @@ -6372,9 +6419,38 @@ mod tests { .expect("default resume has no duplicate handlers"); let empty_json = serde_json::to_value(&empty_wire).unwrap(); assert!(empty_json.get("pluginDirectories").is_none()); + assert!(empty_json.get("disabledMcpServers").is_none()); assert!(empty_json.get("largeOutput").is_none()); } + #[test] + fn session_config_clones_disabled_mcp_servers() { + let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); + let mut create_clone = create.clone(); + create_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + create.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + + let resume = ResumeSessionConfig::new(SessionId::from("sess-1")) + .with_disabled_mcp_servers(["local-files"]); + let mut resume_clone = resume.clone(); + resume_clone + .disabled_mcp_servers + .as_mut() + .expect("configured disabled MCP servers") + .push("remote-github".to_string()); + assert_eq!( + resume.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); + } + #[test] fn session_config_builder_composes() { use indexmap::IndexMap; @@ -6396,6 +6472,7 @@ mod tests { .with_enable_on_demand_instruction_discovery(true) .with_skill_directories([PathBuf::from("/tmp/skills")]) .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) .with_agent("researcher") .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) @@ -6433,6 +6510,10 @@ mod tests { cfg.disabled_skills.as_deref(), Some(&["broken-skill".to_string()][..]) ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); assert_eq!(cfg.agent.as_deref(), Some("researcher")); assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); @@ -6467,6 +6548,7 @@ mod tests { .with_enable_on_demand_instruction_discovery(false) .with_skill_directories([PathBuf::from("/tmp/skills")]) .with_disabled_skills(["broken-skill"]) + .with_disabled_mcp_servers(["local-files"]) .with_agent("researcher") .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) @@ -6504,6 +6586,10 @@ mod tests { cfg.disabled_skills.as_deref(), Some(&["broken-skill".to_string()][..]) ); + assert_eq!( + cfg.disabled_mcp_servers.as_deref(), + Some(&["local-files".to_string()][..]) + ); assert_eq!(cfg.agent.as_deref(), Some("researcher")); assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 4dc7569093..ad520c9f55 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -130,6 +130,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_mcp_servers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents_local_only: Option, @@ -270,6 +272,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_mcp_servers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub custom_agents_local_only: Option, diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 47ebda9f7b..4c1be59f26 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -416,6 +416,51 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { return; } + // Keep GitHub MCP tests hermetic while still capturing the request at + // the CAPI proxy. The tests only need a successful transport handshake; + // no fake tools are exposed. + if (options.requestOptions.path === "/mcp") { + if (options.requestOptions.method !== "POST") { + options.onResponseStart(200, commonResponseHeaders); + options.onResponseEnd(); + return; + } + + const request = JSON.parse(options.body ?? "{}") as { + id?: string | number; + method?: string; + params?: { protocolVersion?: string }; + }; + if (request.id === undefined) { + options.onResponseStart(202, commonResponseHeaders); + options.onResponseEnd(); + return; + } + + const result = + request.method === "initialize" + ? { + protocolVersion: + request.params?.protocolVersion ?? "2025-03-26", + capabilities: { tools: {} }, + serverInfo: { name: "e2e-github-mcp", version: "1.0.0" }, + } + : request.method === "tools/list" + ? { tools: [] } + : {}; + options.onResponseStart(200, { + "content-type": "application/json", + ...commonResponseHeaders, + }); + options.onData( + Buffer.from( + JSON.stringify({ jsonrpc: "2.0", id: request.id, result }), + ), + ); + options.onResponseEnd(); + return; + } + // Handle memory endpoints - return stub responses in tests // Matches: /agents/*/memory/*/enabled, /agents/*/memory/*/recent, etc. if (options.requestOptions.path?.match(/\/agents\/.*\/memory\//)) { diff --git a/test/harness/test-mcp-server.mjs b/test/harness/test-mcp-server.mjs index b2b32606dc..4a5c61d4c8 100644 --- a/test/harness/test-mcp-server.mjs +++ b/test/harness/test-mcp-server.mjs @@ -13,9 +13,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { appendFile } from "node:fs/promises"; import { z } from "zod"; -const server = new McpServer({ name: "env-echo", version: "1.0.0" }); +function getArgument(name) { + const index = process.argv.indexOf(name); + return index === -1 ? undefined : process.argv[index + 1]; +} + +const startupMarkerPath = getArgument("--startup-marker"); +const serverName = getArgument("--server-name") ?? "env-echo"; +const server = new McpServer({ name: serverName, version: "1.0.0" }); server.tool( "get_env", @@ -27,5 +35,7 @@ server.tool( ); const transport = new StdioServerTransport(); +if (startupMarkerPath) { + await appendFile(startupMarkerPath, `${serverName}\n`); +} await server.connect(transport); - From f80e89c2ca7e9d43eac749bca73bb37d63684249 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 4 Aug 2026 15:15:16 -0700 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/harness/test-mcp-server.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/harness/test-mcp-server.mjs b/test/harness/test-mcp-server.mjs index 4a5c61d4c8..a3a84b42b3 100644 --- a/test/harness/test-mcp-server.mjs +++ b/test/harness/test-mcp-server.mjs @@ -36,6 +36,6 @@ server.tool( const transport = new StdioServerTransport(); if (startupMarkerPath) { - await appendFile(startupMarkerPath, `${serverName}\n`); + await appendFile(startupMarkerPath, `${serverName}\n`); } await server.connect(transport); From d53baee087df96c8f660ae7fad8034c2ab7f9734 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 4 Aug 2026 15:51:40 -0700 Subject: [PATCH 3/3] java: Apply Spotless formatting Format the disabled MCP server accessors and Javadocs exactly as required by the Java CI Spotless check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/com/github/copilot/rpc/CreateSessionRequest.java | 4 +++- .../main/java/com/github/copilot/rpc/ResumeSessionConfig.java | 3 ++- .../java/com/github/copilot/rpc/ResumeSessionRequest.java | 4 +++- java/src/main/java/com/github/copilot/rpc/SessionConfig.java | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 2ffe35e647..fcd76b9041 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -683,7 +683,9 @@ public List getDisabledMcpServers() { return disabledMcpServers == null ? null : Collections.unmodifiableList(disabledMcpServers); } - /** Sets disabled MCP server names. @param disabledMcpServers the server names */ + /** + * Sets disabled MCP server names. @param disabledMcpServers the server names + */ public void setDisabledMcpServers(List disabledMcpServers) { this.disabledMcpServers = disabledMcpServers; } diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 7cac5e74b9..f6b175473e 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -1522,7 +1522,8 @@ public List getDisabledMcpServers() { * not started or authenticated on create or cold resume; a resident resume * cannot stop servers already running. * - * @param disabledMcpServers the server names to disable + * @param disabledMcpServers + * the server names to disable * @return this config for method chaining */ public ResumeSessionConfig setDisabledMcpServers(List disabledMcpServers) { diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 4ff7a0dbaa..bbd4868956 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -899,7 +899,9 @@ public List getDisabledMcpServers() { return disabledMcpServers == null ? null : Collections.unmodifiableList(disabledMcpServers); } - /** Sets disabled MCP server names. @param disabledMcpServers the server names */ + /** + * Sets disabled MCP server names. @param disabledMcpServers the server names + */ public void setDisabledMcpServers(List disabledMcpServers) { this.disabledMcpServers = disabledMcpServers; } diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index e0aa1bd7e9..cb88f31d89 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -1213,7 +1213,8 @@ public List getDisabledMcpServers() { * not started or authenticated on create or cold resume; a resident resume * cannot stop servers already running. * - * @param disabledMcpServers the server names to disable + * @param disabledMcpServers + * the server names to disable * @return this config for method chaining */ public SessionConfig setDisabledMcpServers(List disabledMcpServers) {