@mcp-ts/sdk 1.6.1 → 2.0.0

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.
Files changed (114) hide show
  1. package/README.md +12 -6
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-adapter.js +4 -5
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +4 -5
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +3 -3
  9. package/dist/adapters/agui-middleware.d.ts +3 -3
  10. package/dist/adapters/ai-adapter.d.mts +9 -3
  11. package/dist/adapters/ai-adapter.d.ts +9 -3
  12. package/dist/adapters/ai-adapter.js +20 -6
  13. package/dist/adapters/ai-adapter.js.map +1 -1
  14. package/dist/adapters/ai-adapter.mjs +20 -6
  15. package/dist/adapters/ai-adapter.mjs.map +1 -1
  16. package/dist/adapters/langchain-adapter.d.mts +3 -3
  17. package/dist/adapters/langchain-adapter.d.ts +3 -3
  18. package/dist/adapters/langchain-adapter.js +9 -6
  19. package/dist/adapters/langchain-adapter.js.map +1 -1
  20. package/dist/adapters/langchain-adapter.mjs +9 -6
  21. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  22. package/dist/adapters/mastra-adapter.d.mts +1 -1
  23. package/dist/adapters/mastra-adapter.d.ts +1 -1
  24. package/dist/adapters/mastra-adapter.js +5 -1
  25. package/dist/adapters/mastra-adapter.js.map +1 -1
  26. package/dist/adapters/mastra-adapter.mjs +5 -1
  27. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  28. package/dist/bin/mcp-ts.js +7 -1
  29. package/dist/bin/mcp-ts.js.map +1 -1
  30. package/dist/bin/mcp-ts.mjs +7 -1
  31. package/dist/bin/mcp-ts.mjs.map +1 -1
  32. package/dist/client/index.d.mts +2 -2
  33. package/dist/client/index.d.ts +2 -2
  34. package/dist/client/index.js +9 -13
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/index.mjs +9 -13
  37. package/dist/client/index.mjs.map +1 -1
  38. package/dist/client/react.d.mts +7 -7
  39. package/dist/client/react.d.ts +7 -7
  40. package/dist/client/react.js +111 -63
  41. package/dist/client/react.js.map +1 -1
  42. package/dist/client/react.mjs +111 -63
  43. package/dist/client/react.mjs.map +1 -1
  44. package/dist/client/vue.d.mts +7 -7
  45. package/dist/client/vue.d.ts +7 -7
  46. package/dist/client/vue.js +14 -18
  47. package/dist/client/vue.js.map +1 -1
  48. package/dist/client/vue.mjs +14 -18
  49. package/dist/client/vue.mjs.map +1 -1
  50. package/dist/{index-DhA-OEAe.d.ts → index-C9gvpxy5.d.ts} +5 -5
  51. package/dist/{index-bFL4ZF2N.d.mts → index-eaH14_5u.d.mts} +5 -5
  52. package/dist/index.d.mts +6 -6
  53. package/dist/index.d.ts +6 -6
  54. package/dist/index.js +616 -370
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +615 -370
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/{multi-session-client-CHE8QpVE.d.ts → multi-session-client-BYtguGJm.d.ts} +22 -22
  59. package/dist/{multi-session-client-CQsRbxYI.d.mts → multi-session-client-DYNe6az3.d.mts} +22 -22
  60. package/dist/server/index.d.mts +31 -34
  61. package/dist/server/index.d.ts +31 -34
  62. package/dist/server/index.js +531 -256
  63. package/dist/server/index.js.map +1 -1
  64. package/dist/server/index.mjs +530 -256
  65. package/dist/server/index.mjs.map +1 -1
  66. package/dist/shared/index.d.mts +5 -5
  67. package/dist/shared/index.d.ts +5 -5
  68. package/dist/shared/index.js +76 -101
  69. package/dist/shared/index.js.map +1 -1
  70. package/dist/shared/index.mjs +76 -101
  71. package/dist/shared/index.mjs.map +1 -1
  72. package/dist/{tool-router-Dh2804tM.d.ts → tool-router-Ddtybmr0.d.ts} +71 -73
  73. package/dist/{tool-router-BVaV1udm.d.mts → tool-router-Dnd6IOKC.d.mts} +71 -73
  74. package/dist/{types-rIuN1CQi.d.mts → types-BCAG20P6.d.mts} +4 -4
  75. package/dist/{types-rIuN1CQi.d.ts → types-BCAG20P6.d.ts} +4 -4
  76. package/dist/{utils-0qmYrqoa.d.mts → utils-DELRKQPU.d.mts} +1 -1
  77. package/dist/{utils-0qmYrqoa.d.ts → utils-DELRKQPU.d.ts} +1 -1
  78. package/migrations/neon/20260513010000_install_mcp_sessions.sql +69 -0
  79. package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  80. package/{supabase/migrations → migrations/supabase}/20260330195700_install_mcp_sessions.sql +7 -9
  81. package/package.json +14 -5
  82. package/src/adapters/ai-adapter.ts +30 -1
  83. package/src/adapters/langchain-adapter.ts +6 -2
  84. package/src/adapters/mastra-adapter.ts +6 -2
  85. package/src/bin/mcp-ts.ts +8 -1
  86. package/src/client/core/app-host.ts +1 -1
  87. package/src/client/core/sse-client.ts +12 -14
  88. package/src/client/core/types.ts +1 -1
  89. package/src/client/react/oauth-popup.tsx +111 -51
  90. package/src/client/react/use-mcp-apps.tsx +1 -1
  91. package/src/client/react/use-mcp.ts +11 -11
  92. package/src/client/vue/use-mcp.ts +10 -10
  93. package/src/server/handlers/nextjs-handler.ts +18 -15
  94. package/src/server/handlers/sse-handler.ts +29 -29
  95. package/src/server/index.ts +1 -1
  96. package/src/server/mcp/multi-session-client.ts +17 -17
  97. package/src/server/mcp/oauth-client.ts +37 -37
  98. package/src/server/mcp/storage-oauth-provider.ts +17 -17
  99. package/src/server/storage/file-backend.ts +25 -25
  100. package/src/server/storage/index.ts +67 -10
  101. package/src/server/storage/memory-backend.ts +34 -34
  102. package/src/server/storage/neon-backend.ts +281 -0
  103. package/src/server/storage/redis-backend.ts +64 -64
  104. package/src/server/storage/sqlite-backend.ts +33 -33
  105. package/src/server/storage/supabase-backend.ts +23 -24
  106. package/src/server/storage/types.ts +18 -21
  107. package/src/shared/errors.ts +1 -1
  108. package/src/shared/index.ts +1 -2
  109. package/src/shared/meta-tools.ts +4 -6
  110. package/src/shared/schema-compressor.ts +2 -42
  111. package/src/shared/tool-index.ts +89 -84
  112. package/src/shared/tool-router.ts +0 -24
  113. package/src/shared/types.ts +4 -4
  114. /package/{supabase/migrations → migrations/supabase}/20260421010000_add_session_cleanup_cron.sql +0 -0
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@mcp-ts/sdk",
3
- "version": "1.6.1",
3
+ "version": "2.0.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "A lightweight MCP (Model Context Protocol) client library for JavaScript and cross-runtime environments, supporting MCP Apps in host applications and multiple storage backends (Memory, File, Redis, Supabase).",
7
+ "description": "A lightweight MCP (Model Context Protocol) client library for JavaScript and cross-runtime environments, supporting MCP Apps in host applications and multiple storage backends (Memory, File, Redis, Supabase, Neon).",
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.mjs",
10
10
  "types": "./dist/index.d.ts",
@@ -71,7 +71,7 @@
71
71
  "files": [
72
72
  "dist",
73
73
  "src",
74
- "supabase",
74
+ "migrations",
75
75
  "README.md",
76
76
  "LICENSE"
77
77
  ],
@@ -117,13 +117,15 @@
117
117
  "peerDependencies": {
118
118
  "@ag-ui/client": ">=0.0.40",
119
119
  "@langchain/core": "^1.1.39",
120
+ "@neondatabase/serverless": "^1.1.0",
120
121
  "@supabase/supabase-js": "^2.0.0",
121
122
  "ai": "^6.0.0",
122
123
  "better-sqlite3": "^12.0.0",
123
124
  "ioredis": "^5.0.0",
124
125
  "react": ">=18.0.0",
125
126
  "rxjs": ">=7.0.0",
126
- "zod": "^3.23.0"
127
+ "zod": "^3.23.0",
128
+ "json-schema-to-zod": "^2.7.0"
127
129
  },
128
130
  "peerDependenciesMeta": {
129
131
  "react": {
@@ -135,6 +137,9 @@
135
137
  "@langchain/core": {
136
138
  "optional": true
137
139
  },
140
+ "@neondatabase/serverless": {
141
+ "optional": true
142
+ },
138
143
  "zod": {
139
144
  "optional": true
140
145
  },
@@ -152,18 +157,21 @@
152
157
  },
153
158
  "@supabase/supabase-js": {
154
159
  "optional": true
160
+ },
161
+ "json-schema-to-zod": {
162
+ "optional": true
155
163
  }
156
164
  },
157
165
  "dependencies": {
158
166
  "@modelcontextprotocol/ext-apps": "^1.5.0",
159
167
  "@modelcontextprotocol/sdk": "^1.29.0",
160
168
  "json-schema": "^0.4.0",
161
- "json-schema-to-zod": "^2.7.0",
162
169
  "nanoid": "^5.1.6"
163
170
  },
164
171
  "devDependencies": {
165
172
  "@ag-ui/client": "^0.0.52",
166
173
  "@langchain/core": "^1.1.39",
174
+ "@neondatabase/serverless": "^1.1.0",
167
175
  "@playwright/test": "^1.58.0",
168
176
  "@supabase/supabase-js": "^2.48.0",
169
177
  "@types/better-sqlite3": "^7.6.13",
@@ -175,6 +183,7 @@
175
183
  "better-sqlite3": "^12.6.2",
176
184
  "ioredis": "^5.9.2",
177
185
  "ioredis-mock": "^8.13.1",
186
+ "json-schema-to-zod": "^2.7.0",
178
187
  "playwright": "^1.58.0",
179
188
  "react": "^18.3.1",
180
189
  "react-dom": "^18.3.1",
@@ -22,6 +22,13 @@ export interface AIAdapterOptions {
22
22
  * When not provided, all tools are returned as before (backward-compatible).
23
23
  */
24
24
  toolRouter?: ToolRouter;
25
+
26
+ /**
27
+ * Optional custom callback to determine if a tool requires user approval.
28
+ * Can return a boolean or a Promise<boolean>.
29
+ * If not provided, defaults to checking the tool's `destructiveHint` annotation.
30
+ */
31
+ needsApproval?: (tool: any, args: any) => boolean | Promise<boolean>;
25
32
  }
26
33
 
27
34
  /**
@@ -80,7 +87,12 @@ export class AIAdapter {
80
87
  const errorMessage = error instanceof Error ? error.message : String(error);
81
88
  throw new Error(`Tool execution failed: ${errorMessage}`);
82
89
  }
83
- }
90
+ },
91
+ needsApproval: this.options.needsApproval
92
+ ? (args: any) => this.options.needsApproval!(tool, args)
93
+ : (tool.annotations as any)?.destructiveHint === true
94
+ ? () => true
95
+ : undefined
84
96
  }
85
97
  ];
86
98
  })
@@ -166,6 +178,23 @@ export class AIAdapter {
166
178
  // route directly to the correct MCP client
167
179
  return await router.callTool(tool.name, args, namespace);
168
180
  },
181
+ needsApproval: this.options.needsApproval
182
+ ? (args: any) => this.options.needsApproval!(tool, args)
183
+ : (args: any) => {
184
+ // Default HITL logic using annotations
185
+ if (tool.name === 'mcp_execute_tool') {
186
+ const targetToolName = String(args?.toolName ?? "");
187
+ const targetNamespace = String(args?.serverId ?? "") || undefined;
188
+ if (!targetToolName) return false;
189
+ try {
190
+ const targetTool = router.getToolSchema(targetToolName, targetNamespace);
191
+ return (targetTool as any)?.annotations?.destructiveHint === true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+ return (tool.annotations as any)?.destructiveHint === true;
197
+ }
169
198
  },
170
199
  ];
171
200
  })
@@ -96,9 +96,13 @@ export class LangChainAdapter {
96
96
  const zodSchemaString = parseSchema(schema);
97
97
  // eslint-disable-next-line
98
98
  return new Function('z', 'return ' + zodSchemaString)(this.z);
99
- } catch (error) {
99
+ } catch (error: any) {
100
100
  // Fallback: Accept any object if conversion fails
101
- console.warn('[LangChainAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
101
+ if (error.code === 'MODULE_NOT_FOUND') {
102
+ console.warn('[LangChainAdapter] json-schema-to-zod is not installed. To improve type checking, install it with: npm install json-schema-to-zod');
103
+ } else {
104
+ console.warn('[LangChainAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
105
+ }
102
106
  return this.z!.record(this.z!.any()).optional().describe("Dynamic Input");
103
107
  }
104
108
  }
@@ -87,9 +87,13 @@ export class MastraAdapter {
87
87
  const zodSchemaString = parseSchema(schema);
88
88
  // eslint-disable-next-line
89
89
  return new Function('z', 'return ' + zodSchemaString)(this.z);
90
- } catch (error) {
90
+ } catch (error: any) {
91
91
  // Fallback: Accept any object if conversion fails
92
- console.warn('[MastraAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
92
+ if (error.code === 'MODULE_NOT_FOUND') {
93
+ console.warn('[MastraAdapter] json-schema-to-zod is not installed. To improve type checking, install it with: npm install json-schema-to-zod');
94
+ } else {
95
+ console.warn('[MastraAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
96
+ }
93
97
  return this.z!.record(this.z!.any()).optional().describe("Dynamic Input");
94
98
  }
95
99
  }
package/src/bin/mcp-ts.ts CHANGED
@@ -35,7 +35,10 @@ async function initSupabase() {
35
35
  // The supabase/ migrations are at the root of the package.
36
36
  // We need to look up two levels to find 'supabase' folder in the package.
37
37
  const pkgRoot = path.resolve(__dirname, '../..');
38
- const sourceDir = path.join(pkgRoot, 'supabase', 'migrations');
38
+ const sourceDir = resolveFirstExistingPath([
39
+ path.join(pkgRoot, 'migrations', 'supabase'),
40
+ path.join(pkgRoot, 'supabase', 'migrations'),
41
+ ]);
39
42
 
40
43
  if (!fs.existsSync(sourceDir)) {
41
44
  console.error(`❌ Error: Could not find migration files in package at: ${sourceDir}`);
@@ -96,6 +99,10 @@ async function initSupabase() {
96
99
  }
97
100
  }
98
101
 
102
+ function resolveFirstExistingPath(paths: string[]): string {
103
+ return paths.find(candidate => fs.existsSync(candidate)) || paths[0];
104
+ }
105
+
99
106
  run().catch(err => {
100
107
  console.error(err);
101
108
  process.exit(1);
@@ -559,7 +559,7 @@ export class AppHost {
559
559
  private async getSessionId(): Promise<string | undefined> {
560
560
  if (this.sessionId) return this.sessionId;
561
561
  if (!this.client) return undefined;
562
- const result = await this.client.getSessions();
562
+ const result = await this.client.listSessions();
563
563
  return result.sessions?.[0]?.sessionId;
564
564
  }
565
565
 
@@ -20,7 +20,7 @@ import type {
20
20
  SessionListResult,
21
21
  ConnectResult,
22
22
  DisconnectResult,
23
- RestoreSessionResult,
23
+ GetSessionResult,
24
24
  FinishAuthResult,
25
25
  ListToolsRpcResult,
26
26
  ListPromptsResult,
@@ -32,7 +32,7 @@ export interface SSEClientOptions {
32
32
  url: string;
33
33
 
34
34
  /** User/Client identifier */
35
- identity: string;
35
+ userId: string;
36
36
 
37
37
  /** Optional auth token for authenticated requests */
38
38
  authToken?: string;
@@ -87,8 +87,8 @@ export class SSEClient {
87
87
  return this.connected;
88
88
  }
89
89
 
90
- async getSessions(): Promise<SessionListResult> {
91
- return this.sendRequest<SessionListResult>('getSessions');
90
+ async listSessions(): Promise<SessionListResult> {
91
+ return this.sendRequest<SessionListResult>('listSessions');
92
92
  }
93
93
 
94
94
  async connectToServer(params: ConnectParams): Promise<ConnectResult> {
@@ -113,8 +113,8 @@ export class SSEClient {
113
113
  return result;
114
114
  }
115
115
 
116
- async restoreSession(sessionId: string): Promise<RestoreSessionResult> {
117
- return this.sendRequest<RestoreSessionResult>('restoreSession', { sessionId });
116
+ async getSession(sessionId: string): Promise<GetSessionResult> {
117
+ return this.sendRequest<GetSessionResult>('getSession', { sessionId });
118
118
  }
119
119
 
120
120
  async finishAuth(sessionId: string, code: string): Promise<FinishAuthResult> {
@@ -198,7 +198,7 @@ export class SSEClient {
198
198
  const data = await this.readRpcResponseFromStream(response, {
199
199
  delayConnectionEvents:
200
200
  method === 'connect' ||
201
- method === 'restoreSession' ||
201
+ method === 'getSession' ||
202
202
  method === 'finishAuth',
203
203
  });
204
204
  return this.parseRpcResponse<T>(data);
@@ -311,22 +311,20 @@ export class SSEClient {
311
311
  }
312
312
 
313
313
  private buildUrl(): string {
314
- const url = new URL(this.options.url, globalThis.location?.origin);
315
- url.searchParams.set('identity', this.options.identity);
316
- if (this.options.authToken) {
317
- url.searchParams.set('token', this.options.authToken);
318
- }
319
- return url.toString();
314
+ return new URL(this.options.url, globalThis.location?.origin).toString();
320
315
  }
321
316
 
322
317
  private buildHeaders(): HeadersInit {
323
- const headers: HeadersInit = {
318
+ const headers: Record<string, string> = {
324
319
  'Content-Type': 'application/json',
325
320
  'Accept': 'text/event-stream',
321
+ 'x-mcp-user-id': this.options.userId,
326
322
  };
323
+
327
324
  if (this.options.authToken) {
328
325
  headers['Authorization'] = `Bearer ${this.options.authToken}`;
329
326
  }
327
+
330
328
  return headers;
331
329
  }
332
330
 
@@ -17,7 +17,7 @@ export interface AppHostClient {
17
17
  /**
18
18
  * Get list of active sessions
19
19
  */
20
- getSessions(): Promise<SessionListResult>;
20
+ listSessions(): Promise<SessionListResult>;
21
21
 
22
22
  /**
23
23
  * Call a tool on a specific session
@@ -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]);
@@ -72,7 +72,7 @@ export interface McpAppRendererProps extends Pick<UseAppHostOptions, 'sandbox' |
72
72
 
73
73
  type McpAppViewProps = McpAppRendererProps & {
74
74
  /**
75
- * Ref avoids tying `McpAppRenderer` identity to `mcpClient`: when `connections` updates, `useMcp()` still
75
+ * Ref avoids tying `McpAppRenderer` userId to `mcpClient`: when `connections` updates, `useMcp()` still
76
76
  * returns a new object (correct for `useEffect` deps), but the iframe must not remount.
77
77
  */
78
78
  clientRef: MutableRefObject<McpClient | null>;
@@ -25,7 +25,7 @@ export interface UseMcpOptions {
25
25
  /**
26
26
  * User/Client identifier
27
27
  */
28
- identity: string;
28
+ userId: string;
29
29
 
30
30
  /**
31
31
  * Optional auth token
@@ -110,7 +110,7 @@ export interface McpClient {
110
110
  serverName: string;
111
111
  serverUrl: string;
112
112
  callbackUrl: string;
113
- transportType?: 'sse' | 'streamable_http';
113
+ transportType?: 'sse' | 'streamable-http';
114
114
  }) => Promise<string>;
115
115
 
116
116
  /**
@@ -126,7 +126,7 @@ export interface McpClient {
126
126
  serverName: string;
127
127
  serverUrl: string;
128
128
  callbackUrl: string;
129
- transportType?: 'sse' | 'streamable_http';
129
+ transportType?: 'sse' | 'streamable-http';
130
130
  }) => Promise<string>;
131
131
 
132
132
  /**
@@ -220,7 +220,7 @@ export interface McpClient {
220
220
  export function useMcp(options: UseMcpOptions): McpClient {
221
221
  const {
222
222
  url,
223
- identity,
223
+ userId,
224
224
  authToken,
225
225
  autoConnect = true,
226
226
  autoInitialize = true,
@@ -249,7 +249,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
249
249
 
250
250
  const clientOptions: SSEClientOptions = {
251
251
  url,
252
- identity,
252
+ userId,
253
253
  authToken,
254
254
  onConnectionEvent: (event) => {
255
255
  // Update local state based on event
@@ -285,7 +285,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
285
285
  isMountedRef.current = false;
286
286
  client.disconnect();
287
287
  };
288
- }, [url, identity, authToken, autoConnect, autoInitialize]);
288
+ }, [url, userId, authToken, autoConnect, autoInitialize]);
289
289
 
290
290
  /**
291
291
  * Update connections based on event
@@ -441,7 +441,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
441
441
  try {
442
442
  setIsInitializing(true);
443
443
 
444
- const result = await clientRef.current.getSessions();
444
+ const result = await clientRef.current.listSessions();
445
445
  const sessions = result.sessions || [];
446
446
 
447
447
  // Initialize connections
@@ -470,7 +470,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
470
470
  return;
471
471
  }
472
472
  suppressAuthRedirectSessionsRef.current.add(session.sessionId);
473
- await clientRef.current.restoreSession(session.sessionId);
473
+ await clientRef.current.getSession(session.sessionId);
474
474
  } catch (error) {
475
475
  console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
476
476
  } finally {
@@ -498,7 +498,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
498
498
  serverName: string;
499
499
  serverUrl: string;
500
500
  callbackUrl: string;
501
- transportType?: 'sse' | 'streamable_http';
501
+ transportType?: 'sse' | 'streamable-http';
502
502
  }): Promise<string> => {
503
503
  if (!clientRef.current) {
504
504
  throw new Error('SSE client not initialized');
@@ -519,7 +519,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
519
519
  serverName: string;
520
520
  serverUrl: string;
521
521
  callbackUrl: string;
522
- transportType?: 'sse' | 'streamable_http';
522
+ transportType?: 'sse' | 'streamable-http';
523
523
  }): Promise<string> => {
524
524
  if (!clientRef.current) {
525
525
  throw new Error('SSE client not initialized');
@@ -602,7 +602,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
602
602
  }
603
603
  // Ensure this attempt is not suppressed as background restore.
604
604
  suppressAuthRedirectSessionsRef.current.delete(sessionId);
605
- await clientRef.current.restoreSession(sessionId);
605
+ await clientRef.current.getSession(sessionId);
606
606
  }, []);
607
607
 
608
608
  /**