@assistant-ui/react-mcp 0.1.17 → 0.1.18
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/primitives/addForm/McpAddFormAuthFields.js +71 -28
- package/dist/primitives/addForm/McpAddFormAuthFields.js.map +1 -1
- 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.js +31 -18
- package/dist/primitives/addForm/McpAddFormNameField.js.map +1 -1
- 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/McpAddFormUrlField.js +31 -18
- package/dist/primitives/addForm/McpAddFormUrlField.js.map +1 -1
- package/dist/primitives/addForm/context.d.ts +9 -1
- package/dist/primitives/addForm/context.d.ts.map +1 -1
- package/dist/primitives/addForm/context.js.map +1 -1
- package/dist/resources/McpManagerResource.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.js +330 -247
- package/dist/resources/McpManagerResource.js.map +1 -1
- package/dist/resources/McpServerRemovalFence.d.ts +7 -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.map +1 -1
- package/dist/resources/McpServerResource.js +5 -1
- package/dist/resources/McpServerResource.js.map +1 -1
- 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/types.d.ts +10 -8
- package/dist/resources/storage/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/primitives/addForm/McpAddFormAccessibility.test.tsx +261 -0
- package/src/primitives/addForm/McpAddFormAuthFields.tsx +33 -15
- package/src/primitives/addForm/McpAddFormError.tsx +17 -3
- package/src/primitives/addForm/McpAddFormNameField.tsx +13 -1
- package/src/primitives/addForm/McpAddFormRoot.tsx +51 -8
- package/src/primitives/addForm/McpAddFormUrlField.tsx +12 -1
- package/src/primitives/addForm/context.tsx +10 -0
- package/src/resources/McpManagerResource.test.ts +712 -9
- package/src/resources/McpManagerResource.ts +205 -56
- package/src/resources/McpServerRemovalFence.ts +21 -0
- package/src/resources/McpServerResource.ts +5 -1
- 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
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useEffectEvent,
|
|
6
|
+
useRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
} from "react";
|
|
2
9
|
import { useResource, resource, withKey } from "@assistant-ui/tap";
|
|
3
10
|
import {
|
|
4
11
|
useClientLookup,
|
|
@@ -12,6 +19,7 @@ import { createMcpId } from "../utils/createMcpId";
|
|
|
12
19
|
import { clearOAuthProviderAuthState } from "../auth/createOAuthProvider";
|
|
13
20
|
import type { Tool } from "assistant-stream";
|
|
14
21
|
import { McpServerResource } from "./McpServerResource";
|
|
22
|
+
import { withMcpServerRemovalFence } from "./McpServerRemovalFence";
|
|
15
23
|
import { McpLocalStorage } from "./storage/McpLocalStorage";
|
|
16
24
|
import type { MCPStorage, MCPStorageElement } from "./storage/types";
|
|
17
25
|
import { assertUniqueServerIds } from "../utils/serverId";
|
|
@@ -52,6 +60,12 @@ const reportCustomStorageFailure = (
|
|
|
52
60
|
);
|
|
53
61
|
};
|
|
54
62
|
|
|
63
|
+
const reportBlockedCustomServerPersistence = () => {
|
|
64
|
+
console.error(
|
|
65
|
+
"[assistant-ui/react-mcp] custom server changes remain in memory because loading the persisted list failed; remount the manager to retry",
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
55
69
|
const persistCustomServers = async (
|
|
56
70
|
storage: MCPStorage,
|
|
57
71
|
records: MCPCustomServerRecord[],
|
|
@@ -63,37 +77,77 @@ const persistCustomServers = async (
|
|
|
63
77
|
}
|
|
64
78
|
};
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
props: McpManagerResourceProps,
|
|
68
|
-
): ClientOutput<"mcp"> => {
|
|
69
|
-
const connectors = props.connectors ?? NO_CONNECTORS;
|
|
70
|
-
const autoConnect = props.autoConnect ?? true;
|
|
71
|
-
const redirectUri = props.oauthRedirectUri ?? defaultRedirectUri();
|
|
72
|
-
const connectionTimeout = props.connectionTimeout;
|
|
80
|
+
type CustomServerPersistenceQueues = Map<string, Promise<void>>;
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
const enqueueCustomServerTask = (
|
|
83
|
+
persistenceQueues: CustomServerPersistenceQueues,
|
|
84
|
+
scopeKey: string,
|
|
85
|
+
task: () => Promise<void>,
|
|
86
|
+
) => {
|
|
87
|
+
const previous = persistenceQueues.get(scopeKey);
|
|
88
|
+
const next = (previous ?? Promise.resolve()).then(task);
|
|
89
|
+
persistenceQueues.set(scopeKey, next);
|
|
90
|
+
void next.then(() => {
|
|
91
|
+
if (persistenceQueues.get(scopeKey) === next) {
|
|
92
|
+
persistenceQueues.delete(scopeKey);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const enqueueCustomServerPersistence = (
|
|
98
|
+
persistenceQueues: CustomServerPersistenceQueues,
|
|
99
|
+
scopeKey: string,
|
|
100
|
+
storage: MCPStorage,
|
|
101
|
+
records: MCPCustomServerRecord[],
|
|
102
|
+
) =>
|
|
103
|
+
enqueueCustomServerTask(persistenceQueues, scopeKey, () =>
|
|
104
|
+
persistCustomServers(storage, records),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const holdCustomServerPersistence = (
|
|
108
|
+
persistenceQueues: CustomServerPersistenceQueues,
|
|
109
|
+
scopeKey: string,
|
|
110
|
+
) => {
|
|
111
|
+
let release = () => {};
|
|
112
|
+
const gate = new Promise<void>((resolve) => {
|
|
113
|
+
release = resolve;
|
|
114
|
+
});
|
|
115
|
+
enqueueCustomServerTask(persistenceQueues, scopeKey, () => gate);
|
|
116
|
+
return release;
|
|
117
|
+
};
|
|
76
118
|
|
|
119
|
+
type McpCustomServersResourceProps = {
|
|
120
|
+
storage: MCPStorage;
|
|
121
|
+
scopeKey: string;
|
|
122
|
+
persistenceQueues: CustomServerPersistenceQueues;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const useMcpCustomServersResource = ({
|
|
126
|
+
storage,
|
|
127
|
+
scopeKey,
|
|
128
|
+
persistenceQueues,
|
|
129
|
+
}: McpCustomServersResourceProps) => {
|
|
77
130
|
const [customServers, setCustomServers] = useState<MCPCustomServerRecord[]>(
|
|
78
131
|
[],
|
|
79
132
|
);
|
|
80
133
|
const [isHydrated, setIsHydrated] = useState(false);
|
|
81
134
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}, [storage]);
|
|
135
|
+
const customServersRef = useRef<MCPCustomServerRecord[]>([]);
|
|
136
|
+
const hydrationStateRef = useRef<"pending" | "succeeded" | "failed">(
|
|
137
|
+
"pending",
|
|
138
|
+
);
|
|
139
|
+
const hasPendingMutationRef = useRef(false);
|
|
140
|
+
const reportedBlockedPersistenceRef = useRef(false);
|
|
89
141
|
|
|
90
142
|
const hydrate = useEffectEvent(async (signal: { cancelled: boolean }) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
// A revisited scope must not read behind writes still queued against it.
|
|
144
|
+
while (true) {
|
|
145
|
+
const pendingPersistence = persistenceQueues.get(scopeKey);
|
|
146
|
+
if (!pendingPersistence) break;
|
|
147
|
+
await pendingPersistence;
|
|
148
|
+
if (signal.cancelled) return;
|
|
149
|
+
if (persistenceQueues.get(scopeKey) === pendingPersistence) break;
|
|
150
|
+
}
|
|
97
151
|
|
|
98
152
|
let records: Awaited<ReturnType<typeof storage.loadCustomServers>>;
|
|
99
153
|
try {
|
|
@@ -101,21 +155,40 @@ const useMcpManagerResource = (
|
|
|
101
155
|
} catch (error) {
|
|
102
156
|
if (!signal.cancelled) {
|
|
103
157
|
reportCustomStorageFailure("load", error);
|
|
158
|
+
hydrationStateRef.current = "failed";
|
|
159
|
+
if (hasPendingMutationRef.current) {
|
|
160
|
+
reportBlockedCustomServerPersistence();
|
|
161
|
+
reportedBlockedPersistenceRef.current = true;
|
|
162
|
+
}
|
|
163
|
+
setIsHydrated(true);
|
|
104
164
|
}
|
|
105
|
-
markHydrated();
|
|
106
165
|
return;
|
|
107
166
|
}
|
|
108
167
|
|
|
109
|
-
if (signal.cancelled) return;
|
|
110
168
|
// Merge rather than replace so any addCustomServer calls that
|
|
111
169
|
// happened before hydration resolved aren't silently overwritten.
|
|
112
170
|
// Persisted order wins; pre-hydration locals append.
|
|
113
|
-
|
|
171
|
+
const hadPendingMutation = hasPendingMutationRef.current;
|
|
172
|
+
const mergedRecords = (() => {
|
|
173
|
+
const prev = customServersRef.current;
|
|
114
174
|
if (prev.length === 0) return records;
|
|
115
175
|
const persistedIds = new Set(records.map((r) => r.id));
|
|
116
176
|
return [...records, ...prev.filter((r) => !persistedIds.has(r.id))];
|
|
117
|
-
});
|
|
118
|
-
|
|
177
|
+
})();
|
|
178
|
+
customServersRef.current = mergedRecords;
|
|
179
|
+
hydrationStateRef.current = "succeeded";
|
|
180
|
+
hasPendingMutationRef.current = false;
|
|
181
|
+
if (hadPendingMutation) {
|
|
182
|
+
enqueueCustomServerPersistence(
|
|
183
|
+
persistenceQueues,
|
|
184
|
+
scopeKey,
|
|
185
|
+
storage,
|
|
186
|
+
mergedRecords,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (signal.cancelled) return;
|
|
190
|
+
setCustomServers(mergedRecords);
|
|
191
|
+
setIsHydrated(true);
|
|
119
192
|
});
|
|
120
193
|
|
|
121
194
|
useEffect(() => {
|
|
@@ -129,13 +202,66 @@ const useMcpManagerResource = (
|
|
|
129
202
|
};
|
|
130
203
|
}, []);
|
|
131
204
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
205
|
+
const updateCustomServers = useCallback(
|
|
206
|
+
(
|
|
207
|
+
updater: (records: MCPCustomServerRecord[]) => MCPCustomServerRecord[],
|
|
208
|
+
) => {
|
|
209
|
+
const next = updater(customServersRef.current);
|
|
210
|
+
customServersRef.current = next;
|
|
211
|
+
setCustomServers(next);
|
|
212
|
+
|
|
213
|
+
if (hydrationStateRef.current === "succeeded") {
|
|
214
|
+
enqueueCustomServerPersistence(
|
|
215
|
+
persistenceQueues,
|
|
216
|
+
scopeKey,
|
|
217
|
+
storage,
|
|
218
|
+
next,
|
|
219
|
+
);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
hasPendingMutationRef.current = true;
|
|
224
|
+
if (
|
|
225
|
+
hydrationStateRef.current === "failed" &&
|
|
226
|
+
!reportedBlockedPersistenceRef.current
|
|
227
|
+
) {
|
|
228
|
+
reportBlockedCustomServerPersistence();
|
|
229
|
+
reportedBlockedPersistenceRef.current = true;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
[persistenceQueues, scopeKey, storage],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return { customServers, isHydrated, updateCustomServers };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const McpCustomServersResource = resource(useMcpCustomServersResource);
|
|
239
|
+
|
|
240
|
+
const useMcpManagerResource = (
|
|
241
|
+
props: McpManagerResourceProps,
|
|
242
|
+
): ClientOutput<"mcp"> => {
|
|
243
|
+
const connectors = props.connectors ?? NO_CONNECTORS;
|
|
244
|
+
const autoConnect = props.autoConnect ?? true;
|
|
245
|
+
const redirectUri = props.oauthRedirectUri ?? defaultRedirectUri();
|
|
246
|
+
const connectionTimeout = props.connectionTimeout;
|
|
247
|
+
|
|
248
|
+
const storageElement = props.storage ?? McpLocalStorage();
|
|
249
|
+
const storage = useResource(storageElement);
|
|
250
|
+
const [persistenceQueues] = useState<CustomServerPersistenceQueues>(
|
|
251
|
+
() => new Map(),
|
|
252
|
+
);
|
|
253
|
+
const storageScopeKey =
|
|
254
|
+
storage.scopeId === undefined ? "unscoped" : `scoped:${storage.scopeId}`;
|
|
255
|
+
const { customServers, isHydrated, updateCustomServers } = useResource(
|
|
256
|
+
withKey(
|
|
257
|
+
storageScopeKey,
|
|
258
|
+
McpCustomServersResource({
|
|
259
|
+
storage,
|
|
260
|
+
scopeKey: storageScopeKey,
|
|
261
|
+
persistenceQueues,
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
139
265
|
|
|
140
266
|
const serverElements = useMemo(() => {
|
|
141
267
|
assertUniqueServerIds([
|
|
@@ -170,24 +296,32 @@ const useMcpManagerResource = (
|
|
|
170
296
|
const customElements = customServers.map((s) =>
|
|
171
297
|
withKey(
|
|
172
298
|
s.id,
|
|
173
|
-
McpServerResource(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
299
|
+
McpServerResource(
|
|
300
|
+
withMcpServerRemovalFence(
|
|
301
|
+
{
|
|
302
|
+
id: s.id,
|
|
303
|
+
kind: "custom",
|
|
304
|
+
name: s.name,
|
|
305
|
+
url: s.url,
|
|
306
|
+
auth: s.auth,
|
|
307
|
+
storage,
|
|
308
|
+
redirectUri,
|
|
309
|
+
autoConnect,
|
|
310
|
+
connectionTimeout: s.connectionTimeout ?? connectionTimeout,
|
|
311
|
+
...(s.cache !== undefined ? { cache: s.cache } : {}),
|
|
312
|
+
...(s.elicitation !== undefined
|
|
313
|
+
? { elicitation: s.elicitation }
|
|
314
|
+
: {}),
|
|
315
|
+
onRemove: async () => {
|
|
316
|
+
updateCustomServers((prev) =>
|
|
317
|
+
prev.filter((record) => record.id !== s.id),
|
|
318
|
+
);
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
() =>
|
|
322
|
+
holdCustomServerPersistence(persistenceQueues, storageScopeKey),
|
|
323
|
+
),
|
|
324
|
+
),
|
|
191
325
|
),
|
|
192
326
|
);
|
|
193
327
|
return [...connectorElements, ...customElements];
|
|
@@ -198,6 +332,9 @@ const useMcpManagerResource = (
|
|
|
198
332
|
redirectUri,
|
|
199
333
|
autoConnect,
|
|
200
334
|
connectionTimeout,
|
|
335
|
+
updateCustomServers,
|
|
336
|
+
persistenceQueues,
|
|
337
|
+
storageScopeKey,
|
|
201
338
|
]);
|
|
202
339
|
|
|
203
340
|
const lookup = useClientLookup(serverElements);
|
|
@@ -289,7 +426,7 @@ const useMcpManagerResource = (
|
|
|
289
426
|
...(elicitation !== undefined ? { elicitation } : {}),
|
|
290
427
|
createdAt: Date.now(),
|
|
291
428
|
};
|
|
292
|
-
|
|
429
|
+
updateCustomServers((prev) => [...prev, record]);
|
|
293
430
|
return record.id;
|
|
294
431
|
},
|
|
295
432
|
removeServer: async (id) => {
|
|
@@ -307,8 +444,20 @@ const useMcpManagerResource = (
|
|
|
307
444
|
try {
|
|
308
445
|
await lookup.get({ key: id }).remove();
|
|
309
446
|
} catch {
|
|
310
|
-
|
|
311
|
-
|
|
447
|
+
const releasePersistence = holdCustomServerPersistence(
|
|
448
|
+
persistenceQueues,
|
|
449
|
+
storageScopeKey,
|
|
450
|
+
);
|
|
451
|
+
try {
|
|
452
|
+
await clearOAuthProviderAuthState(storage, id);
|
|
453
|
+
updateCustomServers((prev) =>
|
|
454
|
+
prev.filter((record) => record.id !== id),
|
|
455
|
+
);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
releasePersistence();
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
releasePersistence();
|
|
312
461
|
}
|
|
313
462
|
},
|
|
314
463
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { McpServerResourceProps } from "./McpServerResource";
|
|
2
|
+
|
|
3
|
+
const beginRemovalFenceSymbol = Symbol();
|
|
4
|
+
|
|
5
|
+
type McpServerResourceInternalProps = McpServerResourceProps & {
|
|
6
|
+
[beginRemovalFenceSymbol]?: () => () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const withMcpServerRemovalFence = (
|
|
10
|
+
props: McpServerResourceProps,
|
|
11
|
+
beginRemovalFence: () => () => void,
|
|
12
|
+
): McpServerResourceProps =>
|
|
13
|
+
({
|
|
14
|
+
...props,
|
|
15
|
+
[beginRemovalFenceSymbol]: beginRemovalFence,
|
|
16
|
+
}) as McpServerResourceProps;
|
|
17
|
+
|
|
18
|
+
export const beginMcpServerRemovalFence = (
|
|
19
|
+
props: McpServerResourceProps,
|
|
20
|
+
): (() => void) | undefined =>
|
|
21
|
+
(props as McpServerResourceInternalProps)[beginRemovalFenceSymbol]?.();
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
MCPToolInfo,
|
|
35
35
|
} from "../mcp-scope";
|
|
36
36
|
import { createMcpId } from "../utils/createMcpId";
|
|
37
|
+
import { beginMcpServerRemovalFence } from "./McpServerRemovalFence";
|
|
37
38
|
|
|
38
39
|
export type McpServerResourceProps = {
|
|
39
40
|
id: string;
|
|
@@ -666,16 +667,19 @@ const useMcpServerResourceInstance = (
|
|
|
666
667
|
connect: doConnect,
|
|
667
668
|
disconnect: doDisconnect,
|
|
668
669
|
remove: async () => {
|
|
669
|
-
|
|
670
|
+
const releaseRemovalFence = beginMcpServerRemovalFence(props);
|
|
670
671
|
try {
|
|
672
|
+
await doDisconnect();
|
|
671
673
|
await clearOAuthProviderAuthState(props.storage, props.id);
|
|
672
674
|
await props.onRemove();
|
|
673
675
|
} catch (err) {
|
|
676
|
+
releaseRemovalFence?.();
|
|
674
677
|
setLastError({
|
|
675
678
|
message: err instanceof Error ? err.message : String(err),
|
|
676
679
|
});
|
|
677
680
|
throw err;
|
|
678
681
|
}
|
|
682
|
+
releaseRemovalFence?.();
|
|
679
683
|
},
|
|
680
684
|
callTool: async (name, args) => {
|
|
681
685
|
const client = clientRef.current;
|
|
@@ -239,6 +239,8 @@ describe("normalizePersistedAuthState", () => {
|
|
|
239
239
|
|
|
240
240
|
it.each([
|
|
241
241
|
"http://auth.example.com",
|
|
242
|
+
"http://127.example.com",
|
|
243
|
+
"http://127.0.0.1.example.com",
|
|
242
244
|
"data:text/plain,auth",
|
|
243
245
|
"file:///tmp/auth",
|
|
244
246
|
])("drops discovery state with an unsafe URL: %s", (url) => {
|
|
@@ -148,12 +148,13 @@ const isSecureNetworkUrl = (value: unknown): value is string => {
|
|
|
148
148
|
if (!isNonEmptyString(value)) return false;
|
|
149
149
|
try {
|
|
150
150
|
const url = new URL(value);
|
|
151
|
+
const isIpv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
151
152
|
return (
|
|
152
153
|
url.protocol === "https:" ||
|
|
153
154
|
(url.protocol === "http:" &&
|
|
154
155
|
(url.hostname === "localhost" ||
|
|
155
156
|
url.hostname.endsWith(".localhost") ||
|
|
156
|
-
|
|
157
|
+
isIpv4Loopback ||
|
|
157
158
|
url.hostname === "[::1]"))
|
|
158
159
|
);
|
|
159
160
|
} catch {
|
|
@@ -5,14 +5,16 @@ import type { MCPPersistedAuthState } from "../../auth/types";
|
|
|
5
5
|
export type MCPStorage = {
|
|
6
6
|
/**
|
|
7
7
|
* Stable identity of the backing store. Two storages with the same scopeId
|
|
8
|
-
* must read and write the same persisted data. When present,
|
|
9
|
-
* connections and the OAuth write fence key on it
|
|
10
|
-
* differently-scoped storage
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* must read and write the same persisted data. When present, custom servers,
|
|
9
|
+
* server connections, and the OAuth write fence key on it. Swapping to a
|
|
10
|
+
* differently-scoped storage rehydrates its custom servers and reconnects
|
|
11
|
+
* authenticated servers instead of retaining data or a live OAuth flow from
|
|
12
|
+
* the replaced store. Clearing through a same-scoped replacement still waits
|
|
13
|
+
* for writes queued against the storage it replaced. When absent, the fence
|
|
14
|
+
* falls back to object identity while custom servers and connections never
|
|
15
|
+
* re-key. Replacing an unscoped storage can therefore persist the previous
|
|
16
|
+
* store's custom servers into the replacement, and a clear runs unfenced
|
|
17
|
+
* against writes queued by the object it replaced.
|
|
16
18
|
*/
|
|
17
19
|
scopeId?: string;
|
|
18
20
|
loadCustomServers: () => Promise<MCPCustomServerRecord[]>;
|