Skip to content

Commit 53c1a2b

Browse files
feat: surface capabilities.ui.mcpApps and warn on silent drop
Expose the runtime's response capability so consumers can detect when their enableMcpApps opt-in was silently dropped by the runtime gate (MCP_APPS feature flag / COPILOT_MCP_APPS env override unset). For each SDK: - Add mcpApps?: bool to the SessionUiCapabilities type - After session.create / session.resume, if the consumer requested the opt-in but capabilities.ui.mcpApps is not true on the response, log a warning (console.warn / logger.warning / slog / tracing::warn / fmt.Fprintf(os.Stderr, ...)) so the silent drop is discoverable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cba4220 commit 53c1a2b

10 files changed

Lines changed: 169 additions & 2 deletions

File tree

dotnet/src/Client.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
653653

654654
session.WorkspacePath = response.WorkspacePath;
655655
session.SetCapabilities(response.Capabilities);
656+
WarnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionId);
656657
}
657658
catch (Exception ex)
658659
{
@@ -813,6 +814,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
813814

814815
session.WorkspacePath = response.WorkspacePath;
815816
session.SetCapabilities(response.Capabilities);
817+
WarnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionId);
816818
}
817819
catch (Exception ex)
818820
{
@@ -1758,6 +1760,23 @@ private void RemoveSession(string sessionId)
17581760
_sessions.TryRemove(sessionId, out _);
17591761
}
17601762

1763+
/// <summary>
1764+
/// Emit a warning log when the consumer set <c>EnableMcpApps=true</c> on create/resume
1765+
/// but the runtime did not advertise <c>capabilities.ui.mcpApps</c> in the response.
1766+
/// The runtime silently drops the opt-in when its <c>MCP_APPS</c> feature flag (or
1767+
/// <c>COPILOT_MCP_APPS=true</c> env override) is unset, so without this warning a
1768+
/// consumer trying to use MCP Apps would see no error -- just tools that never expose
1769+
/// <c>_meta.ui.resourceUri</c>.
1770+
/// </summary>
1771+
private void WarnIfMcpAppsDropped(bool requested, SessionCapabilities? capabilities, string sessionId)
1772+
{
1773+
if (!requested) return;
1774+
if (capabilities?.Ui?.McpApps == true) return;
1775+
_logger?.LogWarning(
1776+
"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.",
1777+
sessionId);
1778+
}
1779+
17611780
/// <summary>
17621781
/// Disposes the <see cref="CopilotClient"/> synchronously.
17631782
/// </summary>

dotnet/src/Types.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,16 @@ public class SessionUiCapabilities
10661066
/// Whether the host supports interactive elicitation dialogs.
10671067
/// </summary>
10681068
public bool? Elicitation { get; set; }
1069+
1070+
/// <summary>
1071+
/// Whether the runtime has accepted the session's MCP Apps (SEP-1865) opt-in.
1072+
/// <c>true</c> when the consumer set <see cref="SessionConfig.EnableMcpApps"/>
1073+
/// (or <see cref="ResumeSessionConfig.EnableMcpApps"/>) to <c>true</c> on
1074+
/// create/resume <b>and</b> the runtime's <c>MCP_APPS</c> feature flag (or
1075+
/// <c>COPILOT_MCP_APPS=true</c> env override) is on. Otherwise absent or
1076+
/// <c>false</c>, indicating the runtime silently dropped the opt-in.
1077+
/// </summary>
1078+
public bool? McpApps { get; set; }
10691079
}
10701080

10711081
// ============================================================================

go/client.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ import (
5454

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

57+
// warnIfMcpAppsDropped emits a stderr warning when the consumer set
58+
// EnableMcpApps=true on create/resume but the runtime did not advertise
59+
// capabilities.ui.mcpApps in the response. The runtime silently drops the
60+
// opt-in when its MCP_APPS feature flag (or COPILOT_MCP_APPS=true env
61+
// override) is unset, so without this warning a consumer trying to use MCP
62+
// Apps would see no error -- just tools that never expose _meta.ui.resourceUri.
63+
func warnIfMcpAppsDropped(requested bool, capabilities *SessionCapabilities, sessionID string) {
64+
if !requested {
65+
return
66+
}
67+
if capabilities != nil && capabilities.UI != nil && capabilities.UI.McpApps {
68+
return
69+
}
70+
fmt.Fprintf(os.Stderr,
71+
"[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.\n",
72+
sessionID,
73+
)
74+
}
75+
5776
func validateSessionFsConfig(config *SessionFsConfig) error {
5877
if config == nil {
5978
return nil
@@ -777,6 +796,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
777796

778797
session.workspacePath = response.WorkspacePath
779798
session.setCapabilities(response.Capabilities)
799+
warnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionID)
780800

781801
return session, nil
782802
}
@@ -965,6 +985,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
965985

966986
session.workspacePath = response.WorkspacePath
967987
session.setCapabilities(response.Capabilities)
988+
warnIfMcpAppsDropped(config.EnableMcpApps, response.Capabilities, sessionID)
968989

969990
return session, nil
970991
}

go/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,12 @@ type SessionCapabilities struct {
804804
type UICapabilities struct {
805805
// Elicitation indicates whether the host supports interactive elicitation dialogs.
806806
Elicitation bool `json:"elicitation,omitempty"`
807+
// McpApps indicates whether the runtime has accepted the session's MCP Apps
808+
// (SEP-1865) opt-in. True when the consumer set EnableMcpApps=true on
809+
// create/resume AND the runtime's MCP_APPS feature flag (or
810+
// COPILOT_MCP_APPS=true env override) is on. Otherwise false, indicating
811+
// the runtime silently dropped the opt-in.
812+
McpApps bool `json:"mcpApps,omitempty"`
807813
}
808814

809815
// ElicitationResult is the user's response to an elicitation dialog.

nodejs/src/client.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,26 @@ function toWireProviderConfig(provider: ProviderConfig): Record<string, unknown>
8686
*/
8787
const MIN_PROTOCOL_VERSION = 2;
8888

89+
/**
90+
* Emit a `console.warn` when the consumer set `enableMcpApps: true` on
91+
* create/resume but the runtime did not advertise `capabilities.ui.mcpApps`
92+
* in the response. The runtime silently drops the opt-in when its `MCP_APPS`
93+
* feature flag (or `COPILOT_MCP_APPS=true` env override) is unset, so without
94+
* this warning a consumer trying to use MCP Apps would see no error — just
95+
* tools that never expose `_meta.ui.resourceUri`.
96+
*/
97+
function warnIfMcpAppsDropped(
98+
requested: boolean | undefined,
99+
capabilities: { ui?: { mcpApps?: boolean } } | undefined,
100+
sessionId: string
101+
): void {
102+
if (requested && !capabilities?.ui?.mcpApps) {
103+
console.warn(
104+
`[copilot-sdk] 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.`
105+
);
106+
}
107+
}
108+
89109
/**
90110
* Check if value is a Zod schema (has toJSONSchema method)
91111
*/
@@ -849,10 +869,11 @@ export class CopilotClient {
849869
const { workspacePath, capabilities } = response as {
850870
sessionId: string;
851871
workspacePath?: string;
852-
capabilities?: { ui?: { elicitation?: boolean } };
872+
capabilities?: { ui?: { elicitation?: boolean; mcpApps?: boolean } };
853873
};
854874
session["_workspacePath"] = workspacePath;
855875
session.setCapabilities(capabilities);
876+
warnIfMcpAppsDropped(config.enableMcpApps, capabilities, sessionId);
856877
} catch (e) {
857878
this.sessions.delete(sessionId);
858879
throw e;
@@ -990,10 +1011,11 @@ export class CopilotClient {
9901011
const { workspacePath, capabilities } = response as {
9911012
sessionId: string;
9921013
workspacePath?: string;
993-
capabilities?: { ui?: { elicitation?: boolean } };
1014+
capabilities?: { ui?: { elicitation?: boolean; mcpApps?: boolean } };
9941015
};
9951016
session["_workspacePath"] = workspacePath;
9961017
session.setCapabilities(capabilities);
1018+
warnIfMcpAppsDropped(config.enableMcpApps, capabilities, sessionId);
9971019
} catch (e) {
9981020
this.sessions.delete(sessionId);
9991021
throw e;

nodejs/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,14 @@ export interface SessionCapabilities {
483483
ui?: {
484484
/** Whether the host supports interactive elicitation dialogs. */
485485
elicitation?: boolean;
486+
/**
487+
* Whether the runtime has accepted the session's MCP Apps (SEP-1865)
488+
* opt-in. `true` when the consumer set `enableMcpApps: true` on
489+
* create/resume **and** the runtime's `MCP_APPS` feature flag (or
490+
* `COPILOT_MCP_APPS=true` env override) is on. Otherwise absent or
491+
* `false`, indicating the runtime silently dropped the opt-in.
492+
*/
493+
mcpApps?: boolean;
486494
};
487495
}
488496

python/copilot/client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,32 @@
7575

7676
logger = logging.getLogger(__name__)
7777

78+
79+
def _warn_if_mcp_apps_dropped(
80+
requested: bool,
81+
capabilities: dict | None,
82+
session_id: str,
83+
) -> None:
84+
"""Log a warning when ``enable_mcp_apps=True`` was requested but the runtime
85+
did not advertise ``capabilities.ui.mcpApps`` in the response.
86+
87+
The runtime silently drops the opt-in when its ``MCP_APPS`` feature flag
88+
(or ``COPILOT_MCP_APPS=true`` env override) is unset, so without this
89+
warning a consumer trying to use MCP Apps would see no error -- just tools
90+
that never expose ``_meta.ui.resourceUri``.
91+
"""
92+
if not requested:
93+
return
94+
ui = (capabilities or {}).get("ui") or {}
95+
if not ui.get("mcpApps"):
96+
logger.warning(
97+
"Session %s: enable_mcp_apps was requested but the runtime did "
98+
"not advertise capabilities.ui.mcpApps. The runtime's MCP_APPS "
99+
"feature flag or COPILOT_MCP_APPS=true environment override is "
100+
"likely unset; the MCP Apps surface is unavailable for this session.",
101+
session_id,
102+
)
103+
78104
# ============================================================================
79105
# Connection Types
80106
# ============================================================================
@@ -1684,6 +1710,7 @@ async def create_session(
16841710
session._workspace_path = response.get("workspacePath")
16851711
capabilities = response.get("capabilities")
16861712
session._set_capabilities(capabilities)
1713+
_warn_if_mcp_apps_dropped(enable_mcp_apps, capabilities, actual_session_id)
16871714
except BaseException as exc:
16881715
with self._sessions_lock:
16891716
self._sessions.pop(actual_session_id, None)
@@ -2039,6 +2066,7 @@ async def resume_session(
20392066
session._workspace_path = response.get("workspacePath")
20402067
capabilities = response.get("capabilities")
20412068
session._set_capabilities(capabilities)
2069+
_warn_if_mcp_apps_dropped(enable_mcp_apps, capabilities, session_id)
20422070
except BaseException as exc:
20432071
with self._sessions_lock:
20442072
self._sessions.pop(session_id, None)

python/copilot/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,12 @@ class SessionUiCapabilities(TypedDict, total=False):
373373

374374
elicitation: bool
375375
"""Whether the host supports interactive elicitation dialogs."""
376+
mcpApps: bool
377+
"""Whether the runtime has accepted the session's MCP Apps (SEP-1865) opt-in.
378+
``True`` when the consumer set ``enable_mcp_apps=True`` on create/resume and
379+
the runtime's ``MCP_APPS`` feature flag (or ``COPILOT_MCP_APPS=true`` env
380+
override) is on. Otherwise absent or ``False``, indicating the runtime
381+
silently dropped the opt-in."""
376382

377383

378384
class SessionCapabilities(TypedDict, total=False):

rust/src/session.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,11 @@ impl Client {
855855
}));
856856
}
857857
*capabilities.write() = create_result.capabilities.unwrap_or_default();
858+
warn_if_mcp_apps_dropped(
859+
config.request_mcp_apps.unwrap_or(false),
860+
&capabilities.read(),
861+
&session_id,
862+
);
858863

859864
tracing::debug!(
860865
elapsed_ms = total_start.elapsed().as_millis(),
@@ -1020,6 +1025,11 @@ impl Client {
10201025
}
10211026

10221027
*capabilities.write() = resume_capabilities.unwrap_or_default();
1028+
warn_if_mcp_apps_dropped(
1029+
config.request_mcp_apps.unwrap_or(false),
1030+
&capabilities.read(),
1031+
&session_id,
1032+
);
10231033

10241034
tracing::debug!(
10251035
elapsed_ms = total_start.elapsed().as_millis(),
@@ -1044,6 +1054,34 @@ impl Client {
10441054

10451055
type CommandHandlerMap = HashMap<String, Arc<dyn CommandHandler>>;
10461056

1057+
/// Emit a `tracing::warn!` when the consumer set `request_mcp_apps: Some(true)`
1058+
/// on create/resume but the runtime did not advertise `capabilities.ui.mcp_apps`
1059+
/// in the response. The runtime silently drops the opt-in when its `MCP_APPS`
1060+
/// feature flag (or `COPILOT_MCP_APPS=true` env override) is unset, so without
1061+
/// this warning a consumer trying to use MCP Apps would see no error — just
1062+
/// tools that never expose `_meta.ui.resourceUri`.
1063+
fn warn_if_mcp_apps_dropped(
1064+
requested: bool,
1065+
capabilities: &SessionCapabilities,
1066+
session_id: &SessionId,
1067+
) {
1068+
if !requested {
1069+
return;
1070+
}
1071+
let advertised = capabilities
1072+
.ui
1073+
.as_ref()
1074+
.and_then(|ui| ui.mcp_apps)
1075+
.unwrap_or(false);
1076+
if advertised {
1077+
return;
1078+
}
1079+
tracing::warn!(
1080+
session_id = %session_id,
1081+
"request_mcp_apps was set but the runtime did not advertise capabilities.ui.mcpApps; the MCP_APPS feature flag or COPILOT_MCP_APPS=true environment override is likely unset and the MCP Apps surface is unavailable for this session"
1082+
);
1083+
}
1084+
10471085
fn build_command_handler_map(commands: Option<&[CommandDefinition]>) -> Arc<CommandHandlerMap> {
10481086
let map = match commands {
10491087
Some(commands) => commands

rust/src/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3138,6 +3138,15 @@ pub struct UiCapabilities {
31383138
/// Whether the host supports interactive elicitation dialogs.
31393139
#[serde(skip_serializing_if = "Option::is_none")]
31403140
pub elicitation: Option<bool>,
3141+
/// Whether the runtime has accepted the session's MCP Apps (SEP-1865)
3142+
/// opt-in. `Some(true)` when the consumer set
3143+
/// [`SessionConfig::request_mcp_apps`] / [`ResumeSessionConfig::request_mcp_apps`]
3144+
/// to `Some(true)` on create/resume **and** the runtime's `MCP_APPS`
3145+
/// feature flag (or `COPILOT_MCP_APPS=true` env override) is on. Otherwise
3146+
/// absent or `Some(false)`, indicating the runtime silently dropped the
3147+
/// opt-in.
3148+
#[serde(skip_serializing_if = "Option::is_none")]
3149+
pub mcp_apps: Option<bool>,
31413150
}
31423151

31433152
/// Options for the [`SessionUi::input`](crate::session::SessionUi::input) convenience method.

0 commit comments

Comments
 (0)