Skip to content

Commit 94e07cb

Browse files
committed
fix: MCP button opens project settings with project pre-selected
The MCP server button in the workspace header now navigates directly to the project settings with the current project pre-selected, rather than opening a separate workspace MCP modal. - Extended SettingsContext to accept optional projectPath when opening - ProjectSettingsSection now respects initialProjectPath from context - Removed WorkspaceMCPModal from WorkspaceHeader (component still exists) - Updated stories to test new navigation flow
1 parent b98b323 commit 94e07cb

File tree

4 files changed

+42
-135
lines changed

4 files changed

+42
-135
lines changed

src/browser/components/Settings/sections/ProjectSettingsSection.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback, useEffect, useState } from "react";
22
import { useAPI } from "@/browser/contexts/API";
33
import { useProjectContext } from "@/browser/contexts/ProjectContext";
4+
import { useSettings } from "@/browser/contexts/SettingsContext";
45
import {
56
Trash2,
67
Play,
@@ -174,10 +175,11 @@ const ToolAllowlistSection: React.FC<{
174175
export const ProjectSettingsSection: React.FC = () => {
175176
const { api } = useAPI();
176177
const { projects } = useProjectContext();
178+
const { initialProjectPath } = useSettings();
177179
const projectList = Array.from(projects.keys());
178180

179181
// Core state
180-
const [selectedProject, setSelectedProject] = useState<string>("");
182+
const [selectedProject, setSelectedProject] = useState<string>(initialProjectPath ?? "");
181183
const [servers, setServers] = useState<Record<string, MCPServerInfo>>({});
182184
const [loading, setLoading] = useState(false);
183185
const [error, setError] = useState<string | null>(null);
@@ -225,12 +227,14 @@ export const ProjectSettingsSection: React.FC = () => {
225227
setIdleHoursInput(idleHours?.toString() ?? "24");
226228
}, [idleHours]);
227229

228-
// Set default project when projects load
230+
// Set project when initialProjectPath changes or default to first project
229231
useEffect(() => {
230-
if (projectList.length > 0 && !selectedProject) {
232+
if (initialProjectPath && projectList.includes(initialProjectPath)) {
233+
setSelectedProject(initialProjectPath);
234+
} else if (projectList.length > 0 && !selectedProject) {
231235
setSelectedProject(projectList[0]);
232236
}
233-
}, [projectList, selectedProject]);
237+
}, [projectList, selectedProject, initialProjectPath]);
234238

235239
const refresh = useCallback(async () => {
236240
if (!api || !selectedProject) return;

src/browser/components/WorkspaceHeader.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Pencil, Server } from "lucide-react";
33
import { GitStatusIndicator } from "./GitStatusIndicator";
44
import { RuntimeBadge } from "./RuntimeBadge";
55
import { BranchSelector } from "./BranchSelector";
6-
import { WorkspaceMCPModal } from "./WorkspaceMCPModal";
6+
77
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
88
import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
99
import { useGitStatus } from "@/browser/stores/GitStatusStore";
@@ -13,6 +13,7 @@ import type { RuntimeConfig } from "@/common/types/runtime";
1313
import { useTutorial } from "@/browser/contexts/TutorialContext";
1414
import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal";
1515
import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor";
16+
import { useSettings } from "@/browser/contexts/SettingsContext";
1617

1718
interface WorkspaceHeaderProps {
1819
workspaceId: string;
@@ -36,8 +37,8 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
3637
const gitStatus = useGitStatus(workspaceId);
3738
const { canInterrupt } = useWorkspaceSidebarState(workspaceId);
3839
const { startSequence: startTutorial, isSequenceCompleted } = useTutorial();
40+
const { open: openSettings } = useSettings();
3941
const [editorError, setEditorError] = useState<string | null>(null);
40-
const [mcpModalOpen, setMcpModalOpen] = useState(false);
4142

4243
const handleOpenTerminal = useCallback(() => {
4344
openTerminal(workspaceId, runtimeConfig);
@@ -96,15 +97,15 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
9697
<Button
9798
variant="ghost"
9899
size="icon"
99-
onClick={() => setMcpModalOpen(true)}
100+
onClick={() => openSettings("projects", projectPath)}
100101
className="text-muted hover:text-foreground h-6 w-6 shrink-0"
101102
data-testid="workspace-mcp-button"
102103
>
103104
<Server className="h-4 w-4" />
104105
</Button>
105106
</TooltipTrigger>
106107
<TooltipContent side="bottom" align="center">
107-
Configure MCP servers for this workspace
108+
Configure MCP servers for this project
108109
</TooltipContent>
109110
</Tooltip>
110111
<Tooltip>
@@ -141,12 +142,6 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
141142
</TooltipContent>
142143
</Tooltip>
143144
</div>
144-
<WorkspaceMCPModal
145-
workspaceId={workspaceId}
146-
projectPath={projectPath}
147-
open={mcpModalOpen}
148-
onOpenChange={setMcpModalOpen}
149-
/>
150145
</div>
151146
);
152147
};

src/browser/contexts/SettingsContext.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import React, {
1010
interface SettingsContextValue {
1111
isOpen: boolean;
1212
activeSection: string;
13-
open: (section?: string) => void;
13+
/** Pre-selected project path when opening the projects section */
14+
initialProjectPath: string | null;
15+
open: (section?: string, projectPath?: string) => void;
1416
close: () => void;
1517
setActiveSection: (section: string) => void;
1618
}
@@ -28,9 +30,11 @@ const DEFAULT_SECTION = "general";
2830
export function SettingsProvider(props: { children: ReactNode }) {
2931
const [isOpen, setIsOpen] = useState(false);
3032
const [activeSection, setActiveSection] = useState(DEFAULT_SECTION);
33+
const [initialProjectPath, setInitialProjectPath] = useState<string | null>(null);
3134

32-
const open = useCallback((section?: string) => {
35+
const open = useCallback((section?: string, projectPath?: string) => {
3336
if (section) setActiveSection(section);
37+
setInitialProjectPath(projectPath ?? null);
3438
setIsOpen(true);
3539
}, []);
3640

@@ -42,11 +46,12 @@ export function SettingsProvider(props: { children: ReactNode }) {
4246
() => ({
4347
isOpen,
4448
activeSection,
49+
initialProjectPath,
4550
open,
4651
close,
4752
setActiveSection,
4853
}),
49-
[isOpen, activeSection, open, close]
54+
[isOpen, activeSection, initialProjectPath, open, close]
5055
);
5156

5257
return <SettingsContext.Provider value={value}>{props.children}</SettingsContext.Provider>;

src/browser/stories/App.mcp.stories.tsx

Lines changed: 21 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -158,20 +158,24 @@ async function openProjectSettings(canvasElement: HTMLElement): Promise<void> {
158158
mcpHeading.scrollIntoView({ block: "start" });
159159
}
160160

161-
/** Open the workspace MCP modal */
162-
async function openWorkspaceMCPModal(canvasElement: HTMLElement): Promise<void> {
161+
/** Open project settings by clicking MCP button in workspace header */
162+
async function openProjectSettingsViaMCPButton(canvasElement: HTMLElement): Promise<void> {
163163
const canvas = within(canvasElement);
164164
const body = within(canvasElement.ownerDocument.body);
165165

166166
// Wait for workspace header to load
167167
await canvas.findByTestId("workspace-header", {}, { timeout: 10000 });
168168

169-
// Click the MCP server button in the header
169+
// Click the MCP server button in the header - now opens project settings
170170
const mcpButton = await canvas.findByTestId("workspace-mcp-button");
171171
await userEvent.click(mcpButton);
172172

173-
// Wait for dialog
173+
// Wait for settings dialog
174174
await body.findByRole("dialog");
175+
176+
// Scroll to MCP Servers section
177+
const mcpHeading = await body.findByText("MCP Servers");
178+
mcpHeading.scrollIntoView({ behavior: "instant", block: "start" });
175179
}
176180

177181
// ═══════════════════════════════════════════════════════════════════════════════
@@ -283,11 +287,11 @@ export const ProjectSettingsWithToolAllowlist: AppStory = {
283287
};
284288

285289
// ═══════════════════════════════════════════════════════════════════════════════
286-
// WORKSPACE MCP MODAL STORIES
290+
// MCP BUTTON NAVIGATION STORIES
287291
// ═══════════════════════════════════════════════════════════════════════════════
288292

289-
/** Workspace MCP modal with servers from project (no overrides) */
290-
export const WorkspaceMCPNoOverrides: AppStory = {
293+
/** MCP button in header opens project settings with MCP servers visible */
294+
export const MCPButtonOpensProjectSettings: AppStory = {
291295
render: () => (
292296
<AppWithMocks
293297
setup={() =>
@@ -306,48 +310,18 @@ export const WorkspaceMCPNoOverrides: AppStory = {
306310
/>
307311
),
308312
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
309-
await openWorkspaceMCPModal(canvasElement);
313+
await openProjectSettingsViaMCPButton(canvasElement);
310314

311315
const body = within(canvasElement.ownerDocument.body);
312316

313-
// Both servers should be shown and enabled
317+
// Both servers should be shown in project settings
314318
await body.findByText("mux");
315319
await body.findByText("posthog");
316320
},
317321
};
318322

319-
/** Workspace MCP modal - server disabled at project level, can be enabled */
320-
export const WorkspaceMCPProjectDisabledServer: AppStory = {
321-
render: () => (
322-
<AppWithMocks
323-
setup={() =>
324-
setupMCPStory({
325-
servers: {
326-
mux: { command: "npx -y @anthropics/mux-server", disabled: false },
327-
posthog: { command: "npx -y posthog-mcp-server", disabled: true },
328-
},
329-
testResults: {
330-
mux: MOCK_TOOLS,
331-
posthog: POSTHOG_TOOLS,
332-
},
333-
preCacheTools: true,
334-
})
335-
}
336-
/>
337-
),
338-
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
339-
await openWorkspaceMCPModal(canvasElement);
340-
341-
const body = within(canvasElement.ownerDocument.body);
342-
343-
// posthog should show "(disabled at project level)" but switch should still be toggleable
344-
await body.findByText("posthog");
345-
await body.findByText(/disabled at project level/i);
346-
},
347-
};
348-
349-
/** Workspace MCP modal - server disabled at project level, enabled at workspace level */
350-
export const WorkspaceMCPEnabledOverride: AppStory = {
323+
/** MCP button opens project settings with disabled server */
324+
export const MCPButtonWithDisabledServer: AppStory = {
351325
render: () => (
352326
<AppWithMocks
353327
setup={() =>
@@ -356,44 +330,6 @@ export const WorkspaceMCPEnabledOverride: AppStory = {
356330
mux: { command: "npx -y @anthropics/mux-server", disabled: false },
357331
posthog: { command: "npx -y posthog-mcp-server", disabled: true },
358332
},
359-
workspaceOverrides: {
360-
enabledServers: ["posthog"],
361-
},
362-
testResults: {
363-
mux: MOCK_TOOLS,
364-
posthog: POSTHOG_TOOLS,
365-
},
366-
preCacheTools: true,
367-
})
368-
}
369-
/>
370-
),
371-
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
372-
await openWorkspaceMCPModal(canvasElement);
373-
374-
const body = within(canvasElement.ownerDocument.body);
375-
376-
// posthog should be enabled despite project-level disable
377-
await body.findByText("posthog");
378-
await body.findByText(/disabled at project level/i);
379-
380-
// The switch should be ON (enabled at workspace level)
381-
},
382-
};
383-
384-
/** Workspace MCP modal - server enabled at project level, disabled at workspace level */
385-
export const WorkspaceMCPDisabledOverride: AppStory = {
386-
render: () => (
387-
<AppWithMocks
388-
setup={() =>
389-
setupMCPStory({
390-
servers: {
391-
mux: { command: "npx -y @anthropics/mux-server", disabled: false },
392-
posthog: { command: "npx -y posthog-mcp-server", disabled: false },
393-
},
394-
workspaceOverrides: {
395-
disabledServers: ["posthog"],
396-
},
397333
testResults: {
398334
mux: MOCK_TOOLS,
399335
posthog: POSTHOG_TOOLS,
@@ -404,54 +340,21 @@ export const WorkspaceMCPDisabledOverride: AppStory = {
404340
/>
405341
),
406342
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
407-
await openWorkspaceMCPModal(canvasElement);
343+
await openProjectSettingsViaMCPButton(canvasElement);
408344

409345
const body = within(canvasElement.ownerDocument.body);
410346

411-
// mux should be enabled, posthog should be disabled
347+
// Both servers should be visible
412348
await body.findByText("mux");
413349
await body.findByText("posthog");
414350
},
415351
};
416352

417-
/** Workspace MCP modal with tool allowlist filtering */
418-
export const WorkspaceMCPWithToolAllowlist: AppStory = {
419-
render: () => (
420-
<AppWithMocks
421-
setup={() =>
422-
setupMCPStory({
423-
servers: {
424-
posthog: { command: "npx -y posthog-mcp-server", disabled: false },
425-
},
426-
workspaceOverrides: {
427-
toolAllowlist: {
428-
posthog: ["docs-search", "error-details", "list-errors"],
429-
},
430-
},
431-
testResults: {
432-
posthog: POSTHOG_TOOLS,
433-
},
434-
preCacheTools: true,
435-
})
436-
}
437-
/>
438-
),
439-
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
440-
await openWorkspaceMCPModal(canvasElement);
441-
442-
const body = within(canvasElement.ownerDocument.body);
443-
await body.findByText("posthog");
444-
445-
// Should show filtered tool count
446-
await body.findByText(/3 of 14 tools enabled/i);
447-
},
448-
};
449-
450353
// ═══════════════════════════════════════════════════════════════════════════════
451354
// INTERACTION STORIES
452355
// ═══════════════════════════════════════════════════════════════════════════════
453356

454-
/** Interact with tool selector - click All/None buttons */
357+
/** Interact with tool selector in project settings - click All/None buttons */
455358
export const ToolSelectorInteraction: AppStory = {
456359
render: () => (
457360
<AppWithMocks
@@ -469,7 +372,7 @@ export const ToolSelectorInteraction: AppStory = {
469372
/>
470373
),
471374
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
472-
await openWorkspaceMCPModal(canvasElement);
375+
await openProjectSettingsViaMCPButton(canvasElement);
473376

474377
const body = within(canvasElement.ownerDocument.body);
475378

@@ -489,7 +392,7 @@ export const ToolSelectorInteraction: AppStory = {
489392
},
490393
};
491394

492-
/** Toggle server enabled state in workspace modal */
395+
/** Toggle server enabled state in project settings */
493396
export const ToggleServerEnabled: AppStory = {
494397
render: () => (
495398
<AppWithMocks
@@ -509,7 +412,7 @@ export const ToggleServerEnabled: AppStory = {
509412
/>
510413
),
511414
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
512-
await openWorkspaceMCPModal(canvasElement);
415+
await openProjectSettingsViaMCPButton(canvasElement);
513416

514417
const body = within(canvasElement.ownerDocument.body);
515418

0 commit comments

Comments
 (0)