@assistant-ui/react-mcp 0.1.16 → 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/auth/createOAuthProvider.d.ts +13 -2
- package/dist/auth/createOAuthProvider.d.ts.map +1 -1
- package/dist/auth/createOAuthProvider.js +109 -31
- package/dist/auth/createOAuthProvider.js.map +1 -1
- package/dist/auth/types.d.ts +5 -1
- package/dist/auth/types.d.ts.map +1 -1
- 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 +41 -16
- package/dist/resources/McpServerResource.js.map +1 -1
- package/dist/resources/storage/McpLocalStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpLocalStorage.js +16 -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/dist/utils/serverUrl.d.ts +8 -0
- package/dist/utils/serverUrl.d.ts.map +1 -0
- package/dist/utils/serverUrl.js +15 -0
- package/dist/utils/serverUrl.js.map +1 -0
- package/package.json +7 -7
- package/src/auth/createOAuthProvider.test.ts +544 -35
- package/src/auth/createOAuthProvider.ts +201 -46
- package/src/auth/types.ts +5 -1
- 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 +208 -56
- package/src/resources/McpServerRemovalFence.ts +21 -0
- package/src/resources/McpServerResource.test.ts +252 -17
- package/src/resources/McpServerResource.ts +40 -15
- package/src/resources/storage/McpLocalStorage.test.ts +28 -0
- package/src/resources/storage/McpLocalStorage.ts +23 -1
- package/src/resources/storage/types.ts +10 -8
- package/src/utils/serverUrl.test.ts +66 -0
- package/src/utils/serverUrl.ts +23 -0
|
@@ -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,38 +155,113 @@ 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(() => {
|
|
122
195
|
const signal = { cancelled: false };
|
|
196
|
+
// Hydration reads persisted records asynchronously; there is no earlier
|
|
197
|
+
// point than mount at which to start it.
|
|
198
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
123
199
|
void hydrate(signal);
|
|
124
200
|
return () => {
|
|
125
201
|
signal.cancelled = true;
|
|
126
202
|
};
|
|
127
203
|
}, []);
|
|
128
204
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
);
|
|
136
265
|
|
|
137
266
|
const serverElements = useMemo(() => {
|
|
138
267
|
assertUniqueServerIds([
|
|
@@ -167,24 +296,32 @@ const useMcpManagerResource = (
|
|
|
167
296
|
const customElements = customServers.map((s) =>
|
|
168
297
|
withKey(
|
|
169
298
|
s.id,
|
|
170
|
-
McpServerResource(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
),
|
|
188
325
|
),
|
|
189
326
|
);
|
|
190
327
|
return [...connectorElements, ...customElements];
|
|
@@ -195,6 +332,9 @@ const useMcpManagerResource = (
|
|
|
195
332
|
redirectUri,
|
|
196
333
|
autoConnect,
|
|
197
334
|
connectionTimeout,
|
|
335
|
+
updateCustomServers,
|
|
336
|
+
persistenceQueues,
|
|
337
|
+
storageScopeKey,
|
|
198
338
|
]);
|
|
199
339
|
|
|
200
340
|
const lookup = useClientLookup(serverElements);
|
|
@@ -286,7 +426,7 @@ const useMcpManagerResource = (
|
|
|
286
426
|
...(elicitation !== undefined ? { elicitation } : {}),
|
|
287
427
|
createdAt: Date.now(),
|
|
288
428
|
};
|
|
289
|
-
|
|
429
|
+
updateCustomServers((prev) => [...prev, record]);
|
|
290
430
|
return record.id;
|
|
291
431
|
},
|
|
292
432
|
removeServer: async (id) => {
|
|
@@ -304,8 +444,20 @@ const useMcpManagerResource = (
|
|
|
304
444
|
try {
|
|
305
445
|
await lookup.get({ key: id }).remove();
|
|
306
446
|
} catch {
|
|
307
|
-
|
|
308
|
-
|
|
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();
|
|
309
461
|
}
|
|
310
462
|
},
|
|
311
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]?.();
|