@mcp-ts/sdk 1.3.1 → 1.3.3
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 +371 -290
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- 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 +3 -3
- package/dist/adapters/ai-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/mastra-adapter.d.mts +3 -3
- package/dist/adapters/mastra-adapter.d.ts +3 -3
- package/dist/client/index.d.mts +10 -66
- package/dist/client/index.d.ts +10 -66
- package/dist/client/index.js +91 -173
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +91 -173
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +15 -5
- package/dist/client/react.d.ts +15 -5
- package/dist/client/react.js +130 -182
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +130 -182
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +27 -7
- package/dist/client/vue.d.ts +27 -7
- package/dist/client/vue.js +131 -182
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +131 -182
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{events-BgeztGYZ.d.mts → events-CK3N--3g.d.mts} +2 -0
- package/dist/{events-BgeztGYZ.d.ts → events-CK3N--3g.d.ts} +2 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +224 -258
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +224 -258
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CxogNckF.d.mts → multi-session-client-DzjmT7FX.d.mts} +4 -10
- package/dist/{multi-session-client-cox_WXUj.d.ts → multi-session-client-FAFpUzZ4.d.ts} +4 -10
- package/dist/server/index.d.mts +18 -23
- package/dist/server/index.d.ts +18 -23
- package/dist/server/index.js +133 -85
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +133 -85
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +3 -3
- package/dist/shared/index.d.ts +3 -3
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{types-CLccx9wW.d.mts → types-CW6lghof.d.mts} +6 -0
- package/dist/{types-CLccx9wW.d.ts → types-CW6lghof.d.ts} +6 -0
- package/package.json +1 -1
- package/src/client/core/sse-client.ts +354 -493
- package/src/client/react/index.ts +16 -16
- package/src/client/react/use-mcp-apps.tsx +214 -214
- package/src/client/react/use-mcp.ts +84 -19
- package/src/client/vue/use-mcp.ts +119 -44
- package/src/server/handlers/nextjs-handler.ts +207 -217
- package/src/server/handlers/sse-handler.ts +14 -0
- package/src/server/mcp/oauth-client.ts +48 -46
- package/src/server/storage/types.ts +12 -5
- package/src/shared/events.ts +2 -0
- package/src/shared/types.ts +6 -0
package/dist/client/react.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { SSEClient, AppHost } from './index.js';
|
|
2
2
|
export { SSEClientOptions } from './index.js';
|
|
3
|
-
import { c as McpConnectionState, M as McpConnectionEvent } from '../events-
|
|
4
|
-
export { D as Disposable, a as DisposableStore, E as Emitter, b as Event, d as McpObservabilityEvent } from '../events-
|
|
5
|
-
import { T as ToolInfo, k as FinishAuthResult, n as ListToolsRpcResult, L as ListPromptsResult, l as ListResourcesResult } from '../types-
|
|
6
|
-
export { p as McpRpcRequest, q as McpRpcResponse } from '../types-
|
|
3
|
+
import { c as McpConnectionState, M as McpConnectionEvent } from '../events-CK3N--3g.js';
|
|
4
|
+
export { D as Disposable, a as DisposableStore, E as Emitter, b as Event, d as McpObservabilityEvent } from '../events-CK3N--3g.js';
|
|
5
|
+
import { T as ToolInfo, k as FinishAuthResult, n as ListToolsRpcResult, L as ListPromptsResult, l as ListResourcesResult } from '../types-CW6lghof.js';
|
|
6
|
+
export { p as McpRpcRequest, q as McpRpcResponse } from '../types-CW6lghof.js';
|
|
7
7
|
import React$1 from 'react';
|
|
8
8
|
import '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
|
|
@@ -54,6 +54,11 @@ interface UseMcpOptions {
|
|
|
54
54
|
* @default 60000
|
|
55
55
|
*/
|
|
56
56
|
requestTimeout?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Enable client debug logs.
|
|
59
|
+
* @default false
|
|
60
|
+
*/
|
|
61
|
+
debug?: boolean;
|
|
57
62
|
}
|
|
58
63
|
interface McpConnection {
|
|
59
64
|
sessionId: string;
|
|
@@ -63,8 +68,9 @@ interface McpConnection {
|
|
|
63
68
|
transport?: string;
|
|
64
69
|
state: McpConnectionState;
|
|
65
70
|
tools: ToolInfo[];
|
|
71
|
+
authUrl?: string;
|
|
66
72
|
error?: string;
|
|
67
|
-
|
|
73
|
+
createdAt?: Date;
|
|
68
74
|
}
|
|
69
75
|
interface McpClient$1 {
|
|
70
76
|
/**
|
|
@@ -125,6 +131,10 @@ interface McpClient$1 {
|
|
|
125
131
|
* Complete OAuth authorization
|
|
126
132
|
*/
|
|
127
133
|
finishAuth: (sessionId: string, code: string) => Promise<FinishAuthResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Explicitly resume OAuth flow for an existing session
|
|
136
|
+
*/
|
|
137
|
+
resumeAuth: (sessionId: string) => Promise<void>;
|
|
128
138
|
/**
|
|
129
139
|
* Call a tool from a session
|
|
130
140
|
*/
|
package/dist/client/react.js
CHANGED
|
@@ -8,62 +8,27 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
8
8
|
var __defProp = Object.defineProperty;
|
|
9
9
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
10
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
11
|
-
var DEFAULT_REQUEST_TIMEOUT = 6e4;
|
|
12
|
-
var MAX_RECONNECT_ATTEMPTS = 5;
|
|
13
|
-
var BASE_RECONNECT_DELAY = 1e3;
|
|
14
11
|
var SSEClient = class {
|
|
15
12
|
constructor(options) {
|
|
16
13
|
this.options = options;
|
|
17
|
-
__publicField(this, "eventSource", null);
|
|
18
|
-
__publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
|
|
19
14
|
__publicField(this, "resourceCache", /* @__PURE__ */ new Map());
|
|
20
|
-
__publicField(this, "
|
|
21
|
-
__publicField(this, "isManuallyDisconnected", false);
|
|
22
|
-
__publicField(this, "connectionPromise", null);
|
|
23
|
-
__publicField(this, "connectionResolver", null);
|
|
15
|
+
__publicField(this, "connected", false);
|
|
24
16
|
}
|
|
25
|
-
// ============================================
|
|
26
|
-
// Connection Management
|
|
27
|
-
// ============================================
|
|
28
|
-
/**
|
|
29
|
-
* Connect to the SSE endpoint
|
|
30
|
-
*/
|
|
31
17
|
connect() {
|
|
32
|
-
if (this.
|
|
18
|
+
if (this.connected) {
|
|
33
19
|
return;
|
|
34
20
|
}
|
|
35
|
-
this.
|
|
36
|
-
this.options.onStatusChange?.("
|
|
37
|
-
this.
|
|
38
|
-
this.connectionResolver = resolve;
|
|
39
|
-
});
|
|
40
|
-
const url = this.buildUrl();
|
|
41
|
-
this.eventSource = new EventSource(url);
|
|
42
|
-
this.setupEventListeners();
|
|
21
|
+
this.connected = true;
|
|
22
|
+
this.options.onStatusChange?.("connected");
|
|
23
|
+
this.log("RPC mode: post_stream");
|
|
43
24
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Disconnect from the SSE endpoint
|
|
46
|
-
*/
|
|
47
25
|
disconnect() {
|
|
48
|
-
this.
|
|
49
|
-
if (this.eventSource) {
|
|
50
|
-
this.eventSource.close();
|
|
51
|
-
this.eventSource = null;
|
|
52
|
-
}
|
|
53
|
-
this.connectionPromise = null;
|
|
54
|
-
this.connectionResolver = null;
|
|
55
|
-
this.rejectAllPendingRequests(new Error("Connection closed"));
|
|
26
|
+
this.connected = false;
|
|
56
27
|
this.options.onStatusChange?.("disconnected");
|
|
57
28
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Check if connected to the SSE endpoint
|
|
60
|
-
*/
|
|
61
29
|
isConnected() {
|
|
62
|
-
return this.
|
|
30
|
+
return this.connected;
|
|
63
31
|
}
|
|
64
|
-
// ============================================
|
|
65
|
-
// RPC Methods
|
|
66
|
-
// ============================================
|
|
67
32
|
async getSessions() {
|
|
68
33
|
return this.sendRequest("getSessions");
|
|
69
34
|
}
|
|
@@ -99,22 +64,10 @@ var SSEClient = class {
|
|
|
99
64
|
async readResource(sessionId, uri) {
|
|
100
65
|
return this.sendRequest("readResource", { sessionId, uri });
|
|
101
66
|
}
|
|
102
|
-
// ============================================
|
|
103
|
-
// Resource Preloading (for instant UI loading)
|
|
104
|
-
// ============================================
|
|
105
|
-
/**
|
|
106
|
-
* Preload UI resources for tools that have UI metadata.
|
|
107
|
-
* Call this when tools are discovered to enable instant MCP App UI loading.
|
|
108
|
-
*/
|
|
109
67
|
preloadToolUiResources(sessionId, tools) {
|
|
110
68
|
for (const tool of tools) {
|
|
111
69
|
const uri = this.extractUiResourceUri(tool);
|
|
112
|
-
if (!uri) continue;
|
|
113
|
-
if (this.resourceCache.has(uri)) {
|
|
114
|
-
this.log(`Resource already cached: ${uri}`);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
this.log(`Preloading UI resource for tool "${tool.name}": ${uri}`);
|
|
70
|
+
if (!uri || this.resourceCache.has(uri)) continue;
|
|
118
71
|
const promise = this.sendRequest("readResource", { sessionId, uri }).catch((err) => {
|
|
119
72
|
this.log(`Failed to preload resource ${uri}: ${err.message}`, "warn");
|
|
120
73
|
this.resourceCache.delete(uri);
|
|
@@ -123,43 +76,24 @@ var SSEClient = class {
|
|
|
123
76
|
this.resourceCache.set(uri, promise);
|
|
124
77
|
}
|
|
125
78
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Get a preloaded resource from cache, or fetch if not cached.
|
|
128
|
-
*/
|
|
129
79
|
getOrFetchResource(sessionId, uri) {
|
|
130
80
|
const cached = this.resourceCache.get(uri);
|
|
131
|
-
if (cached)
|
|
132
|
-
this.log(`Cache hit for resource: ${uri}`);
|
|
133
|
-
return cached;
|
|
134
|
-
}
|
|
135
|
-
this.log(`Cache miss, fetching resource: ${uri}`);
|
|
81
|
+
if (cached) return cached;
|
|
136
82
|
const promise = this.sendRequest("readResource", { sessionId, uri });
|
|
137
83
|
this.resourceCache.set(uri, promise);
|
|
138
84
|
return promise;
|
|
139
85
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Check if a resource is already cached
|
|
142
|
-
*/
|
|
143
86
|
hasPreloadedResource(uri) {
|
|
144
87
|
return this.resourceCache.has(uri);
|
|
145
88
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Clear the resource cache
|
|
148
|
-
*/
|
|
149
89
|
clearResourceCache() {
|
|
150
90
|
this.resourceCache.clear();
|
|
151
91
|
}
|
|
152
|
-
// ============================================
|
|
153
|
-
// Private: Request Handling
|
|
154
|
-
// ============================================
|
|
155
|
-
/**
|
|
156
|
-
* Send an RPC request and return the response directly from HTTP.
|
|
157
|
-
* This bypasses SSE latency by returning results in the HTTP response body.
|
|
158
|
-
*/
|
|
159
92
|
async sendRequest(method, params) {
|
|
160
|
-
if (this.
|
|
161
|
-
|
|
93
|
+
if (!this.connected) {
|
|
94
|
+
this.connect();
|
|
162
95
|
}
|
|
96
|
+
this.log(`RPC request via post_stream: ${method}`);
|
|
163
97
|
const request = {
|
|
164
98
|
id: `rpc_${nanoid.nanoid(10)}`,
|
|
165
99
|
method,
|
|
@@ -173,103 +107,93 @@ var SSEClient = class {
|
|
|
173
107
|
if (!response.ok) {
|
|
174
108
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
175
109
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
110
|
+
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
111
|
+
if (!contentType.includes("text/event-stream")) {
|
|
112
|
+
const data2 = await response.json();
|
|
113
|
+
return this.parseRpcResponse(data2);
|
|
114
|
+
}
|
|
115
|
+
const data = await this.readRpcResponseFromStream(response);
|
|
116
|
+
return this.parseRpcResponse(data);
|
|
117
|
+
}
|
|
118
|
+
async readRpcResponseFromStream(response) {
|
|
119
|
+
if (!response.body) {
|
|
120
|
+
throw new Error("Streaming response body is missing");
|
|
121
|
+
}
|
|
122
|
+
const reader = response.body.getReader();
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
let buffer = "";
|
|
125
|
+
let rpcResponse = null;
|
|
126
|
+
const dispatchBlock = (block) => {
|
|
127
|
+
const lines = block.split("\n");
|
|
128
|
+
let eventName = "message";
|
|
129
|
+
const dataLines = [];
|
|
130
|
+
for (const rawLine of lines) {
|
|
131
|
+
const line = rawLine.replace(/\r$/, "");
|
|
132
|
+
if (!line || line.startsWith(":")) continue;
|
|
133
|
+
if (line.startsWith("event:")) {
|
|
134
|
+
eventName = line.slice("event:".length).trim();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (line.startsWith("data:")) {
|
|
138
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!dataLines.length) return;
|
|
142
|
+
const payloadText = dataLines.join("\n");
|
|
143
|
+
let payload = payloadText;
|
|
144
|
+
try {
|
|
145
|
+
payload = JSON.parse(payloadText);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
switch (eventName) {
|
|
149
|
+
case "connected":
|
|
150
|
+
this.options.onStatusChange?.("connected");
|
|
151
|
+
break;
|
|
152
|
+
case "connection":
|
|
153
|
+
this.options.onConnectionEvent?.(payload);
|
|
154
|
+
break;
|
|
155
|
+
case "observability":
|
|
156
|
+
this.options.onObservabilityEvent?.(payload);
|
|
157
|
+
break;
|
|
158
|
+
case "rpc-response":
|
|
159
|
+
rpcResponse = payload;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
while (true) {
|
|
164
|
+
const { value, done } = await reader.read();
|
|
165
|
+
if (done) break;
|
|
166
|
+
buffer += decoder.decode(value, { stream: true });
|
|
167
|
+
let separatorMatch = buffer.match(/\r?\n\r?\n/);
|
|
168
|
+
while (separatorMatch && separatorMatch.index !== void 0) {
|
|
169
|
+
const separatorIndex = separatorMatch.index;
|
|
170
|
+
const separatorLength = separatorMatch[0].length;
|
|
171
|
+
const block = buffer.slice(0, separatorIndex);
|
|
172
|
+
buffer = buffer.slice(separatorIndex + separatorLength);
|
|
173
|
+
dispatchBlock(block);
|
|
174
|
+
separatorMatch = buffer.match(/\r?\n\r?\n/);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (buffer.trim()) {
|
|
178
|
+
dispatchBlock(buffer);
|
|
179
|
+
}
|
|
180
|
+
if (!rpcResponse) {
|
|
181
|
+
throw new Error("Missing rpc-response event in streamed RPC result");
|
|
182
|
+
}
|
|
183
|
+
return rpcResponse;
|
|
178
184
|
}
|
|
179
|
-
|
|
180
|
-
* Parse RPC response and handle different response formats
|
|
181
|
-
*/
|
|
182
|
-
parseRpcResponse(data, requestId) {
|
|
185
|
+
parseRpcResponse(data) {
|
|
183
186
|
if ("result" in data) {
|
|
184
187
|
return data.result;
|
|
185
188
|
}
|
|
186
189
|
if ("error" in data && data.error) {
|
|
187
190
|
throw new Error(data.error.message || "Unknown RPC error");
|
|
188
191
|
}
|
|
189
|
-
if ("
|
|
190
|
-
return
|
|
192
|
+
if (data && typeof data === "object" && "id" in data) {
|
|
193
|
+
return void 0;
|
|
191
194
|
}
|
|
192
195
|
throw new Error("Invalid RPC response format");
|
|
193
196
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Wait for RPC response via SSE (legacy fallback)
|
|
196
|
-
*/
|
|
197
|
-
waitForSseResponse(requestId) {
|
|
198
|
-
const timeoutMs = this.options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
199
|
-
return new Promise((resolve, reject) => {
|
|
200
|
-
const timeoutId = setTimeout(() => {
|
|
201
|
-
this.pendingRequests.delete(requestId);
|
|
202
|
-
reject(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
203
|
-
}, timeoutMs);
|
|
204
|
-
this.pendingRequests.set(requestId, {
|
|
205
|
-
resolve,
|
|
206
|
-
reject,
|
|
207
|
-
timeoutId
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Handle RPC response received via SSE (legacy)
|
|
213
|
-
*/
|
|
214
|
-
handleRpcResponse(response) {
|
|
215
|
-
const pending = this.pendingRequests.get(response.id);
|
|
216
|
-
if (!pending) return;
|
|
217
|
-
clearTimeout(pending.timeoutId);
|
|
218
|
-
this.pendingRequests.delete(response.id);
|
|
219
|
-
if (response.error) {
|
|
220
|
-
pending.reject(new Error(response.error.message));
|
|
221
|
-
} else {
|
|
222
|
-
pending.resolve(response.result);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
// ============================================
|
|
226
|
-
// Private: Event Handling
|
|
227
|
-
// ============================================
|
|
228
|
-
setupEventListeners() {
|
|
229
|
-
if (!this.eventSource) return;
|
|
230
|
-
this.eventSource.addEventListener("open", () => {
|
|
231
|
-
this.log("Connected");
|
|
232
|
-
this.reconnectAttempts = 0;
|
|
233
|
-
this.options.onStatusChange?.("connected");
|
|
234
|
-
});
|
|
235
|
-
this.eventSource.addEventListener("connected", () => {
|
|
236
|
-
this.log("Server ready");
|
|
237
|
-
this.connectionResolver?.();
|
|
238
|
-
this.connectionResolver = null;
|
|
239
|
-
});
|
|
240
|
-
this.eventSource.addEventListener("connection", (e) => {
|
|
241
|
-
const event = JSON.parse(e.data);
|
|
242
|
-
this.options.onConnectionEvent?.(event);
|
|
243
|
-
});
|
|
244
|
-
this.eventSource.addEventListener("observability", (e) => {
|
|
245
|
-
const event = JSON.parse(e.data);
|
|
246
|
-
this.options.onObservabilityEvent?.(event);
|
|
247
|
-
});
|
|
248
|
-
this.eventSource.addEventListener("rpc-response", (e) => {
|
|
249
|
-
const response = JSON.parse(e.data);
|
|
250
|
-
this.handleRpcResponse(response);
|
|
251
|
-
});
|
|
252
|
-
this.eventSource.addEventListener("error", () => {
|
|
253
|
-
this.log("Connection error", "error");
|
|
254
|
-
this.options.onStatusChange?.("error");
|
|
255
|
-
this.attemptReconnect();
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
attemptReconnect() {
|
|
259
|
-
if (this.isManuallyDisconnected || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
this.reconnectAttempts++;
|
|
263
|
-
const delay = BASE_RECONNECT_DELAY * this.reconnectAttempts;
|
|
264
|
-
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
265
|
-
setTimeout(() => {
|
|
266
|
-
this.disconnect();
|
|
267
|
-
this.connect();
|
|
268
|
-
}, delay);
|
|
269
|
-
}
|
|
270
|
-
// ============================================
|
|
271
|
-
// Private: Utilities
|
|
272
|
-
// ============================================
|
|
273
197
|
buildUrl() {
|
|
274
198
|
const url = new URL(this.options.url, globalThis.location?.origin);
|
|
275
199
|
url.searchParams.set("identity", this.options.identity);
|
|
@@ -280,20 +204,14 @@ var SSEClient = class {
|
|
|
280
204
|
}
|
|
281
205
|
buildHeaders() {
|
|
282
206
|
const headers = {
|
|
283
|
-
"Content-Type": "application/json"
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
"Accept": "text/event-stream"
|
|
284
209
|
};
|
|
285
210
|
if (this.options.authToken) {
|
|
286
211
|
headers["Authorization"] = `Bearer ${this.options.authToken}`;
|
|
287
212
|
}
|
|
288
213
|
return headers;
|
|
289
214
|
}
|
|
290
|
-
rejectAllPendingRequests(error) {
|
|
291
|
-
for (const [, pending] of this.pendingRequests) {
|
|
292
|
-
clearTimeout(pending.timeoutId);
|
|
293
|
-
pending.reject(error);
|
|
294
|
-
}
|
|
295
|
-
this.pendingRequests.clear();
|
|
296
|
-
}
|
|
297
215
|
extractUiResourceUri(tool) {
|
|
298
216
|
const meta = tool._meta?.ui;
|
|
299
217
|
if (!meta || typeof meta !== "object") return void 0;
|
|
@@ -344,6 +262,7 @@ function useMcp(options) {
|
|
|
344
262
|
} = options;
|
|
345
263
|
const clientRef = react.useRef(null);
|
|
346
264
|
const isMountedRef = react.useRef(true);
|
|
265
|
+
const suppressAuthRedirectSessionsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
347
266
|
const [connections, setConnections] = react.useState([]);
|
|
348
267
|
const [status, setStatus] = react.useState(
|
|
349
268
|
"disconnected"
|
|
@@ -367,7 +286,7 @@ function useMcp(options) {
|
|
|
367
286
|
setStatus(newStatus);
|
|
368
287
|
}
|
|
369
288
|
},
|
|
370
|
-
|
|
289
|
+
debug: options.debug
|
|
371
290
|
};
|
|
372
291
|
const client = new SSEClient(clientOptions);
|
|
373
292
|
clientRef.current = client;
|
|
@@ -384,22 +303,34 @@ function useMcp(options) {
|
|
|
384
303
|
}, [url, identity, authToken, autoConnect, autoInitialize]);
|
|
385
304
|
const updateConnectionsFromEvent = react.useCallback((event) => {
|
|
386
305
|
if (!isMountedRef.current) return;
|
|
306
|
+
const isTransientReconnectState = (state) => state === "INITIALIZING" || state === "VALIDATING" || state === "RECONNECTING" || state === "CONNECTING" || state === "CONNECTED" || state === "DISCOVERING";
|
|
387
307
|
setConnections((prev) => {
|
|
388
308
|
switch (event.type) {
|
|
389
309
|
case "state_changed": {
|
|
390
310
|
const existing = prev.find((c) => c.sessionId === event.sessionId);
|
|
391
311
|
if (existing) {
|
|
312
|
+
const nextState = existing.state === "READY" && isTransientReconnectState(event.state) ? existing.state : event.state;
|
|
392
313
|
return prev.map(
|
|
393
|
-
(c) => c.sessionId === event.sessionId ? {
|
|
314
|
+
(c) => c.sessionId === event.sessionId ? {
|
|
315
|
+
...c,
|
|
316
|
+
state: nextState,
|
|
317
|
+
// update createdAt if present in event, otherwise keep existing
|
|
318
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : c.createdAt
|
|
319
|
+
} : c
|
|
394
320
|
);
|
|
395
321
|
} else {
|
|
322
|
+
if (event.state === "DISCONNECTED") {
|
|
323
|
+
return prev;
|
|
324
|
+
}
|
|
396
325
|
return [
|
|
397
326
|
...prev,
|
|
398
327
|
{
|
|
399
328
|
sessionId: event.sessionId,
|
|
400
329
|
serverId: event.serverId,
|
|
401
330
|
serverName: event.serverName,
|
|
331
|
+
serverUrl: event.serverUrl,
|
|
402
332
|
state: event.state,
|
|
333
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : void 0,
|
|
403
334
|
tools: []
|
|
404
335
|
}
|
|
405
336
|
];
|
|
@@ -416,14 +347,16 @@ function useMcp(options) {
|
|
|
416
347
|
case "auth_required": {
|
|
417
348
|
if (event.authUrl) {
|
|
418
349
|
onLog?.("info", `OAuth required - redirecting to ${event.authUrl}`, { authUrl: event.authUrl });
|
|
419
|
-
if (
|
|
420
|
-
onRedirect
|
|
421
|
-
|
|
422
|
-
window
|
|
350
|
+
if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
|
|
351
|
+
if (onRedirect) {
|
|
352
|
+
onRedirect(event.authUrl);
|
|
353
|
+
} else if (typeof window !== "undefined") {
|
|
354
|
+
window.location.href = event.authUrl;
|
|
355
|
+
}
|
|
423
356
|
}
|
|
424
357
|
}
|
|
425
358
|
return prev.map(
|
|
426
|
-
(c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING" } : c
|
|
359
|
+
(c) => c.sessionId === event.sessionId ? { ...c, state: "AUTHENTICATING", authUrl: event.authUrl } : c
|
|
427
360
|
);
|
|
428
361
|
}
|
|
429
362
|
case "error": {
|
|
@@ -438,7 +371,7 @@ function useMcp(options) {
|
|
|
438
371
|
return prev;
|
|
439
372
|
}
|
|
440
373
|
});
|
|
441
|
-
}, [onLog]);
|
|
374
|
+
}, [onLog, onRedirect]);
|
|
442
375
|
const loadSessions = react.useCallback(async () => {
|
|
443
376
|
if (!clientRef.current) return;
|
|
444
377
|
try {
|
|
@@ -453,7 +386,8 @@ function useMcp(options) {
|
|
|
453
386
|
serverName: s.serverName ?? "Unknown Server",
|
|
454
387
|
serverUrl: s.serverUrl,
|
|
455
388
|
transport: s.transport,
|
|
456
|
-
state: "VALIDATING",
|
|
389
|
+
state: s.active === false ? "AUTHENTICATING" : "VALIDATING",
|
|
390
|
+
createdAt: new Date(s.createdAt),
|
|
457
391
|
tools: []
|
|
458
392
|
}))
|
|
459
393
|
);
|
|
@@ -462,9 +396,15 @@ function useMcp(options) {
|
|
|
462
396
|
sessions.map(async (session) => {
|
|
463
397
|
if (clientRef.current) {
|
|
464
398
|
try {
|
|
399
|
+
if (session.active === false) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
suppressAuthRedirectSessionsRef.current.add(session.sessionId);
|
|
465
403
|
await clientRef.current.restoreSession(session.sessionId);
|
|
466
404
|
} catch (error) {
|
|
467
405
|
console.error(`[useMcp] Failed to validate session ${session.sessionId}:`, error);
|
|
406
|
+
} finally {
|
|
407
|
+
suppressAuthRedirectSessionsRef.current.delete(session.sessionId);
|
|
468
408
|
}
|
|
469
409
|
}
|
|
470
410
|
})
|
|
@@ -512,6 +452,13 @@ function useMcp(options) {
|
|
|
512
452
|
}
|
|
513
453
|
return await clientRef.current.finishAuth(sessionId, code);
|
|
514
454
|
}, []);
|
|
455
|
+
const resumeAuth = react.useCallback(async (sessionId) => {
|
|
456
|
+
if (!clientRef.current) {
|
|
457
|
+
throw new Error("SSE client not initialized");
|
|
458
|
+
}
|
|
459
|
+
suppressAuthRedirectSessionsRef.current.delete(sessionId);
|
|
460
|
+
await clientRef.current.restoreSession(sessionId);
|
|
461
|
+
}, []);
|
|
515
462
|
const callTool = react.useCallback(
|
|
516
463
|
async (sessionId, toolName, toolArgs) => {
|
|
517
464
|
if (!clientRef.current) {
|
|
@@ -590,6 +537,7 @@ function useMcp(options) {
|
|
|
590
537
|
connectSSE,
|
|
591
538
|
disconnectSSE,
|
|
592
539
|
finishAuth,
|
|
540
|
+
resumeAuth,
|
|
593
541
|
callTool,
|
|
594
542
|
listTools,
|
|
595
543
|
listPrompts,
|