fix(ui/mcp): validate the BYOK API Key Help URL is a real URL

The help URL is shown to users as a clickable link in the Connect modal, so
a malformed value saved by an admin renders as a broken link. Add an Ant url
rule on the optional field so an invalid value is caught before submit.
This commit is contained in:
Tin Chi Lo 2026-06-23 21:45:47 -07:00
parent 630a5b531c
commit 705a4a8e33
2 changed files with 57 additions and 0 deletions

View file

@ -78,6 +78,7 @@ const ByokFields: React.FC<ByokFieldsProps> = ({ form }) => {
</span>
}
name="byok_api_key_help_url"
rules={[{ type: "url", message: "Enter a valid URL (including https://)" }]}
>
<Input placeholder="https://docs.example.com/api-keys" />
</Form.Item>

View file

@ -868,6 +868,62 @@ describe("CreateMCPServer BYOK toggle", () => {
expect(payload.credentials).toBeUndefined();
});
it("blocks submit when the API Key Help URL is not a valid URL", async () => {
await selectHttpTransport();
const user = userEvent.setup({ delay: null });
await user.type(getServerNameInput(), "Byok_Server");
await user.type(screen.getByPlaceholderText("https://your-mcp-server.com"), "https://example.com/mcp");
await selectAntOption("Authentication", "Bearer Token");
await waitFor(() => expect(screen.getByText(BYOK_LABEL)).toBeInTheDocument());
await act(async () => {
fireEvent.click(getByokSwitch()!);
});
const helpUrlInput = await screen.findByPlaceholderText("https://docs.example.com/api-keys");
await user.type(helpUrlInput, "not a url");
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: "Add MCP Server" }));
});
await waitFor(() => {
const inlineError = screen.queryByText("Enter a valid URL (including https://)");
const notCalled = !vi.mocked(networking.createMCPServer).mock.calls.length;
expect(inlineError !== null || notCalled).toBe(true);
});
});
it("accepts a valid API Key Help URL and submits with is_byok=true", async () => {
await selectHttpTransport();
const user = userEvent.setup({ delay: null });
await user.type(getServerNameInput(), "Byok_Server");
await user.type(screen.getByPlaceholderText("https://your-mcp-server.com"), "https://example.com/mcp");
await selectAntOption("Authentication", "Bearer Token");
await waitFor(() => expect(screen.getByText(BYOK_LABEL)).toBeInTheDocument());
await act(async () => {
fireEvent.click(getByokSwitch()!);
});
const helpUrlInput = await screen.findByPlaceholderText("https://docs.example.com/api-keys");
await user.type(helpUrlInput, "https://docs.example.com/keys");
vi.mocked(networking.createMCPServer).mockResolvedValue({ server_id: "byok-url-1" });
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: "Add MCP Server" }));
});
await waitFor(() => expect(networking.createMCPServer).toHaveBeenCalledTimes(1));
const [, payload] = vi.mocked(networking.createMCPServer).mock.calls[0];
expect(payload.is_byok).toBe(true);
expect(payload.byok_api_key_help_url).toBe("https://docs.example.com/keys");
});
it("forces is_byok=false when auth_type is switched away from a static type after enabling it", async () => {
await selectHttpTransport();