Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0827b5a
feat: add MCP Apps (SEP-1865) support
mattdholloway May 19, 2026
0d544c0
Merge branch 'main' into feat/mcp-apps-support
mattdholloway May 19, 2026
f251469
feat: add MCP Apps option to Python, Go, .NET, Rust SDKs
mattdholloway May 20, 2026
d90bb0f
Merge branch 'main' into feat/mcp-apps-support
mattdholloway May 20, 2026
7c62138
chore: prettier format mcpAppsSandbox files
mattdholloway May 20, 2026
b301fe4
fix: sanitize CSP domain inputs in mcpAppsSandbox (SEP-1865)
mattdholloway May 20, 2026
8f8b8cf
docs: note runtime MCP_APPS gate on enableMcpApps across SDKs
mattdholloway May 20, 2026
cba4220
Merge branch 'main' into feat/mcp-apps-support
mattdholloway May 20, 2026
53c1a2b
feat: surface capabilities.ui.mcpApps and warn on silent drop
mattdholloway May 20, 2026
f390934
Merge branch 'main' into feat/mcp-apps-support
mattdholloway May 20, 2026
e29ea92
fix: ruff format + add mcp_apps field to Rust e2e UiCapabilities literal
mattdholloway May 20, 2026
af3ee8a
fix: drop sessionId from MCP Apps warning to silence CodeQL clear-tex…
mattdholloway May 21, 2026
fb8cefd
Merge remote-tracking branch 'origin/main' into feat/mcp-apps-support
mattdholloway May 21, 2026
93fa5fe
Merge remote-tracking branch 'origin/main' into feat/mcp-apps-support
mattdholloway May 22, 2026
c3e1524
feat: add enableMcpApps support to Java SDK
mattdholloway May 22, 2026
8541da5
Merge remote-tracking branch 'origin/main' into feat/mcp-apps-support
mattdholloway May 22, 2026
9ab8617
chore: address sanity-check findings
mattdholloway May 22, 2026
7c06494
style: apply spotless formatting to Java MCP Apps additions
mattdholloway May 22, 2026
cf5ecb3
Omit requestMcpApps from wire payload when disabled
mattdholloway May 22, 2026
82ce21e
Merge branch 'main' into feat/mcp-apps-support
mattdholloway May 22, 2026
3d41f8e
fix(mcp-apps): address high-priority review feedback
mattdholloway May 22, 2026
f61d1fe
docs(mcp-apps): address worth-doing review feedback
mattdholloway May 22, 2026
6c19fa2
Merge branch 'main' into feat/mcp-apps-support
mattdholloway May 22, 2026
6ce0f03
fix(go): route MCP Apps warning through log.Default() instead of os.S…
mattdholloway May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ public async Task<CopilotSession> 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,
Expand All @@ -633,6 +634,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance

session.WorkspacePath = response.WorkspacePath;
session.SetCapabilities(response.Capabilities);
WarnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionId);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -775,6 +777,7 @@ public async Task<CopilotSession> 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,
Expand All @@ -793,6 +796,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes

session.WorkspacePath = response.WorkspacePath;
session.SetCapabilities(response.Capabilities);
WarnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionId);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -1676,6 +1680,23 @@ private void RemoveSession(string sessionId)
_sessions.TryRemove(sessionId, out _);
}

/// <summary>
/// Emit a warning log when the consumer set <c>EnableMcpApps=true</c> on create/resume
/// but the runtime did not advertise <c>capabilities.ui.mcpApps</c> in the response.
/// The runtime silently drops the opt-in when its <c>MCP_APPS</c> feature flag (or
/// <c>COPILOT_MCP_APPS=true</c> env override) is unset, so without this warning a
/// consumer trying to use MCP Apps would see no error -- just tools that never expose
/// <c>_meta.ui.resourceUri</c>.
/// </summary>
private void WarnIfMcpAppsDropped(bool requested, SessionCapabilities? capabilities, string sessionId)
{
if (!requested) return;
if (capabilities?.Ui?.McpApps == true) return;
_logger?.LogWarning(
"Session {SessionId}: EnableMcpApps was requested but the runtime did not advertise capabilities.ui.mcpApps. The runtime's MCP_APPS feature flag or COPILOT_MCP_APPS=true environment override is likely unset; the MCP Apps surface is unavailable for this session.",
sessionId);
}

/// <summary>
/// Disposes the <see cref="CopilotClient"/> synchronously.
/// </summary>
Expand Down Expand Up @@ -1960,6 +1981,7 @@ internal record CreateSessionRequest(
InfiniteSessionConfig? InfiniteSessions,
IList<CommandWireDefinition>? Commands = null,
bool? RequestElicitation = null,
bool? RequestMcpApps = null,
string? Traceparent = null,
string? Tracestate = null,
ModelCapabilitiesOverride? ModelCapabilities = null,
Expand Down Expand Up @@ -2022,6 +2044,7 @@ internal record ResumeSessionRequest(
InfiniteSessionConfig? InfiniteSessions,
IList<CommandWireDefinition>? Commands = null,
bool? RequestElicitation = null,
bool? RequestMcpApps = null,
string? Traceparent = null,
string? Tracestate = null,
ModelCapabilitiesOverride? ModelCapabilities = null,
Expand Down
34 changes: 34 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,15 @@ public sealed class SessionUiCapabilities
/// Whether the host supports interactive elicitation dialogs.
/// </summary>
public bool? Elicitation { get; set; }

/// <summary>
/// Whether the runtime has accepted the session's MCP Apps (SEP-1865) opt-in.
/// <c>true</c> when the consumer set <see cref="SessionConfigBase.EnableMcpApps"/>
/// to <c>true</c> on create/resume <b>and</b> the runtime's <c>MCP_APPS</c> feature flag
/// (or <c>COPILOT_MCP_APPS=true</c> env override) is on. Otherwise absent or
/// <c>false</c>, indicating the runtime silently dropped the opt-in.
/// </summary>
public bool? McpApps { get; set; }
}

// ============================================================================
Expand Down Expand Up @@ -2301,6 +2310,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;
Expand Down Expand Up @@ -2415,6 +2425,30 @@ protected SessionConfigBase(SessionConfigBase? other)
/// <summary>Handler for auto-mode-switch requests from the server.</summary>
public Func<AutoModeSwitchRequest, AutoModeSwitchInvocation, Task<AutoModeSwitchResponse>>? OnAutoModeSwitchRequest { get; set; }

/// <summary>
/// Enable MCP Apps (SEP-1865) UI passthrough on this session.
/// <para>
/// When <c>true</c> <b>and</b> the runtime has MCP Apps enabled (via the
/// <c>MCP_APPS</c> feature flag or <c>COPILOT_MCP_APPS=true</c> environment override), the
/// runtime adds the <c>mcp-apps</c> capability to the session, which causes it to advertise
/// the <c>extensions.io.modelcontextprotocol/ui</c> extension to MCP servers (so they expose
/// <c>_meta.ui.resourceUri</c> on tools) and to expose the
/// <c>session.rpc.mcp.apps.{listTools,callTool,readResource,setHostContext,getHostContext,diagnose}</c>
/// JSON-RPC methods.
/// </para>
/// <para>
/// 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 <c>capabilities.ui.mcpApps</c> on the create/resume response to detect this.
/// </para>
/// <para>
/// SDK consumers MUST set this to <c>true</c> only when they have an iframe renderer that can
/// display <c>ui://</c> MCP App bundles. Setting it without a renderer will cause MCP servers
/// to register UI-enabled tool variants the consumer cannot display.
/// </para>
/// </summary>
public bool EnableMcpApps { get; set; }

/// <summary>Hook handlers for session lifecycle events.</summary>
public SessionHooks? Hooks { get; set; }

Expand Down
32 changes: 32 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net"
"os"
"os/exec"
Expand All @@ -54,6 +55,29 @@ import (

const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server"

// warnIfMcpAppsDropped logs a warning via the standard log package when the
// consumer set EnableMcpApps=true on create/resume but the runtime did not
// advertise capabilities.ui.mcpApps in the response. The runtime silently drops
// the opt-in when its MCP_APPS feature flag (or COPILOT_MCP_APPS=true env
// override) is unset, so without this warning a consumer trying to use MCP
// Apps would see no error -- just tools that never expose _meta.ui.resourceUri.
//
// The warning is routed through log.Default(), so consumers can suppress or
// redirect it with log.SetOutput / log.Default().SetOutput (e.g.
// log.Default().SetOutput(io.Discard) to silence).
func warnIfMcpAppsDropped(requested bool, capabilities *SessionCapabilities, sessionID string) {
if !requested {
return
}
if capabilities != nil && capabilities.UI != nil && capabilities.UI.McpApps {
return
}
log.Printf(
"[copilot-sdk] Session %s: EnableMcpApps was requested but the runtime did not advertise capabilities.ui.mcpApps. The runtime's MCP_APPS feature flag or COPILOT_MCP_APPS=true environment override is likely unset; the MCP Apps surface is unavailable for this session.",
sessionID,
)
}

func validateSessionFsConfig(config *SessionFsConfig) error {
if config == nil {
return nil
Expand Down Expand Up @@ -645,6 +669,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
Expand Down Expand Up @@ -752,6 +779,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses

session.workspacePath = response.WorkspacePath
session.setCapabilities(response.Capabilities)
warnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionID)

return session, nil
}
Expand Down Expand Up @@ -863,6 +891,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
Expand Down Expand Up @@ -940,6 +971,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,

session.workspacePath = response.WorkspacePath
session.setCapabilities(response.Capabilities)
warnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionID)

return session, nil
}
Expand Down
59 changes: 59 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,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.
Expand Down Expand Up @@ -1027,6 +1047,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.
Expand Down Expand Up @@ -1200,6 +1226,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
}
type ProviderConfig struct {
// Type is the provider type: "openai", "azure", or "anthropic". Defaults to "openai".
Expand Down Expand Up @@ -1421,6 +1450,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"`
Comment thread
mattdholloway marked this conversation as resolved.
RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"`
Cloud *CloudSessionOptions `json:"cloud,omitempty"`
Expand Down Expand Up @@ -1477,6 +1507,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"`
Traceparent string `json:"traceparent,omitempty"`
Expand Down
27 changes: 27 additions & 0 deletions java/src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.github.copilot.sdk.json.PingResponse;
import com.github.copilot.sdk.json.ResumeSessionConfig;
import com.github.copilot.sdk.json.ResumeSessionResponse;
import com.github.copilot.sdk.json.SessionCapabilities;
import com.github.copilot.sdk.json.SessionConfig;
import com.github.copilot.sdk.json.SessionLifecycleHandler;
import com.github.copilot.sdk.json.SessionListFilter;
Expand Down Expand Up @@ -290,6 +291,30 @@ private static boolean isUnsupportedConnectMethod(JsonRpcException ex) {
return ex.getCode() == METHOD_NOT_FOUND_ERROR_CODE || "Unhandled method connect".equals(ex.getMessage());
}

/**
* Logs a warning when the consumer set {@code enableMcpApps=true} on
* create/resume but the runtime did not advertise
* {@code capabilities.ui.mcpApps} in the response. The runtime silently drops
* the opt-in when its {@code MCP_APPS} feature flag (or
* {@code COPILOT_MCP_APPS=true} env override) is unset, so without this warning
* a consumer trying to use MCP Apps would see no error -- just tools that never
* expose {@code _meta.ui.resourceUri}.
*/
private static void warnIfMcpAppsDropped(boolean requested, SessionCapabilities capabilities, String sessionId) {
if (!requested) {
return;
}
boolean advertised = capabilities != null && capabilities.getUi() != null
&& capabilities.getUi().getMcpApps().orElse(false);
if (advertised) {
return;
}
LOG.warning("Session " + sessionId
+ ": enableMcpApps was requested but the runtime did not advertise capabilities.ui.mcpApps. "
+ "The runtime's MCP_APPS feature flag or COPILOT_MCP_APPS=true environment override is "
+ "likely unset; the MCP Apps surface is unavailable for this session.");
}

/**
* Disconnects from the Copilot server and closes all active sessions.
* <p>
Expand Down Expand Up @@ -467,6 +492,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
rpcNanos);
session.setWorkspacePath(response.workspacePath());
session.setCapabilities(response.capabilities());
warnIfMcpAppsDropped(config.isEnableMcpApps(), response.capabilities(), sessionId);
// If the server returned a different sessionId (e.g. a v2 CLI that ignores
// the client-supplied ID), re-key the sessions map.
String returnedId = response.sessionId();
Expand Down Expand Up @@ -552,6 +578,7 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
rpcNanos);
session.setWorkspacePath(response.workspacePath());
session.setCapabilities(response.capabilities());
warnIfMcpAppsDropped(config.isEnableMcpApps(), response.capabilities(), sessionId);
// If the server returned a different sessionId than what was requested, re-key.
String returnedId = response.sessionId();
if (returnedId != null && !returnedId.equals(sessionId)) {
Expand Down
Loading
Loading