diff --git a/.githooks/pre-commit b/.githooks/pre-commit old mode 100644 new mode 100755 diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 57d55f227..d476c3916 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -878,6 +878,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.InfiniteSessions, Commands: config.Commands?.Select(c => new CommandWireDefinition(c.Name, c.Description)).ToList(), RequestElicitation: config.OnElicitationRequest != null, + RequestMcpApps: config.EnableMcpApps ? true : null, Traceparent: traceparent, Tracestate: tracestate, ModelCapabilities: config.ModelCapabilities, @@ -1059,6 +1060,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.InfiniteSessions, Commands: config.Commands?.Select(c => new CommandWireDefinition(c.Name, c.Description)).ToList(), RequestElicitation: config.OnElicitationRequest != null, + RequestMcpApps: config.EnableMcpApps ? true : null, Traceparent: traceparent, Tracestate: tracestate, ModelCapabilities: config.ModelCapabilities, @@ -2171,6 +2173,7 @@ internal record CreateSessionRequest( InfiniteSessionConfig? InfiniteSessions, IList? Commands = null, bool? RequestElicitation = null, + bool? RequestMcpApps = null, string? Traceparent = null, string? Tracestate = null, ModelCapabilitiesOverride? ModelCapabilities = null, @@ -2243,6 +2246,7 @@ internal record ResumeSessionRequest( InfiniteSessionConfig? InfiniteSessions, IList? Commands = null, bool? RequestElicitation = null, + bool? RequestMcpApps = null, string? Traceparent = null, string? Tracestate = null, ModelCapabilitiesOverride? ModelCapabilities = null, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 1f8c475f5..28fe089f1 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1130,6 +1130,15 @@ public sealed class SessionUiCapabilities /// Whether the host supports interactive elicitation dialogs. /// public bool? Elicitation { get; set; } + + /// + /// Whether the runtime has accepted the session's MCP Apps (SEP-1865) opt-in. + /// true when the consumer set + /// to true on create/resume and the runtime's MCP_APPS feature flag + /// (or COPILOT_MCP_APPS=true env override) is on. Otherwise absent or + /// false, indicating the runtime silently dropped the opt-in. + /// + public bool? McpApps { get; set; } } // ============================================================================ @@ -2346,6 +2355,7 @@ protected SessionConfigBase(SessionConfigBase? other) Agent = other.Agent; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; EnableConfigDiscovery = other.EnableConfigDiscovery; + EnableMcpApps = other.EnableMcpApps; ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null; Hooks = other.Hooks; InfiniteSessions = other.InfiniteSessions; @@ -2507,6 +2517,30 @@ protected SessionConfigBase(SessionConfigBase? other) /// Handler for auto-mode-switch requests from the server. public Func>? OnAutoModeSwitchRequest { get; set; } + /// + /// Enable MCP Apps (SEP-1865) UI passthrough on this session. + /// + /// When true and the runtime has MCP Apps enabled (via the + /// MCP_APPS feature flag or COPILOT_MCP_APPS=true environment override), the + /// runtime adds the mcp-apps capability to the session, which causes it to advertise + /// the extensions.io.modelcontextprotocol/ui extension to MCP servers (so they expose + /// _meta.ui.resourceUri on tools) and to expose the + /// session.rpc.mcp.apps.{listTools,callTool,readResource,setHostContext,getHostContext,diagnose} + /// JSON-RPC methods. + /// + /// + /// If the runtime gate is off, the opt-in is silently dropped server-side (the runtime logs a + /// warning); the session is created normally but the MCP Apps surface is unavailable. Inspect + /// the runtime's capabilities.ui.mcpApps on the create/resume response to detect this. + /// + /// + /// SDK consumers MUST set this to true only when they have an iframe renderer that can + /// display ui:// MCP App bundles. Setting it without a renderer will cause MCP servers + /// to register UI-enabled tool variants the consumer cannot display. + /// + /// + public bool EnableMcpApps { get; set; } + /// Hook handlers for session lifecycle events. public SessionHooks? Hooks { get; set; } diff --git a/go/client.go b/go/client.go index bb1b905c4..307e02d90 100644 --- a/go/client.go +++ b/go/client.go @@ -659,6 +659,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.OnAutoModeSwitchRequest != nil { req.RequestAutoModeSwitch = Bool(true) } + if config.EnableMcpApps { + req.RequestMcpApps = Bool(true) + } if config.Streaming != nil { req.Streaming = config.Streaming @@ -985,6 +988,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.OnAutoModeSwitchRequest != nil { req.RequestAutoModeSwitch = Bool(true) } + if config.EnableMcpApps { + req.RequestMcpApps = Bool(true) + } traceparent, tracestate := getTraceContext(ctx) req.Traceparent = traceparent diff --git a/go/client_test.go b/go/client_test.go index 39358a72a..bff951a9c 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -946,6 +946,65 @@ func TestResumeSessionRequest_RequestElicitation(t *testing.T) { }) } +func TestCreateSessionRequest_RequestMcpApps(t *testing.T) { + t.Run("sends requestMcpApps flag when EnableMcpApps is set", func(t *testing.T) { + req := createSessionRequest{ + RequestMcpApps: Bool(true), + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["requestMcpApps"] != true { + t.Errorf("Expected requestMcpApps to be true, got %v", m["requestMcpApps"]) + } + }) + + t.Run("does not send requestMcpApps when EnableMcpApps is unset", func(t *testing.T) { + req := createSessionRequest{} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["requestMcpApps"]; ok { + t.Error("Expected requestMcpApps to be omitted when not set") + } + }) +} + +func TestResumeSessionRequest_RequestMcpApps(t *testing.T) { + t.Run("sends requestMcpApps flag when EnableMcpApps is set", func(t *testing.T) { + req := resumeSessionRequest{ + SessionID: "s1", + RequestMcpApps: Bool(true), + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["requestMcpApps"] != true { + t.Errorf("Expected requestMcpApps to be true, got %v", m["requestMcpApps"]) + } + }) + + t.Run("does not send requestMcpApps when EnableMcpApps is unset", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1"} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["requestMcpApps"]; ok { + t.Error("Expected requestMcpApps to be omitted when not set") + } + }) +} + func TestResumeSessionRequest_ModeCallbackFlags(t *testing.T) { req := resumeSessionRequest{ SessionID: "s1", diff --git a/go/types.go b/go/types.go index 3a8f2e45e..67f7356f6 100644 --- a/go/types.go +++ b/go/types.go @@ -987,6 +987,26 @@ type SessionConfig struct { // OnAutoModeSwitchRequest is a handler for auto-mode-switch requests from the server. // When provided, enables autoModeSwitch.request callbacks for the session. OnAutoModeSwitchRequest AutoModeSwitchRequestHandler + // EnableMcpApps enables MCP Apps (SEP-1865) UI passthrough on this session. + // + // When true AND the runtime has MCP Apps enabled (via the MCP_APPS feature + // flag or COPILOT_MCP_APPS=true environment override), the runtime adds the + // mcp-apps capability to the session, which causes it to advertise the + // extensions.io.modelcontextprotocol/ui extension to MCP servers (so they + // expose _meta.ui.resourceUri on tools) and to expose the + // session.rpc.mcp.apps.{listTools,callTool,readResource,setHostContext, + // getHostContext,diagnose} JSON-RPC methods. + // + // If the runtime gate is off, the opt-in is silently dropped server-side + // (the runtime logs a warning); the session is created normally but the + // MCP Apps surface is unavailable. Inspect the runtime's + // capabilities.ui.mcpApps on the create/resume response to detect this. + // + // SDK consumers MUST set this to true only when they have an iframe renderer + // that can display ui:// MCP App bundles. Setting it without a renderer will + // cause MCP servers to register UI-enabled tool variants the consumer cannot + // display. + EnableMcpApps bool // GitHubToken is an optional per-session GitHub token used for authentication. // When provided, the session authenticates as the token's owner instead of // using the global client-level auth. @@ -1089,6 +1109,12 @@ type SessionCapabilities struct { type UICapabilities struct { // Elicitation indicates whether the host supports interactive elicitation dialogs. Elicitation bool `json:"elicitation,omitempty"` + // McpApps indicates whether the runtime has accepted the session's MCP Apps + // (SEP-1865) opt-in. True when the consumer set EnableMcpApps=true on + // create/resume AND the runtime's MCP_APPS feature flag (or + // COPILOT_MCP_APPS=true env override) is on. Otherwise false, indicating + // the runtime silently dropped the opt-in. + McpApps bool `json:"mcpApps,omitempty"` } // ElicitationResult is the user's response to an elicitation dialog. @@ -1275,6 +1301,9 @@ type ResumeSessionConfig struct { // OnAutoModeSwitchRequest is a handler for auto-mode-switch requests from the server. // See SessionConfig.OnAutoModeSwitchRequest. OnAutoModeSwitchRequest AutoModeSwitchRequestHandler + // EnableMcpApps enables MCP Apps (SEP-1865) UI passthrough on resume. + // See SessionConfig.EnableMcpApps. + EnableMcpApps bool // Canvases declares canvases this session provides. Sent over the wire on // `session.resume`. See SessionConfig.Canvases. Canvases []CanvasDeclaration @@ -1534,6 +1563,7 @@ type createSessionRequest struct { InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` + RequestMcpApps *bool `json:"requestMcpApps,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` Cloud *CloudSessionOptions `json:"cloud,omitempty"` @@ -1599,6 +1629,7 @@ type resumeSessionRequest struct { InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` + RequestMcpApps *bool `json:"requestMcpApps,omitempty"` GitHubToken string `json:"gitHubToken,omitempty"` RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` Canvases []CanvasDeclaration `json:"canvases,omitempty"` diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index d9ad69282..028acc858 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -144,6 +144,9 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess if (config.getOnElicitationRequest() != null) { request.setRequestElicitation(true); } + if (config.isEnableMcpApps()) { + request.setRequestMcpApps(true); + } if (config.getOnExitPlanMode() != null) { request.setRequestExitPlanMode(true); } @@ -238,6 +241,9 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo if (config.getOnElicitationRequest() != null) { request.setRequestElicitation(true); } + if (config.isEnableMcpApps()) { + request.setRequestMcpApps(true); + } if (config.getOnExitPlanMode() != null) { request.setRequestExitPlanMode(true); } 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 1354e8c33..e18674868 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -114,6 +114,9 @@ public final class CreateSessionRequest { @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("requestMcpApps") + private Boolean requestMcpApps; + @JsonProperty("requestExitPlanMode") private Boolean requestExitPlanMode; @@ -503,6 +506,21 @@ public void clearRequestElicitation() { this.requestElicitation = null; } + /** Gets the requestMcpApps flag. @return the flag */ + public Boolean getRequestMcpApps() { + return requestMcpApps; + } + + /** Sets the requestMcpApps flag. @param requestMcpApps the flag */ + public void setRequestMcpApps(boolean requestMcpApps) { + this.requestMcpApps = requestMcpApps; + } + + /** Clears the requestMcpApps setting, reverting to the default behavior. */ + public void clearRequestMcpApps() { + this.requestMcpApps = null; + } + /** Gets the requestExitPlanMode flag. @return the flag */ public Boolean getRequestExitPlanMode() { return requestExitPlanMode; 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 fa28258b3..39cb30edc 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -74,6 +74,7 @@ public class ResumeSessionConfig { private ElicitationHandler onElicitationRequest; private ExitPlanModeHandler onExitPlanMode; private AutoModeSwitchHandler onAutoModeSwitch; + private boolean enableMcpApps; private String gitHubToken; private String remoteSession; @@ -972,6 +973,31 @@ public ResumeSessionConfig setOnElicitationRequest(ElicitationHandler onElicitat return this; } + /** + * Returns whether MCP Apps (SEP-1865) UI passthrough is enabled on resume. + * + * @return {@code true} if the consumer has opted into MCP Apps, otherwise + * {@code false} + * @see #setEnableMcpApps(boolean) + */ + public boolean isEnableMcpApps() { + return enableMcpApps; + } + + /** + * Enables MCP Apps (SEP-1865) UI passthrough on the resumed session. See + * {@link SessionConfig#setEnableMcpApps(boolean)} for full semantics (runtime + * gate, capability inspection, renderer requirement). + * + * @param enableMcpApps + * {@code true} to opt into MCP Apps support on resume + * @return this config for method chaining + */ + public ResumeSessionConfig setEnableMcpApps(boolean enableMcpApps) { + this.enableMcpApps = enableMcpApps; + return this; + } + /** * Gets the exit-plan-mode request handler. * @@ -1129,6 +1155,7 @@ public ResumeSessionConfig clone() { copy.onElicitationRequest = this.onElicitationRequest; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; + copy.enableMcpApps = this.enableMcpApps; copy.gitHubToken = this.gitHubToken; copy.remoteSession = this.remoteSession; return copy; 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 4321a24aa..5eafd5191 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -119,6 +119,9 @@ public final class ResumeSessionRequest { @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("requestMcpApps") + private Boolean requestMcpApps; + @JsonProperty("requestExitPlanMode") private Boolean requestExitPlanMode; @@ -528,6 +531,21 @@ public void clearRequestElicitation() { this.requestElicitation = null; } + /** Gets the requestMcpApps flag. @return the flag */ + public Boolean getRequestMcpApps() { + return requestMcpApps; + } + + /** Sets the requestMcpApps flag. @param requestMcpApps the flag */ + public void setRequestMcpApps(boolean requestMcpApps) { + this.requestMcpApps = requestMcpApps; + } + + /** Clears the requestMcpApps setting, reverting to the default behavior. */ + public void clearRequestMcpApps() { + this.requestMcpApps = null; + } + /** Gets the requestExitPlanMode flag. @return the flag */ public Boolean getRequestExitPlanMode() { return requestExitPlanMode; 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 2a42df610..67c780b82 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -74,6 +74,7 @@ public class SessionConfig { private ElicitationHandler onElicitationRequest; private ExitPlanModeHandler onExitPlanMode; private AutoModeSwitchHandler onAutoModeSwitch; + private boolean enableMcpApps; private String gitHubToken; private String remoteSession; private CloudSessionOptions cloud; @@ -1033,6 +1034,50 @@ public SessionConfig setOnElicitationRequest(ElicitationHandler onElicitationReq return this; } + /** + * Returns whether MCP Apps (SEP-1865) UI passthrough is enabled on this + * session. + * + * @return {@code true} if the consumer has opted into MCP Apps, otherwise + * {@code false} + * @see #setEnableMcpApps(boolean) + */ + public boolean isEnableMcpApps() { + return enableMcpApps; + } + + /** + * Enables MCP Apps (SEP-1865) UI passthrough on this session. + *

+ * When {@code true} and the runtime has MCP Apps enabled (via the + * {@code MCP_APPS} feature flag or {@code COPILOT_MCP_APPS=true} environment + * override), the runtime adds the {@code mcp-apps} capability to the session, + * which causes it to advertise the + * {@code extensions.io.modelcontextprotocol/ui} extension to MCP servers (so + * they expose {@code _meta.ui.resourceUri} on tools) and to expose the + * {@code session.rpc.mcp.apps.{listTools,callTool,readResource, + * setHostContext,getHostContext,diagnose}} JSON-RPC methods. + *

+ * If the runtime gate is off, the opt-in is silently dropped server-side (the + * runtime logs a warning); the session is created normally but the MCP Apps + * surface is unavailable. Inspect {@link SessionUiCapabilities#getMcpApps()} on + * {@link com.github.copilot.sdk.CopilotSession#getCapabilities()} to detect + * this. + *

+ * SDK consumers MUST set this to {@code true} only when they have an iframe + * renderer that can display {@code ui://} MCP App bundles. Setting it without a + * renderer will cause MCP servers to register UI-enabled tool variants the + * consumer cannot display. + * + * @param enableMcpApps + * {@code true} to opt into MCP Apps support + * @return this config instance for method chaining + */ + public SessionConfig setEnableMcpApps(boolean enableMcpApps) { + this.enableMcpApps = enableMcpApps; + return this; + } + /** * Gets the exit-plan-mode request handler. * @@ -1234,6 +1279,7 @@ public SessionConfig clone() { copy.onElicitationRequest = this.onElicitationRequest; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; + copy.enableMcpApps = this.enableMcpApps; copy.gitHubToken = this.gitHubToken; copy.remoteSession = this.remoteSession; copy.cloud = this.cloud; diff --git a/java/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java b/java/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java index 015220d0c..1d3397c8f 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionUiCapabilities.java @@ -21,6 +21,9 @@ public class SessionUiCapabilities { @JsonProperty("elicitation") private Boolean elicitation; + @JsonProperty("mcpApps") + private Boolean mcpApps; + /** * Returns whether the host supports interactive elicitation dialogs. * @@ -53,4 +56,41 @@ public SessionUiCapabilities clearElicitation() { return this; } + /** + * Returns whether the runtime has accepted the session's MCP Apps (SEP-1865) + * opt-in. Present and {@code true} when the consumer set + * {@code enableMcpApps=true} on create/resume and the runtime's + * {@code MCP_APPS} feature flag (or {@code COPILOT_MCP_APPS=true} env override) + * is on. Otherwise empty or {@code false}, indicating the runtime silently + * dropped the opt-in. + * + * @return an {@link Optional} containing the boolean value, or empty if not set + */ + @JsonIgnore + public Optional getMcpApps() { + return Optional.ofNullable(mcpApps); + } + + /** + * Sets whether the runtime has accepted the MCP Apps opt-in. + * + * @param mcpApps + * {@code true} if MCP Apps is enabled for this session + * @return this instance for method chaining + */ + public SessionUiCapabilities setMcpApps(boolean mcpApps) { + this.mcpApps = mcpApps; + return this; + } + + /** + * Clears the mcpApps setting. + * + * @return this instance for method chaining + */ + public SessionUiCapabilities clearMcpApps() { + this.mcpApps = null; + return this; + } + } diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 7f3cbe8e4..615b5575b 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1097,6 +1097,7 @@ export class CopilotClient { requestPermission: !!config.onPermissionRequest, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, + ...(config.enableMcpApps ? { requestMcpApps: true } : {}), requestExitPlanMode: !!config.onExitPlanModeRequest, requestAutoModeSwitch: !!config.onAutoModeSwitchRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), @@ -1264,6 +1265,7 @@ export class CopilotClient { config.onPermissionRequest !== defaultJoinSessionPermissionHandler, requestUserInput: !!config.onUserInputRequest, requestElicitation: !!config.onElicitationRequest, + ...(config.enableMcpApps ? { requestMcpApps: true } : {}), requestExitPlanMode: !!config.onExitPlanModeRequest, requestAutoModeSwitch: !!config.onAutoModeSwitchRequest, hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 3dfc78e8a..6c8f04c04 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -574,6 +574,14 @@ export interface SessionCapabilities { ui?: { /** Whether the host supports interactive elicitation dialogs. */ elicitation?: boolean; + /** + * Whether the runtime has accepted the session's MCP Apps (SEP-1865) + * opt-in. `true` when the consumer set `enableMcpApps: true` on + * create/resume **and** the runtime's `MCP_APPS` feature flag (or + * `COPILOT_MCP_APPS=true` env override) is on. Otherwise absent or + * `false`, indicating the runtime silently dropped the opt-in. + */ + mcpApps?: boolean; /** Whether the host supports canvas rendering. */ canvases?: boolean; }; @@ -1703,6 +1711,31 @@ export interface SessionConfigBase { */ onElicitationRequest?: ElicitationHandler; + /** + * Enable MCP Apps (SEP-1865) UI passthrough on this session. + * + * When `true` **and** the runtime has MCP Apps enabled (via the + * `MCP_APPS` feature flag or `COPILOT_MCP_APPS=true` environment + * override), the runtime adds the `mcp-apps` capability to the session, + * which causes it to advertise the `extensions.io.modelcontextprotocol/ui` + * extension to MCP servers (so they expose `_meta.ui.resourceUri` on + * tools) and to expose the `session.rpc.mcp.apps.{listTools,callTool, + * readResource,setHostContext,getHostContext,diagnose}` JSON-RPC methods. + * + * If the runtime gate is off, the opt-in is silently dropped server-side + * (the runtime logs a warning); the session is created normally but the + * MCP Apps surface is unavailable. Inspect the runtime's + * `capabilities.ui.mcpApps` on the create/resume response to detect this. + * + * SDK consumers MUST set this to `true` only when they have an iframe + * renderer that can display `ui://` MCP App bundles. Setting it without a + * renderer will cause MCP servers to register UI-enabled tool variants + * the consumer cannot display. + * + * @default false + */ + enableMcpApps?: boolean; + /** * Handler for exit-plan-mode requests from the agent. * When provided, enables `exitPlanMode.request` callbacks. diff --git a/python/copilot/client.py b/python/copilot/client.py index c878129a3..845aa7779 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -91,6 +91,7 @@ logger = logging.getLogger(__name__) + # ============================================================================ # Connection Types # ============================================================================ @@ -1568,6 +1569,7 @@ async def create_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, + enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, @@ -1645,6 +1647,13 @@ async def create_session( session. Optionally associates repository metadata with the cloud session. on_event: Callback for session events. + enable_mcp_apps: Opt into MCP Apps (SEP-1865) UI passthrough. + When True, the SDK sends ``requestMcpApps: True`` on + ``session.create``. The runtime only honors the opt-in when its + ``MCP_APPS`` feature flag (or ``COPILOT_MCP_APPS=true`` env + override) is on; otherwise the request is silently dropped. + Inspect ``capabilities.ui.mcpApps`` on the create response to + detect the drop (the SDK also logs a warning). Returns: A :class:`CopilotSession` instance for the new session. @@ -1727,6 +1736,8 @@ async def create_session( # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) + if enable_mcp_apps: + payload["requestMcpApps"] = True payload["requestExitPlanMode"] = bool(on_exit_plan_mode_request) payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch_request) @@ -2045,6 +2056,7 @@ async def resume_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, + enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, @@ -2120,6 +2132,13 @@ async def resume_session( disabled_skills: Skills to disable. infinite_sessions: Infinite session configuration. on_event: Callback for session events. + enable_mcp_apps: Opt into MCP Apps (SEP-1865) UI passthrough on + resume. When True, the SDK sends ``requestMcpApps: True`` on + ``session.resume``. The runtime only honors the opt-in when its + ``MCP_APPS`` feature flag (or ``COPILOT_MCP_APPS=true`` env + override) is on; otherwise the request is silently dropped. + Inspect ``capabilities.ui.mcpApps`` on the resume response to + detect the drop (the SDK also logs a warning). continue_pending_work: When True, instructs the runtime to continue any tool calls or permission prompts that were still pending when the session was last suspended. When False (the default), the runtime @@ -2217,6 +2236,8 @@ async def resume_session( # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) + if enable_mcp_apps: + payload["requestMcpApps"] = True payload["requestExitPlanMode"] = bool(on_exit_plan_mode_request) payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch_request) diff --git a/python/copilot/session.py b/python/copilot/session.py index 527a8a092..ebbe60dc8 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -397,6 +397,12 @@ class SessionUiCapabilities(TypedDict, total=False): elicitation: bool """Whether the host supports interactive elicitation dialogs.""" + mcpApps: bool + """Whether the runtime has accepted the session's MCP Apps (SEP-1865) opt-in. + ``True`` when the consumer set ``enable_mcp_apps=True`` on create/resume and + the runtime's ``MCP_APPS`` feature flag (or ``COPILOT_MCP_APPS=true`` env + override) is on. Otherwise absent or ``False``, indicating the runtime + silently dropped the opt-in.""" class SessionCapabilities(TypedDict, total=False): diff --git a/rust/src/types.rs b/rust/src/types.rs index 97d726994..43fc4820e 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1131,6 +1131,30 @@ pub struct SessionConfig { pub mcp_servers: Option>, /// When true, the CLI runs config discovery (MCP config files, skills, plugins). pub enable_config_discovery: Option, + /// Enable MCP Apps (SEP-1865) UI passthrough on this session. + /// + /// When `true` **and** the runtime has MCP Apps enabled (via the + /// `MCP_APPS` feature flag or `COPILOT_MCP_APPS=true` environment + /// override), the runtime adds the `mcp-apps` capability to the + /// session, which causes it to advertise the + /// `extensions.io.modelcontextprotocol/ui` extension to MCP servers (so + /// they expose `_meta.ui.resourceUri` on tools) and to expose the + /// `session.rpc.mcp.apps.{listTools,callTool,readResource,setHostContext, + /// getHostContext,diagnose}` JSON-RPC methods. + /// + /// If the runtime gate is off, the opt-in is silently dropped + /// server-side (the runtime logs a warning); the session is created + /// normally but the MCP Apps surface is unavailable. Inspect the + /// runtime's `capabilities.ui.mcpApps` on the create/resume response to + /// detect this. + /// + /// SDK consumers MUST set this to `true` only when they have an iframe + /// renderer that can display `ui://` MCP App bundles. Setting it + /// without a renderer will cause MCP servers to register UI-enabled + /// tool variants the consumer cannot display. + /// + /// Defaults to `false`. + pub enable_mcp_apps: bool, /// Skill directory paths passed through to the GitHub Copilot CLI. pub skill_directories: Option>, /// Additional directories to search for custom instruction files. @@ -1274,6 +1298,7 @@ impl std::fmt::Debug for SessionConfig { .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) .field("enable_config_discovery", &self.enable_config_discovery) + .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("disabled_skills", &self.disabled_skills) @@ -1358,6 +1383,7 @@ impl Default for SessionConfig { excluded_tools: None, mcp_servers: None, enable_config_discovery: None, + enable_mcp_apps: false, skill_directories: None, instruction_directories: None, disabled_skills: None, @@ -1485,6 +1511,7 @@ impl SessionConfig { request_exit_plan_mode, request_auto_mode_switch, request_elicitation, + request_mcp_apps: self.enable_mcp_apps, hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, @@ -1731,6 +1758,13 @@ impl SessionConfig { self } + /// Enable MCP Apps (SEP-1865) UI passthrough on this session. Defaults + /// to `false`. See [`SessionConfig::enable_mcp_apps`]. + pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self { + self.enable_mcp_apps = enable; + self + } + /// Set skill directory paths passed through to the CLI. pub fn with_skill_directories(mut self, paths: I) -> Self where @@ -1928,6 +1962,9 @@ pub struct ResumeSessionConfig { pub mcp_servers: Option>, /// Enable config discovery on resume. pub enable_config_discovery: Option, + /// Enable MCP Apps (SEP-1865) UI passthrough on resume. See + /// [`SessionConfig::enable_mcp_apps`]. Defaults to `false`. + pub enable_mcp_apps: bool, /// Skill directory paths passed through to the GitHub Copilot CLI on resume. pub skill_directories: Option>, /// Additional directories to search for custom instruction files on @@ -2042,6 +2079,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) .field("enable_config_discovery", &self.enable_config_discovery) + .field("enable_mcp_apps", &self.enable_mcp_apps) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("disabled_skills", &self.disabled_skills) @@ -2170,6 +2208,7 @@ impl ResumeSessionConfig { request_exit_plan_mode, request_auto_mode_switch, request_elicitation, + request_mcp_apps: self.enable_mcp_apps, hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, @@ -2231,6 +2270,7 @@ impl ResumeSessionConfig { excluded_tools: None, mcp_servers: None, enable_config_discovery: None, + enable_mcp_apps: false, skill_directories: None, instruction_directories: None, disabled_skills: None, @@ -2456,6 +2496,13 @@ impl ResumeSessionConfig { self } + /// Enable MCP Apps (SEP-1865) UI passthrough on resume. Defaults to + /// `false`. See [`SessionConfig::enable_mcp_apps`]. + pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self { + self.enable_mcp_apps = enable; + self + } + /// Set skill directory paths passed through to the CLI on resume. pub fn with_skill_directories(mut self, paths: I) -> Self where @@ -3593,6 +3640,15 @@ pub struct UiCapabilities { /// Whether the host supports interactive elicitation dialogs. #[serde(skip_serializing_if = "Option::is_none")] pub elicitation: Option, + /// Whether the runtime has accepted the session's MCP Apps (SEP-1865) + /// opt-in. `Some(true)` when the consumer set + /// [`SessionConfig::enable_mcp_apps`] / [`ResumeSessionConfig::enable_mcp_apps`] + /// to `true` on create/resume **and** the runtime's `MCP_APPS` feature + /// flag (or `COPILOT_MCP_APPS=true` env override) is on. Otherwise + /// absent or `Some(false)`, indicating the runtime silently dropped the + /// opt-in. + #[serde(skip_serializing_if = "Option::is_none")] + pub mcp_apps: Option, /// Host-specific canvas capabilities. #[serde(skip_serializing_if = "Option::is_none")] pub canvases: Option, @@ -3879,6 +3935,7 @@ mod tests { assert!(!wire.request_exit_plan_mode); assert!(!wire.request_auto_mode_switch); assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); } #[test] @@ -3893,6 +3950,36 @@ mod tests { assert!(!wire.request_exit_plan_mode); assert!(!wire.request_auto_mode_switch); assert!(!wire.hooks); + assert!(!wire.request_mcp_apps); + } + + #[test] + fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = SessionConfig::default().with_enable_mcp_apps(true); + assert!(cfg.enable_mcp_apps); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("enable-mcp-apps"))) + .expect("enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); + } + + #[test] + fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps")) + .with_enable_mcp_apps(true); + assert!(cfg.enable_mcp_apps); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume enable_mcp_apps config has no duplicate handlers"); + assert!(wire.request_mcp_apps); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); } #[test] diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 15d137760..5369b83b2 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -81,6 +81,8 @@ pub(crate) struct SessionCreateWire { pub request_exit_plan_mode: bool, pub request_auto_mode_switch: bool, pub request_elicitation: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub request_mcp_apps: bool, pub hooks: bool, #[serde(skip_serializing_if = "Option::is_none")] pub skill_directories: Option>, @@ -159,6 +161,8 @@ pub(crate) struct SessionResumeWire { pub request_exit_plan_mode: bool, pub request_auto_mode_switch: bool, pub request_elicitation: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub request_mcp_apps: bool, pub hooks: bool, #[serde(skip_serializing_if = "Option::is_none")] pub skill_directories: Option>, diff --git a/rust/tests/e2e/elicitation.rs b/rust/tests/e2e/elicitation.rs index 7f8ab3bed..5575e67f3 100644 --- a/rust/tests/e2e/elicitation.rs +++ b/rust/tests/e2e/elicitation.rs @@ -383,6 +383,7 @@ async fn session_capabilities_types_are_properly_structured() { let capabilities = github_copilot_sdk::SessionCapabilities { ui: Some(UiCapabilities { elicitation: Some(true), + mcp_apps: None, canvases: None, }), };