PHANTOM
🇮🇳 IN
Skip to content

Bump @modelcontextprotocol/sdk from 1.25.2 to 1.26.0#229

Merged
mihaiplesa merged 1 commit intomainfrom
dependabot/npm_and_yarn/modelcontextprotocol/sdk-1.26.0
Feb 4, 2026
Merged

Bump @modelcontextprotocol/sdk from 1.25.2 to 1.26.0#229
mihaiplesa merged 1 commit intomainfrom
dependabot/npm_and_yarn/modelcontextprotocol/sdk-1.26.0

Conversation

@dependabot
Copy link
Contributor

@dependabot dependabot bot commented on behalf of github Feb 4, 2026

Bumps @modelcontextprotocol/sdk from 1.25.2 to 1.26.0.

Release notes

Sourced from @​modelcontextprotocol/sdk's releases.

v1.26.0

Addresses "Sharing server/transport instances can leak cross-client response data" in this GHSA GHSA-345p-7cg4-v4c7

What's Changed

New Contributors

Full Changelog: modelcontextprotocol/typescript-sdk@v1.25.3...v1.26.0

v1.25.3

What's Changed

Full Changelog: modelcontextprotocol/typescript-sdk@v1.25.2...v1.25.3

Commits
  • fe9c07b chore: bump version to 1.26.0 (#1479)
  • 4f01e7e fix: add non-null assertions for optional setupServer fields in stateful test
  • a05be17 Merge commit from fork
  • 50d9fa3 Fix #1430: Client Credentials providers scopes support (backported) (#1442)
  • aa81a66 fix(deps): resolve npm audit vulnerabilities and bump dependencies (v1.x back...
  • 6aba065 chore: bump v1.25.3 for backport fixes (#1412)
  • 6e8f7e1 fix: prevent Hono from overriding global Response object (v1.x) (#1411)
  • 12ae856 [v1.x backport] Use correct schema for client sampling validation when tools ...
  • See full diff in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    You can disable automated security fix PRs for this repo from the Security Alerts page.

Bumps [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) from 1.25.2 to 1.26.0.
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](modelcontextprotocol/typescript-sdk@v1.25.2...v1.26.0)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/sdk"
  dependency-version: 1.26.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Feb 4, 2026
@dependabot dependabot bot requested a review from jonathansampson as a code owner February 4, 2026 20:37
@dependabot dependabot bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Feb 4, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

[puLL-Merge] - modelcontextprotocol/typescript-sdk@v1.25.2..v1.26.0

Diff
diff --git package.json package.json
index 95a3a5d53..dc02209b1 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
 {
     "name": "@modelcontextprotocol/sdk",
-    "version": "1.25.2",
+    "version": "1.26.0",
     "description": "Model Context Protocol implementation for TypeScript",
     "license": "MIT",
     "author": "Anthropic, PBC (https://anthropic.com)",
@@ -86,7 +86,7 @@
         "client": "tsx scripts/cli.ts client"
     },
     "dependencies": {
-        "@hono/node-server": "^1.19.7",
+        "@hono/node-server": "^1.19.9",
         "ajv": "^8.17.1",
         "ajv-formats": "^3.0.1",
         "content-type": "^1.0.5",
@@ -94,14 +94,15 @@
         "cross-spawn": "^7.0.5",
         "eventsource": "^3.0.2",
         "eventsource-parser": "^3.0.0",
-        "express": "^5.0.1",
-        "express-rate-limit": "^7.5.0",
-        "jose": "^6.1.1",
+        "express": "^5.2.1",
+        "express-rate-limit": "^8.2.1",
+        "hono": "^4.11.4",
+        "jose": "^6.1.3",
         "json-schema-typed": "^8.0.2",
         "pkce-challenge": "^5.0.0",
         "raw-body": "^3.0.0",
         "zod": "^3.25 || ^4.0",
-        "zod-to-json-schema": "^3.25.0"
+        "zod-to-json-schema": "^3.25.1"
     },
     "peerDependencies": {
         "@cfworker/json-schema": "^4.1.1",
@@ -141,5 +142,8 @@
     },
     "resolutions": {
         "strip-ansi": "6.0.1"
+    },
+    "overrides": {
+        "qs": "6.14.1"
     }
 }
diff --git src/client/auth-extensions.ts src/client/auth-extensions.ts
index f3908d2c2..c90404e94 100644
--- src/client/auth-extensions.ts
+++ src/client/auth-extensions.ts
@@ -108,6 +108,11 @@ export interface ClientCredentialsProviderOptions {
      * Optional client name for metadata.
      */
     clientName?: string;
+
+    /**
+     * Space-separated scopes values requested by the client.
+     */
+    scope?: string;
 }
 
 /**
@@ -140,7 +145,8 @@ export class ClientCredentialsProvider implements OAuthClientProvider {
             client_name: options.clientName ?? 'client-credentials-client',
             redirect_uris: [],
             grant_types: ['client_credentials'],
-            token_endpoint_auth_method: 'client_secret_basic'
+            token_endpoint_auth_method: 'client_secret_basic',
+            scope: options.scope
         };
     }
 
@@ -216,6 +222,11 @@ export interface PrivateKeyJwtProviderOptions {
      * Optional JWT lifetime in seconds (default: 300).
      */
     jwtLifetimeSeconds?: number;
+
+    /**
+     * Space-separated scopes values requested by the client.
+     */
+    scope?: string;
 }
 
 /**
@@ -249,7 +260,8 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider {
             client_name: options.clientName ?? 'private-key-jwt-client',
             redirect_uris: [],
             grant_types: ['client_credentials'],
-            token_endpoint_auth_method: 'private_key_jwt'
+            token_endpoint_auth_method: 'private_key_jwt',
+            scope: options.scope
         };
         this.addClientAuthentication = createPrivateKeyJwtAuth({
             issuer: options.clientId,
@@ -324,6 +336,11 @@ export interface StaticPrivateKeyJwtProviderOptions {
      * Optional client name for metadata.
      */
     clientName?: string;
+
+    /**
+     * Space-separated scopes values requested by the client.
+     */
+    scope?: string;
 }
 
 /**
@@ -347,7 +364,8 @@ export class StaticPrivateKeyJwtProvider implements OAuthClientProvider {
             client_name: options.clientName ?? 'static-private-key-jwt-client',
             redirect_uris: [],
             grant_types: ['client_credentials'],
-            token_endpoint_auth_method: 'private_key_jwt'
+            token_endpoint_auth_method: 'private_key_jwt',
+            scope: options.scope
         };
 
         const assertion = options.jwtBearerAssertion;
diff --git src/client/index.ts src/client/index.ts
index 28c0e6253..03a6b40b5 100644
--- src/client/index.ts
+++ src/client/index.ts
@@ -40,6 +40,7 @@ import {
     CreateTaskResultSchema,
     CreateMessageRequestSchema,
     CreateMessageResultSchema,
+    CreateMessageResultWithToolsSchema,
     ToolListChangedNotificationSchema,
     PromptListChangedNotificationSchema,
     ResourceListChangedNotificationSchema,
@@ -452,8 +453,10 @@ export class Client<
                     return taskValidationResult.data;
                 }
 
-                // For non-task requests, validate against CreateMessageResultSchema
-                const validationResult = safeParse(CreateMessageResultSchema, result);
+                // For non-task requests, validate against appropriate schema based on tools presence
+                const hasTools = params.tools || params.toolChoice;
+                const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
+                const validationResult = safeParse(resultSchema, result);
                 if (!validationResult.success) {
                     const errorMessage =
                         validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
diff --git src/examples/server/elicitationFormExample.ts src/examples/server/elicitationFormExample.ts
index 6c0800949..d220806d3 100644
--- src/examples/server/elicitationFormExample.ts
+++ src/examples/server/elicitationFormExample.ts
@@ -14,308 +14,314 @@ import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
 import { isInitializeRequest } from '../../types.js';
 import { createMcpExpressApp } from '../../server/express.js';
 
-// Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults
-// The validator supports format validation (email, date, etc.) if ajv-formats is installed
-const mcpServer = new McpServer(
-    {
-        name: 'form-elicitation-example-server',
-        version: '1.0.0'
-    },
-    {
-        capabilities: {}
-    }
-);
-
-/**
- * Example 1: Simple user registration tool
- * Collects username, email, and password from the user
- */
-mcpServer.registerTool(
-    'register_user',
-    {
-        description: 'Register a new user account by collecting their information',
-        inputSchema: {}
-    },
-    async () => {
-        try {
-            // Request user information through form elicitation
-            const result = await mcpServer.server.elicitInput({
-                mode: 'form',
-                message: 'Please provide your registration information:',
-                requestedSchema: {
-                    type: 'object',
-                    properties: {
-                        username: {
-                            type: 'string',
-                            title: 'Username',
-                            description: 'Your desired username (3-20 characters)',
-                            minLength: 3,
-                            maxLength: 20
-                        },
-                        email: {
-                            type: 'string',
-                            title: 'Email',
-                            description: 'Your email address',
-                            format: 'email'
-                        },
-                        password: {
-                            type: 'string',
-                            title: 'Password',
-                            description: 'Your password (min 8 characters)',
-                            minLength: 8
+// Factory to create a new MCP server per session.
+// Each session needs its own server+transport pair to avoid cross-session contamination.
+const getServer = () => {
+    // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults
+    // The validator supports format validation (email, date, etc.) if ajv-formats is installed
+    const mcpServer = new McpServer(
+        {
+            name: 'form-elicitation-example-server',
+            version: '1.0.0'
+        },
+        {
+            capabilities: {}
+        }
+    );
+
+    /**
+     * Example 1: Simple user registration tool
+     * Collects username, email, and password from the user
+     */
+    mcpServer.registerTool(
+        'register_user',
+        {
+            description: 'Register a new user account by collecting their information',
+            inputSchema: {}
+        },
+        async () => {
+            try {
+                // Request user information through form elicitation
+                const result = await mcpServer.server.elicitInput({
+                    mode: 'form',
+                    message: 'Please provide your registration information:',
+                    requestedSchema: {
+                        type: 'object',
+                        properties: {
+                            username: {
+                                type: 'string',
+                                title: 'Username',
+                                description: 'Your desired username (3-20 characters)',
+                                minLength: 3,
+                                maxLength: 20
+                            },
+                            email: {
+                                type: 'string',
+                                title: 'Email',
+                                description: 'Your email address',
+                                format: 'email'
+                            },
+                            password: {
+                                type: 'string',
+                                title: 'Password',
+                                description: 'Your password (min 8 characters)',
+                                minLength: 8
+                            },
+                            newsletter: {
+                                type: 'boolean',
+                                title: 'Newsletter',
+                                description: 'Subscribe to newsletter?',
+                                default: false
+                            }
                         },
-                        newsletter: {
-                            type: 'boolean',
-                            title: 'Newsletter',
-                            description: 'Subscribe to newsletter?',
-                            default: false
-                        }
-                    },
-                    required: ['username', 'email', 'password']
-                }
-            });
-
-            // Handle the different possible actions
-            if (result.action === 'accept' && result.content) {
-                const { username, email, newsletter } = result.content as {
-                    username: string;
-                    email: string;
-                    password: string;
-                    newsletter?: boolean;
-                };
+                        required: ['username', 'email', 'password']
+                    }
+                });
 
+                // Handle the different possible actions
+                if (result.action === 'accept' && result.content) {
+                    const { username, email, newsletter } = result.content as {
+                        username: string;
+                        email: string;
+                        password: string;
+                        newsletter?: boolean;
+                    };
+
+                    return {
+                        content: [
+                            {
+                                type: 'text',
+                                text: `Registration successful!\n\nUsername: ${username}\nEmail: ${email}\nNewsletter: ${newsletter ? 'Yes' : 'No'}`
+                            }
+                        ]
+                    };
+                } else if (result.action === 'decline') {
+                    return {
+                        content: [
+                            {
+                                type: 'text',
+                                text: 'Registration cancelled by user.'
+                            }
+                        ]
+                    };
+                } else {
+                    return {
+                        content: [
+                            {
+                                type: 'text',
+                                text: 'Registration was cancelled.'
+                            }
+                        ]
+                    };
+                }
+            } catch (error) {
                 return {
                     content: [
                         {
                             type: 'text',
-                            text: `Registration successful!\n\nUsername: ${username}\nEmail: ${email}\nNewsletter: ${newsletter ? 'Yes' : 'No'}`
+                            text: `Registration failed: ${error instanceof Error ? error.message : String(error)}`
                         }
-                    ]
+                    ],
+                    isError: true
+                };
+            }
+        }
+    );
+
+    /**
+     * Example 2: Multi-step workflow with multiple form elicitation requests
+     * Demonstrates how to collect information in multiple steps
+     */
+    mcpServer.registerTool(
+        'create_event',
+        {
+            description: 'Create a calendar event by collecting event details',
+            inputSchema: {}
+        },
+        async () => {
+            try {
+                // Step 1: Collect basic event information
+                const basicInfo = await mcpServer.server.elicitInput({
+                    mode: 'form',
+                    message: 'Step 1: Enter basic event information',
+                    requestedSchema: {
+                        type: 'object',
+                        properties: {
+                            title: {
+                                type: 'string',
+                                title: 'Event Title',
+                                description: 'Name of the event',
+                                minLength: 1
+                            },
+                            description: {
+                                type: 'string',
+                                title: 'Description',
+                                description: 'Event description (optional)'
+                            }
+                        },
+                        required: ['title']
+                    }
+                });
+
+                if (basicInfo.action !== 'accept' || !basicInfo.content) {
+                    return {
+                        content: [{ type: 'text', text: 'Event creation cancelled.' }]
+                    };
+                }
+
+                // Step 2: Collect date and time
+                const dateTime = await mcpServer.server.elicitInput({
+                    mode: 'form',
+                    message: 'Step 2: Enter date and time',
+                    requestedSchema: {
+                        type: 'object',
+                        properties: {
+                            date: {
+                                type: 'string',
+                                title: 'Date',
+                                description: 'Event date',
+                                format: 'date'
+                            },
+                            startTime: {
+                                type: 'string',
+                                title: 'Start Time',
+                                description: 'Event start time (HH:MM)'
+                            },
+                            duration: {
+                                type: 'integer',
+                                title: 'Duration',
+                                description: 'Duration in minutes',
+                                minimum: 15,
+                                maximum: 480
+                            }
+                        },
+                        required: ['date', 'startTime', 'duration']
+                    }
+                });
+
+                if (dateTime.action !== 'accept' || !dateTime.content) {
+                    return {
+                        content: [{ type: 'text', text: 'Event creation cancelled.' }]
+                    };
+                }
+
+                // Combine all collected information
+                const event = {
+                    ...basicInfo.content,
+                    ...dateTime.content
                 };
-            } else if (result.action === 'decline') {
+
                 return {
                     content: [
                         {
                             type: 'text',
-                            text: 'Registration cancelled by user.'
+                            text: `Event created successfully!\n\n${JSON.stringify(event, null, 2)}`
                         }
                     ]
                 };
-            } else {
+            } catch (error) {
                 return {
                     content: [
                         {
                             type: 'text',
-                            text: 'Registration was cancelled.'
+                            text: `Event creation failed: ${error instanceof Error ? error.message : String(error)}`
                         }
-                    ]
+                    ],
+                    isError: true
                 };
             }
-        } catch (error) {
-            return {
-                content: [
-                    {
-                        type: 'text',
-                        text: `Registration failed: ${error instanceof Error ? error.message : String(error)}`
-                    }
-                ],
-                isError: true
-            };
         }
-    }
-);
-
-/**
- * Example 2: Multi-step workflow with multiple form elicitation requests
- * Demonstrates how to collect information in multiple steps
- */
-mcpServer.registerTool(
-    'create_event',
-    {
-        description: 'Create a calendar event by collecting event details',
-        inputSchema: {}
-    },
-    async () => {
-        try {
-            // Step 1: Collect basic event information
-            const basicInfo = await mcpServer.server.elicitInput({
-                mode: 'form',
-                message: 'Step 1: Enter basic event information',
-                requestedSchema: {
-                    type: 'object',
-                    properties: {
-                        title: {
-                            type: 'string',
-                            title: 'Event Title',
-                            description: 'Name of the event',
-                            minLength: 1
-                        },
-                        description: {
-                            type: 'string',
-                            title: 'Description',
-                            description: 'Event description (optional)'
-                        }
-                    },
-                    required: ['title']
-                }
-            });
-
-            if (basicInfo.action !== 'accept' || !basicInfo.content) {
-                return {
-                    content: [{ type: 'text', text: 'Event creation cancelled.' }]
-                };
-            }
-
-            // Step 2: Collect date and time
-            const dateTime = await mcpServer.server.elicitInput({
-                mode: 'form',
-                message: 'Step 2: Enter date and time',
-                requestedSchema: {
-                    type: 'object',
-                    properties: {
-                        date: {
-                            type: 'string',
-                            title: 'Date',
-                            description: 'Event date',
-                            format: 'date'
-                        },
-                        startTime: {
-                            type: 'string',
-                            title: 'Start Time',
-                            description: 'Event start time (HH:MM)'
+    );
+
+    /**
+     * Example 3: Collecting address information
+     * Demonstrates validation with patterns and optional fields
+     */
+    mcpServer.registerTool(
+        'update_shipping_address',
+        {
+            description: 'Update shipping address with validation',
+            inputSchema: {}
+        },
+        async () => {
+            try {
+                const result = await mcpServer.server.elicitInput({
+                    mode: 'form',
+                    message: 'Please provide your shipping address:',
+                    requestedSchema: {
+                        type: 'object',
+                        properties: {
+                            name: {
+                                type: 'string',
+                                title: 'Full Name',
+                                description: 'Recipient name',
+                                minLength: 1
+                            },
+                            street: {
+                                type: 'string',
+                                title: 'Street Address',
+                                minLength: 1
+                            },
+                            city: {
+                                type: 'string',
+                                title: 'City',
+                                minLength: 1
+                            },
+                            state: {
+                                type: 'string',
+                                title: 'State/Province',
+                                minLength: 2,
+                                maxLength: 2
+                            },
+                            zipCode: {
+                                type: 'string',
+                                title: 'ZIP/Postal Code',
+                                description: '5-digit ZIP code'
+                            },
+                            phone: {
+                                type: 'string',
+                                title: 'Phone Number (optional)',
+                                description: 'Contact phone number'
+                            }
                         },
-                        duration: {
-                            type: 'integer',
-                            title: 'Duration',
-                            description: 'Duration in minutes',
-                            minimum: 15,
-                            maximum: 480
-                        }
-                    },
-                    required: ['date', 'startTime', 'duration']
-                }
-            });
-
-            if (dateTime.action !== 'accept' || !dateTime.content) {
-                return {
-                    content: [{ type: 'text', text: 'Event creation cancelled.' }]
-                };
-            }
-
-            // Combine all collected information
-            const event = {
-                ...basicInfo.content,
-                ...dateTime.content
-            };
-
-            return {
-                content: [
-                    {
-                        type: 'text',
-                        text: `Event created successfully!\n\n${JSON.stringify(event, null, 2)}`
-                    }
-                ]
-            };
-        } catch (error) {
-            return {
-                content: [
-                    {
-                        type: 'text',
-                        text: `Event creation failed: ${error instanceof Error ? error.message : String(error)}`
+                        required: ['name', 'street', 'city', 'state', 'zipCode']
                     }
-                ],
-                isError: true
-            };
-        }
-    }
-);
-
-/**
- * Example 3: Collecting address information
- * Demonstrates validation with patterns and optional fields
- */
-mcpServer.registerTool(
-    'update_shipping_address',
-    {
-        description: 'Update shipping address with validation',
-        inputSchema: {}
-    },
-    async () => {
-        try {
-            const result = await mcpServer.server.elicitInput({
-                mode: 'form',
-                message: 'Please provide your shipping address:',
-                requestedSchema: {
-                    type: 'object',
-                    properties: {
-                        name: {
-                            type: 'string',
-                            title: 'Full Name',
-                            description: 'Recipient name',
-                            minLength: 1
-                        },
-                        street: {
-                            type: 'string',
-                            title: 'Street Address',
-                            minLength: 1
-                        },
-                        city: {
-                            type: 'string',
-                            title: 'City',
-                            minLength: 1
-                        },
-                        state: {
-                            type: 'string',
-                            title: 'State/Province',
-                            minLength: 2,
-                            maxLength: 2
-                        },
-                        zipCode: {
-                            type: 'string',
-                            title: 'ZIP/Postal Code',
-                            description: '5-digit ZIP code'
-                        },
-                        phone: {
-                            type: 'string',
-                            title: 'Phone Number (optional)',
-                            description: 'Contact phone number'
-                        }
-                    },
-                    required: ['name', 'street', 'city', 'state', 'zipCode']
-                }
-            });
+                });
 
-            if (result.action === 'accept' && result.content) {
+                if (result.action === 'accept' && result.content) {
+                    return {
+                        content: [
+                            {
+                                type: 'text',
+                                text: `Address updated successfully!\n\n${JSON.stringify(result.content, null, 2)}`
+                            }
+                        ]
+                    };
+                } else if (result.action === 'decline') {
+                    return {
+                        content: [{ type: 'text', text: 'Address update cancelled by user.' }]
+                    };
+                } else {
+                    return {
+                        content: [{ type: 'text', text: 'Address update was cancelled.' }]
+                    };
+                }
+            } catch (error) {
                 return {
                     content: [
                         {
                             type: 'text',
-                            text: `Address updated successfully!\n\n${JSON.stringify(result.content, null, 2)}`
+                            text: `Address update failed: ${error instanceof Error ? error.message : String(error)}`
                         }
-                    ]
-                };
-            } else if (result.action === 'decline') {
-                return {
-                    content: [{ type: 'text', text: 'Address update cancelled by user.' }]
-                };
-            } else {
-                return {
-                    content: [{ type: 'text', text: 'Address update was cancelled.' }]
+                    ],
+                    isError: true
                 };
             }
-        } catch (error) {
-            return {
-                content: [
-                    {
-                        type: 'text',
-                        text: `Address update failed: ${error instanceof Error ? error.message : String(error)}`
-                    }
-                ],
-                isError: true
-            };
         }
-    }
-);
+    );
+
+    return mcpServer;
+};
 
 async function main() {
     const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
@@ -357,7 +363,8 @@ async function main() {
                     }
                 };
 
-                // Connect the transport to the MCP server BEFORE handling the request
+                // Create a new server per session and connect it to the transport
+                const mcpServer = getServer();
                 await mcpServer.connect(transport);
 
                 await transport.handleRequest(req, res, req.body);
diff --git src/examples/server/honoWebStandardStreamableHttp.ts src/examples/server/honoWebStandardStreamableHttp.ts
index ba8805eae..1a19ee8a4 100644
--- src/examples/server/honoWebStandardStreamableHttp.ts
+++ src/examples/server/honoWebStandardStreamableHttp.ts
@@ -15,29 +15,30 @@ import { McpServer } from '../../server/mcp.js';
 import { WebStandardStreamableHTTPServerTransport } from '../../server/webStandardStreamableHttp.js';
 import { CallToolResult } from '../../types.js';
 
-// Create the MCP server
-const server = new McpServer({
-    name: 'hono-webstandard-mcp-server',
-    version: '1.0.0'
-});
+// Factory function to create a new MCP server per request (stateless mode)
+const getServer = () => {
+    const server = new McpServer({
+        name: 'hono-webstandard-mcp-server',
+        version: '1.0.0'
+    });
 
-// Register a simple greeting tool
-server.registerTool(
-    'greet',
-    {
-        title: 'Greeting Tool',
-        description: 'A simple greeting tool',
-        inputSchema: { name: z.string().describe('Name to greet') }
-    },
-    async ({ name }): Promise<CallToolResult> => {
-        return {
-            content: [{ type: 'text', text: `Hello, ${name}! (from Hono + WebStandard transport)` }]
-        };
-    }
-);
+    // Register a simple greeting tool
+    server.registerTool(
+        'greet',
+        {
+            title: 'Greeting Tool',
+            description: 'A simple greeting tool',
+            inputSchema: { name: z.string().describe('Name to greet') }
+        },
+        async ({ name }): Promise<CallToolResult> => {
+            return {
+                content: [{ type: 'text', text: `Hello, ${name}! (from Hono + WebStandard transport)` }]
+            };
+        }
+    );
 
-// Create a stateless transport (no options = no session management)
-const transport = new WebStandardStreamableHTTPServerTransport();
+    return server;
+};
 
 // Create the Hono app
 const app = new Hono();
@@ -56,19 +57,22 @@ app.use(
 // Health check endpoint
 app.get('/health', c => c.json({ status: 'ok' }));
 
-// MCP endpoint
-app.all('/mcp', c => transport.handleRequest(c.req.raw));
+// MCP endpoint - create a fresh transport and server per request (stateless)
+app.all('/mcp', async c => {
+    const transport = new WebStandardStreamableHTTPServerTransport();
+    const server = getServer();
+    await server.connect(transport);
+    return transport.handleRequest(c.req.raw);
+});
 
 // Start the server
 const PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
 
-server.connect(transport).then(() => {
-    console.log(`Starting Hono MCP server on port ${PORT}`);
-    console.log(`Health check: http://localhost:${PORT}/health`);
-    console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
+console.log(`Starting Hono MCP server on port ${PORT}`);
+console.log(`Health check: http://localhost:${PORT}/health`);
+console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
 
-    serve({
-        fetch: app.fetch,
-        port: PORT
-    });
+serve({
+    fetch: app.fetch,
+    port: PORT
 });
diff --git src/examples/server/ssePollingExample.ts src/examples/server/ssePollingExample.ts
index bbecf2fdb..04d9f7751 100644
--- src/examples/server/ssePollingExample.ts
+++ src/examples/server/ssePollingExample.ts
@@ -12,7 +12,7 @@
  * Run with: npx tsx src/examples/server/ssePollingExample.ts
  * Test with: curl or the MCP Inspector
  */
-import { Request, Response } from 'express';
+import { type Request, type Response } from 'express';
 import { randomUUID } from 'node:crypto';
 import { McpServer } from '../../server/mcp.js';
 import { createMcpExpressApp } from '../../server/express.js';
@@ -21,87 +21,92 @@ import { CallToolResult } from '../../types.js';
 import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
 import cors from 'cors';
 
-// Create the MCP server
-const server = new McpServer(
-    {
-        name: 'sse-polling-example',
-        version: '1.0.0'
-    },
-    {
-        capabilities: { logging: {} }
-    }
-);
-
-// Register a long-running tool that demonstrates server-initiated disconnect
-server.tool(
-    'long-task',
-    'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.',
-    {},
-    async (_args, extra): Promise<CallToolResult> => {
-        const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-
-        console.log(`[${extra.sessionId}] Starting long-task...`);
-
-        // Send first progress notification
-        await server.sendLoggingMessage(
-            {
-                level: 'info',
-                data: 'Progress: 25% - Starting work...'
-            },
-            extra.sessionId
-        );
-        await sleep(1000);
-
-        // Send second progress notification
-        await server.sendLoggingMessage(
-            {
-                level: 'info',
-                data: 'Progress: 50% - Halfway there...'
-            },
-            extra.sessionId
-        );
-        await sleep(1000);
-
-        // Server decides to disconnect the client to free resources
-        // Client will reconnect via GET with Last-Event-ID after the transport's retryInterval
-        // Use extra.closeSSEStream callback - available when eventStore is configured
-        if (extra.closeSSEStream) {
-            console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);
-            extra.closeSSEStream();
+// Factory to create a new MCP server per session.
+// Each session needs its own server+transport pair to avoid cross-session contamination.
+const getServer = () => {
+    const server = new McpServer(
+        {
+            name: 'sse-polling-example',
+            version: '1.0.0'
+        },
+        {
+            capabilities: { logging: {} }
         }
+    );
+
+    // Register a long-running tool that demonstrates server-initiated disconnect
+    server.tool(
+        'long-task',
+        'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.',
+        {},
+        async (_args, extra): Promise<CallToolResult> => {
+            const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+            console.log(`[${extra.sessionId}] Starting long-task...`);
 
-        // Continue processing while client is disconnected
-        // Events are stored in eventStore and will be replayed on reconnect
-        await sleep(500);
-        await server.sendLoggingMessage(
-            {
-                level: 'info',
-                data: 'Progress: 75% - Almost done (sent while client disconnected)...'
-            },
-            extra.sessionId
-        );
-
-        await sleep(500);
-        await server.sendLoggingMessage(
-            {
-                level: 'info',
-                data: 'Progress: 100% - Complete!'
-            },
-            extra.sessionId
-        );
-
-        console.log(`[${extra.sessionId}] Task complete`);
-
-        return {
-            content: [
+            // Send first progress notification
+            await server.sendLoggingMessage(
                 {
-                    type: 'text',
-                    text: 'Long task completed successfully!'
-                }
-            ]
-        };
-    }
-);
+                    level: 'info',
+                    data: 'Progress: 25% - Starting work...'
+                },
+                extra.sessionId
+            );
+            await sleep(1000);
+
+            // Send second progress notification
+            await server.sendLoggingMessage(
+                {
+                    level: 'info',
+                    data: 'Progress: 50% - Halfway there...'
+                },
+                extra.sessionId
+            );
+            await sleep(1000);
+
+            // Server decides to disconnect the client to free resources
+            // Client will reconnect via GET with Last-Event-ID after the transport's retryInterval
+            // Use extra.closeSSEStream callback - available when eventStore is configured
+            if (extra.closeSSEStream) {
+                console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);
+                extra.closeSSEStream();
+            }
+
+            // Continue processing while client is disconnected
+            // Events are stored in eventStore and will be replayed on reconnect
+            await sleep(500);
+            await server.sendLoggingMessage(
+                {
+                    level: 'info',
+                    data: 'Progress: 75% - Almost done (sent while client disconnected)...'
+                },
+                extra.sessionId
+            );
+
+            await sleep(500);
+            await server.sendLoggingMessage(
+                {
+                    level: 'info',
+                    data: 'Progress: 100% - Complete!'
+                },
+                extra.sessionId
+            );
+
+            console.log(`[${extra.sessionId}] Task complete`);
+
+            return {
+                content: [
+                    {
+                        type: 'text',
+                        text: 'Long task completed successfully!'
+                    }
+                ]
+            };
+        }
+    );
+
+    return server;
+};
 
 // Set up Express app
 const app = createMcpExpressApp();
@@ -131,7 +136,8 @@ app.all('/mcp', async (req: Request, res: Response) => {
             }
         });
 
-        // Connect the MCP server to the transport
+        // Create a new server per session and connect it to the transport
+        const server = getServer();
         await server.connect(transport);
     }
 
diff --git src/examples/server/standaloneSseWithGetStreamableHttp.ts src/examples/server/standaloneSseWithGetStreamableHttp.ts
index 225ef1f34..97882874d 100644
--- src/examples/server/standaloneSseWithGetStreamableHttp.ts
+++ src/examples/server/standaloneSseWithGetStreamableHttp.ts
@@ -5,35 +5,46 @@ import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
 import { isInitializeRequest, ReadResourceResult } from '../../types.js';
 import { createMcpExpressApp } from '../../server/express.js';
 
-// Create an MCP server with implementation details
-const server = new McpServer({
-    name: 'resource-list-changed-notification-server',
-    version: '1.0.0'
-});
+// Factory to create a new MCP server per session.
+// Each session needs its own server+transport pair to avoid cross-session contamination.
+const getServer = () => {
+    const server = new McpServer({
+        name: 'resource-list-changed-notification-server',
+        version: '1.0.0'
+    });
 
-// Store transports by session ID to send notifications
-const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
+    const addResource = (name: string, content: string) => {
+        const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;
+        server.registerResource(
+            name,
+            uri,
+            { mimeType: 'text/plain', description: `Dynamic resource: ${name}` },
+            async (): Promise<ReadResourceResult> => {
+                return {
+                    contents: [{ uri, text: content }]
+                };
+            }
+        );
+    };
 
-const addResource = (name: string, content: string) => {
-    const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;
-    server.registerResource(
-        name,
-        uri,
-        { mimeType: 'text/plain', description: `Dynamic resource: ${name}` },
-        async (): Promise<ReadResourceResult> => {
-            return {
-                contents: [{ uri, text: content }]
-            };
-        }
-    );
-};
+    addResource('example-resource', 'Initial content for example-resource');
 
-addResource('example-resource', 'Initial content for example-resource');
+    // Periodically add new resources to demonstrate notifications
+    const resourceChangeInterval = setInterval(() => {
+        const name = randomUUID();
+        addResource(name, `Content for ${name}`);
+    }, 5000);
 
-const resourceChangeInterval = setInterval(() => {
-    const name = randomUUID();
-    addResource(name, `Content for ${name}`);
-}, 5000); // Change resources every 5 seconds for testing
+    // Clean up the interval when the server closes
+    server.server.onclose = () => {
+        clearInterval(resourceChangeInterval);
+    };
+
+    return server;
+};
+
+// Store transports by session ID to send notifications
+const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
 
 const app = createMcpExpressApp();
 
@@ -59,7 +70,8 @@ app.post('/mcp', async (req: Request, res: Response) => {
                 }
             });
 
-            // Connect the transport to the MCP server
+            // Create a new server per session and connect it to the transport
+            const server = getServer();
             await server.connect(transport);
 
             // Handle the request - the onsessioninitialized callback will store the transport
@@ -121,7 +133,9 @@ app.listen(PORT, error => {
 // Handle server shutdown
 process.on('SIGINT', async () => {
     console.log('Shutting down server...');
-    clearInterval(resourceChangeInterval);
-    await server.close();
+    for (const sessionId in transports) {
+        await transports[sessionId].close();
+        delete transports[sessionId];
+    }
     process.exit(0);
 });
diff --git src/server/streamableHttp.ts src/server/streamableHttp.ts
index bc310d98e..83801fd2c 100644
--- src/server/streamableHttp.ts
+++ src/server/streamableHttp.ts
@@ -78,14 +78,19 @@ export class StreamableHTTPServerTransport implements Transport {
 
         // Create a request listener that wraps the web standard transport
         // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming
-        this._requestListener = getRequestListener(async (webRequest: Request) => {
-            // Get context if available (set during handleRequest)
-            const context = this._requestContext.get(webRequest);
-            return this._webStandardTransport.handleRequest(webRequest, {
-                authInfo: context?.authInfo,
-                parsedBody: context?.parsedBody
-            });
-        });
+        // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
+        // break frameworks like Next.js whose response classes extend the native Response
+        this._requestListener = getRequestListener(
+            async (webRequest: Request) => {
+                // Get context if available (set during handleRequest)
+                const context = this._requestContext.get(webRequest);
+                return this._webStandardTransport.handleRequest(webRequest, {
+                    authInfo: context?.authInfo,
+                    parsedBody: context?.parsedBody
+                });
+            },
+            { overrideGlobalObjects: false }
+        );
     }
 
     /**
@@ -166,12 +171,17 @@ export class StreamableHTTPServerTransport implements Transport {
         const authInfo = req.auth;
 
         // Create a custom handler that includes our context
-        const handler = getRequestListener(async (webRequest: Request) => {
-            return this._webStandardTransport.handleRequest(webRequest, {
-                authInfo,
-                parsedBody
-            });
-        });
+        // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
+        // break frameworks like Next.js whose response classes extend the native Response
+        const handler = getRequestListener(
+            async (webRequest: Request) => {
+                return this._webStandardTransport.handleRequest(webRequest, {
+                    authInfo,
+                    parsedBody
+                });
+            },
+            { overrideGlobalObjects: false }
+        );
 
         // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion
         // including proper SSE streaming support
diff --git src/server/webStandardStreamableHttp.ts src/server/webStandardStreamableHttp.ts
index 3ae9846c2..c811c54c4 100644
--- src/server/webStandardStreamableHttp.ts
+++ src/server/webStandardStreamableHttp.ts
@@ -210,6 +210,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
     // when sessionId is not set (undefined), it means the transport is in stateless mode
     private sessionIdGenerator: (() => string) | undefined;
     private _started: boolean = false;
+    private _hasHandledRequest: boolean = false;
     private _streamMapping: Map<string, StreamMapping> = new Map();
     private _requestToStreamMapping: Map<RequestId, string> = new Map();
     private _requestResponseMap: Map<RequestId, JSONRPCMessage> = new Map();
@@ -319,6 +320,13 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
      * Returns a Response object (Web Standard)
      */
     async handleRequest(req: Request, options?: HandleRequestOptions): Promise<Response> {
+        // In stateless mode (no sessionIdGenerator), each request must use a fresh transport.
+        // Reusing a stateless transport causes message ID collisions between clients.
+        if (!this.sessionIdGenerator && this._hasHandledRequest) {
+            throw new Error('Stateless transport cannot be reused across requests. Create a new transport per request.');
+        }
+        this._hasHandledRequest = true;
+
         // Validate request headers for DNS rebinding protection
         const validationError = this.validateRequestHeaders(req);
         if (validationError) {
diff --git src/shared/protocol.ts src/shared/protocol.ts
index aa242a647..5bb6d62ed 100644
--- src/shared/protocol.ts
+++ src/shared/protocol.ts
@@ -605,6 +605,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
      * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
      */
     async connect(transport: Transport): Promise<void> {
+        if (this._transport) {
+            throw new Error(
+                'Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.'
+            );
+        }
+
         this._transport = transport;
         const _onclose = this.transport?.onclose;
         this._transport.onclose = () => {
@@ -642,6 +648,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
         this._taskProgressTokens.clear();
         this._pendingDebouncedNotifications.clear();
 
+        // Abort all in-flight request handlers so they stop sending messages
+        for (const controller of this._requestHandlerAbortControllers.values()) {
+            controller.abort();
+        }
+        this._requestHandlerAbortControllers.clear();
+
         const error = McpError.fromError(ErrorCode.ConnectionClosed, 'Connection closed');
 
         this._transport = undefined;
@@ -719,6 +731,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
             sessionId: capturedTransport?.sessionId,
             _meta: request.params?._meta,
             sendNotification: async notification => {
+                if (abortController.signal.aborted) return;
                 // Include related-task metadata if this request is part of a task
                 const notificationOptions: NotificationOptions = { relatedRequestId: request.id };
                 if (relatedTaskId) {
@@ -727,6 +740,9 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
                 await this.notification(notification, notificationOptions);
             },
             sendRequest: async (r, resultSchema, options?) => {
+                if (abortController.signal.aborted) {
+                    throw new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled');
+                }
                 // Include related-task metadata if this request is part of a task
                 const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id };
                 if (relatedTaskId && !requestOptions.relatedTask) {
diff --git test/client/auth-extensions.test.ts test/client/auth-extensions.test.ts
index a7217307d..623d5e4da 100644
--- test/client/auth-extensions.test.ts
+++ test/client/auth-extensions.test.ts
@@ -49,6 +49,35 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
         expect(tokens?.access_token).toBe('test-access-token');
     });
 
+    it('sends scope in token request when ClientCredentialsProvider is configured with scope', async () => {
+        const provider = new ClientCredentialsProvider({
+            clientId: 'my-client',
+            clientSecret: 'my-secret',
+            clientName: 'test-client',
+            scope: 'read write'
+        });
+
+        expect(provider.clientMetadata.scope).toBe('read write');
+
+        const fetchMock = createMockOAuthFetch({
+            resourceServerUrl: RESOURCE_SERVER_URL,
+            authServerUrl: AUTH_SERVER_URL,
+            onTokenRequest: async (_url, init) => {
+                const params = init?.body as URLSearchParams;
+                expect(params).toBeInstanceOf(URLSearchParams);
+                expect(params.get('grant_type')).toBe('client_credentials');
+                expect(params.get('scope')).toBe('read write');
+            }
+        });
+
+        const result = await auth(provider, {
+            serverUrl: RESOURCE_SERVER_URL,
+            fetchFn: fetchMock
+        });
+
+        expect(result).toBe('AUTHORIZED');
+    });
+
     it('authenticates using PrivateKeyJwtProvider with private_key_jwt', async () => {
         const provider = new PrivateKeyJwtProvider({
             clientId: 'client-id',
@@ -92,6 +121,38 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
         expect(assertionFromRequest).toBeTruthy();
     });
 
+    it('sends scope in token request when PrivateKeyJwtProvider is configured with scope', async () => {
+        const provider = new PrivateKeyJwtProvider({
+            clientId: 'client-id',
+            privateKey: 'a-string-secret-at-least-256-bits-long',
+            algorithm: 'HS256',
+            clientName: 'private-key-jwt-client',
+            scope: 'openid profile'
+        });
+
+        expect(provider.clientMetadata.scope).toBe('openid profile');
+
+        const fetchMock = createMockOAuthFetch({
+            resourceServerUrl: RESOURCE_SERVER_URL,
+            authServerUrl: AUTH_SERVER_URL,
+            onTokenRequest: async (_url, init) => {
+                const params = init?.body as URLSearchParams;
+                expect(params).toBeInstanceOf(URLSearchParams);
+                expect(params.get('grant_type')).toBe('client_credentials');
+                expect(params.get('scope')).toBe('openid profile');
+                expect(params.get('client_assertion')).toBeTruthy();
+                expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
+            }
+        });
+
+        const result = await auth(provider, {
+            serverUrl: RESOURCE_SERVER_URL,
+            fetchFn: fetchMock
+        });
+
+        expect(result).toBe('AUTHORIZED');
+    });
+
     it('fails when PrivateKeyJwtProvider is configured with an unsupported algorithm', async () => {
         const provider = new PrivateKeyJwtProvider({
             clientId: 'client-id',
@@ -149,6 +210,39 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
         expect(tokens).toBeTruthy();
         expect(tokens?.access_token).toBe('test-access-token');
     });
+
+    it('sends scope in token request when StaticPrivateKeyJwtProvider is configured with scope', async () => {
+        const staticAssertion = 'header.payload.signature';
+
+        const provider = new StaticPrivateKeyJwtProvider({
+            clientId: 'static-client',
+            jwtBearerAssertion: staticAssertion,
+            clientName: 'static-private-key-jwt-client',
+            scope: 'api:read api:write'
+        });
+
+        expect(provider.clientMetadata.scope).toBe('api:read api:write');
+
+        const fetchMock = createMockOAuthFetch({
+            resourceServerUrl: RESOURCE_SERVER_URL,
+            authServerUrl: AUTH_SERVER_URL,
+            onTokenRequest: async (_url, init) => {
+                const params = init?.body as URLSearchParams;
+                expect(params).toBeInstanceOf(URLSearchParams);
+                expect(params.get('grant_type')).toBe('client_credentials');
+                expect(params.get('scope')).toBe('api:read api:write');
+                expect(params.get('client_assertion')).toBe(staticAssertion);
+                expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
+            }
+        });
+
+        const result = await auth(provider, {
+            serverUrl: RESOURCE_SERVER_URL,
+            fetchFn: fetchMock
+        });
+
+        expect(result).toBe('AUTHORIZED');
+    });
 });
 
 describe('createPrivateKeyJwtAuth', () => {
@@ -304,7 +398,7 @@ describe('createPrivateKeyJwtAuth', () => {
 
         const params = new URLSearchParams();
         await expect(addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)).rejects.toThrow(
-            /Invalid character/
+            /Invalid character|cannot be part of a valid base64/
         );
     });
 
diff --git test/client/index.test.ts test/client/index.test.ts
index 9735eb2ba..f5c6a348d 100644
--- test/client/index.test.ts
+++ test/client/index.test.ts
@@ -4137,3 +4137,129 @@ describe('getSupportedElicitationModes', () => {
         expect(result.supportsUrlMode).toBe(false);
     });
 });
+
+describe('Client sampling validation with tools', () => {
+    test('should validate array content with tool_use when request includes tools', async () => {
+        const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
+
+        const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
+
+        // Handler returns array content with tool_use - should validate with CreateMessageResultWithToolsSchema
+        client.setRequestHandler(CreateMessageRequestSchema, async () => ({
+            model: 'test-model',
+            role: 'assistant',
+            stopReason: 'toolUse',
+            content: [{ type: 'tool_use', id: 'call_1', name: 'test_tool', input: { arg: 'value' } }]
+        }));
+
+        const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+        await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
+
+        const result = await server.createMessage({
+            messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
+            maxTokens: 100,
+            tools: [{ name: 'test_tool', inputSchema: { type: 'object' } }]
+        });
+
+        expect(result.stopReason).toBe('toolUse');
+        expect(Array.isArray(result.content)).toBe(true);
+        expect((result.content as Array<{ type: string }>)[0].type).toBe('tool_use');
+    });
+
+    test('should validate single content when request includes tools', async () => {
+        const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
+
+        const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
+
+        // Handler returns single content (text) - should still validate with CreateMessageResultWithToolsSchema
+        client.setRequestHandler(CreateMessageRequestSchema, async () => ({
+            model: 'test-model',
+            role: 'assistant',
+            content: { type: 'text', text: 'No tool needed' }
+        }));
+
+        const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+        await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
+
+        const result = await server.createMessage({
+            messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
+            maxTokens: 100,
+            tools: [{ name: 'test_tool', inputSchema: { type: 'object' } }]
+        });
+
+        expect((result.content as { type: string }).type).toBe('text');
+    });
+
+    test('should validate single content when request has no tools', async () => {
+        const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
+
+        const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
+
+        // Handler returns single content - should validate with CreateMessageResultSchema
+        client.setRequestHandler(CreateMessageRequestSchema, async () => ({
+            model: 'test-model',
+            role: 'assistant',
+            content: { type: 'text', text: 'Response' }
+        }));
+
+        const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+        await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
+
+        const result = await server.createMessage({
+            messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
+            maxTokens: 100
+        });
+
+        expect((result.content as { type: string }).type).toBe('text');
+    });
+
+    test('should reject array content when request has no tools', async () => {
+        const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
+
+        const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: {} } });
+
+        // Handler returns array content - should fail validation with CreateMessageResultSchema
+        client.setRequestHandler(CreateMessageRequestSchema, async () => ({
+            model: 'test-model',
+            role: 'assistant',
+            content: [{ type: 'text', text: 'Array response' }]
+        }));
+
+        const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+        await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
+
+        await expect(
+            server.createMessage({
+                messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
+                maxTokens: 100
+            })
+        ).rejects.toThrow('Invalid sampling result');
+    });
+
+    test('should validate array content when request includes toolChoice', async () => {
+        const server = new Server({ name: 'test server', version: '1.0' }, { capabilities: {} });
+
+        const client = new Client({ name: 'test client', version: '1.0' }, { capabilities: { sampling: { tools: {} } } });
+
+        // Handler returns array content with tool_use
+        client.setRequestHandler(CreateMessageRequestSchema, async () => ({
+            model: 'test-model',
+            role: 'assistant',
+            stopReason: 'toolUse',
+            content: [{ type: 'tool_use', id: 'call_1', name: 'test_tool', input: {} }]
+        }));
+
+        const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+        await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
+
+        const result = await server.createMessage({
+            messages: [{ role: 'user', content: { type: 'text', text: 'hello' } }],
+            maxTokens: 100,
+            tools: [{ name: 'test_tool', inputSchema: { type: 'object' } }],
+            toolChoice: { mode: 'auto' }
+        });
+
+        expect(result.stopReason).toBe('toolUse');
+        expect(Array.isArray(result.content)).toBe(true);
+    });
+});
diff --git test/integration-tests/stateManagementStreamableHttp.test.ts test/integration-tests/stateManagementStreamableHttp.test.ts
index d79d95c75..672bfb92f 100644
--- test/integration-tests/stateManagementStreamableHttp.test.ts
+++ test/integration-tests/stateManagementStreamableHttp.test.ts
@@ -17,9 +17,11 @@ import { listenOnRandomPort } from '../helpers/http.js';
 describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
     const { z } = entry;
     describe('Streamable HTTP Transport Session Management', () => {
-        // Function to set up the server with optional session management
-        async function setupServer(withSessionManagement: boolean) {
-            const server: Server = createServer();
+        /**
+         * Helper to create and configure a fresh McpServer instance with standard
+         * resources, prompts, and tools for testing.
+         */
+        function createMcpServer(): McpServer {
             const mcpServer = new McpServer(
                 { name: 'test-server', version: '1.0.0' },
                 {
@@ -67,43 +69,67 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
                 }
             );
 
-            // Create transport with or without session management
-            const serverTransport = new StreamableHTTPServerTransport({
-                sessionIdGenerator: withSessionManagement
-                    ? () => randomUUID() // With session management, generate UUID
-                    : undefined // Without session management, return undefined
-            });
+            return mcpServer;
+        }
 
-            await mcpServer.connect(serverTransport);
+        // Function to set up the server with optional session management
+        async function setupServer(withSessionManagement: boolean): Promise<{
+            server: Server;
+            mcpServer?: McpServer;
+            serverTransport?: StreamableHTTPServerTransport;
+            baseUrl: URL;
+        }> {
+            const server: Server = createServer();
 
-            server.on('request', async (req, res) => {
-                await serverTransport.handleRequest(req, res);
-            });
+            if (withSessionManagement) {
+                // Stateful mode: single transport + server for the session
+                const mcpServer = createMcpServer();
+                const serverTransport = new StreamableHTTPServerTransport({
+                    sessionIdGenerator: () => randomUUID()
+                });
+
+                await mcpServer.connect(serverTransport);
 
-            // Start the server on a random port
-            const baseUrl = await listenOnRandomPort(server);
+                server.on('request', async (req, res) => {
+                    await serverTransport.handleRequest(req, res);
+                });
+
+                // Start the server on a random port
+                const baseUrl = await listenOnRandomPort(server);
+
+                return { server, mcpServer, serverTransport, baseUrl };
+            } else {
+                // Stateless mode: create a fresh transport + server per request
+                // to comply with the guard that stateless transports cannot be reused.
+                server.on('request', async (req, res) => {
+                    const mcpServer = createMcpServer();
+                    const serverTransport = new StreamableHTTPServerTransport({
+                        sessionIdGenerator: undefined
+                    });
+                    await mcpServer.connect(serverTransport);
+                    await serverTransport.handleRequest(req, res);
+                    // Close the per-request mcpServer after handling to avoid leaks
+                    await mcpServer.close();
+                });
 
-            return { server, mcpServer, serverTransport, baseUrl };
+                // Start the server on a random port
+                const baseUrl = await listenOnRandomPort(server);
+
+                return { server, baseUrl };
+            }
         }
 
         describe('Stateless Mode', () => {
             let server: Server;
-            let mcpServer: McpServer;
-            let serverTransport: StreamableHTTPServerTransport;
             let baseUrl: URL;
 
             beforeEach(async () => {
                 const setup = await setupServer(false);
                 server = setup.server;
-                mcpServer = setup.mcpServer;
-                serverTransport = setup.serverTransport;
                 baseUrl = setup.baseUrl;
             });
 
             afterEach(async () => {
-                // Clean up resources
-                await mcpServer.close().catch(() => {});
-                await serverTransport.close().catch(() => {});
                 server.close();
             });
 
@@ -259,8 +285,8 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
             beforeEach(async () => {
                 const setup = await setupServer(true);
                 server = setup.server;
-                mcpServer = setup.mcpServer;
-                serverTransport = setup.serverTransport;
+                mcpServer = setup.mcpServer!;
+                serverTransport = setup.serverTransport!;
                 baseUrl = setup.baseUrl;
             });
 
diff --git test/server/streamableHttp.test.ts test/server/streamableHttp.test.ts
index 36a12ca9c..1c4d5ed84 100644
--- test/server/streamableHttp.test.ts
+++ test/server/streamableHttp.test.ts
@@ -1529,20 +1529,56 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
     // Test stateless mode
     describe('StreamableHTTPServerTransport in stateless mode', () => {
         let server: Server;
-        let transport: StreamableHTTPServerTransport;
         let baseUrl: URL;
 
+        // In stateless mode, each request must use a fresh transport + server pair.
+        // The HTTP server creates these per-request and delegates accordingly.
         beforeEach(async () => {
-            const result = await createTestServer({ sessionIdGenerator: undefined });
-            server = result.server;
-            transport = result.transport;
-            baseUrl = result.baseUrl;
+            server = createServer(async (req, res) => {
+                try {
+                    const { transport, mcpServer } = await createStatelessHandler();
+                    await transport.handleRequest(req, res);
+                    // Close the per-request mcpServer after handling to avoid leaks
+                    await mcpServer.close();
+                } catch (error) {
+                    console.error('Error handling request:', error);
+                    if (!res.headersSent) res.writeHead(500).end();
+                }
+            });
+            baseUrl = await listenOnRandomPort(server);
         });
 
         afterEach(async () => {
-            await stopTestServer({ server, transport });
+            server.close();
         });
 
+        /**
+         * Creates a fresh transport + mcpServer pair for a single stateless request.
+         */
+        async function createStatelessHandler(): Promise<{
+            transport: StreamableHTTPServerTransport;
+            mcpServer: McpServer;
+        }> {
+            const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } });
+
+            mcpServer.tool(
+                'greet',
+                'A simple greeting tool',
+                { name: z.string().describe('Name to greet') },
+                async ({ name }): Promise<CallToolResult> => {
+                    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
+                }
+            );
+
+            const transport = new StreamableHTTPServerTransport({
+                sessionIdGenerator: undefined
+            });
+
+            await mcpServer.connect(transport);
+
+            return { transport, mcpServer };
+        }
+
         it('should operate without session ID validation', async () => {
             // Initialize the server first
             const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
@@ -1552,6 +1588,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
             expect(initResponse.headers.get('mcp-session-id')).toBeNull();
 
             // Try request without session ID - should work in stateless mode
+            // (a fresh transport is created per request)
             const toolsResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList);
 
             expect(toolsResponse.status).toBe(200);
@@ -1585,14 +1622,14 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
             expect(response2.status).toBe(200);
         });
 
-        it('should reject second SSE stream even in stateless mode', async () => {
-            // Despite no session ID requirement, the transport still only allows
-            // one standalone SSE stream at a time
+        it('should allow multiple SSE streams in stateless mode with per-request transports', async () => {
+            // Each request gets its own transport, so multiple SSE streams can
+            // coexist since they are handled by separate transport instances
 
             // Initialize the server first
             await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
 
-            // Open first SSE stream
+            // Open first SSE stream - this uses its own per-request transport
             const stream1 = await fetch(baseUrl, {
                 method: 'GET',
                 headers: {
@@ -1602,7 +1639,8 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
             });
             expect(stream1.status).toBe(200);
 
-            // Open second SSE stream - should still be rejected, stateless mode still only allows one
+            // Open second SSE stream - also gets its own per-request transport,
+            // so it should also succeed (each transport only handles one request)
             const stream2 = await fetch(baseUrl, {
                 method: 'GET',
                 headers: {
@@ -1610,7 +1648,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
                     'mcp-protocol-version': '2025-11-25'
                 }
             });
-            expect(stream2.status).toBe(409); // Conflict - only one stream allowed
+            // With per-request transports in stateless mode, each GET gets its own
+            // transport, so the second one also succeeds
+            expect(stream2.status).toBe(200);
         });
     });
 
@@ -2868,17 +2908,20 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
 
         describe('Combined validations', () => {
             it('should validate both host and origin when both are configured', async () => {
-                const result = await createTestServerWithDnsProtection({
+                // In stateless mode, each request needs a fresh transport, so we
+                // test invalid and valid origins with separate server instances.
+
+                // Test with invalid origin
+                const result1 = await createTestServerWithDnsProtection({
                     sessionIdGenerator: undefined,
                     allowedHosts: ['localhost'],
                     allowedOrigins: ['http://localhost:3001'],
                     enableDnsRebindingProtection: true
                 });
-                server = result.server;
-                transport = result.transport;
-                baseUrl = result.baseUrl;
+                server = result1.server;
+                transport = result1.transport;
+                baseUrl = result1.baseUrl;
 
-                // Test with invalid origin (host will be automatically correct via fetch)
                 const response1 = await fetch(baseUrl, {
                     method: 'POST',
                     headers: {
@@ -2893,7 +2936,20 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
                 const body1 = await response1.json();
                 expect(body1.error.message).toBe('Invalid Origin header: http://evil.com');
 
-                // Test with valid origin
+                // Clean up first server
+                await stopTestServer({ server, transport });
+
+                // Test with valid origin using a fresh server+transport
+                const result2 = await createTestServerWithDnsProtection({
+                    sessionIdGenerator: undefined,
+                    allowedHosts: ['localhost'],
+                    allowedOrigins: ['http://localhost:3001'],
+                    enableDnsRebindingProtection: true
+                });
+                server = result2.server;
+                transport = result2.transport;
+                baseUrl = result2.baseUrl;
+
                 const response2 = await fetch(baseUrl, {
                     method: 'POST',
                     headers: {
@@ -2910,6 +2966,89 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
     });
 });
 
+describe('StreamableHTTPServerTransport global Response preservation', () => {
+    it('should not override the global Response object', () => {
+        // Store reference to the original global Response constructor
+        const OriginalResponse = globalThis.Response;
+
+        // Create a custom class that extends Response (similar to Next.js's NextResponse)
+        class CustomResponse extends Response {
+            customProperty = 'test';
+        }
+
+        // Verify instanceof works before creating transport
+        const customResponseBefore = new CustomResponse('test body');
+        expect(customResponseBefore instanceof Response).toBe(true);
+        expect(customResponseBefore instanceof OriginalResponse).toBe(true);
+
+        // Create the transport - this should NOT override globalThis.Response
+        const transport = new StreamableHTTPServerTransport({
+            sessionIdGenerator: () => randomUUID()
+        });
+
+        // Verify the global Response is still the original
+        expect(globalThis.Response).toBe(OriginalResponse);
+
+        // Verify instanceof still works after creating transport
+        const customResponseAfter = new CustomResponse('test body');
+        expect(customResponseAfter instanceof Response).toBe(true);
+        expect(customResponseAfter instanceof OriginalResponse).toBe(true);
+
+        // Verify that instances created before transport initialization still work
+        expect(customResponseBefore instanceof Response).toBe(true);
+
+        // Clean up
+        transport.close();
+    });
+
+    it('should not override the global Response object when calling handleRequest', async () => {
+        // Store reference to the original global Response constructor
+        const OriginalResponse = globalThis.Response;
+
+        // Create a custom class that extends Response
+        class CustomResponse extends Response {
+            customProperty = 'test';
+        }
+
+        const transport = new StreamableHTTPServerTransport({
+            sessionIdGenerator: () => randomUUID()
+        });
+
+        // Create a mock server to test handleRequest
+        const port = await getFreePort();
+        const httpServer = createServer(async (req, res) => {
+            await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res);
+        });
+
+        await new Promise<void>(resolve => {
+            httpServer.listen(port, () => resolve());
+        });
+
+        try {
+            // Make a request to trigger handleRequest
+            await fetch(`http://localhost:${port}`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                    Accept: 'application/json, text/event-stream'
+                },
+                body: JSON.stringify(TEST_MESSAGES.initialize)
+            });
+
+            // Verify the global Response is still the original after handleRequest
+            expect(globalThis.Response).toBe(OriginalResponse);
+
+            // Verify instanceof still works
+            const customResponse = new CustomResponse('test body');
+            expect(customResponse instanceof Response).toBe(true);
+            expect(customResponse instanceof OriginalResponse).toBe(true);
+        } finally {
+            await transport.close();
+            httpServer.close();
+        }
+    });
+});
+
 /**
  * Helper to create test server with DNS rebinding protection options
  */
diff --git test/shared/protocol-transport-handling.test.ts test/shared/protocol-transport-handling.test.ts
index 60eff5c2e..1e698481f 100644
--- test/shared/protocol-transport-handling.test.ts
+++ test/shared/protocol-transport-handling.test.ts
@@ -27,29 +27,39 @@ class MockTransport implements Transport {
     }
 }
 
-describe('Protocol transport handling bug', () => {
-    let protocol: Protocol<Request, Notification, Result>;
+function createProtocol(): Protocol<Request, Notification, Result> {
+    return new (class extends Protocol<Request, Notification, Result> {
+        protected assertCapabilityForMethod(): void {}
+        protected assertNotificationCapability(): void {}
+        protected assertRequestHandlerCapability(): void {}
+        protected assertTaskCapability(): void {}
+        protected assertTaskHandlerCapability(): void {}
+    })();
+}
+
+describe('Protocol transport handling', () => {
     let transportA: MockTransport;
     let transportB: MockTransport;
 
     beforeEach(() => {
-        protocol = new (class extends Protocol<Request, Notification, Result> {
-            protected assertCapabilityForMethod(): void {}
-            protected assertNotificationCapability(): void {}
-            protected assertRequestHandlerCapability(): void {}
-            protected assertTaskCapability(): void {}
-            protected assertTaskHandlerCapability(): void {}
-        })();
-
         transportA = new MockTransport('A');
         transportB = new MockTransport('B');
     });
 
-    test('should send response to the correct transport when multiple clients are connected', async () => {
-        // Set up a request handler that simulates processing time
-        let resolveHandler: (value: Result) => void;
-        const handlerPromise = new Promise<Result>(resolve => {
-            resolveHandler = resolve;
+    test('should send response to the correct transport when using separate protocol instances', async () => {
+        const protocolA = createProtocol();
+        const protocolB = createProtocol();
+
+        // Each protocol gets its own resolver so we can verify responses route correctly
+        let resolveA: (value: Result) => void;
+        let resolveB: (value: Result) => void;
+        let handlerAEnteredResolve: () => void;
+        let handlerBEnteredResolve: () => void;
+        const handlerAEntered = new Promise<void>(resolve => {
+            handlerAEnteredResolve = resolve;
+        });
+        const handlerBEntered = new Promise<void>(resolve => {
+            handlerBEnteredResolve = resolve;
         });
 
         const TestRequestSchema = z.object({
@@ -61,13 +71,22 @@ describe('Protocol transport handling bug', () => {
                 .optional()
         });
 
-        protocol.setRequestHandler(TestRequestSchema, async request => {
-            console.log(`Processing request from ${request.params?.from}`);
-            return handlerPromise;
+        protocolA.setRequestHandler(TestRequestSchema, async () => {
+            return new Promise<Result>(resolve => {
+                resolveA = resolve;
+                handlerAEnteredResolve();
+            });
+        });
+
+        protocolB.setRequestHandler(TestRequestSchema, async () => {
+            return new Promise<Result>(resolve => {
+                resolveB = resolve;
+                handlerBEnteredResolve();
+            });
         });
 
         // Client A connects and sends a request
-        await protocol.connect(transportA);
+        await protocolA.connect(transportA);
 
         const requestFromA = {
             jsonrpc: '2.0' as const,
@@ -79,9 +98,8 @@ describe('Protocol transport handling bug', () => {
         // Simulate client A sending a request
         transportA.onmessage?.(requestFromA);
 
-        // While A's request is being processed, client B connects
-        // This overwrites the transport reference in the protocol
-        await protocol.connect(transportB);
+        // Client B connects to a separate protocol instance
+        await protocolB.connect(transportB);
 
         const requestFromB = {
             jsonrpc: '2.0' as const,
@@ -93,19 +111,18 @@ describe('Protocol transport handling bug', () => {
         // Client B sends its own request
         transportB.onmessage?.(requestFromB);
 
-        // Now complete A's request
-        resolveHandler!({ data: 'responseForA' } as Result);
+        // Wait for both handlers to be invoked so resolvers are captured
+        await handlerAEntered;
+        await handlerBEntered;
 
-        // Wait for async operations to complete
-        await new Promise(resolve => setTimeout(resolve, 10));
+        // Resolve each handler with distinct data
+        resolveA!({ data: 'responseForA' } as Result);
+        resolveB!({ data: 'responseForB' } as Result);
 
-        // Check where the responses went
-        console.log('Transport A received:', transportA.sentMessages);
-        console.log('Transport B received:', transportB.sentMessages);
-
-        // FIXED: Each transport now receives its own response
+        // Wait for response delivery (transport.send is async)
+        await new Promise(resolve => setTimeout(resolve, 10));
 
-        // Transport A should receive response for request ID 1
+        // Each transport receives its own response
         expect(transportA.sentMessages.length).toBe(1);
         expect(transportA.sentMessages[0]).toMatchObject({
             jsonrpc: '2.0',
@@ -113,18 +130,17 @@ describe('Protocol transport handling bug', () => {
             result: { data: 'responseForA' }
         });
 
-        // Transport B should only receive its own response (when implemented)
         expect(transportB.sentMessages.length).toBe(1);
         expect(transportB.sentMessages[0]).toMatchObject({
             jsonrpc: '2.0',
             id: 2,
-            result: { data: 'responseForA' } // Same handler result in this test
+            result: { data: 'responseForB' }
         });
     });
 
-    test('demonstrates the timing issue with multiple rapid connections', async () => {
-        const delays: number[] = [];
-        const results: { transport: string; response: JSONRPCMessage[] }[] = [];
+    test('demonstrates isolation with separate protocol instances for rapid connections', async () => {
+        const protocolA = createProtocol();
+        const protocolB = createProtocol();
 
         const DelayedRequestSchema = z.object({
             method: z.literal('test/delayed'),
@@ -136,21 +152,20 @@ describe('Protocol transport handling bug', () => {
                 .optional()
         });
 
-        // Set up handler with variable delay
-        protocol.setRequestHandler(DelayedRequestSchema, async (request, extra) => {
-            const delay = request.params?.delay || 0;
-            delays.push(delay);
-
-            await new Promise(resolve => setTimeout(resolve, delay));
-
-            return {
-                processedBy: `handler-${extra.requestId}`,
-                delay: delay
-            } as Result;
-        });
+        // Set up handler with variable delay on each protocol
+        for (const protocol of [protocolA, protocolB]) {
+            protocol.setRequestHandler(DelayedRequestSchema, async (request, extra) => {
+                const delay = request.params?.delay || 0;
+                await new Promise(resolve => setTimeout(resolve, delay));
+                return {
+                    processedBy: `handler-${extra.requestId}`,
+                    delay: delay
+                } as Result;
+            });
+        }
 
-        // Rapid succession of connections and requests
-        await protocol.connect(transportA);
+        // Connect and send requests
+        await protocolA.connect(transportA);
         transportA.onmessage?.({
             jsonrpc: '2.0' as const,
             method: 'test/delayed',
@@ -160,7 +175,7 @@ describe('Protocol transport handling bug', () => {
 
         // Connect B while A is processing
         setTimeout(async () => {
-            await protocol.connect(transportB);
+            await protocolB.connect(transportB);
             transportB.onmessage?.({
                 jsonrpc: '2.0' as const,
                 method: 'test/delayed',
@@ -172,18 +187,81 @@ describe('Protocol transport handling bug', () => {
         // Wait for all processing
         await new Promise(resolve => setTimeout(resolve, 100));
 
-        // Collect results
-        if (transportA.sentMessages.length > 0) {
-            results.push({ transport: 'A', response: transportA.sentMessages });
-        }
-        if (transportB.sentMessages.length > 0) {
-            results.push({ transport: 'B', response: transportB.sentMessages });
-        }
-
-        console.log('Timing test results:', results);
-
-        // FIXED: Each transport receives its own responses
+        // Each transport receives its own responses
         expect(transportA.sentMessages.length).toBe(1);
         expect(transportB.sentMessages.length).toBe(1);
     });
+
+    test('connect guard throws when calling connect() twice without closing', async () => {
+        const protocol = createProtocol();
+
+        await protocol.connect(transportA);
+
+        await expect(protocol.connect(transportB)).rejects.toThrow(
+            'Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.'
+        );
+    });
+
+    test('connect succeeds after calling close() first', async () => {
+        const protocol = createProtocol();
+
+        await protocol.connect(transportA);
+        await protocol.close();
+
+        // Should succeed without error
+        await expect(protocol.connect(transportB)).resolves.toBeUndefined();
+    });
+
+    test('close() aborts in-flight request handlers', async () => {
+        const protocol = createProtocol();
+
+        const SlowRequestSchema = z.object({
+            method: z.literal('test/slow')
+        });
+
+        let capturedSignal: AbortSignal | undefined;
+        let capturedSendNotification: ((notification: Notification) => Promise<void>) | undefined;
+        let resolveHandler: () => void;
+        const handlerBlocking = new Promise<void>(resolve => {
+            resolveHandler = resolve;
+        });
+
+        protocol.setRequestHandler(SlowRequestSchema, async (_request, extra) => {
+            capturedSignal = extra.signal;
+            capturedSendNotification = extra.sendNotification;
+            // Block the handler until we release it
+            await handlerBlocking;
+            return {} as Result;
+        });
+
+        await protocol.connect(transportA);
+
+        // Send a request to trigger the handler
+        transportA.onmessage?.({
+            jsonrpc: '2.0' as const,
+            method: 'test/slow',
+            id: 1
+        });
+
+        // Wait for the handler to start and capture the signal
+        await new Promise(resolve => setTimeout(resolve, 10));
+        expect(capturedSignal).toBeDefined();
+        expect(capturedSignal!.aborted).toBe(false);
+
+        // Close the protocol while the handler is still in-flight
+        await protocol.close();
+
+        // The signal should now be aborted
+        expect(capturedSignal!.aborted).toBe(true);
+
+        // sendNotification should be a no-op after close (no error thrown)
+        await expect(capturedSendNotification!({ method: 'notifications/test' } as Notification)).resolves.toBeUndefined();
+
+        // No notification should have been sent to the transport
+        const notifications = transportA.sentMessages.filter((m: JSONRPCMessage) => 'method' in m && m.method === 'notifications/test');
+        expect(notifications).toHaveLength(0);
+
+        // Release the handler so the promise chain completes
+        resolveHandler!();
+    });
 });

Description

This pull request introduces several enhancements and fixes to the TypeScript SDK of the Model Context Protocol. Key changes include dependency updates, new scope options in OAuth providers, refactoring of server setup in examples, and improvements in protocol handling and testing.

Possible Issues

  1. Backward Compatibility: Updating dependencies like express, express-rate-limit, and others can introduce breaking changes if not thoroughly tested.
  2. Performance: The changes introduce a new pattern of creating a fresh server instance per request/session, which might affect performance under heavy load.
  3. Repeated Code Blocks: The new pattern of creating a fresh server instance is duplicated across multiple files; this approach might lead to maintenance challenges.
  4. Testing: The tests for new functionalities (scope handling, stateless mode) seem comprehensive, but edge cases in production should be monitored.
  5. Error Handling: While the error handling for abort signals within protocol methods is improved, the consistency and completeness of error messages across the entire codebase should be reviewed.

Security Hotspots

  1. Dependency Updates: Ensure all new dependencies and updates (e.g., express, jose, etc.) are checked for security vulnerabilities.
  2. OAuth Scopes: Make sure that the scopes provided in OAuth configurations do not lead to over-permissioning.
  3. Dynamic Server Creation: Creating new server instances per request could expose new attack vectors if not managed properly.

Privacy Hotspots

  1. OAuth Scopes: The addition of scopes to client credentials could raise privacy concerns. Ensure scopes are defined according to the principle of least privilege.
Changes

Changes

  • package.json: Updated versions for dependencies including express, express-rate-limit, jose, hono, and others.
  • src/client/auth-extensions.ts: Added scope attribute in OAuth providers options.
  • src/client/index.ts: Enhanced schema validation logic to account for tools presence.
  • src/examples/server/elicitationFormExample.ts: Refactored to create new server instances per session.
  • src/examples/server/honoWebStandardStreamableHttp.ts: Refactored to use fresh server instances per request.
  • src/examples/server/ssePollingExample.ts: Refactored for session-based server creation.
  • src/examples/server/standaloneSseWithGetStreamableHttp.ts: Updated to support per-session server creation.
  • src/server/streamableHttp.ts: Enhanced request listener logic, adding overrideGlobalObjects: false.
  • src/server/webStandardStreamableHttp.ts: Added guard to prevent reuse of stateless transport, added _hasHandledRequest flag.
  • src/shared/protocol.ts: Improved error handling and transport connection logic.
  • test/client/auth-extensions.test.ts: Added tests for OAuth providers with scope.
  • test/client/index.test.ts: Added tests for client sampling validation with tools.
  • test/integration-tests/stateManagementStreamableHttp.test.ts: Updated tests for stateful and stateless modes.
  • test/server/streamableHttp.test.ts: Updated tests to handle multiple SSE streams in stateless mode.
  • test/shared/protocol-transport-handling.test.ts: Added tests for protocol transport handling, including connect guard and abort signal.
sequenceDiagram
    participant User
    participant Server
    participant AuthServer
    participant ResourceServer

    User->>Server: Send OAuth Request with scopes
    Server->>AuthServer: Validate client credentials with scopes
    AuthServer->>Server: Validated response with access token
    Server->>User: Return access token
    User->>Server: Make API call with token
    Server->>ResourceServer: Validate token and provide resource
    ResourceServer->>Server: Resource response
    Server->>User: Return resource
Loading

@mihaiplesa mihaiplesa merged commit 648cc7a into main Feb 4, 2026
4 checks passed
@mihaiplesa mihaiplesa deleted the dependabot/npm_and_yarn/modelcontextprotocol/sdk-1.26.0 branch February 4, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code P2 puLL-Merge security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant