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;
};