diff --git a/shell-ui/src/mcp/MCPRegistrar.test.tsx b/shell-ui/src/mcp/MCPRegistrar.test.tsx index 7f9e27ccd4..25037515ee 100644 --- a/shell-ui/src/mcp/MCPRegistrar.test.tsx +++ b/shell-ui/src/mcp/MCPRegistrar.test.tsx @@ -1,9 +1,19 @@ import type { ToolAnnotations, ToolDescriptor } from '@mcp-b/webmcp-types'; import { render } from '@testing-library/react'; +import type { ReactElement } from 'react'; +import { QueryClient } from 'react-query'; +import { QueryClientProvider } from '../QueryClientProvider'; import type { FederatedModuleInfo } from '../initFederation/ConfigurationProviders'; import { _InternalMCPRegistrar } from './MCPRegistrar'; import type { ToolContext } from './types'; +const renderWithQueryClient = (ui: ReactElement) => { + const queryClient = new QueryClient(); + return render( + {ui}, + ); +}; + // ─── Auth mock ──────────────────────────────────────────────────────────────── const mockGetToken = jest.fn().mockResolvedValue('test-token'); @@ -91,7 +101,7 @@ describe('_InternalMCPRegistrar', () => { [MODULE_KEY]: { createTools: () => [tool] }, }; - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -120,7 +130,7 @@ describe('_InternalMCPRegistrar', () => { }, }; - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -131,6 +141,7 @@ describe('_InternalMCPRegistrar', () => { expect(capturedContext?.selfConfiguration).toEqual(selfConfiguration); expect(capturedNavigate).toBe(mockNavigate); + expect(capturedContext?.queryClient).toBeDefined(); }); it('context.getToken always returns the latest token via ref', async () => { @@ -145,7 +156,7 @@ describe('_InternalMCPRegistrar', () => { }, }; - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -170,7 +181,7 @@ describe('_InternalMCPRegistrar', () => { [MODULE_KEY]: { createTools: () => [tool] }, }; - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -195,7 +206,7 @@ describe('_InternalMCPRegistrar', () => { [MODULE_KEY]: { createTools: () => [tool1, tool2] }, }; - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -216,7 +227,7 @@ describe('_InternalMCPRegistrar', () => { [MODULE_KEY]: { tools: [tool] }, }; - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -238,7 +249,7 @@ describe('_InternalMCPRegistrar', () => { }, }; - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -269,7 +280,7 @@ describe('_InternalMCPRegistrar', () => { }, }; - const { unmount } = render( + const { unmount } = renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -298,7 +309,7 @@ describe('_InternalMCPRegistrar', () => { }; expect(() => - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={moduleExports} mcpToolsModuleInfo={mcpToolsModuleInfo} @@ -313,7 +324,7 @@ describe('_InternalMCPRegistrar', () => { it('registers no tools when module export is missing', () => { // moduleExports doesn't have the expected module key - render( + renderWithQueryClient( <_InternalMCPRegistrar moduleExports={{}} mcpToolsModuleInfo={mcpToolsModuleInfo} diff --git a/shell-ui/src/mcp/MCPRegistrar.tsx b/shell-ui/src/mcp/MCPRegistrar.tsx index 5f27032c41..1109e75fc4 100644 --- a/shell-ui/src/mcp/MCPRegistrar.tsx +++ b/shell-ui/src/mcp/MCPRegistrar.tsx @@ -3,6 +3,7 @@ import type { ModelContextClient, ToolDescriptor } from '@mcp-b/webmcp-types'; import { ComponentWithFederatedImports } from '@scality/module-federation'; import { useEffect, useMemo, useRef } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; +import { useQueryClient } from 'react-query'; import { useNavigate } from 'react-router'; import { useAuth } from '../auth/AuthProvider'; import { @@ -43,6 +44,7 @@ export const _InternalMCPRegistrar = ({ navigate: (path: string) => void; }) => { const { getToken, userData } = useAuth(); + const queryClient = useQueryClient(); // Keep auth refs current so tool execute() always reads fresh credentials // without causing the registration effect to re-run on every render. @@ -61,6 +63,7 @@ export const _InternalMCPRegistrar = ({ get getToken() { return getTokenRef.current; }, get userData() { return userDataRef.current; }, selfConfiguration, + queryClient, }; // Prefer the new createTools factory (supports navigate + dynamic context); // fall back to the legacy static tools array for modules not yet migrated. @@ -99,7 +102,7 @@ export const _InternalMCPRegistrar = ({ navigator.modelContext?.unregisterTool?.(name), ); }; - }, [moduleExports, mcpToolsModuleInfo, selfConfiguration, navigate]); + }, [moduleExports, mcpToolsModuleInfo, selfConfiguration, navigate, queryClient]); return null; }; diff --git a/shell-ui/src/mcp/types.ts b/shell-ui/src/mcp/types.ts index b31bd2713c..de535017f1 100644 --- a/shell-ui/src/mcp/types.ts +++ b/shell-ui/src/mcp/types.ts @@ -4,6 +4,7 @@ * It contains only what shell-ui itself owns. Micro-frontends extend it with their own * app-specific context derived from selfConfiguration. */ +import type { QueryClient } from 'react-query'; import type { UserData } from '../auth/AuthProvider'; export type { UserData }; @@ -21,4 +22,12 @@ export type ToolContext = { * Micro-frontends cast this to their own known config shape to extract endpoints etc. */ selfConfiguration: Record; + /** + * The shell-ui–owned QueryClient, shared across every federated app via + * (see FederatedApp.tsx). Tools use + * this to keep the chat-side UI panels in sync with their mutations — + * `invalidateQueries`, `setQueryData` for optimistic updates, or + * `refetchQueries` — picking the strategy that fits the operation. + */ + queryClient: QueryClient; };