@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
|
@@ -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,59 +77,139 @@ 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
|
+
};
|
|
118
|
+
|
|
119
|
+
const deduplicateCustomServers = (records: MCPCustomServerRecord[]) => {
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
return records.filter((record) => {
|
|
122
|
+
if (seen.has(record.id)) {
|
|
123
|
+
console.error(
|
|
124
|
+
`[assistant-ui/react-mcp] ignored duplicate custom server id "${record.id}" loaded from storage`,
|
|
125
|
+
);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
seen.add(record.id);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type McpCustomServersResourceProps = {
|
|
134
|
+
storage: MCPStorage;
|
|
135
|
+
scopeKey: string;
|
|
136
|
+
persistenceQueues: CustomServerPersistenceQueues;
|
|
137
|
+
};
|
|
76
138
|
|
|
139
|
+
const useMcpCustomServersResource = ({
|
|
140
|
+
storage,
|
|
141
|
+
scopeKey,
|
|
142
|
+
persistenceQueues,
|
|
143
|
+
}: McpCustomServersResourceProps) => {
|
|
77
144
|
const [customServers, setCustomServers] = useState<MCPCustomServerRecord[]>(
|
|
78
145
|
[],
|
|
79
146
|
);
|
|
80
147
|
const [isHydrated, setIsHydrated] = useState(false);
|
|
81
148
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
149
|
+
const customServersRef = useRef<MCPCustomServerRecord[]>([]);
|
|
150
|
+
const hydrationStateRef = useRef<"pending" | "succeeded" | "failed">(
|
|
151
|
+
"pending",
|
|
152
|
+
);
|
|
153
|
+
const hasPendingMutationRef = useRef(false);
|
|
154
|
+
const [removedBeforeHydration] = useState(() => new Set<string>());
|
|
155
|
+
const reportedBlockedPersistenceRef = useRef(false);
|
|
89
156
|
|
|
90
157
|
const hydrate = useEffectEvent(async (signal: { cancelled: boolean }) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
158
|
+
// A revisited scope must not read behind writes still queued against it.
|
|
159
|
+
while (true) {
|
|
160
|
+
const pendingPersistence = persistenceQueues.get(scopeKey);
|
|
161
|
+
if (!pendingPersistence) break;
|
|
162
|
+
await pendingPersistence;
|
|
163
|
+
if (signal.cancelled) return;
|
|
164
|
+
if (persistenceQueues.get(scopeKey) === pendingPersistence) break;
|
|
165
|
+
}
|
|
97
166
|
|
|
98
167
|
let records: Awaited<ReturnType<typeof storage.loadCustomServers>>;
|
|
99
168
|
try {
|
|
100
|
-
|
|
169
|
+
const loadedRecords = await storage.loadCustomServers();
|
|
170
|
+
records = deduplicateCustomServers(loadedRecords);
|
|
101
171
|
} catch (error) {
|
|
102
172
|
if (!signal.cancelled) {
|
|
103
173
|
reportCustomStorageFailure("load", error);
|
|
174
|
+
hydrationStateRef.current = "failed";
|
|
175
|
+
if (hasPendingMutationRef.current) {
|
|
176
|
+
reportBlockedCustomServerPersistence();
|
|
177
|
+
reportedBlockedPersistenceRef.current = true;
|
|
178
|
+
}
|
|
179
|
+
setIsHydrated(true);
|
|
104
180
|
}
|
|
105
|
-
markHydrated();
|
|
106
181
|
return;
|
|
107
182
|
}
|
|
108
|
-
|
|
109
|
-
if (signal.cancelled) return;
|
|
110
183
|
// Merge rather than replace so any addCustomServer calls that
|
|
111
184
|
// happened before hydration resolved aren't silently overwritten.
|
|
112
185
|
// Persisted order wins; pre-hydration locals append.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
186
|
+
const hadPendingMutation = hasPendingMutationRef.current;
|
|
187
|
+
const hydratedRecords = records.filter(
|
|
188
|
+
(record) => !removedBeforeHydration.has(record.id),
|
|
189
|
+
);
|
|
190
|
+
const mergedRecords = (() => {
|
|
191
|
+
const prev = customServersRef.current;
|
|
192
|
+
if (prev.length === 0) return hydratedRecords;
|
|
193
|
+
const persistedIds = new Set(hydratedRecords.map((r) => r.id));
|
|
194
|
+
return [
|
|
195
|
+
...hydratedRecords,
|
|
196
|
+
...prev.filter((r) => !persistedIds.has(r.id)),
|
|
197
|
+
];
|
|
198
|
+
})();
|
|
199
|
+
customServersRef.current = mergedRecords;
|
|
200
|
+
hydrationStateRef.current = "succeeded";
|
|
201
|
+
hasPendingMutationRef.current = false;
|
|
202
|
+
if (hadPendingMutation) {
|
|
203
|
+
enqueueCustomServerPersistence(
|
|
204
|
+
persistenceQueues,
|
|
205
|
+
scopeKey,
|
|
206
|
+
storage,
|
|
207
|
+
mergedRecords,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (signal.cancelled) return;
|
|
211
|
+
setCustomServers(mergedRecords);
|
|
212
|
+
setIsHydrated(true);
|
|
119
213
|
});
|
|
120
214
|
|
|
121
215
|
useEffect(() => {
|
|
@@ -129,13 +223,82 @@ const useMcpManagerResource = (
|
|
|
129
223
|
};
|
|
130
224
|
}, []);
|
|
131
225
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
226
|
+
const updateCustomServers = useCallback(
|
|
227
|
+
(
|
|
228
|
+
updater: (records: MCPCustomServerRecord[]) => MCPCustomServerRecord[],
|
|
229
|
+
) => {
|
|
230
|
+
const next = updater(customServersRef.current);
|
|
231
|
+
customServersRef.current = next;
|
|
232
|
+
setCustomServers(next);
|
|
233
|
+
|
|
234
|
+
if (hydrationStateRef.current === "succeeded") {
|
|
235
|
+
enqueueCustomServerPersistence(
|
|
236
|
+
persistenceQueues,
|
|
237
|
+
scopeKey,
|
|
238
|
+
storage,
|
|
239
|
+
next,
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
hasPendingMutationRef.current = true;
|
|
245
|
+
if (
|
|
246
|
+
hydrationStateRef.current === "failed" &&
|
|
247
|
+
!reportedBlockedPersistenceRef.current
|
|
248
|
+
) {
|
|
249
|
+
reportBlockedCustomServerPersistence();
|
|
250
|
+
reportedBlockedPersistenceRef.current = true;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
[persistenceQueues, scopeKey, storage],
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const removeCustomServer = useCallback(
|
|
257
|
+
(id: string) => {
|
|
258
|
+
if (hydrationStateRef.current === "pending") {
|
|
259
|
+
removedBeforeHydration.add(id);
|
|
260
|
+
}
|
|
261
|
+
updateCustomServers((prev) => prev.filter((record) => record.id !== id));
|
|
262
|
+
},
|
|
263
|
+
[removedBeforeHydration, updateCustomServers],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
customServers,
|
|
268
|
+
isHydrated,
|
|
269
|
+
updateCustomServers,
|
|
270
|
+
removeCustomServer,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const McpCustomServersResource = resource(useMcpCustomServersResource);
|
|
275
|
+
|
|
276
|
+
const useMcpManagerResource = (
|
|
277
|
+
props: McpManagerResourceProps,
|
|
278
|
+
): ClientOutput<"mcp"> => {
|
|
279
|
+
const connectors = props.connectors ?? NO_CONNECTORS;
|
|
280
|
+
const autoConnect = props.autoConnect ?? true;
|
|
281
|
+
const redirectUri = props.oauthRedirectUri ?? defaultRedirectUri();
|
|
282
|
+
const connectionTimeout = props.connectionTimeout;
|
|
283
|
+
|
|
284
|
+
const storageElement = props.storage ?? McpLocalStorage();
|
|
285
|
+
const storage = useResource(storageElement);
|
|
286
|
+
const [persistenceQueues] = useState<CustomServerPersistenceQueues>(
|
|
287
|
+
() => new Map(),
|
|
288
|
+
);
|
|
289
|
+
const storageScopeKey =
|
|
290
|
+
storage.scopeId === undefined ? "unscoped" : `scoped:${storage.scopeId}`;
|
|
291
|
+
const { customServers, isHydrated, updateCustomServers, removeCustomServer } =
|
|
292
|
+
useResource(
|
|
293
|
+
withKey(
|
|
294
|
+
storageScopeKey,
|
|
295
|
+
McpCustomServersResource({
|
|
296
|
+
storage,
|
|
297
|
+
scopeKey: storageScopeKey,
|
|
298
|
+
persistenceQueues,
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
137
301
|
);
|
|
138
|
-
}, [customServers]);
|
|
139
302
|
|
|
140
303
|
const serverElements = useMemo(() => {
|
|
141
304
|
assertUniqueServerIds([
|
|
@@ -170,24 +333,30 @@ const useMcpManagerResource = (
|
|
|
170
333
|
const customElements = customServers.map((s) =>
|
|
171
334
|
withKey(
|
|
172
335
|
s.id,
|
|
173
|
-
McpServerResource(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
336
|
+
McpServerResource(
|
|
337
|
+
withMcpServerRemovalFence(
|
|
338
|
+
{
|
|
339
|
+
id: s.id,
|
|
340
|
+
kind: "custom",
|
|
341
|
+
name: s.name,
|
|
342
|
+
url: s.url,
|
|
343
|
+
auth: s.auth,
|
|
344
|
+
storage,
|
|
345
|
+
redirectUri,
|
|
346
|
+
autoConnect,
|
|
347
|
+
connectionTimeout: s.connectionTimeout ?? connectionTimeout,
|
|
348
|
+
...(s.cache !== undefined ? { cache: s.cache } : {}),
|
|
349
|
+
...(s.elicitation !== undefined
|
|
350
|
+
? { elicitation: s.elicitation }
|
|
351
|
+
: {}),
|
|
352
|
+
onRemove: async () => {
|
|
353
|
+
removeCustomServer(s.id);
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
() =>
|
|
357
|
+
holdCustomServerPersistence(persistenceQueues, storageScopeKey),
|
|
358
|
+
),
|
|
359
|
+
),
|
|
191
360
|
),
|
|
192
361
|
);
|
|
193
362
|
return [...connectorElements, ...customElements];
|
|
@@ -198,6 +367,9 @@ const useMcpManagerResource = (
|
|
|
198
367
|
redirectUri,
|
|
199
368
|
autoConnect,
|
|
200
369
|
connectionTimeout,
|
|
370
|
+
removeCustomServer,
|
|
371
|
+
persistenceQueues,
|
|
372
|
+
storageScopeKey,
|
|
201
373
|
]);
|
|
202
374
|
|
|
203
375
|
const lookup = useClientLookup(serverElements);
|
|
@@ -289,7 +461,7 @@ const useMcpManagerResource = (
|
|
|
289
461
|
...(elicitation !== undefined ? { elicitation } : {}),
|
|
290
462
|
createdAt: Date.now(),
|
|
291
463
|
};
|
|
292
|
-
|
|
464
|
+
updateCustomServers((prev) => [...prev, record]);
|
|
293
465
|
return record.id;
|
|
294
466
|
},
|
|
295
467
|
removeServer: async (id) => {
|
|
@@ -307,8 +479,18 @@ const useMcpManagerResource = (
|
|
|
307
479
|
try {
|
|
308
480
|
await lookup.get({ key: id }).remove();
|
|
309
481
|
} catch {
|
|
310
|
-
|
|
311
|
-
|
|
482
|
+
const releasePersistence = holdCustomServerPersistence(
|
|
483
|
+
persistenceQueues,
|
|
484
|
+
storageScopeKey,
|
|
485
|
+
);
|
|
486
|
+
try {
|
|
487
|
+
await clearOAuthProviderAuthState(storage, id);
|
|
488
|
+
removeCustomServer(id);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
releasePersistence();
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
releasePersistence();
|
|
312
494
|
}
|
|
313
495
|
},
|
|
314
496
|
};
|
|
@@ -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]?.();
|
|
@@ -113,8 +113,8 @@ const requestElicitation = (
|
|
|
113
113
|
client: any,
|
|
114
114
|
message: string,
|
|
115
115
|
requestedSchema: unknown,
|
|
116
|
-
context: { signal: AbortSignal } = {
|
|
117
|
-
signal: new AbortController().signal,
|
|
116
|
+
context: { mcpReq: { signal: AbortSignal } } = {
|
|
117
|
+
mcpReq: { signal: new AbortController().signal },
|
|
118
118
|
},
|
|
119
119
|
) => {
|
|
120
120
|
const handler = client.requestHandlers.get("elicitation/create");
|
|
@@ -156,7 +156,6 @@ const mount = (
|
|
|
156
156
|
cache?: { readonly defaultTtlMs?: number } | undefined;
|
|
157
157
|
elicitation?: boolean | undefined;
|
|
158
158
|
kind?: "connector" | "custom" | undefined;
|
|
159
|
-
storage?: MCPStorage | undefined;
|
|
160
159
|
onRemove?: (() => Promise<void>) | undefined;
|
|
161
160
|
},
|
|
162
161
|
onMount?: (server: ClientOutput<"mcpServer">) => void,
|
|
@@ -292,7 +291,9 @@ describe("McpServerResource automatic authentication", () => {
|
|
|
292
291
|
});
|
|
293
292
|
|
|
294
293
|
try {
|
|
295
|
-
await waitFor(
|
|
294
|
+
await waitFor(
|
|
295
|
+
() => vi.mocked(storage.loadAuthState).mock.calls.length > 0,
|
|
296
|
+
);
|
|
296
297
|
await flushMacrotask();
|
|
297
298
|
|
|
298
299
|
expect(root.getValue().getState()).toMatchObject({
|
|
@@ -338,7 +339,9 @@ describe("McpServerResource automatic authentication", () => {
|
|
|
338
339
|
});
|
|
339
340
|
|
|
340
341
|
try {
|
|
341
|
-
await waitFor(
|
|
342
|
+
await waitFor(
|
|
343
|
+
() => vi.mocked(storage.loadAuthState).mock.calls.length > 0,
|
|
344
|
+
);
|
|
342
345
|
await flushMacrotask();
|
|
343
346
|
|
|
344
347
|
expect(root.getValue().getState()).toMatchObject({
|
|
@@ -1474,7 +1477,7 @@ describe("McpServerResource elicitation", () => {
|
|
|
1474
1477
|
type: "object",
|
|
1475
1478
|
properties: {},
|
|
1476
1479
|
},
|
|
1477
|
-
{ signal: controller.signal },
|
|
1480
|
+
{ mcpReq: { signal: controller.signal } },
|
|
1478
1481
|
);
|
|
1479
1482
|
await waitForResourceUpdate(
|
|
1480
1483
|
() => root.getValue().getState().pendingElicitations.length === 1,
|
|
@@ -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;
|
|
@@ -379,7 +380,7 @@ const useMcpServerResourceInstance = (
|
|
|
379
380
|
};
|
|
380
381
|
elicitationResolversRef.current.set(id, {
|
|
381
382
|
resolve,
|
|
382
|
-
signal: context.signal,
|
|
383
|
+
signal: context.mcpReq.signal,
|
|
383
384
|
onAbort,
|
|
384
385
|
requestedSchema,
|
|
385
386
|
});
|
|
@@ -394,10 +395,10 @@ const useMcpServerResourceInstance = (
|
|
|
394
395
|
]);
|
|
395
396
|
const entry = elicitationResolversRef.current.get(id);
|
|
396
397
|
if (entry) {
|
|
397
|
-
if (context.signal.aborted) {
|
|
398
|
+
if (context.mcpReq.signal.aborted) {
|
|
398
399
|
entry.onAbort();
|
|
399
400
|
} else {
|
|
400
|
-
context.signal.addEventListener("abort", entry.onAbort, {
|
|
401
|
+
context.mcpReq.signal.addEventListener("abort", entry.onAbort, {
|
|
401
402
|
once: true,
|
|
402
403
|
});
|
|
403
404
|
}
|
|
@@ -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;
|
|
@@ -708,7 +712,7 @@ const useMcpServerResourceInstance = (
|
|
|
708
712
|
): readonly { property: string; message: string }[] | undefined => {
|
|
709
713
|
if (response.action === "accept") {
|
|
710
714
|
const entry = elicitationResolversRef.current.get(id);
|
|
711
|
-
if (!entry) return;
|
|
715
|
+
if (!entry) return undefined;
|
|
712
716
|
|
|
713
717
|
if (
|
|
714
718
|
typeof response.content !== "object" ||
|
|
@@ -748,10 +752,11 @@ const useMcpServerResourceInstance = (
|
|
|
748
752
|
content: response.content as ElicitResult["content"],
|
|
749
753
|
};
|
|
750
754
|
resolvePendingElicitation(id, result);
|
|
751
|
-
return;
|
|
755
|
+
return undefined;
|
|
752
756
|
}
|
|
753
757
|
|
|
754
758
|
resolvePendingElicitation(id, { action: response.action });
|
|
759
|
+
return undefined;
|
|
755
760
|
},
|
|
756
761
|
};
|
|
757
762
|
};
|
|
@@ -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[]>;
|