@assistant-ui/react-mcp 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/auth/buildHeaders.d.ts +1 -2
  2. package/dist/auth/buildHeaders.d.ts.map +1 -1
  3. package/dist/auth/createOAuthProvider.d.ts +5 -6
  4. package/dist/auth/createOAuthProvider.d.ts.map +1 -1
  5. package/dist/auth/types.d.ts +1 -2
  6. package/dist/auth/types.d.ts.map +1 -1
  7. package/dist/connector.d.ts +1 -2
  8. package/dist/connector.d.ts.map +1 -1
  9. package/dist/context/McpConnectorByIndexProvider.d.ts +1 -2
  10. package/dist/context/McpConnectorByIndexProvider.d.ts.map +1 -1
  11. package/dist/context/McpCustomServerByIndexProvider.d.ts +1 -2
  12. package/dist/context/McpCustomServerByIndexProvider.d.ts.map +1 -1
  13. package/dist/context/McpServerByIdProvider.d.ts +1 -2
  14. package/dist/context/McpServerByIdProvider.d.ts.map +1 -1
  15. package/dist/hooks/useMcpOAuthCallback.d.ts +5 -6
  16. package/dist/hooks/useMcpOAuthCallback.d.ts.map +1 -1
  17. package/dist/hooks/useMcpOAuthCallback.js +4 -0
  18. package/dist/hooks/useMcpOAuthCallback.js.map +1 -1
  19. package/dist/mcp-scope.d.ts +14 -15
  20. package/dist/mcp-scope.d.ts.map +1 -1
  21. package/dist/primitives/addForm/McpAddFormAuthFields.d.ts +4 -4
  22. package/dist/primitives/addForm/McpAddFormAuthFields.d.ts.map +1 -1
  23. package/dist/primitives/addForm/McpAddFormAuthFields.js +28 -38
  24. package/dist/primitives/addForm/McpAddFormAuthFields.js.map +1 -1
  25. package/dist/primitives/addForm/McpAddFormAuthSelect.d.ts +2 -3
  26. package/dist/primitives/addForm/McpAddFormAuthSelect.d.ts.map +1 -1
  27. package/dist/primitives/addForm/McpAddFormBearerTokenField.d.ts +10 -0
  28. package/dist/primitives/addForm/McpAddFormBearerTokenField.d.ts.map +1 -0
  29. package/dist/primitives/addForm/McpAddFormBearerTokenField.js +50 -0
  30. package/dist/primitives/addForm/McpAddFormBearerTokenField.js.map +1 -0
  31. package/dist/primitives/addForm/McpAddFormCancel.d.ts +2 -3
  32. package/dist/primitives/addForm/McpAddFormCancel.d.ts.map +1 -1
  33. package/dist/primitives/addForm/McpAddFormError.d.ts +2 -3
  34. package/dist/primitives/addForm/McpAddFormError.d.ts.map +1 -1
  35. package/dist/primitives/addForm/McpAddFormError.js +30 -10
  36. package/dist/primitives/addForm/McpAddFormError.js.map +1 -1
  37. package/dist/primitives/addForm/McpAddFormNameField.d.ts +2 -3
  38. package/dist/primitives/addForm/McpAddFormNameField.d.ts.map +1 -1
  39. package/dist/primitives/addForm/McpAddFormNameField.js +31 -18
  40. package/dist/primitives/addForm/McpAddFormNameField.js.map +1 -1
  41. package/dist/primitives/addForm/McpAddFormRoot.d.ts +2 -3
  42. package/dist/primitives/addForm/McpAddFormRoot.d.ts.map +1 -1
  43. package/dist/primitives/addForm/McpAddFormRoot.js +32 -11
  44. package/dist/primitives/addForm/McpAddFormRoot.js.map +1 -1
  45. package/dist/primitives/addForm/McpAddFormScopesField.d.ts +10 -0
  46. package/dist/primitives/addForm/McpAddFormScopesField.d.ts.map +1 -0
  47. package/dist/primitives/addForm/McpAddFormScopesField.js +38 -0
  48. package/dist/primitives/addForm/McpAddFormScopesField.js.map +1 -0
  49. package/dist/primitives/addForm/McpAddFormSubmit.d.ts +2 -3
  50. package/dist/primitives/addForm/McpAddFormSubmit.d.ts.map +1 -1
  51. package/dist/primitives/addForm/McpAddFormUrlField.d.ts +2 -3
  52. package/dist/primitives/addForm/McpAddFormUrlField.d.ts.map +1 -1
  53. package/dist/primitives/addForm/McpAddFormUrlField.js +31 -18
  54. package/dist/primitives/addForm/McpAddFormUrlField.js.map +1 -1
  55. package/dist/primitives/addForm/context.d.ts +13 -6
  56. package/dist/primitives/addForm/context.d.ts.map +1 -1
  57. package/dist/primitives/addForm/context.js.map +1 -1
  58. package/dist/primitives/addForm.d.ts +4 -2
  59. package/dist/primitives/addForm.js +5 -1
  60. package/dist/primitives/addForm.js.map +1 -1
  61. package/dist/primitives/elicitation/McpElicitationPrimitiveAccept.d.ts +2 -3
  62. package/dist/primitives/elicitation/McpElicitationPrimitiveAccept.d.ts.map +1 -1
  63. package/dist/primitives/elicitation/McpElicitationPrimitiveCancel.d.ts +2 -3
  64. package/dist/primitives/elicitation/McpElicitationPrimitiveCancel.d.ts.map +1 -1
  65. package/dist/primitives/elicitation/McpElicitationPrimitiveDecline.d.ts +2 -3
  66. package/dist/primitives/elicitation/McpElicitationPrimitiveDecline.d.ts.map +1 -1
  67. package/dist/primitives/elicitation/McpElicitationPrimitiveError.d.ts +2 -3
  68. package/dist/primitives/elicitation/McpElicitationPrimitiveError.d.ts.map +1 -1
  69. package/dist/primitives/elicitation/McpElicitationPrimitiveFields.d.ts +3 -3
  70. package/dist/primitives/elicitation/McpElicitationPrimitiveFields.d.ts.map +1 -1
  71. package/dist/primitives/elicitation/McpElicitationPrimitiveItems.d.ts +3 -3
  72. package/dist/primitives/elicitation/McpElicitationPrimitiveItems.d.ts.map +1 -1
  73. package/dist/primitives/elicitation/McpElicitationPrimitiveMessage.d.ts +2 -3
  74. package/dist/primitives/elicitation/McpElicitationPrimitiveMessage.d.ts.map +1 -1
  75. package/dist/primitives/elicitation/McpElicitationPrimitiveRoot.d.ts +2 -3
  76. package/dist/primitives/elicitation/McpElicitationPrimitiveRoot.d.ts.map +1 -1
  77. package/dist/primitives/elicitation/context.d.ts +7 -8
  78. package/dist/primitives/elicitation/context.d.ts.map +1 -1
  79. package/dist/primitives/elicitation/initialElicitationDraft.d.ts +1 -2
  80. package/dist/primitives/elicitation/initialElicitationDraft.d.ts.map +1 -1
  81. package/dist/primitives/elicitation/prepareElicitationContent.d.ts +1 -2
  82. package/dist/primitives/elicitation/prepareElicitationContent.d.ts.map +1 -1
  83. package/dist/primitives/elicitation/prepareElicitationContent.js +5 -1
  84. package/dist/primitives/elicitation/prepareElicitationContent.js.map +1 -1
  85. package/dist/primitives/manager/McpManagerAddCustomTrigger.d.ts +2 -3
  86. package/dist/primitives/manager/McpManagerAddCustomTrigger.d.ts.map +1 -1
  87. package/dist/primitives/manager/McpManagerConnectors.d.ts +2 -3
  88. package/dist/primitives/manager/McpManagerConnectors.d.ts.map +1 -1
  89. package/dist/primitives/manager/McpManagerCustomServers.d.ts +2 -3
  90. package/dist/primitives/manager/McpManagerCustomServers.d.ts.map +1 -1
  91. package/dist/primitives/manager/McpManagerRoot.d.ts +2 -3
  92. package/dist/primitives/manager/McpManagerRoot.d.ts.map +1 -1
  93. package/dist/primitives/server/McpServerConnectButton.d.ts +2 -3
  94. package/dist/primitives/server/McpServerConnectButton.d.ts.map +1 -1
  95. package/dist/primitives/server/McpServerDisconnectButton.d.ts +2 -3
  96. package/dist/primitives/server/McpServerDisconnectButton.d.ts.map +1 -1
  97. package/dist/primitives/server/McpServerError.d.ts +2 -3
  98. package/dist/primitives/server/McpServerError.d.ts.map +1 -1
  99. package/dist/primitives/server/McpServerIcon.d.ts +2 -3
  100. package/dist/primitives/server/McpServerIcon.d.ts.map +1 -1
  101. package/dist/primitives/server/McpServerName.d.ts +2 -3
  102. package/dist/primitives/server/McpServerName.d.ts.map +1 -1
  103. package/dist/primitives/server/McpServerOAuthLink.d.ts +2 -3
  104. package/dist/primitives/server/McpServerOAuthLink.d.ts.map +1 -1
  105. package/dist/primitives/server/McpServerRemoveButton.d.ts +2 -3
  106. package/dist/primitives/server/McpServerRemoveButton.d.ts.map +1 -1
  107. package/dist/primitives/server/McpServerRoot.d.ts +2 -3
  108. package/dist/primitives/server/McpServerRoot.d.ts.map +1 -1
  109. package/dist/primitives/server/McpServerStatus.d.ts +2 -3
  110. package/dist/primitives/server/McpServerStatus.d.ts.map +1 -1
  111. package/dist/primitives/server/McpServerToolName.d.ts +2 -3
  112. package/dist/primitives/server/McpServerToolName.d.ts.map +1 -1
  113. package/dist/primitives/server/McpServerTools.d.ts +3 -4
  114. package/dist/primitives/server/McpServerTools.d.ts.map +1 -1
  115. package/dist/resources/McpManagerResource.d.ts +2 -3
  116. package/dist/resources/McpManagerResource.d.ts.map +1 -1
  117. package/dist/resources/McpManagerResource.js +358 -244
  118. package/dist/resources/McpManagerResource.js.map +1 -1
  119. package/dist/resources/McpServerRemovalFence.d.ts +6 -0
  120. package/dist/resources/McpServerRemovalFence.d.ts.map +1 -0
  121. package/dist/resources/McpServerRemovalFence.js +11 -0
  122. package/dist/resources/McpServerRemovalFence.js.map +1 -0
  123. package/dist/resources/McpServerResource.d.ts +3 -4
  124. package/dist/resources/McpServerResource.d.ts.map +1 -1
  125. package/dist/resources/McpServerResource.js +9 -5
  126. package/dist/resources/McpServerResource.js.map +1 -1
  127. package/dist/resources/storage/McpCustomStorage.d.ts +1 -2
  128. package/dist/resources/storage/McpCustomStorage.d.ts.map +1 -1
  129. package/dist/resources/storage/McpLocalStorage.d.ts +4 -5
  130. package/dist/resources/storage/McpLocalStorage.d.ts.map +1 -1
  131. package/dist/resources/storage/McpLocalStorage.js +2 -1
  132. package/dist/resources/storage/McpLocalStorage.js.map +1 -1
  133. package/dist/resources/storage/McpMemoryStorage.d.ts +1 -2
  134. package/dist/resources/storage/McpMemoryStorage.d.ts.map +1 -1
  135. package/dist/resources/storage/types.d.ts +12 -11
  136. package/dist/resources/storage/types.d.ts.map +1 -1
  137. package/dist/resources/validateElicitationContent.d.ts +1 -2
  138. package/dist/resources/validateElicitationContent.d.ts.map +1 -1
  139. package/dist/utils/createMcpId.d.ts +1 -2
  140. package/dist/utils/createMcpId.d.ts.map +1 -1
  141. package/dist/utils/invokeMcpCallback.d.ts +1 -2
  142. package/dist/utils/invokeMcpCallback.d.ts.map +1 -1
  143. package/dist/utils/serverId.d.ts +2 -3
  144. package/dist/utils/serverId.d.ts.map +1 -1
  145. package/dist/utils/serverUrl.d.ts +3 -4
  146. package/dist/utils/serverUrl.d.ts.map +1 -1
  147. package/package.json +11 -10
  148. package/src/hooks/useMcpOAuthCallback.test.ts +53 -0
  149. package/src/hooks/useMcpOAuthCallback.tsx +12 -0
  150. package/src/index.ts +2 -1
  151. package/src/primitives/addForm/McpAddFormAccessibility.test.tsx +310 -0
  152. package/src/primitives/addForm/McpAddFormAuthFields.tsx +26 -16
  153. package/src/primitives/addForm/McpAddFormBearerTokenField.tsx +46 -0
  154. package/src/primitives/addForm/McpAddFormError.tsx +17 -3
  155. package/src/primitives/addForm/McpAddFormNameField.tsx +13 -1
  156. package/src/primitives/addForm/McpAddFormRoot.test.tsx +65 -0
  157. package/src/primitives/addForm/McpAddFormRoot.tsx +51 -8
  158. package/src/primitives/addForm/McpAddFormScopesField.tsx +34 -0
  159. package/src/primitives/addForm/McpAddFormUrlField.tsx +12 -1
  160. package/src/primitives/addForm/context.tsx +10 -0
  161. package/src/primitives/addForm.ts +2 -0
  162. package/src/primitives/elicitation/prepareElicitationContent.ts +8 -6
  163. package/src/resources/McpManagerResource.test.ts +797 -9
  164. package/src/resources/McpManagerResource.ts +242 -60
  165. package/src/resources/McpServerRemovalFence.ts +21 -0
  166. package/src/resources/McpServerResource.test.ts +9 -6
  167. package/src/resources/McpServerResource.ts +11 -6
  168. package/src/resources/storage/McpLocalStorage.test.ts +2 -0
  169. package/src/resources/storage/McpLocalStorage.ts +2 -1
  170. package/src/resources/storage/types.ts +10 -8
@@ -2,7 +2,7 @@ import { createTapRoot, resource, useResource } from "@assistant-ui/tap";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import { useState } from "react";
4
4
  import { defineConnector } from "../connector";
5
- import type { MCPConnector } from "../mcp-scope";
5
+ import type { MCPConnector, MCPCustomServerRecord } from "../mcp-scope";
6
6
  import { assertUniqueServerIds } from "../utils/serverId";
7
7
  import { McpManagerResource } from "./McpManagerResource";
8
8
  import { McpCustomStorage } from "./storage/McpCustomStorage";
@@ -92,6 +92,51 @@ describe("McpManagerResource server ids", () => {
92
92
  expect(() => assertUniqueServerIds(["docs", "linear"])).not.toThrow();
93
93
  });
94
94
 
95
+ it("keeps the first persisted custom server when ids are duplicated", async () => {
96
+ const docsServer: MCPCustomServerRecord = {
97
+ id: "docs",
98
+ name: "Docs",
99
+ url: "https://example.com/docs/mcp",
100
+ auth: { type: "none" },
101
+ createdAt: 1,
102
+ };
103
+ const saveCustomServers = vi.fn(async () => {});
104
+ const consoleError = vi
105
+ .spyOn(console, "error")
106
+ .mockImplementation(() => {});
107
+ const root = mount(
108
+ [],
109
+ McpCustomStorage({
110
+ loadCustomServers: vi.fn(async () => [
111
+ docsServer,
112
+ { ...docsServer, name: "Duplicate Docs", createdAt: 2 },
113
+ ]),
114
+ saveCustomServers,
115
+ loadAuthState: vi.fn(async () => null),
116
+ saveAuthState: vi.fn(async () => {}),
117
+ clearAuthState: vi.fn(async () => {}),
118
+ }),
119
+ );
120
+
121
+ try {
122
+ await vi.waitFor(() =>
123
+ expect(root.getValue().getState().isHydrated).toBe(true),
124
+ );
125
+ expect(root.getValue().getState().customServers).toHaveLength(1);
126
+ expect(root.getValue().getState().customServers[0]).toMatchObject({
127
+ id: "docs",
128
+ name: "Docs",
129
+ });
130
+ expect(saveCustomServers).not.toHaveBeenCalled();
131
+ expect(consoleError).toHaveBeenCalledWith(
132
+ '[assistant-ui/react-mcp] ignored duplicate custom server id "docs" loaded from storage',
133
+ );
134
+ } finally {
135
+ consoleError.mockRestore();
136
+ root.unmount();
137
+ }
138
+ });
139
+
95
140
  it("passes connector cache configuration to its client", async () => {
96
141
  mocks.Client.mockClear();
97
142
  const root = mount([
@@ -278,18 +323,19 @@ describe("McpManagerResource server ids", () => {
278
323
  });
279
324
 
280
325
  describe("McpManagerResource storage failures", () => {
281
- it("handles custom server load failures", async () => {
326
+ it("keeps persistence fenced after custom server load failures", async () => {
282
327
  const error = new Error("load failed");
283
328
  const consoleError = vi
284
329
  .spyOn(console, "error")
285
330
  .mockImplementation(() => {});
331
+ const saveCustomServers = vi.fn(async () => {});
286
332
  const root = mount(
287
333
  [],
288
334
  McpCustomStorage({
289
335
  loadCustomServers: vi.fn(async () => {
290
336
  throw error;
291
337
  }),
292
- saveCustomServers: vi.fn(async () => {}),
338
+ saveCustomServers,
293
339
  loadAuthState: vi.fn(async () => null),
294
340
  saveAuthState: vi.fn(async () => {}),
295
341
  clearAuthState: vi.fn(async () => {}),
@@ -305,6 +351,20 @@ describe("McpManagerResource storage failures", () => {
305
351
  "[assistant-ui/react-mcp] failed to load custom servers:",
306
352
  error,
307
353
  );
354
+
355
+ await root.getValue().addCustomServer({
356
+ name: "Docs",
357
+ url: "https://example.com/docs/mcp",
358
+ auth: { type: "none" },
359
+ });
360
+ await new Promise((resolve) => setTimeout(resolve, 0));
361
+
362
+ expect(root.getValue().getState().customServers).toHaveLength(1);
363
+ expect(saveCustomServers).not.toHaveBeenCalled();
364
+ expect(consoleError).toHaveBeenCalledWith(
365
+ "[assistant-ui/react-mcp] custom server changes remain in memory because loading the persisted list failed; remount the manager to retry",
366
+ );
367
+ expect(consoleError).toHaveBeenCalledTimes(2);
308
368
  } finally {
309
369
  root.unmount();
310
370
  consoleError.mockRestore();
@@ -332,8 +392,7 @@ describe("McpManagerResource storage failures", () => {
332
392
  await vi.waitFor(() =>
333
393
  expect(root.getValue().getState().isHydrated).toBe(true),
334
394
  );
335
- await vi.waitFor(() => expect(saveCustomServers).toHaveBeenCalled());
336
- saveCustomServers.mockClear();
395
+ expect(saveCustomServers).not.toHaveBeenCalled();
337
396
  saveCustomServers.mockRejectedValue(error);
338
397
 
339
398
  await root.getValue().addCustomServer({
@@ -360,6 +419,46 @@ describe("McpManagerResource storage failures", () => {
360
419
  });
361
420
 
362
421
  describe("McpManagerResource storage ordering", () => {
422
+ it("preserves a removal made before custom server hydration finishes", async () => {
423
+ const docsServer: MCPCustomServerRecord = {
424
+ id: "docs",
425
+ name: "Docs",
426
+ url: "https://example.com/docs/mcp",
427
+ auth: { type: "none" },
428
+ createdAt: 1,
429
+ };
430
+ let resolveLoad!: (records: MCPCustomServerRecord[]) => void;
431
+ const load = new Promise<MCPCustomServerRecord[]>((resolve) => {
432
+ resolveLoad = resolve;
433
+ });
434
+ const saveCustomServers = vi.fn(async () => {});
435
+ const root = mount(
436
+ [],
437
+ McpCustomStorage({
438
+ loadCustomServers: vi.fn(() => load),
439
+ saveCustomServers,
440
+ loadAuthState: vi.fn(async () => null),
441
+ saveAuthState: vi.fn(async () => {}),
442
+ clearAuthState: vi.fn(async () => {}),
443
+ }),
444
+ );
445
+
446
+ try {
447
+ await root.getValue().removeServer("docs");
448
+ resolveLoad([docsServer]);
449
+
450
+ await vi.waitFor(() =>
451
+ expect(root.getValue().getState().isHydrated).toBe(true),
452
+ );
453
+ expect(root.getValue().getState().customServers).toHaveLength(0);
454
+ await vi.waitFor(() =>
455
+ expect(saveCustomServers).toHaveBeenCalledWith([]),
456
+ );
457
+ } finally {
458
+ root.unmount();
459
+ }
460
+ });
461
+
363
462
  it("persists custom server updates in invocation order", async () => {
364
463
  let resolveFirstSave: (() => void) | undefined;
365
464
  const firstSave = new Promise<void>((resolve) => {
@@ -389,8 +488,7 @@ describe("McpManagerResource storage ordering", () => {
389
488
  await vi.waitFor(() =>
390
489
  expect(root.getValue().getState().isHydrated).toBe(true),
391
490
  );
392
- await vi.waitFor(() => expect(saveCustomServers).toHaveBeenCalled());
393
- saveCustomServers.mockClear();
491
+ expect(saveCustomServers).not.toHaveBeenCalled();
394
492
  persistedSnapshots.length = 0;
395
493
  blockNextSave = true;
396
494
 
@@ -452,8 +550,7 @@ describe("McpManagerResource storage ordering", () => {
452
550
  await vi.waitFor(() =>
453
551
  expect(root.getValue().getState().isHydrated).toBe(true),
454
552
  );
455
- await vi.waitFor(() => expect(saveCustomServers).toHaveBeenCalled());
456
- saveCustomServers.mockClear();
553
+ expect(saveCustomServers).not.toHaveBeenCalled();
457
554
 
458
555
  rerender();
459
556
 
@@ -464,3 +561,694 @@ describe("McpManagerResource storage ordering", () => {
464
561
  }
465
562
  });
466
563
  });
564
+
565
+ describe("McpManagerResource storage switching", () => {
566
+ const docsServer = {
567
+ id: "docs",
568
+ name: "Docs",
569
+ url: "https://example.com/docs/mcp",
570
+ auth: { type: "none" as const },
571
+ createdAt: 1,
572
+ };
573
+
574
+ it("rehydrates custom servers without copying records from the previous scope", async () => {
575
+ let resolveStorageB:
576
+ | ((records: MCPCustomServerRecord[]) => void)
577
+ | undefined;
578
+ const storageBLoad = new Promise<MCPCustomServerRecord[]>((resolve) => {
579
+ resolveStorageB = resolve;
580
+ });
581
+ const saveStorageB = vi.fn(async (_records: MCPCustomServerRecord[]) => {});
582
+ const storageA = {
583
+ scopeId: "account:a",
584
+ loadCustomServers: vi.fn(async () => [docsServer]),
585
+ saveCustomServers: vi.fn(async () => {}),
586
+ loadAuthState: vi.fn(async () => null),
587
+ saveAuthState: vi.fn(async () => {}),
588
+ clearAuthState: vi.fn(async () => {}),
589
+ };
590
+ const storageB = {
591
+ scopeId: "account:b",
592
+ loadCustomServers: vi.fn(() => storageBLoad),
593
+ saveCustomServers: saveStorageB,
594
+ loadAuthState: vi.fn(async () => null),
595
+ saveAuthState: vi.fn(async () => {}),
596
+ clearAuthState: vi.fn(async () => {}),
597
+ };
598
+ let switchStorage = () => {};
599
+ const DynamicManager = resource(function useDynamicManager() {
600
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
601
+ switchStorage = () => setStorageKey("b");
602
+ return useResource(
603
+ McpManagerResource({
604
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
605
+ autoConnect: false,
606
+ }),
607
+ );
608
+ });
609
+ const root = createTapRoot(function Root() {
610
+ return useResource(DynamicManager());
611
+ });
612
+
613
+ try {
614
+ await vi.waitFor(() =>
615
+ expect(root.getValue().getState().customServers[0]?.id).toBe("docs"),
616
+ );
617
+ expect(storageA.saveCustomServers).not.toHaveBeenCalled();
618
+
619
+ switchStorage();
620
+
621
+ await vi.waitFor(() =>
622
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
623
+ );
624
+ expect(root.getValue().getState()).toMatchObject({
625
+ isHydrated: false,
626
+ customServers: [],
627
+ });
628
+
629
+ resolveStorageB?.([]);
630
+ await vi.waitFor(() => {
631
+ expect(root.getValue().getState().isHydrated).toBe(true);
632
+ expect(root.getValue().getState().customServers).toHaveLength(0);
633
+ });
634
+ expect(saveStorageB).not.toHaveBeenCalled();
635
+
636
+ await root.getValue().addCustomServer({
637
+ name: "Linear",
638
+ url: "https://example.com/linear/mcp",
639
+ auth: { type: "none" },
640
+ });
641
+
642
+ await vi.waitFor(() => expect(saveStorageB).toHaveBeenCalledOnce());
643
+ expect(saveStorageB).toHaveBeenCalledWith([
644
+ expect.objectContaining({ name: "Linear" }),
645
+ ]);
646
+ } finally {
647
+ resolveStorageB?.([]);
648
+ root.unmount();
649
+ }
650
+ });
651
+
652
+ it("keeps custom servers when the storage object retains its scope", async () => {
653
+ const storageA = {
654
+ scopeId: "account:a",
655
+ loadCustomServers: vi.fn(async () => [docsServer]),
656
+ saveCustomServers: vi.fn(async () => {}),
657
+ loadAuthState: vi.fn(async () => null),
658
+ saveAuthState: vi.fn(async () => {}),
659
+ clearAuthState: vi.fn(async () => {}),
660
+ };
661
+ const storageAReplacement = {
662
+ scopeId: "account:a",
663
+ loadCustomServers: vi.fn(async () => []),
664
+ saveCustomServers: vi.fn(async () => {}),
665
+ loadAuthState: vi.fn(async () => null),
666
+ saveAuthState: vi.fn(async () => {}),
667
+ clearAuthState: vi.fn(async () => {}),
668
+ };
669
+ let replaceStorage = () => {};
670
+ const DynamicManager = resource(function useDynamicManager() {
671
+ const [replacement, setReplacement] = useState(false);
672
+ replaceStorage = () => setReplacement(true);
673
+ return useResource(
674
+ McpManagerResource({
675
+ storage: McpCustomStorage(
676
+ replacement ? storageAReplacement : storageA,
677
+ ),
678
+ autoConnect: false,
679
+ }),
680
+ );
681
+ });
682
+ const root = createTapRoot(function Root() {
683
+ return useResource(DynamicManager());
684
+ });
685
+
686
+ try {
687
+ await vi.waitFor(() =>
688
+ expect(root.getValue().getState().customServers[0]?.id).toBe("docs"),
689
+ );
690
+
691
+ replaceStorage();
692
+ await new Promise((resolve) => setTimeout(resolve, 0));
693
+
694
+ expect(storageAReplacement.loadCustomServers).not.toHaveBeenCalled();
695
+ expect(root.getValue().getState().customServers[0]?.id).toBe("docs");
696
+ } finally {
697
+ root.unmount();
698
+ }
699
+ });
700
+
701
+ it("keeps persistence independent across storage scopes", async () => {
702
+ let resolveStorageASave: (() => void) | undefined;
703
+ const pendingStorageASave = new Promise<void>((resolve) => {
704
+ resolveStorageASave = resolve;
705
+ });
706
+ let blockStorageASave = false;
707
+ const storageA = {
708
+ scopeId: "account:a",
709
+ loadCustomServers: vi.fn(async () => []),
710
+ saveCustomServers: vi.fn(async () => {
711
+ if (blockStorageASave) await pendingStorageASave;
712
+ }),
713
+ loadAuthState: vi.fn(async () => null),
714
+ saveAuthState: vi.fn(async () => {}),
715
+ clearAuthState: vi.fn(async () => {}),
716
+ };
717
+ const storageB = {
718
+ scopeId: "account:b",
719
+ loadCustomServers: vi.fn(async () => []),
720
+ saveCustomServers: vi.fn(async () => {}),
721
+ loadAuthState: vi.fn(async () => null),
722
+ saveAuthState: vi.fn(async () => {}),
723
+ clearAuthState: vi.fn(async () => {}),
724
+ };
725
+ let switchStorage = () => {};
726
+ const DynamicManager = resource(function useDynamicManager() {
727
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
728
+ switchStorage = () => setStorageKey("b");
729
+ return useResource(
730
+ McpManagerResource({
731
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
732
+ autoConnect: false,
733
+ }),
734
+ );
735
+ });
736
+ const root = createTapRoot(function Root() {
737
+ return useResource(DynamicManager());
738
+ });
739
+
740
+ try {
741
+ await vi.waitFor(() =>
742
+ expect(root.getValue().getState().isHydrated).toBe(true),
743
+ );
744
+ expect(storageA.saveCustomServers).not.toHaveBeenCalled();
745
+ blockStorageASave = true;
746
+
747
+ await root.getValue().addCustomServer({
748
+ name: "Docs",
749
+ url: "https://example.com/docs/mcp",
750
+ auth: { type: "none" },
751
+ });
752
+ await vi.waitFor(() =>
753
+ expect(storageA.saveCustomServers).toHaveBeenCalledOnce(),
754
+ );
755
+
756
+ switchStorage();
757
+
758
+ await vi.waitFor(() =>
759
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
760
+ );
761
+ await vi.waitFor(() =>
762
+ expect(root.getValue().getState().isHydrated).toBe(true),
763
+ );
764
+ await root.getValue().addCustomServer({
765
+ name: "Linear",
766
+ url: "https://example.com/linear/mcp",
767
+ auth: { type: "none" },
768
+ });
769
+
770
+ await vi.waitFor(() =>
771
+ expect(storageB.saveCustomServers).toHaveBeenCalledWith([
772
+ expect.objectContaining({ name: "Linear" }),
773
+ ]),
774
+ );
775
+ expect(storageA.saveCustomServers).toHaveBeenCalledOnce();
776
+ } finally {
777
+ resolveStorageASave?.();
778
+ root.unmount();
779
+ }
780
+ });
781
+
782
+ it("queues a hydrated mutation before an immediate scope switch", async () => {
783
+ const storageA = {
784
+ scopeId: "account:a",
785
+ loadCustomServers: vi.fn(async () => []),
786
+ saveCustomServers: vi.fn(async () => {}),
787
+ loadAuthState: vi.fn(async () => null),
788
+ saveAuthState: vi.fn(async () => {}),
789
+ clearAuthState: vi.fn(async () => {}),
790
+ };
791
+ const storageB = {
792
+ scopeId: "account:b",
793
+ loadCustomServers: vi.fn(async () => []),
794
+ saveCustomServers: vi.fn(async () => {}),
795
+ loadAuthState: vi.fn(async () => null),
796
+ saveAuthState: vi.fn(async () => {}),
797
+ clearAuthState: vi.fn(async () => {}),
798
+ };
799
+ let switchStorage = () => {};
800
+ const DynamicManager = resource(function useDynamicManager() {
801
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
802
+ switchStorage = () => setStorageKey("b");
803
+ return useResource(
804
+ McpManagerResource({
805
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
806
+ autoConnect: false,
807
+ }),
808
+ );
809
+ });
810
+ const root = createTapRoot(function Root() {
811
+ return useResource(DynamicManager());
812
+ });
813
+
814
+ try {
815
+ await vi.waitFor(() =>
816
+ expect(root.getValue().getState().isHydrated).toBe(true),
817
+ );
818
+
819
+ await root.getValue().addCustomServer({
820
+ name: "Docs",
821
+ url: "https://example.com/docs/mcp",
822
+ auth: { type: "none" },
823
+ });
824
+ switchStorage();
825
+
826
+ await vi.waitFor(() =>
827
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
828
+ );
829
+ await vi.waitFor(() =>
830
+ expect(storageA.saveCustomServers).toHaveBeenCalledWith([
831
+ expect.objectContaining({ name: "Docs" }),
832
+ ]),
833
+ );
834
+ expect(storageB.saveCustomServers).not.toHaveBeenCalled();
835
+ } finally {
836
+ root.unmount();
837
+ }
838
+ });
839
+
840
+ it("waits for pending persistence before revisiting a scope", async () => {
841
+ let resolveStorageASave: (() => void) | undefined;
842
+ const pendingStorageASave = new Promise<void>((resolve) => {
843
+ resolveStorageASave = resolve;
844
+ });
845
+ let persistedStorageARecords: MCPCustomServerRecord[] = [];
846
+ let blockStorageASave = false;
847
+ const storageA = {
848
+ scopeId: "account:a",
849
+ loadCustomServers: vi.fn(async () => [...persistedStorageARecords]),
850
+ saveCustomServers: vi.fn(async (records: MCPCustomServerRecord[]) => {
851
+ if (blockStorageASave) {
852
+ blockStorageASave = false;
853
+ await pendingStorageASave;
854
+ }
855
+ persistedStorageARecords = records;
856
+ }),
857
+ loadAuthState: vi.fn(async () => null),
858
+ saveAuthState: vi.fn(async () => {}),
859
+ clearAuthState: vi.fn(async () => {}),
860
+ };
861
+ const storageB = {
862
+ scopeId: "account:b",
863
+ loadCustomServers: vi.fn(async () => []),
864
+ saveCustomServers: vi.fn(async () => {}),
865
+ loadAuthState: vi.fn(async () => null),
866
+ saveAuthState: vi.fn(async () => {}),
867
+ clearAuthState: vi.fn(async () => {}),
868
+ };
869
+ let setStorage = (_storageKey: "a" | "b") => {};
870
+ const DynamicManager = resource(function useDynamicManager() {
871
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
872
+ setStorage = setStorageKey;
873
+ return useResource(
874
+ McpManagerResource({
875
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
876
+ autoConnect: false,
877
+ }),
878
+ );
879
+ });
880
+ const root = createTapRoot(function Root() {
881
+ return useResource(DynamicManager());
882
+ });
883
+
884
+ try {
885
+ await vi.waitFor(() =>
886
+ expect(root.getValue().getState().isHydrated).toBe(true),
887
+ );
888
+ expect(storageA.saveCustomServers).not.toHaveBeenCalled();
889
+ blockStorageASave = true;
890
+
891
+ await root.getValue().addCustomServer({
892
+ name: "Docs",
893
+ url: "https://example.com/docs/mcp",
894
+ auth: { type: "none" },
895
+ });
896
+ await vi.waitFor(() =>
897
+ expect(storageA.saveCustomServers).toHaveBeenCalledOnce(),
898
+ );
899
+
900
+ setStorage("b");
901
+ await vi.waitFor(() =>
902
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
903
+ );
904
+ const storageALoadCount = storageA.loadCustomServers.mock.calls.length;
905
+
906
+ setStorage("a");
907
+ await new Promise((resolve) => setTimeout(resolve, 0));
908
+ expect(storageA.loadCustomServers).toHaveBeenCalledTimes(
909
+ storageALoadCount,
910
+ );
911
+
912
+ resolveStorageASave?.();
913
+ await vi.waitFor(() =>
914
+ expect(storageA.loadCustomServers.mock.calls.length).toBeGreaterThan(
915
+ storageALoadCount,
916
+ ),
917
+ );
918
+ await vi.waitFor(() =>
919
+ expect(root.getValue().getState().customServers[0]?.name).toBe("Docs"),
920
+ );
921
+ } finally {
922
+ resolveStorageASave?.();
923
+ root.unmount();
924
+ }
925
+ });
926
+
927
+ it("ignores hydration that resolves after changing scopes", async () => {
928
+ let resolveStorageA: ((records: (typeof docsServer)[]) => void) | undefined;
929
+ const storageALoad = new Promise<(typeof docsServer)[]>((resolve) => {
930
+ resolveStorageA = resolve;
931
+ });
932
+ const storageA = {
933
+ scopeId: "account:a",
934
+ loadCustomServers: vi.fn(() => storageALoad),
935
+ saveCustomServers: vi.fn(async () => {}),
936
+ loadAuthState: vi.fn(async () => null),
937
+ saveAuthState: vi.fn(async () => {}),
938
+ clearAuthState: vi.fn(async () => {}),
939
+ };
940
+ const storageB = {
941
+ scopeId: "account:b",
942
+ loadCustomServers: vi.fn(async () => []),
943
+ saveCustomServers: vi.fn(async () => {}),
944
+ loadAuthState: vi.fn(async () => null),
945
+ saveAuthState: vi.fn(async () => {}),
946
+ clearAuthState: vi.fn(async () => {}),
947
+ };
948
+ let switchStorage = () => {};
949
+ const DynamicManager = resource(function useDynamicManager() {
950
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
951
+ switchStorage = () => setStorageKey("b");
952
+ return useResource(
953
+ McpManagerResource({
954
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
955
+ autoConnect: false,
956
+ }),
957
+ );
958
+ });
959
+ const root = createTapRoot(function Root() {
960
+ return useResource(DynamicManager());
961
+ });
962
+
963
+ try {
964
+ await vi.waitFor(() =>
965
+ expect(storageA.loadCustomServers).toHaveBeenCalled(),
966
+ );
967
+ switchStorage();
968
+ await vi.waitFor(() =>
969
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
970
+ );
971
+
972
+ resolveStorageA?.([docsServer]);
973
+
974
+ await vi.waitFor(() =>
975
+ expect(root.getValue().getState().isHydrated).toBe(true),
976
+ );
977
+ expect(root.getValue().getState().customServers).toHaveLength(0);
978
+ } finally {
979
+ resolveStorageA?.([]);
980
+ root.unmount();
981
+ }
982
+ });
983
+
984
+ it("persists a pre-hydration mutation after its load resolves post-switch", async () => {
985
+ let resolveStorageA:
986
+ | ((records: MCPCustomServerRecord[]) => void)
987
+ | undefined;
988
+ const storageALoad = new Promise<MCPCustomServerRecord[]>((resolve) => {
989
+ resolveStorageA = resolve;
990
+ });
991
+ const storageA = {
992
+ scopeId: "account:a",
993
+ loadCustomServers: vi.fn(() => storageALoad),
994
+ saveCustomServers: vi.fn(async () => {}),
995
+ loadAuthState: vi.fn(async () => null),
996
+ saveAuthState: vi.fn(async () => {}),
997
+ clearAuthState: vi.fn(async () => {}),
998
+ };
999
+ const storageB = {
1000
+ scopeId: "account:b",
1001
+ loadCustomServers: vi.fn(async () => []),
1002
+ saveCustomServers: vi.fn(async () => {}),
1003
+ loadAuthState: vi.fn(async () => null),
1004
+ saveAuthState: vi.fn(async () => {}),
1005
+ clearAuthState: vi.fn(async () => {}),
1006
+ };
1007
+ let switchStorage = () => {};
1008
+ const DynamicManager = resource(function useDynamicManager() {
1009
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
1010
+ switchStorage = () => setStorageKey("b");
1011
+ return useResource(
1012
+ McpManagerResource({
1013
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
1014
+ autoConnect: false,
1015
+ }),
1016
+ );
1017
+ });
1018
+ const root = createTapRoot(function Root() {
1019
+ return useResource(DynamicManager());
1020
+ });
1021
+
1022
+ try {
1023
+ await vi.waitFor(() =>
1024
+ expect(storageA.loadCustomServers).toHaveBeenCalled(),
1025
+ );
1026
+ expect(root.getValue().getState().isHydrated).toBe(false);
1027
+
1028
+ await root.getValue().addCustomServer({
1029
+ name: "Docs",
1030
+ url: "https://example.com/docs/mcp",
1031
+ auth: { type: "none" },
1032
+ });
1033
+ switchStorage();
1034
+
1035
+ await vi.waitFor(() =>
1036
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
1037
+ );
1038
+ resolveStorageA?.([]);
1039
+
1040
+ await vi.waitFor(() =>
1041
+ expect(storageA.saveCustomServers).toHaveBeenCalledWith([
1042
+ expect.objectContaining({ name: "Docs" }),
1043
+ ]),
1044
+ );
1045
+ expect(storageB.saveCustomServers).not.toHaveBeenCalled();
1046
+ } finally {
1047
+ resolveStorageA?.([]);
1048
+ root.unmount();
1049
+ }
1050
+ });
1051
+
1052
+ it("does not let a delayed removal affect the replacement scope", async () => {
1053
+ const workspaceBDocsServer = {
1054
+ ...docsServer,
1055
+ name: "Workspace B Docs",
1056
+ url: "https://example.com/workspace-b/docs/mcp",
1057
+ createdAt: 2,
1058
+ };
1059
+ let resolveStorageAClear: (() => void) | undefined;
1060
+ const storageAClear = new Promise<void>((resolve) => {
1061
+ resolveStorageAClear = resolve;
1062
+ });
1063
+ const storageA = {
1064
+ scopeId: "account:a",
1065
+ loadCustomServers: vi.fn(async () => [docsServer]),
1066
+ saveCustomServers: vi.fn(async () => {}),
1067
+ loadAuthState: vi.fn(async () => null),
1068
+ saveAuthState: vi.fn(async () => {}),
1069
+ clearAuthState: vi.fn(() => storageAClear),
1070
+ };
1071
+ const storageB = {
1072
+ scopeId: "account:b",
1073
+ loadCustomServers: vi.fn(async () => [workspaceBDocsServer]),
1074
+ saveCustomServers: vi.fn(async () => {}),
1075
+ loadAuthState: vi.fn(async () => null),
1076
+ saveAuthState: vi.fn(async () => {}),
1077
+ clearAuthState: vi.fn(async () => {}),
1078
+ };
1079
+ let switchStorage = () => {};
1080
+ const DynamicManager = resource(function useDynamicManager() {
1081
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
1082
+ switchStorage = () => setStorageKey("b");
1083
+ return useResource(
1084
+ McpManagerResource({
1085
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
1086
+ autoConnect: false,
1087
+ }),
1088
+ );
1089
+ });
1090
+ const root = createTapRoot(function Root() {
1091
+ return useResource(DynamicManager());
1092
+ });
1093
+
1094
+ try {
1095
+ await vi.waitFor(() =>
1096
+ expect(root.getValue().getState().customServers[0]?.id).toBe("docs"),
1097
+ );
1098
+ const removal = root.getValue().server({ id: "docs" }).remove();
1099
+ await vi.waitFor(() =>
1100
+ expect(storageA.clearAuthState).toHaveBeenCalledWith("docs"),
1101
+ );
1102
+
1103
+ switchStorage();
1104
+
1105
+ await vi.waitFor(() =>
1106
+ expect(root.getValue().getState().customServers[0]?.name).toBe(
1107
+ "Workspace B Docs",
1108
+ ),
1109
+ );
1110
+ resolveStorageAClear?.();
1111
+ await removal;
1112
+
1113
+ expect(root.getValue().getState().customServers).toEqual([
1114
+ expect.objectContaining({
1115
+ id: "docs",
1116
+ name: "Workspace B Docs",
1117
+ }),
1118
+ ]);
1119
+ } finally {
1120
+ resolveStorageAClear?.();
1121
+ root.unmount();
1122
+ }
1123
+ });
1124
+
1125
+ it("waits for an in-flight removal before rehydrating a revisited scope", async () => {
1126
+ let persistedStorageARecords: MCPCustomServerRecord[] = [docsServer];
1127
+ let resolveStorageAClear: (() => void) | undefined;
1128
+ const storageAClear = new Promise<void>((resolve) => {
1129
+ resolveStorageAClear = resolve;
1130
+ });
1131
+ const storageA = {
1132
+ scopeId: "account:a",
1133
+ loadCustomServers: vi.fn(async () => [...persistedStorageARecords]),
1134
+ saveCustomServers: vi.fn(async (records: MCPCustomServerRecord[]) => {
1135
+ persistedStorageARecords = records;
1136
+ }),
1137
+ loadAuthState: vi.fn(async () => null),
1138
+ saveAuthState: vi.fn(async () => {}),
1139
+ clearAuthState: vi.fn(() => storageAClear),
1140
+ };
1141
+ const storageB = {
1142
+ scopeId: "account:b",
1143
+ loadCustomServers: vi.fn(async () => []),
1144
+ saveCustomServers: vi.fn(async () => {}),
1145
+ loadAuthState: vi.fn(async () => null),
1146
+ saveAuthState: vi.fn(async () => {}),
1147
+ clearAuthState: vi.fn(async () => {}),
1148
+ };
1149
+ let setStorage = (_storageKey: "a" | "b") => {};
1150
+ const DynamicManager = resource(function useDynamicManager() {
1151
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
1152
+ setStorage = setStorageKey;
1153
+ return useResource(
1154
+ McpManagerResource({
1155
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
1156
+ autoConnect: false,
1157
+ }),
1158
+ );
1159
+ });
1160
+ const root = createTapRoot(function Root() {
1161
+ return useResource(DynamicManager());
1162
+ });
1163
+
1164
+ try {
1165
+ await vi.waitFor(() =>
1166
+ expect(root.getValue().getState().customServers[0]?.id).toBe("docs"),
1167
+ );
1168
+ const removal = root.getValue().server({ id: "docs" }).remove();
1169
+ await vi.waitFor(() =>
1170
+ expect(storageA.clearAuthState).toHaveBeenCalledWith("docs"),
1171
+ );
1172
+
1173
+ setStorage("b");
1174
+ await vi.waitFor(() =>
1175
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
1176
+ );
1177
+ const storageALoadCount = storageA.loadCustomServers.mock.calls.length;
1178
+
1179
+ setStorage("a");
1180
+ await new Promise((resolve) => setTimeout(resolve, 0));
1181
+ expect(storageA.loadCustomServers).toHaveBeenCalledTimes(
1182
+ storageALoadCount,
1183
+ );
1184
+
1185
+ resolveStorageAClear?.();
1186
+ await removal;
1187
+ await vi.waitFor(() =>
1188
+ expect(storageA.loadCustomServers.mock.calls.length).toBeGreaterThan(
1189
+ storageALoadCount,
1190
+ ),
1191
+ );
1192
+ await vi.waitFor(() =>
1193
+ expect(root.getValue().getState().customServers).toHaveLength(0),
1194
+ );
1195
+ expect(persistedStorageARecords).toEqual([]);
1196
+ } finally {
1197
+ resolveStorageAClear?.();
1198
+ root.unmount();
1199
+ }
1200
+ });
1201
+
1202
+ it("disposes a no-auth custom server from the previous scope", async () => {
1203
+ mocks.StreamableHTTPClientTransport.mockClear();
1204
+ const storageA = {
1205
+ scopeId: "account:a",
1206
+ loadCustomServers: vi.fn(async () => [docsServer]),
1207
+ saveCustomServers: vi.fn(async () => {}),
1208
+ loadAuthState: vi.fn(async () => null),
1209
+ saveAuthState: vi.fn(async () => {}),
1210
+ clearAuthState: vi.fn(async () => {}),
1211
+ };
1212
+ const storageB = {
1213
+ scopeId: "account:b",
1214
+ loadCustomServers: vi.fn(async () => []),
1215
+ saveCustomServers: vi.fn(async () => {}),
1216
+ loadAuthState: vi.fn(async () => null),
1217
+ saveAuthState: vi.fn(async () => {}),
1218
+ clearAuthState: vi.fn(async () => {}),
1219
+ };
1220
+ let switchStorage = () => {};
1221
+ const DynamicManager = resource(function useDynamicManager() {
1222
+ const [storageKey, setStorageKey] = useState<"a" | "b">("a");
1223
+ switchStorage = () => setStorageKey("b");
1224
+ return useResource(
1225
+ McpManagerResource({
1226
+ storage: McpCustomStorage(storageKey === "a" ? storageA : storageB),
1227
+ }),
1228
+ );
1229
+ });
1230
+ const root = createTapRoot(function Root() {
1231
+ return useResource(DynamicManager());
1232
+ });
1233
+
1234
+ try {
1235
+ await vi.waitFor(() =>
1236
+ expect(
1237
+ root.getValue().customServer({ index: 0 }).getState().connectionState,
1238
+ ).toBe("connected"),
1239
+ );
1240
+ const firstTransport = mocks.StreamableHTTPClientTransport.mock
1241
+ .instances[0] as { close: ReturnType<typeof vi.fn> };
1242
+
1243
+ switchStorage();
1244
+
1245
+ await vi.waitFor(() =>
1246
+ expect(storageB.loadCustomServers).toHaveBeenCalled(),
1247
+ );
1248
+ await vi.waitFor(() => expect(firstTransport.close).toHaveBeenCalled());
1249
+ expect(root.getValue().getState().customServers).toHaveLength(0);
1250
+ } finally {
1251
+ root.unmount();
1252
+ }
1253
+ });
1254
+ });