From 82f7904f750dfae504dad7841a691028aa1aedd2 Mon Sep 17 00:00:00 2001 From: Code UX Date: Mon, 6 Jul 2026 09:53:59 +0000 Subject: [PATCH] feat(task T03): implement via codex --- .../settings/panels/SettingsMcpPanel.tsx | 21 +++-- docs/dashboard/design-system-settings.md | 1 + docs/mcp/runtime-and-dispatch.md | 10 ++ .../dashboard/v2/settings-mcp-panel.test.tsx | 91 +++++++++++++++++++ 4 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 tests/dashboard/v2/settings-mcp-panel.test.tsx diff --git a/dashboard/src/v2/components/settings/panels/SettingsMcpPanel.tsx b/dashboard/src/v2/components/settings/panels/SettingsMcpPanel.tsx index 287ce8664c..b99d0043b3 100644 --- a/dashboard/src/v2/components/settings/panels/SettingsMcpPanel.tsx +++ b/dashboard/src/v2/components/settings/panels/SettingsMcpPanel.tsx @@ -242,6 +242,9 @@ export const SettingsMcpPanel: FunctionComponent<{ state: SettingsPageState }> = <> }> {scopeNotice} + + 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. +
{/* Built-in MCP card */} @@ -331,8 +334,9 @@ const JsonMapEditor: FunctionComponent<{ resetKey: string; value?: Record; placeholder: string; + "aria-label"?: string; onChange: (value: Record | undefined) => void; -}> = ({ resetKey, value, placeholder, onChange }) => { +}> = ({ resetKey, value, placeholder, "aria-label": ariaLabel, onChange }) => { const [text, setText] = useState(() => JSON.stringify(value ?? {}, null, 2)); const [error, setError] = useState(null); @@ -372,7 +376,7 @@ const JsonMapEditor: FunctionComponent<{ return (
- + {error ? {error} : null}
); @@ -427,6 +431,9 @@ const CustomServerDetail: FunctionComponent<{ return ( }> + + 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. + onChange({ label: value })} placeholder="Playwright" /> @@ -455,16 +462,16 @@ const CustomServerDetail: FunctionComponent<{ - onChange({ env })} /> + onChange({ env })} /> ) : ( <> - - onChange({ url: value })} placeholder="https://example.com/mcp" mono /> + + onChange({ url: value })} placeholder="https://example.com/mcp" mono aria-label="Server URL" /> - - onChange({ headers })} /> + + onChange({ headers })} /> )} diff --git a/docs/dashboard/design-system-settings.md b/docs/dashboard/design-system-settings.md index 63b415201f..42d4aea2b8 100644 --- a/docs/dashboard/design-system-settings.md +++ b/docs/dashboard/design-system-settings.md @@ -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. diff --git a/docs/mcp/runtime-and-dispatch.md b/docs/mcp/runtime-and-dispatch.md index 5ca5189847..0a9d7b9d6f 100644 --- a/docs/mcp/runtime-and-dispatch.md +++ b/docs/mcp/runtime-and-dispatch.md @@ -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. diff --git a/tests/dashboard/v2/settings-mcp-panel.test.tsx b/tests/dashboard/v2/settings-mcp-panel.test.tsx new file mode 100644 index 0000000000..73db2a74e8 --- /dev/null +++ b/tests/dashboard/v2/settings-mcp-panel.test.tsx @@ -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([]); + const [mcpTools, setMcpTools] = useState([]); + + const systemSettings = { + mcpTools, + customMcpServers, + }; + + return ( + 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(); + + 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(); + }); +});