Skip to content

Commit 248dd06

Browse files
committed
fix(web): improve MCP resource navigation and dialog
- move MCP below Skills in the resources list - keep empty MCP resources selected without showing stale details - simplify and align the Add MCP Server dialog
1 parent 8970711 commit 248dd06

8 files changed

Lines changed: 75 additions & 30 deletions

File tree

web/app/src/hooks/workspace/useWorkspaceController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,8 @@ export function useWorkspaceController() {
630630
const selectMCPServer = useCallback(
631631
(item: MCPServer | null | undefined) => {
632632
if (!item?.name) {
633+
setSelectedHubResourceType("mcp");
634+
setSelectedMCPServerName("");
633635
selectHub();
634636
return;
635637
}

web/app/src/hooks/workspace/useWorkspaceMCPSelection.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ export function useWorkspaceMCPSelection({
5252
}, [mcpServers, setSelectedMCPServerName]);
5353

5454
useEffect(() => {
55-
if (selectedHubResourceType === "mcp" && !mcpServers.length) {
56-
setSelectedHubResourceType(skillCount ? "skill" : "template");
57-
return;
58-
}
5955
if (selectedHubResourceType === "skill" && !skillCount) {
6056
setSelectedHubResourceType(mcpServers.length ? "mcp" : "template");
6157
return;

web/app/src/pages/HubPage/components/HubDetailPane/HubDetailPane.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { localizeTemplateSourceTag } from "@/shared/i18n";
1818
import { ModelsIcon } from "@/components/ui/Icons";
1919
import {
2020
Button,
21+
DialogBody,
2122
DialogCloseButton,
2223
DialogContent,
2324
DialogDescription,
@@ -268,12 +269,14 @@ function parseMCPServerDocument(value: string, t: TranslateFn): MCPServerDocumen
268269
}
269270

270271
function JSONConfigEditor({
272+
hideLabel = false,
271273
invalid = false,
272274
label,
273275
minRows = 12,
274276
onChange,
275277
value,
276278
}: {
279+
hideLabel?: boolean;
277280
invalid?: boolean;
278281
label: string;
279282
minRows?: number;
@@ -343,7 +346,7 @@ function JSONConfigEditor({
343346
className={`hub-json-editor${invalid ? " is-invalid" : ""}`}
344347
style={{ "--hub-json-editor-min-height": minHeight } as CSSProperties}
345348
>
346-
<label className="hub-json-editor-label" htmlFor={editorId}>
349+
<label className={`hub-json-editor-label${hideLabel ? " sr-only" : ""}`} htmlFor={editorId}>
347350
{label}
348351
</label>
349352
<div className="hub-json-editor-shell" ref={editorParentRef} aria-invalid={invalid || undefined} />
@@ -437,10 +440,10 @@ export function HubDetailPane({
437440
const canDeleteSkill = Boolean(selectedSkill && !isReadonlySkill(selectedSkill));
438441
const skillEntries = skillTree?.entries ?? EMPTY_WORKSPACE_ENTRIES;
439442
const activeResourceType = useMemo(() => {
440-
if (selectedResourceType === "mcp" && mcpServers.length) {
443+
if (selectedResourceType === "mcp") {
441444
return "mcp";
442445
}
443-
if (selectedResourceType === "skill" && skills.length) {
446+
if (selectedResourceType === "skill") {
444447
return "skill";
445448
}
446449
if (templates.length) {
@@ -796,7 +799,13 @@ export function HubDetailPane({
796799
*
797800
</span>
798801
<strong>
799-
{templates.length || skills.length || mcpServers.length ? t("resourcesLoading") : t("resourcesEmpty")}
802+
{activeResourceType === "mcp"
803+
? t("resourcesMCPEmpty")
804+
: activeResourceType === "skill"
805+
? t("resourcesSkillsEmpty")
806+
: templates.length || skills.length || mcpServers.length
807+
? t("resourcesLoading")
808+
: t("resourcesEmpty")}
800809
</strong>
801810
</div>
802811
)}
@@ -848,8 +857,9 @@ export function HubDetailPane({
848857
</div>
849858
<DialogCloseButton label={t("close")} size="sm" variant="tertiaryGray" />
850859
</DialogHeader>
851-
<div className="mcp-form">
860+
<DialogBody className="mcp-form">
852861
<JSONConfigEditor
862+
hideLabel
853863
label={t("resourcesMCPServerDocumentJSONLabel")}
854864
value={mcpDraftDocument}
855865
onChange={handleMCPDraftDocumentChange}
@@ -859,7 +869,7 @@ export function HubDetailPane({
859869
{mcpFormError || mcpMutationError ? (
860870
<div className="form-error hub-json-editor-error">{mcpFormError || mcpMutationError}</div>
861871
) : null}
862-
</div>
872+
</DialogBody>
863873
<DialogFooter className="hub-skill-delete-dialog-actions">
864874
<Button variant="secondaryGray" size="sm" disabled={mcpMutationBusy} onClick={closeMCPFormDialog}>
865875
{t("cancel")}

web/app/src/pages/WorkspacePage/components/WorkspaceSidebar/WorkspaceSidebar.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -285,21 +285,6 @@ export function WorkspaceSidebar({
285285
onSelectHub();
286286
},
287287
},
288-
{
289-
active: activeContextSectionId === WorkspaceContextSectionIds.mcpServers,
290-
badge: badgeCount(hub?.mcpServers?.length),
291-
groupId: WorkspaceContextSectionIds.mcpServers,
292-
icon: navigationIcon(Server),
293-
id: "mcp-servers",
294-
label: t("resourcesMCPLabel"),
295-
onSelect: () => {
296-
if (firstMCPServer && onSelectMCPServer) {
297-
onSelectMCPServer(firstMCPServer);
298-
return;
299-
}
300-
onSelectHub();
301-
},
302-
},
303288
{
304289
active: activeContextSectionId === WorkspaceContextSectionIds.hubSkills,
305290
badge: badgeCount(hub?.skills.length),
@@ -315,6 +300,21 @@ export function WorkspaceSidebar({
315300
onSelectHub();
316301
},
317302
},
303+
{
304+
active: activeContextSectionId === WorkspaceContextSectionIds.mcpServers,
305+
badge: badgeCount(hub?.mcpServers?.length),
306+
groupId: WorkspaceContextSectionIds.mcpServers,
307+
icon: navigationIcon(Server),
308+
id: "mcp-servers",
309+
label: t("resourcesMCPLabel"),
310+
onSelect: () => {
311+
if (onSelectMCPServer) {
312+
onSelectMCPServer(firstMCPServer);
313+
return;
314+
}
315+
onSelectHub();
316+
},
317+
},
318318
{
319319
active: activeContextSectionId === WorkspaceContextSectionIds.models,
320320
badge: badgeCount(modelProviders?.providers.length),

web/app/src/pages/WorkspacePage/components/WorkspaceSidebar/WorkspaceTabPanels.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1129,8 +1129,8 @@ export function WorkspaceTabPanels({
11291129
) : workspaceTab === WorkspaceTabs.hub ? (
11301130
<div className={styles.panel} role="tabpanel" aria-label={t("resourcesTab")}>
11311131
{renderHubTemplateSection()}
1132-
{renderMCPSection()}
11331132
{renderHubSkillSection()}
1133+
{renderMCPSection()}
11341134
{renderModelProviderSection()}
11351135
{renderSkillUploadDialog()}
11361136
</div>

web/app/src/shared/i18n/messages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const messages = {
171171
resourcesMCPDelete: "删除",
172172
resourcesMCPCreateTitle: "添加 MCP Server",
173173
resourcesMCPEditTitle: "编辑 MCP Server",
174-
resourcesMCPFormDescription: "配置会保存到全局 MCP catalog state.json 的 mcpServers 中。",
174+
resourcesMCPFormDescription: "添加可供智能体使用的 MCP server 配置。",
175175
resourcesMCPNameLabel: "名称",
176176
resourcesMCPNamePlaceholder: "例如 filesystem",
177177
resourcesMCPServerDocumentJSONLabel: "MCP server JSON",
@@ -1189,7 +1189,7 @@ export const messages = {
11891189
resourcesMCPDelete: "Delete",
11901190
resourcesMCPCreateTitle: "Add MCP Server",
11911191
resourcesMCPEditTitle: "Edit MCP Server",
1192-
resourcesMCPFormDescription: "This config is saved into mcpServers in the global MCP catalog state.json.",
1192+
resourcesMCPFormDescription: "Add an MCP server configuration that agents can use.",
11931193
resourcesMCPNameLabel: "Name",
11941194
resourcesMCPNamePlaceholder: "For example filesystem",
11951195
resourcesMCPServerDocumentJSONLabel: "MCP server JSON",

web/app/tests/components/HubDetailPane.test.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function t(key: string, params: Record<string, string | number> = {}) {
2222
"MCP server definition must be an mcpServers JSON object with exactly one server.",
2323
resourcesMCPDelete: "Delete",
2424
resourcesMCPDeleteConfirmMessage: 'Delete MCP server "{name}"?',
25+
resourcesMCPEmpty: "No MCP servers available yet.",
2526
resourcesMCPLoading: "Loading MCP servers",
2627
resourcesMCPSave: "Save",
2728
resourcesMCPSaving: "Saving...",
@@ -65,7 +66,7 @@ const template = {
6566
},
6667
};
6768

68-
function renderHubDetailPane() {
69+
function renderHubDetailPane(selectedResourceType: "mcp" | "skill" | "template" = "template") {
6970
return render(
7071
<HubDetailPane
7172
locale="en"
@@ -81,7 +82,10 @@ function renderHubDetailPane() {
8182
onSelectSkillFile: vi.fn(),
8283
onSelectTemplate: vi.fn(),
8384
onSelectWorkspaceFile: vi.fn(),
84-
selectedResourceType: "template",
85+
mcpServers: [],
86+
selectedMCPServer: null,
87+
selectedMCPServerName: "",
88+
selectedResourceType,
8589
selectedSkill: null,
8690
selectedSkillPath: "",
8791
selectedTemplate: template,
@@ -230,6 +234,13 @@ function renderMCPDetailPane() {
230234
}
231235

232236
describe("HubDetailPane", () => {
237+
it("keeps the MCP empty state visible when templates are available", () => {
238+
renderHubDetailPane("mcp");
239+
240+
expect(screen.getByText("No MCP servers available yet.")).toBeInTheDocument();
241+
expect(screen.queryByText("demo-template")).not.toBeInTheDocument();
242+
});
243+
233244
it("opens markdown files in a dialog with preview and code modes", async () => {
234245
const user = userEvent.setup();
235246
renderHubDetailPane();

web/app/tests/components/WorkspaceSidebar.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const labels: Record<string, string> = {
2727
notificationsSection: "Notifications",
2828
resourcesModelProvidersSection: "Model Providers",
2929
resourcesSkillsLabel: "Skills",
30+
resourcesMCPLabel: "MCP",
3031
resourcesTab: "Resources",
3132
resourcesTemplatesSection: "Templates",
3233
scheduledTasksTab: "Scheduled",
@@ -168,6 +169,31 @@ function renderSidebar(overrides: Partial<WorkspaceSidebarProps> = {}) {
168169
}
169170

170171
describe("WorkspaceSidebar", () => {
172+
it("selects the MCP resource type when the MCP list is empty", () => {
173+
const onSelectMCPServer = vi.fn();
174+
175+
renderSidebar({
176+
hub: {
177+
loaded: true,
178+
listError: "",
179+
mcpServers: [],
180+
selectedHubResourceType: "skill",
181+
selectedHubSkillName: "shell",
182+
selectedHubTemplateId: "",
183+
skills: [{ name: "shell", description: "Shell helpers" }],
184+
skillsError: "",
185+
templates: [],
186+
uploadBusy: false,
187+
uploadError: "",
188+
} as unknown as WorkspaceSidebarProps["hub"],
189+
onSelectMCPServer,
190+
});
191+
192+
fireEvent.click(screen.getByRole("button", { name: "MCP" }));
193+
194+
expect(onSelectMCPServer).toHaveBeenCalledWith(null);
195+
});
196+
171197
it("uses primary resource navigation to show the selected resource list", () => {
172198
const onToggleWorkspaceGroup = vi.fn();
173199
const onSelectModelProvider = vi.fn();

0 commit comments

Comments
 (0)