mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
544 lines
16 KiB
TypeScript
544 lines
16 KiB
TypeScript
// ========================================
|
|
// useMcpServers Hook
|
|
// ========================================
|
|
// TanStack Query hooks for MCP server management
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
fetchMcpServers,
|
|
updateMcpServer,
|
|
createMcpServer,
|
|
deleteMcpServer,
|
|
toggleMcpServer,
|
|
fetchMcpTemplates,
|
|
saveMcpTemplate,
|
|
deleteMcpTemplate,
|
|
installMcpTemplate,
|
|
codexRemoveServer,
|
|
codexToggleServer,
|
|
fetchAllProjects,
|
|
fetchOtherProjectsServers,
|
|
crossCliCopy,
|
|
type McpServer,
|
|
type McpServersResponse,
|
|
type McpProjectConfigType,
|
|
type McpTemplate,
|
|
type McpTemplateInstallRequest,
|
|
type AllProjectsResponse,
|
|
type OtherProjectsServersResponse,
|
|
type CrossCliCopyRequest,
|
|
type CrossCliCopyResponse,
|
|
} from '../lib/api';
|
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
|
|
|
// Query key factory
|
|
export const mcpServersKeys = {
|
|
all: ['mcpServers'] as const,
|
|
lists: () => [...mcpServersKeys.all, 'list'] as const,
|
|
list: (scope?: 'project' | 'global') => [...mcpServersKeys.lists(), scope] as const,
|
|
};
|
|
|
|
// Query key factory for MCP templates
|
|
export const mcpTemplatesKeys = {
|
|
all: ['mcpTemplates'] as const,
|
|
lists: () => [...mcpTemplatesKeys.all, 'list'] as const,
|
|
list: (category?: string) => [...mcpTemplatesKeys.lists(), category] as const,
|
|
search: (query: string) => [...mcpTemplatesKeys.all, 'search', query] as const,
|
|
categories: () => [...mcpTemplatesKeys.all, 'categories'] as const,
|
|
};
|
|
|
|
// Query key factory for projects
|
|
export const projectsKeys = {
|
|
all: ['projects'] as const,
|
|
list: () => [...projectsKeys.all, 'list'] as const,
|
|
servers: (paths?: string[]) => [...projectsKeys.all, 'servers', ...(paths ?? [])] as const,
|
|
};
|
|
|
|
// Default stale time: 2 minutes (MCP servers change occasionally)
|
|
const STALE_TIME = 2 * 60 * 1000;
|
|
|
|
export interface UseMcpServersOptions {
|
|
scope?: 'project' | 'global';
|
|
staleTime?: number;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export interface UseMcpServersReturn {
|
|
servers: McpServer[];
|
|
projectServers: McpServer[];
|
|
globalServers: McpServer[];
|
|
totalCount: number;
|
|
enabledCount: number;
|
|
isLoading: boolean;
|
|
isFetching: boolean;
|
|
error: Error | null;
|
|
refetch: () => Promise<void>;
|
|
invalidate: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Hook for fetching MCP servers
|
|
*/
|
|
export function useMcpServers(options: UseMcpServersOptions = {}): UseMcpServersReturn {
|
|
const { scope, staleTime = STALE_TIME, enabled = true } = options;
|
|
const queryClient = useQueryClient();
|
|
|
|
const projectPath = useWorkflowStore(selectProjectPath);
|
|
const queryEnabled = enabled && !!projectPath;
|
|
|
|
const query = useQuery({
|
|
queryKey: mcpServersKeys.list(scope),
|
|
queryFn: () => fetchMcpServers(projectPath),
|
|
staleTime,
|
|
enabled: queryEnabled,
|
|
retry: 2,
|
|
});
|
|
|
|
const projectServers = query.data?.project ?? [];
|
|
const globalServers = query.data?.global ?? [];
|
|
const allServers = scope === 'project' ? projectServers : scope === 'global' ? globalServers : [...projectServers, ...globalServers];
|
|
|
|
const enabledServers = allServers.filter((s) => s.enabled);
|
|
|
|
const refetch = async () => {
|
|
await query.refetch();
|
|
};
|
|
|
|
const invalidate = async () => {
|
|
await queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
};
|
|
|
|
return {
|
|
servers: allServers,
|
|
projectServers,
|
|
globalServers,
|
|
totalCount: allServers.length,
|
|
enabledCount: enabledServers.length,
|
|
isLoading: query.isLoading,
|
|
isFetching: query.isFetching,
|
|
error: query.error,
|
|
refetch,
|
|
invalidate,
|
|
};
|
|
}
|
|
|
|
// ========== Mutations ==========
|
|
|
|
export interface UseUpdateMcpServerReturn {
|
|
updateServer: (serverName: string, config: Partial<McpServer>, configType?: McpProjectConfigType) => Promise<McpServer>;
|
|
isUpdating: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useUpdateMcpServer(): UseUpdateMcpServerReturn {
|
|
const queryClient = useQueryClient();
|
|
const projectPath = useWorkflowStore(selectProjectPath);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: ({ serverName, config, configType }: { serverName: string; config: Partial<McpServer>; configType?: McpProjectConfigType }) =>
|
|
updateMcpServer(serverName, config, { projectPath: projectPath ?? undefined, configType }),
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
updateServer: (serverName, config, configType) => mutation.mutateAsync({ serverName, config, configType }),
|
|
isUpdating: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
export interface UseCreateMcpServerReturn {
|
|
createServer: (server: McpServer, configType?: McpProjectConfigType) => Promise<McpServer>;
|
|
isCreating: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useCreateMcpServer(): UseCreateMcpServerReturn {
|
|
const queryClient = useQueryClient();
|
|
const projectPath = useWorkflowStore(selectProjectPath);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: ({ server, configType }: { server: McpServer; configType?: McpProjectConfigType }) =>
|
|
createMcpServer(server, { projectPath: projectPath ?? undefined, configType }),
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
createServer: (server, configType) => mutation.mutateAsync({ server, configType }),
|
|
isCreating: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
export interface UseDeleteMcpServerReturn {
|
|
deleteServer: (serverName: string, scope: 'project' | 'global') => Promise<void>;
|
|
isDeleting: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useDeleteMcpServer(): UseDeleteMcpServerReturn {
|
|
const queryClient = useQueryClient();
|
|
const projectPath = useWorkflowStore(selectProjectPath);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: ({ serverName, scope }: { serverName: string; scope: 'project' | 'global' }) =>
|
|
deleteMcpServer(serverName, scope, { projectPath: projectPath ?? undefined }),
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
deleteServer: (serverName, scope) => mutation.mutateAsync({ serverName, scope }),
|
|
isDeleting: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
export interface UseToggleMcpServerReturn {
|
|
toggleServer: (serverName: string, enabled: boolean) => Promise<McpServer>;
|
|
isToggling: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useToggleMcpServer(): UseToggleMcpServerReturn {
|
|
const queryClient = useQueryClient();
|
|
const projectPath = useWorkflowStore(selectProjectPath);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: ({ serverName, enabled }: { serverName: string; enabled: boolean }) =>
|
|
toggleMcpServer(serverName, enabled, { projectPath: projectPath ?? undefined }),
|
|
onMutate: async ({ serverName, enabled }) => {
|
|
await queryClient.cancelQueries({ queryKey: mcpServersKeys.all });
|
|
const previousServers = queryClient.getQueryData<McpServersResponse>(mcpServersKeys.list());
|
|
|
|
// Optimistic update
|
|
queryClient.setQueryData<McpServersResponse>(mcpServersKeys.list(), (old) => {
|
|
if (!old) return old;
|
|
const updateServer = (servers: McpServer[]) =>
|
|
servers.map((s) => (s.name === serverName ? { ...s, enabled } : s));
|
|
return {
|
|
project: updateServer(old.project),
|
|
global: updateServer(old.global),
|
|
};
|
|
});
|
|
|
|
return { previousServers };
|
|
},
|
|
onError: (_error, _vars, context) => {
|
|
if (context?.previousServers) {
|
|
queryClient.setQueryData(mcpServersKeys.list(), context.previousServers);
|
|
}
|
|
},
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
toggleServer: (serverName, enabled) => mutation.mutateAsync({ serverName, enabled }),
|
|
isToggling: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Combined hook for all MCP server mutations
|
|
*/
|
|
export function useMcpServerMutations() {
|
|
const update = useUpdateMcpServer();
|
|
const create = useCreateMcpServer();
|
|
const remove = useDeleteMcpServer();
|
|
const toggle = useToggleMcpServer();
|
|
|
|
return {
|
|
updateServer: update.updateServer,
|
|
isUpdating: update.isUpdating,
|
|
createServer: create.createServer,
|
|
isCreating: create.isCreating,
|
|
deleteServer: remove.deleteServer,
|
|
isDeleting: remove.isDeleting,
|
|
toggleServer: toggle.toggleServer,
|
|
isToggling: toggle.isToggling,
|
|
isMutating: update.isUpdating || create.isCreating || remove.isDeleting || toggle.isToggling,
|
|
};
|
|
}
|
|
|
|
// ========================================
|
|
// MCP Template Hooks
|
|
// ========================================
|
|
|
|
// Default stale time for templates: 5 minutes (templates change rarely)
|
|
const TEMPLATES_STALE_TIME = 5 * 60 * 1000;
|
|
|
|
export interface UseMcpTemplatesOptions {
|
|
category?: string;
|
|
staleTime?: number;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export interface UseMcpTemplatesReturn {
|
|
templates: McpTemplate[];
|
|
isLoading: boolean;
|
|
isFetching: boolean;
|
|
error: Error | null;
|
|
refetch: () => Promise<void>;
|
|
invalidate: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Hook for fetching MCP templates with optional category filter
|
|
*/
|
|
export function useMcpTemplates(options: UseMcpTemplatesOptions = {}): UseMcpTemplatesReturn {
|
|
const { category, staleTime = TEMPLATES_STALE_TIME, enabled = true } = options;
|
|
const queryClient = useQueryClient();
|
|
|
|
const query = useQuery({
|
|
queryKey: mcpTemplatesKeys.list(category),
|
|
queryFn: () => fetchMcpTemplates(),
|
|
staleTime,
|
|
enabled,
|
|
retry: 2,
|
|
});
|
|
|
|
const refetch = async () => {
|
|
await query.refetch();
|
|
};
|
|
|
|
const invalidate = async () => {
|
|
await queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
|
};
|
|
|
|
return {
|
|
templates: category
|
|
? query.data?.filter((t) => t.category === category) ?? []
|
|
: query.data ?? [],
|
|
isLoading: query.isLoading,
|
|
isFetching: query.isFetching,
|
|
error: query.error,
|
|
refetch,
|
|
invalidate,
|
|
};
|
|
}
|
|
|
|
export interface UseCreateTemplateReturn {
|
|
createTemplate: (template: Omit<McpTemplate, 'id' | 'createdAt' | 'updatedAt'>) => Promise<{ success: boolean; id?: number; error?: string }>;
|
|
isCreating: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
/**
|
|
* Hook for creating or updating MCP templates
|
|
*/
|
|
export function useCreateTemplate(): UseCreateTemplateReturn {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (template: Omit<McpTemplate, 'id' | 'createdAt' | 'updatedAt'>) =>
|
|
saveMcpTemplate(template),
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
createTemplate: mutation.mutateAsync,
|
|
isCreating: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
export interface UseDeleteTemplateReturn {
|
|
deleteTemplate: (templateName: string) => Promise<{ success: boolean; error?: string }>;
|
|
isDeleting: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
/**
|
|
* Hook for deleting MCP templates
|
|
*/
|
|
export function useDeleteTemplate(): UseDeleteTemplateReturn {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (templateName: string) => deleteMcpTemplate(templateName),
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
deleteTemplate: mutation.mutateAsync,
|
|
isDeleting: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
export interface UseInstallTemplateReturn {
|
|
installTemplate: (request: McpTemplateInstallRequest) => Promise<{ success: boolean; serverName?: string; error?: string }>;
|
|
isInstalling: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
/**
|
|
* Hook for installing MCP templates to project or global scope
|
|
*/
|
|
export function useInstallTemplate(): UseInstallTemplateReturn {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (request: McpTemplateInstallRequest) => installMcpTemplate(request),
|
|
onSettled: () => {
|
|
// Invalidate both templates and servers since installation affects both
|
|
queryClient.invalidateQueries({ queryKey: mcpTemplatesKeys.all });
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
installTemplate: mutation.mutateAsync,
|
|
isInstalling: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
// ========================================
|
|
// Codex MCP Hooks
|
|
// ========================================
|
|
|
|
export interface UseCodexMutationsReturn {
|
|
removeServer: (serverName: string) => Promise<{ success: boolean; error?: string }>;
|
|
toggleServer: (serverName: string, enabled: boolean) => Promise<{ success: boolean; error?: string }>;
|
|
isRemoving: boolean;
|
|
isToggling: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
/**
|
|
* Combined hook for Codex MCP mutations (remove and toggle)
|
|
*/
|
|
export function useCodexMutations(): UseCodexMutationsReturn {
|
|
const queryClient = useQueryClient();
|
|
|
|
const removeMutation = useMutation({
|
|
mutationFn: (serverName: string) => codexRemoveServer(serverName),
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: ({ serverName, enabled }: { serverName: string; enabled: boolean }) =>
|
|
codexToggleServer(serverName, enabled),
|
|
onMutate: async ({ serverName, enabled }) => {
|
|
// Optimistic update could be added here if needed
|
|
return { serverName, enabled };
|
|
},
|
|
onError: (_error, _vars, context) => {
|
|
// Rollback on error
|
|
console.error('Failed to toggle Codex MCP server:', _error);
|
|
},
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
removeServer: removeMutation.mutateAsync,
|
|
isRemoving: removeMutation.isPending,
|
|
toggleServer: (serverName, enabled) => toggleMutation.mutateAsync({ serverName, enabled }),
|
|
isToggling: toggleMutation.isPending,
|
|
error: removeMutation.error || toggleMutation.error,
|
|
};
|
|
}
|
|
|
|
// ========================================
|
|
// Project Operations Hooks
|
|
// ========================================
|
|
|
|
export interface UseProjectOperationsReturn {
|
|
projects: string[];
|
|
currentProject?: string;
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
refetch: () => Promise<void>;
|
|
copyToCodex: (request: CrossCliCopyRequest) => Promise<CrossCliCopyResponse>;
|
|
copyFromCodex: (request: CrossCliCopyRequest) => Promise<CrossCliCopyResponse>;
|
|
isCopying: boolean;
|
|
fetchOtherServers: (projectPaths?: string[]) => Promise<OtherProjectsServersResponse>;
|
|
isFetchingServers: boolean;
|
|
}
|
|
|
|
/**
|
|
* Combined hook for project operations (all projects, cross-CLI copy, other projects' servers)
|
|
*/
|
|
export function useProjectOperations(): UseProjectOperationsReturn {
|
|
const queryClient = useQueryClient();
|
|
const projectPath = useWorkflowStore(selectProjectPath);
|
|
|
|
// Fetch all projects
|
|
const projectsQuery = useQuery({
|
|
queryKey: projectsKeys.list(),
|
|
queryFn: () => fetchAllProjects(),
|
|
staleTime: STALE_TIME,
|
|
enabled: true,
|
|
retry: 2,
|
|
});
|
|
|
|
// Cross-CLI copy mutation
|
|
const copyMutation = useMutation({
|
|
mutationFn: (request: CrossCliCopyRequest) => crossCliCopy(request),
|
|
onSettled: () => {
|
|
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
|
},
|
|
});
|
|
|
|
// Other projects servers query
|
|
const serversQuery = useQuery({
|
|
queryKey: projectsKeys.servers(),
|
|
queryFn: () => fetchOtherProjectsServers(),
|
|
staleTime: STALE_TIME,
|
|
enabled: false, // Manual trigger only
|
|
retry: 1,
|
|
});
|
|
|
|
const refetch = async () => {
|
|
await projectsQuery.refetch();
|
|
};
|
|
|
|
const fetchOtherServers = async (projectPaths?: string[]) => {
|
|
return await queryClient.fetchQuery({
|
|
queryKey: projectsKeys.servers(projectPaths),
|
|
queryFn: () => fetchOtherProjectsServers(projectPaths),
|
|
staleTime: STALE_TIME,
|
|
});
|
|
};
|
|
|
|
return {
|
|
projects: projectsQuery.data?.projects ?? [],
|
|
currentProject: projectsQuery.data?.currentProject ?? projectPath ?? undefined,
|
|
isLoading: projectsQuery.isLoading,
|
|
error: projectsQuery.error,
|
|
refetch,
|
|
copyToCodex: (request) => copyMutation.mutateAsync({
|
|
...request,
|
|
source: 'claude',
|
|
target: 'codex',
|
|
projectPath: request.projectPath ?? projectPath ?? undefined,
|
|
}),
|
|
copyFromCodex: (request) => copyMutation.mutateAsync({
|
|
...request,
|
|
source: 'codex',
|
|
target: 'claude',
|
|
projectPath: request.projectPath ?? projectPath ?? undefined,
|
|
}),
|
|
isCopying: copyMutation.isPending,
|
|
fetchOtherServers,
|
|
isFetchingServers: serversQuery.isFetching,
|
|
};
|
|
}
|