feat(ai-chat): external MCP servers admin UI (E3)" -m "Admin 'AI / External tools (MCP)' settings section: list/add/edit/delete

external MCP servers, per-server enable toggle and Test (lists the server's
tools), write-only auth headers (never shown), tool allowlist, and a Tavily
preset (key in the Authorization header, not the URL). Consumes the existing
admin /workspace/ai-mcp-servers endpoints. Fixes a discriminated-union narrowing
type error in the (previously untracked) server form.
This commit is contained in:
vvzvlad
2026-06-17 05:57:37 +03:00
parent 6ec91c8a2c
commit eefbf67288
6 changed files with 671 additions and 0 deletions
@@ -0,0 +1,99 @@
import {
useMutation,
useQuery,
useQueryClient,
UseQueryResult,
} from "@tanstack/react-query";
import {
getAiMcpServers,
createAiMcpServer,
updateAiMcpServer,
deleteAiMcpServer,
testAiMcpServer,
IAiMcpServer,
IAiMcpServerCreate,
IAiMcpServerUpdate,
IAiMcpServerTestResult,
} from "@/features/workspace/services/ai-mcp-server-service.ts";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
const aiMcpServersKey = ["ai-mcp-servers"];
export function useAiMcpServersQuery(
enabled: boolean = true,
): UseQueryResult<IAiMcpServer[], Error> {
return useQuery({
queryKey: aiMcpServersKey,
queryFn: () => getAiMcpServers(),
enabled,
});
}
export function useCreateAiMcpServerMutation() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation<IAiMcpServer, Error, IAiMcpServerCreate>({
mutationFn: (data) => createAiMcpServer(data),
onSuccess: () => {
notifications.show({ message: t("Created successfully") });
queryClient.invalidateQueries({ queryKey: aiMcpServersKey });
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
message: errorMessage ?? t("Failed to update data"),
color: "red",
});
},
});
}
export function useUpdateAiMcpServerMutation() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation<IAiMcpServer, Error, IAiMcpServerUpdate>({
mutationFn: (data) => updateAiMcpServer(data),
onSuccess: () => {
notifications.show({ message: t("Updated successfully") });
queryClient.invalidateQueries({ queryKey: aiMcpServersKey });
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
message: errorMessage ?? t("Failed to update data"),
color: "red",
});
},
});
}
export function useDeleteAiMcpServerMutation() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation<{ success: true }, Error, string>({
mutationFn: (id) => deleteAiMcpServer(id),
onSuccess: () => {
notifications.show({ message: t("Deleted successfully") });
queryClient.invalidateQueries({ queryKey: aiMcpServersKey });
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
message: errorMessage ?? t("Failed to update data"),
color: "red",
});
},
});
}
// Tests a saved server by id. The result ({ ok, tools } | { ok, error }) is
// rendered inline by the caller, so this mutation has no notifications.
export function useTestAiMcpServerMutation() {
return useMutation<IAiMcpServerTestResult, Error, string>({
mutationFn: (id) => testAiMcpServer(id),
});
}