@mcp-ts/sdk 1.3.2 → 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.
Files changed (58) hide show
  1. package/README.md +405 -406
  2. package/dist/adapters/agui-adapter.d.mts +1 -1
  3. package/dist/adapters/agui-adapter.d.ts +1 -1
  4. package/dist/adapters/agui-middleware.d.mts +1 -1
  5. package/dist/adapters/agui-middleware.d.ts +1 -1
  6. package/dist/adapters/ai-adapter.d.mts +1 -1
  7. package/dist/adapters/ai-adapter.d.ts +1 -1
  8. package/dist/adapters/langchain-adapter.d.mts +1 -1
  9. package/dist/adapters/langchain-adapter.d.ts +1 -1
  10. package/dist/adapters/mastra-adapter.d.mts +1 -1
  11. package/dist/adapters/mastra-adapter.d.ts +1 -1
  12. package/dist/client/index.d.mts +8 -64
  13. package/dist/client/index.d.ts +8 -64
  14. package/dist/client/index.js +91 -173
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +91 -173
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +12 -2
  19. package/dist/client/react.d.ts +12 -2
  20. package/dist/client/react.js +119 -182
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +119 -182
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +24 -4
  25. package/dist/client/vue.d.ts +24 -4
  26. package/dist/client/vue.js +121 -182
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +121 -182
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +215 -250
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +215 -250
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/{multi-session-client-B1DBx5yR.d.mts → multi-session-client-DzjmT7FX.d.mts} +1 -0
  37. package/dist/{multi-session-client-DyFzyJUx.d.ts → multi-session-client-FAFpUzZ4.d.ts} +1 -0
  38. package/dist/server/index.d.mts +16 -21
  39. package/dist/server/index.d.ts +16 -21
  40. package/dist/server/index.js +124 -77
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/server/index.mjs +124 -77
  43. package/dist/server/index.mjs.map +1 -1
  44. package/dist/shared/index.d.mts +2 -2
  45. package/dist/shared/index.d.ts +2 -2
  46. package/dist/shared/index.js.map +1 -1
  47. package/dist/shared/index.mjs.map +1 -1
  48. package/dist/{types-PjM1W07s.d.mts → types-CW6lghof.d.mts} +5 -0
  49. package/dist/{types-PjM1W07s.d.ts → types-CW6lghof.d.ts} +5 -0
  50. package/package.json +1 -1
  51. package/src/client/core/sse-client.ts +354 -493
  52. package/src/client/react/use-mcp.ts +75 -23
  53. package/src/client/vue/use-mcp.ts +111 -48
  54. package/src/server/handlers/nextjs-handler.ts +207 -217
  55. package/src/server/handlers/sse-handler.ts +10 -0
  56. package/src/server/mcp/oauth-client.ts +41 -32
  57. package/src/server/storage/types.ts +12 -5
  58. package/src/shared/types.ts +5 -0
@@ -1,217 +1,207 @@
1
- /**
2
- * Next.js App Router Handler for MCP SSE
3
- * Provides a clean, zero-boilerplate API for Next.js applications
4
- */
5
-
6
- import { SSEConnectionManager, type ClientMetadata } from './sse-handler.js';
7
- import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events.js';
8
- import type { McpRpcResponse } from '../../shared/types.js';
9
-
10
- export interface NextMcpHandlerOptions {
11
- /**
12
- * Extract identity from request (default: from 'identity' query param)
13
- */
14
- getIdentity?: (request: Request) => string | null;
15
-
16
- /**
17
- * Extract auth token from request (default: from 'token' query param or Authorization header)
18
- */
19
- getAuthToken?: (request: Request) => string | null;
20
-
21
- /**
22
- * Authenticate user and verify access (optional)
23
- * Return true if user is authenticated, false otherwise
24
- */
25
- authenticate?: (identity: string, token: string | null) => Promise<boolean> | boolean;
26
-
27
- /**
28
- * Heartbeat interval in milliseconds (default: 30000)
29
- */
30
- heartbeatInterval?: number;
31
-
32
- /**
33
- * Static OAuth client metadata defaults (for all connections)
34
- * Use this for single-tenant applications with fixed branding
35
- */
36
- clientDefaults?: ClientMetadata;
37
-
38
- /**
39
- * Dynamic OAuth client metadata getter (per-request, useful for multi-tenant)
40
- * Use this when you need different branding based on request (tenant, domain, etc.)
41
- * Takes precedence over clientDefaults
42
- */
43
- getClientMetadata?: (request: Request) => ClientMetadata | Promise<ClientMetadata>;
44
- }
45
-
46
- // Global manager store - shared across requests for the same user
47
- const managers = new Map<string, SSEConnectionManager>();
48
-
49
- /**
50
- * Creates Next.js App Router handlers (GET and POST) for MCP SSE endpoint
51
- *
52
- * @example
53
- * ```typescript
54
- * // app/api/mcp/route.ts
55
- * import { createNextMcpHandler } from '@mcp-ts/core/server';
56
- *
57
- * export const { GET, POST } = createNextMcpHandler();
58
- * ```
59
- */
60
- export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
61
- const {
62
- getIdentity = (request: Request) => new URL(request.url).searchParams.get('identity'),
63
- getAuthToken = (request: Request) => {
64
- const url = new URL(request.url);
65
- return url.searchParams.get('token') || request.headers.get('authorization');
66
- },
67
- authenticate = () => true,
68
- heartbeatInterval = 30000,
69
- clientDefaults,
70
- getClientMetadata,
71
- } = options;
72
-
73
- /**
74
- * GET handler - Establishes SSE connection
75
- */
76
- async function GET(request: Request): Promise<Response> {
77
- const identity = getIdentity(request);
78
- const authToken = getAuthToken(request);
79
-
80
- if (!identity) {
81
- return new Response('Missing identity', { status: 400 });
82
- }
83
-
84
- // Validate auth
85
- const isAuthorized = await authenticate(identity, authToken);
86
- if (!isAuthorized) {
87
- return new Response('Unauthorized', { status: 401 });
88
- }
89
-
90
- // Create TransformStream for SSE
91
- const stream = new TransformStream();
92
- const writer = stream.writable.getWriter();
93
- const encoder = new TextEncoder();
94
-
95
- // Helper to send SSE events
96
- const sendSSE = (event: string, data: any) => {
97
- const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
98
- writer.write(encoder.encode(message)).catch(() => {
99
- // Client disconnected, ignore write errors
100
- });
101
- };
102
-
103
- // Clean up previous manager if exists (prevents memory leaks on reconnect)
104
- const previousManager = managers.get(identity);
105
- if (previousManager) {
106
- previousManager.dispose();
107
- }
108
-
109
- // Resolve client metadata (dynamic takes precedence over static)
110
- const resolvedClientMetadata = getClientMetadata
111
- ? await getClientMetadata(request)
112
- : clientDefaults;
113
-
114
- // Create new manager
115
- const manager = new SSEConnectionManager(
116
- {
117
- identity,
118
- heartbeatInterval,
119
- clientDefaults: resolvedClientMetadata, // Pass resolved metadata
120
- },
121
- (event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => {
122
- // Determine event type and send via SSE
123
- if ('id' in event) {
124
- // RPC response
125
- sendSSE('rpc-response', event);
126
- } else if ('type' in event && 'sessionId' in event) {
127
- // Connection event
128
- sendSSE('connection', event);
129
- } else {
130
- // Observability event
131
- sendSSE('observability', event);
132
- }
133
- }
134
- );
135
-
136
- managers.set(identity, manager);
137
-
138
- // Send connected event AFTER manager is registered (prevents race condition
139
- // where client sends POST before manager is available)
140
- sendSSE('connected', { timestamp: Date.now() });
141
-
142
- // Handle client disconnect
143
- const abortController = new AbortController();
144
- request.signal?.addEventListener('abort', () => {
145
- manager.dispose();
146
- managers.delete(identity);
147
- writer.close().catch(() => { });
148
- abortController.abort();
149
- });
150
-
151
- // Return SSE response
152
- return new Response(stream.readable, {
153
- status: 200,
154
- headers: {
155
- 'Content-Type': 'text/event-stream',
156
- 'Cache-Control': 'no-cache, no-transform',
157
- 'Connection': 'keep-alive',
158
- 'X-Accel-Buffering': 'no',
159
- },
160
- });
161
- }
162
-
163
- /**
164
- * POST handler - Handles RPC requests
165
- */
166
- async function POST(request: Request): Promise<Response> {
167
- const identity = getIdentity(request);
168
- const authToken = getAuthToken(request);
169
-
170
- if (!identity) {
171
- return Response.json({ error: { code: 'MISSING_IDENTITY', message: 'Missing identity' } }, { status: 400 });
172
- }
173
-
174
- // Validate auth
175
- const isAuthorized = await authenticate(identity, authToken);
176
- if (!isAuthorized) {
177
- return Response.json({ error: { code: 'UNAUTHORIZED', message: 'Unauthorized' } }, { status: 401 });
178
- }
179
-
180
- try {
181
- const body = await request.json();
182
-
183
- // Get existing manager (created by GET endpoint)
184
- const manager = managers.get(identity);
185
-
186
- if (!manager) {
187
- return Response.json(
188
- {
189
- error: {
190
- code: 'NO_CONNECTION',
191
- message: 'No SSE connection found. Please establish SSE connection first.',
192
- },
193
- },
194
- { status: 400 }
195
- );
196
- }
197
-
198
- // Handle the request and return response directly (bypasses SSE latency)
199
- const response = await manager.handleRequest(body);
200
-
201
- // Return the actual RPC response for immediate use by client
202
- return Response.json(response);
203
- } catch (error) {
204
- return Response.json(
205
- {
206
- error: {
207
- code: 'EXECUTION_ERROR',
208
- message: error instanceof Error ? error.message : 'Unknown error',
209
- },
210
- },
211
- { status: 500 }
212
- );
213
- }
214
- }
215
-
216
- return { GET, POST };
217
- }
1
+ /**
2
+ * Next.js App Router Handler for MCP
3
+ * Stateless transport for serverless environments:
4
+ * - POST + `Accept: text/event-stream` streams progress + rpc-response
5
+ * - POST + JSON accepts direct RPC result response
6
+ */
7
+
8
+ import { SSEConnectionManager, type ClientMetadata } from './sse-handler.js';
9
+ import type { McpConnectionEvent, McpObservabilityEvent } from '../../shared/events.js';
10
+ import type { McpRpcResponse } from '../../shared/types.js';
11
+
12
+ function isRpcResponseEvent(event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse): event is McpRpcResponse {
13
+ return 'id' in event && ('result' in event || 'error' in event);
14
+ }
15
+
16
+ export interface NextMcpHandlerOptions {
17
+ /**
18
+ * Extract identity from request (default: from 'identity' query param)
19
+ */
20
+ getIdentity?: (request: Request) => string | null;
21
+
22
+ /**
23
+ * Extract auth token from request (default: from 'token' query param or Authorization header)
24
+ */
25
+ getAuthToken?: (request: Request) => string | null;
26
+
27
+ /**
28
+ * Authenticate user and verify access (optional)
29
+ * Return true if user is authenticated, false otherwise
30
+ */
31
+ authenticate?: (identity: string, token: string | null) => Promise<boolean> | boolean;
32
+
33
+ /**
34
+ * Heartbeat interval in milliseconds (default: 30000)
35
+ */
36
+ heartbeatInterval?: number;
37
+
38
+ /**
39
+ * Static OAuth client metadata defaults (for all connections)
40
+ */
41
+ clientDefaults?: ClientMetadata;
42
+
43
+ /**
44
+ * Dynamic OAuth client metadata getter (per-request)
45
+ */
46
+ getClientMetadata?: (request: Request) => ClientMetadata | Promise<ClientMetadata>;
47
+ }
48
+
49
+ export function createNextMcpHandler(options: NextMcpHandlerOptions = {}) {
50
+ const {
51
+ getIdentity = (request: Request) => new URL(request.url).searchParams.get('identity'),
52
+ getAuthToken = (request: Request) => {
53
+ const url = new URL(request.url);
54
+ return url.searchParams.get('token') || request.headers.get('authorization');
55
+ },
56
+ authenticate = () => true,
57
+ heartbeatInterval = 30000,
58
+ clientDefaults,
59
+ getClientMetadata,
60
+ } = options;
61
+
62
+ const toManagerOptions = (identity: string, resolvedClientMetadata?: ClientMetadata) => ({
63
+ identity,
64
+ heartbeatInterval,
65
+ clientDefaults: resolvedClientMetadata,
66
+ });
67
+
68
+ async function resolveClientMetadata(request: Request): Promise<ClientMetadata | undefined> {
69
+ return getClientMetadata ? await getClientMetadata(request) : clientDefaults;
70
+ }
71
+
72
+ async function GET(): Promise<Response> {
73
+ return Response.json(
74
+ {
75
+ error: {
76
+ code: 'METHOD_NOT_ALLOWED',
77
+ message: 'Use POST /api/mcp. For streaming use Accept: text/event-stream.',
78
+ },
79
+ },
80
+ { status: 405 }
81
+ );
82
+ }
83
+
84
+ async function POST(request: Request): Promise<Response> {
85
+ const identity = getIdentity(request);
86
+ const authToken = getAuthToken(request);
87
+ const acceptsEventStream = (request.headers.get('accept') || '').toLowerCase().includes('text/event-stream');
88
+
89
+ if (!identity) {
90
+ return Response.json({ error: { code: 'MISSING_IDENTITY', message: 'Missing identity' } }, { status: 400 });
91
+ }
92
+
93
+ const isAuthorized = await authenticate(identity, authToken);
94
+ if (!isAuthorized) {
95
+ return Response.json({ error: { code: 'UNAUTHORIZED', message: 'Unauthorized' } }, { status: 401 });
96
+ }
97
+
98
+ let rawBody = '';
99
+ try {
100
+ rawBody = await request.text();
101
+ const body = rawBody ? JSON.parse(rawBody) : null;
102
+
103
+ if (!body || typeof body !== 'object') {
104
+ return Response.json(
105
+ {
106
+ error: {
107
+ code: 'INVALID_REQUEST',
108
+ message: 'Invalid JSON-RPC request body',
109
+ },
110
+ },
111
+ { status: 400 }
112
+ );
113
+ }
114
+
115
+ const resolvedClientMetadata = await resolveClientMetadata(request);
116
+
117
+ if (!acceptsEventStream) {
118
+ const manager = new SSEConnectionManager(
119
+ toManagerOptions(identity, resolvedClientMetadata),
120
+ () => { }
121
+ );
122
+ try {
123
+ const response = await manager.handleRequest(body as any);
124
+ return Response.json(response);
125
+ } finally {
126
+ manager.dispose();
127
+ }
128
+ }
129
+
130
+ const stream = new TransformStream();
131
+ const writer = stream.writable.getWriter();
132
+ const encoder = new TextEncoder();
133
+ let streamWritable = true;
134
+
135
+ const sendSSE = (event: string, data: unknown) => {
136
+ if (!streamWritable) return;
137
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
138
+ writer.write(encoder.encode(message)).catch(() => {
139
+ streamWritable = false;
140
+ });
141
+ };
142
+
143
+ const manager = new SSEConnectionManager(
144
+ toManagerOptions(identity, resolvedClientMetadata),
145
+ (event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse) => {
146
+ if (isRpcResponseEvent(event)) {
147
+ sendSSE('rpc-response', event);
148
+ } else if ('type' in event && 'sessionId' in event) {
149
+ sendSSE('connection', event);
150
+ } else {
151
+ sendSSE('observability', event);
152
+ }
153
+ }
154
+ );
155
+
156
+ sendSSE('connected', { timestamp: Date.now() });
157
+
158
+ void (async () => {
159
+ try {
160
+ await manager.handleRequest(body as any);
161
+ } catch (error) {
162
+ const err = error instanceof Error ? error : new Error('Unknown error');
163
+ sendSSE('rpc-response', {
164
+ id: (body as any).id || 'unknown',
165
+ error: {
166
+ code: 'EXECUTION_ERROR',
167
+ message: err.message,
168
+ },
169
+ } satisfies McpRpcResponse);
170
+ } finally {
171
+ streamWritable = false;
172
+ manager.dispose();
173
+ writer.close().catch(() => { });
174
+ }
175
+ })();
176
+
177
+ return new Response(stream.readable, {
178
+ status: 200,
179
+ headers: {
180
+ 'Content-Type': 'text/event-stream',
181
+ 'Cache-Control': 'no-cache, no-transform',
182
+ 'Connection': 'keep-alive',
183
+ 'X-Accel-Buffering': 'no',
184
+ },
185
+ });
186
+ } catch (error) {
187
+ const err = error instanceof Error ? error : new Error('Unknown error');
188
+ console.error('[MCP Next Handler] Failed to handle RPC', {
189
+ identity,
190
+ message: err.message,
191
+ stack: err.stack,
192
+ rawBody: rawBody.slice(0, 500),
193
+ });
194
+ return Response.json(
195
+ {
196
+ error: {
197
+ code: 'EXECUTION_ERROR',
198
+ message: err.message,
199
+ },
200
+ },
201
+ { status: 500 }
202
+ );
203
+ }
204
+ }
205
+
206
+ return { GET, POST };
207
+ }
@@ -226,6 +226,7 @@ export class SSEConnectionManager {
226
226
  serverUrl: s.serverUrl,
227
227
  transport: s.transportType,
228
228
  createdAt: s.createdAt,
229
+ active: s.active !== false,
229
230
  })),
230
231
  };
231
232
  }
@@ -249,6 +250,15 @@ export class SSEConnectionManager {
249
250
  );
250
251
 
251
252
  if (duplicate) {
253
+ // If the existing session is still pending OAuth, treat connect as "resume auth"
254
+ // instead of failing with duplicate connection error.
255
+ if (duplicate.active === false) {
256
+ await this.restoreSession({ sessionId: duplicate.sessionId });
257
+ return {
258
+ sessionId: duplicate.sessionId,
259
+ success: true,
260
+ };
261
+ }
252
262
  throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
253
263
  }
254
264
 
@@ -363,29 +363,34 @@ export class MCPClient {
363
363
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
364
364
  this.createdAt = Date.now();
365
365
  console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
366
- await storage.createSession({
367
- sessionId: this.sessionId,
368
- identity: this.identity,
369
- serverId: this.serverId,
370
- serverName: this.serverName,
371
- serverUrl: this.serverUrl,
372
- callbackUrl: this.callbackUrl,
373
- transportType: this.transportType || 'streamable_http',
374
- createdAt: this.createdAt,
375
- }, Math.floor(STATE_EXPIRATION_MS / 1000)); // Short TTL until connection succeeds
376
- }
377
- }
366
+ await storage.createSession({
367
+ sessionId: this.sessionId,
368
+ identity: this.identity,
369
+ serverId: this.serverId,
370
+ serverName: this.serverName,
371
+ serverUrl: this.serverUrl,
372
+ callbackUrl: this.callbackUrl,
373
+ transportType: this.transportType || 'streamable_http',
374
+ createdAt: this.createdAt,
375
+ active: false,
376
+ }, Math.floor(STATE_EXPIRATION_MS / 1000)); // Short TTL until connection succeeds
377
+ }
378
+ }
378
379
 
379
380
  /**
380
381
  * Saves current session state to storage
381
382
  * Creates new session if it doesn't exist, updates if it does
382
- * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
383
- * @private
384
- */
385
- private async saveSession(ttl: number = SESSION_TTL_SECONDS): Promise<void> {
386
- if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
387
- return;
388
- }
383
+ * @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
384
+ * @param active - Session status marker used to avoid unnecessary TTL rewrites
385
+ * @private
386
+ */
387
+ private async saveSession(
388
+ ttl: number = SESSION_TTL_SECONDS,
389
+ active: boolean = true
390
+ ): Promise<void> {
391
+ if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
392
+ return;
393
+ }
389
394
 
390
395
  const sessionData = {
391
396
  sessionId: this.sessionId,
@@ -393,10 +398,11 @@ export class MCPClient {
393
398
  serverId: this.serverId,
394
399
  serverName: this.serverName,
395
400
  serverUrl: this.serverUrl,
396
- callbackUrl: this.callbackUrl,
397
- transportType: (this.transportType || 'streamable_http') as TransportType,
398
- createdAt: this.createdAt || Date.now(),
399
- };
401
+ callbackUrl: this.callbackUrl,
402
+ transportType: (this.transportType || 'streamable_http') as TransportType,
403
+ createdAt: this.createdAt || Date.now(),
404
+ active,
405
+ };
400
406
 
401
407
  // Try to update first, create if doesn't exist
402
408
  const existingSession = await storage.getSession(this.identity, this.sessionId);
@@ -506,13 +512,16 @@ export class MCPClient {
506
512
  this.emitStateChange('CONNECTED');
507
513
  this.emitProgress('Connected successfully');
508
514
 
509
- // Only save/update session if transport type changed (connection negotiation)
510
- // This avoids unnecessary writes to storage on every connect
511
- const existingSession = await storage.getSession(this.identity, this.sessionId);
512
- if (!existingSession || existingSession.transportType !== this.transportType) {
513
- console.log(`[MCPClient] Saving session ${this.sessionId} (new or transport changed)`);
514
- await this.saveSession(SESSION_TTL_SECONDS);
515
- }
515
+ // Promote short-lived OAuth-pending session TTL to long-lived active TTL once.
516
+ // Also persist when transport negotiation changed the effective transport.
517
+ const existingSession = await storage.getSession(this.identity, this.sessionId);
518
+ const needsTransportUpdate = !existingSession || existingSession.transportType !== this.transportType;
519
+ const needsTtlPromotion = !existingSession || existingSession.active !== true;
520
+
521
+ if (needsTransportUpdate || needsTtlPromotion) {
522
+ console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
523
+ await this.saveSession(SESSION_TTL_SECONDS, true);
524
+ }
516
525
  } catch (error) {
517
526
  /** Handle Authentication Errors */
518
527
  if (
@@ -522,7 +531,7 @@ export class MCPClient {
522
531
  this.emitStateChange('AUTHENTICATING');
523
532
  // Save session with 10min TTL for OAuth pending state
524
533
  console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
525
- await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000));
534
+ await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
526
535
 
527
536
  /** Get OAuth authorization URL if available */
528
537
  let authUrl = '';
@@ -633,7 +642,7 @@ export class MCPClient {
633
642
  this.emitStateChange('CONNECTED');
634
643
  // Update session with 12hr TTL after successful OAuth
635
644
  console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
636
- await this.saveSession(SESSION_TTL_SECONDS);
645
+ await this.saveSession(SESSION_TTL_SECONDS, true);
637
646
 
638
647
  return; // Success, exit function
639
648
 
@@ -5,8 +5,8 @@ import type {
5
5
  OAuthClientInformationMixed,
6
6
  } from '@modelcontextprotocol/sdk/shared/auth.js';
7
7
 
8
- export interface SessionData {
9
- sessionId: string;
8
+ export interface SessionData {
9
+ sessionId: string;
10
10
  serverId?: string; // Database server ID for mapping
11
11
  serverName?: string;
12
12
  serverUrl: string;
@@ -14,9 +14,16 @@ export interface SessionData {
14
14
  callbackUrl: string;
15
15
  createdAt: number;
16
16
  identity: string;
17
- headers?: Record<string, string>;
18
- // OAuth data (consolidated)
19
- clientInformation?: OAuthClientInformationMixed;
17
+ headers?: Record<string, string>;
18
+ /**
19
+ * Session status marker used for TTL transitions:
20
+ * - false: short-lived intermediate/error/auth-pending session state
21
+ * (keep this value when connection/auth is incomplete or failed)
22
+ * - true: active long-lived session state after successful connection/auth completion
23
+ */
24
+ active?: boolean;
25
+ // OAuth data (consolidated)
26
+ clientInformation?: OAuthClientInformationMixed;
20
27
  tokens?: OAuthTokens;
21
28
  codeVerifier?: string;
22
29
  clientId?: string;
@@ -224,6 +224,11 @@ export interface SessionInfo {
224
224
  serverUrl: string;
225
225
  transport: TransportType;
226
226
  createdAt: number;
227
+ /**
228
+ * Session readiness for auto-restore.
229
+ * false means auth is pending and should be resumed explicitly by user action.
230
+ */
231
+ active?: boolean;
227
232
  }
228
233
 
229
234
  export interface SessionListResult {