diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
--- a/dist/cjs/client/index.d.ts
+++ b/dist/cjs/client/index.d.ts
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
      *
      * For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
      */
+    callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
+    callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
     callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
         [x: string]: unknown;
         content: ({
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
--- a/dist/esm/client/index.d.ts
+++ b/dist/esm/client/index.d.ts
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
      *
      * For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
      */
+    callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
+    callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
     callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
         [x: string]: unknown;
         content: ({
diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644
--- a/dist/cjs/client/index.js
+++ b/dist/cjs/client/index.js
@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
     }
     async connect(transport, options) {
         await super.connect(transport);
+        transport.onsessionexpired = async () => {
+            await this._initialize(transport);
+        };
         // When transport sessionId is already set this means we are trying to reconnect.
         // In this case we don't need to initialize again.
         if (transport.sessionId !== undefined) {
             return;
         }
         try {
-            const result = await this.request({
-                method: 'initialize',
-                params: {
-                    protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
-                    capabilities: this._capabilities,
-                    clientInfo: this._clientInfo
-                }
-            }, types_js_1.InitializeResultSchema, options);
-            if (result === undefined) {
-                throw new Error(`Server sent invalid initialize result: ${result}`);
-            }
-            if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
-                throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
-            }
-            this._serverCapabilities = result.capabilities;
-            this._serverVersion = result.serverInfo;
-            // HTTP transports must set the protocol version in each header after initialization.
-            if (transport.setProtocolVersion) {
-                transport.setProtocolVersion(result.protocolVersion);
-            }
-            this._instructions = result.instructions;
-            await this.notification({
-                method: 'notifications/initialized'
-            });
-            // Set up list changed handlers now that we know server capabilities
-            if (this._pendingListChangedConfig) {
-                this._setupListChangedHandlers(this._pendingListChangedConfig);
-                this._pendingListChangedConfig = undefined;
-            }
+            await this._initialize(transport, options);
         }
         catch (error) {
             // Disconnect if initialization fails.
@@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol {
             throw error;
         }
     }
+    async _initialize(transport, options) {
+        const result = await this.request({
+            method: 'initialize',
+            params: {
+                protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
+                capabilities: this._capabilities,
+                clientInfo: this._clientInfo
+            }
+        }, types_js_1.InitializeResultSchema, options);
+        if (result === undefined) {
+            throw new Error(`Server sent invalid initialize result: ${result}`);
+        }
+        if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
+            throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
+        }
+        this._serverCapabilities = result.capabilities;
+        this._serverVersion = result.serverInfo;
+        // HTTP transports must set the protocol version in each header after initialization.
+        if (transport.setProtocolVersion) {
+            transport.setProtocolVersion(result.protocolVersion);
+        }
+        this._instructions = result.instructions;
+        await this.notification({
+            method: 'notifications/initialized'
+        });
+        // Set up list changed handlers now that we know server capabilities
+        if (this._pendingListChangedConfig) {
+            this._setupListChangedHandlers(this._pendingListChangedConfig);
+            this._pendingListChangedConfig = undefined;
+        }
+    }
     /**
      * After initialization has completed, this will be populated with the server's reported capabilities.
      */
@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol {
      * Called after listTools() to pre-compile validators for better performance.
      */
-    cacheToolMetadata(tools) {
-        this._cachedToolOutputValidators.clear();
-        this._cachedKnownTaskTools.clear();
-        this._cachedRequiredTaskTools.clear();
+    cacheToolMetadata(tools, reset = true) {
+        if (reset) {
+            this._cachedToolOutputValidators.clear();
+            this._cachedKnownTaskTools.clear();
+            this._cachedRequiredTaskTools.clear();
+        }
         for (const tool of tools) {
             // If the tool has an outputSchema, create and cache the validator
             if (tool.outputSchema) {
@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol {
     async listTools(params, options) {
         const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options);
         // Cache the tools and their output schemas for future validation
-        this.cacheToolMetadata(result.tools);
+        this.cacheToolMetadata(result.tools, params?.cursor === undefined);
         return result;
     }
     /**
diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
--- a/dist/cjs/client/streamableHttp.js
+++ b/dist/cjs/client/streamableHttp.js
@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport {
         this.onclose?.();
     }
     async send(message, options) {
+        return this._send(message, options, false);
+    }
+    async _recoverSession(expiredSessionId) {
+        if (this._sessionRecovery) {
+            await this._sessionRecovery;
+            return true;
+        }
+        if (this._sessionId !== expiredSessionId)
+            return true;
+        this._sessionId = undefined;
+        this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
         try {
+            await this._sessionRecovery;
+        }
+        catch (error) {
+            this._sessionId = undefined;
+            await this.close();
+            throw error;
+        }
+        finally {
+            this._sessionRecovery = undefined;
+        }
+        return true;
+    }
+    async _send(message, options, isSessionRetry) {
+        try {
+            if (this._sessionRecovery && !(0, types_js_1.isInitializeRequest)(message) && !(0, types_js_1.isInitializedNotification)(message)) {
+                await this._sessionRecovery;
+                if (options?.isRequestActive?.() === false) {
+                    throw new Error('Request is no longer active');
+                }
+            }
             const { resumptionToken, onresumptiontoken } = options || {};
             if (resumptionToken) {
                 // If we have at last event ID, we need to reconnect the SSE stream
@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport {
                 return;
             }
             const headers = await this._commonHeaders();
+            const requestSessionId = headers.get('mcp-session-id') ?? undefined;
             headers.set('content-type', 'application/json');
             headers.set('accept', 'application/json, text/event-stream');
             const init = {
@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport {
             const response = await (this._fetch ?? fetch)(this._url, init);
             // Handle session ID received during initialization
             const sessionId = response.headers.get('mcp-session-id');
-            if (sessionId) {
+            if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
                 this._sessionId = sessionId;
             }
             if (!response.ok) {
                 const text = await response.text().catch(() => null);
+                if (response.status === 404 && requestSessionId && !isSessionRetry && !(0, types_js_1.isInitializedNotification)(message)) {
+                    const recovered = await this._recoverSession(requestSessionId);
+                    if (options?.isRequestActive?.() === false) {
+                        throw new Error('Request is no longer active');
+                    }
+                    if (recovered) {
+                        return this._send(message, options, true);
+                    }
+                }
                 if (response.status === 401 && this._authProvider) {
                     // Prevent infinite recursion when server returns 401 after successful auth
                     if (this._hasCompletedAuthFlow) {
@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport {
                     // Mark that we completed auth flow
                     this._hasCompletedAuthFlow = true;
                     // Purposely _not_ awaited, so we don't call onerror twice
-                    return this.send(message);
+                    return this._send(message, options, isSessionRetry);
                 }
                 if (response.status === 403 && this._authProvider) {
                     const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport {
                         if (result !== 'AUTHORIZED') {
                             throw new auth_js_1.UnauthorizedError();
                         }
-                        return this.send(message);
+                        return this._send(message, options, isSessionRetry);
                     }
                 }
                 throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644
--- a/dist/cjs/shared/protocol.js
+++ b/dist/cjs/shared/protocol.js
@@ -744,7 +744,12 @@ class Protocol {
             }
             else {
                 // No related task - send through transport normally
-                this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
+                this._transport.send(jsonrpcRequest, {
+                    relatedRequestId,
+                    resumptionToken,
+                    onresumptiontoken,
+                    isRequestActive: () => this._responseHandlers.has(messageId)
+                }).catch(error => {
                     this._cleanupTimeout(messageId);
                     reject(error);
                 });
diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
--- a/dist/cjs/client/auth.d.ts
+++ b/dist/cjs/client/auth.d.ts
@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
  * @returns A Promise that resolves to an OAuthError instance
  */
 export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+export declare function determineScope(options: {
+    requestedScope?: string;
+    resourceMetadata?: OAuthProtectedResourceMetadata;
+    authServerMetadata?: AuthorizationServerMetadata;
+    clientMetadata: OAuthClientMetadata;
+}): string | undefined;
 /**
  * Orchestrates the full auth flow with a server.
  *
diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644
--- a/dist/cjs/client/auth.js
+++ b/dist/cjs/client/auth.js
@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0;
 exports.selectClientAuthMethod = selectClientAuthMethod;
 exports.parseErrorResponse = parseErrorResponse;
 exports.auth = auth;
+exports.determineScope = determineScope;
 exports.isHttpsUrl = isHttpsUrl;
 exports.selectResourceURL = selectResourceURL;
 exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
@@ -186,6 +187,19 @@ async function auth(provider, options) {
         throw error;
     }
 }
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
+    let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
+    if (effectiveScope &&
+        authServerMetadata?.scopes_supported?.includes('offline_access') &&
+        !effectiveScope.split(' ').includes('offline_access') &&
+        clientMetadata.grant_types?.includes('refresh_token')) {
+        effectiveScope = `${effectiveScope} offline_access`;
+    }
+    return effectiveScope;
+}
 async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
     // Check if the provider has cached discovery state to skip discovery
     const cachedState = await provider.discoveryState?.();
@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
         });
     }
     const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
-    // Apply scope selection strategy (SEP-835):
-    // 1. WWW-Authenticate scope (passed via `scope` param)
-    // 2. PRM scopes_supported
-    // 3. Client metadata scope (user-configured fallback)
-    // The resolved scope is used consistently for both DCR and the authorization request.
-    const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
+    const resolvedScope = determineScope({
+        requestedScope: scope,
+        resourceMetadata,
+        authServerMetadata: metadata,
+        clientMetadata: provider.clientMetadata
+    });
     // Handle client registration if needed
     let clientInformation = await Promise.resolve(provider.clientInformation());
     if (!clientInformation) {
@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
     if (scope) {
         authorizationUrl.searchParams.set('scope', scope);
     }
-    if (scope?.includes('offline_access')) {
+    if (scope?.split(' ').includes('offline_access')) {
         // if the request includes the OIDC-only "offline_access" scope,
         // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
         // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
--- a/dist/esm/client/auth.d.ts
+++ b/dist/esm/client/auth.d.ts
@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
  * @returns A Promise that resolves to an OAuthError instance
  */
 export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+export declare function determineScope(options: {
+    requestedScope?: string;
+    resourceMetadata?: OAuthProtectedResourceMetadata;
+    authServerMetadata?: AuthorizationServerMetadata;
+    clientMetadata: OAuthClientMetadata;
+}): string | undefined;
 /**
  * Orchestrates the full auth flow with a server.
  *
diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644
--- a/dist/esm/client/auth.js
+++ b/dist/esm/client/auth.js
@@ -161,6 +161,19 @@ export async function auth(provider, options) {
         throw error;
     }
 }
+/**
+ * Selects scopes per the MCP spec and augments them for refresh token support.
+ */
+export function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
+    let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
+    if (effectiveScope &&
+        authServerMetadata?.scopes_supported?.includes('offline_access') &&
+        !effectiveScope.split(' ').includes('offline_access') &&
+        clientMetadata.grant_types?.includes('refresh_token')) {
+        effectiveScope = `${effectiveScope} offline_access`;
+    }
+    return effectiveScope;
+}
 async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
     // Check if the provider has cached discovery state to skip discovery
     const cachedState = await provider.discoveryState?.();
@@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
         });
     }
     const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
-    // Apply scope selection strategy (SEP-835):
-    // 1. WWW-Authenticate scope (passed via `scope` param)
-    // 2. PRM scopes_supported
-    // 3. Client metadata scope (user-configured fallback)
-    // The resolved scope is used consistently for both DCR and the authorization request.
-    const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
+    const resolvedScope = determineScope({
+        requestedScope: scope,
+        resourceMetadata,
+        authServerMetadata: metadata,
+        clientMetadata: provider.clientMetadata
+    });
     // Handle client registration if needed
     let clientInformation = await Promise.resolve(provider.clientInformation());
     if (!clientInformation) {
@@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli
     if (scope) {
         authorizationUrl.searchParams.set('scope', scope);
     }
-    if (scope?.includes('offline_access')) {
+    if (scope?.split(' ').includes('offline_access')) {
         // if the request includes the OIDC-only "offline_access" scope,
         // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
         // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644
--- a/dist/esm/client/index.js
+++ b/dist/esm/client/index.js
@@ -284,41 +284,16 @@ export class Client extends Protocol {
     }
     async connect(transport, options) {
         await super.connect(transport);
+        transport.onsessionexpired = async () => {
+            await this._initialize(transport);
+        };
         // When transport sessionId is already set this means we are trying to reconnect.
         // In this case we don't need to initialize again.
         if (transport.sessionId !== undefined) {
             return;
         }
         try {
-            const result = await this.request({
-                method: 'initialize',
-                params: {
-                    protocolVersion: LATEST_PROTOCOL_VERSION,
-                    capabilities: this._capabilities,
-                    clientInfo: this._clientInfo
-                }
-            }, InitializeResultSchema, options);
-            if (result === undefined) {
-                throw new Error(`Server sent invalid initialize result: ${result}`);
-            }
-            if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
-                throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
-            }
-            this._serverCapabilities = result.capabilities;
-            this._serverVersion = result.serverInfo;
-            // HTTP transports must set the protocol version in each header after initialization.
-            if (transport.setProtocolVersion) {
-                transport.setProtocolVersion(result.protocolVersion);
-            }
-            this._instructions = result.instructions;
-            await this.notification({
-                method: 'notifications/initialized'
-            });
-            // Set up list changed handlers now that we know server capabilities
-            if (this._pendingListChangedConfig) {
-                this._setupListChangedHandlers(this._pendingListChangedConfig);
-                this._pendingListChangedConfig = undefined;
-            }
+            await this._initialize(transport, options);
         }
         catch (error) {
             // Disconnect if initialization fails.
@@ -326,6 +301,37 @@ export class Client extends Protocol {
             throw error;
         }
     }
+    async _initialize(transport, options) {
+        const result = await this.request({
+            method: 'initialize',
+            params: {
+                protocolVersion: LATEST_PROTOCOL_VERSION,
+                capabilities: this._capabilities,
+                clientInfo: this._clientInfo
+            }
+        }, InitializeResultSchema, options);
+        if (result === undefined) {
+            throw new Error(`Server sent invalid initialize result: ${result}`);
+        }
+        if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
+            throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
+        }
+        this._serverCapabilities = result.capabilities;
+        this._serverVersion = result.serverInfo;
+        // HTTP transports must set the protocol version in each header after initialization.
+        if (transport.setProtocolVersion) {
+            transport.setProtocolVersion(result.protocolVersion);
+        }
+        this._instructions = result.instructions;
+        await this.notification({
+            method: 'notifications/initialized'
+        });
+        // Set up list changed handlers now that we know server capabilities
+        if (this._pendingListChangedConfig) {
+            this._setupListChangedHandlers(this._pendingListChangedConfig);
+            this._pendingListChangedConfig = undefined;
+        }
+    }
     /**
      * After initialization has completed, this will be populated with the server's reported capabilities.
      */
@@ -537,9 +543,11 @@ export class Client extends Protocol {
      * Called after listTools() to pre-compile validators for better performance.
      */
-    cacheToolMetadata(tools) {
-        this._cachedToolOutputValidators.clear();
-        this._cachedKnownTaskTools.clear();
-        this._cachedRequiredTaskTools.clear();
+    cacheToolMetadata(tools, reset = true) {
+        if (reset) {
+            this._cachedToolOutputValidators.clear();
+            this._cachedKnownTaskTools.clear();
+            this._cachedRequiredTaskTools.clear();
+        }
         for (const tool of tools) {
             // If the tool has an outputSchema, create and cache the validator
             if (tool.outputSchema) {
@@ -565,7 +573,7 @@ export class Client extends Protocol {
     async listTools(params, options) {
         const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
         // Cache the tools and their output schemas for future validation
-        this.cacheToolMetadata(result.tools);
+        this.cacheToolMetadata(result.tools, params?.cursor === undefined);
         return result;
     }
     /**
diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644
--- a/dist/esm/client/streamableHttp.js
+++ b/dist/esm/client/streamableHttp.js
@@ -1,5 +1,5 @@
 import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
-import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
 import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
 import { EventSourceParserStream } from 'eventsource-parser/stream';
 // Default reconnection options for StreamableHTTP connections
@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport {
         this.onclose?.();
     }
     async send(message, options) {
+        return this._send(message, options, false);
+    }
+    async _recoverSession(expiredSessionId) {
+        if (this._sessionRecovery) {
+            await this._sessionRecovery;
+            return true;
+        }
+        if (this._sessionId !== expiredSessionId)
+            return true;
+        this._sessionId = undefined;
+        this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
         try {
+            await this._sessionRecovery;
+        }
+        catch (error) {
+            this._sessionId = undefined;
+            await this.close();
+            throw error;
+        }
+        finally {
+            this._sessionRecovery = undefined;
+        }
+        return true;
+    }
+    async _send(message, options, isSessionRetry) {
+        try {
+            if (this._sessionRecovery && !isInitializeRequest(message) && !isInitializedNotification(message)) {
+                await this._sessionRecovery;
+                if (options?.isRequestActive?.() === false) {
+                    throw new Error('Request is no longer active');
+                }
+            }
             const { resumptionToken, onresumptiontoken } = options || {};
             if (resumptionToken) {
                 // If we have at last event ID, we need to reconnect the SSE stream
@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport {
                 return;
             }
             const headers = await this._commonHeaders();
+            const requestSessionId = headers.get('mcp-session-id') ?? undefined;
             headers.set('content-type', 'application/json');
             headers.set('accept', 'application/json, text/event-stream');
             const init = {
@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport {
             const response = await (this._fetch ?? fetch)(this._url, init);
             // Handle session ID received during initialization
             const sessionId = response.headers.get('mcp-session-id');
-            if (sessionId) {
+            if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
                 this._sessionId = sessionId;
             }
             if (!response.ok) {
                 const text = await response.text().catch(() => null);
+                if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitializedNotification(message)) {
+                    const recovered = await this._recoverSession(requestSessionId);
+                    if (options?.isRequestActive?.() === false) {
+                        throw new Error('Request is no longer active');
+                    }
+                    if (recovered) {
+                        return this._send(message, options, true);
+                    }
+                }
                 if (response.status === 401 && this._authProvider) {
                     // Prevent infinite recursion when server returns 401 after successful auth
                     if (this._hasCompletedAuthFlow) {
@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport {
                     // Mark that we completed auth flow
                     this._hasCompletedAuthFlow = true;
                     // Purposely _not_ awaited, so we don't call onerror twice
-                    return this.send(message);
+                    return this._send(message, options, isSessionRetry);
                 }
                 if (response.status === 403 && this._authProvider) {
                     const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport {
                         if (result !== 'AUTHORIZED') {
                             throw new UnauthorizedError();
                         }
-                        return this.send(message);
+                        return this._send(message, options, isSessionRetry);
                     }
                 }
                 throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644
--- a/dist/esm/shared/protocol.js
+++ b/dist/esm/shared/protocol.js
@@ -740,7 +740,12 @@ export class Protocol {
             }
             else {
                 // No related task - send through transport normally
-                this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
+                this._transport.send(jsonrpcRequest, {
+                    relatedRequestId,
+                    resumptionToken,
+                    onresumptiontoken,
+                    isRequestActive: () => this._responseHandlers.has(messageId)
+                }).catch(error => {
                     this._cleanupTimeout(messageId);
                     reject(error);
                 });
