Skip to content

Commit 5b41e1f

Browse files
fix(app): toggle MCP servers in the session's location from settings (#59132)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
1 parent 08ac673 commit 5b41e1f

3 files changed

Lines changed: 66 additions & 22 deletions

File tree

‎packages/app/e2e/regression/mcp-workspace-toggle.spec.ts‎

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ const sessionID = "ses_mcp_workspace"
1212

1313
const title = "Workspace MCP routing"
1414

15-
type Surface = "popover" | "dialog"
15+
type Surface = "popover" | "dialog" | "settings"
1616

1717
// A session in a worktree of the project. The harness answers the MCP list for each location directory and stores
1818
// connect/disconnect results per directory; `requests` records every MCP request and the directory it targeted.
19-
// The popover belongs to the summary extension; the dialog (Ctrl+;) is the app's own MCP toggle.
19+
// The popover belongs to the summary extension; the dialog (Ctrl+;) is the app's own MCP toggle; settings is the
20+
// Extensions page opened (Ctrl+,) from the session.
2021
async function open(page: Page, input: { surface: Surface } & Pick<MockServerConfig, "mcp" | "onMcpAction">) {
2122
const requests: { path: string; directory: string }[] = []
2223
page.on("request", (request) => {
@@ -38,16 +39,32 @@ async function open(page: Page, input: { surface: Surface } & Pick<MockServerCon
3839
await page.goto(sessionHref(sessionID))
3940
await expectSessionTitle(page, title)
4041
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
41-
const panel = page.getByRole("dialog", { name: input.surface === "popover" ? "MCP" : "MCPs", exact: true })
42+
43+
const panel =
44+
input.surface === "settings"
45+
? page.getByTestId("settings-screen")
46+
: page.getByRole("dialog", { name: input.surface === "popover" ? "MCP" : "MCPs", exact: true })
4247

4348
const show = async () => {
4449
if (input.surface === "dialog") return page.keyboard.press("Control+;")
50+
51+
if (input.surface === "settings") {
52+
await page.keyboard.press("Control+,")
53+
54+
return panel.getByRole("tab", { name: "Extensions", exact: true }).click()
55+
}
56+
4557
await page.getByRole("button", { name: "Session details", exact: true }).click()
4658
await page.getByRole("button", { name: "MCP", exact: true }).click()
4759
}
4860

4961
await show()
50-
await expect(panel.getByText("figma-desktop", { exact: true })).toBeVisible()
62+
// The settings row also names its switch with a visually hidden label.
63+
await expect(
64+
input.surface === "settings"
65+
? panel.getByRole("switch", { name: "figma-desktop", exact: true })
66+
: panel.getByText("figma-desktop", { exact: true }),
67+
).toBeVisible()
5168

5269
return { requests, panel, show, toggle: panel.getByRole("switch") }
5370
}
@@ -93,6 +110,26 @@ for (const shared of [true, false]) {
93110
})
94111
}
95112

113+
test("settings shows and toggles the MCP of the session it was opened from", async ({ page }) => {
114+
const actions: Parameters<NonNullable<MockServerConfig["onMcpAction"]>>[0][] = []
115+
116+
const view = await open(page, {
117+
surface: "settings",
118+
mcp: (target) => [{ name: "figma-desktop", status: { status: target === workspace ? "connected" : "disabled" } }],
119+
onMcpAction: (action) => void actions.push(action),
120+
})
121+
122+
await expect(view.toggle).toBeChecked()
123+
await expect(view.toggle).toBeEnabled()
124+
view.requests.length = 0
125+
126+
await view.panel.locator('[data-slot="switch-control"]').click()
127+
await expect(view.toggle).not.toBeChecked()
128+
await expect(view.toggle).toBeEnabled()
129+
expect(actions).toEqual([{ server: "figma-desktop", action: "disconnect", directory: workspace }])
130+
expect(view.requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/disconnect", directory: workspace })
131+
})
132+
96133
for (const surface of ["popover", "dialog"] as const) {
97134
test(`shows connection failures from the MCP ${surface} and allows reconnecting`, async ({ page }) => {
98135
const error = "Streamable HTTP error: Error POSTing to endpoint: 404 Not Found"

‎packages/app/src/settings/providers/extensions.tsx‎

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,25 @@ interface PluginRowItem {
2222
}
2323

2424
export const SettingsExtensions: Component<{
25+
directory?: string
2526
subtab?: SettingsView["subtab"]
2627
onSubtab: (value: SettingsView["subtab"]) => void
2728
}> = (props) => {
2829
const language = useLanguage()
2930
const serverSdk = useServerSDK()
3031
const data = useData()
3132

32-
// Reading `.latest` rethrows a failed fetch, so these lists fall back to empty instead of crashing the page.
33-
const [mcpList, { refetch: refetchMcp }] = createResource(
34-
() => serverSdk.connection.status() === "connected",
35-
() =>
36-
serverSdk.api.mcp
37-
.list()
38-
.then((result) => result.data)
39-
.catch(() => []),
40-
{ initialValue: [] },
41-
)
42-
43-
const toggleMcp = useMcpToggle(() => undefined, refetchMcp)
33+
// MCP connections are per Location, so read and toggle them where the active session runs, as the TUI does. The
34+
// names come from the default Location so this page lists only the servers available to all projects.
35+
const location = () => (props.directory ? { directory: props.directory } : undefined)
36+
const toggleMcp = useMcpToggle(() => props.directory)
4437

4538
const mcps = createMemo<McpRowItem[]>(() => {
46-
return (mcpList.latest ?? []).map((server) => ({
47-
name: server.name,
48-
enabled: server.status.status === "connected",
39+
const active = data.location.mcp.server.list(location()) ?? []
40+
41+
return [...new Set((data.location.mcp.server.list() ?? []).map((server) => server.name))].sort().map((name) => ({
42+
name,
43+
enabled: active.find((server) => server.name === name)?.status.status === "connected",
4944
}))
5045
})
5146

@@ -68,7 +63,11 @@ export const SettingsExtensions: Component<{
6863

6964
createEffect(() => {
7065
if (serverSdk.connection.status() !== "connected") return
71-
void data.location.skill.sync().catch(() => undefined)
66+
void Promise.all([
67+
data.location.skill.sync(),
68+
data.location.mcp.server.sync(),
69+
data.location.mcp.server.sync(location()),
70+
]).catch(() => undefined)
7271
})
7372
const skills = () => data.location.skill.list() ?? []
7473

‎packages/app/src/settings/shell.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,11 @@ function RootSettings() {
419419
/>
420420
</Tabs.Content>
421421
<Tabs.Content value="extensions" class="settings-panel">
422-
<SettingsExtensions subtab={surface.view().subtab} onSubtab={(value) => surface.subtab(value)} />
422+
<SettingsExtensions
423+
directory={sourceServer() === server ? sourceDirectory() : undefined}
424+
subtab={surface.view().subtab}
425+
onSubtab={(value) => surface.subtab(value)}
426+
/>
423427
</Tabs.Content>
424428
</SettingsServerDataScope>
425429
)}
@@ -519,7 +523,11 @@ function ServerSettings(props: { entry: SettingsServer }) {
519523
/>
520524
</Tabs.Content>
521525
<Tabs.Content value="extensions" class="settings-panel">
522-
<SettingsExtensions subtab={surface.view().subtab} onSubtab={(value) => surface.subtab(value)} />
526+
<SettingsExtensions
527+
directory={activeDirectory()}
528+
subtab={surface.view().subtab}
529+
onSubtab={(value) => surface.subtab(value)}
530+
/>
523531
</Tabs.Content>
524532
</SettingsServerDataScope>
525533
)}

0 commit comments

Comments
 (0)