@mcp-ts/sdk 1.6.1 → 1.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-ts/sdk",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,6 +42,19 @@ export interface McpOAuthCallbackContentProps {
42
42
 
43
43
  const AUTH_CODE_MESSAGE = 'MCP_AUTH_CODE';
44
44
  const AUTH_RESULT_MESSAGE = 'MCP_AUTH_RESULT';
45
+ const AUTH_CHANNEL_NAME = 'mcp-auth-channel';
46
+
47
+ function createAuthBroadcastChannel(): BroadcastChannel | null {
48
+ if (typeof BroadcastChannel === 'undefined') {
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ return new BroadcastChannel(AUTH_CHANNEL_NAME);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
45
58
 
46
59
  function postPopupResult(
47
60
  popupWindow: WindowProxy | null,
@@ -51,13 +64,23 @@ function postPopupResult(
51
64
  error?: string;
52
65
  }
53
66
  ): void {
54
- popupWindow?.postMessage(
55
- {
56
- type: AUTH_RESULT_MESSAGE,
57
- ...result,
58
- },
59
- window.location.origin
60
- );
67
+ const payload = {
68
+ type: AUTH_RESULT_MESSAGE,
69
+ ...result,
70
+ };
71
+
72
+ try {
73
+ popupWindow?.postMessage(payload, window.location.origin);
74
+ } catch {
75
+ // COOP can leave a WindowProxy reference that is no longer usable.
76
+ // The BroadcastChannel path below is the reliable fallback.
77
+ }
78
+
79
+ const channel = createAuthBroadcastChannel();
80
+ if (channel) {
81
+ channel.postMessage(payload);
82
+ channel.close();
83
+ }
61
84
  }
62
85
 
63
86
  /**
@@ -130,14 +153,16 @@ export function useMcpOAuthPopup<TConnection extends OAuthPopupConnectionLike>(
130
153
  finishAuth: (sessionId: string, code: string) => Promise<unknown>
131
154
  ): void {
132
155
  const pendingPopupsRef = useRef<Map<string, WindowProxy>>(new Map());
156
+ const processingCodesRef = useRef<Set<string>>(new Set());
133
157
 
134
158
  useEffect(() => {
135
159
  const handleMessage = async (event: MessageEvent) => {
136
- if (event.origin !== window.location.origin) {
160
+ if (event.origin && event.origin !== window.location.origin) {
137
161
  return;
138
162
  }
139
163
 
140
- if (event.data?.type !== AUTH_CODE_MESSAGE || !event.data.code) {
164
+ const code = typeof event.data?.code === 'string' ? event.data.code : '';
165
+ if (event.data?.type !== AUTH_CODE_MESSAGE || !code) {
141
166
  return;
142
167
  }
143
168
 
@@ -146,56 +171,84 @@ export function useMcpOAuthPopup<TConnection extends OAuthPopupConnectionLike>(
146
171
  : null;
147
172
  const targetSessionId = typeof event.data.sessionId === 'string' ? event.data.sessionId : '';
148
173
 
174
+ if (popupWindow && targetSessionId) {
175
+ pendingPopupsRef.current.set(targetSessionId, popupWindow);
176
+ }
177
+
149
178
  if (!targetSessionId) {
150
- postPopupResult(popupWindow, {
151
- success: false,
152
- error: 'Missing OAuth session identifier',
153
- });
179
+ if (popupWindow) {
180
+ postPopupResult(popupWindow, {
181
+ success: false,
182
+ error: 'Missing OAuth session identifier',
183
+ });
184
+ }
154
185
  return;
155
186
  }
156
187
 
157
188
  const targetSession = connections.find((connection) => connection.sessionId === targetSessionId);
158
189
  if (!targetSession) {
159
- postPopupResult(popupWindow, {
160
- sessionId: targetSessionId,
161
- success: false,
162
- error: 'OAuth session not found in the current client state',
163
- });
190
+ if (popupWindow) {
191
+ postPopupResult(popupWindow, {
192
+ sessionId: targetSessionId,
193
+ success: false,
194
+ error: 'OAuth session not found in the current client state',
195
+ });
196
+ }
164
197
  return;
165
198
  }
166
199
 
167
- if (popupWindow) {
168
- pendingPopupsRef.current.set(targetSession.sessionId, popupWindow);
200
+ const codeKey = `${targetSession.sessionId}:${code}`;
201
+ if (processingCodesRef.current.has(codeKey)) {
202
+ return;
169
203
  }
204
+ processingCodesRef.current.add(codeKey);
170
205
 
171
206
  try {
172
- await finishAuth(targetSession.sessionId, event.data.code);
207
+ await finishAuth(targetSession.sessionId, code);
173
208
  } catch (error) {
209
+ processingCodesRef.current.delete(codeKey);
174
210
  pendingPopupsRef.current.delete(targetSession.sessionId);
175
- postPopupResult(popupWindow, {
176
- sessionId: targetSession.sessionId,
177
- success: false,
178
- error: error instanceof Error ? error.message : 'Failed to finish auth',
179
- });
211
+ if (popupWindow) {
212
+ postPopupResult(popupWindow, {
213
+ sessionId: targetSession.sessionId,
214
+ success: false,
215
+ error: error instanceof Error ? error.message : 'Failed to finish auth',
216
+ });
217
+ }
218
+ }
219
+ };
220
+
221
+ const channel = createAuthBroadcastChannel();
222
+ const handleChannelMessage = (event: MessageEvent) => {
223
+ if (event.data?.type === AUTH_CODE_MESSAGE) {
224
+ void handleMessage(event);
180
225
  }
181
226
  };
182
227
 
183
228
  window.addEventListener('message', handleMessage);
184
- return () => window.removeEventListener('message', handleMessage);
229
+ channel?.addEventListener('message', handleChannelMessage);
230
+
231
+ return () => {
232
+ window.removeEventListener('message', handleMessage);
233
+ channel?.removeEventListener('message', handleChannelMessage);
234
+ channel?.close();
235
+ };
185
236
  }, [connections, finishAuth]);
186
237
 
187
238
  useEffect(() => {
188
239
  for (const connection of connections) {
189
- const popupWindow = pendingPopupsRef.current.get(connection.sessionId);
190
- if (!popupWindow) {
191
- continue;
192
- }
240
+ const popupWindow = pendingPopupsRef.current.get(connection.sessionId) || null;
193
241
 
194
- if (connection.state === 'AUTHENTICATED') {
242
+ if (connection.state === 'AUTHENTICATED' || connection.state === 'READY' || connection.state === 'CONNECTED') {
195
243
  postPopupResult(popupWindow, {
196
244
  sessionId: connection.sessionId,
197
245
  success: true,
198
246
  });
247
+ for (const codeKey of processingCodesRef.current) {
248
+ if (codeKey.startsWith(`${connection.sessionId}:`)) {
249
+ processingCodesRef.current.delete(codeKey);
250
+ }
251
+ }
199
252
  pendingPopupsRef.current.delete(connection.sessionId);
200
253
  continue;
201
254
  }
@@ -206,6 +259,11 @@ export function useMcpOAuthPopup<TConnection extends OAuthPopupConnectionLike>(
206
259
  success: false,
207
260
  error: connection.error || 'Failed to complete authorization',
208
261
  });
262
+ for (const codeKey of processingCodesRef.current) {
263
+ if (codeKey.startsWith(`${connection.sessionId}:`)) {
264
+ processingCodesRef.current.delete(codeKey);
265
+ }
266
+ }
209
267
  pendingPopupsRef.current.delete(connection.sessionId);
210
268
  }
211
269
  }
@@ -238,16 +296,13 @@ export function McpOAuthCallbackContent({
238
296
  const [phase, setPhase] = useState<'loading' | 'success' | 'error'>(debugPhase || 'loading');
239
297
  const [errorMessage, setErrorMessage] = useState('');
240
298
 
241
- const openerMissing = typeof window !== 'undefined' ? !window.opener : false;
242
299
  const missingCode = !code;
243
300
  const missingSessionId = !sessionId;
244
- const blockingError = openerMissing
245
- ? 'Error: No opener window found. This window should be opened from the app.'
246
- : missingCode
247
- ? 'Error: No authorization code received.'
248
- : missingSessionId
249
- ? 'Error: No OAuth state received.'
250
- : null;
301
+ const blockingError = missingCode
302
+ ? 'Error: No authorization code received.'
303
+ : missingSessionId
304
+ ? 'Error: No OAuth state received.'
305
+ : null;
251
306
 
252
307
  useEffect(() => {
253
308
  if (debugPhase) {
@@ -263,9 +318,9 @@ export function McpOAuthCallbackContent({
263
318
  }
264
319
 
265
320
  let closed = false;
266
-
321
+ const channel = createAuthBroadcastChannel();
267
322
  const handleResult = (event: MessageEvent) => {
268
- if (event.origin !== window.location.origin) {
323
+ if (event.origin && event.origin !== window.location.origin) {
269
324
  return;
270
325
  }
271
326
 
@@ -280,6 +335,7 @@ export function McpOAuthCallbackContent({
280
335
  if (event.data.success) {
281
336
  setPhase('success');
282
337
  window.removeEventListener('message', handleResult);
338
+ channel?.close();
283
339
  closed = true;
284
340
  window.setTimeout(() => window.close(), 1200);
285
341
  return;
@@ -294,23 +350,27 @@ export function McpOAuthCallbackContent({
294
350
  };
295
351
 
296
352
  window.addEventListener('message', handleResult);
353
+ channel?.addEventListener('message', handleResult);
354
+
355
+ const payload = { type: AUTH_CODE_MESSAGE, code, sessionId };
297
356
 
298
- try {
299
- window.opener.postMessage(
300
- { type: AUTH_CODE_MESSAGE, code, sessionId },
301
- window.location.origin
302
- );
303
- } catch (error) {
304
- console.error('Failed to communicate with opener:', error);
305
- window.setTimeout(() => {
357
+ if (window.opener) {
358
+ try {
359
+ window.opener.postMessage(payload, window.location.origin);
360
+ } catch {
306
361
  setPhase('error');
307
362
  setErrorMessage('Error: Could not communicate with main window.');
308
- }, 0);
363
+ }
364
+ }
365
+
366
+ if (channel) {
367
+ channel.postMessage(payload);
309
368
  }
310
369
 
311
370
  return () => {
312
371
  if (!closed) {
313
372
  window.removeEventListener('message', handleResult);
373
+ channel?.close();
314
374
  }
315
375
  };
316
376
  }, [blockingError, code, sessionId, debugPhase]);