Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2528,11 +2528,43 @@ public void OnSessionEvent(string sessionId, JsonElement? @event)
var evt = SessionEvent.FromJson(@event.Value.GetRawText());
if (evt != null)
{
NormalizeStoppedMcpStatus(evt);
session.DispatchEvent(evt);
}
}
}

// Temporary backward-compat shim.
//
// Newer runtimes report a distinct "stopped" MCP server status that this
// SDK build's generated McpServerStatus does not yet know about. Remap it
// back to "not_configured" (the pre-"stopped" behavior) so status-based
// logic keeps its previous meaning. Only the two MCP status events are
// inspected. Remove once "stopped" is added to the generated types.
private static void NormalizeStoppedMcpStatus(SessionEvent evt)
{
switch (evt)
{
case SessionMcpServersLoadedEvent loaded:
foreach (var server in loaded.Data.Servers)
{
if (server.Status.Value == "stopped")
{
server.Status = McpServerStatus.NotConfigured;
}
}

break;
case SessionMcpServerStatusChangedEvent changed:
if (changed.Data.Status.Value == "stopped")
{
changed.Data.Status = McpServerStatus.NotConfigured;
}

break;
}
}

public void OnSessionLifecycle(string type, string sessionId, JsonElement? metadata)
{
SessionLifecycleEvent evt = type switch
Expand Down
23 changes: 23 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2339,6 +2339,7 @@ func (c *Client) handleSessionEvent(req sessionEventRequest) {
if req.SessionID == "" {
return
}
normalizeStoppedMCPStatus(&req.Event)
// Dispatch to session
c.sessionsMux.Lock()
session, ok := c.sessions[req.SessionID]
Expand All @@ -2349,6 +2350,28 @@ func (c *Client) handleSessionEvent(req sessionEventRequest) {
}
}

// normalizeStoppedMCPStatus is a temporary backward-compat shim.
//
// Newer runtimes report a distinct "stopped" MCP server status that this SDK
// build's generated MCPServerStatus does not yet know about. Remap it back to
// "not_configured" (the pre-"stopped" behavior) so status-based logic keeps its
// previous meaning. Only the two MCP status events are inspected. Remove once
// "stopped" is added to the generated types.
func normalizeStoppedMCPStatus(event *SessionEvent) {
switch data := event.Data.(type) {
case *rpc.SessionMCPServersLoadedData:
for i := range data.Servers {
if data.Servers[i].Status == "stopped" {
data.Servers[i].Status = "not_configured"
}
}
case *rpc.SessionMCPServerStatusChangedData:
if data.Status == "stopped" {
data.Status = "not_configured"
}
}
}

// handleUserInputRequest handles a user input request from the CLI server.
func (c *Client) handleUserInputRequest(req userInputRequest) (*userInputResponse, *jsonrpc2.Error) {
if req.SessionID == "" || req.Question == "" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.copilot.generated.SessionEvent;
import com.github.copilot.rpc.AutoModeSwitchRequest;
import com.github.copilot.rpc.ExitPlanModeRequest;
Expand Down Expand Up @@ -102,7 +103,16 @@ private void handleSessionEvent(JsonNode params) {

CopilotSession session = sessions.get(sessionId);
if (session != null && eventNode != null) {
SessionEvent event = MAPPER.treeToValue(eventNode, SessionEvent.class);
SessionEvent event;
try {
event = MAPPER.treeToValue(eventNode, SessionEvent.class);
} catch (Exception parseError) {
// Backward-compat: a newer runtime may report an MCP server
// status this build's enum lacks (e.g. "stopped"). Remap it to
// the previous value and retry once.
normalizeStoppedMcpStatus(eventNode);
event = MAPPER.treeToValue(eventNode, SessionEvent.class);
Comment on lines +113 to +114
}
if (event != null) {
session.dispatchEvent(event);
}
Expand All @@ -112,6 +122,46 @@ private void handleSessionEvent(JsonNode params) {
}
}

/**
* Temporary backward-compat shim.
*
* <p>Newer runtimes report a distinct {@code stopped} MCP server status that this SDK build's
* generated {@code McpServerStatus} enum does not yet know about, which throws while parsing MCP
* status events. Rewrite it in the raw event tree back to {@code not_configured} (the
* pre-{@code stopped} behavior) so a retry can parse. Remove once {@code stopped} is added to
* the generated enum.
*/
private static void normalizeStoppedMcpStatus(JsonNode eventNode) {
if (eventNode == null || !eventNode.isObject()) {
return;
}
JsonNode typeNode = eventNode.get("type");
JsonNode dataNode = eventNode.get("data");
if (typeNode == null || dataNode == null || !dataNode.isObject()) {
return;
}
String type = typeNode.asText();
if ("session.mcp_servers_loaded".equals(type)) {
JsonNode servers = dataNode.get("servers");
if (servers != null && servers.isArray()) {
for (JsonNode server : servers) {
remapStoppedStatus(server);
}
}
} else if ("session.mcp_server_status_changed".equals(type)) {
remapStoppedStatus(dataNode);
}
}

private static void remapStoppedStatus(JsonNode node) {
if (node instanceof ObjectNode obj) {
JsonNode status = obj.get("status");
if (status != null && "stopped".equals(status.asText())) {
obj.put("status", "not_configured");
}
}
}

private void handleLifecycleEvent(JsonNode params) {
try {
String type = params.has("type") ? params.get("type").asText() : "";
Expand Down
72 changes: 58 additions & 14 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,63 +21,63 @@ import { fileURLToPath } from "node:url";
import {
createMessageConnection,
ErrorCodes,
type Message,
MessageConnection,
ResponseError,
StreamMessageReader,
StreamMessageWriter,
type Message,
} from "vscode-jsonrpc/node.js";
import {
createServerRpc,
createInternalServerRpc,
registerClientGlobalApiHandlers,
registerClientSessionApiHandlers,
} from "./generated/rpc.js";
import type { CopilotRequestHandler } from "./copilotRequestHandler.js";
import { createCopilotRequestAdapter } from "./copilotRequestHandler.js";
import type { FfiRuntimeHost } from "./ffiRuntimeHost.js";
import type {
GitHubTelemetryNotification,
OpenCanvasInstance,
SessionUpdateOptionsParams,
} from "./generated/rpc.js";
import {
createInternalServerRpc,
createServerRpc,
registerClientGlobalApiHandlers,
registerClientSessionApiHandlers,
} from "./generated/rpc.js";
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
import { CopilotSession } from "./session.js";
import type { FfiRuntimeHost } from "./ffiRuntimeHost.js";
import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js";
import { createCopilotRequestAdapter } from "./copilotRequestHandler.js";
import type { CopilotRequestHandler } from "./copilotRequestHandler.js";
import { getTraceContext } from "./telemetry.js";
import { ToolSet } from "./toolSet.js";
import type {
AutoModeSwitchRequest,
AutoModeSwitchResponse,
BearerTokenProvider,
CopilotClientMode,
CopilotClientOptions,
CustomAgentConfig,
ExitPlanModeRequest,
ExitPlanModeResult,
ForegroundSessionInfo,
GetAuthStatusResponse,
BearerTokenProvider,
GetStatusResponse,
InternalRuntimeConnection,
RuntimeConnection,
LargeToolOutputConfig,
MCPServerConfig,
ModelInfo,
NamedProviderConfig,
ProviderConfig,
ResumeSessionConfig,
RuntimeConnection,
SectionTransformFn,
SessionCapabilities,
SessionConfig,
SessionConfigBase,
SystemMessageConfig,
SessionCapabilities,
SessionEvent,
SessionFsConfig,
SessionLifecycleEvent,
SessionLifecycleEventType,
SessionLifecycleHandler,
SessionListFilter,
SessionMetadata,
SystemMessageConfig,
SystemMessageCustomizeConfig,
TelemetryConfig,
Tool,
Expand Down Expand Up @@ -472,6 +472,41 @@ class TeardownResilientStreamMessageWriter extends StreamMessageWriter {
}
}

/**
* Temporary backward-compat shim.
*
* Newer runtimes report a distinct `stopped` MCP server status that this SDK
* build's `McpServerStatus` union does not yet include. Rewrite it in the raw
* event payload back to `not_configured` (the pre-`stopped` behavior) before the
* event is dispatched, so status-based logic keeps its previous meaning. Only the
* two MCP status events are inspected. Remove once `stopped` is added to the
* generated types.
*/
function normalizeStoppedMcpStatus(event: unknown): void {
if (typeof event !== "object" || event === null) {
return;
}
const { type, data } = event as { type?: unknown; data?: unknown };
if (typeof data !== "object" || data === null) {
return;
}
const remap = (obj: unknown): void => {
if (obj && typeof obj === "object" && (obj as { status?: unknown }).status === "stopped") {
(obj as { status?: unknown }).status = "not_configured";
}
};
if (type === "session.mcp_servers_loaded") {
const servers = (data as { servers?: unknown }).servers;
if (Array.isArray(servers)) {
for (const server of servers) {
remap(server);
}
}
} else if (type === "session.mcp_server_status_changed") {
remap(data);
}
}

export class CopilotClient {
private cliStartTimeout: ReturnType<typeof setTimeout> | null = null;
private cliProcess: ChildProcess | null = null;
Expand Down Expand Up @@ -2846,6 +2881,15 @@ export class CopilotClient {
const session = this.sessions.get((notification as { sessionId: string }).sessionId);
const event = (notification as { event: SessionEvent }).event;
if (session) {
// Forward-compat: only inspect the two MCP status events, which are
// the only ones that can carry the new "stopped" status.
const eventType = (event as { type?: unknown }).type;
if (
eventType === "session.mcp_servers_loaded" ||
eventType === "session.mcp_server_status_changed"
) {
normalizeStoppedMcpStatus(event);
}
session._dispatchEvent(event);
}
}
Expand Down
44 changes: 42 additions & 2 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,32 @@ class SessionBackgroundEvent(SessionLifecycleEventBase):
)


def _normalize_stopped_mcp_status(event: Any) -> None:
"""Temporary backward-compat shim.

Newer runtimes report a distinct ``stopped`` MCP server status that this SDK
build's generated ``McpServerStatus`` enum does not yet know about, which
raises ``ValueError`` while parsing MCP status events. Rewrite it in the raw
event payload back to ``not_configured`` (the pre-``stopped`` behavior) so a
retry can parse. Remove once ``stopped`` is added to the generated enum.
"""
if not isinstance(event, dict):
return
data = event.get("data")
if not isinstance(data, dict):
return
event_type = event.get("type")
if event_type == "session.mcp_servers_loaded":
servers = data.get("servers")
if isinstance(servers, list):
for server in servers:
if isinstance(server, dict) and server.get("status") == "stopped":
server["status"] = "not_configured"
elif event_type == "session.mcp_server_status_changed":
if data.get("status") == "stopped":
data["status"] = "not_configured"


def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent:
"""Construct the correct :class:`SessionLifecycleEvent` variant from a wire dict."""
metadata = None
Expand Down Expand Up @@ -4142,7 +4168,14 @@ def handle_notification(method: str, params: dict):
session_id = params["sessionId"]
event_dict = params["event"]
# Convert dict to SessionEvent object
event = session_event_from_dict(event_dict)
try:
event = session_event_from_dict(event_dict)
except ValueError:
# Backward-compat: a newer runtime may report an MCP server
# status this build's enum lacks (e.g. "stopped"). Remap it to
# the previous value and retry once.
_normalize_stopped_mcp_status(event_dict)
event = session_event_from_dict(event_dict)
with self._sessions_lock:
session = self._sessions.get(session_id)
if session:
Expand Down Expand Up @@ -4262,7 +4295,14 @@ def handle_notification(method: str, params: dict):
session_id = params["sessionId"]
event_dict = params["event"]
# Convert dict to SessionEvent object
event = session_event_from_dict(event_dict)
try:
event = session_event_from_dict(event_dict)
except ValueError:
# Backward-compat: a newer runtime may report an MCP server
# status this build's enum lacks (e.g. "stopped"). Remap it to
# the previous value and retry once.
_normalize_stopped_mcp_status(event_dict)
event = session_event_from_dict(event_dict)
session = self._sessions.get(session_id)
if session:
session._dispatch_event(event)
Expand Down
Loading
Loading