fix(site/src/pages/AgentsPage): persist empty MCP selection (#28238)

Removing the final optional MCP server from an existing chat produced an
empty selection, but the message request omitted `mcp_server_ids`. The
API interprets an omitted field as preserving the current selection.

Send the selected MCP server IDs for every message, including an empty
array. Add a Storybook interaction test that removes the final MCP
server and verifies the request contains `mcp_server_ids: []`.

<details>
<summary>Manual verification on a local dev instance</summary>

Setup: `./scripts/develop.sh`, an Anthropic provider with
`claude-haiku-4-5`, and a local test MCP server registered with
availability `default_on`.

With this branch, chat `75285d9f`:

1. The new chat showed the MCP chip selected.
2. Sent a message, then removed the chip with the X control.
3. Sent a second message, then reloaded the page.
4. No MCP chip appeared, and the picker toggle stayed off.
5. `GET /api/experimental/chats/{id}` returned `mcp_server_ids: []`.

With the one-line change reverted, chat `751ac191` repeated the same
flow. The chip returned as selected after the reload, and the API
returned `mcp_server_ids: ["b63a2a3a-..."]`.

Not covered: `force_on` servers, plan mode interaction, and queued
messages during streaming.

</details>

Generated by Coder Agents.
This commit is contained in:
Ethan
2026-08-18 13:34:02 +08:00
committed by GitHub
parent d15800b494
commit 30dc7ebd71
2 changed files with 54 additions and 6 deletions
@@ -26,6 +26,7 @@ import {
MockChat,
MockChatMessage,
MockChatQueuedMessage,
MockMCPServerConfig,
} from "#/testHelpers/chatEntities";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import {
@@ -255,7 +256,10 @@ const buildChatAuthorizationQuery = (
const buildQueries = (
chat: TypesGen.Chat,
messagesData: TypesGen.ChatMessagesResponse,
opts?: { diffUrl?: string },
opts?: {
diffUrl?: string;
mcpServers?: readonly TypesGen.MCPServerConfig[];
},
) => {
const diffStatus: TypesGen.ChatDiffStatus = {
chat_id: CHAT_ID,
@@ -301,7 +305,7 @@ const buildQueries = (
},
{ key: chatModelsKey, data: mockModelCatalog },
{ key: chatModelConfigs().queryKey, data: mockModelConfigs },
{ key: mcpServersKey, data: [] },
{ key: mcpServersKey, data: opts?.mcpServers ?? [] },
buildChatAuthorizationQuery(chat, {
canShareChat: {
action: "share",
@@ -3217,6 +3221,53 @@ export const SendResponseAfterChatSwitch: Story = {
},
};
export const RemoveLastMCPServer: Story = {
parameters: {
queries: buildQueries(
{
id: CHAT_ID,
...baseChatFields,
title: "Remove last MCP server",
status: "waiting",
mcp_server_ids: [MockMCPServerConfig.id],
},
{ messages: [], queued_messages: [], has_more: false },
{
diffUrl: undefined,
mcpServers: [MockMCPServerConfig],
},
),
},
beforeEach: () => {
spyOn(API.experimental, "getUserSkills").mockResolvedValue([]);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const sendSpy = spyOn(
API.experimental,
"createChatMessage",
).mockResolvedValue({ queued: false });
await userEvent.click(
await canvas.findByRole("button", { name: "Remove MCP Server" }),
);
const editor = await canvas.findByTestId("chat-message-input");
await userEvent.click(editor);
await userEvent.type(editor, "Send without MCP tools");
await userEvent.keyboard("{Enter}");
await waitFor(() => {
expect(sendSpy).toHaveBeenCalledTimes(1);
});
expect(sendSpy).toHaveBeenCalledWith(
CHAT_ID,
expect.objectContaining({
mcp_server_ids: [],
}),
);
},
};
/**
* The send flow renders the durable user row once the server accepts the
* prompt, before the assistant turn produces any output.
+1 -4
View File
@@ -1799,10 +1799,7 @@ const AgentChatPage: FC = () => {
content,
model_config_id: selectedModelConfigID,
reasoning_effort: effectiveReasoningEffort,
mcp_server_ids:
effectiveMCPServerIds.length > 0
? [...effectiveMCPServerIds]
: undefined,
mcp_server_ids: [...effectiveMCPServerIds],
...(planModeSwitch !== undefined
? {
plan_mode: