@mcp-ts/sdk 2.4.0 → 2.4.1

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 (48) hide show
  1. package/README.md +58 -19
  2. package/dist/adapters/agui-adapter.d.mts +2 -2
  3. package/dist/adapters/agui-adapter.d.ts +2 -2
  4. package/dist/adapters/agui-middleware.d.mts +2 -2
  5. package/dist/adapters/agui-middleware.d.ts +2 -2
  6. package/dist/adapters/ai-adapter.d.mts +2 -2
  7. package/dist/adapters/ai-adapter.d.ts +2 -2
  8. package/dist/adapters/langchain-adapter.d.mts +2 -2
  9. package/dist/adapters/langchain-adapter.d.ts +2 -2
  10. package/dist/adapters/mastra-adapter.d.mts +2 -2
  11. package/dist/adapters/mastra-adapter.d.ts +2 -2
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/react.d.mts +4 -4
  15. package/dist/client/react.d.ts +4 -4
  16. package/dist/client/vue.d.mts +4 -4
  17. package/dist/client/vue.d.ts +4 -4
  18. package/dist/{events-CK3N--3g.d.mts → events-C4m7tK1U.d.mts} +0 -1
  19. package/dist/{events-CK3N--3g.d.ts → events-C4m7tK1U.d.ts} +0 -1
  20. package/dist/{index-CtXvKl8N.d.ts → index-Ch7ouNSa.d.ts} +1 -1
  21. package/dist/{index-ByIjEReo.d.mts → index-L5XoXgsb.d.mts} +1 -1
  22. package/dist/index.d.mts +3 -3
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +71 -20
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +71 -20
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/{multi-session-client-CnvZEGPY.d.ts → multi-session-client-Bwm-efqg.d.ts} +28 -5
  29. package/dist/{multi-session-client-CIMUGF8S.d.mts → multi-session-client-k-9RvWvi.d.mts} +28 -5
  30. package/dist/server/index.d.mts +4 -4
  31. package/dist/server/index.d.ts +4 -4
  32. package/dist/server/index.js +71 -20
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/server/index.mjs +71 -20
  35. package/dist/server/index.mjs.map +1 -1
  36. package/dist/shared/index.d.mts +1 -1
  37. package/dist/shared/index.d.ts +1 -1
  38. package/dist/shared/index.js.map +1 -1
  39. package/dist/shared/index.mjs.map +1 -1
  40. package/migrations/v2.3.4/neon/20260513010000_install_mcp_sessions.sql +69 -0
  41. package/migrations/v2.3.4/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
  42. package/migrations/v2.3.4/supabase/20260330195700_install_mcp_sessions.sql +82 -0
  43. package/migrations/v2.3.4/supabase/20260421010000_add_session_cleanup_cron.sql +32 -0
  44. package/package.json +1 -1
  45. package/src/server/handlers/sse-handler.ts +7 -5
  46. package/src/server/mcp/multi-session-client.ts +46 -5
  47. package/src/server/mcp/oauth-client.ts +60 -10
  48. package/src/shared/events.ts +0 -1
@@ -0,0 +1,69 @@
1
+ -- Create the mcp_sessions table for Neon Postgres.
2
+ -- Run this with an owner/admin role, then grant app access with the least-privilege SQL below.
3
+
4
+ CREATE TABLE IF NOT EXISTS public.mcp_sessions (
5
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ session_id TEXT NOT NULL UNIQUE,
7
+ user_id TEXT NOT NULL,
8
+ server_id TEXT,
9
+ server_name TEXT,
10
+ server_url TEXT NOT NULL,
11
+ transport_type TEXT NOT NULL,
12
+ callback_url TEXT NOT NULL,
13
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
14
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
15
+ expires_at TIMESTAMPTZ NOT NULL,
16
+ active BOOLEAN DEFAULT false,
17
+ headers JSONB,
18
+ client_information JSONB,
19
+ tokens JSONB,
20
+ code_verifier TEXT,
21
+ client_id TEXT
22
+ );
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_mcp_sessions_user_id ON public.mcp_sessions(user_id);
25
+ CREATE INDEX IF NOT EXISTS idx_mcp_sessions_expires_at ON public.mcp_sessions(expires_at);
26
+
27
+ CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()
28
+ RETURNS TRIGGER AS $$
29
+ BEGIN
30
+ NEW.updated_at = now();
31
+ RETURN NEW;
32
+ END;
33
+ $$ LANGUAGE plpgsql;
34
+
35
+ DROP TRIGGER IF EXISTS trg_mcp_sessions_updated_at ON public.mcp_sessions;
36
+
37
+ CREATE TRIGGER trg_mcp_sessions_updated_at
38
+ BEFORE UPDATE ON public.mcp_sessions
39
+ FOR EACH ROW
40
+ EXECUTE FUNCTION public.set_current_timestamp_updated_at();
41
+
42
+ -- Optional production configuration:
43
+ -- Create a dedicated app role and use its credentials in NEON_DATABASE_URL.
44
+ -- Replace neondb and the password before running.
45
+ --
46
+ -- CREATE ROLE mcp_service_role LOGIN PASSWORD 'replace-with-a-strong-password';
47
+ -- GRANT CONNECT ON DATABASE neondb TO mcp_service_role;
48
+ -- GRANT USAGE ON SCHEMA public TO mcp_service_role;
49
+ -- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mcp_sessions TO mcp_service_role;
50
+ -- GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO mcp_service_role;
51
+
52
+ -- Optional RLS configuration:
53
+ -- Uncomment and run this block after creating mcp_service_role if you want
54
+ -- to enforce access through Row Level Security for the dedicated app role.
55
+ --
56
+ -- REVOKE ALL ON public.mcp_sessions FROM PUBLIC;
57
+ -- REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM PUBLIC;
58
+ --
59
+ -- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mcp_sessions TO mcp_service_role;
60
+ -- GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO mcp_service_role;
61
+ --
62
+ -- ALTER TABLE public.mcp_sessions ENABLE ROW LEVEL SECURITY;
63
+ --
64
+ -- CREATE POLICY mcp_service_role_full_access
65
+ -- ON public.mcp_sessions
66
+ -- FOR ALL
67
+ -- TO mcp_service_role
68
+ -- USING (true)
69
+ -- WITH CHECK (true);
@@ -0,0 +1,35 @@
1
+ -- Optional Neon pg_cron cleanup jobs.
2
+ --
3
+ -- Neon pg_cron requires endpoint-level setup before this migration can run:
4
+ -- configure cron.database_name for your compute endpoint, restart the compute,
5
+ -- then install/schedule jobs from the target database.
6
+ --
7
+ -- If pg_cron is not enabled for your Neon project, skip this migration and run
8
+ -- storage.cleanupExpiredSessions() from your application scheduler instead.
9
+
10
+ CREATE EXTENSION IF NOT EXISTS pg_cron;
11
+
12
+ -- Keep reruns idempotent without NOTICE noise.
13
+ SELECT cron.unschedule(jobname)
14
+ FROM cron.job
15
+ WHERE jobname IN (
16
+ 'mcp-cleanup-transient-sessions',
17
+ 'mcp-cleanup-dormant-sessions'
18
+ );
19
+
20
+ -- Stage 1: Short-term Transient Purge (every 5 minutes)
21
+ -- Removes failed connections, abandoned OAuth flows, and other inactive
22
+ -- sessions whose TTL has expired.
23
+ SELECT cron.schedule(
24
+ 'mcp-cleanup-transient-sessions',
25
+ '*/5 * * * *',
26
+ $$DELETE FROM public.mcp_sessions WHERE expires_at < now() AND active IS NOT TRUE;$$
27
+ );
28
+
29
+ -- Stage 2: Long-term Dormancy Eviction (daily at midnight UTC)
30
+ -- Removes active sessions that have not been touched for 30+ days.
31
+ SELECT cron.schedule(
32
+ 'mcp-cleanup-dormant-sessions',
33
+ '0 0 * * *',
34
+ $$DELETE FROM public.mcp_sessions WHERE active = true AND updated_at < now() - interval '30 days';$$
35
+ );
@@ -0,0 +1,82 @@
1
+ -- Create the mcp_sessions table
2
+ CREATE TABLE IF NOT EXISTS public.mcp_sessions (
3
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ session_id TEXT NOT NULL UNIQUE,
5
+ user_id TEXT NOT NULL, -- Will store the application user's ID
6
+ server_id TEXT,
7
+ server_name TEXT,
8
+ server_url TEXT NOT NULL,
9
+ transport_type TEXT NOT NULL,
10
+ callback_url TEXT NOT NULL,
11
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
12
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
13
+ expires_at TIMESTAMPTZ NOT NULL,
14
+ active BOOLEAN DEFAULT false,
15
+ headers JSONB,
16
+ client_information JSONB,
17
+ tokens JSONB,
18
+ code_verifier TEXT,
19
+ client_id TEXT
20
+ );
21
+
22
+ -- Add an index on user_id for faster lookups
23
+ CREATE INDEX IF NOT EXISTS idx_mcp_sessions_user_id ON public.mcp_sessions(user_id);
24
+ -- Add an index on expires_at to speed up the cleanup job
25
+ CREATE INDEX IF NOT EXISTS idx_mcp_sessions_expires_at ON public.mcp_sessions(expires_at);
26
+
27
+ -- Trigger to automatically update the 'updated_at' column
28
+ CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()
29
+ RETURNS TRIGGER AS $$
30
+ BEGIN
31
+ NEW.updated_at = now();
32
+ RETURN NEW;
33
+ END;
34
+ $$ LANGUAGE plpgsql;
35
+
36
+ DROP TRIGGER IF EXISTS trg_mcp_sessions_updated_at ON public.mcp_sessions;
37
+ CREATE TRIGGER trg_mcp_sessions_updated_at
38
+ BEFORE UPDATE ON public.mcp_sessions
39
+ FOR EACH ROW
40
+ EXECUTE FUNCTION public.set_current_timestamp_updated_at();
41
+
42
+ -- Enable Row Level Security (RLS)
43
+ ALTER TABLE public.mcp_sessions ENABLE ROW LEVEL SECURITY;
44
+
45
+ -- Policy 1: Users can read their own sessions
46
+ CREATE POLICY "Users can view their own sessions"
47
+ ON public.mcp_sessions
48
+ FOR SELECT
49
+ TO authenticated
50
+ USING (
51
+ auth.uid()::text = user_id
52
+ );
53
+
54
+ -- Policy 2: Users can insert their own sessions
55
+ CREATE POLICY "Users can insert their own sessions"
56
+ ON public.mcp_sessions
57
+ FOR INSERT
58
+ TO authenticated
59
+ WITH CHECK (
60
+ auth.uid()::text = user_id
61
+ );
62
+
63
+ -- Policy 3: Users can update their own sessions
64
+ CREATE POLICY "Users can update their own sessions"
65
+ ON public.mcp_sessions
66
+ FOR UPDATE
67
+ TO authenticated
68
+ USING (
69
+ auth.uid()::text = user_id
70
+ )
71
+ WITH CHECK (
72
+ auth.uid()::text = user_id
73
+ );
74
+
75
+ -- Policy 4: Users can delete their own sessions
76
+ CREATE POLICY "Users can delete their own sessions"
77
+ ON public.mcp_sessions
78
+ FOR DELETE
79
+ TO authenticated
80
+ USING (
81
+ auth.uid()::text = user_id
82
+ );
@@ -0,0 +1,32 @@
1
+ -- Enable the pg_cron extension (available on all Supabase plans).
2
+ -- This is idempotent and safe to run multiple times.
3
+ CREATE EXTENSION IF NOT EXISTS pg_cron;
4
+
5
+ -- -----------------------------------------------------------------------------
6
+ -- Stage 1: Short-term Transient Purge (every 5 minutes)
7
+ -- -----------------------------------------------------------------------------
8
+ -- Targets sessions that are NOT active (failed connections, abandoned OAuth
9
+ -- flows, mid-flow errors) whose TTL has expired. Active sessions are explicitly
10
+ -- excluded from this sweep to preserve automation credentials.
11
+ --
12
+ -- The idx_mcp_sessions_expires_at index ensures this is a fast indexed scan.
13
+ SELECT cron.schedule(
14
+ 'cleanup-transient-sessions',
15
+ '*/5 * * * *',
16
+ $$DELETE FROM public.mcp_sessions WHERE expires_at < now() AND active IS NOT TRUE;$$
17
+ );
18
+
19
+ -- -----------------------------------------------------------------------------
20
+ -- Stage 2: Long-term Dormancy Eviction (daily at midnight UTC)
21
+ -- -----------------------------------------------------------------------------
22
+ -- Safety net for sessions that were successfully established (active = true)
23
+ -- but have been completely untouched for 30+ days. This prevents "active"
24
+ -- records from persisting indefinitely if they are genuinely abandoned.
25
+ SELECT cron.schedule(
26
+ 'cleanup-dormant-sessions',
27
+ '0 0 * * *',
28
+ $$DELETE FROM public.mcp_sessions WHERE active = true AND updated_at < now() - interval '30 days';$$
29
+ );
30
+
31
+ -- Add a comment on the extension for visibility in Supabase Dashboard
32
+ COMMENT ON EXTENSION pg_cron IS 'Automated Session Lifecycle Management.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-ts/sdk",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -356,8 +356,8 @@ export class SSEConnectionManager {
356
356
  const client = this.clients.get(sessionId);
357
357
 
358
358
  if (client) {
359
+ // clearSession() handles DELETE + local cleanup internally.
359
360
  await client.clearSession();
360
- client.disconnect();
361
361
  this.clients.delete(sessionId);
362
362
  } else {
363
363
  // Handle orphaned sessions (e.g., OAuth flow failed before client was stored)
@@ -606,16 +606,18 @@ export class SSEConnectionManager {
606
606
  /**
607
607
  * Cleanup and close all connections
608
608
  */
609
- dispose(): void {
609
+ async dispose(): Promise<void> {
610
610
  this.isActive = false;
611
611
 
612
612
  if (this.heartbeatTimer) {
613
613
  clearInterval(this.heartbeatTimer);
614
614
  }
615
615
 
616
- for (const client of this.clients.values()) {
617
- client.disconnect();
618
- }
616
+ // Send HTTP DELETE to each Streamable HTTP server before closing, per spec.
617
+ // Run in parallel so shutdown is not serialised across many sessions.
618
+ await Promise.all(
619
+ Array.from(this.clients.values()).map((client) => client.disconnect())
620
+ );
619
621
 
620
622
  this.clients.clear();
621
623
  }
@@ -100,6 +100,9 @@ export class MultiSessionClient {
100
100
  * Connects a single session, with built-in deduplication to prevent race conditions.
101
101
  *
102
102
  * - If a client for this session already exists and is connected, returns immediately.
103
+ * - If the existing client entry is no longer connected (e.g. it was explicitly
104
+ * disconnected), it is evicted so that `establishConnectionWithRetries` creates a
105
+ * fresh transport — preventing "Client already connected" errors from the SDK.
103
106
  * - If a connection attempt for this session is already in-flight (e.g. from a
104
107
  * concurrent call), it joins the existing promise instead of starting a new one.
105
108
  * This is the key concurrency lock — the `connectionPromises` map acts as a
@@ -107,9 +110,19 @@ export class MultiSessionClient {
107
110
  * - On completion (success or failure), the promise is cleaned up from the map.
108
111
  */
109
112
  private async connectSession(session: Session): Promise<void> {
110
- const existingClient = this.clients.find(c => c.getSessionId() === session.sessionId);
111
- if (existingClient?.isConnected()) {
112
- return;
113
+ const existing = this.clients.find(c => c.getSessionId() === session.sessionId);
114
+
115
+ if (existing) {
116
+ if (existing.isConnected()) {
117
+ // Genuinely connected — nothing to do.
118
+ return;
119
+ }
120
+
121
+ // Client entry exists but is no longer connected (explicit disconnect or
122
+ // a prior failed reconnect attempt). Remove it so the fresh connect below
123
+ // starts with a clean slate and the underlying SDK Client doesn't complain
124
+ // about an already-attached transport.
125
+ this.clients = this.clients.filter(c => c !== existing);
113
126
  }
114
127
 
115
128
  // Avoid concurrent connection attempts for the same session
@@ -197,9 +210,33 @@ export class MultiSessionClient {
197
210
  */
198
211
  async connect(): Promise<void> {
199
212
  const sessions = await this.getActiveSessions();
213
+ const activeSessionIds = new Set(sessions.map(s => s.sessionId));
214
+
215
+ // Prune stale clients whose sessions no longer exist in storage.
216
+ // Otherwise a lingering MCPClient with a still-alive SDK Client
217
+ // transport triggers OAuth refreshes on listTools(), and the
218
+ // StorageOAuthClientProvider.state() -> patchCredentials() call
219
+ // fails with a FK violation because the session was deleted.
220
+ this.clients = this.clients.filter(c => activeSessionIds.has(c.getSessionId()));
221
+
200
222
  await this.connectInBatches(sessions);
201
223
  }
202
224
 
225
+ /**
226
+ * Drops all cached `MCPClient` instances and reconnects fresh from storage.
227
+ *
228
+ * Call this when downstream MCP servers have expired their transport sessions
229
+ * (e.g. after a remote server restart) and subsequent tool calls return
230
+ * "Session not found. Reconnect without session header." errors.
231
+ *
232
+ * OAuth tokens are preserved in the storage backend — no re-authentication
233
+ * is required. Only the in-memory transport sessions are cleared.
234
+ */
235
+ async reconnect(): Promise<void> {
236
+ await this.disconnect();
237
+ await this.connect();
238
+ }
239
+
203
240
  /**
204
241
  * Returns all currently connected `MCPClient` instances.
205
242
  *
@@ -213,11 +250,15 @@ export class MultiSessionClient {
213
250
  /**
214
251
  * Gracefully disconnects all active MCP clients and clears the internal client list.
215
252
  *
253
+ * For Streamable HTTP sessions, each client sends an HTTP DELETE to its MCP
254
+ * endpoint per the spec before closing locally. All disconnects run in
255
+ * parallel so shutdown is not serialised across many sessions.
256
+ *
216
257
  * Call this during server shutdown or when a user logs out to free up
217
258
  * underlying transport resources (SSE streams, HTTP connections, etc.).
218
259
  */
219
- disconnect(): void {
220
- this.clients.forEach((client) => client.disconnect());
260
+ async disconnect(): Promise<void> {
261
+ await Promise.all(this.clients.map((client) => client.disconnect()));
221
262
  this.clients = [];
222
263
  }
223
264
  }
@@ -242,7 +242,7 @@ export class MCPClient {
242
242
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
243
243
  * This wrapper enforces a timeout and properly uses AbortController to unblock the request.
244
244
  */
245
- fetch: (url: RequestInfo | URL, init?: RequestInit) => {
245
+ fetch: async (url: RequestInfo | URL, init?: RequestInit) => {
246
246
  const timeout = 30000;
247
247
  const controller = new AbortController();
248
248
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -251,7 +251,20 @@ export class MCPClient {
251
251
  (AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal) :
252
252
  controller.signal;
253
253
 
254
- return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
254
+ try {
255
+ const response = await fetch(url, { ...init, signal });
256
+
257
+ const hasSessionHeader = init?.headers && new Headers(init.headers as HeadersInit).has('mcp-session-id');
258
+
259
+ if (response.status === 404 && hasSessionHeader) {
260
+ this.client = null;
261
+ throw new Error("MCP_SESSION_EXPIRED: Downstream session was not found on the server.");
262
+ }
263
+
264
+ return response;
265
+ } finally {
266
+ clearTimeout(timeoutId);
267
+ }
255
268
  }
256
269
  };
257
270
 
@@ -497,6 +510,27 @@ export class MCPClient {
497
510
  * @throws {Error} When connection fails for other reasons
498
511
  */
499
512
  async connect(): Promise<void> {
513
+ // Re-entry guard: if a previous SDK Client is still attached to a transport
514
+ // (e.g. a stale session from a prior connect() call on the same MCPClient
515
+ // instance), close and detach it before proceeding. The MCP SDK client will
516
+ // throw if asked to connect while a transport is already attached, and the
517
+ // stale transport would send an expired mcp-session-id to the remote server
518
+ // causing "Session not found. Reconnect without session header." errors.
519
+ //
520
+ // We also null out this.client so that initialize() creates a fresh Client
521
+ // instance with a clean transport slot, rather than reusing the old one.
522
+ // The oauthProvider is intentionally preserved — OAuth tokens remain valid
523
+ // across reconnects; only the transport session needs to be renegotiated.
524
+ if (this.client?.transport) {
525
+ this.transport = null;
526
+ try {
527
+ await this.client.close();
528
+ } catch {
529
+ // Closing a transport that may have already failed is best-effort.
530
+ }
531
+ this.client = null;
532
+ }
533
+
500
534
  await this.initialize();
501
535
 
502
536
  if (!this.client || !this.oauthProvider) {
@@ -997,7 +1031,7 @@ export class MCPClient {
997
1031
  }
998
1032
 
999
1033
  await sessions.delete(this.userId, this.sessionId);
1000
- this.disconnect();
1034
+ await this.disconnect();
1001
1035
  }
1002
1036
 
1003
1037
  /**
@@ -1009,10 +1043,29 @@ export class MCPClient {
1009
1043
  }
1010
1044
 
1011
1045
  /**
1012
- * Disconnects from the MCP server and cleans up resources
1013
- * Does not remove session from Redis - use clearSession() for that
1046
+ * Disconnects from the MCP server and cleans up resources.
1047
+ * Does not remove session from Redis use clearSession() for that.
1048
+ *
1049
+ * For Streamable HTTP sessions, sends an HTTP DELETE to the MCP endpoint
1050
+ * before closing, as recommended by the MCP Streamable HTTP spec
1051
+ * (section "Session Management", rule 5). This is best-effort — errors
1052
+ * (e.g. server already restarted, 404/405 responses) are silently ignored.
1014
1053
  */
1015
- disconnect(reason?: string): void {
1054
+ async disconnect(): Promise<void> {
1055
+ // Per the MCP Streamable HTTP spec (2025-11-25), clients SHOULD send an
1056
+ // HTTP DELETE with the mcp-session-id header when they no longer need a
1057
+ // session. The server MAY respond with 405 if it doesn't support explicit
1058
+ // termination — terminateSession() handles that gracefully.
1059
+ // SSEClientTransport has no session concept, so we guard with instanceof.
1060
+ if (this.transport instanceof StreamableHTTPClientTransport) {
1061
+ try {
1062
+ await this.transport.terminateSession();
1063
+ } catch {
1064
+ // Best-effort: server may be unreachable or may have already expired
1065
+ // the session. Either way, we proceed with local cleanup.
1066
+ }
1067
+ }
1068
+
1016
1069
  if (this.client) {
1017
1070
  this.client.close();
1018
1071
  }
@@ -1026,7 +1079,6 @@ export class MCPClient {
1026
1079
  type: 'disconnected',
1027
1080
  sessionId: this.sessionId,
1028
1081
  serverId: this.serverId,
1029
- reason,
1030
1082
  timestamp: Date.now(),
1031
1083
  });
1032
1084
 
@@ -1036,9 +1088,7 @@ export class MCPClient {
1036
1088
  message: `Disconnected from ${this.serverId}`,
1037
1089
  sessionId: this.sessionId,
1038
1090
  serverId: this.serverId,
1039
- payload: {
1040
- reason: reason || 'unknown',
1041
- },
1091
+ payload: {},
1042
1092
  timestamp: Date.now(),
1043
1093
  id: nanoid(),
1044
1094
  });
@@ -121,7 +121,6 @@ export type McpConnectionEvent =
121
121
  type: 'disconnected';
122
122
  sessionId: string;
123
123
  serverId: string;
124
- reason?: string;
125
124
  timestamp: number;
126
125
  }
127
126
  | {