diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index dff4681a80..527261e790 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1250,6 +1250,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, + config.AllowAllMcpServerInstructions, config.McpOAuthTokenStorage, config.AuthClientIdMetadataUrl, "direct", @@ -1503,6 +1504,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, config.McpServers, + config.AllowAllMcpServerInstructions, config.McpOAuthTokenStorage, config.AuthClientIdMetadataUrl, "direct", @@ -3034,6 +3036,7 @@ internal record CreateSessionRequest( bool? Streaming, bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, + bool? AllowAllMcpServerInstructions, McpOAuthTokenStorageMode? McpOAuthTokenStorage, string? AuthClientIdMetadataUrl, string? EnvValueMode, @@ -3164,6 +3167,7 @@ internal record ResumeSessionRequest( bool? Streaming, bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, + bool? AllowAllMcpServerInstructions, McpOAuthTokenStorageMode? McpOAuthTokenStorage, string? AuthClientIdMetadataUrl, string? EnvValueMode, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 1cc4919093..7490f00044 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -3288,6 +3288,7 @@ protected SessionConfigBase(SessionConfigBase? other) DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; IncludedBuiltinSkills = other.IncludedBuiltinSkills is not null ? [.. other.IncludedBuiltinSkills] : null; DisabledMcpServers = other.DisabledMcpServers is not null ? [.. other.DisabledMcpServers] : null; + AllowAllMcpServerInstructions = other.AllowAllMcpServerInstructions; EnableCitations = other.EnableCitations; EnableFileChangeTracking = other.EnableFileChangeTracking; EnableConfigDiscovery = other.EnableConfigDiscovery; @@ -3716,6 +3717,13 @@ protected SessionConfigBase(SessionConfigBase? other) /// public IDictionary? McpServers { get; set; } + /// + /// Whether instructions from every configured MCP server are included in the + /// system prompt. Enabling this broadens the default trust boundary; only use + /// it with trusted servers. When null, the runtime default applies. + /// + public bool? AllowAllMcpServerInstructions { get; set; } + /// /// Controls how MCP OAuth tokens are stored for this session. /// Default: for safe multitenant behavior. diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index 173569788c..cbe0e301a9 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -512,6 +512,54 @@ public async Task SessionRequests_Serialize_CapiAutoTier(AutoTier tier, string e } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SessionRequests_Forward_McpServerInstructionPolicy(bool value) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + AllowAllMcpServerInstructions = value, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-with-mcp-instruction-policy", new ResumeSessionConfig + { + AllowAllMcpServerInstructions = value, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + Assert.Equal(value, request.Params.GetProperty("allowAllMcpServerInstructions").GetBoolean()); + } + } + + [Fact] + public async Task SessionRequests_Omit_McpServerInstructionPolicy_WhenUnset() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-without-mcp-instruction-policy", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + Assert.False(request.Params.TryGetProperty("allowAllMcpServerInstructions", out _)); + } + } + [Theory] [InlineData("efficiency")] [InlineData("balance")] diff --git a/go/client.go b/go/client.go index e450c595f2..b9910a5717 100644 --- a/go/client.go +++ b/go/client.go @@ -902,6 +902,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.WorkingDirectory = config.WorkingDirectory req.AdditionalDirectories = config.AdditionalDirectories req.MCPServers = config.MCPServers + req.AllowAllMCPServerInstructions = config.AllowAllMCPServerInstructions req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL req.EnvValueMode = "direct" @@ -1322,6 +1323,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } req.ContinuePendingWork = config.ContinuePendingWork req.MCPServers = config.MCPServers + req.AllowAllMCPServerInstructions = config.AllowAllMCPServerInstructions req.MCPOAuthTokenStorage = config.MCPOAuthTokenStorage req.AuthClientIDMetadataURL = config.AuthClientIDMetadataURL req.EnvValueMode = "direct" diff --git a/go/client_test.go b/go/client_test.go index 52587c8462..dc64849a51 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1970,6 +1970,43 @@ func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) { }) } +func TestSessionRequests_AllowAllMCPServerInstructions(t *testing.T) { + for _, tc := range []struct { + name string + value *bool + want any + }{ + {name: "true", value: Bool(true), want: true}, + {name: "false", value: Bool(false), want: false}, + {name: "omitted", value: nil, want: nil}, + } { + t.Run(tc.name, func(t *testing.T) { + requests := []any{ + createSessionRequest{AllowAllMCPServerInstructions: tc.value}, + resumeSessionRequest{SessionID: "s1", AllowAllMCPServerInstructions: tc.value}, + } + for _, request := range requests { + data, err := json.Marshal(request) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var payload map[string]any + if err := json.Unmarshal(data, &payload); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + got, present := payload["allowAllMcpServerInstructions"] + if tc.value == nil { + if present { + t.Fatalf("Expected policy to be omitted, got %v", got) + } + } else if !present || got != tc.want { + t.Fatalf("Expected policy %v, got %v", tc.want, got) + } + } + }) + } +} + func TestSessionRequests_Memory(t *testing.T) { t.Run("create includes memory in JSON when enabled", func(t *testing.T) { req := createSessionRequest{Memory: &MemoryConfiguration{Enabled: true}} diff --git a/go/types.go b/go/types.go index 7bc5bfb9af..b0ad83ace8 100644 --- a/go/types.go +++ b/go/types.go @@ -1490,6 +1490,11 @@ type SessionConfig struct { ModelCapabilities *rpc.ModelCapabilitiesOverride // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig + // AllowAllMCPServerInstructions controls whether instructions from every + // configured MCP server are included in the system prompt. Enabling this + // broadens the default trust boundary; only use it with trusted servers. + // Nil leaves the runtime default unchanged. + AllowAllMCPServerInstructions *bool // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -2053,6 +2058,11 @@ type ResumeSessionConfig struct { IncludeSubAgentStreamingEvents *bool // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig + // AllowAllMCPServerInstructions controls whether instructions from every + // configured MCP server are included in the system prompt. Enabling this + // broadens the default trust boundary; only use it with trusted servers. + // Nil leaves the runtime default unchanged. + AllowAllMCPServerInstructions *bool // MCPOAuthTokenStorage controls how MCP OAuth tokens are stored for this session. // When empty, the runtime default ("in-memory") is used. MCPOAuthTokenStorage string @@ -2650,6 +2660,7 @@ type createSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` @@ -2762,6 +2773,7 @@ type resumeSessionRequest struct { IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` MCPOAuthTokenStorage string `json:"mcpOAuthTokenStorage,omitempty"` AuthClientIDMetadataURL string `json:"authClientIdMetadataUrl,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index b336e70472..2b620fc10f 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -148,6 +148,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); + request.setAllowAllMcpServerInstructions(config.getAllowAllMcpServerInstructions()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl()); request.setCustomAgents(config.getCustomAgents()); @@ -304,6 +305,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo } config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); request.setMcpServers(config.getMcpServers()); + request.setAllowAllMcpServerInstructions(config.getAllowAllMcpServerInstructions()); request.setMcpOAuthTokenStorage(config.getMcpOAuthTokenStorage()); request.setAuthClientIdMetadataUrl(config.getAuthClientIdMetadataUrl()); request.setCustomAgents(config.getCustomAgents()); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index b7fc219258..76240e8b34 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -116,6 +116,9 @@ public final class CreateSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; + @JsonProperty("allowAllMcpServerInstructions") + private Boolean allowAllMcpServerInstructions; + @JsonProperty("mcpOAuthTokenStorage") private String mcpOAuthTokenStorage; @@ -594,6 +597,16 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } + /** Gets the MCP server instruction policy. @return the policy value */ + public Boolean getAllowAllMcpServerInstructions() { + return allowAllMcpServerInstructions; + } + + /** Sets the MCP server instruction policy. @param value the policy value */ + public void setAllowAllMcpServerInstructions(Boolean value) { + this.allowAllMcpServerInstructions = value; + } + /** Gets MCP OAuth token storage mode. @return the storage mode */ public String getMcpOAuthTokenStorage() { return mcpOAuthTokenStorage; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index ec31562323..b024639ff1 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -85,6 +85,7 @@ public class ResumeSessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; + private Boolean allowAllMcpServerInstructions; private String mcpOAuthTokenStorage; private String authClientIdMetadataUrl; private List customAgents; @@ -1397,6 +1398,30 @@ public ResumeSessionConfig setMcpServers(Map mcpServers return this; } + /** + * Gets whether instructions from every configured MCP server are included in + * the system prompt. + * + * @return the policy value, or {@code null} when the runtime default applies + */ + public Boolean getAllowAllMcpServerInstructions() { + return allowAllMcpServerInstructions; + } + + /** + * Controls whether instructions from every configured MCP server are included + * in the system prompt. Enabling this broadens the default trust boundary; only + * use it with trusted servers. + * + * @param allowAllMcpServerInstructions + * the explicit policy value, or {@code null} for the runtime default + * @return this config instance for method chaining + */ + public ResumeSessionConfig setAllowAllMcpServerInstructions(Boolean allowAllMcpServerInstructions) { + this.allowAllMcpServerInstructions = allowAllMcpServerInstructions; + return this; + } + /** * Gets the MCP OAuth token storage mode. * @@ -2143,6 +2168,7 @@ public ResumeSessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.allowAllMcpServerInstructions = this.allowAllMcpServerInstructions; copy.mcpOAuthTokenStorage = this.mcpOAuthTokenStorage; copy.authClientIdMetadataUrl = this.authClientIdMetadataUrl; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 42d0ee536d..6fac9e0a32 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -159,6 +159,9 @@ public final class ResumeSessionRequest { @JsonProperty("mcpServers") private Map mcpServers; + @JsonProperty("allowAllMcpServerInstructions") + private Boolean allowAllMcpServerInstructions; + @JsonProperty("mcpOAuthTokenStorage") private String mcpOAuthTokenStorage; @@ -820,6 +823,16 @@ public void setMcpServers(Map mcpServers) { this.mcpServers = mcpServers; } + /** Gets the MCP server instruction policy. @return the policy value */ + public Boolean getAllowAllMcpServerInstructions() { + return allowAllMcpServerInstructions; + } + + /** Sets the MCP server instruction policy. @param value the policy value */ + public void setAllowAllMcpServerInstructions(Boolean value) { + this.allowAllMcpServerInstructions = value; + } + /** Gets MCP OAuth token storage mode. @return the storage mode */ public String getMcpOAuthTokenStorage() { return mcpOAuthTokenStorage; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java index d77642c103..b17b6d9a8b 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -74,6 +74,7 @@ public class SessionConfig { private boolean streaming; private Boolean includeSubAgentStreamingEvents; private Map mcpServers; + private Boolean allowAllMcpServerInstructions; private String mcpOAuthTokenStorage; private String authClientIdMetadataUrl; private List customAgents; @@ -1050,6 +1051,30 @@ public SessionConfig setMcpServers(Map mcpServers) { return this; } + /** + * Gets whether instructions from every configured MCP server are included in + * the system prompt. + * + * @return the policy value, or {@code null} when the runtime default applies + */ + public Boolean getAllowAllMcpServerInstructions() { + return allowAllMcpServerInstructions; + } + + /** + * Controls whether instructions from every configured MCP server are included + * in the system prompt. Enabling this broadens the default trust boundary; only + * use it with trusted servers. + * + * @param allowAllMcpServerInstructions + * the explicit policy value, or {@code null} for the runtime default + * @return this config instance for method chaining + */ + public SessionConfig setAllowAllMcpServerInstructions(Boolean allowAllMcpServerInstructions) { + this.allowAllMcpServerInstructions = allowAllMcpServerInstructions; + return this; + } + /** * Gets the MCP OAuth token storage mode. * @@ -2272,6 +2297,7 @@ public SessionConfig clone() { copy.streaming = this.streaming; copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.allowAllMcpServerInstructions = this.allowAllMcpServerInstructions; copy.mcpOAuthTokenStorage = this.mcpOAuthTokenStorage; copy.authClientIdMetadataUrl = this.authClientIdMetadataUrl; copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index edc40175d0..6d2fe820ce 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -241,6 +241,21 @@ void testBuildCreateRequestSetsPluginDirectoriesAndLargeOutput() throws Exceptio .contains("\"disabledMcpServers\":[\"local-files\",\"remote-github\"]")); } + @Test + void testBuildCreateRequestForwardsMcpServerInstructionPolicy() throws Exception { + for (boolean value : List.of(true, false)) { + var request = SessionRequestBuilder + .buildCreateRequest(new SessionConfig().setAllowAllMcpServerInstructions(value)); + assertEquals(value, request.getAllowAllMcpServerInstructions()); + assertTrue(JsonRpcClient.getObjectMapper().writeValueAsString(request) + .contains("\"allowAllMcpServerInstructions\":" + value)); + } + var omitted = SessionRequestBuilder.buildCreateRequest(new SessionConfig()); + assertNull(omitted.getAllowAllMcpServerInstructions()); + assertFalse(JsonRpcClient.getObjectMapper().writeValueAsString(omitted) + .contains("allowAllMcpServerInstructions")); + } + @Test void testBuildCreateRequestSetsMemory() { var memory = new MemoryConfiguration().setEnabled(true); @@ -538,6 +553,21 @@ void testBuildResumeRequestSetsPluginDirectoriesAndLargeOutput() throws Exceptio .contains("\"disabledMcpServers\":[\"local-files-r\"]")); } + @Test + void testBuildResumeRequestForwardsMcpServerInstructionPolicy() throws Exception { + for (boolean value : List.of(true, false)) { + var request = SessionRequestBuilder.buildResumeRequest("sid-policy", + new ResumeSessionConfig().setAllowAllMcpServerInstructions(value)); + assertEquals(value, request.getAllowAllMcpServerInstructions()); + assertTrue(JsonRpcClient.getObjectMapper().writeValueAsString(request) + .contains("\"allowAllMcpServerInstructions\":" + value)); + } + var omitted = SessionRequestBuilder.buildResumeRequest("sid-policy", new ResumeSessionConfig()); + assertNull(omitted.getAllowAllMcpServerInstructions()); + assertFalse(JsonRpcClient.getObjectMapper().writeValueAsString(omitted) + .contains("allowAllMcpServerInstructions")); + } + @Test void testBuildResumeRequestSetsMemory() { var memory = new MemoryConfiguration().setEnabled(false); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 6e4b4fb5b4..b28b061e76 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1705,6 +1705,9 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), + ...(config.allowAllMcpServerInstructions !== undefined + ? { allowAllMcpServerInstructions: config.allowAllMcpServerInstructions } + : {}), mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, authClientIdMetadataUrl: config.authClientIdMetadataUrl, envValueMode: "direct", @@ -1987,6 +1990,9 @@ export class CopilotClient { ? { enableGitHubTelemetryForwarding: true } : {}), mcpServers: toWireMcpServers(config.mcpServers), + ...(config.allowAllMcpServerInstructions !== undefined + ? { allowAllMcpServerInstructions: config.allowAllMcpServerInstructions } + : {}), mcpOAuthTokenStorage: config.mcpOAuthTokenStorage, authClientIdMetadataUrl: config.authClientIdMetadataUrl, envValueMode: "direct", diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index efff9b47df..d07e18af95 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2752,6 +2752,17 @@ export interface SessionConfigBase { */ mcpServers?: Record; + /** + * Include instructions from every MCP server in the system prompt instead + * of only allowlisted servers. + * + * Enabling this broadens the session's instruction trust boundary. Only + * enable it when every configured MCP server is trusted. + * + * @default false + */ + allowAllMcpServerInstructions?: boolean; + /** * Custom agent configurations for the session. */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 24adb5cb6e..8ad3f6c19a 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -4384,6 +4384,56 @@ describe("CopilotClient", () => { }); }); +describe("allowAllMcpServerInstructions serialization", () => { + async function startCaptureClient() { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + }); + const sendRequest = vi.fn(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + vi.spyOn(client as any, "connectToServer").mockImplementation(async () => { + (client as any).connection = { sendRequest, dispose: vi.fn() }; + }); + vi.spyOn(client as any, "verifyProtocolVersion").mockResolvedValue(undefined); + await client.start(); + onTestFinished(() => client.forceStop()); + return { client, sendRequest }; + } + + it.each([true, false])("forwards %s on create and resume", async (value) => { + const { client, sendRequest } = await startCaptureClient(); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + allowAllMcpServerInstructions: value, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + allowAllMcpServerInstructions: value, + }); + + const createCall = sendRequest.mock.calls.find(([method]) => method === "session.create"); + const resumeCall = sendRequest.mock.calls.find(([method]) => method === "session.resume"); + expect(createCall![1].allowAllMcpServerInstructions).toBe(value); + expect(resumeCall![1].allowAllMcpServerInstructions).toBe(value); + }); + + it("omits the option on create and resume by default", async () => { + const { client, sendRequest } = await startCaptureClient(); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll }); + + const createCall = sendRequest.mock.calls.find(([method]) => method === "session.create"); + const resumeCall = sendRequest.mock.calls.find(([method]) => method === "session.resume"); + expect(createCall![1]).not.toHaveProperty("allowAllMcpServerInstructions"); + expect(resumeCall![1]).not.toHaveProperty("allowAllMcpServerInstructions"); + }); +}); + describe("managedSettings serialization", () => { async function captureCreateParams(config: Record): Promise { const client = new CopilotClient(); diff --git a/python/copilot/client.py b/python/copilot/client.py index d8d2f7e3f4..4e16f9eb13 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2293,6 +2293,7 @@ async def create_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, + allow_all_mcp_server_instructions: bool | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, auth_client_id_metadata_url: str | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, @@ -2425,6 +2426,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. + allow_all_mcp_server_instructions: Whether to trust and include + instructions from every configured MCP server. This broadens + the default trust boundary and should only be enabled for + trusted servers. When omitted, the runtime default applies. 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). @@ -2735,6 +2740,8 @@ async def create_session( # Add MCP servers configuration if provided if mcp_servers: payload["mcpServers"] = _mcp_servers_to_wire(mcp_servers) + if allow_all_mcp_server_instructions is not None: + payload["allowAllMcpServerInstructions"] = allow_all_mcp_server_instructions # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: @@ -3078,6 +3085,7 @@ async def resume_session( streaming: bool | None = None, include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, + allow_all_mcp_server_instructions: bool | None = None, mcp_oauth_token_storage: Literal["persistent", "in-memory"] | None = None, auth_client_id_metadata_url: str | None = None, embedding_cache_storage: Literal["persistent", "in-memory"] | None = None, @@ -3213,6 +3221,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. + allow_all_mcp_server_instructions: Whether to trust and include + instructions from every configured MCP server. This broadens + the default trust boundary and should only be enabled for + trusted servers. When omitted, the runtime default applies. 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). @@ -3518,6 +3530,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_to_wire(mcp_servers) + if allow_all_mcp_server_instructions is not None: + payload["allowAllMcpServerInstructions"] = allow_all_mcp_server_instructions # Mode "empty" defaults MCP OAuth token storage to in-memory; caller wins. mcp_oauth_token_storage = _mcp_oauth_token_storage_default(mode, mcp_oauth_token_storage) if mcp_oauth_token_storage is not None: diff --git a/python/test_client.py b/python/test_client.py index 2e3868ef1c..9fd6382afd 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -1373,6 +1373,29 @@ async def mock_request(method, params, **kwargs): ) assert "disabledMcpServers" not in captured["session.create"] assert "disabledMcpServers" not in captured["session.resume"] + + for value in (True, False): + policy_session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + allow_all_mcp_server_instructions=value, + ) + await client.resume_session( + policy_session.session_id, + on_permission_request=PermissionHandler.approve_all, + allow_all_mcp_server_instructions=value, + ) + assert captured["session.create"]["allowAllMcpServerInstructions"] is value + assert captured["session.resume"]["allowAllMcpServerInstructions"] is value + + policy_omitted_session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + ) + await client.resume_session( + policy_omitted_session.session_id, + on_permission_request=PermissionHandler.approve_all, + ) + assert "allowAllMcpServerInstructions" not in captured["session.create"] + assert "allowAllMcpServerInstructions" not in captured["session.resume"] finally: await client.force_stop() diff --git a/rust/src/types.rs b/rust/src/types.rs index 332a48d18c..3b1d417a8f 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -2002,6 +2002,10 @@ pub struct SessionConfig { pub included_builtin_skills: Option>, /// MCP server configurations passed through to the CLI. pub mcp_servers: Option>, + /// Whether instructions from every configured MCP server are included in + /// the system prompt. Enabling this broadens the default trust boundary; + /// only use it with trusted servers. `None` leaves the runtime default. + pub allow_all_mcp_server_instructions: Option, /// Controls how MCP OAuth tokens are stored for this session. /// /// - `"persistent"` — tokens are stored in the OS keychain (shared across sessions). @@ -2325,6 +2329,10 @@ impl std::fmt::Debug for SessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) + .field( + "allow_all_mcp_server_instructions", + &self.allow_all_mcp_server_instructions, + ) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field( "auth_client_id_metadata_url", @@ -2469,6 +2477,7 @@ impl Default for SessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, + allow_all_mcp_server_instructions: None, mcp_oauth_token_storage: None, auth_client_id_metadata_url: None, enable_config_discovery: None, @@ -2639,6 +2648,7 @@ impl SessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, + allow_all_mcp_server_instructions: self.allow_all_mcp_server_instructions, mcp_oauth_token_storage: self.mcp_oauth_token_storage, auth_client_id_metadata_url: self.auth_client_id_metadata_url, embedding_cache_storage: self.embedding_cache_storage, @@ -2970,6 +2980,15 @@ impl SessionConfig { self } + /// Include instructions from every configured MCP server. + /// + /// Enabling this broadens the default trust boundary; only use it with + /// trusted servers. + pub fn with_allow_all_mcp_server_instructions(mut self, allow: bool) -> Self { + self.allow_all_mcp_server_instructions = Some(allow); + self + } + /// Set MCP OAuth token storage mode. /// /// - `"persistent"` — tokens stored in the OS keychain. @@ -3461,6 +3480,10 @@ pub struct ResumeSessionConfig { pub included_builtin_skills: Option>, /// Re-supply MCP servers so they remain available after app restart. pub mcp_servers: Option>, + /// Whether instructions from every configured MCP server are included in + /// the system prompt. Enabling this broadens the default trust boundary; + /// only use it with trusted servers. `None` leaves the runtime default. + pub allow_all_mcp_server_instructions: Option, /// Controls how MCP OAuth tokens are stored for this session. /// See [`SessionConfig::mcp_oauth_token_storage`] for details. pub mcp_oauth_token_storage: Option, @@ -3696,6 +3719,10 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("excluded_builtin_agents", &self.excluded_builtin_agents) .field("included_builtin_skills", &self.included_builtin_skills) .field("mcp_servers", &self.mcp_servers) + .field( + "allow_all_mcp_server_instructions", + &self.allow_all_mcp_server_instructions, + ) .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage) .field( "auth_client_id_metadata_url", @@ -3882,6 +3909,7 @@ impl ResumeSessionConfig { excluded_builtin_agents: self.excluded_builtin_agents, tool_filter_precedence: "excluded", mcp_servers: self.mcp_servers, + allow_all_mcp_server_instructions: self.allow_all_mcp_server_instructions, mcp_oauth_token_storage: self.mcp_oauth_token_storage, auth_client_id_metadata_url: self.auth_client_id_metadata_url, embedding_cache_storage: self.embedding_cache_storage, @@ -3992,6 +4020,7 @@ impl ResumeSessionConfig { excluded_builtin_agents: None, included_builtin_skills: None, mcp_servers: None, + allow_all_mcp_server_instructions: None, mcp_oauth_token_storage: None, auth_client_id_metadata_url: None, enable_config_discovery: None, @@ -4300,6 +4329,15 @@ impl ResumeSessionConfig { self } + /// Include instructions from every configured MCP server on resume. + /// + /// Enabling this broadens the default trust boundary; only use it with + /// trusted servers. + pub fn with_allow_all_mcp_server_instructions(mut self, allow: bool) -> Self { + self.allow_all_mcp_server_instructions = Some(allow); + self + } + /// Set MCP OAuth token storage mode on resume. /// See [`SessionConfig::with_mcp_oauth_token_storage`] for details. pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into) -> Self { @@ -7104,6 +7142,36 @@ mod tests { assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none()); } + #[test] + fn mcp_server_instruction_policy_reaches_create_and_resume_wire_payloads() { + for value in [true, false] { + let (create_wire, _) = SessionConfig::default() + .with_allow_all_mcp_server_instructions(value) + .into_wire(None) + .expect("create config is valid"); + let create_json = serde_json::to_value(&create_wire).unwrap(); + assert_eq!(create_json["allowAllMcpServerInstructions"], value); + + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("policy")) + .with_allow_all_mcp_server_instructions(value) + .into_wire() + .expect("resume config is valid"); + let resume_json = serde_json::to_value(&resume_wire).unwrap(); + assert_eq!(resume_json["allowAllMcpServerInstructions"], value); + } + + let (create_wire, _) = SessionConfig::default().into_wire(None).unwrap(); + let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("policy")) + .into_wire() + .unwrap(); + assert!( + serde_json::to_value(create_wire).unwrap()["allowAllMcpServerInstructions"].is_null() + ); + assert!( + serde_json::to_value(resume_wire).unwrap()["allowAllMcpServerInstructions"].is_null() + ); + } + #[test] fn session_config_clones_disabled_mcp_servers() { let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]); diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 325dfdaaf1..f32bb24b33 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -92,6 +92,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub allow_all_mcp_server_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth_client_id_metadata_url: Option, @@ -253,6 +255,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub allow_all_mcp_server_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub mcp_oauth_token_storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth_client_id_metadata_url: Option,