@assistant-ui/react-mcp 0.0.16 → 0.0.17
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/mcp-scope.d.ts +3 -0
- package/dist/mcp-scope.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.d.ts +2 -1
- package/dist/resources/McpManagerResource.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.js +122 -107
- package/dist/resources/McpManagerResource.js.map +1 -1
- package/dist/resources/McpServerResource.d.ts +1 -0
- package/dist/resources/McpServerResource.d.ts.map +1 -1
- package/dist/resources/McpServerResource.js +18 -2
- package/dist/resources/McpServerResource.js.map +1 -1
- package/dist/utils/serverId.d.ts +2 -1
- package/dist/utils/serverId.d.ts.map +1 -1
- package/dist/utils/serverId.js +8 -1
- package/dist/utils/serverId.js.map +1 -1
- package/package.json +8 -5
- package/src/mcp-scope.ts +3 -0
- package/src/resources/McpManagerResource.test.ts +40 -0
- package/src/resources/McpManagerResource.ts +21 -2
- package/src/resources/McpServerResource.test.ts +221 -0
- package/src/resources/McpServerResource.ts +41 -2
- package/src/utils/serverId.ts +12 -0
package/dist/mcp-scope.d.ts
CHANGED
|
@@ -19,12 +19,14 @@ type MCPConnector = {
|
|
|
19
19
|
url: string;
|
|
20
20
|
icon?: string | undefined;
|
|
21
21
|
auth: MCPAuthConfig;
|
|
22
|
+
connectionTimeout?: number | undefined;
|
|
22
23
|
};
|
|
23
24
|
type MCPCustomServerRecord = {
|
|
24
25
|
id: string;
|
|
25
26
|
name: string;
|
|
26
27
|
url: string;
|
|
27
28
|
auth: MCPAuthConfig;
|
|
29
|
+
connectionTimeout?: number | undefined;
|
|
28
30
|
createdAt: number;
|
|
29
31
|
};
|
|
30
32
|
type MCPServerKind = "connector" | "custom";
|
|
@@ -84,6 +86,7 @@ type MCPManagerMethods = {
|
|
|
84
86
|
name: string;
|
|
85
87
|
url: string;
|
|
86
88
|
auth: MCPAuthConfig;
|
|
89
|
+
connectionTimeout?: number | undefined;
|
|
87
90
|
}) => Promise<string>;
|
|
88
91
|
removeServer: (id: string) => Promise<void>;
|
|
89
92
|
};
|
package/dist/mcp-scope.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-scope.d.ts","names":[],"sources":["../src/mcp-scope.ts"],"mappings":";KAEY,aAAA;EACN,IAAA;AAAA;EACA,IAAA;EAAgB,KAAA;AAAA;EAEhB,IAAA;EACA,MAAA;EACA,qBAAA;EACA,aAAA;EACA,oBAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,KAGM,YAAA;EACV,EAAA;EACA,IAAA;EACA,GAAA;EACA,IAAA;EACA,IAAA,EAAM,aAAa;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"mcp-scope.d.ts","names":[],"sources":["../src/mcp-scope.ts"],"mappings":";KAEY,aAAA;EACN,IAAA;AAAA;EACA,IAAA;EAAgB,KAAA;AAAA;EAEhB,IAAA;EACA,MAAA;EACA,qBAAA;EACA,aAAA;EACA,oBAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,KAGM,YAAA;EACV,EAAA;EACA,IAAA;EACA,GAAA;EACA,IAAA;EACA,IAAA,EAAM,aAAa;EACnB,iBAAA;AAAA;AAAA,KAGU,qBAAA;EACV,EAAA;EACA,IAAA;EACA,GAAA;EACA,IAAA,EAAM,aAAa;EACnB,iBAAA;EACA,SAAA;AAAA;AAAA,KAGU,aAAA;AAAA,KAEA,kBAAA;AAAA,KAQA,WAAA;EACV,IAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,KAGU,cAAA;EACV,EAAA;EACA,IAAA,EAAM,aAAA;EACN,IAAA;EACA,GAAA;EACA,IAAA;EACA,eAAA,EAAiB,kBAAA;EACjB,SAAA;IAAa,OAAA;EAAA;EACb,KAAA,EAAO,WAAA;EACP,gBAAA;AAAA;AAAA,KAGU,eAAA;EACV,OAAA,EAAS,cAAA;EACT,UAAA,EAAY,cAAA;EACZ,aAAA,EAAe,cAAA;EACf,UAAA;AAAA;AAAA,KAGU,gBAAA;EACV,QAAA,QAAgB,cAAA;EAChB,OAAA,QAAe,OAAA;EACf,UAAA,QAAkB,OAAA;EAClB,MAAA,QAAc,OAAA;EACd,QAAA,GAAW,IAAA,UAAc,IAAA,cAAkB,OAAA,WA3BhC;EA6BX,YAAA,GAAe,GAAA,aAAgB,OAAA,WA1BP;EA4BxB,YAAA,GAAe,WAAA,aAAwB,OAAA;AAAA;AAAA,KAG7B,cAAA;EACN,EAAA;AAAA;EACA,IAAA;EAAmB,KAAA;AAAA;EACnB,IAAA;EAAgB,KAAA;AAAA;AAAA,KAEV,iBAAA;EACV,QAAA,QAAgB,eAAA,EA/BC;EAiCjB,MAAA,GAAS,KAAA,EAAO,cAAA,KAAmB,gBAAA,EAhCtB;EAkCb,SAAA,GAAY,KAAA;IAAS,KAAA;EAAA,MAAoB,gBAAA,EAhCzB;EAkChB,YAAA,GAAe,KAAA;IAAS,KAAA;EAAA,MAAoB,gBAAA;EAC5C,eAAA,GAAkB,KAAA;IAChB,IAAA;IACA,GAAA;IACA,IAAA,EAAM,aAAA;IACN,iBAAA;EAAA,MACI,OAAA;EACN,YAAA,GAAe,EAAA,aAAe,OAAA;AAAA;AAAA;EAAA,UAIpB,aAAA;IACR,GAAA;MACE,OAAA,EAAS,iBAAA;IAAA;IAEX,SAAA;MACE,OAAA,EAAS,gBAAA;MACT,IAAA;QAAQ,MAAA;QAAe,KAAA,EAAO,cAAA;MAAA;IAAA;EAAA;AAAA"}
|
|
@@ -7,7 +7,8 @@ type McpManagerResourceProps = {
|
|
|
7
7
|
connectors?: MCPConnector[] | undefined;
|
|
8
8
|
storage?: MCPStorageElement | undefined; /** OAuth redirect target. Defaults to `${origin}/mcp/callback`. */
|
|
9
9
|
oauthRedirectUri?: string | undefined; /** Connect on mount when usable auth exists. Default true. */
|
|
10
|
-
autoConnect?: boolean | undefined;
|
|
10
|
+
autoConnect?: boolean | undefined; /** Optional timeout in milliseconds for connect/listTools calls. Disabled by default. */
|
|
11
|
+
connectionTimeout?: number | undefined;
|
|
11
12
|
};
|
|
12
13
|
declare const McpManagerResource: import("@assistant-ui/tap").Resource<ClientOutput<"mcp">, [props: McpManagerResourceProps]>;
|
|
13
14
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpManagerResource.d.ts","names":[],"sources":["../../src/resources/McpManagerResource.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"McpManagerResource.d.ts","names":[],"sources":["../../src/resources/McpManagerResource.ts"],"mappings":";;;;;KAqBY,uBAAA;EACV,UAAA,GAAa,YAAA;EACb,OAAA,GAAU,iBAAiB,cAFM;EAIjC,gBAAA,uBAF2B;EAI3B,WAAA,wBALa;EAOb,iBAAA;AAAA;AAAA,cA4OW,kBAAA,8BAAkB,QAAA,CAAA,YAAA,UAAA,KAAA,EAAA,uBAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertUniqueServerIds } from "../utils/serverId.js";
|
|
1
2
|
import { McpServerResource } from "./McpServerResource.js";
|
|
2
3
|
import { McpLocalStorage } from "./storage/McpLocalStorage.js";
|
|
3
4
|
import { attachTransformScopes, useAssistantClientRef, useClientLookup } from "@assistant-ui/store";
|
|
@@ -12,7 +13,7 @@ function defaultRedirectUri() {
|
|
|
12
13
|
}
|
|
13
14
|
const NO_CONNECTORS = [];
|
|
14
15
|
const useMcpManagerResource = (props) => {
|
|
15
|
-
const $ = c(
|
|
16
|
+
const $ = c(72);
|
|
16
17
|
const connectors = props.connectors ?? NO_CONNECTORS;
|
|
17
18
|
const autoConnect = props.autoConnect ?? true;
|
|
18
19
|
let t0;
|
|
@@ -22,6 +23,7 @@ const useMcpManagerResource = (props) => {
|
|
|
22
23
|
$[1] = t0;
|
|
23
24
|
} else t0 = $[1];
|
|
24
25
|
const redirectUri = t0;
|
|
26
|
+
const connectionTimeout = props.connectionTimeout;
|
|
25
27
|
let t1;
|
|
26
28
|
if ($[2] !== props.storage) {
|
|
27
29
|
t1 = props.storage ?? McpLocalStorage();
|
|
@@ -110,88 +112,94 @@ const useMcpManagerResource = (props) => {
|
|
|
110
112
|
$[16] = t8;
|
|
111
113
|
} else t8 = $[16];
|
|
112
114
|
useEffect(t7, t8);
|
|
115
|
+
assertUniqueServerIds([...connectors.map(_temp2), ...customServers.map(_temp3)]);
|
|
113
116
|
let t9;
|
|
114
|
-
if ($[17] !== autoConnect || $[18] !==
|
|
117
|
+
if ($[17] !== autoConnect || $[18] !== connectionTimeout || $[19] !== connectors || $[20] !== customServers || $[21] !== redirectUri || $[22] !== storage) {
|
|
115
118
|
let t10;
|
|
116
|
-
if ($[
|
|
117
|
-
t10 = (
|
|
118
|
-
id:
|
|
119
|
+
if ($[24] !== autoConnect || $[25] !== connectionTimeout || $[26] !== redirectUri || $[27] !== storage) {
|
|
120
|
+
t10 = (c_0) => withKey(c_0.id, McpServerResource({
|
|
121
|
+
id: c_0.id,
|
|
119
122
|
kind: "connector",
|
|
120
|
-
name:
|
|
121
|
-
url:
|
|
122
|
-
icon:
|
|
123
|
-
auth:
|
|
123
|
+
name: c_0.name,
|
|
124
|
+
url: c_0.url,
|
|
125
|
+
icon: c_0.icon,
|
|
126
|
+
auth: c_0.auth,
|
|
124
127
|
storage,
|
|
125
128
|
redirectUri,
|
|
126
129
|
autoConnect,
|
|
127
|
-
|
|
130
|
+
connectionTimeout: c_0.connectionTimeout ?? connectionTimeout,
|
|
131
|
+
onRemove: _temp4
|
|
128
132
|
}));
|
|
129
|
-
$[
|
|
130
|
-
$[
|
|
131
|
-
$[
|
|
132
|
-
$[
|
|
133
|
-
|
|
133
|
+
$[24] = autoConnect;
|
|
134
|
+
$[25] = connectionTimeout;
|
|
135
|
+
$[26] = redirectUri;
|
|
136
|
+
$[27] = storage;
|
|
137
|
+
$[28] = t10;
|
|
138
|
+
} else t10 = $[28];
|
|
134
139
|
const connectorElements = connectors.map(t10);
|
|
135
140
|
let t11;
|
|
136
|
-
if ($[
|
|
137
|
-
t11 = (
|
|
138
|
-
id:
|
|
141
|
+
if ($[29] !== autoConnect || $[30] !== connectionTimeout || $[31] !== redirectUri || $[32] !== storage) {
|
|
142
|
+
t11 = (s_0) => withKey(s_0.id, McpServerResource({
|
|
143
|
+
id: s_0.id,
|
|
139
144
|
kind: "custom",
|
|
140
|
-
name:
|
|
141
|
-
url:
|
|
142
|
-
auth:
|
|
145
|
+
name: s_0.name,
|
|
146
|
+
url: s_0.url,
|
|
147
|
+
auth: s_0.auth,
|
|
143
148
|
storage,
|
|
144
149
|
redirectUri,
|
|
145
150
|
autoConnect,
|
|
151
|
+
connectionTimeout: s_0.connectionTimeout ?? connectionTimeout,
|
|
146
152
|
onRemove: async () => {
|
|
147
|
-
setCustomServers((prev_0) => prev_0.filter((x) => x.id !==
|
|
153
|
+
setCustomServers((prev_0) => prev_0.filter((x) => x.id !== s_0.id));
|
|
148
154
|
}
|
|
149
155
|
}));
|
|
150
|
-
$[
|
|
151
|
-
$[
|
|
152
|
-
$[
|
|
153
|
-
$[
|
|
154
|
-
|
|
156
|
+
$[29] = autoConnect;
|
|
157
|
+
$[30] = connectionTimeout;
|
|
158
|
+
$[31] = redirectUri;
|
|
159
|
+
$[32] = storage;
|
|
160
|
+
$[33] = t11;
|
|
161
|
+
} else t11 = $[33];
|
|
155
162
|
const customElements = customServers.map(t11);
|
|
156
163
|
t9 = [...connectorElements, ...customElements];
|
|
157
164
|
$[17] = autoConnect;
|
|
158
|
-
$[18] =
|
|
159
|
-
$[19] =
|
|
160
|
-
$[20] =
|
|
161
|
-
$[21] =
|
|
162
|
-
$[22] =
|
|
163
|
-
|
|
165
|
+
$[18] = connectionTimeout;
|
|
166
|
+
$[19] = connectors;
|
|
167
|
+
$[20] = customServers;
|
|
168
|
+
$[21] = redirectUri;
|
|
169
|
+
$[22] = storage;
|
|
170
|
+
$[23] = t9;
|
|
171
|
+
} else t9 = $[23];
|
|
164
172
|
const lookup = useClientLookup(t9);
|
|
165
173
|
const all = lookup.state;
|
|
166
174
|
let t10;
|
|
167
|
-
if ($[
|
|
168
|
-
t10 = all.filter(
|
|
169
|
-
$[
|
|
170
|
-
$[
|
|
171
|
-
} else t10 = $[
|
|
175
|
+
if ($[34] !== all) {
|
|
176
|
+
t10 = all.filter(_temp5);
|
|
177
|
+
$[34] = all;
|
|
178
|
+
$[35] = t10;
|
|
179
|
+
} else t10 = $[35];
|
|
172
180
|
let t11;
|
|
173
|
-
if ($[
|
|
174
|
-
t11 = all.filter(
|
|
175
|
-
$[
|
|
176
|
-
$[
|
|
177
|
-
} else t11 = $[
|
|
181
|
+
if ($[36] !== all) {
|
|
182
|
+
t11 = all.filter(_temp6);
|
|
183
|
+
$[36] = all;
|
|
184
|
+
$[37] = t11;
|
|
185
|
+
} else t11 = $[37];
|
|
178
186
|
let t12;
|
|
179
|
-
if ($[
|
|
187
|
+
if ($[38] !== all || $[39] !== isHydrated || $[40] !== t10 || $[41] !== t11) {
|
|
180
188
|
t12 = {
|
|
181
189
|
servers: all,
|
|
182
190
|
connectors: t10,
|
|
183
191
|
customServers: t11,
|
|
184
192
|
isHydrated
|
|
185
193
|
};
|
|
186
|
-
$[
|
|
187
|
-
$[
|
|
188
|
-
$[
|
|
189
|
-
$[
|
|
190
|
-
$[
|
|
191
|
-
} else t12 = $[
|
|
194
|
+
$[38] = all;
|
|
195
|
+
$[39] = isHydrated;
|
|
196
|
+
$[40] = t10;
|
|
197
|
+
$[41] = t11;
|
|
198
|
+
$[42] = t12;
|
|
199
|
+
} else t12 = $[42];
|
|
192
200
|
const state = t12;
|
|
193
201
|
let out;
|
|
194
|
-
if ($[
|
|
202
|
+
if ($[43] !== lookup || $[44] !== state.servers) {
|
|
195
203
|
out = {};
|
|
196
204
|
for (const server of state.servers) {
|
|
197
205
|
if (server.connectionState !== "connected") continue;
|
|
@@ -205,62 +213,62 @@ const useMcpManagerResource = (props) => {
|
|
|
205
213
|
};
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
|
-
$[
|
|
209
|
-
$[
|
|
210
|
-
$[
|
|
211
|
-
} else out = $[
|
|
216
|
+
$[43] = lookup;
|
|
217
|
+
$[44] = state.servers;
|
|
218
|
+
$[45] = out;
|
|
219
|
+
} else out = $[45];
|
|
212
220
|
const toolkit = out;
|
|
213
221
|
const clientRef = useAssistantClientRef();
|
|
214
222
|
let t13;
|
|
215
223
|
let t14;
|
|
216
|
-
if ($[
|
|
224
|
+
if ($[46] !== clientRef || $[47] !== toolkit) {
|
|
217
225
|
t13 = () => {
|
|
218
226
|
const client = clientRef.current;
|
|
219
227
|
if (!client) return;
|
|
220
228
|
return client.modelContext().register({ getModelContext: () => ({ tools: toolkit }) });
|
|
221
229
|
};
|
|
222
230
|
t14 = [toolkit, clientRef];
|
|
223
|
-
$[
|
|
224
|
-
$[
|
|
225
|
-
$[
|
|
226
|
-
$[
|
|
231
|
+
$[46] = clientRef;
|
|
232
|
+
$[47] = toolkit;
|
|
233
|
+
$[48] = t13;
|
|
234
|
+
$[49] = t14;
|
|
227
235
|
} else {
|
|
228
|
-
t13 = $[
|
|
229
|
-
t14 = $[
|
|
236
|
+
t13 = $[48];
|
|
237
|
+
t14 = $[49];
|
|
230
238
|
}
|
|
231
239
|
useEffect(t13, t14);
|
|
232
240
|
let t15;
|
|
233
|
-
if ($[
|
|
241
|
+
if ($[50] !== lookup || $[51] !== state) {
|
|
234
242
|
t15 = (kind, index) => {
|
|
235
243
|
const list = kind === "connector" ? state.connectors : state.customServers;
|
|
236
244
|
const entry = list[index];
|
|
237
245
|
if (!entry) throw new Error(`McpManagerResource: no ${kind} at index ${index} (length ${list.length})`);
|
|
238
246
|
return lookup.get({ key: entry.id });
|
|
239
247
|
};
|
|
240
|
-
$[
|
|
241
|
-
$[
|
|
242
|
-
$[
|
|
243
|
-
} else t15 = $[
|
|
248
|
+
$[50] = lookup;
|
|
249
|
+
$[51] = state;
|
|
250
|
+
$[52] = t15;
|
|
251
|
+
} else t15 = $[52];
|
|
244
252
|
const serverByKind = t15;
|
|
245
253
|
let t16;
|
|
246
|
-
if ($[
|
|
254
|
+
if ($[53] !== state) {
|
|
247
255
|
t16 = () => state;
|
|
248
|
-
$[
|
|
249
|
-
$[
|
|
250
|
-
} else t16 = $[
|
|
256
|
+
$[53] = state;
|
|
257
|
+
$[54] = t16;
|
|
258
|
+
} else t16 = $[54];
|
|
251
259
|
let t17;
|
|
252
|
-
if ($[
|
|
260
|
+
if ($[55] !== lookup || $[56] !== serverByKind) {
|
|
253
261
|
t17 = (query) => {
|
|
254
262
|
if ("id" in query) return lookup.get({ key: query.id });
|
|
255
263
|
return serverByKind(query.kind, query.index);
|
|
256
264
|
};
|
|
257
|
-
$[
|
|
258
|
-
$[
|
|
259
|
-
$[
|
|
260
|
-
} else t17 = $[
|
|
265
|
+
$[55] = lookup;
|
|
266
|
+
$[56] = serverByKind;
|
|
267
|
+
$[57] = t17;
|
|
268
|
+
} else t17 = $[57];
|
|
261
269
|
let t18;
|
|
262
270
|
let t19;
|
|
263
|
-
if ($[
|
|
271
|
+
if ($[58] !== serverByKind) {
|
|
264
272
|
t18 = (t20) => {
|
|
265
273
|
const { index: index_0 } = t20;
|
|
266
274
|
return serverByKind("connector", index_0);
|
|
@@ -269,47 +277,48 @@ const useMcpManagerResource = (props) => {
|
|
|
269
277
|
const { index: index_1 } = t21;
|
|
270
278
|
return serverByKind("custom", index_1);
|
|
271
279
|
};
|
|
272
|
-
$[
|
|
273
|
-
$[
|
|
274
|
-
$[
|
|
280
|
+
$[58] = serverByKind;
|
|
281
|
+
$[59] = t18;
|
|
282
|
+
$[60] = t19;
|
|
275
283
|
} else {
|
|
276
|
-
t18 = $[
|
|
277
|
-
t19 = $[
|
|
284
|
+
t18 = $[59];
|
|
285
|
+
t19 = $[60];
|
|
278
286
|
}
|
|
279
287
|
let t20;
|
|
280
|
-
if ($[
|
|
288
|
+
if ($[61] === Symbol.for("react.memo_cache_sentinel")) {
|
|
281
289
|
t20 = async (t21) => {
|
|
282
|
-
const { name, url, auth } = t21;
|
|
290
|
+
const { name, url, auth, connectionTimeout: connectionTimeout_0 } = t21;
|
|
283
291
|
const record = {
|
|
284
292
|
id: typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
285
293
|
name,
|
|
286
294
|
url,
|
|
287
295
|
auth,
|
|
296
|
+
connectionTimeout: connectionTimeout_0,
|
|
288
297
|
createdAt: Date.now()
|
|
289
298
|
};
|
|
290
299
|
setCustomServers((prev_1) => [...prev_1, record]);
|
|
291
300
|
return record.id;
|
|
292
301
|
};
|
|
293
|
-
$[
|
|
294
|
-
} else t20 = $[
|
|
302
|
+
$[61] = t20;
|
|
303
|
+
} else t20 = $[61];
|
|
295
304
|
let t21;
|
|
296
|
-
if ($[
|
|
305
|
+
if ($[62] !== lookup || $[63] !== state || $[64] !== storage) {
|
|
297
306
|
t21 = async (id) => {
|
|
298
|
-
if (state.connectors.some((
|
|
307
|
+
if (state.connectors.some((c_1) => c_1.id === id)) throw new Error(`Cannot remove connector "${id}" — connectors are app-defined and not removable. Use a custom server id instead.`);
|
|
299
308
|
try {
|
|
300
309
|
await lookup.get({ key: id }).remove();
|
|
301
310
|
} catch {
|
|
302
311
|
await storage.clearAuthState(id);
|
|
303
|
-
setCustomServers((prev_2) => prev_2.filter((
|
|
312
|
+
setCustomServers((prev_2) => prev_2.filter((s_3) => s_3.id !== id));
|
|
304
313
|
}
|
|
305
314
|
};
|
|
306
|
-
$[
|
|
307
|
-
$[
|
|
308
|
-
$[
|
|
309
|
-
$[
|
|
310
|
-
} else t21 = $[
|
|
315
|
+
$[62] = lookup;
|
|
316
|
+
$[63] = state;
|
|
317
|
+
$[64] = storage;
|
|
318
|
+
$[65] = t21;
|
|
319
|
+
} else t21 = $[65];
|
|
311
320
|
let t22;
|
|
312
|
-
if ($[
|
|
321
|
+
if ($[66] !== t16 || $[67] !== t17 || $[68] !== t18 || $[69] !== t19 || $[70] !== t21) {
|
|
313
322
|
t22 = {
|
|
314
323
|
getState: t16,
|
|
315
324
|
server: t17,
|
|
@@ -318,13 +327,13 @@ const useMcpManagerResource = (props) => {
|
|
|
318
327
|
addCustomServer: t20,
|
|
319
328
|
removeServer: t21
|
|
320
329
|
};
|
|
321
|
-
$[
|
|
322
|
-
$[
|
|
323
|
-
$[
|
|
324
|
-
$[
|
|
325
|
-
$[
|
|
326
|
-
$[
|
|
327
|
-
} else t22 = $[
|
|
330
|
+
$[66] = t16;
|
|
331
|
+
$[67] = t17;
|
|
332
|
+
$[68] = t18;
|
|
333
|
+
$[69] = t19;
|
|
334
|
+
$[70] = t21;
|
|
335
|
+
$[71] = t22;
|
|
336
|
+
} else t22 = $[71];
|
|
328
337
|
return t22;
|
|
329
338
|
};
|
|
330
339
|
const McpManagerResource = resource(useMcpManagerResource);
|
|
@@ -334,12 +343,18 @@ attachTransformScopes(useMcpManagerResource, (scopes, parent) => {
|
|
|
334
343
|
function _temp(r) {
|
|
335
344
|
return r.id;
|
|
336
345
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return s_0.kind === "connector";
|
|
346
|
+
function _temp2(c) {
|
|
347
|
+
return c.id;
|
|
340
348
|
}
|
|
341
|
-
function
|
|
342
|
-
return
|
|
349
|
+
function _temp3(s) {
|
|
350
|
+
return s.id;
|
|
351
|
+
}
|
|
352
|
+
async function _temp4() {}
|
|
353
|
+
function _temp5(s_1) {
|
|
354
|
+
return s_1.kind === "connector";
|
|
355
|
+
}
|
|
356
|
+
function _temp6(s_2) {
|
|
357
|
+
return s_2.kind === "custom";
|
|
343
358
|
}
|
|
344
359
|
//#endregion
|
|
345
360
|
export { McpManagerResource };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpManagerResource.js","names":["useState","useEffect","useMemo","useEffectEvent","useRef","useResource","resource","withKey","useClientLookup","useAssistantClientRef","attachTransformScopes","ClientOutput","ModelContext","Tool","McpServerResource","McpLocalStorage","MCPStorageElement","MCPAuthConfig","MCPConnector","MCPCustomServerRecord","MCPManagerState","McpManagerResourceProps","connectors","storage","oauthRedirectUri","autoConnect","defaultRedirectUri","window","location","origin","NO_CONNECTORS","useMcpManagerResource","props","$","_c","t0","redirectUri","t1","storageElement","t2","Symbol","for","customServers","setCustomServers","isHydrated","setIsHydrated","hydratedRef","t3","signal","markHydrated","cancelled","current","records","loadCustomServers","t4","error","prev","length","persistedIds","Set","map","_temp","filter","r_0","has","r","id","hydrate","signal_0","t5","t6","records_0","saveCustomServers","persistCustomServers","t7","t8","t9","t10","c","kind","name","url","icon","auth","onRemove","_temp2","connectorElements","t11","s","prev_0","x","customElements","serverElements","lookup","all","state","_temp3","_temp4","t12","servers","out","server","connectionState","tool","tools","fullName","type","description","undefined","parameters","inputSchema","execute","args","get","key","callTool","toolkit","clientRef","t13","t14","client","modelContext","register","getModelContext","t15","index","list","entry","Error","serverByKind","t16","t17","query","t18","t19","t20","index_0","t21","index_1","record","crypto","randomUUID","Date","now","Math","random","toString","slice","createdAt","prev_1","some","c_0","remove","clearAuthState","prev_2","s_2","t22","getState","connector","customServer","addCustomServer","removeServer","McpManagerResource","scopes","parent","source","s_0","s_1"],"sources":["../../src/resources/McpManagerResource.ts"],"sourcesContent":["import { useState, useEffect, useMemo, useEffectEvent, useRef } from \"react\";\nimport { useResource, resource, withKey } from \"@assistant-ui/tap\";\nimport {\n useClientLookup,\n useAssistantClientRef,\n attachTransformScopes,\n type ClientOutput,\n} from \"@assistant-ui/store\";\nimport { ModelContext } from \"@assistant-ui/core/store\";\nimport type { Tool } from \"assistant-stream\";\nimport { McpServerResource } from \"./McpServerResource\";\nimport { McpLocalStorage } from \"./storage/McpLocalStorage\";\nimport type { MCPStorageElement } from \"./storage/types\";\nimport type {\n MCPAuthConfig,\n MCPConnector,\n MCPCustomServerRecord,\n MCPManagerState,\n} from \"../mcp-scope\";\n\nexport type McpManagerResourceProps = {\n connectors?: MCPConnector[] | undefined;\n storage?: MCPStorageElement | undefined;\n /** OAuth redirect target. Defaults to `${origin}/mcp/callback`. */\n oauthRedirectUri?: string | undefined;\n /** Connect on mount when usable auth exists. Default true. */\n autoConnect?: boolean | undefined;\n};\n\nfunction defaultRedirectUri(): string {\n if (typeof window === \"undefined\") return \"\";\n return `${window.location.origin}/mcp/callback`;\n}\n\n// Stable empty fallback so an absent `connectors` prop doesn't produce a fresh\n// array each render (which would invalidate the serverElements memo below).\nconst NO_CONNECTORS: MCPConnector[] = [];\n\nconst useMcpManagerResource = (\n props: McpManagerResourceProps,\n): ClientOutput<\"mcp\"> => {\n const connectors = props.connectors ?? NO_CONNECTORS;\n const autoConnect = props.autoConnect ?? true;\n const redirectUri = props.oauthRedirectUri ?? defaultRedirectUri();\n\n const storageElement = props.storage ?? McpLocalStorage();\n const storage = useResource(storageElement);\n\n const [customServers, setCustomServers] = useState<MCPCustomServerRecord[]>(\n [],\n );\n const [isHydrated, setIsHydrated] = useState(false);\n\n const hydratedRef = useRef(false);\n\n const hydrate = useEffectEvent(async (signal: { cancelled: boolean }) => {\n const markHydrated = () => {\n if (!signal.cancelled) {\n hydratedRef.current = true;\n setIsHydrated(true);\n }\n };\n\n let records: Awaited<ReturnType<typeof storage.loadCustomServers>>;\n try {\n records = await storage.loadCustomServers();\n } catch (error) {\n markHydrated();\n throw error;\n }\n\n if (signal.cancelled) return;\n // Merge rather than replace so any addCustomServer calls that\n // happened before hydration resolved aren't silently overwritten.\n // Persisted order wins; pre-hydration locals append.\n setCustomServers((prev) => {\n if (prev.length === 0) return records;\n const persistedIds = new Set(records.map((r) => r.id));\n return [...records, ...prev.filter((r) => !persistedIds.has(r.id))];\n });\n markHydrated();\n });\n\n useEffect(() => {\n const signal = { cancelled: false };\n void hydrate(signal);\n return () => {\n signal.cancelled = true;\n };\n }, []);\n\n const persistCustomServers = useEffectEvent(\n async (records: MCPCustomServerRecord[]) => {\n if (!hydratedRef.current) return;\n await storage.saveCustomServers(records);\n },\n );\n\n useEffect(() => {\n void persistCustomServers(customServers);\n }, [customServers]);\n\n const serverElements = useMemo(() => {\n const connectorElements = connectors.map((c) =>\n withKey(\n c.id,\n McpServerResource({\n id: c.id,\n kind: \"connector\",\n name: c.name,\n url: c.url,\n icon: c.icon,\n auth: c.auth,\n storage,\n redirectUri,\n autoConnect,\n onRemove: async () => {\n // connectors cannot be removed\n },\n }),\n ),\n );\n const customElements = customServers.map((s) =>\n withKey(\n s.id,\n McpServerResource({\n id: s.id,\n kind: \"custom\",\n name: s.name,\n url: s.url,\n auth: s.auth,\n storage,\n redirectUri,\n autoConnect,\n onRemove: async () => {\n setCustomServers((prev) => prev.filter((x) => x.id !== s.id));\n },\n }),\n ),\n );\n return [...connectorElements, ...customElements];\n }, [connectors, customServers, storage, redirectUri, autoConnect]);\n\n const lookup = useClientLookup(serverElements);\n\n const state = useMemo<MCPManagerState>(() => {\n const all = lookup.state;\n return {\n servers: all,\n connectors: all.filter((s) => s.kind === \"connector\"),\n customServers: all.filter((s) => s.kind === \"custom\"),\n isHydrated,\n };\n }, [lookup.state, isHydrated]);\n\n // ─── Auto-register MCP tools as frontend tools in modelContext ─────\n // Build the toolkit from connected servers; re-register when the visible\n // tool surface changes. Tool names are prefixed with the server id to\n // avoid collisions across connected servers — connector ids must not\n // contain `__` (enforced by `defineConnector`); `addCustomServer`\n // generates UUIDs that satisfy the constraint by construction.\n const toolkit = useMemo<Record<string, Tool<any, any>>>(() => {\n const out: Record<string, Tool<any, any>> = {};\n for (const server of state.servers) {\n if (server.connectionState !== \"connected\") continue;\n for (const tool of server.tools) {\n const fullName = `${server.id}__${tool.name}`;\n out[fullName] = {\n type: \"frontend\",\n ...(tool.description !== undefined\n ? { description: tool.description }\n : {}),\n parameters: tool.inputSchema as never,\n execute: (args) =>\n lookup.get({ key: server.id }).callTool(tool.name, args as unknown),\n };\n }\n }\n return out;\n }, [state, lookup]);\n\n const clientRef = useAssistantClientRef();\n\n useEffect(() => {\n const client = clientRef.current;\n if (!client) return;\n return client.modelContext().register({\n getModelContext: () => ({ tools: toolkit }),\n });\n }, [toolkit, clientRef]);\n\n const serverByKind = (kind: \"connector\" | \"custom\", index: number) => {\n const list = kind === \"connector\" ? state.connectors : state.customServers;\n const entry = list[index];\n if (!entry) {\n throw new Error(\n `McpManagerResource: no ${kind} at index ${index} (length ${list.length})`,\n );\n }\n return lookup.get({ key: entry.id });\n };\n\n return {\n getState: () => state,\n server: (query) => {\n if (\"id\" in query) return lookup.get({ key: query.id });\n return serverByKind(query.kind, query.index);\n },\n connector: ({ index }) => serverByKind(\"connector\", index),\n customServer: ({ index }) => serverByKind(\"custom\", index),\n addCustomServer: async ({ name, url, auth }) => {\n const record: MCPCustomServerRecord = {\n id:\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name,\n url,\n auth: auth as MCPAuthConfig,\n createdAt: Date.now(),\n };\n setCustomServers((prev) => [...prev, record]);\n return record.id;\n },\n removeServer: async (id) => {\n // removeServer is custom-server only — connectors are app-defined\n // and not user-removable. Refuse rather than silently no-op.\n if (state.connectors.some((c) => c.id === id)) {\n throw new Error(\n `Cannot remove connector \"${id}\" — connectors are app-defined and not removable. Use a custom server id instead.`,\n );\n }\n // Delegate to McpServerResource.remove() which disconnects,\n // clears auth state, and unregisters from customServers in one\n // place. Fallback to manual cleanup if the lookup is empty\n // (server already gone).\n try {\n await lookup.get({ key: id }).remove();\n } catch {\n await storage.clearAuthState(id);\n setCustomServers((prev) => prev.filter((s) => s.id !== id));\n }\n },\n };\n};\n\nexport const McpManagerResource = resource(useMcpManagerResource);\n\n// Ensure modelContext exists as a sibling when the manager mounts. If an\n// ancestor (e.g. a chat runtime) already provides modelContext, this is a\n// no-op; otherwise it's auto-mounted alongside `mcp`.\nattachTransformScopes(useMcpManagerResource, (scopes, parent) => {\n if (!scopes.modelContext && parent.modelContext.source === null) {\n scopes.modelContext = ModelContext();\n }\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"McpManagerResource.js","names":["useState","useEffect","useMemo","useEffectEvent","useRef","useResource","resource","withKey","useClientLookup","useAssistantClientRef","attachTransformScopes","ClientOutput","ModelContext","Tool","McpServerResource","McpLocalStorage","MCPStorageElement","assertUniqueServerIds","MCPAuthConfig","MCPConnector","MCPCustomServerRecord","MCPManagerState","McpManagerResourceProps","connectors","storage","oauthRedirectUri","autoConnect","connectionTimeout","defaultRedirectUri","window","location","origin","NO_CONNECTORS","useMcpManagerResource","props","$","_c","t0","redirectUri","t1","storageElement","t2","Symbol","for","customServers","setCustomServers","isHydrated","setIsHydrated","hydratedRef","t3","signal","markHydrated","cancelled","current","records","loadCustomServers","t4","error","prev","length","persistedIds","Set","map","_temp","filter","r_0","has","r","id","hydrate","signal_0","t5","t6","records_0","saveCustomServers","persistCustomServers","t7","t8","_temp2","_temp3","t9","t10","c_0","c","kind","name","url","icon","auth","onRemove","_temp4","connectorElements","t11","s_0","s","prev_0","x","customElements","serverElements","lookup","all","state","_temp5","_temp6","t12","servers","out","server","connectionState","tool","tools","fullName","type","description","undefined","parameters","inputSchema","execute","args","get","key","callTool","toolkit","clientRef","t13","t14","client","modelContext","register","getModelContext","t15","index","list","entry","Error","serverByKind","t16","t17","query","t18","t19","t20","index_0","t21","index_1","connectionTimeout_0","record","crypto","randomUUID","Date","now","Math","random","toString","slice","createdAt","prev_1","some","c_1","remove","clearAuthState","prev_2","s_3","t22","getState","connector","customServer","addCustomServer","removeServer","McpManagerResource","scopes","parent","source","s_1","s_2"],"sources":["../../src/resources/McpManagerResource.ts"],"sourcesContent":["import { useState, useEffect, useMemo, useEffectEvent, useRef } from \"react\";\nimport { useResource, resource, withKey } from \"@assistant-ui/tap\";\nimport {\n useClientLookup,\n useAssistantClientRef,\n attachTransformScopes,\n type ClientOutput,\n} from \"@assistant-ui/store\";\nimport { ModelContext } from \"@assistant-ui/core/store\";\nimport type { Tool } from \"assistant-stream\";\nimport { McpServerResource } from \"./McpServerResource\";\nimport { McpLocalStorage } from \"./storage/McpLocalStorage\";\nimport type { MCPStorageElement } from \"./storage/types\";\nimport { assertUniqueServerIds } from \"../utils/serverId\";\nimport type {\n MCPAuthConfig,\n MCPConnector,\n MCPCustomServerRecord,\n MCPManagerState,\n} from \"../mcp-scope\";\n\nexport type McpManagerResourceProps = {\n connectors?: MCPConnector[] | undefined;\n storage?: MCPStorageElement | undefined;\n /** OAuth redirect target. Defaults to `${origin}/mcp/callback`. */\n oauthRedirectUri?: string | undefined;\n /** Connect on mount when usable auth exists. Default true. */\n autoConnect?: boolean | undefined;\n /** Optional timeout in milliseconds for connect/listTools calls. Disabled by default. */\n connectionTimeout?: number | undefined;\n};\n\nfunction defaultRedirectUri(): string {\n if (typeof window === \"undefined\") return \"\";\n return `${window.location.origin}/mcp/callback`;\n}\n\n// Stable empty fallback so an absent `connectors` prop doesn't produce a fresh\n// array each render (which would invalidate the serverElements memo below).\nconst NO_CONNECTORS: MCPConnector[] = [];\n\nconst useMcpManagerResource = (\n props: McpManagerResourceProps,\n): ClientOutput<\"mcp\"> => {\n const connectors = props.connectors ?? NO_CONNECTORS;\n const autoConnect = props.autoConnect ?? true;\n const redirectUri = props.oauthRedirectUri ?? defaultRedirectUri();\n const connectionTimeout = props.connectionTimeout;\n\n const storageElement = props.storage ?? McpLocalStorage();\n const storage = useResource(storageElement);\n\n const [customServers, setCustomServers] = useState<MCPCustomServerRecord[]>(\n [],\n );\n const [isHydrated, setIsHydrated] = useState(false);\n\n const hydratedRef = useRef(false);\n\n const hydrate = useEffectEvent(async (signal: { cancelled: boolean }) => {\n const markHydrated = () => {\n if (!signal.cancelled) {\n hydratedRef.current = true;\n setIsHydrated(true);\n }\n };\n\n let records: Awaited<ReturnType<typeof storage.loadCustomServers>>;\n try {\n records = await storage.loadCustomServers();\n } catch (error) {\n markHydrated();\n throw error;\n }\n\n if (signal.cancelled) return;\n // Merge rather than replace so any addCustomServer calls that\n // happened before hydration resolved aren't silently overwritten.\n // Persisted order wins; pre-hydration locals append.\n setCustomServers((prev) => {\n if (prev.length === 0) return records;\n const persistedIds = new Set(records.map((r) => r.id));\n return [...records, ...prev.filter((r) => !persistedIds.has(r.id))];\n });\n markHydrated();\n });\n\n useEffect(() => {\n const signal = { cancelled: false };\n void hydrate(signal);\n return () => {\n signal.cancelled = true;\n };\n }, []);\n\n const persistCustomServers = useEffectEvent(\n async (records: MCPCustomServerRecord[]) => {\n if (!hydratedRef.current) return;\n await storage.saveCustomServers(records);\n },\n );\n\n useEffect(() => {\n void persistCustomServers(customServers);\n }, [customServers]);\n\n const serverElements = useMemo(() => {\n assertUniqueServerIds([\n ...connectors.map((c) => c.id),\n ...customServers.map((s) => s.id),\n ]);\n\n const connectorElements = connectors.map((c) =>\n withKey(\n c.id,\n McpServerResource({\n id: c.id,\n kind: \"connector\",\n name: c.name,\n url: c.url,\n icon: c.icon,\n auth: c.auth,\n storage,\n redirectUri,\n autoConnect,\n connectionTimeout: c.connectionTimeout ?? connectionTimeout,\n onRemove: async () => {\n // connectors cannot be removed\n },\n }),\n ),\n );\n const customElements = customServers.map((s) =>\n withKey(\n s.id,\n McpServerResource({\n id: s.id,\n kind: \"custom\",\n name: s.name,\n url: s.url,\n auth: s.auth,\n storage,\n redirectUri,\n autoConnect,\n connectionTimeout: s.connectionTimeout ?? connectionTimeout,\n onRemove: async () => {\n setCustomServers((prev) => prev.filter((x) => x.id !== s.id));\n },\n }),\n ),\n );\n return [...connectorElements, ...customElements];\n }, [\n connectors,\n customServers,\n storage,\n redirectUri,\n autoConnect,\n connectionTimeout,\n ]);\n\n const lookup = useClientLookup(serverElements);\n\n const state = useMemo<MCPManagerState>(() => {\n const all = lookup.state;\n return {\n servers: all,\n connectors: all.filter((s) => s.kind === \"connector\"),\n customServers: all.filter((s) => s.kind === \"custom\"),\n isHydrated,\n };\n }, [lookup.state, isHydrated]);\n\n // ─── Auto-register MCP tools as frontend tools in modelContext ─────\n // Build the toolkit from connected servers; re-register when the visible\n // tool surface changes. Tool names are prefixed with the server id to\n // avoid collisions across connected servers — connector ids must not\n // contain `__` (enforced by `defineConnector`); `addCustomServer`\n // generates UUIDs that satisfy the constraint by construction.\n const toolkit = useMemo<Record<string, Tool<any, any>>>(() => {\n const out: Record<string, Tool<any, any>> = {};\n for (const server of state.servers) {\n if (server.connectionState !== \"connected\") continue;\n for (const tool of server.tools) {\n const fullName = `${server.id}__${tool.name}`;\n out[fullName] = {\n type: \"frontend\",\n ...(tool.description !== undefined\n ? { description: tool.description }\n : {}),\n parameters: tool.inputSchema as never,\n execute: (args) =>\n lookup.get({ key: server.id }).callTool(tool.name, args as unknown),\n };\n }\n }\n return out;\n }, [state, lookup]);\n\n const clientRef = useAssistantClientRef();\n\n useEffect(() => {\n const client = clientRef.current;\n if (!client) return;\n return client.modelContext().register({\n getModelContext: () => ({ tools: toolkit }),\n });\n }, [toolkit, clientRef]);\n\n const serverByKind = (kind: \"connector\" | \"custom\", index: number) => {\n const list = kind === \"connector\" ? state.connectors : state.customServers;\n const entry = list[index];\n if (!entry) {\n throw new Error(\n `McpManagerResource: no ${kind} at index ${index} (length ${list.length})`,\n );\n }\n return lookup.get({ key: entry.id });\n };\n\n return {\n getState: () => state,\n server: (query) => {\n if (\"id\" in query) return lookup.get({ key: query.id });\n return serverByKind(query.kind, query.index);\n },\n connector: ({ index }) => serverByKind(\"connector\", index),\n customServer: ({ index }) => serverByKind(\"custom\", index),\n addCustomServer: async ({ name, url, auth, connectionTimeout }) => {\n const record: MCPCustomServerRecord = {\n id:\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name,\n url,\n auth: auth as MCPAuthConfig,\n connectionTimeout,\n createdAt: Date.now(),\n };\n setCustomServers((prev) => [...prev, record]);\n return record.id;\n },\n removeServer: async (id) => {\n // removeServer is custom-server only — connectors are app-defined\n // and not user-removable. Refuse rather than silently no-op.\n if (state.connectors.some((c) => c.id === id)) {\n throw new Error(\n `Cannot remove connector \"${id}\" — connectors are app-defined and not removable. Use a custom server id instead.`,\n );\n }\n // Delegate to McpServerResource.remove() which disconnects,\n // clears auth state, and unregisters from customServers in one\n // place. Fallback to manual cleanup if the lookup is empty\n // (server already gone).\n try {\n await lookup.get({ key: id }).remove();\n } catch {\n await storage.clearAuthState(id);\n setCustomServers((prev) => prev.filter((s) => s.id !== id));\n }\n },\n };\n};\n\nexport const McpManagerResource = resource(useMcpManagerResource);\n\n// Ensure modelContext exists as a sibling when the manager mounts. If an\n// ancestor (e.g. a chat runtime) already provides modelContext, this is a\n// no-op; otherwise it's auto-mounted alongside `mcp`.\nattachTransformScopes(useMcpManagerResource, (scopes, parent) => {\n if (!scopes.modelContext && parent.modelContext.source === null) {\n scopes.modelContext = ModelContext();\n }\n});\n"],"mappings":"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"}
|
|
@@ -13,6 +13,7 @@ type McpServerResourceProps = {
|
|
|
13
13
|
storage: MCPStorage;
|
|
14
14
|
redirectUri: string;
|
|
15
15
|
autoConnect: boolean;
|
|
16
|
+
connectionTimeout?: number | undefined;
|
|
16
17
|
onRemove: () => Promise<void>;
|
|
17
18
|
};
|
|
18
19
|
declare const McpServerResource: import("@assistant-ui/tap").Resource<ClientOutput<"mcpServer">, [props: McpServerResourceProps]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpServerResource.d.ts","names":[],"sources":["../../src/resources/McpServerResource.ts"],"mappings":";;;;;KAsBY,sBAAA;EACV,EAAA;EACA,IAAA,EAAM,aAAA;EACN,IAAA;EACA,GAAA;EACA,IAAA;EACA,IAAA,EAAM,aAAA;EACN,OAAA,EAAS,UAAA;EACT,WAAA;EACA,WAAA;EACA,QAAA,QAAgB,OAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"McpServerResource.d.ts","names":[],"sources":["../../src/resources/McpServerResource.ts"],"mappings":";;;;;KAsBY,sBAAA;EACV,EAAA;EACA,IAAA,EAAM,aAAA;EACN,IAAA;EACA,GAAA;EACA,IAAA;EACA,IAAA,EAAM,aAAA;EACN,OAAA,EAAS,UAAA;EACT,WAAA;EACA,WAAA;EACA,iBAAA;EACA,QAAA,QAAgB,OAAA;AAAA;AAAA,cAgSL,iBAAA,8BAAiB,QAAA,CAAA,YAAA,gBAAA,KAAA,EAAA,sBAAA"}
|
|
@@ -15,6 +15,21 @@ const useMcpServerResource = (props) => {
|
|
|
15
15
|
const [authorizationUrl, setAuthorizationUrl] = useState(null);
|
|
16
16
|
const clientRef = useRef(null);
|
|
17
17
|
const transportRef = useRef(null);
|
|
18
|
+
const withConnectionTimeout = useEffectEvent(async (promise, phase, startedAt) => {
|
|
19
|
+
const timeoutMs = props.connectionTimeout;
|
|
20
|
+
if (timeoutMs === void 0) return await promise;
|
|
21
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
22
|
+
const timeoutError = () => /* @__PURE__ */ new Error(`MCP server "${props.id}" timed out while ${phase} after ${timeoutMs}ms.`);
|
|
23
|
+
if (remainingMs <= 0) throw timeoutError();
|
|
24
|
+
let timeout;
|
|
25
|
+
try {
|
|
26
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
27
|
+
timeout = setTimeout(() => reject(timeoutError()), remainingMs);
|
|
28
|
+
})]);
|
|
29
|
+
} finally {
|
|
30
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
18
33
|
const buildTransport = useEffectEvent(async () => {
|
|
19
34
|
if (props.auth.type === "oauth") {
|
|
20
35
|
const authProvider = createOAuthProvider({
|
|
@@ -40,8 +55,9 @@ const useMcpServerResource = (props) => {
|
|
|
40
55
|
name: "assistant-ui-mcp",
|
|
41
56
|
version: "0.0.0"
|
|
42
57
|
});
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
const startedAt_0 = Date.now();
|
|
59
|
+
await withConnectionTimeout(client.connect(transport), "connecting", startedAt_0);
|
|
60
|
+
const list = await withConnectionTimeout(client.listTools(), "listing tools", startedAt_0);
|
|
45
61
|
clientRef.current = client;
|
|
46
62
|
transportRef.current = transport;
|
|
47
63
|
setTools(list.tools.map((t) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpServerResource.js","names":["useState","useRef","useEffect","useMemo","useEffectEvent","resource","ClientOutput","Client","StreamableHTTPClientTransport","StreamableHTTPClientTransportOptions","UnauthorizedError","Transport","createOAuthProvider","buildHeaders","assertValidServerId","MCPStorage","MCPAuthConfig","MCPConnectionState","MCPServerKind","MCPServerState","MCPToolInfo","McpServerResourceProps","id","kind","name","url","icon","auth","storage","redirectUri","autoConnect","onRemove","Promise","useMcpServerResource","props","connectionState","setConnectionState","tools","setTools","lastError","setLastError","message","authorizationUrl","setAuthorizationUrl","clientRef","transportRef","buildTransport","type","authProvider","serverId","config","onAuthorizationUrl","toString","URL","persisted","loadAuthState","headers","transportOpts","requestInit","finalizeConnect","transport","client","version","connect","list","listTools","current","map","t","info","inputSchema","description","undefined","closeTransport","close","doConnect","err","Error","String","doDisconnect","doCompleteAuth","callbackUrl","code","searchParams","get","finishAuth","tryAutoConnect","signal","cancelled","token","tokens","catch","state","getState","disconnect","remove","clearAuthState","callTool","args","arguments","Record","readResource","uri","completeAuth","McpServerResource"],"sources":["../../src/resources/McpServerResource.ts"],"sourcesContent":["import { useState, useRef, useEffect, useMemo, useEffectEvent } from \"react\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type { ClientOutput } from \"@assistant-ui/store\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n StreamableHTTPClientTransport,\n type StreamableHTTPClientTransportOptions,\n} from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { UnauthorizedError } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { createOAuthProvider } from \"../auth/createOAuthProvider\";\nimport { buildHeaders } from \"../auth/buildHeaders\";\nimport { assertValidServerId } from \"../utils/serverId\";\nimport type { MCPStorage } from \"./storage/types\";\nimport type {\n MCPAuthConfig,\n MCPConnectionState,\n MCPServerKind,\n MCPServerState,\n MCPToolInfo,\n} from \"../mcp-scope\";\n\nexport type McpServerResourceProps = {\n id: string;\n kind: MCPServerKind;\n name: string;\n url: string;\n icon?: string | undefined;\n auth: MCPAuthConfig;\n storage: MCPStorage;\n redirectUri: string;\n autoConnect: boolean;\n onRemove: () => Promise<void>;\n};\n\nconst useMcpServerResource = (\n props: McpServerResourceProps,\n): ClientOutput<\"mcpServer\"> => {\n assertValidServerId(props.id);\n const [connectionState, setConnectionState] =\n useState<MCPConnectionState>(\"disconnected\");\n const [tools, setTools] = useState<MCPToolInfo[]>([]);\n const [lastError, setLastError] = useState<{ message: string } | null>(null);\n const [authorizationUrl, setAuthorizationUrl] = useState<string | null>(null);\n\n const clientRef = useRef<Client | null>(null);\n const transportRef = useRef<StreamableHTTPClientTransport | null>(null);\n\n const buildTransport = useEffectEvent(\n async (): Promise<StreamableHTTPClientTransport> => {\n if (props.auth.type === \"oauth\") {\n const authProvider = createOAuthProvider({\n serverId: props.id,\n config: props.auth,\n storage: props.storage,\n redirectUri: props.redirectUri,\n onAuthorizationUrl: (url) => setAuthorizationUrl(url.toString()),\n });\n return new StreamableHTTPClientTransport(new URL(props.url), {\n authProvider,\n });\n }\n if (props.auth.type === \"bearer\") {\n const persisted = await props.storage.loadAuthState(props.id);\n const headers = buildHeaders(props.auth, persisted);\n const transportOpts: StreamableHTTPClientTransportOptions = {};\n if (headers) transportOpts.requestInit = { headers };\n return new StreamableHTTPClientTransport(\n new URL(props.url),\n transportOpts,\n );\n }\n return new StreamableHTTPClientTransport(new URL(props.url));\n },\n );\n\n const finalizeConnect = useEffectEvent(\n async (transport: StreamableHTTPClientTransport) => {\n const client = new Client({\n name: \"assistant-ui-mcp\",\n version: \"0.0.0\",\n });\n // SDK's StreamableHTTPClientTransport.sessionId is `string | undefined`\n // but Transport.sessionId is declared `string?` — under\n // exactOptionalPropertyTypes the SDK's own classes don't satisfy its\n // Transport interface. Cast to bridge the gap.\n await client.connect(transport as unknown as Transport);\n // Defer ref assignment until listTools() also succeeds — otherwise a\n // post-connect failure leaves stale refs that `callTool()` would\n // happily walk into, producing confusing SDK errors instead of\n // \"not connected\".\n const list = await client.listTools();\n clientRef.current = client;\n transportRef.current = transport;\n setTools(\n list.tools.map((t) => {\n const info: MCPToolInfo = {\n name: t.name,\n inputSchema: t.inputSchema,\n };\n if (t.description !== undefined) info.description = t.description;\n return info;\n }),\n );\n setConnectionState(\"connected\");\n },\n );\n\n const closeTransport = async () => {\n const t = transportRef.current;\n transportRef.current = null;\n clientRef.current = null;\n if (t) {\n try {\n await t.close();\n } catch {\n // ignore close errors\n }\n }\n };\n\n const doConnect = useEffectEvent(async () => {\n // Close any prior transport/client so a re-connect doesn't leak.\n await closeTransport();\n setConnectionState(\"connecting\");\n setLastError(null);\n setAuthorizationUrl(null);\n // Clear tools so a reconnect (error → connect, or authRequired\n // → connect) doesn't expose the previous attempt's tool list.\n setTools([]);\n let transport: StreamableHTTPClientTransport | null = null;\n try {\n transport = await buildTransport();\n // Don't assign to transportRef until connect succeeds — otherwise a\n // failed `listTools()` leaves an orphaned transport that future\n // doConnect / doDisconnect calls treat as live.\n await finalizeConnect(transport);\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n // OAuth: keep the transport alive so completeAuth can call\n // finishAuth on it. Closing it before storing would leave a\n // closed transport on transportRef.\n transportRef.current = transport;\n setConnectionState(\"authRequired\");\n } else {\n if (transport) {\n try {\n await transport.close();\n } catch {\n // ignore close errors\n }\n }\n setLastError({\n message: err instanceof Error ? err.message : String(err),\n });\n setConnectionState(\"error\");\n }\n }\n });\n\n const doDisconnect = useEffectEvent(async () => {\n setTools([]);\n setAuthorizationUrl(null);\n setConnectionState(\"disconnected\");\n await closeTransport();\n });\n\n const doCompleteAuth = useEffectEvent(async (callbackUrl: string) => {\n setConnectionState(\"authPending\");\n setLastError(null);\n try {\n const url = new URL(callbackUrl);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"missing authorization code in callback URL\");\n let transport = transportRef.current;\n if (!transport) {\n transport = await buildTransport();\n transportRef.current = transport;\n }\n await transport.finishAuth(code);\n setAuthorizationUrl(null);\n await finalizeConnect(transport);\n } catch (err) {\n await closeTransport();\n setLastError({\n message: err instanceof Error ? err.message : String(err),\n });\n setConnectionState(\"error\");\n }\n });\n\n const tryAutoConnect = useEffectEvent(\n async (signal: { cancelled: boolean }) => {\n if (!props.autoConnect) return;\n if (props.auth.type === \"none\") {\n void doConnect();\n return;\n }\n // Static connector token short-circuits the storage read.\n if (props.auth.type === \"bearer\" && props.auth.token) {\n void doConnect();\n return;\n }\n const persisted = await props.storage.loadAuthState(props.id);\n if (signal.cancelled) return;\n if (props.auth.type === \"oauth\") {\n if (!persisted?.tokens) return;\n } else if (!persisted?.token) {\n return;\n }\n void doConnect();\n },\n );\n\n // Auto-connect on mount when usable auth exists.\n useEffect(() => {\n const signal = { cancelled: false };\n void tryAutoConnect(signal);\n return () => {\n signal.cancelled = true;\n const t = transportRef.current;\n transportRef.current = null;\n clientRef.current = null;\n if (t) t.close().catch(() => {});\n };\n }, []);\n\n const state = useMemo<MCPServerState>(\n () => ({\n id: props.id,\n kind: props.kind,\n name: props.name,\n url: props.url,\n icon: props.icon,\n connectionState,\n lastError,\n tools,\n authorizationUrl,\n }),\n [\n props.id,\n props.kind,\n props.name,\n props.url,\n props.icon,\n connectionState,\n lastError,\n tools,\n authorizationUrl,\n ],\n );\n\n return {\n getState: () => state,\n connect: doConnect,\n disconnect: doDisconnect,\n remove: async () => {\n await doDisconnect();\n await props.storage.clearAuthState(props.id);\n await props.onRemove();\n },\n callTool: async (name, args) => {\n const client = clientRef.current;\n if (!client) {\n throw new Error(`MCP server \"${props.id}\" is not connected`);\n }\n return await client.callTool({\n name,\n arguments: args as Record<string, unknown> | undefined,\n });\n },\n readResource: async (uri) => {\n const client = clientRef.current;\n if (!client) {\n throw new Error(`MCP server \"${props.id}\" is not connected`);\n }\n return await client.readResource({ uri });\n },\n completeAuth: doCompleteAuth,\n };\n};\n\nexport const McpServerResource = resource(useMcpServerResource);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"McpServerResource.js","names":["useState","useRef","useEffect","useMemo","useEffectEvent","resource","ClientOutput","Client","StreamableHTTPClientTransport","StreamableHTTPClientTransportOptions","UnauthorizedError","Transport","createOAuthProvider","buildHeaders","assertValidServerId","MCPStorage","MCPAuthConfig","MCPConnectionState","MCPServerKind","MCPServerState","MCPToolInfo","McpServerResourceProps","id","kind","name","url","icon","auth","storage","redirectUri","autoConnect","connectionTimeout","onRemove","Promise","useMcpServerResource","props","connectionState","setConnectionState","tools","setTools","lastError","setLastError","message","authorizationUrl","setAuthorizationUrl","clientRef","transportRef","withConnectionTimeout","promise","T","phase","startedAt","timeoutMs","undefined","remainingMs","Date","now","timeoutError","Error","timeout","ReturnType","setTimeout","race","_","reject","clearTimeout","buildTransport","type","authProvider","serverId","config","onAuthorizationUrl","toString","URL","persisted","loadAuthState","headers","transportOpts","requestInit","finalizeConnect","transport","client","version","connect","list","listTools","current","map","t","info","inputSchema","description","closeTransport","close","doConnect","err","String","doDisconnect","doCompleteAuth","callbackUrl","code","searchParams","get","finishAuth","tryAutoConnect","signal","cancelled","token","tokens","catch","state","getState","disconnect","remove","clearAuthState","callTool","args","arguments","Record","readResource","uri","completeAuth","McpServerResource"],"sources":["../../src/resources/McpServerResource.ts"],"sourcesContent":["import { useState, useRef, useEffect, useMemo, useEffectEvent } from \"react\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type { ClientOutput } from \"@assistant-ui/store\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n StreamableHTTPClientTransport,\n type StreamableHTTPClientTransportOptions,\n} from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { UnauthorizedError } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { createOAuthProvider } from \"../auth/createOAuthProvider\";\nimport { buildHeaders } from \"../auth/buildHeaders\";\nimport { assertValidServerId } from \"../utils/serverId\";\nimport type { MCPStorage } from \"./storage/types\";\nimport type {\n MCPAuthConfig,\n MCPConnectionState,\n MCPServerKind,\n MCPServerState,\n MCPToolInfo,\n} from \"../mcp-scope\";\n\nexport type McpServerResourceProps = {\n id: string;\n kind: MCPServerKind;\n name: string;\n url: string;\n icon?: string | undefined;\n auth: MCPAuthConfig;\n storage: MCPStorage;\n redirectUri: string;\n autoConnect: boolean;\n connectionTimeout?: number | undefined;\n onRemove: () => Promise<void>;\n};\n\nconst useMcpServerResource = (\n props: McpServerResourceProps,\n): ClientOutput<\"mcpServer\"> => {\n assertValidServerId(props.id);\n const [connectionState, setConnectionState] =\n useState<MCPConnectionState>(\"disconnected\");\n const [tools, setTools] = useState<MCPToolInfo[]>([]);\n const [lastError, setLastError] = useState<{ message: string } | null>(null);\n const [authorizationUrl, setAuthorizationUrl] = useState<string | null>(null);\n\n const clientRef = useRef<Client | null>(null);\n const transportRef = useRef<StreamableHTTPClientTransport | null>(null);\n\n const withConnectionTimeout = useEffectEvent(\n async <T>(\n promise: Promise<T>,\n phase: \"connecting\" | \"listing tools\",\n startedAt: number,\n ): Promise<T> => {\n const timeoutMs = props.connectionTimeout;\n if (timeoutMs === undefined) return await promise;\n const remainingMs = timeoutMs - (Date.now() - startedAt);\n const timeoutError = () =>\n new Error(\n `MCP server \"${props.id}\" timed out while ${phase} after ${timeoutMs}ms.`,\n );\n if (remainingMs <= 0) throw timeoutError();\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(timeoutError()), remainingMs);\n }),\n ]);\n } finally {\n if (timeout !== undefined) clearTimeout(timeout);\n }\n },\n );\n\n const buildTransport = useEffectEvent(\n async (): Promise<StreamableHTTPClientTransport> => {\n if (props.auth.type === \"oauth\") {\n const authProvider = createOAuthProvider({\n serverId: props.id,\n config: props.auth,\n storage: props.storage,\n redirectUri: props.redirectUri,\n onAuthorizationUrl: (url) => setAuthorizationUrl(url.toString()),\n });\n return new StreamableHTTPClientTransport(new URL(props.url), {\n authProvider,\n });\n }\n if (props.auth.type === \"bearer\") {\n const persisted = await props.storage.loadAuthState(props.id);\n const headers = buildHeaders(props.auth, persisted);\n const transportOpts: StreamableHTTPClientTransportOptions = {};\n if (headers) transportOpts.requestInit = { headers };\n return new StreamableHTTPClientTransport(\n new URL(props.url),\n transportOpts,\n );\n }\n return new StreamableHTTPClientTransport(new URL(props.url));\n },\n );\n\n const finalizeConnect = useEffectEvent(\n async (transport: StreamableHTTPClientTransport) => {\n const client = new Client({\n name: \"assistant-ui-mcp\",\n version: \"0.0.0\",\n });\n const startedAt = Date.now();\n // SDK's StreamableHTTPClientTransport.sessionId is `string | undefined`\n // but Transport.sessionId is declared `string?` — under\n // exactOptionalPropertyTypes the SDK's own classes don't satisfy its\n // Transport interface. Cast to bridge the gap.\n await withConnectionTimeout(\n client.connect(transport as unknown as Transport),\n \"connecting\",\n startedAt,\n );\n // Defer ref assignment until listTools() also succeeds — otherwise a\n // post-connect failure leaves stale refs that `callTool()` would\n // happily walk into, producing confusing SDK errors instead of\n // \"not connected\".\n const list = await withConnectionTimeout(\n client.listTools(),\n \"listing tools\",\n startedAt,\n );\n clientRef.current = client;\n transportRef.current = transport;\n setTools(\n list.tools.map((t) => {\n const info: MCPToolInfo = {\n name: t.name,\n inputSchema: t.inputSchema,\n };\n if (t.description !== undefined) info.description = t.description;\n return info;\n }),\n );\n setConnectionState(\"connected\");\n },\n );\n\n const closeTransport = async () => {\n const t = transportRef.current;\n transportRef.current = null;\n clientRef.current = null;\n if (t) {\n try {\n await t.close();\n } catch {\n // ignore close errors\n }\n }\n };\n\n const doConnect = useEffectEvent(async () => {\n // Close any prior transport/client so a re-connect doesn't leak.\n await closeTransport();\n setConnectionState(\"connecting\");\n setLastError(null);\n setAuthorizationUrl(null);\n // Clear tools so a reconnect (error → connect, or authRequired\n // → connect) doesn't expose the previous attempt's tool list.\n setTools([]);\n let transport: StreamableHTTPClientTransport | null = null;\n try {\n transport = await buildTransport();\n // Don't assign to transportRef until connect succeeds — otherwise a\n // failed `listTools()` leaves an orphaned transport that future\n // doConnect / doDisconnect calls treat as live.\n await finalizeConnect(transport);\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n // OAuth: keep the transport alive so completeAuth can call\n // finishAuth on it. Closing it before storing would leave a\n // closed transport on transportRef.\n transportRef.current = transport;\n setConnectionState(\"authRequired\");\n } else {\n if (transport) {\n try {\n await transport.close();\n } catch {\n // ignore close errors\n }\n }\n setLastError({\n message: err instanceof Error ? err.message : String(err),\n });\n setConnectionState(\"error\");\n }\n }\n });\n\n const doDisconnect = useEffectEvent(async () => {\n setTools([]);\n setAuthorizationUrl(null);\n setConnectionState(\"disconnected\");\n await closeTransport();\n });\n\n const doCompleteAuth = useEffectEvent(async (callbackUrl: string) => {\n setConnectionState(\"authPending\");\n setLastError(null);\n try {\n const url = new URL(callbackUrl);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"missing authorization code in callback URL\");\n let transport = transportRef.current;\n if (!transport) {\n transport = await buildTransport();\n transportRef.current = transport;\n }\n await transport.finishAuth(code);\n setAuthorizationUrl(null);\n await finalizeConnect(transport);\n } catch (err) {\n await closeTransport();\n setLastError({\n message: err instanceof Error ? err.message : String(err),\n });\n setConnectionState(\"error\");\n }\n });\n\n const tryAutoConnect = useEffectEvent(\n async (signal: { cancelled: boolean }) => {\n if (!props.autoConnect) return;\n if (props.auth.type === \"none\") {\n void doConnect();\n return;\n }\n // Static connector token short-circuits the storage read.\n if (props.auth.type === \"bearer\" && props.auth.token) {\n void doConnect();\n return;\n }\n const persisted = await props.storage.loadAuthState(props.id);\n if (signal.cancelled) return;\n if (props.auth.type === \"oauth\") {\n if (!persisted?.tokens) return;\n } else if (!persisted?.token) {\n return;\n }\n void doConnect();\n },\n );\n\n // Auto-connect on mount when usable auth exists.\n useEffect(() => {\n const signal = { cancelled: false };\n void tryAutoConnect(signal);\n return () => {\n signal.cancelled = true;\n const t = transportRef.current;\n transportRef.current = null;\n clientRef.current = null;\n if (t) t.close().catch(() => {});\n };\n }, []);\n\n const state = useMemo<MCPServerState>(\n () => ({\n id: props.id,\n kind: props.kind,\n name: props.name,\n url: props.url,\n icon: props.icon,\n connectionState,\n lastError,\n tools,\n authorizationUrl,\n }),\n [\n props.id,\n props.kind,\n props.name,\n props.url,\n props.icon,\n connectionState,\n lastError,\n tools,\n authorizationUrl,\n ],\n );\n\n return {\n getState: () => state,\n connect: doConnect,\n disconnect: doDisconnect,\n remove: async () => {\n await doDisconnect();\n await props.storage.clearAuthState(props.id);\n await props.onRemove();\n },\n callTool: async (name, args) => {\n const client = clientRef.current;\n if (!client) {\n throw new Error(`MCP server \"${props.id}\" is not connected`);\n }\n return await client.callTool({\n name,\n arguments: args as Record<string, unknown> | undefined,\n });\n },\n readResource: async (uri) => {\n const client = clientRef.current;\n if (!client) {\n throw new Error(`MCP server \"${props.id}\" is not connected`);\n }\n return await client.readResource({ uri });\n },\n completeAuth: doCompleteAuth,\n };\n};\n\nexport const McpServerResource = resource(useMcpServerResource);\n"],"mappings":"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"}
|
package/dist/utils/serverId.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/utils/serverId.d.ts
|
|
2
2
|
declare function assertValidServerId(id: string): void;
|
|
3
|
+
declare function assertUniqueServerIds(ids: Iterable<string>): void;
|
|
3
4
|
//#endregion
|
|
4
|
-
export { assertValidServerId };
|
|
5
|
+
export { assertUniqueServerIds, assertValidServerId };
|
|
5
6
|
//# sourceMappingURL=serverId.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverId.d.ts","names":[],"sources":["../../src/utils/serverId.ts"],"mappings":";iBAIgB,mBAAA,CAAoB,EAAU"}
|
|
1
|
+
{"version":3,"file":"serverId.d.ts","names":[],"sources":["../../src/utils/serverId.ts"],"mappings":";iBAIgB,mBAAA,CAAoB,EAAU;AAAA,iBAgB9B,qBAAA,CAAsB,GAAqB,EAAhB,QAAQ"}
|
package/dist/utils/serverId.js
CHANGED
|
@@ -4,7 +4,14 @@ function assertValidServerId(id) {
|
|
|
4
4
|
if (id.includes("__")) throw new Error(`MCP server id "${id}" must not contain "__" — that sequence is reserved for separating server id from tool name.`);
|
|
5
5
|
if (id.includes("")) throw new Error(`MCP server id "${id}" must not contain the ASCII Unit Separator (\\x1f) — it is reserved as the list delimiter in primitive id-list selectors.`);
|
|
6
6
|
}
|
|
7
|
+
function assertUniqueServerIds(ids) {
|
|
8
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9
|
+
for (const id of ids) {
|
|
10
|
+
if (seen.has(id)) throw new Error(`McpManagerResource received duplicate MCP server id "${id}". Server ids must be unique because they are used for lookups, OAuth routing, and tool name prefixes.`);
|
|
11
|
+
seen.add(id);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
7
14
|
//#endregion
|
|
8
|
-
export { assertValidServerId };
|
|
15
|
+
export { assertUniqueServerIds, assertValidServerId };
|
|
9
16
|
|
|
10
17
|
//# sourceMappingURL=serverId.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverId.js","names":["assertValidServerId","id","length","Error","includes"],"sources":["../../src/utils/serverId.ts"],"sourcesContent":["// Server ids are joined with tool names via `__` to form unique tool\n// names for the AI SDK. The separator must not appear inside ids,\n// otherwise two distinct `(serverId, toolName)` pairs could produce\n// the same composite name.\nexport function assertValidServerId(id: string): void {\n if (id.length === 0) {\n throw new Error(\"MCP server id must not be empty.\");\n }\n if (id.includes(\"__\")) {\n throw new Error(\n `MCP server id \"${id}\" must not contain \"__\" — that sequence is reserved for separating server id from tool name.`,\n );\n }\n if (id.includes(\"\\x1f\")) {\n throw new Error(\n `MCP server id \"${id}\" must not contain the ASCII Unit Separator (\\\\x1f) — it is reserved as the list delimiter in primitive id-list selectors.`,\n );\n }\n}\n"],"mappings":";AAIA,SAAgBA,oBAAoBC,IAAkB;CACpD,IAAIA,GAAGC,WAAW,GAChB,MAAM,IAAIC,MAAM,kCAAkC;CAEpD,IAAIF,GAAGG,SAAS,IAAI,GAClB,MAAM,IAAID,MACR,kBAAkBF,GAAE,6FACtB;CAEF,IAAIA,GAAGG,SAAS,GAAM,GACpB,MAAM,IAAID,MACR,kBAAkBF,GAAE,2HACtB;AAEJ"}
|
|
1
|
+
{"version":3,"file":"serverId.js","names":["assertValidServerId","id","length","Error","includes","assertUniqueServerIds","ids","Iterable","seen","Set","has","add"],"sources":["../../src/utils/serverId.ts"],"sourcesContent":["// Server ids are joined with tool names via `__` to form unique tool\n// names for the AI SDK. The separator must not appear inside ids,\n// otherwise two distinct `(serverId, toolName)` pairs could produce\n// the same composite name.\nexport function assertValidServerId(id: string): void {\n if (id.length === 0) {\n throw new Error(\"MCP server id must not be empty.\");\n }\n if (id.includes(\"__\")) {\n throw new Error(\n `MCP server id \"${id}\" must not contain \"__\" — that sequence is reserved for separating server id from tool name.`,\n );\n }\n if (id.includes(\"\\x1f\")) {\n throw new Error(\n `MCP server id \"${id}\" must not contain the ASCII Unit Separator (\\\\x1f) — it is reserved as the list delimiter in primitive id-list selectors.`,\n );\n }\n}\n\nexport function assertUniqueServerIds(ids: Iterable<string>): void {\n const seen = new Set<string>();\n for (const id of ids) {\n if (seen.has(id)) {\n throw new Error(\n `McpManagerResource received duplicate MCP server id \"${id}\". Server ids must be unique because they are used for lookups, OAuth routing, and tool name prefixes.`,\n );\n }\n seen.add(id);\n }\n}\n"],"mappings":";AAIA,SAAgBA,oBAAoBC,IAAkB;CACpD,IAAIA,GAAGC,WAAW,GAChB,MAAM,IAAIC,MAAM,kCAAkC;CAEpD,IAAIF,GAAGG,SAAS,IAAI,GAClB,MAAM,IAAID,MACR,kBAAkBF,GAAE,6FACtB;CAEF,IAAIA,GAAGG,SAAS,GAAM,GACpB,MAAM,IAAID,MACR,kBAAkBF,GAAE,2HACtB;AAEJ;AAEA,SAAgBI,sBAAsBC,KAA6B;CACjE,MAAME,uBAAO,IAAIC,IAAY;CAC7B,KAAK,MAAMR,MAAMK,KAAK;EACpB,IAAIE,KAAKE,IAAIT,EAAE,GACb,MAAM,IAAIE,MACR,wDAAwDF,GAAE,uGAC5D;EAEFO,KAAKG,IAAIV,EAAE;CACb;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "MCP server configuration and connection primitives for @assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
31
|
"@radix-ui/react-primitive": "^2.1.6",
|
|
32
|
-
"@assistant-ui/core": "0.2.
|
|
32
|
+
"@assistant-ui/core": "0.2.20",
|
|
33
33
|
"@assistant-ui/store": "0.2.19",
|
|
34
34
|
"@assistant-ui/tap": "0.9.3",
|
|
35
|
-
"assistant-stream": "0.3.
|
|
35
|
+
"assistant-stream": "0.3.25"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@types/react": "*",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/react": "^19.2.17",
|
|
48
48
|
"react": "^19.2.7",
|
|
49
|
-
"
|
|
49
|
+
"vitest": "^4.1.9",
|
|
50
|
+
"@assistant-ui/x-buildutils": "0.0.17"
|
|
50
51
|
},
|
|
51
52
|
"publishConfig": {
|
|
52
53
|
"access": "public",
|
|
@@ -62,6 +63,8 @@
|
|
|
62
63
|
"url": "https://github.com/assistant-ui/assistant-ui/issues"
|
|
63
64
|
},
|
|
64
65
|
"scripts": {
|
|
65
|
-
"build": "aui-build"
|
|
66
|
+
"build": "aui-build",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"test:watch": "vitest"
|
|
66
69
|
}
|
|
67
70
|
}
|
package/src/mcp-scope.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type MCPConnector = {
|
|
|
19
19
|
url: string;
|
|
20
20
|
icon?: string | undefined;
|
|
21
21
|
auth: MCPAuthConfig;
|
|
22
|
+
connectionTimeout?: number | undefined;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
export type MCPCustomServerRecord = {
|
|
@@ -26,6 +27,7 @@ export type MCPCustomServerRecord = {
|
|
|
26
27
|
name: string;
|
|
27
28
|
url: string;
|
|
28
29
|
auth: MCPAuthConfig;
|
|
30
|
+
connectionTimeout?: number | undefined;
|
|
29
31
|
createdAt: number;
|
|
30
32
|
};
|
|
31
33
|
|
|
@@ -93,6 +95,7 @@ export type MCPManagerMethods = {
|
|
|
93
95
|
name: string;
|
|
94
96
|
url: string;
|
|
95
97
|
auth: MCPAuthConfig;
|
|
98
|
+
connectionTimeout?: number | undefined;
|
|
96
99
|
}) => Promise<string>;
|
|
97
100
|
removeServer: (id: string) => Promise<void>;
|
|
98
101
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { defineConnector } from "../connector";
|
|
4
|
+
import type { MCPConnector } from "../mcp-scope";
|
|
5
|
+
import { assertUniqueServerIds } from "../utils/serverId";
|
|
6
|
+
import { McpManagerResource } from "./McpManagerResource";
|
|
7
|
+
import { McpMemoryStorage } from "./storage/McpMemoryStorage";
|
|
8
|
+
|
|
9
|
+
const connector = (id: string, name = id): MCPConnector =>
|
|
10
|
+
defineConnector({
|
|
11
|
+
id,
|
|
12
|
+
name,
|
|
13
|
+
url: `https://example.com/${id}/mcp`,
|
|
14
|
+
auth: { type: "none" },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const mount = (connectors: MCPConnector[]) =>
|
|
18
|
+
createTapRoot(function Root() {
|
|
19
|
+
return useResource(
|
|
20
|
+
McpManagerResource({
|
|
21
|
+
connectors,
|
|
22
|
+
storage: McpMemoryStorage(),
|
|
23
|
+
autoConnect: false,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("McpManagerResource server ids", () => {
|
|
29
|
+
it("throws when connectors reuse an id", () => {
|
|
30
|
+
expect(() =>
|
|
31
|
+
mount([connector("docs", "Docs"), connector("docs", "Internal Docs")]),
|
|
32
|
+
).toThrow(
|
|
33
|
+
'McpManagerResource received duplicate MCP server id "docs". Server ids must be unique because they are used for lookups, OAuth routing, and tool name prefixes.',
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("allows distinct ids", () => {
|
|
38
|
+
expect(() => assertUniqueServerIds(["docs", "linear"])).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -11,6 +11,7 @@ import type { Tool } from "assistant-stream";
|
|
|
11
11
|
import { McpServerResource } from "./McpServerResource";
|
|
12
12
|
import { McpLocalStorage } from "./storage/McpLocalStorage";
|
|
13
13
|
import type { MCPStorageElement } from "./storage/types";
|
|
14
|
+
import { assertUniqueServerIds } from "../utils/serverId";
|
|
14
15
|
import type {
|
|
15
16
|
MCPAuthConfig,
|
|
16
17
|
MCPConnector,
|
|
@@ -25,6 +26,8 @@ export type McpManagerResourceProps = {
|
|
|
25
26
|
oauthRedirectUri?: string | undefined;
|
|
26
27
|
/** Connect on mount when usable auth exists. Default true. */
|
|
27
28
|
autoConnect?: boolean | undefined;
|
|
29
|
+
/** Optional timeout in milliseconds for connect/listTools calls. Disabled by default. */
|
|
30
|
+
connectionTimeout?: number | undefined;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
function defaultRedirectUri(): string {
|
|
@@ -42,6 +45,7 @@ const useMcpManagerResource = (
|
|
|
42
45
|
const connectors = props.connectors ?? NO_CONNECTORS;
|
|
43
46
|
const autoConnect = props.autoConnect ?? true;
|
|
44
47
|
const redirectUri = props.oauthRedirectUri ?? defaultRedirectUri();
|
|
48
|
+
const connectionTimeout = props.connectionTimeout;
|
|
45
49
|
|
|
46
50
|
const storageElement = props.storage ?? McpLocalStorage();
|
|
47
51
|
const storage = useResource(storageElement);
|
|
@@ -101,6 +105,11 @@ const useMcpManagerResource = (
|
|
|
101
105
|
}, [customServers]);
|
|
102
106
|
|
|
103
107
|
const serverElements = useMemo(() => {
|
|
108
|
+
assertUniqueServerIds([
|
|
109
|
+
...connectors.map((c) => c.id),
|
|
110
|
+
...customServers.map((s) => s.id),
|
|
111
|
+
]);
|
|
112
|
+
|
|
104
113
|
const connectorElements = connectors.map((c) =>
|
|
105
114
|
withKey(
|
|
106
115
|
c.id,
|
|
@@ -114,6 +123,7 @@ const useMcpManagerResource = (
|
|
|
114
123
|
storage,
|
|
115
124
|
redirectUri,
|
|
116
125
|
autoConnect,
|
|
126
|
+
connectionTimeout: c.connectionTimeout ?? connectionTimeout,
|
|
117
127
|
onRemove: async () => {
|
|
118
128
|
// connectors cannot be removed
|
|
119
129
|
},
|
|
@@ -132,6 +142,7 @@ const useMcpManagerResource = (
|
|
|
132
142
|
storage,
|
|
133
143
|
redirectUri,
|
|
134
144
|
autoConnect,
|
|
145
|
+
connectionTimeout: s.connectionTimeout ?? connectionTimeout,
|
|
135
146
|
onRemove: async () => {
|
|
136
147
|
setCustomServers((prev) => prev.filter((x) => x.id !== s.id));
|
|
137
148
|
},
|
|
@@ -139,7 +150,14 @@ const useMcpManagerResource = (
|
|
|
139
150
|
),
|
|
140
151
|
);
|
|
141
152
|
return [...connectorElements, ...customElements];
|
|
142
|
-
}, [
|
|
153
|
+
}, [
|
|
154
|
+
connectors,
|
|
155
|
+
customServers,
|
|
156
|
+
storage,
|
|
157
|
+
redirectUri,
|
|
158
|
+
autoConnect,
|
|
159
|
+
connectionTimeout,
|
|
160
|
+
]);
|
|
143
161
|
|
|
144
162
|
const lookup = useClientLookup(serverElements);
|
|
145
163
|
|
|
@@ -208,7 +226,7 @@ const useMcpManagerResource = (
|
|
|
208
226
|
},
|
|
209
227
|
connector: ({ index }) => serverByKind("connector", index),
|
|
210
228
|
customServer: ({ index }) => serverByKind("custom", index),
|
|
211
|
-
addCustomServer: async ({ name, url, auth }) => {
|
|
229
|
+
addCustomServer: async ({ name, url, auth, connectionTimeout }) => {
|
|
212
230
|
const record: MCPCustomServerRecord = {
|
|
213
231
|
id:
|
|
214
232
|
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
@@ -217,6 +235,7 @@ const useMcpManagerResource = (
|
|
|
217
235
|
name,
|
|
218
236
|
url,
|
|
219
237
|
auth: auth as MCPAuthConfig,
|
|
238
|
+
connectionTimeout,
|
|
220
239
|
createdAt: Date.now(),
|
|
221
240
|
};
|
|
222
241
|
setCustomServers((prev) => [...prev, record]);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { MCPStorage } from "./storage/types";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => {
|
|
6
|
+
const clients: any[] = [];
|
|
7
|
+
const transports: any[] = [];
|
|
8
|
+
const connectResults: Array<() => Promise<void>> = [];
|
|
9
|
+
const listToolsResults: Array<
|
|
10
|
+
() => Promise<{
|
|
11
|
+
tools: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
inputSchema: unknown;
|
|
15
|
+
}>;
|
|
16
|
+
}>
|
|
17
|
+
> = [];
|
|
18
|
+
|
|
19
|
+
const Client = vi.fn().mockImplementation(function Client(this: any) {
|
|
20
|
+
const index = clients.length;
|
|
21
|
+
this.connect = vi.fn(() => connectResults[index]?.() ?? Promise.resolve());
|
|
22
|
+
this.listTools = vi.fn(
|
|
23
|
+
() => listToolsResults[index]?.() ?? Promise.resolve({ tools: [] }),
|
|
24
|
+
);
|
|
25
|
+
this.callTool = vi.fn();
|
|
26
|
+
this.readResource = vi.fn();
|
|
27
|
+
clients.push(this);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const StreamableHTTPClientTransport = vi
|
|
31
|
+
.fn()
|
|
32
|
+
.mockImplementation(function StreamableHTTPClientTransport(this: any) {
|
|
33
|
+
this.close = vi.fn(() => Promise.resolve());
|
|
34
|
+
this.finishAuth = vi.fn(() => Promise.resolve());
|
|
35
|
+
transports.push(this);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
Client,
|
|
40
|
+
StreamableHTTPClientTransport,
|
|
41
|
+
clients,
|
|
42
|
+
transports,
|
|
43
|
+
connectResults,
|
|
44
|
+
listToolsResults,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
49
|
+
Client: mocks.Client,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
|
53
|
+
StreamableHTTPClientTransport: mocks.StreamableHTTPClientTransport,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const { McpServerResource } = await import("./McpServerResource");
|
|
57
|
+
|
|
58
|
+
const never = <T>() => new Promise<T>(() => {});
|
|
59
|
+
|
|
60
|
+
const tick = async () => {
|
|
61
|
+
await Promise.resolve();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const waitFor = async (predicate: () => boolean) => {
|
|
65
|
+
for (let i = 0; i < 20; i++) {
|
|
66
|
+
if (predicate()) return;
|
|
67
|
+
await tick();
|
|
68
|
+
}
|
|
69
|
+
expect(predicate()).toBe(true);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const createStorage = (): MCPStorage => ({
|
|
73
|
+
loadCustomServers: vi.fn(async () => []),
|
|
74
|
+
saveCustomServers: vi.fn(async () => {}),
|
|
75
|
+
loadAuthState: vi.fn(async () => null),
|
|
76
|
+
saveAuthState: vi.fn(async () => {}),
|
|
77
|
+
clearAuthState: vi.fn(async () => {}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const mount = (props?: { connectionTimeout?: number | undefined }) => {
|
|
81
|
+
const connectionTimeout =
|
|
82
|
+
props && "connectionTimeout" in props ? props.connectionTimeout : 10_000;
|
|
83
|
+
|
|
84
|
+
return createTapRoot(function Root() {
|
|
85
|
+
return useResource(
|
|
86
|
+
McpServerResource({
|
|
87
|
+
id: "docs",
|
|
88
|
+
kind: "connector",
|
|
89
|
+
name: "Docs",
|
|
90
|
+
url: "https://example.com/mcp",
|
|
91
|
+
auth: { type: "none" },
|
|
92
|
+
storage: createStorage(),
|
|
93
|
+
redirectUri: "https://example.com/callback",
|
|
94
|
+
autoConnect: false,
|
|
95
|
+
connectionTimeout,
|
|
96
|
+
onRemove: vi.fn(async () => {}),
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
describe("McpServerResource connectionTimeout", () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.useFakeTimers();
|
|
105
|
+
mocks.clients.length = 0;
|
|
106
|
+
mocks.transports.length = 0;
|
|
107
|
+
mocks.connectResults.length = 0;
|
|
108
|
+
mocks.listToolsResults.length = 0;
|
|
109
|
+
mocks.Client.mockClear();
|
|
110
|
+
mocks.StreamableHTTPClientTransport.mockClear();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
vi.useRealTimers();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("fails the connection when client.connect hangs", async () => {
|
|
118
|
+
mocks.connectResults.push(() => never());
|
|
119
|
+
const root = mount();
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const connectPromise = root.getValue().connect();
|
|
123
|
+
await waitFor(() => mocks.clients[0]?.connect.mock.calls.length === 1);
|
|
124
|
+
|
|
125
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
126
|
+
await connectPromise;
|
|
127
|
+
|
|
128
|
+
expect(root.getValue().getState()).toMatchObject({
|
|
129
|
+
connectionState: "error",
|
|
130
|
+
tools: [],
|
|
131
|
+
lastError: {
|
|
132
|
+
message:
|
|
133
|
+
'MCP server "docs" timed out while connecting after 10000ms.',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
expect(mocks.transports[0].close).toHaveBeenCalledTimes(1);
|
|
137
|
+
} finally {
|
|
138
|
+
root.unmount();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("fails the connection when client.listTools hangs", async () => {
|
|
143
|
+
mocks.listToolsResults.push(() => never());
|
|
144
|
+
const root = mount();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const connectPromise = root.getValue().connect();
|
|
148
|
+
await waitFor(() => mocks.clients[0]?.listTools.mock.calls.length === 1);
|
|
149
|
+
|
|
150
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
151
|
+
await connectPromise;
|
|
152
|
+
|
|
153
|
+
expect(root.getValue().getState()).toMatchObject({
|
|
154
|
+
connectionState: "error",
|
|
155
|
+
tools: [],
|
|
156
|
+
lastError: {
|
|
157
|
+
message:
|
|
158
|
+
'MCP server "docs" timed out while listing tools after 10000ms.',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
expect(mocks.transports[0].close).toHaveBeenCalledTimes(1);
|
|
162
|
+
} finally {
|
|
163
|
+
root.unmount();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("uses one timeout budget for connect and listTools", async () => {
|
|
168
|
+
mocks.connectResults.push(
|
|
169
|
+
() => new Promise<void>((resolve) => setTimeout(resolve, 9_000)),
|
|
170
|
+
);
|
|
171
|
+
mocks.listToolsResults.push(() => never());
|
|
172
|
+
const root = mount();
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const connectPromise = root.getValue().connect();
|
|
176
|
+
await waitFor(() => mocks.clients[0]?.connect.mock.calls.length === 1);
|
|
177
|
+
|
|
178
|
+
await vi.advanceTimersByTimeAsync(9_000);
|
|
179
|
+
await waitFor(() => mocks.clients[0]?.listTools.mock.calls.length === 1);
|
|
180
|
+
await vi.advanceTimersByTimeAsync(999);
|
|
181
|
+
await tick();
|
|
182
|
+
expect(root.getValue().getState().connectionState).toBe("connecting");
|
|
183
|
+
|
|
184
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
185
|
+
await connectPromise;
|
|
186
|
+
|
|
187
|
+
expect(root.getValue().getState()).toMatchObject({
|
|
188
|
+
connectionState: "error",
|
|
189
|
+
tools: [],
|
|
190
|
+
lastError: {
|
|
191
|
+
message:
|
|
192
|
+
'MCP server "docs" timed out while listing tools after 10000ms.',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
expect(mocks.transports[0].close).toHaveBeenCalledTimes(1);
|
|
196
|
+
} finally {
|
|
197
|
+
root.unmount();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("keeps waiting when connectionTimeout is undefined", async () => {
|
|
202
|
+
mocks.connectResults.push(() => never());
|
|
203
|
+
const root = mount({ connectionTimeout: undefined });
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
void root.getValue().connect();
|
|
207
|
+
await waitFor(() => mocks.clients[0]?.connect.mock.calls.length === 1);
|
|
208
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
209
|
+
await tick();
|
|
210
|
+
|
|
211
|
+
expect(root.getValue().getState()).toMatchObject({
|
|
212
|
+
connectionState: "connecting",
|
|
213
|
+
tools: [],
|
|
214
|
+
lastError: null,
|
|
215
|
+
});
|
|
216
|
+
expect(mocks.transports[0].close).not.toHaveBeenCalled();
|
|
217
|
+
} finally {
|
|
218
|
+
root.unmount();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -30,6 +30,7 @@ export type McpServerResourceProps = {
|
|
|
30
30
|
storage: MCPStorage;
|
|
31
31
|
redirectUri: string;
|
|
32
32
|
autoConnect: boolean;
|
|
33
|
+
connectionTimeout?: number | undefined;
|
|
33
34
|
onRemove: () => Promise<void>;
|
|
34
35
|
};
|
|
35
36
|
|
|
@@ -46,6 +47,35 @@ const useMcpServerResource = (
|
|
|
46
47
|
const clientRef = useRef<Client | null>(null);
|
|
47
48
|
const transportRef = useRef<StreamableHTTPClientTransport | null>(null);
|
|
48
49
|
|
|
50
|
+
const withConnectionTimeout = useEffectEvent(
|
|
51
|
+
async <T>(
|
|
52
|
+
promise: Promise<T>,
|
|
53
|
+
phase: "connecting" | "listing tools",
|
|
54
|
+
startedAt: number,
|
|
55
|
+
): Promise<T> => {
|
|
56
|
+
const timeoutMs = props.connectionTimeout;
|
|
57
|
+
if (timeoutMs === undefined) return await promise;
|
|
58
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
59
|
+
const timeoutError = () =>
|
|
60
|
+
new Error(
|
|
61
|
+
`MCP server "${props.id}" timed out while ${phase} after ${timeoutMs}ms.`,
|
|
62
|
+
);
|
|
63
|
+
if (remainingMs <= 0) throw timeoutError();
|
|
64
|
+
|
|
65
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
66
|
+
try {
|
|
67
|
+
return await Promise.race([
|
|
68
|
+
promise,
|
|
69
|
+
new Promise<never>((_, reject) => {
|
|
70
|
+
timeout = setTimeout(() => reject(timeoutError()), remainingMs);
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
} finally {
|
|
74
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
49
79
|
const buildTransport = useEffectEvent(
|
|
50
80
|
async (): Promise<StreamableHTTPClientTransport> => {
|
|
51
81
|
if (props.auth.type === "oauth") {
|
|
@@ -80,16 +110,25 @@ const useMcpServerResource = (
|
|
|
80
110
|
name: "assistant-ui-mcp",
|
|
81
111
|
version: "0.0.0",
|
|
82
112
|
});
|
|
113
|
+
const startedAt = Date.now();
|
|
83
114
|
// SDK's StreamableHTTPClientTransport.sessionId is `string | undefined`
|
|
84
115
|
// but Transport.sessionId is declared `string?` — under
|
|
85
116
|
// exactOptionalPropertyTypes the SDK's own classes don't satisfy its
|
|
86
117
|
// Transport interface. Cast to bridge the gap.
|
|
87
|
-
await
|
|
118
|
+
await withConnectionTimeout(
|
|
119
|
+
client.connect(transport as unknown as Transport),
|
|
120
|
+
"connecting",
|
|
121
|
+
startedAt,
|
|
122
|
+
);
|
|
88
123
|
// Defer ref assignment until listTools() also succeeds — otherwise a
|
|
89
124
|
// post-connect failure leaves stale refs that `callTool()` would
|
|
90
125
|
// happily walk into, producing confusing SDK errors instead of
|
|
91
126
|
// "not connected".
|
|
92
|
-
const list = await
|
|
127
|
+
const list = await withConnectionTimeout(
|
|
128
|
+
client.listTools(),
|
|
129
|
+
"listing tools",
|
|
130
|
+
startedAt,
|
|
131
|
+
);
|
|
93
132
|
clientRef.current = client;
|
|
94
133
|
transportRef.current = transport;
|
|
95
134
|
setTools(
|
package/src/utils/serverId.ts
CHANGED
|
@@ -17,3 +17,15 @@ export function assertValidServerId(id: string): void {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export function assertUniqueServerIds(ids: Iterable<string>): void {
|
|
22
|
+
const seen = new Set<string>();
|
|
23
|
+
for (const id of ids) {
|
|
24
|
+
if (seen.has(id)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`McpManagerResource received duplicate MCP server id "${id}". Server ids must be unique because they are used for lookups, OAuth routing, and tool name prefixes.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
seen.add(id);
|
|
30
|
+
}
|
|
31
|
+
}
|