Skip to content
Merged
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
21 changes: 14 additions & 7 deletions dashboard/src/v2/components/settings/panels/SettingsMcpPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ export const SettingsMcpPanel: FunctionComponent<{ state: SettingsPageState }> =
<>
<SectionCard title="MCP Servers" watermark="MCP" icon={<Server strokeWidth={2.4} />}>
<NoticePanel title={isProject ? "Project scope" : "System scope"}>{scopeNotice}</NoticePanel>
<NoticePanel title="MCP connection modes">
Code UX exposes its built-in MCP server over stdio by default. To let external MCP clients connect over authenticated Streamable HTTP, start Code UX with MCP_HTTP_* environment variables or --mcp-http* flags, then add remote custom servers below with HTTP / SSE.
</NoticePanel>

<div className="grid gap-3 md:grid-cols-2">
{/* Built-in MCP card */}
Expand Down Expand Up @@ -331,8 +334,9 @@ const JsonMapEditor: FunctionComponent<{
resetKey: string;
value?: Record<string, string>;
placeholder: string;
"aria-label"?: string;
onChange: (value: Record<string, string> | undefined) => void;
}> = ({ resetKey, value, placeholder, onChange }) => {
}> = ({ resetKey, value, placeholder, "aria-label": ariaLabel, onChange }) => {
const [text, setText] = useState<string>(() => JSON.stringify(value ?? {}, null, 2));
const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -372,7 +376,7 @@ const JsonMapEditor: FunctionComponent<{

return (
<div className="flex w-full flex-col gap-1.5">
<TextAreaInput value={text} onChange={handle} rows={5} placeholder={placeholder} />
<TextAreaInput value={text} onChange={handle} rows={5} placeholder={placeholder} aria-label={ariaLabel} />
{error ? <span className="text-[11px] font-medium text-status-red">{error}</span> : null}
</div>
);
Expand Down Expand Up @@ -427,6 +431,9 @@ const CustomServerDetail: FunctionComponent<{

return (
<SectionCard title={server.label || server.name || "MCP server"} watermark="MCP" icon={<Server strokeWidth={2.4} />}>
<NoticePanel title="HTTP / SSE setup">
Choose HTTP / SSE for a remote MCP server that already exposes an HTTP or SSE endpoint. Paste the server URL below, add optional auth headers as a JSON object, and Code UX injects the updated config on the next CLI run.
</NoticePanel>
<Row label="Display name" description="Shown on the MCP servers list.">
<TextInput value={server.label ?? ""} onChange={(value) => onChange({ label: value })} placeholder="Playwright" />
</Row>
Expand Down Expand Up @@ -455,16 +462,16 @@ const CustomServerDetail: FunctionComponent<{
<TextAreaInput value={(server.args ?? []).join("\n")} onChange={onArgsChange} rows={4} placeholder={"@playwright/mcp@latest"} />
</Row>
<Row label="Environment (JSON)" description="Optional object of env var name to value passed to the command.">
<JsonMapEditor resetKey={server.id} value={server.env} placeholder={'{\n "API_KEY": "..."\n}'} onChange={(env) => onChange({ env })} />
<JsonMapEditor resetKey={server.id} value={server.env} placeholder={'{\n "API_KEY": "..."\n}'} aria-label="Environment JSON" onChange={(env) => onChange({ env })} />
</Row>
</>
) : (
<>
<Row label="Server URL" description="HTTP/SSE endpoint for the MCP server.">
<TextInput value={server.url ?? ""} onChange={(value) => onChange({ url: value })} placeholder="https://example.com/mcp" mono />
<Row label="Server URL" description="Paste the HTTP or SSE endpoint URL provided by the MCP server.">
<TextInput value={server.url ?? ""} onChange={(value) => onChange({ url: value })} placeholder="https://example.com/mcp" mono aria-label="Server URL" />
</Row>
<Row label="Auth headers (JSON)" description="Optional object of header name to value, e.g. Authorization tokens.">
<JsonMapEditor resetKey={server.id} value={server.headers} placeholder={'{\n "Authorization": "Bearer ..."\n}'} onChange={(headers) => onChange({ headers })} />
<Row label="Auth headers (JSON)" description="Optional JSON object of header names to string values, for example Authorization tokens.">
<JsonMapEditor resetKey={server.id} value={server.headers} placeholder={'{\n "Authorization": "Bearer ..."\n}'} aria-label="Auth headers JSON" onChange={(headers) => onChange({ headers })} />
</Row>
</>
)}
Expand Down
1 change: 1 addition & 0 deletions docs/dashboard/design-system-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This document defines the visual patterns and rules for the Settings workspace.
* Provider-instance cards announce local action results in-card through `ActionFeedbackRegion`. Enable/disable, auth-mode changes, dashboard login, and remove affordances distinguish local unsaved changes from persisted state; destructive remove actions require a target-named confirmation click before invoking the change and suppress duplicate activation while pending.
* Pill choices and toggles use `controlFeedback` for focus, hover, active, and selected cues. Arrow keys move between pill radio choices and update the selected value. Reduced motion snaps the selected rail and color changes while preserving the checked state and visible label.
* Quality Assurance trigger agent assignment uses checkbox-based multi-select groups with trigger-specific accessible names. Empty selection is a visible built-in QA fallback state and must not write placeholder preset ids.
* MCP custom server transport selection keeps radiogroup semantics. HTTP / SSE setup must expose the URL field and auth headers JSON editor with durable accessible names, keep the generated config preview in a labelled region, and state that saved changes apply on the next CLI run.

3. **High-Risk Actions**:
* Destructive actions in the Danger Zone (`Wipe Project`, `Wipe Database`) use the `danger` tone, yielding clear semantic `bg-status-red text-white` presentation. Panels themselves hint at danger via red-tinted borders and backgrounds.
Expand Down
10 changes: 10 additions & 0 deletions docs/mcp/runtime-and-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ That endpoint:
- exposes the same project-manager tool surface as stdio
- no longer exposes a separate worker-control-plane runtime

## Dashboard Settings Path

The Settings > MCP panel explains both runtime connection modes in place:

- Code UX exposes the built-in MCP server over stdio by default.
- Authenticated Streamable HTTP for external MCP clients is enabled at startup with `MCP_HTTP_*` environment variables or `--mcp-http*` flags.
- Custom remote MCP servers are added from system scope by choosing `HTTP / SSE`, pasting the server URL, and optionally entering auth headers as a JSON object of header names to string values.
- HTTP custom server previews use `{ type: "http", url, headers }`; stdio custom server previews use command, args, and env.
- Custom server changes are injected into MCP-capable CLI containers on the next CLI run. Project scope can enable, disable, or override inherited system servers, but new custom servers are created at system scope.

## Error Handling

- Axios errors are unwrapped for user-friendly API messages.
Expand Down
91 changes: 91 additions & 0 deletions tests/dashboard/v2/settings-mcp-panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/** @vitest-environment happy-dom */
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
import { useState } from "preact/hooks";
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, within } from "@testing-library/preact";
import * as matchers from "@testing-library/jest-dom/matchers";
import { SettingsMcpPanel } from "../../../dashboard/src/v2/components/settings/panels/SettingsMcpPanel.js";
import type { CustomMcpServer, McpToolToggle } from "../../../dashboard/src/v2/types.js";

expect.extend(matchers);

vi.mock("gsap", () => ({
default: {
context: vi.fn((callback: () => void) => {
callback();
return { revert: vi.fn() };
}),
fromTo: vi.fn(),
},
}));

vi.mock("../../../dashboard/src/v2/hooks/use-reduced-motion.js", () => ({
useGsapDurations: () => ({ feedback: { duration: 0 } }),
useReducedMotion: () => true,
useResolvedMotionDuration: (duration: number | string) => duration,
}));

const TestHarness = () => {
const [customMcpServers, setCustomMcpServers] = useState<CustomMcpServer[]>([]);
const [mcpTools, setMcpTools] = useState<McpToolToggle[]>([]);

const systemSettings = {
mcpTools,
customMcpServers,
};

return (
<SettingsMcpPanel
state={{
activeScope: "system",
selectedProject: null,
systemSettings,
projectSettings: null,
updateSystem: (updater: (current: typeof systemSettings) => typeof systemSettings) => {
const next = updater(systemSettings);
setMcpTools(next.mcpTools);
setCustomMcpServers(next.customMcpServers);
},
updateProject: vi.fn(),
} as any}
/>
);
};

describe("SettingsMcpPanel", () => {
afterEach(() => {
cleanup();
});

it("guides HTTP/SSE custom server setup and keeps the generated preview accurate", () => {
render(<TestHarness />);

expect(screen.getByText(/exposes its built-in MCP server over stdio by default/i)).toBeInTheDocument();
expect(screen.getByText(/MCP_HTTP_\* environment variables or --mcp-http\* flags/i)).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: /Add MCP server/i }));

expect(screen.getByText("HTTP / SSE setup")).toBeInTheDocument();
expect(screen.getByText(/Choose HTTP \/ SSE for a remote MCP server/i)).toBeInTheDocument();
expect(screen.getByText(/Code UX injects the updated config on the next CLI run/i)).toBeInTheDocument();
expect(screen.getByRole("radio", { name: /HTTP \/ SSE/i })).toHaveAttribute("aria-checked", "true");

const serverUrl = screen.getByLabelText("Server URL");
const authHeaders = screen.getByLabelText("Auth headers JSON");
expect(serverUrl).toBeInTheDocument();
expect(authHeaders).toBeInTheDocument();

fireEvent.input(screen.getByPlaceholderText("playwright"), { target: { value: "remote_docs" } });
fireEvent.input(serverUrl, { target: { value: "https://mcp.example.test/sse" } });
fireEvent.input(authHeaders, { target: { value: '{\n "Authorization": "Bearer test-token"\n}' } });

const preview = screen.getByRole("region", { name: /generated MCP configuration preview/i });
expect(within(preview).getByText(/"remote_docs":/)).toBeInTheDocument();
expect(within(preview).getByText(/"type": "http"/)).toBeInTheDocument();
expect(within(preview).getByText(/"url": "https:\/\/mcp\.example\.test\/sse"/)).toBeInTheDocument();
expect(within(preview).getByText(/"headers":/)).toBeInTheDocument();
expect(within(preview).getByText(/"Authorization": "Bearer test-token"/)).toBeInTheDocument();
});
});