@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.
- package/README.md +12 -6
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-adapter.js +4 -5
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +4 -5
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/ai-adapter.d.mts +9 -3
- package/dist/adapters/ai-adapter.d.ts +9 -3
- package/dist/adapters/ai-adapter.js +20 -6
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +20 -6
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.js +9 -6
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +9 -6
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.js +5 -1
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +5 -1
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/bin/mcp-ts.js +7 -1
- package/dist/bin/mcp-ts.js.map +1 -1
- package/dist/bin/mcp-ts.mjs +7 -1
- package/dist/bin/mcp-ts.mjs.map +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +9 -13
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +9 -13
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +7 -7
- package/dist/client/react.d.ts +7 -7
- package/dist/client/react.js +111 -63
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +111 -63
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +7 -7
- package/dist/client/vue.d.ts +7 -7
- package/dist/client/vue.js +14 -18
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +14 -18
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-DhA-OEAe.d.ts → index-C9gvpxy5.d.ts} +5 -5
- package/dist/{index-bFL4ZF2N.d.mts → index-eaH14_5u.d.mts} +5 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +616 -370
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +615 -370
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CHE8QpVE.d.ts → multi-session-client-BYtguGJm.d.ts} +22 -22
- package/dist/{multi-session-client-CQsRbxYI.d.mts → multi-session-client-DYNe6az3.d.mts} +22 -22
- package/dist/server/index.d.mts +31 -34
- package/dist/server/index.d.ts +31 -34
- package/dist/server/index.js +531 -256
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +530 -256
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +5 -5
- package/dist/shared/index.d.ts +5 -5
- package/dist/shared/index.js +76 -101
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +76 -101
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-Dh2804tM.d.ts → tool-router-Ddtybmr0.d.ts} +71 -73
- package/dist/{tool-router-BVaV1udm.d.mts → tool-router-Dnd6IOKC.d.mts} +71 -73
- package/dist/{types-rIuN1CQi.d.mts → types-BCAG20P6.d.mts} +4 -4
- package/dist/{types-rIuN1CQi.d.ts → types-BCAG20P6.d.ts} +4 -4
- package/dist/{utils-0qmYrqoa.d.mts → utils-DELRKQPU.d.mts} +1 -1
- package/dist/{utils-0qmYrqoa.d.ts → utils-DELRKQPU.d.ts} +1 -1
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +69 -0
- package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
- package/{supabase/migrations → migrations/supabase}/20260330195700_install_mcp_sessions.sql +7 -9
- package/package.json +14 -5
- package/src/adapters/ai-adapter.ts +30 -1
- package/src/adapters/langchain-adapter.ts +6 -2
- package/src/adapters/mastra-adapter.ts +6 -2
- package/src/bin/mcp-ts.ts +8 -1
- package/src/client/core/app-host.ts +1 -1
- package/src/client/core/sse-client.ts +12 -14
- package/src/client/core/types.ts +1 -1
- package/src/client/react/oauth-popup.tsx +111 -51
- package/src/client/react/use-mcp-apps.tsx +1 -1
- package/src/client/react/use-mcp.ts +11 -11
- package/src/client/vue/use-mcp.ts +10 -10
- package/src/server/handlers/nextjs-handler.ts +18 -15
- package/src/server/handlers/sse-handler.ts +29 -29
- package/src/server/index.ts +1 -1
- package/src/server/mcp/multi-session-client.ts +17 -17
- package/src/server/mcp/oauth-client.ts +37 -37
- package/src/server/mcp/storage-oauth-provider.ts +17 -17
- package/src/server/storage/file-backend.ts +25 -25
- package/src/server/storage/index.ts +67 -10
- package/src/server/storage/memory-backend.ts +34 -34
- package/src/server/storage/neon-backend.ts +281 -0
- package/src/server/storage/redis-backend.ts +64 -64
- package/src/server/storage/sqlite-backend.ts +33 -33
- package/src/server/storage/supabase-backend.ts +23 -24
- package/src/server/storage/types.ts +18 -21
- package/src/shared/errors.ts +1 -1
- package/src/shared/index.ts +1 -2
- package/src/shared/meta-tools.ts +4 -6
- package/src/shared/schema-compressor.ts +2 -42
- package/src/shared/tool-index.ts +89 -84
- package/src/shared/tool-router.ts +0 -24
- package/src/shared/types.ts +4 -4
- /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": "
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
return this.sendRequest<SessionListResult>('
|
|
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
|
|
117
|
-
return this.sendRequest<
|
|
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 === '
|
|
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
|
-
|
|
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:
|
|
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
|
|
package/src/client/core/types.ts
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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,
|
|
207
|
+
await finishAuth(targetSession.sessionId, code);
|
|
173
208
|
} catch (error) {
|
|
209
|
+
processingCodesRef.current.delete(codeKey);
|
|
174
210
|
pendingPopupsRef.current.delete(targetSession.sessionId);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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 =
|
|
245
|
-
? 'Error: No
|
|
246
|
-
:
|
|
247
|
-
? 'Error: No
|
|
248
|
-
:
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
}
|
|
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`
|
|
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
|
-
|
|
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' | '
|
|
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' | '
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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' | '
|
|
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' | '
|
|
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.
|
|
605
|
+
await clientRef.current.getSession(sessionId);
|
|
606
606
|
}, []);
|
|
607
607
|
|
|
608
608
|
/**
|