@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.
- package/dist/auth/buildHeaders.d.ts +1 -2
- package/dist/auth/buildHeaders.d.ts.map +1 -1
- package/dist/auth/createOAuthProvider.d.ts +5 -6
- package/dist/auth/createOAuthProvider.d.ts.map +1 -1
- package/dist/auth/types.d.ts +1 -2
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/connector.d.ts +1 -2
- package/dist/connector.d.ts.map +1 -1
- package/dist/context/McpConnectorByIndexProvider.d.ts +1 -2
- package/dist/context/McpConnectorByIndexProvider.d.ts.map +1 -1
- package/dist/context/McpCustomServerByIndexProvider.d.ts +1 -2
- package/dist/context/McpCustomServerByIndexProvider.d.ts.map +1 -1
- package/dist/context/McpServerByIdProvider.d.ts +1 -2
- package/dist/context/McpServerByIdProvider.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.d.ts +5 -6
- package/dist/hooks/useMcpOAuthCallback.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.js +4 -0
- package/dist/hooks/useMcpOAuthCallback.js.map +1 -1
- package/dist/mcp-scope.d.ts +14 -15
- package/dist/mcp-scope.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormAuthFields.d.ts +4 -4
- package/dist/primitives/addForm/McpAddFormAuthFields.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormAuthFields.js +28 -38
- package/dist/primitives/addForm/McpAddFormAuthFields.js.map +1 -1
- package/dist/primitives/addForm/McpAddFormAuthSelect.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormAuthSelect.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormBearerTokenField.d.ts +10 -0
- package/dist/primitives/addForm/McpAddFormBearerTokenField.d.ts.map +1 -0
- package/dist/primitives/addForm/McpAddFormBearerTokenField.js +50 -0
- package/dist/primitives/addForm/McpAddFormBearerTokenField.js.map +1 -0
- package/dist/primitives/addForm/McpAddFormCancel.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormCancel.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormError.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormError.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormError.js +30 -10
- package/dist/primitives/addForm/McpAddFormError.js.map +1 -1
- package/dist/primitives/addForm/McpAddFormNameField.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormNameField.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormNameField.js +31 -18
- package/dist/primitives/addForm/McpAddFormNameField.js.map +1 -1
- package/dist/primitives/addForm/McpAddFormRoot.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormRoot.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormRoot.js +32 -11
- package/dist/primitives/addForm/McpAddFormRoot.js.map +1 -1
- package/dist/primitives/addForm/McpAddFormScopesField.d.ts +10 -0
- package/dist/primitives/addForm/McpAddFormScopesField.d.ts.map +1 -0
- package/dist/primitives/addForm/McpAddFormScopesField.js +38 -0
- package/dist/primitives/addForm/McpAddFormScopesField.js.map +1 -0
- package/dist/primitives/addForm/McpAddFormSubmit.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormSubmit.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormUrlField.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormUrlField.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormUrlField.js +31 -18
- package/dist/primitives/addForm/McpAddFormUrlField.js.map +1 -1
- package/dist/primitives/addForm/context.d.ts +13 -6
- package/dist/primitives/addForm/context.d.ts.map +1 -1
- package/dist/primitives/addForm/context.js.map +1 -1
- package/dist/primitives/addForm.d.ts +4 -2
- package/dist/primitives/addForm.js +5 -1
- package/dist/primitives/addForm.js.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveAccept.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveAccept.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveCancel.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveCancel.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveDecline.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveDecline.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveError.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveError.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveFields.d.ts +3 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveFields.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveItems.d.ts +3 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveItems.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveMessage.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveMessage.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveRoot.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveRoot.d.ts.map +1 -1
- package/dist/primitives/elicitation/context.d.ts +7 -8
- package/dist/primitives/elicitation/context.d.ts.map +1 -1
- package/dist/primitives/elicitation/initialElicitationDraft.d.ts +1 -2
- package/dist/primitives/elicitation/initialElicitationDraft.d.ts.map +1 -1
- package/dist/primitives/elicitation/prepareElicitationContent.d.ts +1 -2
- package/dist/primitives/elicitation/prepareElicitationContent.d.ts.map +1 -1
- package/dist/primitives/elicitation/prepareElicitationContent.js +5 -1
- package/dist/primitives/elicitation/prepareElicitationContent.js.map +1 -1
- package/dist/primitives/manager/McpManagerAddCustomTrigger.d.ts +2 -3
- package/dist/primitives/manager/McpManagerAddCustomTrigger.d.ts.map +1 -1
- package/dist/primitives/manager/McpManagerConnectors.d.ts +2 -3
- package/dist/primitives/manager/McpManagerConnectors.d.ts.map +1 -1
- package/dist/primitives/manager/McpManagerCustomServers.d.ts +2 -3
- package/dist/primitives/manager/McpManagerCustomServers.d.ts.map +1 -1
- package/dist/primitives/manager/McpManagerRoot.d.ts +2 -3
- package/dist/primitives/manager/McpManagerRoot.d.ts.map +1 -1
- package/dist/primitives/server/McpServerConnectButton.d.ts +2 -3
- package/dist/primitives/server/McpServerConnectButton.d.ts.map +1 -1
- package/dist/primitives/server/McpServerDisconnectButton.d.ts +2 -3
- package/dist/primitives/server/McpServerDisconnectButton.d.ts.map +1 -1
- package/dist/primitives/server/McpServerError.d.ts +2 -3
- package/dist/primitives/server/McpServerError.d.ts.map +1 -1
- package/dist/primitives/server/McpServerIcon.d.ts +2 -3
- package/dist/primitives/server/McpServerIcon.d.ts.map +1 -1
- package/dist/primitives/server/McpServerName.d.ts +2 -3
- package/dist/primitives/server/McpServerName.d.ts.map +1 -1
- package/dist/primitives/server/McpServerOAuthLink.d.ts +2 -3
- package/dist/primitives/server/McpServerOAuthLink.d.ts.map +1 -1
- package/dist/primitives/server/McpServerRemoveButton.d.ts +2 -3
- package/dist/primitives/server/McpServerRemoveButton.d.ts.map +1 -1
- package/dist/primitives/server/McpServerRoot.d.ts +2 -3
- package/dist/primitives/server/McpServerRoot.d.ts.map +1 -1
- package/dist/primitives/server/McpServerStatus.d.ts +2 -3
- package/dist/primitives/server/McpServerStatus.d.ts.map +1 -1
- package/dist/primitives/server/McpServerToolName.d.ts +2 -3
- package/dist/primitives/server/McpServerToolName.d.ts.map +1 -1
- package/dist/primitives/server/McpServerTools.d.ts +3 -4
- package/dist/primitives/server/McpServerTools.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.d.ts +2 -3
- package/dist/resources/McpManagerResource.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.js +358 -244
- package/dist/resources/McpManagerResource.js.map +1 -1
- package/dist/resources/McpServerRemovalFence.d.ts +6 -0
- package/dist/resources/McpServerRemovalFence.d.ts.map +1 -0
- package/dist/resources/McpServerRemovalFence.js +11 -0
- package/dist/resources/McpServerRemovalFence.js.map +1 -0
- package/dist/resources/McpServerResource.d.ts +3 -4
- package/dist/resources/McpServerResource.d.ts.map +1 -1
- package/dist/resources/McpServerResource.js +9 -5
- package/dist/resources/McpServerResource.js.map +1 -1
- package/dist/resources/storage/McpCustomStorage.d.ts +1 -2
- package/dist/resources/storage/McpCustomStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpLocalStorage.d.ts +4 -5
- package/dist/resources/storage/McpLocalStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpLocalStorage.js +2 -1
- package/dist/resources/storage/McpLocalStorage.js.map +1 -1
- package/dist/resources/storage/McpMemoryStorage.d.ts +1 -2
- package/dist/resources/storage/McpMemoryStorage.d.ts.map +1 -1
- package/dist/resources/storage/types.d.ts +12 -11
- package/dist/resources/storage/types.d.ts.map +1 -1
- package/dist/resources/validateElicitationContent.d.ts +1 -2
- package/dist/resources/validateElicitationContent.d.ts.map +1 -1
- package/dist/utils/createMcpId.d.ts +1 -2
- package/dist/utils/createMcpId.d.ts.map +1 -1
- package/dist/utils/invokeMcpCallback.d.ts +1 -2
- package/dist/utils/invokeMcpCallback.d.ts.map +1 -1
- package/dist/utils/serverId.d.ts +2 -3
- package/dist/utils/serverId.d.ts.map +1 -1
- package/dist/utils/serverUrl.d.ts +3 -4
- package/dist/utils/serverUrl.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/hooks/useMcpOAuthCallback.test.ts +53 -0
- package/src/hooks/useMcpOAuthCallback.tsx +12 -0
- package/src/index.ts +2 -1
- package/src/primitives/addForm/McpAddFormAccessibility.test.tsx +310 -0
- package/src/primitives/addForm/McpAddFormAuthFields.tsx +26 -16
- package/src/primitives/addForm/McpAddFormBearerTokenField.tsx +46 -0
- package/src/primitives/addForm/McpAddFormError.tsx +17 -3
- package/src/primitives/addForm/McpAddFormNameField.tsx +13 -1
- package/src/primitives/addForm/McpAddFormRoot.test.tsx +65 -0
- package/src/primitives/addForm/McpAddFormRoot.tsx +51 -8
- package/src/primitives/addForm/McpAddFormScopesField.tsx +34 -0
- package/src/primitives/addForm/McpAddFormUrlField.tsx +12 -1
- package/src/primitives/addForm/context.tsx +10 -0
- package/src/primitives/addForm.ts +2 -0
- package/src/primitives/elicitation/prepareElicitationContent.ts +8 -6
- package/src/resources/McpManagerResource.test.ts +797 -9
- package/src/resources/McpManagerResource.ts +242 -60
- package/src/resources/McpServerRemovalFence.ts +21 -0
- package/src/resources/McpServerResource.test.ts +9 -6
- package/src/resources/McpServerResource.ts +11 -6
- package/src/resources/storage/McpLocalStorage.test.ts +2 -0
- package/src/resources/storage/McpLocalStorage.ts +2 -1
- 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("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|